@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.
Files changed (42) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +692 -106
  3. package/dist/app-shell.js +6294 -4939
  4. package/dist/{appshell-B0wh5Lq0.js → appshell-CahhZy7v.js} +316 -297
  5. package/dist/semantic-contrast.test.ts +154 -0
  6. package/dist/style-entry.d.ts +117 -82
  7. package/dist/testing.d.ts +122 -85
  8. package/dist/testing.js +1 -1
  9. package/dist/theme-bridge.test.ts +101 -1
  10. package/dist/theme.bridge.css +91 -0
  11. package/dist/theme.css +6 -1
  12. package/dist/themes/_template.css +7 -1
  13. package/dist/themes/bloom.css +0 -6
  14. package/dist/themes/cream.css +0 -6
  15. package/dist/themes/default.css +389 -69
  16. package/dist/vite-plugin.d.ts +117 -82
  17. package/package.json +1 -1
  18. package/skills/app-shell-patterns/SKILL.md +7 -5
  19. package/skills/app-shell-patterns/references/api/define-module.md +2 -2
  20. package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
  21. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
  22. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
  23. package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
  24. package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
  25. package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
  26. package/skills/app-shell-patterns/references/components/card.md +79 -7
  27. package/skills/app-shell-patterns/references/components/data-table.md +267 -52
  28. package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
  29. package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
  30. package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -0
  31. package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
  32. package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
  33. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
  34. package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
  35. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
  36. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
  37. package/skills/app-shell-patterns/references/concepts/styling-theming.md +255 -58
  38. package/skills/app-shell-patterns/references/migrations.md +21 -0
  39. package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
  40. package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
  41. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
  42. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +2 -2
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Multi Select
3
- description: Floating bottom action bar for bulk operations on selected list rows
3
+ description: Bulk operations on selected DataTable rows, from a bar that takes over the table footer
4
4
  ---
5
5
 
6
6
  <!-- GENERATED by docs-kit from docs-src/patterns/interaction-multi-select.docs.outline.md — do not edit by hand. -->
@@ -10,28 +10,27 @@ description: Floating bottom action bar for bulk operations on selected list row
10
10
  ## When to Use
11
11
 
12
12
  - ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete)
13
- - Selection is initiated by clicking a leading-column checkbox on rows
14
- - Selection state needs to persist across pagination and filter changes
13
+ - The list is a `DataTable` — selection and the bulk-action bar are built in; you only declare the actions
14
+ - Some actions apply to only part of a selection (approve only pending rows, delete only archived ones)
15
15
 
16
16
  ## Layout
17
17
 
18
- Floating action bar appears the moment selection count goes from 0 → 1, anchored to the bottom of the viewport, centered horizontally, with elevation. It disappears when selection returns to 0.
18
+ Pass `rowSelection.actions` to `useDataTable`. The moment one row is selected, `DataTable.Footer` becomes the bulk-action bar: a tinted strip with the selection count, the actions, and Clear, with pagination kept on the right. It turns back into the normal footer when the selection empties. In `<Layout fill>` the footer is already pinned; on a page that scrolls, the bar sticks to the bottom of the viewport until the end of the table is reached.
19
19
 
20
20
  ```
21
- +---------------------------------------------------------+
22
- | Layout.Header title [Filter] [Create] |
23
- +---------------------------------------------------------+
24
- | Layout.Column |
25
- | Table.Root |
26
- | [x] | Col | Col | Col | Col |
27
- | [x] | row | row | row | row |
28
- | [ ] | row | row | row | row |
29
- | [x] | row | row | row | row |
30
- | |
31
- | +--------------------------------------+ |
32
- | | 3 selected [Archive] [Export] [⋯] [Clear] | |
33
- | +--------------------------------------+ |
34
- +---------------------------------------------------------+
21
+ +--------------------------------------------------------------------+
22
+ | Layout.Header title [Create] |
23
+ +--------------------------------------------------------------------+
24
+ | Layout.Column |
25
+ | DataTable.Root |
26
+ | Toolbar [Filters] [Columns] |
27
+ | [x] | Col | Col | Col | Col |
28
+ | [x] | row | row | row | row |
29
+ | [ ] | row | row | row | row |
30
+ | [x] | row | row | row | row |
31
+ | DataTable.Footer, while rows are selected: |
32
+ | 3 of 240 selected | [Confirm] [Export] [⋯] | Clear ‹ 1/10 › |
33
+ +--------------------------------------------------------------------+
35
34
  ```
