@tailor-platform/app-shell 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +692 -106
- package/dist/app-shell.js +6294 -4939
- package/dist/{appshell-B0wh5Lq0.js → appshell-CahhZy7v.js} +316 -297
- package/dist/semantic-contrast.test.ts +154 -0
- package/dist/style-entry.d.ts +117 -82
- package/dist/testing.d.ts +122 -85
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +101 -1
- package/dist/theme.bridge.css +91 -0
- package/dist/theme.css +6 -1
- package/dist/themes/_template.css +7 -1
- package/dist/themes/bloom.css +0 -6
- package/dist/themes/cream.css +0 -6
- package/dist/themes/default.css +389 -69
- package/dist/vite-plugin.d.ts +117 -82
- package/package.json +1 -1
- package/skills/app-shell-patterns/SKILL.md +7 -5
- package/skills/app-shell-patterns/references/api/define-module.md +2 -2
- package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
- package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
- package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
- package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
- package/skills/app-shell-patterns/references/components/card.md +79 -7
- package/skills/app-shell-patterns/references/components/data-table.md +267 -52
- package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
- package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -0
- package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
- package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +255 -58
- package/skills/app-shell-patterns/references/migrations.md +21 -0
- package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
- package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +2 -2
|
@@ -22,7 +22,10 @@ import {
|
|
|
22
22
|
type DataTableData,
|
|
23
23
|
type DataTableRootProps,
|
|
24
24
|
type DataTablePaginationProps,
|
|
25
|
+
type DataTableAction,
|
|
25
26
|
type RowAction,
|
|
27
|
+
type RowSelectionOptions,
|
|
28
|
+
type SelectionAction,
|
|
26
29
|
type UseDataTableOptions,
|
|
27
30
|
type UseDataTableReturn,
|
|
28
31
|
type MetadataFieldOptions,
|
|
@@ -144,15 +147,15 @@ function JournalsPage() {
|
|
|
144
147
|
|
|
145
148
|
`DataTable` is a namespace object. All sub-components read state from `DataTable.Root` via context.
|
|
146
149
|
|
|
147
|
-
| Sub-component | Description
|
|
148
|
-
| -------------------------- |
|
|
149
|
-
| `DataTable.Root` | Context provider. Wraps all other sub-components. Required.
|
|
150
|
-
| `DataTable.Table` | Renders the `<table>` with headers and body. Required.
|
|
151
|
-
| `DataTable.Toolbar` | DataTable-specific toolbar container. Optional; pass `columnSettings` for the built-in Columns control.
|
|
152
|
-
| `DataTable.ColumnSettings` | The placeable Columns control (show/hide + reorder + pin). Use it in a generic [`Toolbar`](./toolbar.md) for custom layouts.
|
|
153
|
-
| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`.
|
|
154
|
-
| `DataTable.Footer` | Footer container for pagination and other footer content. Optional.
|
|
155
|
-
| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`.
|
|
150
|
+
| Sub-component | Description |
|
|
151
|
+
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
152
|
+
| `DataTable.Root` | Context provider. Wraps all other sub-components. Required. |
|
|
153
|
+
| `DataTable.Table` | Renders the `<table>` with headers and body. Required. |
|
|
154
|
+
| `DataTable.Toolbar` | DataTable-specific toolbar container. Optional; pass `columnSettings` for the built-in Columns control. |
|
|
155
|
+
| `DataTable.ColumnSettings` | The placeable Columns control (show/hide + reorder + pin). Use it in a generic [`Toolbar`](./toolbar.md) for custom layouts. |
|
|
156
|
+
| `DataTable.Filters` | Add-filter panel + active filter chips, auto-generated from column filter configs. Requires `control` from `useCollectionVariables`. |
|
|
157
|
+
| `DataTable.Footer` | Footer container for pagination and other footer content. Optional. Becomes the bulk-action bar while rows are selected — see [Selection actions](#selection-actions). |
|
|
158
|
+
| `DataTable.Pagination` | Pre-built pagination controls with optional row count and selection info. Requires `control` from `useCollectionVariables`. Place inside `DataTable.Footer`. |
|
|
156
159
|
|
|
157
160
|
### `DataTable.Root` Props
|
|
158
161
|
|
|
@@ -220,7 +223,7 @@ By default `DataTable.Filters` renders the active filter chips plus the **Add fi
|
|
|
220
223
|
| Rows selected and `total` is not provided | `Y row(s) selected` |
|
|
221
224
|
| No selection enabled and no `total` | _(nothing displayed)_ |
|
|
222
225
|
|
|
223
|
-
Row selection is enabled by providing `onSelectionChange` to `useDataTable`. The `total` value comes from `DataTableData.total`.
|
|
226
|
+
Row selection is enabled by providing `rowSelection` or `onSelectionChange` to `useDataTable`. The `total` value comes from `DataTableData.total`. While the footer shows the bulk-action bar (see [Selection actions](#selection-actions)), the bar owns the selection count and this text is omitted.
|
|
224
227
|
|
|
225
228
|
When pagination changes page or page size, `DataTable.Table` resets its own scroll container to the top automatically. That applies whether navigation comes from the built-in `DataTable.Pagination` or from custom controls using the same table context.
|
|
226
229
|
|
|
@@ -368,9 +371,119 @@ The trigger is a native `<button>`, so Enter/Space activation and the focus ring
|
|
|
368
371
|
|
|
369
372
|
**Known limitation — row count.** Detail rows are real `<tr>` elements, so a screen reader counts them: ten records with two expanded announces as twelve rows. Fixing this needs `aria-rowcount` plus explicit `aria-rowindex` on every row (with detail rows sharing their parent's index) and correct interaction with pagination; `role="presentation"` on the detail row would fix the count but remove the panel from screen-reader table navigation. Neither is implemented.
|
|
370
373
|
|
|
374
|
+
## Row selection
|
|
375
|
+
|
|
376
|
+
Pass `rowSelection` to `useDataTable` to enable the checkbox column. Rows must have a string or number `id`; selected ids are their string-normalized values, and rows without an id are excluded. An empty configuration (`rowSelection: {}`) enables uncontrolled selection without a notification callback. Bulk actions belong in `rowSelection.actions`; `rowSelection: { actions: [] }` still enables checkboxes, but does not show the bulk-action bar.
|
|
377
|
+
|
|
378
|
+
### `RowSelectionOptions`
|
|
379
|
+
|
|
380
|
+
| Property | Type | Description |
|
|
381
|
+
| -------------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
382
|
+
| `selectedIds` | `string[]` | Controlled selected ids. Requires `onChange` and cannot be combined with `defaultSelectedIds`. |
|
|
383
|
+
| `defaultSelectedIds` | `string[]` | Initial selected ids in uncontrolled mode. Later changes are ignored. |
|
|
384
|
+
| `onChange` | `(ids: string[]) => void` | Notifies user-driven selection changes; required in controlled mode and optional in uncontrolled mode. |
|
|
385
|
+
| `actions` | `SelectionAction[]` | Optional bulk actions shown in `DataTable.Footer` while ids are selected. Omitted or empty arrays show no bar. |
|
|
386
|
+
|
|
387
|
+
### Controlled selection
|
|
388
|
+
|
|
389
|
+
Provide `selectedIds` and `onChange` together to own the selection. The `RowSelectionOptions` type requires `onChange` in controlled mode and rejects combining `selectedIds` with `defaultSelectedIds`.
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
393
|
+
|
|
394
|
+
const table = useDataTable<Order>({
|
|
395
|
+
columns,
|
|
396
|
+
data,
|
|
397
|
+
rowSelection: { selectedIds, onChange: setSelectedIds },
|
|
398
|
+
});
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Commands such as `toggleRowSelection(row)` request a new array through `onChange`; the rendered selection changes only when the owner updates `selectedIds`. Each command derives from the current committed value, so multiple commands before the owner commits share the same base. Compute batched changes yourself and set `selectedIds` directly instead of issuing repeated commands.
|
|
402
|
+
|
|
403
|
+
### Uncontrolled selection
|
|
404
|
+
|
|
405
|
+
Omit `selectedIds` to let the table own selection state. `defaultSelectedIds` initializes it only when the hook mounts; later changes to that default do not reset selection. `onChange` is optional.
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
const table = useDataTable<Order>({
|
|
409
|
+
columns,
|
|
410
|
+
data,
|
|
411
|
+
rowSelection: { defaultSelectedIds: ["INV-1001"] },
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Keep the controlled/uncontrolled mode stable while mounted. Callbacks notify user-driven selection changes, not mounting or external `selectedIds` updates. `onSelectionChange` is deprecated but remains supported for compatibility. Replace `onSelectionChange: handleSelectionChange` with `rowSelection: { onChange: handleSelectionChange }` to keep uncontrolled notification behavior. `rowSelection.onChange` takes precedence when both callbacks are supplied, and `onSelectionChange` is used when the new configuration has no callback.
|
|
416
|
+
|
|
417
|
+
Selection spans pages. The header checkbox adds or removes only the current page's rows, preserving ids selected on other pages; `clearSelection()` clears every page. Selection stores IDs only, not row objects. Both `selectedIds` and bulk-action payloads include all selected ids, including ids whose rows have not been loaded. Fetch any records required for eligibility checks or execution in the consumer's ID-based workflow.
|
|
418
|
+
|
|
419
|
+
## Selection actions
|
|
420
|
+
|
|
421
|
+
Pass `rowSelection.actions` to `useDataTable` and, while at least one id is selected, `DataTable.Footer` becomes a bulk-action bar: the selection count, your actions, and a **Clear** button, with the footer's own children (usually `DataTable.Pagination`) kept alongside. `rowSelection: { actions }` enables uncontrolled selection without an `onChange` callback. Omitted or empty `actions` show no bar, while the `rowSelection` object still enables checkboxes. There is nothing new to compose in JSX — just keep `DataTable.Footer` in the tree.
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
425
|
+
const [deleteTargetIds, setDeleteTargetIds] = useState<string[] | null>(null);
|
|
426
|
+
|
|
427
|
+
const table = useDataTable<Vendor>({
|
|
428
|
+
columns,
|
|
429
|
+
data,
|
|
430
|
+
control,
|
|
431
|
+
rowSelection: {
|
|
432
|
+
selectedIds,
|
|
433
|
+
onChange: setSelectedIds,
|
|
434
|
+
actions: [
|
|
435
|
+
{
|
|
436
|
+
id: "delete",
|
|
437
|
+
label: "Delete",
|
|
438
|
+
variant: "destructive",
|
|
439
|
+
onClick: (ids) => setDeleteTargetIds([...ids]),
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
return (
|
|
446
|
+
<>
|
|
447
|
+
<DataTable.Root value={table}>
|
|
448
|
+
<DataTable.Table />
|
|
449
|
+
<DataTable.Footer>
|
|
450
|
+
<DataTable.Pagination />
|
|
451
|
+
</DataTable.Footer>
|
|
452
|
+
</DataTable.Root>
|
|
453
|
+
|
|
454
|
+
{deleteTargetIds !== null && (
|
|
455
|
+
<DeleteVendorsDialog
|
|
456
|
+
ids={deleteTargetIds}
|
|
457
|
+
onCancel={() => setDeleteTargetIds(null)}
|
|
458
|
+
onDeleted={(deletedIds: string[]) => {
|
|
459
|
+
const deleted = new Set(deletedIds);
|
|
460
|
+
setSelectedIds((current) => current.filter((id) => !deleted.has(id)));
|
|
461
|
+
setDeleteTargetIds(null);
|
|
462
|
+
}}
|
|
463
|
+
/>
|
|
464
|
+
)}
|
|
465
|
+
</>
|
|
466
|
+
);
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`DeleteVendorsDialog` is a consumer-owned component, not an AppShell component. It can fetch the target records, explain eligibility (for example, “Three of five selected vendors can be deleted; two are in use”), confirm, and execute. Backend validation remains authoritative; `onDeleted` reports only ids actually deleted. Removing those ids from the **current** selection preserves ineligible records and records selected after the dialog opened. Cancellation or failure leaves selection unchanged.
|
|
470
|
+
|
|
471
|
+
- **`onClick` receives a click-time snapshot of all selected ids.** The array is a copy, in selection order, including ids whose rows have not been loaded. Page visits and loaded-row availability do not change the target. Selection actions have no row-based `canApply`, automatic eligible count, or eligibility-based disabled state; the consumer owns eligibility checks, required fetching, confirmation UI, and execution. Row actions can still use `canApply` for their concrete row.
|
|
472
|
+
- **Return the promise from `onClick`.** While it is pending, the bar disables every action and shows a spinner on the running one, so a slow request can't be fired twice. Neither success nor rejection changes selection; there is no `keepSelection` option. A rejection is logged as `[DataTable] Selection action "…" failed`; showing it to the user is up to your handler.
|
|
473
|
+
- **Confirm dialogs can hand a later request back with `run`.** An `onClick` that only opens a dialog returns nothing. Keep the `run` helper and call it from the dialog's confirm button — `await run(deleteVendors(ids))` — to track that request in the bar. This only tracks pending state; it does not clear selection. Clear is disabled while a request is pending, and pending state survives the bar closing and reopening. Use controlled `rowSelection` to remove only successful ids, or call the explicit `clearSelection` helper if clearing every page is intentional.
|
|
474
|
+
- **Selection spans pages.** The header checkbox adds or removes only the current page; **Clear** empties everything.
|
|
475
|
+
- **Three actions stay inline.** The fourth onward collapse into a **More actions** menu, in array order. Put the most frequent first; a destructive action placed last sits safely in the menu.
|
|
476
|
+
- **Confirm destructive actions.** `variant: "destructive"` only styles the action. Open a confirm dialog from `onClick` before deleting — see the [confirm pattern](../patterns/interaction-confirm.md).
|
|
477
|
+
- **The bar stays reachable.** In `<Layout fill>` the footer is already pinned. On a page that scrolls, the bar sticks to the bottom of the viewport while rows are selected and settles back into place at the end of the table. It sticks to the nearest scrolling container: inside a scrolling drawer or panel it rides that container instead, and inside a wrapper that clips its overflow (such as a card with `overflow: hidden`) it simply stays in the footer.
|
|
478
|
+
- **Share presentation with `rowActions`.** Both action types extend the non-generic [`DataTableAction`](#datatableaction) (`id`, `label`, `icon`, `variant`). Keep row predicates on `RowAction<TRow>` and bulk business rules in the consumer's ID-based handler.
|
|
479
|
+
|
|
480
|
+
### Accessibility
|
|
481
|
+
|
|
482
|
+
The bar is a `role="toolbar"` named "Bulk actions", so Arrow, Home, and End move between its controls (see [`Toolbar`](./toolbar.md)) while Tab still reaches each one. The selection count is announced through a polite live region from the first selection on. When the bar closes while focus is inside it — after **Clear**, or an action that clears — focus returns to the header checkbox instead of dropping to `<body>`.
|
|
483
|
+
|
|
371
484
|
## `useDataTable`
|
|
372
485
|
|
|
373
|
-
Creates the table state object to pass to `DataTable.Root`.
|
|
486
|
+
Creates the table state object to pass to `DataTable.Root`. The hook return value and `useDataTableContext()` both expose the original selection configuration as `rowSelection`, including `rowSelection.actions`. Read the current selection from `selectedIds` and update it through the selection commands.
|
|
374
487
|
|
|
375
488
|
```tsx
|
|
376
489
|
const table = useDataTable({
|
|
@@ -383,19 +496,20 @@ const table = useDataTable({
|
|
|
383
496
|
|
|
384
497
|
### Options
|
|
385
498
|
|
|
386
|
-
| Option | Type | Description
|
|
387
|
-
| ------------------- | ---------------------------------- |
|
|
388
|
-
| `columns` | `Column<TRow>[]` | Column definitions. Required.
|
|
389
|
-
| `data` | `DataTableData<TRow> \| undefined` | Fetched data. Pass `undefined` while loading.
|
|
390
|
-
| `loading` | `boolean` | When `true`, renders a loading skeleton.
|
|
391
|
-
| `error` | `Error \| null` | When set, renders an error message in the table body.
|
|
392
|
-
| `control` | `CollectionControl` | Collection control from `useCollectionVariables()`. Required for `DataTable.Pagination` and `DataTable.Filters`.
|
|
393
|
-
| `onClickRow` | `(row: TRow) => void` | Called when the user clicks a row. Adds a pointer cursor to rows.
|
|
394
|
-
| `tableId` | `string` | Stable id used to persist per-user column layout (visibility, order, pinning) to `localStorage`. When omitted, column layout is in-memory only and resets on reload.
|
|
395
|
-
| `rowActions` | `RowAction<TRow>[]` | Per-row action items rendered in a kebab-menu column. The column is omitted when empty or not provided.
|
|
396
|
-
| `
|
|
397
|
-
| `
|
|
398
|
-
| `
|
|
499
|
+
| Option | Type | Description |
|
|
500
|
+
| ------------------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
501
|
+
| `columns` | `Column<TRow>[]` | Column definitions. Required. |
|
|
502
|
+
| `data` | `DataTableData<TRow> \| undefined` | Fetched data. Pass `undefined` while loading. |
|
|
503
|
+
| `loading` | `boolean` | When `true`, renders a loading skeleton. |
|
|
504
|
+
| `error` | `Error \| null` | When set, renders an error message in the table body. |
|
|
505
|
+
| `control` | `CollectionControl` | Collection control from `useCollectionVariables()`. Required for `DataTable.Pagination` and `DataTable.Filters`. |
|
|
506
|
+
| `onClickRow` | `(row: TRow) => void` | Called when the user clicks a row. Adds a pointer cursor to rows. |
|
|
507
|
+
| `tableId` | `string` | Stable id used to persist per-user column layout (visibility, order, pinning) to `localStorage`. When omitted, column layout is in-memory only and resets on reload. |
|
|
508
|
+
| `rowActions` | `RowAction<TRow>[]` | Per-row action items rendered in a kebab-menu column. The column is omitted when empty or not provided. |
|
|
509
|
+
| `rowSelection` | `RowSelectionOptions` | Controlled `selectedIds` + `onChange`, or uncontrolled `defaultSelectedIds` + optional `onChange`, with optional `actions`. Providing even `{}` enables checkboxes. See [Row selection](#row-selection). |
|
|
510
|
+
| `onSelectionChange` | `(ids: string[]) => void` | **Deprecated:** use `rowSelection.onChange`. Still supported for compatibility; enables checkboxes and is used when the new callback is absent. |
|
|
511
|
+
| `rowExpansion` | `RowExpansionOptions<TRow>` | Expandable detail rows: `render`, plus optional `canExpand` / `getLabel`, and `expandedIds` + `onChange` together for controlled mode. See [Expandable rows](#expandable-rows). |
|
|
512
|
+
| `sort` | `false \| { multiple?: boolean }` | Sort behaviour. `false` disables sorting entirely. `{ multiple: true }` enables multi-column sorting. Omit or pass `{}` for single-column sort (default). |
|
|
399
513
|
|
|
400
514
|
### `DataTableData`
|
|
401
515
|
|
|
@@ -693,18 +807,75 @@ The `filter` property on a column accepts the same base shape as `FilterConfig`,
|
|
|
693
807
|
|
|
694
808
|
### Filter Types and Operators
|
|
695
809
|
|
|
696
|
-
| Type | Input editor
|
|
697
|
-
| ---------- |
|
|
698
|
-
| `string` | Text
|
|
699
|
-
| `number` | Number
|
|
700
|
-
| `
|
|
701
|
-
| `
|
|
702
|
-
| `
|
|
703
|
-
| `
|
|
704
|
-
| `
|
|
705
|
-
| `
|
|
810
|
+
| Type | Input editor | Supported operators |
|
|
811
|
+
| ---------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
812
|
+
| `string` | Text | `eq`, `ne`, `contains`, `notContains`, `hasPrefix`, `hasSuffix`, `notHasPrefix`, `notHasSuffix`, `in`, `nin` |
|
|
813
|
+
| `number` | Number | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
|
|
814
|
+
| `decimal` | Decimal text (string value) | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
|
|
815
|
+
| `datetime` | Calendar / DatePicker + Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
|
|
816
|
+
| `date` | **Calendar / DatePicker** | `eq` (_exact date_), `gte` (_after_), `lte` (_before_), **`between`** |
|
|
817
|
+
| `time` | Time | `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, **`between`**, `in`, `nin` |
|
|
818
|
+
| `enum` | Dropdown | `eq`, `ne`, `in`, `nin` |
|
|
819
|
+
| `boolean` | Toggle | `eq`, `ne` |
|
|
820
|
+
| `uuid` | Text | `eq`, `ne`, `in`, `nin` |
|
|
821
|
+
|
|
822
|
+
When the `between` operator is selected on a `number`, `decimal`, `datetime`, `date`, or `time` column, the value editor renders a range input with **min**/**max** (or **From**/**To** for dates) bounds.
|
|
823
|
+
|
|
824
|
+
### Decimal Filters
|
|
825
|
+
|
|
826
|
+
TailorDB `db.decimal()` fields are generated as metadata `type: "decimal"`. They support numeric sort and filter operators, but values remain **strings**, matching the GraphQL `Decimal` scalar without JavaScript number rounding. Scalar filters, `in`/`nin` arrays, and `between` bounds all use strings in `variables.query`.
|
|
827
|
+
|
|
828
|
+
The editor accepts signed decimal and scientific notation (for example, `"9007199254740993.123456789"` or `"1.23e-10"`) and checks range ordering without rounding. Sorting is delegated to the backend. As with `number`, `in`/`nin` are available programmatically, not in the built-in editor.
|
|
829
|
+
|
|
830
|
+
Regenerate metadata with the updated SDK plugin to replace older decimal fields marked as `"number"`. Decimal metadata does not select a numeric cell renderer; the default text rendering preserves the original value.
|
|
831
|
+
|
|
832
|
+
### Datetime Filters and Business Timezones
|
|
833
|
+
|
|
834
|
+
> **Important:** Set [`AppShell.timeZone`](./app-shell.md#timezone) explicitly when datetime filters must follow a shared business timezone. Without it, the filter editors use each user's local timezone. Users in different timezones can enter the same date and time but search against different instants, causing expected records to be missed. Setting `locale` alone does **not** set the timezone.
|
|
835
|
+
|
|
836
|
+
`locale` and `timeZone` are independent:
|
|
837
|
+
|
|
838
|
+
| Setting | Controls | Example |
|
|
839
|
+
| ---------- | ------------------------------------------------------------------------------------ | -------------- |
|
|
840
|
+
| `locale` | UI language and locale-specific date/time formatting | `"ja-JP"` |
|
|
841
|
+
| `timeZone` | The timezone used to interpret the date and time selected in datetime filter editors | `"Asia/Tokyo"` |
|
|
842
|
+
|
|
843
|
+
For example, a Japanese-language application can use a US business timezone. Choose the timezone that defines the business's date boundaries, not one inferred from the UI language or the user's location:
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
import { AppShell } from "@tailor-platform/app-shell";
|
|
847
|
+
|
|
848
|
+
<AppShell modules={modules} locale="ja-JP" timeZone="America/New_York">
|
|
849
|
+
{/* Existing application content */}
|
|
850
|
+
</AppShell>;
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
Use an IANA timezone such as `"Asia/Tokyo"` or `"America/New_York"`, rather than a fixed UTC offset, so daylight-saving changes are accounted for. If per-user local-time filtering is intentional, leave `timeZone` unset; otherwise, configure the shared business timezone even when every user has the same `locale`.
|
|
854
|
+
|
|
855
|
+
#### Local input, RFC 3339 output
|
|
856
|
+
|
|
857
|
+
The datetime editor shows the date and time in the configured timezone, but sends an **instant** — a specific point in time — as an RFC 3339 UTC string. For example, the same input `2026-10-09 00:00` produces:
|
|
858
|
+
|
|
859
|
+
| Timezone used by the editor | Serialized filter value |
|
|
860
|
+
| --------------------------- | -------------------------- |
|
|
861
|
+
| `Asia/Tokyo` | `2026-10-08T15:00:00.000Z` |
|
|
862
|
+
| `America/New_York` | `2026-10-09T04:00:00.000Z` |
|
|
863
|
+
|
|
864
|
+
Applying or updating a datetime filter through `DataTable.Filters` passes these strings to `CollectionControl.addFilter`, so consumers reading `control.filters` receive UTC instants rather than timezone-less local datetime strings. A `between` filter serializes both `min` and `max` as instants. Existing RFC 3339 values are displayed in the editor's timezone without changing the instant they represent. `date` and `time` filters retain their `YYYY-MM-DD` and `HH:mm` formats.
|
|
865
|
+
|
|
866
|
+
A datetime `eq` filter matches one instant, not every record on a calendar day. To search a whole business day on a datetime field, use the start of that day as an inclusive lower bound and the start of the next day as an exclusive upper bound in the backend query. Calculate both boundaries in the business timezone; do not assume every day is exactly 24 hours.
|
|
706
867
|
|
|
707
|
-
When
|
|
868
|
+
When `tableMetadata` is supplied to `useCollectionVariables` or `useURLCollectionVariables`, datetime filters are normalized before the first query and before `addFilter` or `setFilters` updates the state. This also covers filters restored from URLs or saved views. Timezone-less legacy datetime strings are interpreted in the AppShell timezone; values with `Z` or an explicit offset retain their original instant. Single values, `in` / `nin` arrays, and both bounds of `between` filters are normalized. Invalid datetime values throw a `TypeError` rather than silently removing a search constraint; invalid updates leave the previous filter state unchanged. Datetime strings must use the ISO date-and-time form with `T`; date-only strings and browser-specific date formats are not accepted for datetime fields. If migrating legacy values, first establish which timezone they originally represented.
|
|
869
|
+
|
|
870
|
+
Without `tableMetadata`, the collection hook cannot distinguish datetime fields from ordinary strings and leaves filter values unchanged. Supply RFC 3339 datetime values with an explicit timezone in programmatic or persisted filters on this path. Setting a DataTable column's `filter.type` configures the UI but does not supply metadata to the collection hook.
|
|
871
|
+
|
|
872
|
+
Built-in `type: "date"` cells display timestamps in the AppShell timezone, and the cell menu uses that same timezone when adding a temporal filter. Date-only `YYYY-MM-DD` cell values retain their calendar date instead of shifting to the previous or next day. The setting does not change the browser's default timezone: when using `Intl.DateTimeFormat` in a custom cell renderer, pass the same timezone explicitly if its output must agree with the filters.
|
|
873
|
+
|
|
874
|
+
#### Calendar dates are not instants
|
|
875
|
+
|
|
876
|
+
Use the `date` field/filter type for values whose meaning is a calendar date, such as a purchase-order date or an accounting date. `2026-10-09` should remain that date regardless of where the user is located. Use `datetime` for events that occurred at a particular instant, such as a record's creation time.
|
|
877
|
+
|
|
878
|
+
If an existing backend stores document dates as datetime instants, a shared business timezone can make filter boundaries consistent, but it does not fix that data model. Changing only the column's filter type to `date` does not migrate the stored values or change the backend's query contract. Align the backend field type, data, and filters when moving to date-only semantics.
|
|
708
879
|
|
|
709
880
|
### Date Filters
|
|
710
881
|
|
|
@@ -717,7 +888,7 @@ When the `between` operator is selected on a `number`, `datetime`, `date`, or `t
|
|
|
717
888
|
| `lte` | _before_ | on or before (inclusive) |
|
|
718
889
|
| `between` | _between_ | inclusive min–max range |
|
|
719
890
|
|
|
720
|
-
`gt` / `lt` / `ne` are intentionally dropped — the inclusive _after_ / _before_ cover the intent. The filter chip shows the value as a locale-formatted date (e.g. `15 Jun 2026`), and the picker resolves its locale/timezone from the AppShell context. (Only `date` is remapped this way; `datetime` and `time` keep the full numeric operator set
|
|
891
|
+
`gt` / `lt` / `ne` are intentionally dropped — the inclusive _after_ / _before_ cover the intent. The filter chip shows the value as a locale-formatted date (e.g. `15 Jun 2026`), and the picker resolves its locale/timezone from the AppShell context. (Only `date` is remapped this way; `datetime` and `time` keep the full numeric operator set. Datetime editors pair a calendar/date picker with a time input; `time` uses a native time input.)
|
|
721
892
|
|
|
722
893
|
### String Filter Case Sensitivity
|
|
723
894
|
|
|
@@ -743,14 +914,56 @@ When `caseSensitive` is omitted or `false`, the filter is case-insensitive. When
|
|
|
743
914
|
|
|
744
915
|
## `RowAction`
|
|
745
916
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
|
749
|
-
|
|
|
750
|
-
| `
|
|
751
|
-
| `
|
|
752
|
-
| `
|
|
753
|
-
|
|
917
|
+
A row action in the kebab-menu column. `RowAction<TRow>` extends the non-generic [`DataTableAction`](#datatableaction) (`id`, `label`, `icon`, `variant`) and adds row-based behavior.
|
|
918
|
+
|
|
919
|
+
| Property | Type | Description |
|
|
920
|
+
| ------------ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
921
|
+
| `canApply` | `(row: TRow) => boolean` | Return `false` to disable the action for a given row. |
|
|
922
|
+
| `isDisabled` | `(row: TRow) => boolean` | **Deprecated** — use `canApply`, which reads the other way round. Still honoured: the action is disabled if either one says so. |
|
|
923
|
+
| `onClick` | `(row: TRow) => void` | Called when the action is clicked. |
|
|
924
|
+
|
|
925
|
+
## `SelectionAction`
|
|
926
|
+
|
|
927
|
+
A non-generic bulk action for selected ids. Extends [`DataTableAction`](#datatableaction). See [Selection actions](#selection-actions).
|
|
928
|
+
|
|
929
|
+
| Property | Type | Description |
|
|
930
|
+
| --------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
931
|
+
| `onClick` | `(ids: string[], helpers: SelectionActionHelpers) => void \| Promise<unknown>` | Receives a copy of all selected ids at click time, including unloaded ids. Return a promise, or pass a later request to `helpers.run`, for pending-state tracking only. Selection is never cleared automatically. |
|
|
932
|
+
|
|
933
|
+
`SelectionActionHelpers` provides `clearSelection(): void` to explicitly clear every page and `run<T>(request: PromiseLike<T>): PromiseLike<T>` to track a later request without changing selection. Eligibility, fetching, confirmation, execution, and result-based selection updates belong to the consumer.
|
|
934
|
+
|
|
935
|
+
## `DataTableAction`
|
|
936
|
+
|
|
937
|
+
Non-generic presentation shared by `RowAction<TRow>` and `SelectionAction`. Share these fields, not eligibility rules: row actions can evaluate a concrete row, while selection actions operate on ids.
|
|
938
|
+
|
|
939
|
+
| Property | Type | Description |
|
|
940
|
+
| --------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
941
|
+
| `id` | `string` | Stable identifier for the action. |
|
|
942
|
+
| `label` | `string` | Display label — the menu item for a row action, the button (or **More actions** item) for a selection action. |
|
|
943
|
+
| `icon` | `ReactNode` | Optional icon shown beside the label. |
|
|
944
|
+
| `variant` | `"default" \| "destructive"` | Visual style. A destructive action still needs its own confirmation step. |
|
|
945
|
+
|
|
946
|
+
```tsx
|
|
947
|
+
const archive: DataTableAction = {
|
|
948
|
+
id: "archive",
|
|
949
|
+
label: "Archive",
|
|
950
|
+
};
|
|
951
|
+
|
|
952
|
+
useDataTable<Order>({
|
|
953
|
+
columns,
|
|
954
|
+
data,
|
|
955
|
+
rowActions: [
|
|
956
|
+
{
|
|
957
|
+
...archive,
|
|
958
|
+
canApply: (order) => order.status !== "Archived",
|
|
959
|
+
onClick: (order) => archiveOrders([order.id]),
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
rowSelection: {
|
|
963
|
+
actions: [{ ...archive, onClick: (ids) => archiveOrders(ids) }],
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
```
|
|
754
967
|
|
|
755
968
|
## `createColumnHelper`
|
|
756
969
|
|
|
@@ -810,13 +1023,13 @@ const { variables, control } = useCollectionVariables({
|
|
|
810
1023
|
|
|
811
1024
|
### Options
|
|
812
1025
|
|
|
813
|
-
| Option | Type | Description
|
|
814
|
-
| ----------------------- | ------------------------------------ |
|
|
815
|
-
| `params.pageSize` | `number` | Initial page size. Default: `20`.
|
|
816
|
-
| `params.initialFilters` | `Filter[]` | Filters applied on first render.
|
|
817
|
-
| `params.initialSort` | `SortState[]` | Sort applied on first render.
|
|
818
|
-
| `tableMetadata` | `TableMetadata` | Generated table metadata.
|
|
819
|
-
| `onParamsChange` | `(params: CollectionParams) => void` | Called after each filter, sort, or page-size change with the current params.
|
|
1026
|
+
| Option | Type | Description |
|
|
1027
|
+
| ----------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
1028
|
+
| `params.pageSize` | `number` | Initial page size. Default: `20`. |
|
|
1029
|
+
| `params.initialFilters` | `Filter[]` | Filters applied on first render. |
|
|
1030
|
+
| `params.initialSort` | `SortState[]` | Sort applied on first render. |
|
|
1031
|
+
| `tableMetadata` | `TableMetadata` | Generated table metadata. Enables typed variables and datetime normalization (see [Datetime filters](#datetime-filters-and-business-timezones)). |
|
|
1032
|
+
| `onParamsChange` | `(params: CollectionParams) => void` | Called after each filter, sort, or page-size change with the current params. |
|
|
820
1033
|
|
|
821
1034
|
### Return Value
|
|
822
1035
|
|
|
@@ -870,7 +1083,7 @@ The return value is identical to `useCollectionVariables` — `variables` and `c
|
|
|
870
1083
|
|
|
871
1084
|
### Options
|
|
872
1085
|
|
|
873
|
-
All options accepted by `useCollectionVariables` are accepted here too. `tableMetadata` is optional but recommended for typed variables and correct URL round-tripping of typed field values (numbers and booleans are preserved correctly).
|
|
1086
|
+
All options accepted by `useCollectionVariables` are accepted here too. `tableMetadata` is optional but recommended for typed variables and correct URL round-tripping of typed field values (numbers and booleans are preserved correctly, and datetime filters are normalized to RFC 3339 instants before querying).
|
|
874
1087
|
|
|
875
1088
|
### URL format
|
|
876
1089
|
|
|
@@ -912,6 +1125,8 @@ function MyCustomPagination() {
|
|
|
912
1125
|
}
|
|
913
1126
|
```
|
|
914
1127
|
|
|
1128
|
+
The selection is here too — `selectedIds`, `selectAllRows` / `deselectAllRows` for the current page, and `clearSelection` — for the rare case the built-in [selection actions](#selection-actions) bar doesn't fit. Selection exposes IDs only; consumers fetch any row objects they need.
|
|
1129
|
+
|
|
915
1130
|
## SDK Plugin (`@tailor-platform/sdk-plugin-app-shell`)
|
|
916
1131
|
|
|
917
1132
|
The SDK plugin generates `tableMetadata` from TailorDB table definitions at generate time. This metadata bridges your schema to the DataTable — it specifies how each field should be rendered and filtered (e.g. date pickers for datetime fields, dropdown for enum fields).
|
|
@@ -181,6 +181,14 @@ Locale and timezone come from AppShell automatically. Override per field with `l
|
|
|
181
181
|
<DatePicker aria-label="Date" locale="ja-JP" />
|
|
182
182
|
```
|
|
183
183
|
|
|
184
|
+
### Day of the week
|
|
185
|
+
|
|
186
|
+
`showDayOfWeek` adds the locale's short day of the week where the locale places it — `2025/12/19(金)` for ja-JP, `Fri, 12/19/2025` for en-US. It is read-only and derived from the entered date: it shows a placeholder until the date is complete, and while a date segment is mid-entry it keeps the previous day of the week (muted), so typing `25` into the day never flashes the 2nd's. Screen readers hear the full day name with each date segment's value.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<DatePicker aria-label="Closing date" showDayOfWeek />
|
|
190
|
+
```
|
|
191
|
+
|
|
184
192
|
## Keyboard
|
|
185
193
|
|
|
186
194
|
- **Segments:** `↑`/`↓` increment/decrement, digits type-to-fill (auto-advance), `←`/`→` move between segments, `Backspace` clears, `/` commits the current segment and advances.
|
|
@@ -191,26 +199,29 @@ Locale and timezone come from AppShell automatically. Override per field with `l
|
|
|
191
199
|
|
|
192
200
|
### DateFieldProps
|
|
193
201
|
|
|
194
|
-
| Prop | Type | Description
|
|
195
|
-
| -------------------------------- | ------------------------------------------------------------- |
|
|
196
|
-
| `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value
|
|
197
|
-
| `onChange` | `(v: DateValue \| null) => void` | Fires when the value changes
|
|
198
|
-
| `onBlur` | `() => void` | Fires when focus leaves the whole segmented control
|
|
199
|
-
| `minValue` / `maxValue` | `DateValue` | Inclusive date range bounds
|
|
200
|
-
| `isDateUnavailable` | `(date: DateValue) => boolean` | Marks specific dates unavailable
|
|
201
|
-
| `isDisabled` | `boolean` | Disables interaction and form submission
|
|
202
|
-
| `isReadOnly` | `boolean` | Allows focus/navigation without editing
|
|
203
|
-
| `isRequired` | `boolean` | Marks the control required
|
|
204
|
-
| `isInvalid` | `boolean` | Adds invalid styling / `aria-invalid` to the segmented UI
|
|
205
|
-
| `placeholderValue` | `DateValue` | Seeds unset segments
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
202
|
+
| Prop | Type | Description |
|
|
203
|
+
| -------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------ |
|
|
204
|
+
| `value` / `defaultValue` | `DateValue \| null` | Controlled / uncontrolled value |
|
|
205
|
+
| `onChange` | `(v: DateValue \| null) => void` | Fires when the value changes |
|
|
206
|
+
| `onBlur` | `() => void` | Fires when focus leaves the whole segmented control |
|
|
207
|
+
| `minValue` / `maxValue` | `DateValue` | Inclusive date range bounds |
|
|
208
|
+
| `isDateUnavailable` | `(date: DateValue) => boolean` | Marks specific dates unavailable |
|
|
209
|
+
| `isDisabled` | `boolean` | Disables interaction and form submission |
|
|
210
|
+
| `isReadOnly` | `boolean` | Allows focus/navigation without editing |
|
|
211
|
+
| `isRequired` | `boolean` | Marks the control required |
|
|
212
|
+
| `isInvalid` | `boolean` | Adds invalid styling / `aria-invalid` to the segmented UI |
|
|
213
|
+
| `placeholderValue` | `DateValue` | Seeds unset segments |
|
|
214
|
+
| `granularity` | `"day" \| "hour" \| "minute" \| "second"` | Smallest editable unit; time granularities add time segments |
|
|
215
|
+
| `hourCycle` | `12 \| 24` | Force 12- or 24-hour time; defaults to the locale |
|
|
216
|
+
| `autoFocus` | `boolean` | Focus the first segment on mount |
|
|
217
|
+
| `locale` | `string` | BCP-47 locale override |
|
|
218
|
+
| `showDayOfWeek` | `boolean` | Show the locale's short day of the week next to the date |
|
|
219
|
+
| `name` | `string` | Emits a form value through the proxy input |
|
|
220
|
+
| `id` | `string` | Proxy input id (use with external `<label htmlFor>`). |
|
|
221
|
+
| `firstDayOfWeek` | `"sun" \| "mon" \| "tue" \| "wed" \| "thu" \| "fri" \| "sat"` | Override the locale week start used by `w` / `k` shortcuts |
|
|
222
|
+
| `aria-label` / `aria-labelledby` | `string` | Accessible name |
|
|
223
|
+
| `aria-describedby` | `string` | IDs of description / error elements |
|
|
224
|
+
| `className` | `string` | Root element class |
|
|
214
225
|
|
|
215
226
|
### DatePickerProps
|
|
216
227
|
|
|
@@ -227,7 +238,7 @@ See the calendar docs in-code: controlled/uncontrolled value, min/max, unavailab
|
|
|
227
238
|
|
|
228
239
|
### DateRangePickerProps
|
|
229
240
|
|
|
230
|
-
The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `firstDayOfWeek`, `timeZone`, `locale`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:
|
|
241
|
+
The `DatePickerProps` surface (labeling, `isInvalid`, `min/maxValue`, `isDateUnavailable`, `granularity`, `hourCycle`, `firstDayOfWeek`, `timeZone`, `locale`, `showDayOfWeek`, `autoFocus`, `isDisabled/ReadOnly/Required`), with the range-specific differences:
|
|
231
242
|
|
|
232
243
|
| Prop | Type | Description |
|
|
233
244
|
| ------------------------ | -------------------------------- | ------------------------------------------------------------------------------- |
|