moostjs/atscript-ui · Archived

atscript-ui-tables

>- 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` …

First seen May 13, 2026

Installation

$ npx skills add moostjs/atscript-ui --skill atscript-ui-tables

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from moostjs/atscript-ui.

npx skills add moostjs/atscript-ui

Browse all from moostjs/atscript-ui

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 25,416 B
  • docs SUMMARY.md 1,043 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 17 installs

SKILL.md

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](references/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](references/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](references/sorting-pagination.md#search-relevance-suppression).
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).

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](references/getting-started.md) Install matrix, <AsTableRoot> props, the default :types + :controls maps, slot binding contract
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
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
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
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}
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
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)
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)
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
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)
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)

Customization

Full swap-surface reference (slots, maps, dispatched control keys): [customization.md](references/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](references/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.