@tailor-platform/app-shell 1.2.0 → 1.3.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.css +1 -1
- package/dist/app-shell.d.ts +92 -17
- package/dist/app-shell.js +2391 -2185
- package/package.json +11 -10
- package/skills/app-shell-patterns/SKILL.md +67 -0
- package/skills/app-shell-patterns/references/fundamental/components.md +704 -0
- package/skills/app-shell-patterns/references/fundamental/design-system.md +355 -0
- package/skills/app-shell-patterns/references/fundamental/graphql.md +205 -0
- package/skills/app-shell-patterns/references/patterns/detail-hero-with-actions.md +157 -0
- package/skills/app-shell-patterns/references/patterns/form-modal.md +169 -0
- package/skills/app-shell-patterns/references/patterns/form-sectioned.md +126 -0
- package/skills/app-shell-patterns/references/patterns/form-single-page.md +105 -0
- package/skills/app-shell-patterns/references/patterns/form-wizard.md +134 -0
- package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +75 -0
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +173 -0
- package/skills/app-shell-patterns/references/patterns/interaction-toast.md +69 -0
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +132 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: pattern/interaction/confirm
|
|
3
|
+
name: Confirm
|
|
4
|
+
category: pattern
|
|
5
|
+
subcategory: interaction
|
|
6
|
+
description: Confirmation dialog before destructive or irreversible actions
|
|
7
|
+
requiredImports: [Dialog, Button, Input]
|
|
8
|
+
tags: [dialog, confirm, destructive, delete, irreversible]
|
|
9
|
+
do:
|
|
10
|
+
- Before a destructive or irreversible action (delete, cancel, void, archive)
|
|
11
|
+
- Before bulk actions that affect many records
|
|
12
|
+
- When the action's consequence isn't obvious from the trigger
|
|
13
|
+
dont:
|
|
14
|
+
- Routine reversible actions — use interaction/toast with optional Undo instead
|
|
15
|
+
- Form submission for non-destructive create/edit — submit handlers don't need a confirm
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# pattern/interaction/confirm
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- Before a destructive or irreversible action (delete, cancel, void, archive)
|
|
23
|
+
- Before bulk actions that affect many records
|
|
24
|
+
- When the action's consequence isn't obvious from the trigger
|
|
25
|
+
|
|
26
|
+
## Page Implementation
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
/* pattern: interaction/confirm */
|
|
30
|
+
import { Button, Dialog } from "@tailor-platform/app-shell";
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
orderId: string;
|
|
34
|
+
onDelete: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default function ConfirmDialog({ orderId, onDelete }: Props) {
|
|
38
|
+
return (
|
|
39
|
+
<Dialog.Root>
|
|
40
|
+
<Dialog.Trigger render={<Button variant="destructive" />}>Delete</Dialog.Trigger>
|
|
41
|
+
<Dialog.Content>
|
|
42
|
+
<Dialog.Header>
|
|
43
|
+
<Dialog.Title>Delete order {orderId}?</Dialog.Title>
|
|
44
|
+
<Dialog.Description>
|
|
45
|
+
This will remove the order and all its line items. This action cannot be undone.
|
|
46
|
+
</Dialog.Description>
|
|
47
|
+
</Dialog.Header>
|
|
48
|
+
<Dialog.Footer>
|
|
49
|
+
<Dialog.Close render={<Button variant="outline" />}>Cancel</Dialog.Close>
|
|
50
|
+
<Button variant="destructive" onClick={onDelete}>
|
|
51
|
+
Delete
|
|
52
|
+
</Button>
|
|
53
|
+
</Dialog.Footer>
|
|
54
|
+
</Dialog.Content>
|
|
55
|
+
</Dialog.Root>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Copy Rules
|
|
61
|
+
|
|
62
|
+
- Title is a question, names the object: "Delete order ORD-1234?", "Cancel invoice INV-001?"
|
|
63
|
+
- Body names the object and the consequence: what will change, what will be lost, whether it's reversible
|
|
64
|
+
- Confirm button verb matches the title: "Delete", "Cancel invoice" — never "OK" or "Yes"
|
|
65
|
+
|
|
66
|
+
## Constraints
|
|
67
|
+
|
|
68
|
+
- Dialog renders as bottom sheet below 1024px; centered max-w-sm at 1024+
|
|
69
|
+
- Confirm button must use `variant="destructive"` for destructive actions
|
|
70
|
+
|
|
71
|
+
## Anti-patterns
|
|
72
|
+
|
|
73
|
+
- Vague titles like "Are you sure?" — gives users nothing to evaluate
|
|
74
|
+
- Cancel rendered as the primary visual treatment — promotes the wrong default
|
|
75
|
+
- Confirm button without `variant="destructive"` for destructive actions — no visual signal
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: pattern/interaction/multi-select
|
|
3
|
+
name: Multi Select
|
|
4
|
+
category: pattern
|
|
5
|
+
subcategory: interaction
|
|
6
|
+
description: Floating bottom action bar for bulk operations on selected list rows
|
|
7
|
+
requiredImports: [Table, Checkbox, Button, Menu]
|
|
8
|
+
tags: [bulk, selection, toolbar, floating-bar, multi-select, batch]
|
|
9
|
+
do:
|
|
10
|
+
- ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete)
|
|
11
|
+
- Selection is initiated by clicking a leading-column checkbox on rows
|
|
12
|
+
- Selection state needs to persist across pagination and filter changes
|
|
13
|
+
dont:
|
|
14
|
+
- A list where bulk action is genuinely impossible (single-select only)
|
|
15
|
+
- A pure picker/selector inside a Dialog whose footer already gates the action
|
|
16
|
+
- Destructive bulk action triggered without confirmation — pair with interaction/confirm
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
# pattern/interaction/multi-select
|
|
20
|
+
|
|
21
|
+
## When to Use
|
|
22
|
+
|
|
23
|
+
- ANY list page where rows can be acted on in bulk (archive, assign, export, approve, delete)
|
|
24
|
+
- Selection is initiated by clicking a leading-column checkbox on rows
|
|
25
|
+
- Selection state needs to persist across pagination and filter changes
|
|
26
|
+
|
|
27
|
+
## Layout
|
|
28
|
+
|
|
29
|
+
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.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
+---------------------------------------------------------+
|
|
33
|
+
| Layout.Header title [Filter] [Create] |
|
|
34
|
+
+---------------------------------------------------------+
|
|
35
|
+
| Layout.Column |
|
|
36
|
+
| Table.Root |
|
|
37
|
+
| [x] | Col | Col | Col | Col |
|
|
38
|
+
| [x] | row | row | row | row |
|
|
39
|
+
| [ ] | row | row | row | row |
|
|
40
|
+
| [x] | row | row | row | row |
|
|
41
|
+
| |
|
|
42
|
+
| +--------------------------------------+ |
|
|
43
|
+
| | 3 selected [Archive] [Export] [⋯] [Clear] | |
|
|
44
|
+
| +--------------------------------------+ |
|
|
45
|
+
+---------------------------------------------------------+
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Page Implementation
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
/* pattern: interaction/multi-select */
|
|
52
|
+
import { useState } from "react";
|
|
53
|
+
import { Button, Table, Menu } from "@tailor-platform/app-shell";
|
|
54
|
+
import type { Order } from "./mock";
|
|
55
|
+
|
|
56
|
+
type Props = {
|
|
57
|
+
orders: Order[];
|
|
58
|
+
onArchive: (ids: string[]) => void;
|
|
59
|
+
onExport: (ids: string[]) => void;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export default function MultiSelect({ orders, onArchive, onExport }: Props) {
|
|
63
|
+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
64
|
+
|
|
65
|
+
const toggleRow = (id: string) => {
|
|
66
|
+
setSelectedIds((prev) => {
|
|
67
|
+
const next = new Set(prev);
|
|
68
|
+
if (next.has(id)) next.delete(id);
|
|
69
|
+
else next.add(id);
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const toggleAll = () => {
|
|
75
|
+
if (selectedIds.size === orders.length) {
|
|
76
|
+
setSelectedIds(new Set());
|
|
77
|
+
} else {
|
|
78
|
+
setSelectedIds(new Set(orders.map((o) => o.id)));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const clearSelection = () => setSelectedIds(new Set());
|
|
83
|
+
const selectedCount = selectedIds.size;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<Table.Root>
|
|
88
|
+
<Table.Header>
|
|
89
|
+
<Table.Row>
|
|
90
|
+
<Table.Head className="astw:w-10">
|
|
91
|
+
<input
|
|
92
|
+
type="checkbox"
|
|
93
|
+
checked={selectedIds.size === orders.length && orders.length > 0}
|
|
94
|
+
onChange={toggleAll}
|
|
95
|
+
aria-label="Select all on page"
|
|
96
|
+
/>
|
|
97
|
+
</Table.Head>
|
|
98
|
+
<Table.Head>Order #</Table.Head>
|
|
99
|
+
<Table.Head>Status</Table.Head>
|
|
100
|
+
<Table.Head>Total</Table.Head>
|
|
101
|
+
</Table.Row>
|
|
102
|
+
</Table.Header>
|
|
103
|
+
<Table.Body>
|
|
104
|
+
{orders.map((order) => (
|
|
105
|
+
<Table.Row key={order.id}>
|
|
106
|
+
<Table.Cell>
|
|
107
|
+
<input
|
|
108
|
+
type="checkbox"
|
|
109
|
+
checked={selectedIds.has(order.id)}
|
|
110
|
+
onChange={() => toggleRow(order.id)}
|
|
111
|
+
aria-label={`Select ${order.number}`}
|
|
112
|
+
/>
|
|
113
|
+
</Table.Cell>
|
|
114
|
+
<Table.Cell>{order.number}</Table.Cell>
|
|
115
|
+
<Table.Cell>{order.status}</Table.Cell>
|
|
116
|
+
<Table.Cell>${order.total.toLocaleString()}</Table.Cell>
|
|
117
|
+
</Table.Row>
|
|
118
|
+
))}
|
|
119
|
+
</Table.Body>
|
|
120
|
+
</Table.Root>
|
|
121
|
+
|
|
122
|
+
{selectedCount > 0 && (
|
|
123
|
+
<div
|
|
124
|
+
role="toolbar"
|
|
125
|
+
aria-label="Bulk actions"
|
|
126
|
+
className="fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-md border bg-surface-2 px-3 py-2 shadow-lg"
|
|
127
|
+
>
|
|
128
|
+
<span className="text-sm font-medium">{selectedCount} selected</span>
|
|
129
|
+
<Button size="sm" onClick={() => onArchive([...selectedIds])}>
|
|
130
|
+
Archive
|
|
131
|
+
</Button>
|
|
132
|
+
<Button size="sm" variant="outline" onClick={() => onExport([...selectedIds])}>
|
|
133
|
+
Export
|
|
134
|
+
</Button>
|
|
135
|
+
<Menu.Root>
|
|
136
|
+
<Menu.Trigger>
|
|
137
|
+
<Button size="sm" variant="outline" aria-label="More actions">
|
|
138
|
+
⋯
|
|
139
|
+
</Button>
|
|
140
|
+
</Menu.Trigger>
|
|
141
|
+
<Menu.Content>
|
|
142
|
+
<Menu.Item>Assign owner</Menu.Item>
|
|
143
|
+
<Menu.Item>Tag</Menu.Item>
|
|
144
|
+
<Menu.Separator />
|
|
145
|
+
<Menu.Item>Delete</Menu.Item>
|
|
146
|
+
</Menu.Content>
|
|
147
|
+
</Menu.Root>
|
|
148
|
+
<Button size="sm" variant="ghost" onClick={clearSelection}>
|
|
149
|
+
Clear
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Constraints
|
|
159
|
+
|
|
160
|
+
- Count label + Clear button are always present in the bar
|
|
161
|
+
- Max 3 inline action buttons — 4th onward collapse behind an overflow `Menu`
|
|
162
|
+
- Destructive bulk actions MUST open an `interaction/confirm` dialog
|
|
163
|
+
- Filter or sort change must NOT silently clear the selection
|
|
164
|
+
- Pagination MUST preserve selection across pages
|
|
165
|
+
|
|
166
|
+
## Anti-patterns
|
|
167
|
+
|
|
168
|
+
- Placing bulk-action buttons in the page header — bulk actions belong only in the floating bar
|
|
169
|
+
- Hiding the bar behind row hover or right-click — bar must be visible when selection > 0
|
|
170
|
+
- Omitting the count or the Clear affordance — both are mandatory
|
|
171
|
+
- Letting filter/sort changes silently drop selection
|
|
172
|
+
- Firing destructive bulk actions without an interaction/confirm step
|
|
173
|
+
- Per-row `Menu` actions as a substitute for bulk actions when selection > 0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: pattern/interaction/toast
|
|
3
|
+
name: Toast
|
|
4
|
+
category: pattern
|
|
5
|
+
subcategory: interaction
|
|
6
|
+
description: Lightweight feedback after mutations — success or error notifications
|
|
7
|
+
requiredImports: [Button]
|
|
8
|
+
tags: [toast, feedback, notification, mutation, success, error]
|
|
9
|
+
do:
|
|
10
|
+
- Feedback after a mutation (create, update, delete) — success or error
|
|
11
|
+
- Lightweight async signal that doesn't need to block the UI
|
|
12
|
+
- Confirming work the user just initiated
|
|
13
|
+
dont:
|
|
14
|
+
- Destructive action that has not yet executed — use interaction/confirm
|
|
15
|
+
- Long-running blocking operation — use an inline progress UI, not a toast
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# pattern/interaction/toast
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- Feedback after a mutation (create, update, delete) — success or error
|
|
23
|
+
- Lightweight async signal that doesn't need to block the UI
|
|
24
|
+
- Confirming work the user just initiated
|
|
25
|
+
|
|
26
|
+
## Page Implementation
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
/* pattern: interaction/toast */
|
|
30
|
+
import { Button, useToast } from "@tailor-platform/app-shell";
|
|
31
|
+
|
|
32
|
+
type Props = {
|
|
33
|
+
orderId: string;
|
|
34
|
+
onApprove: () => Promise<void>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default function ToastExample({ orderId, onApprove }: Props) {
|
|
38
|
+
const toast = useToast();
|
|
39
|
+
|
|
40
|
+
const handleApprove = async () => {
|
|
41
|
+
try {
|
|
42
|
+
await onApprove();
|
|
43
|
+
toast.success(`Order ${orderId} approved`);
|
|
44
|
+
} catch {
|
|
45
|
+
toast.error("Failed to approve order. Try again.");
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return <Button onClick={handleApprove}>Approve</Button>;
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Copy Rules
|
|
54
|
+
|
|
55
|
+
- Success: name what happened, including the object identifier. "Order #1234 created", "Product archived".
|
|
56
|
+
- Error: state what failed and why. "Failed to save: SKU already exists", "Couldn't archive product: network error".
|
|
57
|
+
- Avoid generic messages like "Success" or "Something went wrong".
|
|
58
|
+
|
|
59
|
+
## Constraints
|
|
60
|
+
|
|
61
|
+
- Toast renders as a top-right overlay; mobile (<1024) anchors to bottom-center
|
|
62
|
+
- Stack max one visible at a time; replace prior toast on new emission
|
|
63
|
+
- Success auto-dismisses after 3s; Error is sticky (no auto-dismiss)
|
|
64
|
+
|
|
65
|
+
## Anti-patterns
|
|
66
|
+
|
|
67
|
+
- Toast on every navigation — creates noise; reserve for mutation feedback
|
|
68
|
+
- More than one toast stacking — replace, don't accumulate
|
|
69
|
+
- Blocking the UI on a toast — toasts are non-modal by definition
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
slug: pattern/list/dense-scan
|
|
3
|
+
name: Dense Scan List
|
|
4
|
+
category: pattern
|
|
5
|
+
subcategory: list
|
|
6
|
+
description: High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination
|
|
7
|
+
requiredImports:
|
|
8
|
+
[
|
|
9
|
+
DataTable,
|
|
10
|
+
useDataTable,
|
|
11
|
+
useCollectionVariables,
|
|
12
|
+
createColumnHelper,
|
|
13
|
+
Layout,
|
|
14
|
+
Card,
|
|
15
|
+
Button,
|
|
16
|
+
Badge,
|
|
17
|
+
Link,
|
|
18
|
+
Menu,
|
|
19
|
+
Tabs,
|
|
20
|
+
]
|
|
21
|
+
tags: [table, bulk-action, filter, pagination, datatable, connection]
|
|
22
|
+
do:
|
|
23
|
+
- Browsing many records of one entity type (orders, POs, products) with GraphQL pagination
|
|
24
|
+
- Operators sort, filter, and select rows; row click navigates to detail
|
|
25
|
+
- Optionally a bucket control (Tabs) aligned to one categorical dimension (status, type)
|
|
26
|
+
dont:
|
|
27
|
+
- Side-by-side match/reconcile views comparing two grids
|
|
28
|
+
- A tiny/static list where DataTable would be heavyweight — use Table.Root manually
|
|
29
|
+
- Inline editable cells — use pattern/detail or pattern/form/modal instead
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
# pattern/list/dense-scan
|
|
33
|
+
|
|
34
|
+
## When to Use
|
|
35
|
+
|
|
36
|
+
- Browsing many records of one entity type (orders, POs, products, invoices) with GraphQL pagination
|
|
37
|
+
- Operators sort, filter, and select rows; row click navigates to detail
|
|
38
|
+
- Optionally: a bucket control (`Tabs`, segmented buttons) aligned to one categorical dimension the business cares about (status, fulfillment stage, type)
|
|
39
|
+
|
|
40
|
+
## Column Definition
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import type { Column } from "@tailor-platform/app-shell";
|
|
44
|
+
import { Badge } from "@tailor-platform/app-shell";
|
|
45
|
+
|
|
46
|
+
export type Order = {
|
|
47
|
+
id: string;
|
|
48
|
+
orderNumber: string;
|
|
49
|
+
customer: string;
|
|
50
|
+
status: "draft" | "confirmed" | "shipped" | "delivered";
|
|
51
|
+
amount: number;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const statusVariant = {
|
|
56
|
+
draft: "neutral",
|
|
57
|
+
confirmed: "outline-info",
|
|
58
|
+
shipped: "outline-warning",
|
|
59
|
+
delivered: "outline-success",
|
|
60
|
+
} as const;
|
|
61
|
+
|
|
62
|
+
export const columns: Column<Order>[] = [
|
|
63
|
+
{ label: "Order #", accessor: (row) => row.orderNumber },
|
|
64
|
+
{ label: "Customer", accessor: (row) => row.customer },
|
|
65
|
+
{
|
|
66
|
+
label: "Status",
|
|
67
|
+
render: (row) => <Badge variant={statusVariant[row.status]}>{row.status}</Badge>,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "Amount",
|
|
71
|
+
render: (row) => `${row.amount.toLocaleString()}`,
|
|
72
|
+
},
|
|
73
|
+
{ label: "Created", accessor: (row) => row.createdAt },
|
|
74
|
+
];
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Page Implementation
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
/* pattern: list/dense-scan */
|
|
81
|
+
import { DataTable, useDataTable, Button, Input } from "@tailor-platform/app-shell";
|
|
82
|
+
import type { Order } from "./columns";
|
|
83
|
+
import { columns } from "./columns";
|
|
84
|
+
import type { DataTableData } from "@tailor-platform/app-shell";
|
|
85
|
+
|
|
86
|
+
type Props = {
|
|
87
|
+
data: DataTableData<Order>;
|
|
88
|
+
onCreateClick: () => void;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default function DenseScanList({ data, onCreateClick }: Props) {
|
|
92
|
+
const table = useDataTable({ data, columns });
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<div className="space-y-4">
|
|
96
|
+
<div className="flex items-center justify-between">
|
|
97
|
+
<Input placeholder="Search orders..." className="max-w-sm" />
|
|
98
|
+
<Button onClick={onCreateClick}>Create Order</Button>
|
|
99
|
+
</div>
|
|
100
|
+
<DataTable.Root value={table}>
|
|
101
|
+
<DataTable.Table />
|
|
102
|
+
</DataTable.Root>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Variants
|
|
109
|
+
|
|
110
|
+
- **Toolbar chips only (`DataTable.Filters`)** — best when filters map cleanly to typed column metadata / enum facets
|
|
111
|
+
- **Tabs only above `DataTable`** — best when workflows are organized as obvious buckets
|
|
112
|
+
- **Tabs + chips** — when buckets are primary and finer filters help
|
|
113
|
+
- **Bulk selection** — `onSelectionChange` hook on `useDataTable`; combine with `interaction/multi-select`
|
|
114
|
+
- **`Table` primitives** — small static lists without collection hooks
|
|
115
|
+
|
|
116
|
+
## Constraints
|
|
117
|
+
|
|
118
|
+
- Column count: 4-8 recommended
|
|
119
|
+
- Must include pagination — never render unbounded lists
|
|
120
|
+
- Status Badge colors must use design system tokens (variant prop)
|
|
121
|
+
- Bulk actions toolbar appears only when ≥1 row is selected
|
|
122
|
+
- Whole row is clickable via `onClickRow`; no per-row "View" / "Open" buttons
|
|
123
|
+
- Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete)
|
|
124
|
+
|
|
125
|
+
## Anti-patterns
|
|
126
|
+
|
|
127
|
+
- Building a bespoke table + custom pagination instead of `DataTable` + `useCollectionVariables`
|
|
128
|
+
- Tabs that mutate only local UI state while pagination/filters assume the full server set
|
|
129
|
+
- Using `<table>` directly instead of `<DataTable>` for live collections
|
|
130
|
+
- Client-side filtering on 1000+ records without server-side support
|
|
131
|
+
- Inline editable cells — use `pattern/detail/*` or `pattern/form/modal` instead
|
|
132
|
+
- Per-row "View" / "Open" buttons duplicating the row-click navigation
|