@tailor-platform/app-shell 1.2.0 → 1.4.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.
@@ -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