@atscript/ui
Framework-agnostic core for type-driven UIs. Reads compiled Atscript metadata, builds form and table definitions, exposes a pluggable field resolver, validators, value-help, and a battery of pure utilities consumed by every Vue package. No Vue, no React — plain TypeScript.
Contents
- Plugin
- Form types
- Form factories
- Table types
- Table factory
- Field resolver
- Annotation key constants
- Validators
- Path utilities
- Value-help
- Grid layout
- Decimal helpers
- Column helpers
- Error map utilities
- Type guards
- Misc utilities
- Client factory
- Meta cache
Plugin
@atscript/ui/plugin exposes the build-time plugin that registers every static @ui.* annotation key. Wire it in atscript.config.ts:
import uiPlugin from "@atscript/ui/plugin";
export default {
plugins: [uiPlugin()],
};For dynamic @ui.fn.* and @ui.form.validate, also register @atscript/ui-fns/plugin.
Form types
FormDef
Complete form definition produced by createFormDef(type).
interface FormDef {
type: TAtscriptAnnotatedType;
rootField: FormFieldDef;
fields: FormFieldDef[];
flatMap: Map<string, TAtscriptAnnotatedType>;
}FormFieldDef
Thin pointer to one atscript prop. Metadata lives on prop.metadata and is resolved on demand.
interface FormFieldDef {
/** Dot-separated path relative to the parent data container. `""` = root. */
path: string;
prop: TAtscriptAnnotatedType;
/** Render-kind: structural names (`array`, `object`, `union`, `tuple`) or primitive override from `@ui.form.type`. */
type: string;
/**
* Optional `@ui.form.type` / `@ui.type` override that lets a structured kind
* (`array`, `object`, `union`, `tuple`) dispatch to a different built-in
* renderer in the `:types` map. Reserved for built-in ids — custom
* components are wired via `@ui.form.component` + `:components`.
*/
customType?: string;
phantom: boolean;
name: string;
/** True when no `ui.fn.*` keys exist on the prop — perf flag. */
allStatic: boolean;
}FormArrayFieldDef, FormObjectFieldDef, FormUnionFieldDef, FormTupleFieldDef
Extended field defs for structural kinds.
interface FormArrayFieldDef extends FormFieldDef {
itemType: TAtscriptAnnotatedType;
itemField: FormFieldDef;
}
interface FormObjectFieldDef extends FormFieldDef {
objectDef: FormDef;
}
interface FormUnionFieldDef extends FormFieldDef {
unionVariants: FormUnionVariant[];
}
interface FormTupleFieldDef extends FormFieldDef {
itemFields: FormFieldDef[];
}FormUnionVariant
One branch of a union.
interface FormUnionVariant {
label: string;
type: TAtscriptAnnotatedType;
/** Pre-built FormDef for object variants. */
def?: FormDef;
/** Pre-built field def for primitive variants. */
itemField?: FormFieldDef;
/** "string" | "number" | "boolean" for primitive variants. */
designType?: string;
}TFormAction
interface TFormAction {
id: string;
label: string;
}TFormEntryOptions
A select/radio option — plain string or { key, label } pair.
type TFormEntryOptions = { key: string; label: string } | string;Form factories
createFormDef(type)
Builds a FormDef from an annotated type. Walks props, pre-resolves structural sub-defs, and caches the flat map.
function createFormDef(type: TAtscriptAnnotatedType): FormDef;buildUnionVariants(type)
Returns the variant list for a union prop — used internally by createFormDef for union fields and union array items.
function buildUnionVariants(type: TAtscriptAnnotatedType): FormUnionVariant[];createFormData(type, resolver?)
Creates the wrapped data container { value: domainData } populated from atscript defaults and @meta.default. The optional resolver (TFormValueResolver) overrides the per-field defaulting strategy.
function createFormData<T extends TAtscriptAnnotatedType>(
type: T,
resolver?: TFormValueResolver,
): { value: TAtscriptDataType<T> };createFormValueResolver(data?, context?)
Returns a value resolver that reads @meta.default or @ui.form.fn.value, given a scope.
type TFormValueResolver = (prop: TAtscriptAnnotatedType, path: string) => unknown;
function createFormValueResolver(
data?: Record<string, unknown>,
context?: Record<string, unknown>,
): TFormValueResolver;Table types
TableDef
Complete table definition built by createTableDef(meta, type).
interface TableDef {
type: TAtscriptAnnotatedType;
columns: ColumnDef[];
flatMap: Map<string, TAtscriptAnnotatedType>;
primaryKeys: string[];
/** Preferred row identifier for URL/wire addressing. */
preferredId: string[];
crud: TCrudPermissions;
canRemove: boolean;
actions: TableActionsModel;
searchable: boolean;
vectorSearchable: boolean;
searchIndexes: SearchIndexInfo[];
relations: RelationInfo[];
}ColumnDef
A single column definition built from field metadata + annotations.
interface ColumnDef {
path: string;
label: string;
type: string;
component?: string;
sortable: boolean;
filterable: boolean;
nullable: boolean;
visible: boolean;
width?: string;
maxLen?: number;
order: number;
options?: { key: string; label: string }[];
valueHelpInfo?: ValueHelpInfo;
currencyCode?: string;
currencyRefField?: string;
unitCode?: string;
unitRefField?: string;
precisionScale?: number;
/** Synthesised columns (e.g. row-actions) — locked chrome, excluded from `columnNames`. */
fixed?: boolean;
}MetaResponse
The payload returned by the moost-db /meta endpoint.
interface MetaResponse {
searchable: boolean;
vectorSearchable: boolean;
searchIndexes: SearchIndexInfo[];
primaryKeys: string[];
preferredId: string[];
crud: TCrudPermissions;
actions: TDbActionInfo[];
relations: RelationInfo[];
fields: Record<string, FieldMeta>;
type: TSerializedAnnotatedType;
}
interface FieldMeta {
sortable: boolean;
filterable: boolean;
}
interface SearchIndexInfo {
name: string;
description?: string;
type?: "text" | "vector";
}
interface RelationInfo {
name: string;
direction: "to" | "from" | "via";
isArray: boolean;
}TableActionsModel
Server-declared actions grouped by level.
interface TableActionsModel {
table: TDbActionInfo[];
row: TDbActionInfo[];
rows: TDbActionInfo[];
default: {
table?: TDbActionInfo;
row?: TDbActionInfo;
rows?: TDbActionInfo;
};
}TableQueryState, SortControl, PaginationControl
interface SortControl {
field: string;
direction: "asc" | "desc";
}
interface PaginationControl {
page: number;
itemsPerPage: number;
}
interface TableQueryState {
sort?: SortControl[];
pagination?: PaginationControl;
search?: string;
filters?: Record<string, unknown>;
}Table factory
createTableDef(meta, type)
Builds a TableDef from a MetaResponse plus the deserialized atscript type.
function createTableDef(meta: MetaResponse, type: TAtscriptAnnotatedType): TableDef;See Annotations Reference for how @ui.table.* and @db.* annotations populate ColumnDef.
Field resolver
The resolver is pluggable so static-only consumers ship with zero new Function overhead and dynamic consumers (@atscript/ui-fns) opt in by replacing the global instance.
FieldResolver
interface FieldResolver {
resolveFieldProp<T>(
prop: TAtscriptAnnotatedType,
fnKey: string,
staticKey: string | undefined,
scope: Record<string, unknown>,
opts?: TResolveOptions<T>,
): T | undefined;
resolveFormProp<T>(
type: TAtscriptAnnotatedType,
fnKey: string,
staticKey: string | undefined,
scope: Record<string, unknown>,
opts?: TResolveOptions<T>,
): T | undefined;
hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
}
interface TResolveOptions<T> {
staticAsBoolean?: boolean;
transform?: (raw: unknown) => T;
}StaticFieldResolver
Class implementing FieldResolver with static-only semantics — fn keys are ignored.
setResolver(resolver) / getResolver() / defaultResolver
function setResolver(resolver: FieldResolver): void;
function getResolver(): FieldResolver;
const defaultResolver: StaticFieldResolver;Standalone helpers
function resolveFieldProp<T>(prop, fnKey, staticKey, scope, opts?): T | undefined;
function resolveFormProp<T>(type, fnKey, staticKey, scope, opts?): T | undefined;
function resolveStatic<T>(metadata, staticKey, opts?): T | undefined;
function hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean;
function getFieldMeta<K extends keyof AtscriptMetadata>(
prop: TAtscriptAnnotatedType,
key: K,
): AtscriptMetadata[K] | undefined;resolveStatic is exposed so dynamic resolvers (in ui-fns) can fall back to it without duplicating logic.
parseStaticAttrs(value) / resolveAttrs(prop, scope, keys?)
Resolve @ui.form.attr and the dynamic @ui.form.fn.attr counterpart into a single record. keys overrides the static/fn key pair for @ui.table.attr and friends.
function parseStaticAttrs(value: unknown): Record<string, unknown> | undefined;
function resolveAttrs(
prop: TAtscriptAnnotatedType,
scope: Record<string, unknown>,
keys?: { staticKey?: string; fnKey?: string },
): Record<string, unknown> | undefined;Annotation key constants
Every supported annotation has a stringly-typed constant exported from @atscript/ui so consumers can build resolvers, mappers, or codegen against named keys instead of magic strings.
Cross-surface
| Name | Value |
|---|---|
UI_TYPE | "ui.type" |
Form static keys
UI_FORM_PLACEHOLDER, UI_FORM_HINT, UI_FORM_CLASSES, UI_FORM_STYLES, UI_FORM_AUTOCOMPLETE, UI_FORM_DISABLED, UI_FORM_OPTIONS, UI_FORM_ORDER, UI_FORM_TYPE, UI_FORM_COMPONENT, UI_FORM_HIDDEN, UI_FORM_ATTR, UI_FORM_GRID_COL_SPAN, UI_FORM_GRID_ROW_SPAN, UI_FORM_SUBMIT_TEXT, UI_FORM_LABEL_SINGULAR, UI_FORM_ACTION, UI_FORM_PREFIX, UI_FORM_PREFIX_REF, UI_FORM_PREFIX_ICON, UI_FORM_SUFFIX, UI_FORM_SUFFIX_REF, UI_FORM_SUFFIX_ICON, UI_FORM_VALIDATE.
Form dynamic keys
UI_FORM_FN_PREFIX, UI_FORM_FN_LABEL, UI_FORM_FN_PLACEHOLDER, UI_FORM_FN_DESCRIPTION, UI_FORM_FN_HINT, UI_FORM_FN_HIDDEN, UI_FORM_FN_DISABLED, UI_FORM_FN_READONLY, UI_FORM_FN_OPTIONS, UI_FORM_FN_ATTR, UI_FORM_FN_VALUE, UI_FORM_FN_CLASSES, UI_FORM_FN_STYLES, UI_FORM_FN_TITLE, UI_FORM_FN_SUBMIT_TEXT, UI_FORM_FN_SUBMIT_DISABLED.
Table static keys
UI_TABLE_WIDTH, UI_TABLE_COMPONENT, UI_TABLE_HIDDEN, UI_TABLE_ATTR, UI_TABLE_CLASSES, UI_TABLE_STYLES, UI_TABLE_TYPE, UI_TABLE_ORDER.
Table dynamic keys
UI_TABLE_FN_PREFIX, UI_TABLE_FN_ATTR, UI_TABLE_FN_CLASSES, UI_TABLE_FN_STYLES.
Dictionary
UI_DICT_LABEL, UI_DICT_DESCR, UI_DICT_ATTR, UI_DICT_FILTERABLE, UI_DICT_SORTABLE, UI_DICT_SEARCHABLE.
DB-aware
DB_REL_FK, DB_HTTP_PATH, DB_AMOUNT_CURRENCY, DB_AMOUNT_CURRENCY_REF, DB_UNIT, DB_UNIT_REF, DB_COLUMN_PRECISION.
Workflow
WF_ACTION_WITH_DATA.
Meta / expect
META_LABEL, META_ID, META_DESCRIPTION, META_READONLY, META_REQUIRED, META_DEFAULT, META_SENSITIVE, EXPECT_MAX_LENGTH.
Validators
getFormValidator(def, opts?)
Returns a reusable validator function bound to a FormDef. Built once, called per submit.
function getFormValidator(
def: FormDef,
opts?: Partial<TValidatorOptions>,
): (callOpts: {
data: Record<string, unknown>;
context?: Record<string, unknown>;
}) => Record<string, string>;Returns an errors record keyed by dot-separated field path (empty when valid).
createFieldValidator(prop, opts?)
Field-level validator with internal Validator caching. Returns true on success or the first error message string.
function createFieldValidator(
prop: TAtscriptAnnotatedType,
opts?: { rootOnly?: boolean },
): (value: unknown, externalCtx?: { data: unknown; context: unknown }) => true | string;Default validator plugins
setDefaultValidatorPlugins(plugins) installs validator plugins globally; both getFormValidator and createFieldValidator apply them. getDefaultValidatorPlugins() returns the current list. @atscript/ui-fns uses this to wire its uiFnsValidatorPlugin().
function setDefaultValidatorPlugins(plugins: TValidatorPlugin[]): void;
function getDefaultValidatorPlugins(): TValidatorPlugin[];See the Validation guide for end-to-end usage.
Path utilities
Form data is wrapped: { value: domainData }. These helpers de-reference the wrapper automatically.
getByPath(data, path) / setByPath(data, path, value)
function getByPath(obj: Record<string, unknown>, path: string): unknown;
function setByPath(obj: Record<string, unknown>, path: string, value: unknown): void;path === "" returns or replaces the entire obj.value. Intermediate objects are auto-created on set.
detectUnionVariant(value, variants)
Returns the index of the variant matching value. Uses a discriminator when atscript detects one, otherwise probes each variant's validator. Falls back to 0.
function detectUnionVariant(value: unknown, variants: FormUnionVariant[]): number;Value-help
ValueHelpInfo
Sync probe for a value-help-eligible prop (FK or ref).
interface ValueHelpInfo {
/** HTTP path of the value-help target. */
url: string;
/** Field on the target that this FK references. */
targetField: string;
}extractValueHelp(prop) / extractLiteralOptions(prop) / isPureLiteralUnion(prop)
function extractValueHelp(prop: TAtscriptAnnotatedType): ValueHelpInfo | undefined;
function extractLiteralOptions(
prop: TAtscriptAnnotatedType,
): { key: string; label: string }[] | undefined;
function isPureLiteralUnion(prop: TAtscriptAnnotatedType): boolean;ValueHelpClient
Thin wrapper over a pre-built Client (from @atscript/db-client) that runs FK-flavoured searches against the value-help target. Most consumers stick to resolveValueHelp() plus their own Client; this class formalises the search semantics (full-text $search when the target is searchable, $or-regex fallback otherwise) so picker UIs don't reimplement them.
class ValueHelpClient {
constructor(client: Client);
search(resolved: ResolvedValueHelp, opts?: ValueHelpSearchOptions): Promise<ValueHelpResult>;
}
interface ValueHelpSearchOptions {
/** Search term. Empty / undefined returns all records. */
text?: string;
/** `"form"` = PK + label + descr; `"filter"` = all dict fields including attrs. Default: `"form"`. */
mode?: "form" | "filter";
/** Max results. Default: 20. */
limit?: number;
/** Override the computed `$select` fields. */
select?: string[];
}
interface ValueHelpResult {
items: Record<string, unknown>[];
}resolveValueHelp(url) / resetValueHelpCache()
Globally caches resolved value-help endpoints by URL so multiple fields pointing at the same target share one /meta fetch.
function resolveValueHelp(url: string): Promise<ResolvedValueHelp>;
function resetValueHelpCache(): void;
interface ResolvedValueHelp {
url: string;
primaryKeys: string[];
labelField: string;
descrField: string | undefined;
attrFields: string[];
filterableFields: string[];
sortableFields: string[];
searchable: boolean;
targetType: TAtscriptAnnotatedType;
}valueHelpDictPaths(resolved)
Returns the dict-view path set of a resolved value-help target (PKs + label + descr + attr fields). Filter dialogs use it to clamp visible columns to the dictionary subset.
function valueHelpDictPaths(resolved: ResolvedValueHelp): Set<string>;Option helpers
function optKey(opt: TFormEntryOptions): string;
function optLabel(opt: TFormEntryOptions): string;
function parseStaticOptions(value: unknown): TFormEntryOptions[] | undefined;
function resolveOptions(
prop: TAtscriptAnnotatedType,
scope: Record<string, unknown>,
): TFormEntryOptions[] | undefined;resolveOptions checks @ui.form.options, then @ui.form.fn.options via the active resolver, then literal-union extraction.
Grid layout
Framework-agnostic helpers for @ui.form.grid.colSpan / @ui.form.grid.rowSpan. Each annotation has the shape { desktop, narrow? } — getFieldMeta(prop, UI_FORM_GRID_COL_SPAN) returns a GridSpanArgs.
const DEFAULT_COL_SPAN: number; // 12
const DEFAULT_ROW_SPAN: number; // 1
interface GridSpec {
col: { desktop: number; narrow: number };
row: { desktop: number; narrow: number };
}
interface GridSpanArgs {
desktop: string;
narrow?: string;
}
/** Accepts `"1"`–`"12"` and the aliases `"full"` (12), `"half"` (6), `"third"` (4). */
function parseColSpan(raw: string | undefined): number | undefined;
/** Accepts numeric strings `"1"`+; rejects `"0"`, negatives, decimals, aliases. */
function parseRowSpan(raw: string | undefined): number | undefined;
/** Compose a resolved spec from already-extracted `colSpan` / `rowSpan` annotation values. */
function resolveGridSpec(
colSpan: GridSpanArgs | undefined,
rowSpan: GridSpanArgs | undefined,
): GridSpec;
/** Emit `col-span-X` / `row-span-X` + `as-narrow:` variants for the spec. */
function buildGridClasses(spec: GridSpec): string;See Grid Layout.
Decimal helpers
Framework-agnostic decimal-string formatting and parsing — shared by @atscript/vue-form AsDecimal and @atscript/vue-table cells. Storage is string-only so DB-precision decimals never bounce through floats.
interface CurrencyDisplay {
decimals: number;
symbol: string;
}
interface DecimalParts {
integer: string;
fraction: string;
sign: "+" | "-";
}
interface FormatDecimalOptions {
currency?: string;
locale?: string;
/** Number of fractional digits to display. */
scale?: number;
}
function enforceScale(value: string, scale: number): string;
function formatDecimalForDisplay(value: string, opts?: FormatDecimalOptions): string;
function parseDecimalInput(input: string, opts?: FormatDecimalOptions): string | undefined;
function getCurrencyDecimals(currency: string, locale?: string): number;
function getCurrencyDisplayParts(currency: string, locale?: string): CurrencyDisplay;
function getDecimalSeparator(locale?: string): string;
function getThousandsSeparator(locale?: string): string;
function groupInteger(integer: string, locale?: string): string;
function joinDecimalString(parts: DecimalParts): string;
function splitDecimalString(value: string): DecimalParts;Column helpers
function getVisibleColumns(def: TableDef, hidden?: Set<string>): ColumnDef[];
function getSortableColumns(def: TableDef): ColumnDef[];
function getFilterableColumns(def: TableDef): ColumnDef[];
function getColumn(def: TableDef, path: string): ColumnDef | undefined;Error map utilities
/** Merge any number of partial error maps; falsy values are dropped, later maps win when both have a string. */
function mergeErrorMaps(
...maps: Array<Record<string, string | undefined> | undefined>
): Record<string, string>;
/** Yields every ancestor prefix longest-first, including the path itself. `"a.b.c"` → `"a.b.c", "a.b", "a"`. */
function* iteratePathAncestors(path: string): Generator<string>;
/** Build `Map<absolutePath, descendantErrorCount>` so each struct in the tree renders an error-count badge in O(1). */
function buildDescendantErrorCounts(
errors: Record<string, string | undefined>,
): Map<string, number>;buildDescendantErrorCounts powers the count badges on AsObject headers.
Type guards
function isArrayField(field: FormFieldDef): field is FormArrayFieldDef;
function isObjectField(field: FormFieldDef): field is FormObjectFieldDef;
function isUnionField(field: FormFieldDef): field is FormUnionFieldDef;
function isTupleField(field: FormFieldDef): field is FormTupleFieldDef;Misc utilities
function asArray<T>(x: T | T[]): T[];
/** `@ui.form.label.singular` for array fields; falls back to `"item"`. */
function resolveSingularLabel(prop: TAtscriptAnnotatedType | undefined): string;
/** Always returns a `MeasurementInfo` — individual fields are `undefined` when their annotation is absent. */
function extractMeasurement(prop: TAtscriptAnnotatedType): MeasurementInfo;
function str(value: unknown): string;extractMeasurement reads @db.amount.currency* / @db.unit* / @db.column.precision and returns a structured info record consumed by AsField for currency/unit adornments.
Client factory
ClientFactory is the contract Vue tables and value-help use to build HTTP clients. Override globally to inject auth headers / retries / interceptors.
type ClientFactory = (url: string) => Client; // `Client` from `@atscript/db-client`
function setDefaultClientFactory(factory: ClientFactory): void;
function getDefaultClientFactory(): ClientFactory; // never `undefined` — falls back to `(url) => new Client(url)`
function resetDefaultClientFactory(): void;Meta cache
A single /meta fetch per URL is cached across useTable instances and resolveValueHelp calls. getMetaEntry is synchronous — the promises on the entry resolve once the underlying fetch settles.
function getMetaEntry(url: string, factory?: ClientFactory): MetaCacheEntry;
function resetMetaCache(): void;
interface MetaCacheEntry {
client: Client; // from `@atscript/db-client`
meta: Promise<MetaResponse>;
type: Promise<TAtscriptAnnotatedType>; // pre-deserialized
resolved?: Promise<ResolvedValueHelp>; // populated lazily by `resolveValueHelp`
tableDef?: Promise<TableDef>; // populated lazily by Vue `useTable`
}Cross-links
- Forms — Annotations Reference
- Forms — Validation
- Forms — References (FK)
- Tables — Annotations Reference
- @atscript/ui-fns — dynamic resolver
- atscript.dev —
.assyntax andValidator