36
35
 
37
36
  ## Page Implementation
@@ -40,120 +39,132 @@ Floating action bar appears the moment selection count goes from 0 → 1, anchor
40
39
 
41
40
  ```tsx
42
41
  function InteractionMultiSelect() {
43
- const orders = mockOrders;
44
- const onArchive = (ids: string[]) => window.alert(`Archiving ${ids.length}`);
45
- const onExport = (ids: string[]) => window.alert(`Exporting ${ids.length}`);
46
-
47
- const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
48
-
49
- const toggleRow = (id: string) => {
50
- setSelectedIds((prev) => {
51
- const next = new Set(prev);
52
- if (next.has(id)) next.delete(id);
53
- else next.add(id);
54
- return next;
55
- });
42
+ const [selectedIds, setSelectedIds] = useState<string[]>([]);
43
+ const [pendingCancel, setPendingCancel] = useState<PendingCancel | null>(null);
44
+ const [cancelling, setCancelling] = useState(false);
45
+ const [cancelError, setCancelError] = useState<string | null>(null);
46
+
47
+ const removeCompletedIds = (ids: string[]) => {
48
+ const completed = new Set(ids);
49
+ setSelectedIds((current) => current.filter((id) => !completed.has(id)));
56
50
  };
57
51
 
58
- const toggleAll = () => {
59
- if (selectedIds.size === orders.length) {
60
- setSelectedIds(new Set());
61
- } else {
62
- setSelectedIds(new Set(orders.map((o) => o.id)));
63
- }
64
- };
65
-
66
- const clearSelection = () => setSelectedIds(new Set());
67
- const selectedCount = selectedIds.size;
52
+ const actions: SelectionAction[] = [
53
+ {
54
+ id: "confirm",
55
+ label: "Confirm",
56
+ // The operation returns actual successes; never clear ineligible or newer selections.
57
+ onClick: async (ids) => removeCompletedIds(await confirmOrders(ids)),
58
+ },
59
+ {
60
+ id: "export",
61
+ label: "Export",
62
+ onClick: (ids) => exportOrders(ids),
63
+ },
64
+ {
65
+ id: "assign",
66
+ label: "Assign owner",
67
+ onClick: async (ids) => window.alert(`Assigning ${ids.length} order(s)`),
68
+ },
69
+ {
70
+ // 4th action: lands in the More actions menu.
71
+ id: "cancel",
72
+ label: "Cancel",
73
+ variant: "destructive",
74
+ onClick: (ids, { run }) => {
75
+ setCancelError(null);
76
+ setPendingCancel({ ids: [...ids], run });
77
+ },
78
+ },
79
+ ];
80
+
81
+ // In a real app, fetch eligibility for these snapshot ids before showing this explanation.
82
+ const cancellableCount = mockOrders.filter(
83
+ (order) => pendingCancel?.ids.includes(order.id) && order.status !== "Shipped",
84
+ ).length;
85
+
86
+ const table = useDataTable({
87
+ columns,
88
+ data: { rows: mockOrders, total: mockOrders.length },
89
+ rowSelection: { selectedIds, onChange: setSelectedIds, actions },
90
+ });
68
91
 
69
92
  return (
70
93
  <>
71
- <Table.Root>
72
- <Table.Header>
73
- <Table.Row>
74
- <Table.Head className="w-10">
75
- <input
76
- type="checkbox"
77
- checked={selectedIds.size === orders.length && orders.length > 0}
78
- onChange={toggleAll}
79
- aria-label="Select all on page"
80
- />
81
- </Table.Head>
82
- <Table.Head>Order #</Table.Head>
83
- <Table.Head>Status</Table.Head>
84
- <Table.Head>Total</Table.Head>
85
- </Table.Row>
86
- </Table.Header>
87
- <Table.Body>
88
- {orders.map((order) => (
89
- <Table.Row key={order.id}>
90
- <Table.Cell>
91
- <input
92
- type="checkbox"
93
- checked={selectedIds.has(order.id)}
94
- onChange={() => toggleRow(order.id)}
95
- aria-label={`Select ${order.number}`}
96
- />
97
- </Table.Cell>
98
- <Table.Cell>{order.number}</Table.Cell>
99
- <Table.Cell>{order.status}</Table.Cell>
100
- <Table.Cell>${order.total.toLocaleString()}</Table.Cell>
101
- </Table.Row>
102
- ))}
103
- </Table.Body>
104
- </Table.Root>
105
-
106
- {selectedCount > 0 && (
107
- <div
108
- role="toolbar"
109
- aria-label="Bulk actions"
110
- className="fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-md border bg-popover px-3 py-2 shadow-lg"
111
- >
112
- <span className="text-sm font-medium">{selectedCount} selected</span>
113
- <Button size="sm" onClick={() => onArchive([...selectedIds])}>
114
- Archive
115
- </Button>
116
- <Button size="sm" variant="outline" onClick={() => onExport([...selectedIds])}>
117
- Export
118
- </Button>
119
- <Menu.Root>
120
- <Menu.Trigger>
121
- <Button size="sm" variant="outline" aria-label="More actions">
122
- ⋯
123
- </Button>
124
- </Menu.Trigger>
125
- <Menu.Content>
126
- <Menu.Item>Assign owner</Menu.Item>
127
- <Menu.Item>Tag</Menu.Item>
128
- <Menu.Separator />
129
- <Menu.Item>Delete</Menu.Item>
130
- </Menu.Content>
131
- </Menu.Root>
132
- <Button size="sm" variant="ghost" onClick={clearSelection}>
133
- Clear
134
- </Button>
135
- </div>
136
- )}
94
+ <DataTable.Root value={table}>
95
+ <DataTable.Table />
96
+ <DataTable.Footer>
97
+ <DataTable.Pagination />
98
+ </DataTable.Footer>
99
+ </DataTable.Root>
100
+
101
+ <Dialog.Root
102
+ open={pendingCancel !== null}
103
+ onOpenChange={(open) => {
104
+ if (!open && !cancelling) setPendingCancel(null);
105
+ }}
106
+ >
107
+ <Dialog.Content>
108
+ <Dialog.Header>
109
+ <Dialog.Title>Cancel selected orders?</Dialog.Title>
110
+ <Dialog.Description>
111
+ {cancellableCount} of {pendingCancel?.ids.length ?? 0} selected orders can be
112
+ cancelled; shipped orders are excluded. Cancelled orders can&apos;t be reopened.
113
+ </Dialog.Description>
114
+ </Dialog.Header>
115
+ {cancelError && <p role="alert">{cancelError}</p>}
116
+ <Dialog.Footer>
117
+ <Dialog.Close render={<Button variant="outline" disabled={cancelling} />}>
118
+ Keep orders
119
+ </Dialog.Close>
120
+ <Button
121
+ variant="destructive"
122
+ disabled={cancelling || cancellableCount === 0}
123
+ onClick={async () => {
124
+ if (!pendingCancel || cancelling) return;
125
+ setCancelling(true);
126
+ setCancelError(null);
127
+ try {
128
+ const cancelledIds = await pendingCancel.run(cancelOrders(pendingCancel.ids));
129
+ removeCompletedIds(cancelledIds);
130
+ setPendingCancel(null);
131
+ } catch {
132
+ setCancelError("Cancellation failed. Please try again.");
133
+ } finally {
134
+ setCancelling(false);
135
+ }
136
+ }}
137
+ >
138
+ {cancelling ? "Cancelling…" : "Cancel orders"}
139
+ </Button>
140
+ </Dialog.Footer>
141
+ </Dialog.Content>
142
+ </Dialog.Root>
137
143
  </>
138
144
  );
139
145
  }
140
146
  ```
