--- URL: "ui.atscript.dev/" LLMS_URL: "ui.atscript.dev/index.md" layout: "home" hero2: text: "Your types render themselves." tagline: "Forms, smart tables and multi-step HTTP flows — declared once in `.as`, rendered everywhere. No render boilerplate, no manual wiring, no schema drift." actions: - theme: "brand" text: "Quick Start" link: "/guide/quick-start" - theme: "alt" text: "View on GitHub" link: "https://github.com/moostjs/atscript-ui" --- --- URL: "ui.atscript.dev/api/moost-ui-presets" LLMS_URL: "ui.atscript.dev/api/moost-ui-presets.md" --- # @atscript/moost-ui-presets Moost controller + atscript schema for table-preset persistence. Subclass `AsPresetsController` to wire the table to your auth layer; the rest (CRUD, dirty detection, public-label uniqueness, per-user quotas, capabilities endpoint) ships out of the box. Pair with [`@atscript/vue-table`](/api/vue-table)'s `usePresets()` / `` to ship full preset UX. ## Contents - [Controller](#controller) - [Schema — AsPresetEntry](#schema-aspresetentry) - [Re-exports](#re-exports) ## Controller ### `AsPresetsController` Abstract base extending `AsDbController` from `@atscript/moost-db`. Inherits the standard CRUD endpoints (GET, POST, PATCH, DELETE) and adds: - `GET /capabilities?app=...&tableKey=...` — returns the user's preset capabilities (publish rights, quota). - Read gating — `transformFilter` injects an owner-aware filter so users only see their own private rows and everyone's public rows. - Write hooks — `onWrite` enforces label uniqueness, quota, and stamps `userLabel` / `publicLabel` / `aspects`. - Delete gating — `onRemove` only allows owners to delete. ```typescript @Inherit() abstract class AsPresetsController< T extends TAtscriptAnnotatedType = typeof AsPresetEntry, > extends AsDbController { protected maxPresetsPerUser: number; // 10 protected abstract getCurrentUser(): Promise; protected getMaxPresetsPerUser( app: string, tableKey: string, user: string, ): Promise; protected canPublishPresets( app: string, tableKey: string, user: string, ): Promise; protected getUserLabel(user: string): Promise; @Get("capabilities") capabilities(query: AsCapabilitiesQuery): Promise; } ``` ### Implementation example ```typescript import { Controller } from "moost"; import { AsPresetsController } from "@atscript/moost-ui-presets"; @Controller("/presets") class PresetsController extends AsPresetsController { constructor(private readonly auth: AuthService) { super(); } protected async getCurrentUser(): Promise { const user = await this.auth.currentUser(); if (!user) throw new HttpError(401, "Unauthorized"); return user.id; } /** Free tier: 10 presets. Paid: 100. Admin: unlimited. */ protected async getMaxPresetsPerUser(app: string, _table: string, user: string) { const tier = await this.auth.tierOf(user); return tier === "admin" ? Number.MAX_SAFE_INTEGER : tier === "paid" ? 100 : 10; } protected async canPublishPresets(_app: string, _table: string, user: string) { return (await this.auth.tierOf(user)) !== "free"; } protected async getUserLabel(user: string) { return (await this.auth.profileOf(user))?.displayName; } } ``` ### Extension points | Method | Default | Override when | | ------------------------------------------- | ------------------------------------- | ---------------------------------------------------- | | `getCurrentUser()` | abstract | Always — wire to your auth layer. | | `getMaxPresetsPerUser(app, tableKey, user)` | returns `this.maxPresetsPerUser` (10) | Tiered quotas (admin / paid / free). | | `canPublishPresets(app, tableKey, user)` | returns `true` | Restrict public presets by tier or per-table policy. | | `getUserLabel(user)` | returns `undefined` | Surface display names on shared presets. | ### REST endpoints All endpoints inherited from `AsDbController` work with `AsPresetEntry` rows. The controller adds: - `GET /capabilities?app=&tableKey=` → `PresetCapabilities`. The `@atscript/ui-table` [`PresetsClient`](/api/ui-table#presets-http-clients) targets this exact controller. ## Schema — AsPresetEntry The atscript-db row shape. Stored in table `as_presets`. ```atscript @db.table 'as_presets' export interface AsPresetEntry { @meta.id @db.default.uuid id: string /** Row kind — drives the polymorphic `data` column. */ type: 'preset' | 'userConf' | 'appConf' /** App scope. Required on every row. */ app: string /** Per-table scope. Required for `preset` / `userConf`; absent for `appConf`. */ tableKey?: string /** Owner user id. Stamped by the controller from session. */ user: string /** Optional display name for shared presets. */ userLabel?: string /** Visible to other users when true. Only meaningful for `type='preset'`. */ public?: boolean /** Mirror of `data.label` — top-level for indexable lookups. `type='preset'` only. */ label?: string /** Race-safe public-name uniqueness column. `type='preset' AND public=true` only. */ publicLabel?: string /** Aspects this preset claims — derived by the controller on every write. */ aspects?: PresetAspect[] @db.json data: PresetData | UserConfData | AppConfData @db.default.now createdAt: number @db.default.now updatedAt: number } ``` ### Polymorphic `data` shape - **`type='preset'`** → [`PresetData`](/api/ui-table#presets-application-types): `{ label, content?: PresetSnapshotWire }`. The snapshot blob carries `columns`, `filters`, `filterOps`, `sorters`, `itemsPerPage`. - **`type='userConf'`** → [`UserConfData`](/api/ui-table#presets-application-types): `{ defaultPresetId?, favPresetIds? }`. Per-table user preferences. - **`type='appConf'`** → [`AppConfData`](/api/ui-table#presets-application-types): `{ appearance?, language?, timezone?, density?, dateFormat?, firstDayOfWeek?, customJson? }`. App-wide per-user preferences (one row per `(user, app)`). ### Validation rules enforced by the controller - `aspects[]` is rebuilt from `data.content` keys on every write — clients can't fake aspect claims. - `label` is mirrored from `data.label` on every `type='preset'` write. - `publicLabel` is stamped equal to `label` on `type='preset' AND public=true` writes, NULL otherwise. The composite unique index `preset_public_label_idx (app, tableKey, publicLabel)` relies on NULL ≠ NULL semantics so private / userConf / appConf rows never collide. - `user` is stamped from `getCurrentUser()` on every write — clients can't author rows under another user. - Per-user quota is checked on insert; exceeding it returns HTTP `409` with `code: "preset_limit_reached"` and `{ limit, count }` in the body. ## Re-exports From [`@atscript/ui-table`](/api/ui-table) — re-exported so consumers can grab schema-relevant types from one import: ```typescript export { // Types type AppConfData, type AsPresetEntryData, type AsPresetsErrorCode, type FilterCondition, type FilterConditionType, type PresetAspect, type PresetCapabilities, type PresetData, type PresetLimitReachedBody, type PresetSnapshotWire, type UserConfData, // Constants APP_CONF_PREFIX, PRESET_ASPECTS, RESERVED_ID_PREFIXES, SYSTEM_PRESET_PREFIX, USER_CONF_PREFIX, // Helpers appConfId, userConfId, } from "@atscript/ui-table"; ``` `appConfId(user, app)` and `userConfId(user, app, tableKey)` are the deterministic id builders the controller uses for the singleton `appConf` and per-table `userConf` rows. ## Cross-links - [Tables — Presets](/tables/presets), [Server-Side Presets](/tables/server-presets) - [@atscript/ui-table](/api/ui-table) — wire types, draft helpers, `PresetsClient` - [@atscript/vue-table](/api/vue-table) — `usePresets()`, ``, `` - [db.atscript.dev — Tables](https://db.atscript.dev/api/tables) — base CRUD endpoints inherited from `AsDbController` - atscript.dev — `.as` syntax, `@db.*` annotations --- URL: "ui.atscript.dev/api/moost-wf" LLMS_URL: "ui.atscript.dev/api/moost-wf.md" --- # @atscript/moost-wf Server-side workflow runtime that pairs with [`@atscript/vue-wf`](/api/vue-wf). Decorators (`@WfInput`, `@WfAction`), a schema-aware composable (`useAtscriptWf`), helpers for context passing and action discovery, plus an opt-in store implementation backed by atscript-db. Atscript-typed forms are the contract — the client renders whatever schema the server returns. ## Contents - [Plugin](#plugin) - [Decorators](#decorators) - [Composables](#composables) - [Helpers](#helpers) - [Outlet helpers](#outlet-helpers) - [Finish-screen envelope](#finish-screen-envelope) - [Store subpath](#store-subpath) ## Plugin ### `@atscript/moost-wf/plugin` Registers the workflow-specific annotation keys at build time. ```typescript import wfPlugin from "@atscript/moost-wf/plugin"; export default { plugins: [wfPlugin()], }; ``` Annotations registered: - `@wf.context.pass 'key'` — whitelist a workflow context key for the `inputRequired` response. Only listed keys reach the client. - `@wf.action.withData 'id'` — form action that ships partial form data with deep-partial validation (e.g. "save draft"). - `@wf.store.fromContext 'path'` — shadow-column copy from `state.context` into a top-level DB column (`AsWfStore` only). Path is dot-notation; field must be optional or have `@meta.default` / `@db.default`; primitive type only (`string` | `number` | `boolean`). ## Decorators ### `@WfInput(opts?)` Parameter decorator that resolves to the validated, typed input for the current step. Built on top of `useAtscriptWf()` with the action-aware policy matrix baked in. ```typescript function WfInput(opts?: { pass?: boolean }): ParameterDecorator; ``` ```typescript import { WfInput } from "@atscript/moost-wf"; import { Step } from "@moostjs/event-wf"; import type { LoginForm } from "./LoginForm.as"; class AuthFlow { @Step("login") async login(@WfInput() input: LoginForm) { try { return await this.auth.login(input.username, input.password); } catch { // requireInput() builds a StepRetriableError — throw it to re-pause. throw useAtscriptWf(LoginForm).requireInput({ errors: { password: "Invalid credentials" }, }); } } } ``` Policy matrix: | Situation | Behaviour | | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | No action fired | Strict full validation against the schema. Missing input throws `StepRetriableError`. | | Action declared in `@wf.action.withData` | Deep-partial validation — present fields validated, missing fields OK. | | Action declared in `@ui.form.action` | Input must be absent. Without `pass: true`, the decorator throws (no-data actions exclude this step). | | Action declared in `@ui.form.action`, `pass: true` | The step opts in to handling the no-data action: parameter resolves to `undefined`, body still runs. | | Unknown action | `StepRetriableError` propagates with `__form: 'Action "" is not supported'`. Same guard fires from `@WfAction(Form)` before the step body runs. | Use `?:` syntax (`input?: Form`), not a union (`input: Form | undefined`) — TS emits `Object` for union-typed parameters in decorator metadata, which breaks atscript reflection. `@WfInput({ pass: true })` composes `@Optional()` internally so the parameter being `undefined` is fine for global validator pipes. ### `@WfAction(Form)` Parameter decorator that resolves to the current workflow action name (or `undefined` for plain submits). The form type argument is **required** — the decorator validates the incoming action against the form's `@ui.form.action` / `@wf.action.withData` whitelist and throws `StepRetriableError` for unknown actions before the step body runs. ```typescript function WfAction(type: TAtscriptAnnotatedType): ParameterDecorator; ``` ```typescript @Step("mfa-verify") async mfaVerify( @WfAction(PincodeForm) action: string | undefined, @WfInput() input: PincodeForm, ) { if (action === "resend") { await this.sendOtp(this.ctx.email); return; } await this.verifyCode(input.code); } ``` The form-argument whitelist is a security guard: step bodies never see an action that wasn't declared on the form. Use the same form type the step receives via `@WfInput`. ## Composables ### `useAtscriptWf(type)` Schema-driven workflow I/O primitives. Returns three pure, independent helpers — composable consumers can interleave their own logic between checking the action and validating the input. ```typescript function useAtscriptWf( type: TAtscriptAnnotatedType, ): { resolveInput(opts?: { partial?: "deep" }): InferDataType; resolveAction(): string | undefined; requireInput(opts?: { errors?: Record; formMessage?: string; }): StepRetriableError; }; ``` | Method | Purpose | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `resolveInput(opts?)` | Validates `state.input` against the schema and returns it typed. Throws `StepRetriableError` if input is missing or invalid. **Does not consult the action.** | | `resolveAction()` | Returns the action name (or `undefined`). Throws `StepRetriableError` if the action is not declared on the schema. **Does not consult the input.** | | `requireInput(opts?)` | Builder for `StepRetriableError`. `errors` is field-keyed; `formMessage` becomes the top-level `__form` error. Use it to bail out from custom branches. | Validator instances are cached per `(type, opts)` pair. ```typescript import { useAtscriptWf } from "@atscript/moost-wf"; import { LoginForm } from "./LoginForm.as"; @Step("login") async login() { const wf = useAtscriptWf(LoginForm); const action = wf.resolveAction(); if (action === "forgot") { await this.sendPasswordReset(); return; } const input = wf.resolveInput(); // ... auth ... if (failed) { throw wf.requireInput({ errors: { password: "Invalid credentials" } }); } } ``` ## Helpers ### `serializeFormSchema(type)` Cached per-type serializer for `inputRequired.payload`. Drops `@wf.context.pass` from the output (server-only) and ships FK fields with shallow refs (`refDepth: 0.5`) so clients can resolve value-help without dragging full target trees onto the wire. ```typescript function serializeFormSchema(type: TAtscriptAnnotatedType): unknown; ``` ### `extractPassContext(type, wfContext)` Extract whitelisted context keys (those marked `@wf.context.pass`) from workflow state. ```typescript function extractPassContext( type: TAtscriptAnnotatedType, wfContext: Record, ): Record; ``` ### `getFormActions(type)` Inspects a form type's metadata and returns the declared `actions` and `actionsWithData` ids — used internally by `useAtscriptWf` to classify the incoming action. ```typescript interface TFormActions { actions: string[]; actionsWithData: string[]; } function getFormActions(type: TAtscriptAnnotatedType): TFormActions; ``` Reads `@ui.form.action` and `@wf.action.withData`. Results are cached per type identity. ## Outlet helpers ### `createAsHttpOutlet()` Pre-configured HTTP outlet for ``. Wraps generic form payloads in `{ inputRequired: { payload, transport, context } }` while letting signal payloads (`finished` / `sent` / `outlet` / `error`) flow through unwrapped. ### `handleAsOutletRequest(opts, deps)` Drop-in replacement for `handleWfOutletRequest` (from `@moostjs/event-wf`). Adds the `finished: true` marker that `` reads when a step calls `useWfFinished().set({ value })`. ```typescript import { createAsHttpOutlet, handleAsOutletRequest } from "@atscript/moost-wf"; @Post("wf") async handle() { // ... build deps ... return handleAsOutletRequest( { allow: ["auth/login"], state: () => new EncapsulatedStateStrategy({ secret: WF_SECRET }), outlets: [createAsHttpOutlet()], token: { read: ["body"], write: "body", name: "wfs" }, }, deps, ); } ``` ## Finish-screen envelope Helpers for the unified `WfFinished` envelope rendered by `` on the client. Reach for `finishWf` / `abortWf` instead of raw `useWfFinished()` unless you need to set HTTP-level cookies alongside the envelope. ### `finishWf(opts?)` Builds and writes a `WfFinished` envelope to `useWfFinished()`. ```typescript function finishWf(opts?: FinishWfOpts): void; interface FinishWfOpts { data?: unknown; message?: WfMessage; next?: WfNext; } ``` ### `abortWf(reason, opts?)` Same shape as `finishWf`, additionally sets `aborted: true` and the abort `reason`. ```typescript function abortWf(reason: string, opts?: FinishWfOpts): void; ``` ### `isWfFinished(value)` Type guard for `WfFinished` envelopes — useful in custom transports or outlet wrappers. ```typescript function isWfFinished(value: unknown): value is WfFinished; ``` ### Envelope types `WfFinished` carries `{ aborted?, reason?, data?, message?, next? }`. `WfNext` is the `{ trigger: 'immediate' | 'auto' | 'manual', ... }` discriminated union the client renders. `WfMessage`, `WfButton`, and `WfActionRequest` round out the schema. See [Finish Screens](/workflows/finish-screens) for the full surface. ## Store subpath `@atscript/moost-wf/store` ships an opt-in `WfStateStore` implementation backed by an atscript-db table. ### `AsWfStateRecord` The base atscript interface defining the columns the store reads/writes. Consumers extend it with their own `@meta.id`-bearing primary-key column. ```atscript // .as files import the model from `/store.as` (raw atscript source). // .ts files import the runtime class from `/store` (compiled module). import { AsWfStateRecord } from '@atscript/moost-wf/store.as' @db.table 'wf_state' interface WfStateRow extends AsWfStateRecord { @meta.id @db.default.uuid id: string // Shadow columns — auto-populated from state.context on every set() @wf.store.fromContext 'approval.approver' approver?: string } ``` Columns provided by `AsWfStateRecord`: `handle`, `schemaId`, `state` (JSON), `expiresAt?`, `createdAt`, `updatedAt`, `createdBy?`, `lastUpdatedBy?`. ### `AsWfStore` Persistent `WfStateStore` (from `@prostojs/wf/outlets`) backed by an `AtscriptDbTable`. Lifts `schemaId` out of the JSON blob into an indexed top-level column; populates `@wf.store.fromContext`-annotated shadow columns on every write. ```typescript class AsWfStore implements WfStateStore { constructor(opts: AsWfStoreOptions); set(handle: string, state: WfState, expiresAt?: number): Promise; get(handle: string): Promise<{ state: WfState; expiresAt?: number } | null>; delete(handle: string): Promise; /** Race-safe single-use consume — `findRow` → `deleteMany` → `deletedCount === 1` gate. */ getAndDelete(handle: string): Promise<{ state: WfState; expiresAt?: number } | null>; /** Drop expired rows. `retention` extends the grace window; `Infinity` is a no-op. */ cleanup(opts?: { retention?: number }): Promise; /** Re-apply `@wf.store.fromContext` shadow columns to existing rows. */ heal(opts?: { filter?: Record; batchSize?: number }): Promise; } interface AsWfStoreOptions { table: AtscriptDbTable; clock?: { now(): number }; actor?: () => string | undefined; } ``` ### Shadow-column mechanism When a field on the consumer's row schema carries `@wf.store.fromContext 'path'`: 1. On every `set()`, the store reads `state.context` along the dot-path. 2. If the resolved value is `string` / `number` / `boolean`, it is written to the matching DB column. 3. On miss (or type mismatch), **optional** fields get `null` (clears stale); non-optional default-bearing fields are omitted (DB default fires on insert, prior value sticks on update). 4. Type mismatches emit a single console warning per field per store instance. `heal()` re-applies shadows after you add a new annotation, so existing rows backfill without waiting for each workflow to next pause. ### Subclassing `AsWfStore` is subclass-friendly. Override: - `getActor()` — custom auth source. - `findRow(handle)` — sharded / multi-tenant lookup. Preserve the `getAndDelete` contract. - `buildSetPayload(handle, state, opts)` — add custom columns alongside the base payload. - `applyShadows(payload, state)` — change how shadow values are resolved or coerced. - `scanShadowFields()` — read shadow specs from a different annotation key. - `resolvePath(obj, path)` / `coerceShadowValue(raw, spec)` — change traversal or type-coercion rules. ## Cross-links - [Workflows — Overview](/workflows/) - [Workflows — Server Authoring](/workflows/server-authoring) - [Workflows — Form Input](/workflows/form-input) - [Workflows — Actions](/workflows/actions) - [Workflows — Context Passing](/workflows/context) - [Workflows — State Persistence](/workflows/state-persistence) - [Workflows — Outlets & Resume](/workflows/outlets-resume) - [@atscript/vue-wf](/api/vue-wf) — client runtime - atscript.dev — `.as` syntax and serialization - atscript-db.dev — `AtscriptDbTable`, `@db.*` annotations --- URL: "ui.atscript.dev/api/ui" LLMS_URL: "ui.atscript.dev/api/ui.md" --- # @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](#plugin) - [Form types](#form-types) - [Form factories](#form-factories) - [Table types](#table-types) - [Table factory](#table-factory) - [Field resolver](#field-resolver) - [Annotation key constants](#annotation-key-constants) - [Validators](#validators) - [Path utilities](#path-utilities) - [Value-help](#value-help) - [Grid layout](#grid-layout) - [Decimal helpers](#decimal-helpers) - [Column helpers](#column-helpers) - [Error map utilities](#error-map-utilities) - [Type guards](#type-guards) - [Misc utilities](#misc-utilities) - [Client factory](#client-factory) - [Meta cache](#meta-cache) ## 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`](/api/ui-fns). ## Form types ### `FormDef` Complete form definition produced by `createFormDef(type)`. ```typescript interface FormDef { type: TAtscriptAnnotatedType; rootField: FormFieldDef; fields: FormFieldDef[]; flatMap: Map; } ``` ### `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, opts?)` Builds a `FormDef` from an annotated type. Walks props, pre-resolves structural sub-defs, and caches the flat map. ```typescript function createFormDef(type: TAtscriptAnnotatedType, opts?: { versionColumn?: string }): FormDef; ``` When `opts.versionColumn` is supplied, the matching prop is excluded from the returned `fields[]` (so `AsForm`'s renderer doesn't paint it) but remains in `flatMap` and in the form's underlying data wrapper. This is the contract OCC needs: hide the version input from users while keeping the value in the wire payload so the server can lift it into `$cas`. Pass `meta.versionColumn` directly. ```ts formDef.value = createFormDef(deserializeAnnotatedType(meta.type), { versionColumn: meta.versionColumn, }); ``` See the [Edit forms with optimistic concurrency](/tables/edit-form-occ) pattern guide for the end-to-end flow. ### `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( type: T, resolver?: TFormValueResolver, ): { value: TAtscriptDataType }; ``` ### `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, context?: Record, ): TFormValueResolver; ``` ## Table types ### `TableDef` Complete table definition built by `createTableDef(meta, type)`. ```typescript interface TableDef { type: TAtscriptAnnotatedType; columns: ColumnDef[]; flatMap: Map; primaryKeys: string[]; /** Preferred row identifier for URL/wire addressing. */ preferredId: string[]; /** Mirrors `MetaResponse.versionColumn` — name of the OCC version column when the table opts into `@db.column.version`. */ versionColumn?: string; crud: TCrudPermissions; canRemove: boolean; actions: TableActionsModel; searchable: boolean; vectorSearchable: boolean; searchIndexes: SearchIndexInfo[]; relations: RelationInfo[]; } ``` `createTableDef` propagates `versionColumn` from the meta envelope and skips the matching entry from `def.columns`, so filter/sort/column-picker dialogs ignore the version column automatically — see [Edit forms with optimistic concurrency](/tables/edit-form-occ). ### `ColumnDef` A single column definition built from field metadata + annotations. ```typescript interface ColumnDef { path: string; label: string; type: string; component?: string; /** Extra sibling leaf paths to fetch when this column is visible — see [Custom Cells](/tables/custom-cells). */ selectWith?: 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[]; versionColumn?: string; crud: TCrudPermissions; actions: TDbActionInfo[]; relations: RelationInfo[]; fields: Record; 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; } ``` **`versionColumn?: string`** — name of the server-managed row version column on OCC-protected tables (declared with `@db.column.version` in your `.as` schema). Absent on tables that don't opt into OCC. Consumer code should pass this through to `createFormDef` so the version field doesn't render as an editable input, while still riding in the form data for the server's `$cas` round-trip. See the [Edit forms with optimistic concurrency](/tables/edit-form-occ) pattern guide for the full flow. ### `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; } ``` ## 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](/tables/annotations) 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( prop: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record, opts?: TResolveOptions, ): T | undefined; resolveFormProp( type: TAtscriptAnnotatedType, fnKey: string, staticKey: string | undefined, scope: Record, opts?: TResolveOptions, ): T | undefined; hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean; } interface TResolveOptions { 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(prop, fnKey, staticKey, scope, opts?): T | undefined; function resolveFormProp(type, fnKey, staticKey, scope, opts?): T | undefined; function resolveStatic(metadata, staticKey, opts?): T | undefined; function hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean; function getFieldMeta( 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 | undefined; function resolveAttrs( prop: TAtscriptAnnotatedType, scope: Record, keys?: { staticKey?: string; fnKey?: string }, ): Record | 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_SELECT_WITH`, `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, ): (callOpts: { data: Record; context?: Record; }) => Record; ``` 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](/forms/validation) 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, path: string): unknown; function setByPath(obj: Record, 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; } 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[]; } ``` ### `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; 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; ``` See [Forms — References (FK)](/forms/references). ### 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, ): 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](/forms/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): 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 | undefined> ): Record; /** Yields every ancestor prefix longest-first, including the path itself. `"a.b.c"` → `"a.b.c", "a.b", "a"`. */ function* iteratePathAncestors(path: string): Generator; /** Build `Map` so each struct in the tree renders an error-count badge in O(1). */ function buildDescendantErrorCounts( errors: Record, ): Map; ``` `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(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; type: Promise; // pre-deserialized resolved?: Promise; // populated lazily by `resolveValueHelp` tableDef?: Promise; // populated lazily by Vue `useTable` } ``` ## Cross-links - [Forms — Annotations Reference](/forms/annotations) - [Forms — Validation](/forms/validation) - [Forms — References (FK)](/forms/references) - [Tables — Annotations Reference](/tables/annotations) - [@atscript/ui-fns](/api/ui-fns) — dynamic resolver - atscript.dev — `.as` syntax and `Validator` --- URL: "ui.atscript.dev/api/ui-fns" LLMS_URL: "ui.atscript.dev/api/ui-fns.md" --- # @atscript/ui-fns Opt-in dynamic resolver that wires `@ui.form.fn.*`, `@ui.table.fn.*`, and `@ui.form.validate` annotation strings into the global resolver in [`@atscript/ui`](/api/ui). Without this package every `@ui.fn.*` annotation is silently ignored. ## Contents - [Plugin](#plugin) - [Installer](#installer) - [Dynamic resolver](#dynamic-resolver) - [Compilers](#compilers) - [Validator plugin](#validator-plugin) - [Helpers](#helpers) - [Types](#types) - [Security](#security) - [Performance](#performance) ## Plugin `@atscript/ui-fns/plugin` registers the build-time `@ui.fn.*` annotation keys (form + table) and `@ui.form.validate`. Add it next to `uiPlugin()` in `atscript.config.ts`: ```typescript import uiPlugin from "@atscript/ui/plugin"; import uiFnsPlugin from "@atscript/ui-fns/plugin"; export default { plugins: [uiPlugin(), uiFnsPlugin()], }; ``` ## Installer ### `installDynamicResolver()` One-shot installer. Calls `setResolver(new DynamicFieldResolver())` and `setDefaultValidatorPlugins([uiFnsValidatorPlugin()])`. Invoke once at app startup, before any form/table is built. ```typescript import { installDynamicResolver } from "@atscript/ui-fns"; installDynamicResolver(); ``` After this, every `resolveFieldProp`/`resolveFormProp` call walks the `ui.fn.*` keys first and compiles them on demand. Static annotations still work exactly as in `@atscript/ui`. ## Dynamic resolver ### `DynamicFieldResolver` Class implementing [`FieldResolver`](/api/ui#field-resolver). Compiles and evaluates `@ui.form.fn.*` / `@ui.table.fn.*` annotation strings on demand, falling back to static resolution via `resolveStatic`. ```typescript class DynamicFieldResolver implements FieldResolver { resolveFieldProp(prop, fnKey, staticKey, scope, opts?): T | undefined; resolveFormProp(type, fnKey, staticKey, scope, opts?): T | undefined; hasComputedAnnotations(prop: TAtscriptAnnotatedType): boolean; } ``` `hasComputedAnnotations` returns true when at least one metadata key on the prop starts with `ui.form.fn.` or `ui.table.fn.` — the Vue form/table layers use this as a perf flag to skip per-keystroke re-resolution for fully static fields. ### Attr handling Keys ending in `attr` (`UI_FORM_FN_ATTR`, `UI_TABLE_FN_ATTR`) store `[{ name, fn }]` arrays instead of a single fn string; the resolver evaluates each entry and merges into a single record. ## Compilers All three functions return a callable bound to a [`TFnScope`](#types). Compiled functions are cached in an internal `FNPool` keyed by the source string. ### `compileFieldFn(fnStr)` ```typescript function compileFieldFn(fnStr: string): (scope: TFnScope) => R; ``` The string is wrapped as: ```javascript return ()(v, data, context, entry) ``` So a field-level dynamic annotation reads like: ```atscript @ui.form.fn.hidden '(v, data) => !data.optedIn' ``` ### `compileTopFn(fnStr)` ```typescript function compileTopFn(fnStr: string): (scope: TFnScope) => R; ``` Form-level wrapping — no leaf value or entry: ```javascript return ()(data, context) ``` Used for root-interface `@ui.form.fn.title`, `@ui.form.fn.description`, `@ui.form.fn.submit.text`, `@ui.form.fn.submit.disabled`. ### `compileValidatorFn(fnStr)` Delegates to `compileFieldFn` with a narrowed return type. Returns `true` for valid, or a string error message for invalid. ```typescript function compileValidatorFn(fnStr: string): (scope: TFnScope) => boolean | string; ``` ## Validator plugin ### `uiFnsValidatorPlugin()` Returns a [`TValidatorPlugin`](/api/ui#validators) that walks `@ui.form.validate` annotations on a field, compiles each, and reports the first non-`true` result as the field's validation error. ```typescript function uiFnsValidatorPlugin(): TValidatorPlugin; ``` The plugin is auto-registered by `installDynamicResolver()`. Re-export and add it manually if you need a fine-grained Validator instance. ```atscript @ui.form.validate '(v, data) => v === data.passwordConfirm || "Passwords do not match"' password: string ``` See [Validation](/forms/validation) for end-to-end usage. ## Helpers ### `buildFieldEntry(prop, baseScope, path, opts?)` Implements the dual-scope pattern used by the resolver and the validator plugin: 1. Resolve constraints (`disabled`, `hidden`, `readonly`) from `baseScope`. 2. Assemble a `TFieldEvaluated` entry object. 3. Build the full scope `{ ...baseScope, entry }`. 4. Resolve `options` against the full scope (so option expressions can read the entry's `type` / `disabled`). ```typescript function buildFieldEntry( prop: TAtscriptAnnotatedType, baseScope: TFnScope, path: string, opts?: TBuildFieldEntryOpts, ): TFnScope; type TBuildFieldEntryOpts = Partial< Pick< TFieldEvaluated, "name" | "type" | "component" | "optional" | "disabled" | "hidden" | "readonly" > >; ``` The returned scope is the full evaluation scope (with `entry` populated). Use it when invoking other compiled fns that depend on resolved field state. ## Types ### `TFnScope` The eval scope passed to every compiled function. Properties become positional arguments inside the wrapping shim: ```typescript interface TFnScope, C = Record> { /** Current field value (leaf). Undefined for form-level fns. */ v?: V; /** Whole form's domain data. */ data: D; /** Caller-supplied formContext. */ context: C; /** Resolved field meta (label, type, disabled, hidden, options, …). */ entry?: TFieldEvaluated; /** Action name when the fn is evaluated during action dispatch. */ action?: string; } ``` ### `TComputed` Convenience union for "static value or scope-bound function". ```typescript type TComputed = T | ((scope: TFnScope) => T); ``` ### `TFieldEvaluated` Resolved per-field snapshot passed to validators and computed annotations as `entry`. ```typescript interface TFieldEvaluated { field: string; type: string; component?: string; name: string; disabled?: boolean; optional?: boolean; hidden?: boolean; readonly?: boolean; options?: TFormEntryOptions[]; } ``` ### `TValidatorContext` Per-call context passed via `validator.validate(value, safe, context)` and unpacked by `uiFnsValidatorPlugin`. ```typescript interface TValidatorContext { data: Record; context: Record; } ``` ### `TBuildFieldEntryOpts` Defined above next to [`buildFieldEntry`](#helpers). ## Security `compileFieldFn`, `compileTopFn`, and `compileValidatorFn` all use `new Function` under the hood (via `@prostojs/deserialize-fn`'s `FNPool`). The compiled function runs in the host JS realm — it sees `globalThis`, can throw, can loop forever. This is **only safe for compile-time-validated schemas you author or trust**. Never accept user-authored fn strings from the network and feed them through this resolver. The atscript compiler validates expression syntax at build time so a typo fails CI rather than at runtime. Browser deployments with strict CSP require `unsafe-eval`. ## Performance - Every fn string is compiled once and cached in `FNPool` for the lifetime of the process. Identical strings on different fields share the same compiled function. - `hasComputedAnnotations` short-circuits Vue's per-keystroke re-resolution: fields where every annotation is static skip the resolver loop entirely. The `FormFieldDef.allStatic` flag mirrors this on the form side. - `buildFieldEntry` resolves constraints once, then reuses the entry across `options` and validator evaluation in the same submit pass. ## Cross-links - [Forms — Dynamic Fields](/forms/dynamic-fields) - [Forms — Validation](/forms/validation) - [@atscript/ui — FieldResolver](/api/ui#field-resolver) - [@atscript/ui — Validators](/api/ui#validators) --- URL: "ui.atscript.dev/api/ui-styles" LLMS_URL: "ui.atscript.dev/api/ui-styles.md" --- # @atscript/ui-styles The vunor-powered UnoCSS preset, the safelist extractor that drives our as-component class collection, the `as-*` shortcut tree, the baked-icon collection, and the `unplugin-vue-components` resolver. Spread `asPresetVunor()` into UnoCSS, register `AsResolver()` in Vite, and every `as-*` class used by `@atscript/vue-form` / `@atscript/vue-table` / `@atscript/vue-wf` resolves correctly. ## Contents - [Preset](#preset) - [Extractor](#extractor) - [Base UnoCSS config](#base-unocss-config) - [Shortcuts](#shortcuts) - [Component class maps](#component-class-maps) - [Icons](#icons) - [vunor re-exports](#vunor-re-exports) - [Vite subpath — AsResolver](#vite-subpath-asresolver) - [Pre-built CSS subpaths](#pre-built-css-subpaths) ## Preset ### `asPresetVunor(options?)` Returns an array of UnoCSS presets + rules + safelist entries spreadable into `unocss.config.ts`. Bundles vunor (palette / scope / layer / surface / c8 / i8), the `as-*` shortcut tree, the baked icon collection (`i-as-*`), and the safelist extractor. ```typescript // AsBaseUnoConfigOptions IS vunor's presetVunor option type — every field // (`baseRadius`, `palette`, `fingertip`, `typography`, `animation`, …) is // accepted flat at the top level. Omitted fields fall back to the baked // `defaultAsVunorOptions`. `palette.colors` and `fingertip` shallow-merge // per-key so you can override one entry without redeclaring the whole map. type AsVunorPresetOptions = NonNullable[0]>; interface AsBaseUnoConfigOptions extends AsVunorPresetOptions {} interface AsPresetVunorOptions extends AsBaseUnoConfigOptions { /** Drop a kebab-case component's classes from the safelist when the consumer has replaced it. * Typed against the generated `AsComponentName` union — autocompletes, typos fail the build. */ excludeComponents?: AsComponentName[]; /** Replace built-in `i-as-` icons with custom SVG strings. Unknown keys ignored. */ iconOverrides?: Record; } function asPresetVunor(options?: AsPresetVunorOptions): Preset[]; /** Baked-in vunor theme atscript-ui has shipped since day one. Exported so * consumers can read or extend it (e.g. spread + tweak one field). */ const defaultAsVunorOptions: AsVunorPresetOptions; ``` ```typescript import { defineConfig } from "unocss"; import { asPresetVunor } from "@atscript/ui-styles"; export default defineConfig({ presets: [ ...asPresetVunor({ baseRadius: "0.5rem", palette: { colors: { primary: "#a855f7" } }, // keeps grey/neutral/error defaults fingertip: { m: "36px" }, // keeps xs/s/l/xl defaults excludeComponents: ["as-input"], }), ], }); ``` See [Styling — Installation](/styling/installation) and [Theme & Palette](/styling/theme). ## Extractor ### `createAsExtractor(opts)` Builds the UnoCSS source-code extractor that pre-seeds the safelist with every class our components reference. Used internally by `asPresetVunor`; expose your own for advanced setups (e.g. running the safelist build inside a different bundler). ```typescript interface AsExtractorOptions { /** Kebab-case component names to drop from the safelist. */ excludeComponents?: AsComponentName[]; } function createAsExtractor(opts?: AsExtractorOptions): { name: string; extract(ctx: { code: string; id?: string }): Set; }; ``` ## Base UnoCSS config ### `createAsBaseUnoConfig(opts?)` Returns the minimal UnoCSS base config (vunor + presets + safelist) the demo apps use. Most consumers spread `asPresetVunor()` directly; reach for `createAsBaseUnoConfig` only when you want the entire opinionated config object pre-assembled. ```typescript function createAsBaseUnoConfig(opts?: AsBaseUnoConfigOptions): UserConfig; ``` ## Shortcuts The shortcut tree is split per package so consumers can opt in. Each export is a `TVunorShortcut[]` ready to spread into a vunor config. ```typescript import { commonShortcuts, formShortcuts, tableShortcuts, wfShortcuts, aoothShortcuts, allShortcuts, } from "@atscript/ui-styles"; const commonShortcuts: TVunorShortcut[]; const formShortcuts: TVunorShortcut[]; const tableShortcuts: TVunorShortcut[]; const wfShortcuts: TVunorShortcut[]; const aoothShortcuts: TVunorShortcut[]; const allShortcuts: TVunorShortcut[]; // mergeVunorShortcuts of the five above ``` `asPresetVunor()` already injects `allShortcuts`. Spread individual shortcut sets only when assembling a custom preset for a subset of the libraries. See [The as-\* Shortcut System](/styling/shortcuts). ## Component class maps Generated at build time by walking every `as-*.vue` template. Drives the safelist extractor and the optional component-exclusion knob. ```typescript /** Union of every published kebab-case component name — generated from the component set; powers `excludeComponents` autocomplete (typos fail the build). */ type AsComponentName = | "as-action" | "as-array" | /* …every published component… */ "as-window-table"; /** Map: kebab-case component name → list of class names it references (own classes only). */ const componentClasses: Record; /** Map: kebab-case component name → tracked components it renders or lazy-mounts. * The extractor expands these recursively; each entry is independently veto-able * via `excludeComponents`. See the Bundle Optimization guide. */ const componentCompanions: Record; /** Map: kebab-case component name → owning package ("form" | "table" | "wf"). */ const componentPackages: Record; /** Union of every class referenced by the given kebab-case component names, companions included. * Names in `exclude` contribute nothing and are not traversed through. */ function getComponentClasses(names: readonly string[], exclude?: ReadonlySet): string[]; /** Same as `getComponentClasses`, but takes helper-function names and expands them via `helperAliases`. */ function getHelperClasses(...helpers: string[]): string[]; /** * Map: default-bundle helper function name → list of kebab-case components it wires up. * Keys are `createDefaultCellTypes`, `createDefaultControls`, `createDefaultTypes`. * A call to `createDefaultControls()` in your source pulls in the classes for every default * component it returns. */ const helperAliases: Record; ``` ## Icons ### `bakedIcons` ```typescript const bakedIcons: Record; ``` Map of semantic icon name → Iconify id or inline SVG string. Baked at publish time so the preset works offline and inside CSP-restricted environments. Override individual entries via `asPresetVunor({ iconOverrides })`. Built-in semantic names include: `search`, `close`, `plus`, `chevron-{up,down,left,right}`, `chevron-double-*`, `arrow-{up,down}`, `grip`, `filter`, `filter-ops`, `sort-asc`, `value-help`, `sun`, `moon`, `check`, `check-square`, `sorters`, `refresh`, `columns`, `eye`, `eye-off`, `eye-slash`, `star`, `star-filled`, `pin`, `pin-filled`, `settings`, `ellipsis`, `menu`, `trash`, `loading`, `warning`, `field-empty`, `field-fill`. Reference an icon in your own shortcut with `i-as-`: ```typescript defineShortcuts({ "my-button": "i-as-plus c8-filled scope-primary", }); ``` See [Styling — Icons](/styling/icons). ## vunor re-exports For convenience, the package re-exports the vunor shortcut authoring helpers and the `TVunorShortcut` type so consumers don't need a second import. ```typescript import { defineShortcuts, mergeVunorShortcuts, toUnoShortcut } from "@atscript/ui-styles"; import type { TVunorShortcut } from "@atscript/ui-styles"; function defineShortcuts(map: Record): TVunorShortcut[]; function mergeVunorShortcuts(sets: TVunorShortcut[][]): TVunorShortcut[]; function toUnoShortcut(shortcut: TVunorShortcut): readonly [string | RegExp, string]; ``` ## Vite subpath — AsResolver ### `@atscript/ui-styles/vite` ```typescript import { AsResolver } from "@atscript/ui-styles/vite"; import Components from "unplugin-vue-components/vite"; export default { plugins: [ Components({ resolvers: [AsResolver()], }), ], }; ``` `AsResolver()` returns a `ComponentResolverObject` that auto-imports the **Tier 1 primary components** — `AsField`, `AsFilters`, `AsForm`, `AsIterator`, `AsPresetPicker`, `AsTable`, `AsTableActions`, `AsTableRoot`, `AsWfForm`, `AsWindowTable` — on first use in a template. (Authoritative source: `primaryComponents` in `packages/ui-styles/src/generated/component-classes.ts`.) Tier-2 defaults (`AsInput`, `AsFilterDialog`, …) are not auto-resolved — users import them explicitly when composing custom defaults. Composables (`useTable`, …) are never handled by `unplugin-vue-components`. ```typescript function AsResolver(): ComponentResolverObject; ``` ## Pre-built CSS subpaths For apps that don't run UnoCSS at all, the package ships pre-baked CSS bundles. Importing one of these subpaths for side-effect injects every class our components need. ```typescript import "@atscript/ui-styles/css"; // alias for /css/all import "@atscript/ui-styles/css/all"; // form + table + wf + aooth + common import "@atscript/ui-styles/css/form"; // form only import "@atscript/ui-styles/css/table"; // table only import "@atscript/ui-styles/css/wf"; // wf only import "@atscript/ui-styles/css/aooth"; // @atscript/vue-aooth components only ``` Caveat: pre-built CSS skips theme tuning. Use `asPresetVunor()` if you want to change the palette, base radius, fingertip ladder, or icon set. See [Pre-built CSS](/styling/prebuilt-css). ## Cross-links - [Styling — Overview](/styling/) - [Styling — Installation](/styling/installation) - [Styling — Theme & Palette](/styling/theme) - [Styling — Icons](/styling/icons) - [Styling — The as-\* Shortcut System](/styling/shortcuts) - [Styling — Pre-built CSS](/styling/prebuilt-css) - [Guide — Bundle Optimization](/guide/bundle-optimization) - vunor — internal shortcuts engine atscript-ui composes against (`vunor`, `vunor/theme`, `vunor/utils`) --- URL: "ui.atscript.dev/api/ui-table" LLMS_URL: "ui.atscript.dev/api/ui-table.md" --- # @atscript/ui-table Framework-agnostic table model. Filter conditions, filter→Uniquery conversion, sorter/filter merging, URL state bridging, preset wire types, window-mode pagination math, column widths, selection helpers, and the abstract `TableStateMethods` / `TableStateData` contracts that `@atscript/vue-table` fulfills. Zero Vue dependency — React or Svelte adapters reuse everything here. ## Contents - [Filter model](#filter-model) - [Filter input format](#filter-input-format) - [Filters and Uniquery](#filters-and-uniquery) - [Date shortcuts](#date-shortcuts) - [Presets — wire types](#presets-wire-types) - [Presets — application types](#presets-application-types) - [Presets — id helpers](#presets-id-helpers) - [Presets — system presets](#presets-system-presets) - [Presets — dirty detection](#presets-dirty-detection) - [Presets — local draft](#presets-local-draft) - [Presets — HTTP clients](#presets-http-clients) - [Query builder](#query-builder) - [URL query bridge](#url-query-bridge) - [Selection](#selection) - [State contracts](#state-contracts) - [Window mode helpers](#window-mode-helpers) - [Column widths](#column-widths) - [Utilities](#utilities) ## Filter model ### `FilterConditionType` Canonical operator names used in filter conditions. `bw` ("between") is the range operator — there is no separate `between` alias. ```typescript type FilterConditionType = | "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "starts" | "ends" | "bw" | "null" | "notNull" | "regex"; ``` ### `FilterCondition` A single per-field condition. Operands live in a positional array — most operators read `value[0]`; `bw` reads `value[0]` (low) + `value[1]` (high); `null` / `notNull` ignore `value`. ```typescript interface FilterCondition { type: FilterConditionType; value: (string | number | boolean)[]; } ``` ### `FieldFilters` Per-field map of applied conditions. The framework treats absence and empty array as equivalent. ```typescript type FieldFilters = Record; ``` ### Condition helpers ```typescript function isFilled(cond: FilterCondition): boolean; function hasSecondValue(type: FilterConditionType): boolean; // `true` only for `"bw"` function isSimpleEq(cond: FilterCondition): boolean; function conditionLabel(type: FilterConditionType): string; function filledFilterCount(filters: FieldFilters): number; function filterTokenLabel( path: string, conditions: FilterCondition[], columnLabel?: string, ): string; const NULL_OPS: ReadonlySet; // { "null", "notNull" } ``` `isFilled` returns true when the condition has at least one operand bound (or uses a `NULL_OPS` operator). `filledFilterCount` is used by toolbar badges. `filterTokenLabel` summarises a field's filled conditions into a chip-friendly token. ### Conditions per type ```typescript type ColumnFilterType = "text" | "number" | "boolean" | "date" | "enum" | "ref"; /** Drops `"null"` / `"notNull"` when `nullable === false`. */ function conditionsForType( type: ColumnFilterType, nullable?: boolean, ): readonly FilterConditionType[]; /** Map a `ColumnDef.type` string to its filter-type bucket. */ function columnFilterType(columnType: string): ColumnFilterType; ``` `conditionsForType` returns the operator set the filter picker should offer for a given column type. ## Filter input format Tools for parsing and formatting filter values typed by the user. ```typescript /** Parses operator shorthand (`>100`, `*foo*`, `lo...hi`, ``, `!`, `/regex/`). */ function parseFilterInput( raw: string, columnType: ColumnFilterType, nullable?: boolean, ): FilterCondition | undefined; /** Format a condition back into the same operator-shorthand string. Round-trips with `parseFilterInput`. */ function formatFilterCondition(cond: FilterCondition): string; /** Default operator for a fresh filter picker: `"contains"` for `text`/`enum`/`ref`, else `"eq"`. */ function defaultCondition(columnType: ColumnFilterType): FilterConditionType; function escapeRegex(value: string): string; function unescapeRegex(value: string): string; ``` See [Filtering](/tables/filtering). ## Filters and Uniquery `filtersToUniqueryFilter` produces a `@uniqu/core`-compatible filter object the server consumes; `uniqueryFilterToFieldFilters` rebuilds `FieldFilters` from a stored Uniquery object (used when applying a preset or URL state). ```typescript function filtersToUniqueryFilter( filters: FieldFilters, columns: ColumnDef[], ): Record | undefined; function uniqueryFilterToFieldFilters( filter: Record | undefined, columns: ColumnDef[], ): FieldFilters; ``` ## Date shortcuts Pre-built date-range presets used by the date filter picker. Each shortcut produces an ISO date pair (`YYYY-MM-DD`) intended for a `bw` (between) condition. ```typescript interface DateShortcut { label: string; dates: [start: string, end: string]; } /** Returns the canonical shortcut list relative to `now` (defaults to current date). */ function dateShortcuts(now?: Date): DateShortcut[]; // → Last 7 Days, Last 30 Days, Month to Date, Last 90 Days, Last 6 Months, // Last 12 Months, Year to Date ``` ## Presets — wire types ### `PresetSnapshot` In-memory snapshot of table state for preset persistence. Dict-shaped — aspects mirror the runtime state, not the wire format. The wire form (entries-arrays for atscript validation) is `PresetSnapshotWire`. Each top-level key is opt-in: a key's presence claims that aspect; absent keys are left untouched on apply. ```typescript interface PresetSnapshot { columns?: { columnNames: string[]; /** Override-only diff against column defaults — never serialise the default itself. */ columnWidths?: Record; }; /** Displayed filter field paths (the visible-input list). */ filters?: string[]; /** Applied filter conditions keyed by field path. */ filterOps?: FieldFilters; sorters?: SortControl[]; itemsPerPage?: number; } ``` ### `PresetSnapshotWire`, `PresetColumnWidthEntry`, `PresetFilterOpEntry`, `PresetSorterEntry` Persisted wire form (what hits the DB / URL). Matches the `.as` model in [`@atscript/moost-ui-presets`](/api/moost-ui-presets). ### `PresetAspect`, `AspectMask`, `PRESET_ASPECTS`, `derivePresetAspects` ```typescript type PresetAspect = "columns" | "filters" | "filterOps" | "sorters" | "itemsPerPage"; type AspectMask = ReadonlySet | readonly PresetAspect[]; const PRESET_ASPECTS: readonly PresetAspect[]; function derivePresetAspects(snapshot: PresetSnapshot | PresetSnapshotWire): PresetAspect[]; ``` `derivePresetAspects(snapshot)` returns the list of aspects actually present on a snapshot — used by the controller when stamping `aspects[]` on a preset row. ### `toWireSnapshot(snapshot)` / `fromWireSnapshot(wire)` ```typescript function toWireSnapshot(snapshot: PresetSnapshot): PresetSnapshotWire; function fromWireSnapshot(wire: PresetSnapshotWire): PresetSnapshot; ``` Bidirectional bridge between the application shape (sets, ordered arrays, etc.) and the JSON wire shape. ## Presets — application types ```typescript interface AsPresetEntryRow { id: string; type: "preset" | "userConf" | "appConf"; app: string; tableKey?: string; user: string; userLabel?: string; public?: boolean; label?: string; publicLabel?: string; aspects?: PresetAspect[]; data: AsPresetEntryData; createdAt: number; updatedAt: number; } type AsPresetEntryData = PresetData | UserConfData | AppConfData; interface PresetData { label: string; content?: PresetSnapshotWire; } interface UserConfData { defaultPresetId?: string; favPresetIds?: string[]; } interface AppConfData { appearance?: "system" | "light" | "dark"; language?: string; timezone?: string; density?: "compact" | "cozy" | "comfortable"; dateFormat?: "iso" | "us" | "eu"; firstDayOfWeek?: 0 | 1 | 6; customJson?: string; } interface PresetCapabilities { /** Whether the current user may set `public: true` on presets in this scope. */ canPublish: boolean; /** Per-`(app, tableKey, user)` preset cap. */ presetLimit: number; /** Server-known opaque identity for the current user. */ userId: string; } interface PresetLimitReachedBody { code: "preset_limit_reached"; limit: number; count: number; } type AsPresetsErrorCode = | "preset_limit_reached" | "reserved_id" | "public_name_conflict" | "missing_scope" | "missing_id" | "invalid_type" | "type_immutable" | "identity_immutable" | "preset_not_found" | "publish_forbidden" | "action_unsupported"; ``` ## Presets — id helpers ```typescript const SYSTEM_PRESET_PREFIX: "sys:"; const USER_CONF_PREFIX: "uc:"; const APP_CONF_PREFIX: "ac:"; const RESERVED_ID_PREFIXES: readonly string[]; const STANDARD_PRESET_ID: "sys:standard"; function userConfId(user: string, app: string, tableKey: string): string; function appConfId(user: string, app: string): string; function isSystemPresetId(id: string): boolean; function normaliseSystemPresetId(id: string): string; ``` User-authored preset ids must NOT start with any prefix in `RESERVED_ID_PREFIXES` — the controller rejects writes that try. ## Presets — system presets System presets are read-only, framework-defined snapshots like "Standard view". Apps may also register custom synthetic presets via `useTable({ preset.systemPresets })`. ```typescript interface SystemPreset { id: string; // "sys:standard" or "sys:" label: string; snapshot: PresetSnapshot; aspects: PresetAspect[]; } interface SystemPresetInput { id: string; label: string; snapshot: Partial; } function resolveSystemPresets( inputs?: SystemPresetInput[], defaults?: { columnNames?: string[] }, ): SystemPreset[]; ``` `resolveSystemPresets` always emits a `sys:standard` entry as item 0 and follows it with the consumer-supplied list. ## Presets — dirty detection ```typescript function stableStringify(value: unknown): string; function isDirtyAgainst( current: PresetSnapshot, claimed: AspectMask, baseline: PresetSnapshot, ): boolean; ``` `stableStringify` produces deterministic JSON (sorted keys) so a snapshot's hash is stable across reorders. `isDirtyAgainst` returns true when at least one claimed aspect differs from baseline. ## Presets — local draft Opt-in localStorage overlay that captures the user's in-flight tweaks. Persists `columns`/`filters`/`sorters` and optionally `itemsPerPage`; `filterOps`, `searchTerm`, and pagination are never persisted. ```typescript interface PresetDraft { presetId: string; columns?: PresetSnapshot["columns"]; filters?: PresetSnapshot["filters"]; sorters?: PresetSnapshot["sorters"]; itemsPerPage?: number; } type DraftPersistedAspect = "columns" | "filters" | "sorters" | "itemsPerPage"; const DRAFT_PERSISTED_ASPECTS: readonly DraftPersistedAspect[]; function serializeDraft(draft: PresetDraft): string; function deserializeDraft(raw: string): PresetDraft | undefined; function isEmptyDraft(draft: PresetDraft): boolean; function draftMatchesPreset(draft: PresetDraft, snapshot: PresetSnapshot): boolean; ``` ## Presets — HTTP clients ### `PresetsClient` Framework-agnostic wrapper over `@atscript/db-client`'s `Client` for the [`AsPresetsController`](/api/moost-ui-presets) endpoint. Owns preset CRUD, userConf upserts, and capability probing. Stateless — every method is a fresh request. ```typescript interface PresetsClientConfig { /** Controller mount URL, e.g. `"/db/_presets"`. */ url: string; app: string; tableKey: string; /** Pre-built `Client` (auth-configured by the host). Wins over `clientFactory`. */ client?: Client; /** Builds a `Client` for the given URL. Defaults to `getDefaultClientFactory()`. */ clientFactory?: (url: string) => Client; /** Fetch impl for the `GET /capabilities` side-channel. Defaults to `globalThis.fetch`. */ fetch?: typeof globalThis.fetch; } interface PresetsListResult { presets: AsPresetEntryRow[]; /** type='userConf' row for this `(user, app, tableKey)`, or null. */ userConf: AsPresetEntryRow | null; /** `null` when the capabilities load failed; `undefined` when the call skipped capabilities. */ capabilities: PresetCapabilities | null | undefined; /** True when the controller responded 401/403 — UI silently hides. */ denied: boolean; } interface PresetsSaveAsOptions { public?: boolean; } interface PresetsSaveResult { id: string; } class PresetsClient { constructor(config: PresetsClientConfig); /** Lists owned + public preset rows AND the user's userConf row. `{ capabilities: false }` skips the capabilities fetch. */ list(opts?: { capabilities?: boolean }): Promise; /** `GET ${url}/capabilities?app=...&tableKey=...` — out-of-band of the CRUD plumbing. */ loadCapabilities(): Promise; /** Overwrite the active preset's content (label preserved). */ savePreset(id: string, label: string, snapshot: PresetSnapshot): Promise; /** Create a new preset row. Server stamps `user`, generates `id`, derives `aspects`. */ savePresetAs( label: string, snapshot: PresetSnapshot, opts?: PresetsSaveAsOptions, ): Promise; renamePreset(id: string, label: string): Promise; setPublic(id: string, value: boolean): Promise; deletePreset(id: string): Promise; /** Upsert the userConf row keyed on `${USER_CONF_PREFIX}${user}:${app}:${tableKey}`. */ upsertUserConf( existing: AsPresetEntryRow | null, patch: Partial, user?: string, ): Promise; } ``` ### `PresetsHttpError` / `isAuthError` ```typescript class PresetsHttpError extends Error { readonly status: number; constructor(status: number, message: string); } function isAuthError(err: unknown): boolean; // true for HTTP 401/403 on `ClientError` or `PresetsHttpError` ``` ### `AppPrefsClient` Loads the `appConf` row for `(user, app)` — app-wide preferences like density, locale, appearance. Independent of any table; devs may use this standalone. ```typescript interface AppPrefsClientConfig { /** Same controller URL the presets table is mounted on. */ url: string; app: string; client?: Client; clientFactory?: (url: string) => Client; } interface AppPrefsLoadResult { /** Full row (server-stamped id, user, timestamps), or `null`. */ row: AsPresetEntryRow | null; /** Convenience accessor for `row.data`, or `null`. */ prefs: AppConfData | null; /** True when the controller responded 401/403. */ denied: boolean; } class AppPrefsClient { constructor(config: AppPrefsClientConfig); load(): Promise; /** Upsert the appConf row. `existing` comes from a prior `load()` so the verb is correct. Returns the row id on success. */ save( existing: AsPresetEntryRow | null, patch: Partial, user?: string, ): Promise; } ``` See [Server-Side Presets](/tables/server-presets). ## Query builder ### `buildTableQuery(opts)` Pure function that translates table UI state into a `Uniquery` request. Merges user filters with force filters, prepends force sorters, projects `$select`, applies search and the `$actions` flag. ```typescript interface BuildTableQueryOptions { /** Paths of visible columns — emitted as `controls.$select`. */ visibleColumnPaths: string[]; /** User-configured sorters. */ sorters: SortControl[]; /** Always-applied sorters (prepended before user sorters). */ forceSorters?: SortControl[]; /** User-configured field filters. */ filters: FieldFilters; /** Always-applied Uniquery filter (AND'd with user filters). */ forceFilters?: FilterExpr; /** Full-text search term. */ search?: string; /** Search index name for `$search:`. */ searchIndex?: string; /** Set `controls.$actions = true` to receive per-row `$actions: string[]`. */ includeActions?: boolean; } function buildTableQuery(opts: BuildTableQueryOptions): Uniquery; // `Uniquery` from `@uniqu/core` ``` ### `mergeSorters` / `mergeFilters` Used by `buildTableQuery` to combine force + user controls; safe to call directly when composing presets / URL state. ```typescript function mergeSorters(force: SortControl[], user: SortControl[]): SortControl[]; /** AND-merge two `FilterExpr` trees, producing a wire shape that survives the Uniquery parser collapse. */ function mergeFilters(force?: FilterExpr, user?: FilterExpr): FilterExpr | undefined; ``` ## URL query bridge Two-way bridge between table state and URL query strings. Per-aspect gates (`filters` / `sorters` / `search` / `pagination`) are honoured symmetrically by encoder and decoder — pass the same `sync` config to both so the round-trip matches. ```typescript interface UrlQueryStateLike { filters: FieldFilters; sorters: SortControl[]; /** 1-based page number; `1` is the default and is omitted from the URL. */ page?: number; /** Per-page size; omitted from the URL when equal to `defaultItemsPerPage`. */ itemsPerPage?: number; /** Full-text search term; omitted when empty. */ searchTerm?: string; } interface UrlQueryStateSnapshot { filters: FieldFilters; sorters: SortControl[]; /** Raw `$skip` offset when present (no page math — recipients divide by their own `itemsPerPage`). */ skip?: number; searchTerm: string; } interface UrlQuerySync { /** `true` / `undefined` (default): all filters. `false` / `[]`: no filters in URL. `string[]`: allowlist of field paths. */ filters?: boolean | string[]; /** Same `boolean | string[]` semantics; allowlist matches `SortControl.field`. */ sorters?: boolean | string[]; /** Round-trip `searchTerm` as `$search`. Default `true`. */ search?: boolean; /** Round-trip pagination (`$skip`). Default `true`. */ pagination?: boolean; } interface UrlQueryDefaults { /** Consumer's `:limit` prop. Used to compute `$skip` and to suppress `$limit` writes. */ defaultItemsPerPage: number; /** Per-aspect sync gates. Omitted = full sync. */ sync?: UrlQuerySync; } interface UrlQueryParseOptions { /** Field paths the table knows about. Conditions on fields outside this set are silently dropped. */ knownFields?: Iterable; /** Per-aspect sync gates — must match the encoder's config. */ sync?: UrlQuerySync; } /** Tri-state resolution of a per-aspect gate: `"all"` (pass-through), `"none"` (off), or an allowlist Set. */ type AspectGate = "all" | "none" | Set; function resolveAspectGate(value: boolean | string[] | undefined): AspectGate; /** Returns the URL query string (no leading `?`). Returns `""` for the default view. */ function stateToUrlQueryString(state: UrlQueryStateLike, defaults: UrlQueryDefaults): string; /** Robust by design — schema drift never breaks the recipient's view. */ function urlQueryStringToState( urlString: string, opts?: UrlQueryParseOptions, ): UrlQueryStateSnapshot; ``` See [URL State](/tables/url-state). ## Selection ```typescript type SelectionMode = "none" | "single" | "multi"; function togglePk(set: unknown[], pk: unknown): unknown[]; function trimSelection(set: unknown[], pks: Iterable): unknown[]; function rowsToPks( rows: Record[], rowValueFn: (row: Record) => unknown, ): unknown[]; ``` `togglePk` returns a new array — never mutates. `trimSelection` drops any selected pk not present in `pks` (used when the row set narrows). ## State contracts `ConfigTab`, `TableStateData`, and `TableStateMethods` are the framework-agnostic contracts that the Vue `ReactiveTableState` implements. Reuse them when writing a React/Svelte port so the rest of `@atscript/ui-table` keeps working. ```typescript type ConfigTab = "columns" | "filters" | "sorters"; interface TableStateData { tableDef: TableDef | null; columnNames: string[]; allColumns: ColumnDef[]; columnWidths: ColumnWidthsMap; filterFields: string[]; filters: FieldFilters; sorters: SortControl[]; searchTerm: string; pagination: PaginationControl; selectedRows: unknown[]; results: Record[]; totalCount: number; // ... } interface TableStateMethods { query(): Promise; loadRange(from: number, to: number): Promise; setColumnNames(names: string[]): void; setColumnWidths(widths: ColumnWidthsMap): void; setFilters(filters: FieldFilters, fields?: string[]): void; setSorters(sorters: SortControl[]): void; setSearchTerm(value: string): void; setPagination(p: PaginationControl): void; setSelectedRows(rows: unknown[]): void; applyUrlQuery(urlString: string): void; // ... } ``` The full mutator list mirrors `ReactiveTableState` in [`@atscript/vue-table`](/api/vue-table) — see that page for the complete enumeration with reactive types. ## Window mode helpers Used by `` to compute page-aligned fetch ranges and merge incoming results into the universal cache. ```typescript const DEFAULT_ROW_HEIGHT_PX: number; interface PageAlignedBlock { /** 1-based page number, ready to pass to a server pagination param. */ page: number; /** Index of the first row in the block. */ firstIndex: number; } function pageAlignedBlocksFor(skip: number, limit: number, blockSize: number): PageAlignedBlock[]; function blockStartFor(absIdx: number, blockSize: number): number; function clampTopIndex(topIndex: number, totalCount: number, viewport: number): number; interface MergeResult { /* ... */ } function walkForwardAbsorb(results: MergeResult): void; function walkBackwardAbsorb(results: MergeResult): void; /** `"jump"` — viewport sits in uncached territory; centre a fetch around it. `"steady"` — fetch one block at the edge that's running out. */ type FetchPlanMode = "jump" | "steady"; interface FetchPlan { skip: number; limit: number; mode: FetchPlanMode; } interface PlanFetchArgs { top: number; viewport: number; totalCount: number; cache: Map; blockSize: number; /** Threshold below which a steady prefetch fires. Typically `blockSize / 4`. */ buffer: number; } function planFetch(args: PlanFetchArgs): FetchPlan | null; ``` `planFetch` is the centerpiece: given the viewport and the universal-cache state, it returns the single fetch the renderer should issue next (or `null` when the cache already satisfies the visible range). ## Column widths ```typescript interface ColumnWidthEntry { w: string; // current rendered width d: string; // computed default } type ColumnWidthsMap = Record; const MAX_DEFAULT_COLUMN_WIDTH_PX: number; function computeDefaultColumnWidth(column: ColumnDef): string; function reconcileColumnWidthDefaults( current: ColumnWidthsMap, columns: ColumnDef[], ): ColumnWidthsMap; ``` `computeDefaultColumnWidth` prefers `@ui.table.width`, falls back to type + `@expect.maxLen`, clamps to `MAX_DEFAULT_COLUMN_WIDTH_PX`. `reconcileColumnWidthDefaults` is called whenever the column set changes so widths stay populated for every visible column. ## Utilities ```typescript function debounce unknown>(fn: T, ms: number): T & { cancel(): void }; function arraysEqual(a: readonly T[], b: readonly T[]): boolean; function sameColumnSet(a: readonly string[], b: readonly string[]): boolean; function setsEqual(a: ReadonlySet, b: ReadonlySet): boolean; function sortersEqual(a: readonly SortControl[], b: readonly SortControl[]): boolean; type ColumnReorderPosition = "before" | "after"; /** Pure — returns a new array. Returns input unchanged when paths are missing or the move is a no-op. */ function reorderColumnNames( names: string[], fromPath: string, toPath: string, position: ColumnReorderPosition, ): string[]; ``` ## Cross-links - [Tables — Filtering](/tables/filtering) - [Tables — Sorting](/tables/sorting) - [Tables — Pagination & Virtualization](/tables/pagination) - [Tables — Presets](/tables/presets) - [Tables — URL State](/tables/url-state) - [@atscript/vue-table](/api/vue-table) — Vue-side `ReactiveTableState` - [@atscript/moost-ui-presets](/api/moost-ui-presets) — server endpoint --- URL: "ui.atscript.dev/api/vue-aooth" LLMS_URL: "ui.atscript.dev/api/vue-aooth.md" --- # @atscript/vue-aooth Custom Vue 3 form-field components for [Aooth](https://aooth.moost.org)-flavoured flows — multi-consent collection, password-policy display, QR-code enrolment, one-shot link/token sharing, and SSO / social-login provider picking. All five implement the `TAsComponentProps` contract from [`@atscript/vue-form`](/api/vue-form) and drop into `` via the `@ui.form.component` annotation. Typically paired with [`@atscript/vue-wf`](/api/vue-wf) when the consents, policies, TOTP URIs, or magic links arrive as part of a workflow `formContext`. ## Contents - [Subpath](#subpath) - [Component — AsConsentArray](#component-asconsentarray) - [Component — AsPasswordRules](#component-aspasswordrules) - [Component — AsQrCode](#component-asqrcode) - [Component — AsCopy](#component-ascopy) - [Component — AsSsoProviders](#component-asssoproviders) - [Custom-component patterns used](#custom-component-patterns-used) ## Subpath ```typescript // barrel import { AsConsentArray, AsCopy, AsPasswordRules, AsQrCode, AsSsoProviders, } from "@atscript/vue-aooth"; // per-component (for granular bundling) import AsConsentArray from "@atscript/vue-aooth/as-consent-array"; import AsCopy from "@atscript/vue-aooth/as-copy"; import AsPasswordRules from "@atscript/vue-aooth/as-password-rules"; import AsQrCode from "@atscript/vue-aooth/as-qr-code"; import AsSsoProviders from "@atscript/vue-aooth/as-sso-providers"; // descriptor types (prop payload item shapes) import type { AsConsentArrayItem, AsPasswordRulesPolicy, AsSsoProvider } from "@atscript/vue-aooth"; ``` None of the five are auto-resolved — all are Tier-2 swap targets. Register them by name in `` and tag the field with `@ui.form.component`. The narrative guide with end-to-end examples lives at [Forms · Aooth components](/forms/aooth-components). The descriptor types behind the custom props are exported too — use them to type the lists you place on `formContext`: - `AsConsentArrayItem` — one `pendingConsents` entry for [`AsConsentArray`](#component-asconsentarray). - `AsPasswordRulesPolicy` — one `policies` entry for [`AsPasswordRules`](#component-aspasswordrules). - `AsSsoProvider` — one `providers` entry for [`AsSsoProviders`](#component-asssoproviders). ## Component — AsConsentArray Multi-consent checkbox group. The bound value is a `string[]` containing the ids of every checked consent. When the source list is empty, the entire field is suppressed — useful for "show the legal block only when the backend says it has new consents to collect". ### Props Extends `TAsComponentProps` with one custom prop fed via `@ui.form.fn.attr`: ```typescript interface AsConsentArrayItem { /** Value committed to the bound `string[]` when checked. */ id: string; /** Checkbox label. Supports `[text](url)` markdown links for http(s)/mailto URLs. */ text: string; /** * Non-empty string ⇒ mandatory consent; the string is the surfaced * error message. Empty / undefined ⇒ optional. */ required?: string; } interface AsConsentArrayProps extends TAsComponentProps { pendingConsents?: AsConsentArrayItem[]; } ``` ### Behaviour - One row per `pendingConsents` entry. The whole shell is hidden when `pendingConsents` is empty or omitted. - A required consent renders an asterisk marker on its label. Missing required consents surface their `required` message inline beneath the row (the shell-level error footer is suppressed to avoid duplication). - Consent text is parsed for markdown links `[label](url)`. Only `http://`, `https://`, and `mailto:` URLs render as anchors; any other scheme falls back to the verbatim source string so dangerous schemes (e.g. `javascript:`) can't sneak through. - Validation runs through `useAsField` — registered alongside the field-state entry `` already provides, so all rules run at submit time and external errors propagate. - The committed array preserves insertion order — checks append, unchecks remove. The display order follows `pendingConsents`. ### How to wire ```atscript // auth-step.as export interface SignupStep { @meta.label 'Email' @meta.required 'Email required' email: string.email @ui.form.component 'AsConsentArray' @ui.form.fn.attr 'pendingConsents', '(_v, _d, ctx) => ctx.pendingConsents' consents: string[] } ``` The consents list comes from the form's `formContext` — typically supplied by the backend through ``'s server round-trip, so the same field schema works whether your backend ships one consent or ten. ```vue ``` ## Component — AsPasswordRules Display-only readout of password-policy fulfilment. Pair it with a sibling password field; the component re-evaluates on every password keystroke and toggles each row's met / unmet state. ### Props `TAsComponentProps` (no value binding — the field is display-only, conventionally typed as `ui.paragraph` so the form treats it as non-data chrome) plus: ```typescript interface AsPasswordRulesPolicy { /** Function-string evaluated through @atscript/ui-fns' compileFieldFn. */ rule: string; /** Plain label shown to the user. */ description?: string; /** Backend-supplied wording reserved for future submit-failure surfacing. */ errorMessage?: string; } interface AsPasswordRulesProps extends TAsComponentProps { policies?: AsPasswordRulesPolicy[]; /** Current password value — typically sourced from a sibling field. */ password?: string; } ``` ### Behaviour - Each policy row reads `data-passed="true" | "false"` — style the met / unmet glyph and text through descendant selectors on `.as-password-rules-row`. - The `rule` string is compiled with `compileFieldFn` from `@atscript/ui-fns`, sharing the same FNPool cache the framework uses for `@ui.form.fn.*` annotations. Compiled rules are invoked positionally — the rule's first argument receives the current password. - Empty password ⇒ all rows render as unpassed, regardless of what a no-op rule would technically return. This avoids misleading "all good!" cues at the empty-input state. - A broken / throwing rule is treated as unpassed and logged once per distinct rule source (no console flooding on every keystroke). ### How to wire ```atscript // password-step.as export interface PasswordStep { @meta.label 'New password' @meta.required 'Password required' @ui.form.type 'password' newPassword: string @ui.form.type 'paragraph' @ui.form.component 'AsPasswordRules' @ui.form.fn.attr 'policies', '(_v, _d, ctx) => ctx.passwordPolicies' @ui.form.fn.attr 'password', '(_, data) => data.newPassword' passwordHints: string } ``` `policies` is sourced from `formContext.passwordPolicies` (server-supplied, typically arriving via ``); `password` watches the live sibling value via `(_, data) => data.newPassword`. The compiled `rule` strings receive that password as their first argument. ```vue ``` ## Component — AsQrCode Phantom field that renders an SVG QR code from any string. Primarily used for TOTP enrolment: for `otpauth://` URIs it also surfaces the `?secret=` query param beneath the SVG as a manual-entry fallback for users who can't scan. ### Props `TAsComponentProps` plus: ```typescript interface AsQrCodeProps extends TAsComponentProps { /** SVG width in px. Default 192. */ size?: number; /** QR error-correction level. Default "M". */ errorCorrection?: "L" | "M" | "Q" | "H"; /** * Show the parsed `?secret=` fallback for `otpauth://` values. * Default true — set false when surfacing the secret defeats the * threat model (e.g. screen-sharing demos). */ manualSecret?: boolean; } ``` ### Behaviour - The value is read as `props.value ?? props.model?.value`. Phantom registrations (`ui.paragraph` + `@ui.form.fn.value`) push through `props.value`; data-bound fields use `model.value`. - `qrcode` is an **optional peer dependency** — install it in the consumer app only for flows that use `AsQrCode`. The module is dynamic-imported, so apps without the dep pay no bundle cost. - For `otpauth://` URIs the parsed secret renders as plain text beneath the SVG unless `:manualSecret="false"`. ```bash npm install qrcode ``` ### How to wire ```atscript // enrol-step.as export interface EnrolStep { @meta.label 'Scan with your authenticator app' @ui.form.fn.value '(v, data, ctx) => ctx.totpUri' @ui.form.component 'AsQrCode' totpUri: ui.paragraph } ``` `totpUri` is supplied from `formContext` (server-side via `@wf.context.pass 'totpUri'` on a workflow form). See [Phantom vs data-bound](/forms/aooth-components#phantom-vs-data-bound) for the full pattern. ## Component — AsCopy Phantom field that surfaces a one-shot string with a read-only input and a Copy button. Click writes through `navigator.clipboard.writeText` and swaps the button label to "Copied" for ~1.5 s; focusing the input selects the full value for fallback Ctrl+C / Cmd+C copy. ### Props `TAsComponentProps` plus: ```typescript interface AsCopyProps extends TAsComponentProps { /** Idle button label. Default "Copy". */ copyLabel?: string; /** Post-copy button label. Default "Copied". */ copiedLabel?: string; } ``` ### Behaviour - Value is read as `props.value ?? props.model?.value` — same phantom + data-bound dual mode as `AsQrCode`. - Failed clipboard writes (no permission, no API) flip the button back and surface "Copy failed — select and copy manually" through the field's error slot. ### How to wire ```atscript // share-step.as export interface ShareStep { @meta.label 'Magic link' @ui.form.fn.value '(v, data, ctx) => ctx.magicLink' @ui.form.component 'AsCopy' magicLink: ui.paragraph } ``` Use it for magic links, share tokens, generated identifiers — any one-off value the user needs to paste somewhere else. ## Component — AsSsoProviders SSO / social-login provider picker. Renders providers as a main stack of full-width buttons; any provider flagged `secondary: true` drops below an "or" divider as a compact chip. Each provider button is a one-click action — clicking selects the provider (writes its id to the bound model) _and_ fires the field's `@ui.form.action` in a single click, so there is no separate submit button. The bound value is the selected provider id. When the source list is empty the entire field is suppressed. ### Props `TAsComponentProps` with one custom prop fed via `@ui.form.attr` / `@ui.form.fn.attr`: ```typescript interface AsSsoProvider { /** Committed to the bound model (`model.value = id`) on click and carried by the fired form action. */ id: string; /** * Rendered VERBATIM. The backend owns the full display string (e.g. * "Continue with Google" for a main-stack button, "Discord" for a * secondary chip). The component never composes a "Continue with {name}" prefix. */ text: string; /** * Optional CSS class painting the brand glyph (e.g. `i-logos-google-icon`). * Applied as-is; the consumer owns the icon collection / safelist coverage, * same contract as `prefixIcon`. */ icon?: string; /** * `true` ⇒ renders as a compact chip below the "or" divider; omitted / false * ⇒ renders as a full-width button in the main stack (the default). */ secondary?: boolean; } interface AsSsoProvidersProps extends TAsComponentProps { providers?: AsSsoProvider[]; } ``` ### Behaviour - One-click contract: a click sets `model.value = provider.id` then emits the form `action` declared by `@ui.form.action` on the field. `` surfaces it as `@action(name, data)` with the selected provider already in `data`. Without a wired `@ui.form.action` the click still commits the id but emits nothing. - Providers default to the prominent main stack (full-width buttons); only `secondary: true` opts into the compact chip group below the "or" divider. The "or" divider renders only when _both_ groups are non-empty — a lone group never dangles a divider. - `text` is rendered verbatim. The component never prefixes "Continue with …"; the backend supplies the complete string per provider. - `icon` is applied as-is. Icon classes referenced from `.as` files aren't seen by the static class extractor, so safelist whatever collection you ship. - Empty or missing `providers` hides the whole field — the same render-only-when-the-backend-supplied-providers pattern as `AsConsentArray`. This also guards the transient first render before `@ui.form.fn.attr` resolves. - No separate submit / "Continue" control: the component renders chromeless and suppresses the shell's footer action link, since the provider buttons themselves _are_ the action. ### How to wire ```atscript // login-step.as export interface LoginStep { @ui.form.component 'AsSsoProviders' @ui.form.action 'sso', 'Continue' @ui.form.fn.attr 'providers', '(_v, _d, ctx) => ctx.ssoProviders' ssoProvider?: string } ``` The provider list comes from the form's `formContext` — typically supplied by the backend through ``'s server round-trip, so the same field schema works whether you offer one provider or ten. In a workflow form, add `@wf.action.withData 'sso'` on the same field so the chosen `ssoProvider` is submitted with the action (the same mechanism `forgotPassword` uses to carry a typed value). An optional `ssoProvider?` won't block a password login that submits without one. ```vue ``` ## Custom-component patterns used These components are production examples of the patterns documented in [Custom Field Components](/forms/custom-components): - **`AsConsentArray`** demonstrates the "`useAsField` inside a custom component" pattern — registering an extra field-state rule alongside the one `` already provides, so per-item required validation runs at submit alongside the schema-level pipeline. - **`AsPasswordRules`** demonstrates the "re-use `compileFieldFn` for fn-string arrays" pattern — evaluating consumer-supplied policy rule strings through the framework's shared FNPool instead of allocating a private one. - **`AsQrCode`** and **`AsCopy`** demonstrate the **phantom display field** pattern — `ui.paragraph` + `@ui.form.fn.value` reading from `formContext`, so a server-supplied value renders without ever entering the bound data. - **`AsSsoProviders`** demonstrates emitting a **form action from a custom component** — one-click select + fire `@ui.form.action` in a single click — together with the `@wf.action.withData` data-carrying-action pattern for workflow forms. ## Cross-links - [@atscript/vue-form](/api/vue-form) — the underlying form renderer - [@atscript/vue-wf](/api/vue-wf) — pairs naturally for Aooth-driven workflow flows - [@atscript/ui-fns](/api/ui-fns) — supplies `compileFieldFn` - [Aooth](https://aooth.moost.org) — auth + RBAC for the Moost / atscript ecosystem --- URL: "ui.atscript.dev/api/vue-form" LLMS_URL: "ui.atscript.dev/api/vue-form.md" --- # @atscript/vue-form Vue 3 form library backed by `@atscript/ui`. Three tiers of components, ~30 composables, two factories, and one auto-resolver entry. Every Tier-1 component you tag (``, ``, ``) wraps a public composable, so apps can drop the renderer and rebuild their own form root without losing functionality. ## Contents - [Tier 1 — Primary components](#tier-1-primary-components) - [Tier 2 — Default field components](#tier-2-default-field-components) - [Factories](#factories) - [Composables — form / state](#composables-form-state) - [Composables — field & structure](#composables-field-structure) - [Composables — value help / dropdown](#composables-value-help-dropdown) - [Composables — choreography](#composables-choreography) - [Composables — date / number / decimal](#composables-date-number-decimal) - [Composables — context / utility](#composables-context-utility) - [Component prop & emit types](#component-prop-emit-types) - [Other types](#other-types) ## Tier 1 — Primary components Imported automatically by `AsResolver()` (see [@atscript/ui-styles](/api/ui-styles)). ### `AsForm` Top-level form renderer. **Props** (subset — see `useAsForm` for full reactive contract): ```typescript interface AsFormProps { def: FormDef; formData?: { value: unknown }; formContext?: unknown; types: TAsTypeComponents; components?: Record; errors?: Record; firstValidation?: "on-change" | "touched-on-blur" | "on-blur" | "on-submit"; hideRootTitle?: boolean; hideSubmit?: boolean; loading?: boolean; clientFactory?: ClientFactory; } ``` **Emits**: - `submit(data)` — validation passed, data ready to ship. - `error(errors)` — validation failed; payload is `{ path, message }[]`. - `action(name, data)` — phantom `` button dispatched. - `unsupportedAction(name, data)` — action name not declared on the form's type. - `change(type, path, value, data)` — granular per-field change; `type` is `TAsChangeType`. **Slots**: `form.header`, `form.before`, `form.after`, `form.error`, `form.submit`, `form.footer`, `form.loading`. Every slot receives the unified `slotProps` bag (see [`useAsForm` return](#useasform-options)); `form.error` also gets `message` / `dismiss`, `form.submit` also gets `text`. See [Forms — Slots & the slotProps bag](/forms/customization#slots-the-slotprops-bag). ### `AsField` Renders a single field given a `FormFieldDef`. Use when laying out form fields manually instead of letting `AsForm` iterate. ```typescript interface AsFieldProps { field: FormFieldDef; /** External validation error to surface on this field. */ error?: string; /** Wires the array-item remove affordance when rendered inside an array. */ onRemove?: () => void; canRemove?: boolean; removeLabel?: string; /** Index of this field within a parent array — drives the `#N` suffix on labels. */ arrayIndex?: number; } ``` ### `AsIterator` Iterates the fields of a `FormDef` and renders each through `AsField`. Used by `AsObject` for nested objects and exposed for advanced compositions. ```typescript interface AsIteratorProps { def: FormDef; /** Optional dotted segment to prepend to every child field's path. */ pathPrefix?: string; onRemove?: () => void; canRemove?: boolean; removeLabel?: string; } ``` ### `AsCollapsible` Section chrome with a collapsible `` header plus `actions` / `badges` / `title-extras` / `body` / `empty` slots. `AsObject` / `AsArray` / `AsTuple` render nested structures through it. Public so a custom `@ui.form.component` can own its own section header and inject header-row actions. See [Collapsible Sections](/forms/collapsible-sections) for the narrative. ```typescript interface AsCollapsibleProps { /** Header heading text. */ title?: string; /** Sub-text under the title. */ description?: string; /** REQUIRED. ≤0 → root (no chrome), odd → section, even → island. */ level: number; /** When true and not enabled, the `empty` slot renders instead of the section. Default `false`. */ optional?: boolean; /** Gates the `empty` slot — shown when `optional && !optionalEnabled`. Default `false`. */ optionalEnabled?: boolean; /** Unique key registered with the sections store (expand/collapse + auto-open). */ path: string; /** Renders an alert row at the top of the body. */ error?: string; /** Open on first mount. Defaults to `false`. */ defaultOpen?: boolean; /** Hides via `v-show` — stays mounted and registered. */ hidden?: boolean; /** Appends a `#N` suffix to the title. */ arrayIndex?: number; } ``` **Slots**: `title-extras`, `badges`, `actions` (all render inside ``), `body`, `empty`. **Expose**: `runAndFocus(fn)` / `runAndFocusNew(fn)` — open-then-focus helpers (focus first / first-new focusable inside after toggle). Used by array/optional add-flows; rarely needed directly. ## Tier 2 — Default field components Default swap targets for the `types` map. Importable both via the package root and the kebab subpath used by `AsResolver`. ```typescript import { AsInput, AsSelect } from "@atscript/vue-form"; // or, for tree-shake-friendly single imports import AsInput from "@atscript/vue-form/as-input"; ``` Every component implements `TAsComponentProps`. The columns below list the `@ui.type` value that maps to each component in `createDefaultTypes()`. | Component | Default `type` keys | Notes | | --------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------- | | `AsFieldShell` | (all) | Chrome wrapper — title, description, error, hint, prefix/suffix. Used internally by every other default. | | `AsInput` | `text`, `textarea`, `password` | Plain string input. | | `AsNumber` | `number` | Integer input with prefix/suffix, currency-aware. | | `AsDecimal` | `decimal` | Decimal-string input with `precisionScale` enforcement. | | `AsSelect` | `select` | `