@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Multi Select
|
|
3
|
-
description:
|
|
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
|
-
-
|
|
14
|
-
-
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
24
|
-
| Layout.Column
|
|
25
|
-
|
|
|
26
|
-
| [
|
|
27
|
-
| [x] |
|
|
28
|
-
| [
|
|
29
|
-
| [
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
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
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setSelectedIds((
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
<
|
|
72
|
-
<Table
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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'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
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
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
|
-
-
|
|
155
|
-
-
|
|
156
|
-
-
|
|
157
|
-
-
|
|
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
|
|
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
|
|
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
|