141
147
 
142
- <!-- source: multi-select.tsx -->
143
-
144
148
  ## Constraints
145
149
 
146
- - Count label + Clear button are always present in the bar
147
- - Max 3 inline action buttons — 4th onward collapse behind an overflow `Menu`
148
- - Destructive bulk actions MUST open an `interaction/confirm` dialog
149
- - Filter or sort change must NOT silently clear the selection
150
- - Pagination MUST preserve selection across pages
150
+ - Declare bulk actions as the non-generic `SelectionAction[]` in `useDataTable`'s `rowSelection.actions` — do NOT hand-build the bar, a floating panel, or checkboxes; the footer renders the selection count, actions, overflow menu, and Clear. Providing `rowSelection` enables checkboxes even when `actions` is omitted or empty; the bar requires non-empty actions and selected ids
151
+ - Keep `DataTable.Footer` in the tree — it is where the bar renders
152
+ - Selection stores IDs only, not row objects. `onClick` receives a click-time copy of all selected ids (`string[]`), including unloaded ids; fetch any records required for the operation in the consumer's ID-based workflow
153
+ - The consumer owns eligibility checks, required fetching, confirmation UI, and execution; backend validation remains authoritative. Selection actions have no row-based `canApply`, automatic eligible count, or eligibility-based disabled state
154
+ - Return the promise from `onClick` for async work: the bar disables its actions and shows a spinner while it runs. Success and rejection both leave selection unchanged; there is no `keepSelection` option. Rejections are logged; show errors in the consumer UI
155
+ - Use controlled `rowSelection: { selectedIds, onChange }` to remove only ids actually changed, with a functional update against the current selection. Preserve ineligible ids and ids selected after the operation started; cancellation or failure does not clear selection
156
+ - At most 3 actions render inline, in array order; the 4th onward move to the More actions menu — order by frequency and put a destructive action last
157
+ - Destructive bulk actions MUST open an `interaction/confirm` dialog from `onClick`. Optionally keep the `run` helper and pass it the later request (`await run(deleteRows(ids))`) for pending tracking only. `clearSelection` explicitly clears every page when that is intentional
158
+ - Share only presentation fields (`id`, `label`, `icon`, `variant`) as a non-generic `DataTableAction`. `RowAction<TRow>` retains `canApply` for its concrete row; bulk eligibility belongs in the ID-based consumer workflow
159
+ - Selection persists across pagination, filter, and sort changes — do NOT clear it on those
151
160
 
