atscript-ui-tables
Install
npx skills add moostjs/atscript-ui # installs all atscript-ui skills (this one + general + forms + wf + styles)
npx skills add moostjs/atscript # sibling — .as language
npx skills add moostjs/atscript-db # sibling — moost-db backs most tables
pnpm add @atscript/core @atscript/typescript @atscript/ui @atscript/ui-table @atscript/vue-table vue
pnpm add @atscript/vue-form # required peer (cell dispatch, action forms)
pnpm add @atscript/ui-fns # opt-in: dynamic @ui.table.fn.*
pnpm add @atscript/db-client # moost-db browser client
pnpm add @atscript/moost-ui-presets # server-side preset persistence (optional)
Quick start
// src/product.as
@db.table 'products'
@db.depth.limit 0
export interface Product {
@meta.id @db.default.increment
id: number
@meta.label 'SKU'
@ui.table.width '8em'
sku: string
@meta.label 'Name'
@db.index.fulltext 'name_fts'
name: string
@meta.label 'Price'
@db.column.precision 2
@db.amount.currency 'USD'
price: number
@meta.label 'In stock'
inStock: boolean
}
<script setup lang="ts">
import { createDefaultCellTypes } from "@atscript/vue-table";
const types = createDefaultCellTypes();
</script>
<template>
<AsTableRoot url="/api/db/tables/products" :types="types" :limit="20">
<AsTableActions />
<AsFilters />
<AsTable :column-menu="{ sort: true, filters: true, hide: true, resetWidth: true }" />
</AsTableRoot>
</template>
Replace url= with :query-fn="..." for a custom backend. See query.md.
Invariants
| # |
Rule |
| 1 |
Programmatic state changes are picked up by the root watcher automatically. Each mutator on state (e.g. setFieldFilter, setColumnWidth, setSearchTerm) touches exactly one entity; the root watcher on [filters, sorters, pagination, columnNames] schedules the next query. Calling state.query() to "apply" a programmatic change will double-fetch and skip debouncing. |
| 2 |
state.query() is reserved for user-initiated refresh. Wire it to a refresh button, pull-to-refresh, or devtools — not to apply programmatic state changes — except query({ silent: true }) for timer-driven live refresh (invariant 14). |
| 3 |
filterFields (display) and filters (applied) are independent. Hiding a filter input via filterFields does NOT clear filters[path]. Clearing filters[path] does NOT hide the input. When building your own dialogs, write the new arrays directly — the root watcher reconciles. Cleanup loops that delete filters entries because filterFields shrank will fight the model and cause double-fetches. |
| 4 |
@ui.table.type is for built-in renderer ids only. Built-ins: text, number, boolean, date, datetime, relative, array, object, union, enum, ref. Custom cells use @ui.table.component + the :components map. |
| 5 |
Per-field filter conditions: inclusions OR-merge, exclusions AND-merge. Inclusion ops (eq, contains, starts, ends, gt, gte, lt, lte, bw, regex, null) → OR within field. Exclusion ops (ne, notNull) → AND within field. Across fields → AND. See filtersToUniqueryFilter in ui-table. |
| 6 |
Force filters / sorters AND-merge; user can't remove them. Pass via useTable({ forceFilters, forceSorters }); they always prepend and dedupe by field — user mutations to filters / sorters never override them. |
| 7 |
Window mode fetches in fixed-size blocks. <AsWindowTable> issues block-aligned fetches (default 100 rows) as the viewport scrolls; tune via <AsTableRoot :block-size> and :drag-release-debounce-ms (higher = fewer fetches during fast scroll). Custom virtual renderers read rows via state.dataAt(absIndex) / state.loadingAt(absIndex) / state.errorAt(absIndex) — see query.md. |
| 8 |
Presets opt-in per-aspect. A PresetSnapshot carries any subset of columns, filters, filterOps, sorters, itemsPerPage. Absent keys leave that slice untouched on apply. Use toWireSnapshot / fromWireSnapshot when crossing the network — never send the raw runtime dict. |
| 9 |
Reserved preset id prefixes: sys: (system, client-only, never persisted), uc: (user config, deterministic id uc:<user>:<app>:<tableKey>), ac: (app config, deterministic ac:<user>:<app>). Client writes to sys:* are rejected by the server controller. |
| 10 |
Server preset read gate: user = current OR (type='preset' AND public=true). Once-public-always-public — revoking publish permission doesn't unpublish existing rows. |
| 11 |
/meta response carries preferredId on every row-returning read. moost-db widens $select automatically; cells/actions/refs can rely on identity. Aggregate ($groupBy) and $count responses are NOT widened — see atscript-db skill, invariant 10. |
| 12 |
:controls takes only your overrides. Every dispatch site falls back internally (controls[key] ?? default), so passing createDefaultControls() wholesale is redundant AND opts the lazy dialogs into eager bundling+mounting. rowActions is not seeded — the __actions column resolves controls.rowActions ?? types.__actions ?? AsRowActions. |
| 13 |
$sort overrides search relevance — opt into ignoreSortersWhenSearched to preserve it. A preset/v-model/header sort emitted alongside $search replaces relevance ranking on a scored backend (Atlas $search), so search returns the right row set in browse order. <AsTableRoot :ignore-sorters-when-searched> (default false) suppresses user sorters at query time while searching; forceSorters still emit. The flag is a model — sorting mid-search flips it off for the session; a new search resets it. Only for relevance-ranked backends. See sorting-pagination.md. |
| 14 |
query({ silent: true }) is the sanctioned timer-driven live-refresh path. Re-runs the current query (live filters/sorters/search/pagination/$actions) with no querying flip — no spinner, no skeletons, no query overlay. Loud-wins coalescing: a real (loud) query landing the same tick still shows the spinner. On failure, rows/totalCount/error state are left untouched — never blanks or toasts the grid. Preserves scroll position. Rides on keep-rows-until-settle — a stable contract where every query, silent or not, keeps prior rows visible until the response settles, then swaps results + totalCount atomically. See query.md. |
Key imports
// Tier 1 — primary (auto-imported by AsResolver)
import {
AsTableRoot,
AsTable,
AsWindowTable,
AsTableActions,
AsFilters,
AsPresetPicker,
} from "@atscript/vue-table";
// Tier 2 — defaults (swap targets)
import {
// cells
AsCellArray,
AsCellDate,
AsCellJson,
AsCellNumber,
AsCellUnion,
AsTableCellValue,
// dialogs
AsConfigDialog,
AsFilterDialog,
AsPresetDialog,
AsConfirmDialog,
// filter ui + headers + rows
AsFilterField,
AsFilterInput,
AsTableHeaderCell,
AsRowActions,
AsColumnMenu,
} from "@atscript/vue-table";
// AsActionFormDialog is on a dedicated subpath — it pulls in @atscript/vue-form,
// so the table root lazy-mounts it. Import this only to override / eager-load.
import AsActionFormDialog from "@atscript/vue-table/as-action-form-dialog";
// Composables
import {
useTable,
useTableContext,
useTableContextOptional,
createTableState,
createStaticTableState,
useTableSelection,
useTableNavBridge,
useTableFilter,
useTableSearch,
useTableActions,
useTableUrlQuery,
useAppPrefs,
usePresets,
useLocalDraft,
useCellLocale,
provideCellLocale,
useTableComponent,
} from "@atscript/vue-table";
// Factories
import { createDefaultControls, createDefaultCellTypes } from "@atscript/vue-table";
// Types (re-exported)
import type {
TAsTableControls,
TAsCellTypeComponents,
ReactiveTableState,
ColumnMenuConfig,
ConfigTab,
TableActionsState,
ActionResult,
} from "@atscript/vue-table";
// Framework-agnostic table model (filter / preset / query primitives)
import {
FilterCondition,
FieldFilters,
filtersToUniqueryFilter,
PresetSnapshot,
toWireSnapshot,
fromWireSnapshot,
buildTableQuery,
mergeFilters,
mergeSorters,
stateToUrlQueryString,
urlQueryStringToState,
} from "@atscript/ui-table";
// Server-side preset controller (Moost)
import { AsPresetsController, AsPresetEntry } from "@atscript/moost-ui-presets";
References — load only what's needed
| Domain |
File |
When |
| First contact |
getting-started.md |
Install matrix, <AsTableRoot> props, the default :types + :controls maps, slot binding contract |
| Query / data wiring |
query.md |
url= (moost-db) vs queryFn (custom), buildTableQuery Uniquery assembly, force filters/sorters, meta endpoint, mutators-are-pure principle in detail |
| Filtering |
filtering.md |
Filter model (FieldFilters / FilterCondition / 13 condition types), OR/AND semantics, filtersToUniqueryFilter translation, <AsFilters> / <AsFilterField> / <AsFilterDialog>, value-help inside filter dialogs |
| Sorting + pagination |
sorting-pagination.md |
Sort model + multi-sort, header click semantics, <AsConfigDialog> sorters tab, paginated <AsTable> vs virtualized <AsWindowTable>, block-aligned fetching, dragReleaseDebounceMs tuning |
| Cells |
cells.md |
Built-in cell components + default type map, provideCellLocale (language + timezone), custom cells via @ui.table.component + :components, slot API (#header-<path>, #cell-<path>, #empty, #query-loading, #error), per-cell styling via @ui.table.{classes,styles,attr} |
| State persistence |
state-persistence.md |
<AsConfigDialog> tabs (columns/sorters/filters), useTableUrlQuery (router two-way bind), client presets (PresetSnapshot, useLocalDraft, usePresets, useAppPrefs, <AsPresetPicker>, system/user/public, dateShortcuts), server presets via AsPresetsController |
| Actions + selection |
actions-selection.md |
Row / table actions on the .as type, <AsActionFormDialog> (action input form via vue-form), state.selectedRows (Set<PK>), togglePk / trimSelection / rowsToPks, state.actions.invoke(action, pk?, opts?), the __actions synthetic column; navigate-action links (navigateHrefFor, resolveHref, anchor vs button, native new-tab silent to @action) |
| Customization |
customization.md |
When swapping table chrome: per-column / state slots (#cell-*, #header-*, #empty, #error, #last-row), :types / :components cell swap, replacing dialogs via :controls (and which control keys actually dispatch), custom row-actions cell, rendering without a header row (:headless) |
| Edit form + OCC |
edit-form-occ.md |
When building a row-edit <AsForm> against an OCC table: @db.column.version, meta.versionColumn → createFormDef, catching VersionMismatchError / currentVersion on submit |
| Annotation catalog |
annotations.md |
When writing or looking up any @ui.table.* / @ui.table.fn.* / @ui.dict.* annotation — args, constants, defaults (SSOT lives in the root atscript-ui skill) |
| Bundle optimization |
bundle-optimization.md |
When asked about table bundle size: lazy dialog latches, controls.X flipping a dialog eager, AsActionFormDialog pulling in vue-form, shedding unused chrome CSS via excludeComponents (SSOT lives in the root atscript-ui skill) |
Customization
Full swap-surface reference (slots, maps, dispatched control keys): customization.md.
Tables expose three swap surfaces, layered on the tier model:
- Tier 1 —
<AsTableRoot>, <AsTable>, <AsWindowTable>, <AsFilters>, <AsTableActions>, <AsPresetPicker> are the integration surface. Build with useTable / createTableState directly if you need a custom shell.
- Tier 2 — default cells (
AsCellArray, AsCellDate, AsCellJson, AsCellNumber, AsCellUnion, AsTableCellValue) and default dialogs (AsConfigDialog, AsFilterDialog, AsPresetDialog, AsConfirmDialog, AsRowActions, AsColumnMenu, AsTableHeaderCell). These are what you swap.
- Tier 3 — internal composition (header cells, virtualizer pieces, fields selector, sorter config). Not tagged directly; styles ride with the defaults that use them.
Swap a built-in cell renderer (:types)
:types maps built-in cell ids (per invariant 4: text, number, boolean, date, datetime, relative, array, object, union, enum, ref) to a component:
<script setup lang="ts">
import { createDefaultCellTypes } from "@atscript/vue-table";
import MyDateCell from "./MyDateCell.vue";
const types = { ...createDefaultCellTypes(), date: MyDateCell, datetime: MyDateCell };
</script>
<template>
<AsTableRoot url="/api/db/tables/products" :types="types" />
</template>
Swap a specific cell (:components + @ui.table.component)
For column-specific cells, opt in on the .as type and supply the named component:
@ui.table.component 'price-tag'
price: number
<script setup lang="ts">
import PriceTag from "./PriceTag.vue";
const components = { "price-tag": PriceTag };
</script>
<template>
<AsTableRoot url="..." :components="components" />
</template>
Wrap a cell in useCellLocale / provideCellLocale if it needs locale + timezone from useAppPrefs.
Swap a dialog (controls.*)
The toolbar, config, filter, and preset dialogs are swappable through controls. Pass only the entries you replace — spreading createDefaultControls() defeats dialog lazy-loading (see invariant 12):
<script setup lang="ts">
import MyFilterDialog from "./MyFilterDialog.vue";
const controls = { filterDialog: MyFilterDialog };
</script>
<template>
<AsTableRoot url="..." :controls="controls" />
</template>
Custom dialogs just write the new arrays back to state.filterFields / state.filters / state.sorters / state.columnNames — the root watcher reconciles and re-queries (see invariants 1–3 and query.md). Don't call state.query() or run cleanup loops mirroring display state into applied state; both fight the watcher.
Style consequence
Replacing AsFilterDialog with a custom dialog that doesn't tag the as-filter-* shortcuts drops those classes out of your bundle automatically. Keep the default whenever its chrome fits — you'll spend less time on style maintenance. For granular opt-out, atscript-ui-styles ships per-domain shortcut groups (tableShortcuts, formShortcuts, …) you can compose narrower than the default allShortcuts.
See also
Reference docs: https://ui.atscript.dev/tables/. Source: https://github.com/moostjs/atscript-ui.
1---2name: atscript-ui-tables3description: Render searchable, filterable, sortable, paginated, or virtualized tables from `.as` annotated types with `@atscript/vue-table` + `@atscript/ui-table`. Use when working with `<AsTableRoot>`, `<AsTable>`, `<AsWindowTable>`, `<AsFilters>`, `<AsPresetPicker>`, `<AsConfigDialog>`, or `<AsTableActions>`; when writing `@ui.table.*` / `@ui.table.fn.*` / `@ui.dict.*` annotations; when wiring a custom `queryFn` or a `moost-db` URL; when building custom cells via `@ui.table.component` + `:components` (and `provideCellLocale`); when persisting state to the URL (`useTableUrlQuery`) or to presets (`usePresets`, `useLocalDraft`, `useAppPrefs`, `AsPresetPicker`), including server-side via `AsPresetsController` from `@atscript/moost-ui-presets`; when wiring row/table actions (`AsActionFormDialog`) and selection (`togglePk`, `trimSelection`); or when tuning virtualization and block-aligned fetching. Out of scope: forms (use `atscript-ui-forms`), workflow forms (use `atscript-ui-wf`), styling (use `atscript-ui-styles`).4---56# atscript-ui-tables78## Install910```bash11npx skills add moostjs/atscript-ui # installs all atscript-ui skills (this one + general + forms + wf + styles)12npx skills add moostjs/atscript # sibling — .as language13npx skills add moostjs/atscript-db # sibling — moost-db backs most tables14```1516```bash17pnpm add @atscript/core @atscript/typescript @atscript/ui @atscript/ui-table @atscript/vue-table vue18pnpm add @atscript/vue-form # required peer (cell dispatch, action forms)19pnpm add @atscript/ui-fns # opt-in: dynamic @ui.table.fn.*20pnpm add @atscript/db-client # moost-db browser client21pnpm add @atscript/moost-ui-presets # server-side preset persistence (optional)22```2324## Quick start2526```atscript27// src/product.as28@db.table 'products'29@db.depth.limit 030export interface Product {31 @meta.id @db.default.increment32 id: number3334 @meta.label 'SKU'35 @ui.table.width '8em'36 sku: string3738 @meta.label 'Name'39 @db.index.fulltext 'name_fts'40 name: string4142 @meta.label 'Price'43 @db.column.precision 244 @db.amount.currency 'USD'45 price: number4647 @meta.label 'In stock'48 inStock: boolean49}50```5152```vue53<script setup lang="ts">54import { createDefaultCellTypes } from "@atscript/vue-table";5556const types = createDefaultCellTypes();57</script>5859<template>60 <AsTableRoot url="/api/db/tables/products" :types="types" :limit="20">61 <AsTableActions />62 <AsFilters />63 <AsTable :column-menu="{ sort: true, filters: true, hide: true, resetWidth: true }" />64 </AsTableRoot>65</template>66```6768Replace `url=` with `:query-fn="..."` for a custom backend. See [query.md](references/query.md).6970## Invariants7172| # | Rule |73| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |74| 1 | **Programmatic state changes are picked up by the root watcher automatically.** Each mutator on `state` (e.g. `setFieldFilter`, `setColumnWidth`, `setSearchTerm`) touches exactly one entity; the root watcher on `[filters, sorters, pagination, columnNames]` schedules the next query. Calling `state.query()` to "apply" a programmatic change will double-fetch and skip debouncing. |75| 2 | **`state.query()` is reserved for user-initiated refresh.** Wire it to a refresh button, pull-to-refresh, or devtools — not to apply programmatic state changes — except `query({ silent: true })` for timer-driven live refresh (invariant 14). |76| 3 | **`filterFields` (display) and `filters` (applied) are independent.** Hiding a filter input via `filterFields` does NOT clear `filters[path]`. Clearing `filters[path]` does NOT hide the input. When building your own dialogs, write the new arrays directly — the root watcher reconciles. Cleanup loops that delete `filters` entries because `filterFields` shrank will fight the model and cause double-fetches. |77| 4 | **`@ui.table.type` is for built-in renderer ids only.** Built-ins: `text`, `number`, `boolean`, `date`, `datetime`, `relative`, `array`, `object`, `union`, `enum`, `ref`. Custom cells use `@ui.table.component` + the `:components` map. |78| 5 | **Per-field filter conditions: inclusions OR-merge, exclusions AND-merge.** Inclusion ops (`eq`, `contains`, `starts`, `ends`, `gt`, `gte`, `lt`, `lte`, `bw`, `regex`, `null`) → OR within field. Exclusion ops (`ne`, `notNull`) → AND within field. Across fields → AND. See `filtersToUniqueryFilter` in `ui-table`. |79| 6 | **Force filters / sorters AND-merge; user can't remove them.** Pass via `useTable({ forceFilters, forceSorters })`; they always prepend and dedupe by field — user mutations to `filters` / `sorters` never override them. |80| 7 | **Window mode fetches in fixed-size blocks.** `<AsWindowTable>` issues block-aligned fetches (default 100 rows) as the viewport scrolls; tune via `<AsTableRoot :block-size>` and `:drag-release-debounce-ms` (higher = fewer fetches during fast scroll). Custom virtual renderers read rows via `state.dataAt(absIndex)` / `state.loadingAt(absIndex)` / `state.errorAt(absIndex)` — see [query.md](references/query.md). |81| 8 | **Presets opt-in per-aspect.** A `PresetSnapshot` carries any subset of `columns`, `filters`, `filterOps`, `sorters`, `itemsPerPage`. Absent keys leave that slice untouched on apply. Use `toWireSnapshot` / `fromWireSnapshot` when crossing the network — never send the raw runtime dict. |82| 9 | **Reserved preset id prefixes**: `sys:` (system, client-only, never persisted), `uc:` (user config, deterministic id `uc:<user>:<app>:<tableKey>`), `ac:` (app config, deterministic `ac:<user>:<app>`). Client writes to `sys:*` are rejected by the server controller. |83| 10 | **Server preset read gate**: `user = current OR (type='preset' AND public=true)`. Once-public-always-public — revoking publish permission doesn't unpublish existing rows. |84| 11 | **`/meta` response carries `preferredId` on every row-returning read.** `moost-db` widens `$select` automatically; cells/actions/refs can rely on identity. Aggregate (`$groupBy`) and `$count` responses are NOT widened — see atscript-db skill, invariant 10. |85| 12 | **`:controls` takes only your overrides.** Every dispatch site falls back internally (`controls[key] ?? default`), so passing `createDefaultControls()` wholesale is redundant AND opts the lazy dialogs into eager bundling+mounting. `rowActions` is not seeded — the `__actions` column resolves `controls.rowActions ?? types.__actions ?? AsRowActions`. |86| 13 | **`$sort` overrides search relevance — opt into `ignoreSortersWhenSearched` to preserve it.** A preset/`v-model`/header sort emitted alongside `$search` replaces relevance ranking on a scored backend (Atlas `$search`), so search returns the right row set in browse order. `<AsTableRoot :ignore-sorters-when-searched>` (default `false`) suppresses user sorters at query time while searching; `forceSorters` still emit. The flag is a model — sorting mid-search flips it off for the session; a new search resets it. Only for relevance-ranked backends. See [sorting-pagination.md](references/sorting-pagination.md#search-relevance-suppression). |87| 14 | **`query({ silent: true })` is the sanctioned timer-driven live-refresh path.** Re-runs the current query (live filters/sorters/search/pagination/`$actions`) with no `querying` flip — no spinner, no skeletons, no query overlay. Loud-wins coalescing: a real (loud) query landing the same tick still shows the spinner. On failure, rows/`totalCount`/error state are left untouched — never blanks or toasts the grid. Preserves scroll position. Rides on keep-rows-until-settle — a stable contract where every query, silent or not, keeps prior rows visible until the response settles, then swaps `results` + `totalCount` atomically. See [query.md](references/query.md#silent-live-refresh). |8889## Key imports9091```ts92// Tier 1 — primary (auto-imported by AsResolver)93import {94 AsTableRoot,95 AsTable,96 AsWindowTable,97 AsTableActions,98 AsFilters,99 AsPresetPicker,100} from "@atscript/vue-table";101102// Tier 2 — defaults (swap targets)103import {104 // cells105 AsCellArray,106 AsCellDate,107 AsCellJson,108 AsCellNumber,109 AsCellUnion,110 AsTableCellValue,111 // dialogs112 AsConfigDialog,113 AsFilterDialog,114 AsPresetDialog,115 AsConfirmDialog,116 // filter ui + headers + rows117 AsFilterField,118 AsFilterInput,119 AsTableHeaderCell,120 AsRowActions,121 AsColumnMenu,122} from "@atscript/vue-table";123124// AsActionFormDialog is on a dedicated subpath — it pulls in @atscript/vue-form,125// so the table root lazy-mounts it. Import this only to override / eager-load.126import AsActionFormDialog from "@atscript/vue-table/as-action-form-dialog";127128// Composables129import {130 useTable,131 useTableContext,132 useTableContextOptional,133 createTableState,134 createStaticTableState,135 useTableSelection,136 useTableNavBridge,137 useTableFilter,138 useTableSearch,139 useTableActions,140 useTableUrlQuery,141 useAppPrefs,142 usePresets,143 useLocalDraft,144 useCellLocale,145 provideCellLocale,146 useTableComponent,147} from "@atscript/vue-table";148149// Factories150import { createDefaultControls, createDefaultCellTypes } from "@atscript/vue-table";151152// Types (re-exported)153import type {154 TAsTableControls,155 TAsCellTypeComponents,156 ReactiveTableState,157 ColumnMenuConfig,158 ConfigTab,159 TableActionsState,160 ActionResult,161} from "@atscript/vue-table";162163// Framework-agnostic table model (filter / preset / query primitives)164import {165 FilterCondition,166 FieldFilters,167 filtersToUniqueryFilter,168 PresetSnapshot,169 toWireSnapshot,170 fromWireSnapshot,171 buildTableQuery,172 mergeFilters,173 mergeSorters,174 stateToUrlQueryString,175 urlQueryStringToState,176} from "@atscript/ui-table";177178// Server-side preset controller (Moost)179import { AsPresetsController, AsPresetEntry } from "@atscript/moost-ui-presets";180```181182## References — load only what's needed183184| Domain | File | When |185| -------------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |186| First contact | [getting-started.md](references/getting-started.md) | Install matrix, `<AsTableRoot>` props, the default `:types` + `:controls` maps, slot binding contract |187| Query / data wiring | [query.md](references/query.md) | `url=` (moost-db) vs `queryFn` (custom), `buildTableQuery` Uniquery assembly, force filters/sorters, meta endpoint, mutators-are-pure principle in detail |188| Filtering | [filtering.md](references/filtering.md) | Filter model (`FieldFilters` / `FilterCondition` / 13 condition types), OR/AND semantics, `filtersToUniqueryFilter` translation, `<AsFilters>` / `<AsFilterField>` / `<AsFilterDialog>`, value-help inside filter dialogs |189| Sorting + pagination | [sorting-pagination.md](references/sorting-pagination.md) | Sort model + multi-sort, header click semantics, `<AsConfigDialog>` sorters tab, paginated `<AsTable>` vs virtualized `<AsWindowTable>`, block-aligned fetching, `dragReleaseDebounceMs` tuning |190| Cells | [cells.md](references/cells.md) | Built-in cell components + default type map, `provideCellLocale` (language + timezone), custom cells via `@ui.table.component` + `:components`, slot API (`#header-<path>`, `#cell-<path>`, `#empty`, `#query-loading`, `#error`), per-cell styling via `@ui.table.{classes,styles,attr}` |191| State persistence | [state-persistence.md](references/state-persistence.md) | `<AsConfigDialog>` tabs (columns/sorters/filters), `useTableUrlQuery` (router two-way bind), client presets (`PresetSnapshot`, `useLocalDraft`, `usePresets`, `useAppPrefs`, `<AsPresetPicker>`, system/user/public, `dateShortcuts`), server presets via `AsPresetsController` |192| Actions + selection | [actions-selection.md](references/actions-selection.md) | Row / table actions on the `.as` type, `<AsActionFormDialog>` (action input form via vue-form), `state.selectedRows` (`Set<PK>`), `togglePk` / `trimSelection` / `rowsToPks`, `state.actions.invoke(action, pk?, opts?)`, the `__actions` synthetic column; navigate-action links (`navigateHrefFor`, `resolveHref`, anchor vs button, native new-tab silent to `@action`) |193| Customization | [customization.md](references/customization.md) | When swapping table chrome: per-column / state slots (`#cell-*`, `#header-*`, `#empty`, `#error`, `#last-row`), `:types` / `:components` cell swap, replacing dialogs via `:controls` (and which control keys actually dispatch), custom row-actions cell, rendering without a header row (`:headless`) |194| Edit form + OCC | [edit-form-occ.md](references/edit-form-occ.md) | When building a row-edit `<AsForm>` against an OCC table: `@db.column.version`, `meta.versionColumn` → `createFormDef`, catching `VersionMismatchError` / `currentVersion` on submit |195| Annotation catalog | [annotations.md](../atscript-ui/references/annotations.md) | When writing or looking up any `@ui.table.*` / `@ui.table.fn.*` / `@ui.dict.*` annotation — args, constants, defaults (SSOT lives in the root `atscript-ui` skill) |196| Bundle optimization | [bundle-optimization.md](../atscript-ui/references/bundle-optimization.md) | When asked about table bundle size: lazy dialog latches, `controls.X` flipping a dialog eager, `AsActionFormDialog` pulling in vue-form, shedding unused chrome CSS via `excludeComponents` (SSOT lives in the root `atscript-ui` skill) |197198## Customization199200Full swap-surface reference (slots, maps, dispatched control keys): [customization.md](references/customization.md).201202Tables expose three swap surfaces, layered on the tier model:203204- **Tier 1** — `<AsTableRoot>`, `<AsTable>`, `<AsWindowTable>`, `<AsFilters>`, `<AsTableActions>`, `<AsPresetPicker>` are the integration surface. Build with `useTable` / `createTableState` directly if you need a custom shell.205- **Tier 2** — default cells (`AsCellArray`, `AsCellDate`, `AsCellJson`, `AsCellNumber`, `AsCellUnion`, `AsTableCellValue`) and default dialogs (`AsConfigDialog`, `AsFilterDialog`, `AsPresetDialog`, `AsConfirmDialog`, `AsRowActions`, `AsColumnMenu`, `AsTableHeaderCell`). These are what you swap.206- **Tier 3** — internal composition (header cells, virtualizer pieces, fields selector, sorter config). Not tagged directly; styles ride with the defaults that use them.207208### Swap a built-in cell renderer (`:types`)209210`:types` maps built-in cell ids (per invariant 4: `text`, `number`, `boolean`, `date`, `datetime`, `relative`, `array`, `object`, `union`, `enum`, `ref`) to a component:211212```vue213<script setup lang="ts">214import { createDefaultCellTypes } from "@atscript/vue-table";215import MyDateCell from "./MyDateCell.vue";216217const types = { ...createDefaultCellTypes(), date: MyDateCell, datetime: MyDateCell };218</script>219220<template>221 <AsTableRoot url="/api/db/tables/products" :types="types" />222</template>223```224225### Swap a specific cell (`:components` + `@ui.table.component`)226227For column-specific cells, opt in on the `.as` type and supply the named component:228229```atscript230@ui.table.component 'price-tag'231price: number232```233234```vue235<script setup lang="ts">236import PriceTag from "./PriceTag.vue";237const components = { "price-tag": PriceTag };238</script>239240<template>241 <AsTableRoot url="..." :components="components" />242</template>243```244245Wrap a cell in `useCellLocale` / `provideCellLocale` if it needs locale + timezone from `useAppPrefs`.246247### Swap a dialog (`controls.*`)248249The toolbar, config, filter, and preset dialogs are swappable through `controls`. Pass only the entries you replace — spreading `createDefaultControls()` defeats dialog lazy-loading (see invariant 12):250251```vue252<script setup lang="ts">253import MyFilterDialog from "./MyFilterDialog.vue";254255const controls = { filterDialog: MyFilterDialog };256</script>257258<template>259 <AsTableRoot url="..." :controls="controls" />260</template>261```262263Custom dialogs just write the new arrays back to `state.filterFields` / `state.filters` / `state.sorters` / `state.columnNames` — the root watcher reconciles and re-queries (see invariants 1–3 and [query.md](references/query.md)). Don't call `state.query()` or run cleanup loops mirroring display state into applied state; both fight the watcher.264265### Style consequence266267Replacing `AsFilterDialog` with a custom dialog that doesn't tag the `as-filter-*` shortcuts drops those classes out of your bundle automatically. Keep the default whenever its chrome fits — you'll spend less time on style maintenance. For granular opt-out, `atscript-ui-styles` ships per-domain shortcut groups (`tableShortcuts`, `formShortcuts`, …) you can compose narrower than the default `allShortcuts`.268269## See also270271Reference docs: https://ui.atscript.dev/tables/. Source: https://github.com/moostjs/atscript-ui.