Skip to content

@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

@atscript/ui/plugin exposes the build-time plugin that registers every static @ui.* annotation key. Wire it in atscript.config.ts:

typescript
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).

typescript
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.

typescript
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.

typescript
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.

typescript
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

typescript
interface TFormAction {
  id: string;
  label: string;
}

TFormEntryOptions

A select/radio option — plain string or { key, label } pair.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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).

typescript
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.

typescript
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.

typescript
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.

typescript
interface TableActionsModel {
  table: TDbActionInfo[];
  row: TDbActionInfo[];
  rows: TDbActionInfo[];
  default: {
    table?: TDbActionInfo;
    row?: TDbActionInfo;
    rows?: TDbActionInfo;
  };
}

TableQueryState, SortControl, PaginationControl

typescript
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.

typescript
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

typescript
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

typescript
function setResolver(resolver: FieldResolver): void;
function getResolver(): FieldResolver;
const defaultResolver: StaticFieldResolver;

Standalone helpers

typescript
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.

typescript
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

NameValue
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.

typescript
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.

typescript
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().

typescript
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)

typescript
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.

typescript
function detectUnionVariant(value: unknown, variants: FormUnionVariant[]): number;

Value-help

ValueHelpInfo

Sync probe for a value-help-eligible prop (FK or ref).

typescript
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)

typescript
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.

typescript
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.

typescript
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.

typescript
function valueHelpDictPaths(resolved: ResolvedValueHelp): Set<string>;

See Forms — References (FK).

Option helpers

typescript
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.

typescript
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.

typescript
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

typescript
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

typescript
/** 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

typescript
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

typescript
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.

typescript
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.

typescript
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`
}

Released under the MIT License.