152
161
  ## Anti-patterns
153
162
 
154
- - Placing bulk-action buttons in the page header — bulk actions belong only in the floating bar
155
- - Hiding the bar behind row hover or right-click — bar must be visible when selection > 0
156
- - Omitting the count or the Clear affordance — both are mandatory
157
- - Letting filter/sort changes silently drop selection
163
+ - Hand-rolled selection (`useState<Set>`, raw `<input type="checkbox">`, `Table.Root`) when the list is a DataTable
164
+ - A floating or fixed-position action bar over the table instead of the footer bar
165
+ - Placing bulk-action buttons in the page header or the table toolbar
166
+ - Treating the currently loaded rows as the full selection or deriving bulk eligibility from incomplete row data
167
+ - Clearing the whole selection after partial success, or overwriting it with the click-time snapshot and losing newer selections
168
+ - Fire-and-forget async `onClick` that doesn't return its promise or use `run` — users can fire it twice
158
169
  - Firing destructive bulk actions without an interaction/confirm step
159
170
  - Per-row `Menu` actions as a substitute for bulk actions when selection > 0
@@ -67,7 +67,7 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
67
67
  - **Toolbar chips only (`DataTable.Filters`)** — best when filters map cleanly to typed column metadata / enum facets
68
68
  - **Tabs only above `DataTable`** — best when workflows are organized as obvious buckets
69
69
  - **Tabs + chips** — when buckets are primary and finer filters help
70
- - **Bulk selection** — `onSelectionChange` hook on `useDataTable`; combine with `interaction/multi-select`
70
+ - **Bulk actions** — `rowSelection.actions` on `useDataTable` turns `DataTable.Footer` into a bulk-action bar while rows are selected; see `interaction/multi-select`
71
71
  - **`Table` primitives** — small static lists without collection hooks
72
72
 
73
73
  ## Constraints
@@ -77,7 +77,7 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
77
77
  - Table-first pages use `<Layout fill>` so title/toolbar/header/footer stay pinned and only rows scroll
78
78
  - Handle every state: `DataTable` renders the loading skeleton and error row; always provide a **labelled empty state** (what the list is + how to add the first record) rather than a bare empty table
79
79
  - Status Badge colors must use design system tokens (variant prop): the **primary** status column uses **filled** semantic variants; **secondary** status columns (delivery, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules)
80
- - Bulk actions toolbar appears only when ≥1 row is selected
80
+ - Bulk actions go in the footer bar from `rowSelection.actions`, which appears only while ≥1 row is selected — never a separate toolbar or floating bar
81
81
  - Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. No per-row "View" / "Open" buttons
82
82
  - Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete)
83
83
  - Wide lists (many columns / horizontal scroll): pin the column users scan by to the **left** — the record's identifier (invoice / order **reference**) or its **name** (customer, product) — so it stays anchored while the rest scrolls; optionally pin a single high-signal **status** or **total** to the **right**. Keep the pinned set small (≈1 left, at most 1 right) — over-pinning eats the scroll area. Let users override via `<DataTable.Toolbar columnSettings>` (show/hide + reorder + re-pin) with a stable `tableId` so each user's layout persists