@tailor-platform/app-shell 1.8.0 → 1.10.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.
@@ -146,7 +146,8 @@ html
146
146
  background-color: transparent;
147
147
  }
148
148
 
149
- html [data-slot="button"][class*="astw:bg-background"][class*="astw:border"] {
149
+ html [data-slot="button"][class*="astw:bg-background"][class*="astw:border"],
150
+ html [data-slot="data-table-filter-chip"] {
150
151
  background-color: transparent;
151
152
  transition:
152
153
  background-color 0.12s ease,
@@ -146,7 +146,8 @@ html
146
146
  background-color: transparent;
147
147
  }
148
148
 
149
- html [data-slot="button"][class*="astw:bg-background"][class*="astw:border"] {
149
+ html [data-slot="button"][class*="astw:bg-background"][class*="astw:border"],
150
+ html [data-slot="data-table-filter-chip"] {
150
151
  background-color: transparent;
151
152
  transition:
152
153
  background-color 0.12s ease,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/app-shell",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "An opinionated React application framework for building ERP applications on Tailor Platform",
5
5
  "keywords": [
6
6
  "app-shell",
@@ -46,7 +46,7 @@
46
46
  "@base-ui/react": "^1.5.0",
47
47
  "@internationalized/date": "3.12.2",
48
48
  "@standard-schema/spec": "^1.1.0",
49
- "@tailor-platform/auth-public-client": "^0.5.0",
49
+ "@tailor-platform/auth-public-client": "^0.5.1",
50
50
  "change-case": "^5.4.4",
51
51
  "class-variance-authority": "^0.7.1",
52
52
  "clsx": "^2.1.1",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: app-shell-patterns
3
- description: UI pattern catalog for building pages with @tailor-platform/app-shell components
3
+ description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — or when choosing the right AppShell component, layout, or design token for a UI."
4
4
  ---
5
5
 
6
6
  # App-Shell Patterns
@@ -13,41 +13,41 @@ Select and implement the correct UI pattern using @tailor-platform/app-shell com
13
13
 
14
14
  These are the foundational rules that underpin all patterns. All patterns build on top of these references.
15
15
 
16
- | File | Description |
17
- | ---- | ----------- |
18
- | [components.md](references/fundamental/components.md) | components reference |
16
+ | File | Description |
17
+ | ----------------------------------------------------------- | ----------------------- |
18
+ | [components.md](references/fundamental/components.md) | components reference |
19
19
  | [design-system.md](references/fundamental/design-system.md) | design-system reference |
20
- | [graphql.md](references/fundamental/graphql.md) | graphql reference |
20
+ | [graphql.md](references/fundamental/graphql.md) | graphql reference |
21
21
 
22
22
  ## Available Patterns
23
23
 
24
24
  ### detail
25
25
 
26
- | Slug | Name | Description |
27
- | ---- | ---- | ----------- |
26
+ | Slug | Name | Description |
27
+ | ----------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------- |
28
28
  | [`detail/hero-with-actions`](references/patterns/detail-hero-with-actions.md) | Hero With Actions Detail | Single-record detail view with workflow actions and activity timeline |
29
29
 
30
30
  ### form
31
31
 
32
- | Slug | Name | Description |
33
- | ---- | ---- | ----------- |
34
- | [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen |
35
- | [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections |
36
- | [`form/single-page`](references/patterns/form-single-page.md) | Single Page Form | Routed full-page form for moderate field count (6-15) without natural sectioning |
37
- | [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
32
+ | Slug | Name | Description |
33
+ | ------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------- |
34
+ | [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen |
35
+ | [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections |
36
+ | [`form/single-page`](references/patterns/form-single-page.md) | Single Page Form | Routed full-page form for moderate field count (6-15) without natural sectioning |
37
+ | [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
38
38
 
39
39
  ### interaction
40
40
 
41
- | Slug | Name | Description |
42
- | ---- | ---- | ----------- |
43
- | [`interaction/confirm`](references/patterns/interaction-confirm.md) | Confirm | Confirmation dialog before destructive or irreversible actions |
44
- | [`interaction/multi-select`](references/patterns/interaction-multi-select.md) | Multi Select | Floating bottom action bar for bulk operations on selected list rows |
45
- | [`interaction/toast`](references/patterns/interaction-toast.md) | Toast | Lightweight feedback after mutations — success or error notifications |
41
+ | Slug | Name | Description |
42
+ | ----------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------- |
43
+ | [`interaction/confirm`](references/patterns/interaction-confirm.md) | Confirm | Confirmation dialog before destructive or irreversible actions |
44
+ | [`interaction/multi-select`](references/patterns/interaction-multi-select.md) | Multi Select | Floating bottom action bar for bulk operations on selected list rows |
45
+ | [`interaction/toast`](references/patterns/interaction-toast.md) | Toast | Lightweight feedback after mutations — success or error notifications |
46
46
 
47
47
  ### list
48
48
 
49
- | Slug | Name | Description |
50
- | ---- | ---- | ----------- |
49
+ | Slug | Name | Description |
50
+ | ----------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------- |
51
51
  | [`list/dense-scan`](references/patterns/list-dense-scan.md) | Dense Scan List | High-density scannable list backed by GraphQL connections with DataTable, sort, filters, and pagination |
52
52
 
53
53
  ## How to Use
@@ -65,3 +65,15 @@ These are the foundational rules that underpin all patterns. All patterns build
65
65
  - NEVER mix patterns in a single page component
66
66
  - ALWAYS use AppShell components — do NOT use raw HTML or third-party UI libraries
67
67
  - If no entry matches, compose directly from fundamental references
68
+
69
+ ### Cross-cutting UX rules (apply to every screen)
70
+
71
+ Full rationale in [`design-system.md`](references/fundamental/design-system.md) → Composition & emphasis rules.
72
+
73
+ - **One primary action per view:** at most one primary/filled `Button`; everything else is `outline`/`secondary`/`ghost`.
74
+ - **Status badges by semantic color:** a **filled** semantic variant for the record's primary/lifecycle status, **`outline-*`** for secondary statuses, **`subtle-*`** for tags; reserve brand `default` for non-status emphasis.
75
+ - **No duplicate actions:** an action lives in exactly one place — never repeat the same action in both `Layout.Header` and `ActionPanel`.
76
+ - **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`.
77
+ - **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row.
78
+ - **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one.
79
+ - **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path.
@@ -1,6 +1,6 @@
1
1
  # AppShell Components
2
2
 
3
- > **AppShell version:** `0.36.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver)
3
+ > **AppShell version:** `1.7.0` (matches `packages/erp-kit/templates/scaffold/app/*/frontend/package.json` pinned `@tailor-platform/app-shell` semver)
4
4
  > **Source of truth:** `@tailor-platform/app-shell` exports
5
5
  > **Update process:** see "Keeping this file in sync" at the bottom
6
6
 
@@ -81,8 +81,8 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell";
81
81
  <Layout.Header title="Purchase orders" actions={[<Button key="create">Create</Button>]}>
82
82
  <Tabs.Root value={bucket} onValueChange={setBucket}>
83
83
  <Tabs.List>
84
- <Tabs.Trigger value="all">All</Tabs.Trigger>
85
- <Tabs.Trigger value="open">Open</Tabs.Trigger>
84
+ <Tabs.Tab value="all">All</Tabs.Tab>
85
+ <Tabs.Tab value="open">Open</Tabs.Tab>
86
86
  </Tabs.List>
87
87
  </Tabs.Root>
88
88
  </Layout.Header>
@@ -109,6 +109,28 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell";
109
109
 
110
110
  **Used in patterns:** every page pattern (`list/*`, `detail/*`, `form/*`).
111
111
 
112
+ ### `Grid`
113
+
114
+ **Import:** `import { Grid } from '@tailor-platform/app-shell'`
115
+ **Purpose:** Presentational CSS-Grid container for laying tiles/cards into equal or custom columns with responsive reflow — the canonical wrapper for **metric / KPI strips** and card galleries. Purely layout; makes no data assumptions.
116
+ **API:** `GridProps` — `columns` (number | CSS track string like `"280px 1fr"` | responsive object `{ initial, sm, md, lg, xl }`), `gap` (spacing step, default `3`) plus `gapX` / `gapY`, `minChildWidth` (auto-fit: as many ≥Npx columns as fit, no breakpoints needed), `rows`, `flow`, `align`, `justify`. Sub-component **`Grid.Item`** for spanning/placement — `colSpan`, `rowSpan`, `colStart`, `colEnd` (all responsive). Root carries `data-slot="grid"`.
117
+ **Example — responsive KPI grid:**
118
+
119
+ ```tsx
120
+ import { Grid, MetricCard } from "@tailor-platform/app-shell";
121
+
122
+ <Grid columns={{ initial: 1, md: 2, xl: 4 }} gap={4}>
123
+ <MetricCard title="Net total" value="$1,500" trend={{ direction: "up", value: "+5%" }} />
124
+ <MetricCard title="Open orders" value="42" />
125
+ <MetricCard title="Overdue" value="7" />
126
+ <MetricCard title="Suppliers" value="18" />
127
+ </Grid>;
128
+ ```
129
+
130
+ **Used in patterns:** metric / KPI strips on `detail/*` and dashboards; any equal-column card layout.
131
+
132
+ **Notes:** Metric tiles and KPI cards **always** go in a `Grid` — never stacked one-per-row or in a single column. Use a responsive `columns` object (e.g. `{ initial: 1, md: 2, xl: 4 }`) so tiles reflow at smaller widths; reach for `minChildWidth` when the tile count is dynamic.
133
+
112
134
  ### `SidebarLayout`
113
135
 
114
136
  **Import:** `import { SidebarLayout } from '@tailor-platform/app-shell'`
@@ -266,6 +288,28 @@ import { Button, Link } from '@tailor-platform/app-shell';
266
288
  **API:** Compound — `Tooltip.Root`, `Tooltip.Trigger`, `Tooltip.Content`.
267
289
  **Used in patterns:** any pattern with icon-only buttons (must have `aria-label` AND a tooltip).
268
290
 
291
+ ### `Tabs`
292
+
293
+ **Import:** `import { Tabs } from '@tailor-platform/app-shell'`
294
+ **Purpose:** In-page tab navigation — split one record's sections (Overview / Line items / Activity) or bucket a list (All / Open / …) into switchable panels. Presentational; owns only the active-tab state.
295
+ **API:** Compound — `Tabs.Root` (`variant`: `default | line | capsule`; controlled `value` + `onValueChange`, or uncontrolled `defaultValue`), `Tabs.List`, `Tabs.Tab` (`value`), `Tabs.Panel` (`value`). Note: the sub-component is **`Tabs.Tab`**, not `Tabs.Trigger`.
296
+ **Example:**
297
+
298
+ ```tsx
299
+ <Tabs.Root defaultValue="overview" variant="line">
300
+ <Tabs.List>
301
+ <Tabs.Tab value="overview">Overview</Tabs.Tab>
302
+ <Tabs.Tab value="items">Line items</Tabs.Tab>
303
+ </Tabs.List>
304
+ <Tabs.Panel value="overview">{/* … */}</Tabs.Panel>
305
+ <Tabs.Panel value="items">{/* … */}</Tabs.Panel>
306
+ </Tabs.Root>
307
+ ```
308
+
309
+ **Used in patterns:** `list-dense-scan` (bucket tabs composed **above** `DataTable.Root`, synced to `useCollectionVariables` — see the `DataTable` "Bucket tabs" note), `detail/*` (sectioned record content).
310
+
311
+ **Notes:** For lists, AppShell's own filtering surface is **toolbar chips** (`DataTable.Filters`), not tabs — reach for `Tabs` only when the business genuinely thinks in a small set of named buckets. Don't render a `Tab` per enum value where a filter chip belongs.
312
+
269
313
  ---
270
314
 
271
315
  ## Display
@@ -276,14 +320,27 @@ import { Button, Link } from '@tailor-platform/app-shell';
276
320
 
277
321
  **Import:** `import { Badge } from '@tailor-platform/app-shell'`
278
322
  **Purpose:** Status labels and small categorical chips.
279
- **API:** `BadgeProps` — `variant`: `default | success | warning | neutral | error | destructive`. Plus `badgeVariants` CVA for custom-styled siblings.
323
+ **API:** `BadgeProps` — `variant` (15 total):
324
+
325
+ - **Filled** (high emphasis): `default` (primary), `success`, `warning`, `error`, `neutral`, `info`
326
+ - **Subtle** (low emphasis, tinted): `subtle-success`, `subtle-warning`, `subtle-error`, `subtle-info`
327
+ - **Outline** (renders a status dot — for row/list statuses): `outline-success`, `outline-warning`, `outline-error`, `outline-info`, `outline-neutral`
328
+
329
+ Plus `badgeVariants` CVA for custom-styled siblings.
280
330
  **Example:**
281
331
 
282
332
  ```tsx
283
- <Badge variant="success">Active</Badge>
333
+ // Primary / lifecycle status — filled semantic variant
334
+ <Badge variant="success">Confirmed</Badge>
335
+ // Secondary status (delivery, billing) — outline with status dot
336
+ <Badge variant="outline-warning">Partially received</Badge>
337
+ // Tag / label — subtle
338
+ <Badge variant="subtle-info">New</Badge>
284
339
  ```
285
340
 
286
- **Used in patterns:** `list/*` (status column), `detail/*` (header status).
341
+ **Used in patterns:** `list/*` (status column → `outline-*`), `detail/*` (header status).
342
+
343
+ **Notes:** Encode status by **semantic color** with a primary/secondary split: a record's **primary/lifecycle status** uses a **filled** semantic variant (one per row / one in a detail header); **secondary statuses** (delivery, billing) use **`outline-*`** (status dot); tags use **`subtle-*`**. Reserve **`default`** (brand) for non-status emphasis. Full rule: **`design-system.md`** → Composition & emphasis rules.
287
344
 
288
345
  ### `Table`
289
346
 
@@ -291,7 +348,7 @@ import { Button, Link } from '@tailor-platform/app-shell';
291
348
 
292
349
  **Import:** `import { Table } from '@tailor-platform/app-shell'`
293
350
  **Purpose:** Semantic data table with scrollable container.
294
- **API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner `<table>`.
351
+ **API:** Compound — `Table.Root`, `Table.Header`, `Table.Body`, `Table.Footer`, `Table.Row`, `Table.Head`, `Table.Cell`, `Table.Caption`. `Table.Root` accepts `containerClassName` for the outer wrapper, `className` for the inner `<table>`. `Table.Head` and `Table.Cell` accept **`align`** (`"left" | "center" | "right"`, default `"left"`) — prefer this over ad-hoc `className="astw:text-right"` and pair the same alignment on the head and its column's cells (right for numeric/money, center for compact status/icon columns).
295
352
  **Example:**
296
353
 
297
354
  ```tsx
@@ -300,7 +357,7 @@ import { Button, Link } from '@tailor-platform/app-shell';
300
357
  <Table.Row>
301
358
  <Table.Head>Order</Table.Head>
302
359
  <Table.Head>Status</Table.Head>
303
- <Table.Head className="astw:text-right">Total</Table.Head>
360
+ <Table.Head align="right">Total</Table.Head>
304
361
  </Table.Row>
305
362
  </Table.Header>
306
363
  <Table.Body>
@@ -310,7 +367,7 @@ import { Button, Link } from '@tailor-platform/app-shell';
310
367
  <Table.Cell>
311
368
  <Badge variant={statusVariant(o.status)}>{o.status}</Badge>
312
369
  </Table.Cell>
313
- <Table.Cell className="astw:text-right">{formatMoney(o.total)}</Table.Cell>
370
+ <Table.Cell align="right">{formatMoney(o.total)}</Table.Cell>
314
371
  </Table.Row>
315
372
  ))}
316
373
  </Table.Body>
@@ -330,7 +387,7 @@ import { Button, Link } from '@tailor-platform/app-shell';
330
387
 
331
388
  **Import:** compound namespace + helpers from `'@tailor-platform/app-shell'`, e.g. `DataTable`, `useDataTable`, `useCollectionVariables`, `createColumnHelper`, and types such as `Column`, `UseDataTableReturn`.
332
389
 
333
- **Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column).
390
+ **Purpose:** Production list screens over GraphQL **connections**. Owns toolbar filter chips (**`DataTable.Filters`** from column `filter` configs), header sort, **`DataTable.Pagination`** (cursor-first; First/Last when `total` is provided), loading skeleton/error row, **`onClickRow`**, **`rowActions`** (kebab column), **`onSelectionChange`** (checkbox column), **column pinning** (`pin: "left" | "right"`) and built-in **column settings** (`<DataTable.Toolbar columnSettings>` — user show/hide + reorder + pin, persisted per-user via **`tableId`**).
334
391
 
335
392
  **Primitives:** Builds on low-level **`Table`**; do not reinvent pagination/filters manually unless the dataset is trivial.
336
393
 
@@ -347,12 +404,13 @@ const table = useDataTable({
347
404
  data: fetching ? undefined : mappedFromQuery,
348
405
  loading: fetching,
349
406
  control,
407
+ tableId: "purchase-orders", // persist user column layout to localStorage
350
408
  onClickRow: (row) => navigate(detailHref(row)),
351
409
  // onSelectionChange, rowActions, sort: …
352
410
  });
353
411
 
354
412
  <DataTable.Root value={table}>
355
- <DataTable.Toolbar>
413
+ <DataTable.Toolbar columnSettings>
356
414
  <DataTable.Filters />
357
415
  </DataTable.Toolbar>
358
416
  <DataTable.Table />
@@ -362,6 +420,12 @@ const table = useDataTable({
362
420
  </DataTable.Root>;
363
421
  ```
364
422
 
423
+ **Row navigation:** whole row is clickable via **`onClickRow`** → `navigate(detailHref(row))`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. Never add a per-row "View" / "Open" / "→" button. (A first-class row-interaction API is under design — see the row-interaction tracking issue.)
424
+
425
+ **Column pinning & settings:** set `pin: "left" | "right"` on a `Column` to freeze it during horizontal scroll — selection auto-pins left, row-actions auto-pins right (a `width` is optional but recommended for stable sizing). Pass **`columnSettings`** to **`DataTable.Toolbar`** to render the built-in "Columns" control (show/hide, reorder, re-pin) at the top-right; pass a stable, **unique** **`tableId`** to persist each user's layout to `localStorage` (a per-user preference — intentionally not in the URL like filters/sort).
426
+
427
+ **Column alignment:** each `Column` accepts **`align`** (`"left" | "right"`) applied to both header and body cell. Numeric `type` columns (`"number"`, `"money"`) default to `"right"` automatically so digits align on the decimal place — pass `align="left"` to opt out; everything else defaults to `"left"`.
428
+
365
429
  **Metadata path:** Prefer `createColumnHelper` + `inferColumns(tableMetadata.order)` (`@tailor-platform/app-shell-sdk-plugin` codegen) when available so enum/datetime/string filters bind to the right editors.
366
430
 
367
431
  **Bucket tabs / segmented UX:** AppShell defines **toolbar chips**, not lifecycle tabs. When design places **`Tabs`** (All / Draft / …) inside the card, compose them **above** `DataTable.Root` and synchronize tab-driven bucket state with **`useCollectionVariables`** (`variables.query` / filters)—see **`patterns/list/dense-scan.md`**.
@@ -426,25 +490,31 @@ const table = useDataTable({
426
490
  **Import:** `import { MetricCard } from '@tailor-platform/app-shell'`
427
491
  **Purpose:** KPI tile for dashboards or hero metric strip.
428
492
  **API:** `MetricCardProps` — `title`, `value`, `trend: { direction, value }`, `description`, `icon`.
429
- **Example:**
493
+ **Example:** metric tiles are laid out in a `Grid` (never one-per-row):
430
494
 
431
495
  ```tsx
432
- <MetricCard
433
- title="Net total"
434
- value="$1,500"
435
- trend={{ direction: "up", value: "+5%" }}
436
- description="vs last month"
437
- icon={<DollarSign />}
438
- />
496
+ <Grid columns={{ initial: 1, md: 2, xl: 4 }} gap={4}>
497
+ <MetricCard
498
+ title="Net total"
499
+ value="$1,500"
500
+ trend={{ direction: "up", value: "+5%" }}
501
+ description="vs last month"
502
+ icon={<DollarSign />}
503
+ />
504
+ <MetricCard title="Open orders" value="42" />
505
+ {/* …more tiles */}
506
+ </Grid>
439
507
  ```
440
508
 
441
509
  **Used in patterns:** KPI tiles, dashboards, **`detail/*`** metric strips where specs call for them.
442
510
 
511
+ **Notes:** Always wrap metric tiles in **`Grid`** (see the `Grid` entry) — a column of single-width `MetricCard`s stacked one-per-row is an anti-pattern.
512
+
443
513
  ### `DocumentProgressCard`
444
514
 
445
515
  **Import:** `import { DocumentProgressCard } from '@tailor-platform/app-shell'`
446
- **Purpose:** Generic document lifecycle/fulfilment state — optional percentage, stacked progress bar, status legend; arbitrary `segments`.
447
- **API:** `DocumentProgressCardProps` — `segments` (`{ label, value, color? }[]`), `title?`, `percent?`, `legend?` (defaults to `segments`), `total?` (bar denominator; larger than the sum leaves an empty track), `className?`. View-only — `percent` is explicit.
516
+ **Purpose:** Presentational card for a document's lifecycle / fulfilment state — an optional headline percentage, a stacked progress bar, and a status legend. Domain-agnostic and view-only: pass an arbitrary set of `segments` (e.g. shipped / returned / pending) plus an explicit `percent`; derive both in the consumer.
517
+ **API:** `DocumentProgressCardProps` — `segments` (required; array of `{ label, value, color }`), `percent?` (0–100, headline shown top-right), `title?`, `legend?` (defaults to `segments`; supply only when the legend should differ from the bar, e.g. overlapping buckets), `total?` (bar denominator; defaults to the sum of segment values — a larger value leaves an unfilled remainder), `className?`. Segment `color`: `indigo | pink | green | amber | red | blue | neutral`.
448
518
  **Example:**
449
519
 
450
520
  ```tsx
@@ -459,7 +529,7 @@ const table = useDataTable({
459
529
  />
460
530
  ```
461
531
 
462
- **Used in patterns:** **`detail/*`** right-rail cards for arbitrary status breakdowns (shipped/cancelled/pending, PO received/returned/yet-to-receive, etc.). For the purchase-order fulfilment recipe (derived %, net-received/returned bar split, legend override), see `docs/components/document-progress-card.md`.
532
+ **Used in patterns:** `detail/*` — fulfilment / lifecycle summary cards (e.g. a purchase-order receipt progress) in the main column or right rail.
463
533
 
464
534
  ### `Avatar`
465
535
 
@@ -520,6 +590,24 @@ const table = useDataTable({
520
590
  **API:** Compound — `ActivityCard.Root`, `ActivityCard.Items` (generic over item type), plus `ActivityCardProps`, `ActivityCardItem`, `ActivityCardItemProps`. Items render with timestamp + actor + description.
521
591
  **Used in patterns:** `detail/hero-with-actions` (right column or bottom section).
522
592
 
593
+ ### `Alert`
594
+
595
+ **Import:** `import { Alert } from '@tailor-platform/app-shell'`
596
+ **Purpose:** Inline, in-page banner for a persistent status message — a form/record error, a warning, or a success/info notice attached to a section. (Transient notifications use `useToast`; blocking confirmations use `Dialog` — see `interaction/confirm`.)
597
+ **API:** Compound — `Alert.Root` (`variant`: `neutral | success | warning | error | info`, default `neutral`; a matching icon is rendered automatically; optional `action?: ReactNode`, `dismissible?: boolean`, `onDismiss?: () => void`), `Alert.Title`, `Alert.Description`.
598
+ **Example:**
599
+
600
+ ```tsx
601
+ <Alert.Root variant="error">
602
+ <Alert.Title>Couldn't load line items</Alert.Title>
603
+ <Alert.Description>Check your connection and try again.</Alert.Description>
604
+ </Alert.Root>
605
+ ```
606
+
607
+ **Used in patterns:** any data-backed screen's **error** state (inline error + retry) and record-level warnings — the error affordance the composition rules require (**`design-system.md`** → Composition & emphasis rules → States).
608
+
609
+ **Notes:** Encode severity by `variant` (semantic color), same discipline as `Badge` — don't use `error` for a routine notice. Pair a retry/next-step control via `action` rather than a separate stray button.
610
+
523
611
  ---
524
612
 
525
613
  ## Forms
@@ -555,11 +643,20 @@ const table = useDataTable({
555
643
  **API:** `InputProps` — extends native `<input>` props.
556
644
  **Used in patterns:** all `form/*`.
557
645
 
646
+ ### `Checkbox`
647
+
648
+ **Import:** `import { Checkbox } from '@tailor-platform/app-shell'`
649
+ **Purpose:** Boolean form control. Maps to spec field type: `boolean`. Also used for row selection and filter toggles.
650
+ **API:** `CheckboxProps` — extends Base UI `Checkbox.Root` (`checked`/`defaultChecked`, `onCheckedChange`, `indeterminate`, `disabled`, `required`, `name`, `inputRef`) and adds an inline `label`. Integrates with `Field` for invalid/disabled/label state exactly like `Input`/`Select` — no bespoke `error` prop.
651
+ **Accessible name:** the inline `label` prop (enclosing) or a sibling `Field.Label`; for a bare box (e.g. a table row-select cell) pass `aria-label`.
652
+ **Used in patterns:** `form/*` (boolean fields), `list/*` (row selection), DataTable filter/column controls.
653
+
558
654
  ### `Select`
559
655
 
560
656
  **Import:** `import { Select } from '@tailor-platform/app-shell'`
561
657
  **Purpose:** Dropdown for fixed enumerations.
562
658
  **API:** Compound. Sync version (small option lists). For async/typeahead, see `Combobox` and `Autocomplete`. Type: `SelectAsyncFetcher<T>` for the async variant.
659
+ **Accessible name:** inside a `Form` the field label provides it automatically. When used **standalone** (outside a `Form`), pass `aria-label` (or `aria-labelledby` / `id`) — these now forward to the underlying combobox element, so it isn't left named only by its current value. Same applies to `Combobox` and `Autocomplete`.
563
660
  **Used in patterns:** all `form/*` (enum fields).
564
661
 
565
662
  ### `Combobox`
@@ -642,7 +739,7 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g
642
739
 
643
740
  ### `RouteParams`, `PageComponent`, `PageMeta`, `AppShellPageProps`, `AppShellRegister`, `ContextData` (types)
644
741
 
645
- Types for declaring page props (`appShellPageProps = { meta, guards, loader }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage.
742
+ Types for declaring page props (`appShellPageProps = { meta, guards }`), reading typed route params, and registering route types globally. See `project-setup.md` for usage.
646
743
 
647
744
  ---
648
745
 
@@ -353,3 +353,27 @@ When a custom component proves reusable across 2+ apps, promote it upstream into
353
353
  | Two-column detail at <1024 | right column collapses below main — do not override |
354
354
  | Inline ID, code, table number | `text-mono` |
355
355
  | Timestamp, label, subtle metadata | `text-caption text-fg-subtle` |
356
+
357
+ ### Composition & emphasis rules
358
+
359
+ These are visual-composition rules every screen must follow, regardless of pattern. They exist because emphasis only works when it is scarce.
360
+
361
+ **Emphasis budget.** Attention is a budget you spend once per scan region.
362
+
363
+ - **One primary action per view.** A screen (or a card/section) has at most one filled/primary `Button`; everything else is `outline`, `secondary`, or `ghost`. If two things look equally important, neither reads as important.
364
+ - **Badges** encode status by semantic color, with a clear primary/secondary split:
365
+ - A record's **primary / lifecycle status** (PO status, SO status) → a **filled semantic** variant (`success` / `warning` / `error` / `info` / `neutral`) — one per row in a list, one in a detail header.
366
+ - **Secondary statuses** (delivery, billing, fulfilment) and dense supporting columns → **`outline-*`** (with status dot).
367
+ - **Tags / labels** ("New", "Returned") → **`subtle-*`**.
368
+ - Reserve **`default`** (brand fill) for non-status emphasis — never the brand color as a routine status. The defect to avoid: making _every_ chip a loud fill, or giving secondary statuses the same weight as the primary one. (Variants: **`components.md`** → `Badge`.)
369
+ - **Color:** status colors (`success`/`warning`/`error`/`info`) signal meaning, not decoration — don't tint neutral content.
370
+
371
+ **Hierarchy.** One `h1` per page (the `Layout.Header` title). Section headings step down (`h2` → `h3`); never skip levels for size — pick the role token (`design-system.md` §4 Typography), not the pixel size.
372
+
373
+ **States — never ship only the happy path.** Every data-backed screen handles:
374
+
375
+ - **Loading** — skeleton/placeholder, not a blank flash.
376
+ - **Empty** — a labelled empty state (what it is, how to add the first record), not a bare empty table.
377
+ - **Error** — an inline error with a retry affordance, not a silent failure.
378
+
379
+ **Spacing rhythm.** Use the spacing scale (§4) consistently — equal gaps between sibling sections, consistent card padding. A one-off `gap` or `padding` that doesn't match its siblings reads as a mistake.
@@ -191,7 +191,7 @@ Every page component:
191
191
  };
192
192
  ```
193
193
 
194
- 3. Uses `appShellPageProps.guards` for permission gates and `appShellPageProps.loader` for route loaders. See [project-setup.md](project-setup.md).
194
+ 3. Uses `appShellPageProps.guards` for permission gates. See [project-setup.md](project-setup.md).
195
195
 
196
196
  ## Quick reference
197
197
 
@@ -5,7 +5,21 @@ category: pattern
5
5
  subcategory: detail
6
6
  description: Single-record detail view with workflow actions and activity timeline
7
7
  requiredImports:
8
- [Layout, Badge, Button, Menu, DescriptionCard, Card, Table, ActionPanel, ActivityCard]
8
+ [
9
+ Layout,
10
+ Badge,
11
+ Button,
12
+ Menu,
13
+ DescriptionCard,
14
+ Card,
15
+ Table,
16
+ ActionPanel,
17
+ ActivityCard,
18
+ Grid,
19
+ MetricCard,
20
+ DocumentProgressCard,
21
+ Alert,
22
+ ]
9
23
  tags: [detail, actions, timeline, workflow, two-column]
10
24
  do:
11
25
  - Single-record detail view (Order #1234, Supplier ABC, Product SKU-42)
@@ -51,6 +65,33 @@ Composition rules:
51
65
 
52
66
  Responsive: <1024 collapses to single column with right column rendered below main; 1024–1280 two columns with narrow side; >1280 two columns at full width.
53
67
 
68
+ ## Layout variants
69
+
70
+ Choose the column structure from the **record** — the pattern is "hero + workflow + (optional) history," and the layout flexes. The skill never mandates a fixed column count.
71
+
72
+ - **Main + right rail** (most common) — the record has workflow actions and/or activity/related summaries → right `Layout.Column area="right"` holds `ActionPanel`, `ActivityCard`, integration cards.
73
+ - **Single main column** — the record is mostly descriptive with few/no workflow actions and no meaningful history → one `Layout.Column`. Don't invent a rail just to fill space.
74
+ - **Main + left rail** (`area="left"`, wider 320px) — when a persistent section index / summary should lead the reading order.
75
+ - Either column holds **N cards** as the record requires.
76
+
77
+ ## Optional cards
78
+
79
+ Add these only when the record calls for them — none is mandatory. Each is a card like any other in the columns above.
80
+
81
+ - **Metric strip** — when the record has headline KPIs (totals, counts), lead the main column with `MetricCard`s wrapped in a `Grid` (never one per row). See the `Grid` and `MetricCard` entries in `components.md`.
82
+
83
+ ```tsx
84
+ <Grid columns={{ initial: 1, md: 2, xl: 4 }} gap={4}>
85
+ <MetricCard title="Net total" value="$12,480" trend={{ direction: "up", value: "+4%" }} />
86
+ <MetricCard title="Lines" value="18" />
87
+ <MetricCard title="Received" value="11" />
88
+ <MetricCard title="Outstanding" value="7" />
89
+ </Grid>
90
+ ```
91
+
92
+ - **`DocumentProgressCard`** — when the record has a fulfilment / lifecycle breakdown (received vs returned vs pending, shipment status). Sits in the main column or the right rail; derive `percent` and `segments` in the consumer.
93
+ - **`Alert`** — a record-level inline banner (blocking error, on-hold warning) above the columns; use the **error** variant for the pattern's required error state. Transient feedback uses `useToast`, not `Alert`.
94
+
54
95
  ## Page Implementation
55
96
 
56
97
  ```tsx
@@ -109,16 +150,16 @@ export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Pr
109
150
  <Table.Header>
110
151
  <Table.Row>
111
152
  <Table.Head>SKU</Table.Head>
112
- <Table.Head>Qty</Table.Head>
113
- <Table.Head>Total</Table.Head>
153
+ <Table.Head align="right">Qty</Table.Head>
154
+ <Table.Head align="right">Total</Table.Head>
114
155
  </Table.Row>
115
156
  </Table.Header>
116
157
  <Table.Body>
117
158
  {order.lineItems.map((item) => (
118
159
  <Table.Row key={item.id}>
119
160
  <Table.Cell>{item.sku}</Table.Cell>
120
- <Table.Cell>{item.qty}</Table.Cell>
121
- <Table.Cell>${item.total.toLocaleString()}</Table.Cell>
161
+ <Table.Cell align="right">{item.qty}</Table.Cell>
162
+ <Table.Cell align="right">${item.total.toLocaleString()}</Table.Cell>
122
163
  </Table.Row>
123
164
  ))}
124
165
  </Table.Body>
@@ -143,9 +184,10 @@ export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Pr
143
184
 
144
185
  ## Constraints
145
186
 
146
- - More than 2 primary actions in the header → move overflow into a `Menu`.
187
+ - **Header carries the single primary CTA + the status `Badge`** — not workflow actions. Workflow actions (Approve, Reject, Archive, …) live in the `ActionPanel`. If the header needs more than ~2 actions, move the overflow into a `Menu`.
188
+ - **Never duplicate an action** across `Layout.Header` and `ActionPanel` — each action has exactly one home.
147
189
  - Every content section MUST sit inside `Card.Root` (or `DescriptionCard`, which already self-contains). Raw divs are not allowed.
148
- - `ActionPanel` is workflow-only — never back-navigation.
190
+ - `ActionPanel` is workflow-only — never back-navigation (that lives in the breadcrumb).
149
191
  - `Table.Root` inside a Card requires `containerClassName="astw:px-6"`.
150
192
 
151
193
  ## Anti-patterns
@@ -155,3 +197,5 @@ export default function HeroWithActionsDetail({ order, onApprove, onCancel }: Pr
155
197
  - Bare `<div>` sections in the main column — every content section MUST sit inside `Card.Root`.
156
198
  - `ActionPanel` containing back-navigation (e.g. "Back to Product List") — back navigation lives in `Layout.Header`'s breadcrumb.
157
199
  - A `Table.Root` inside a Card without `containerClassName="astw:px-6"` — the first column lands flush against the card edge.
200
+ - The same action in both `Layout.Header` and the `ActionPanel` — duplicating it makes neither read as canonical. Pick one home.
201
+ - Giving every status the same loud weight — the record's primary/lifecycle status is a **filled** semantic badge; secondary statuses (fulfilment, billing) use **`outline-*`** (see `design-system.md` → Composition & emphasis rules).
@@ -52,11 +52,13 @@ export type Order = {
52
52
  createdAt: string;
53
53
  };
54
54
 
55
+ // Primary status column — filled semantic variants (one per row).
56
+ // Secondary status columns (e.g. delivery, billing) use outline-* instead.
55
57
  const statusVariant = {
56
58
  draft: "neutral",
57
- confirmed: "outline-info",
58
- shipped: "outline-warning",
59
- delivered: "outline-success",
59
+ confirmed: "info",
60
+ shipped: "warning",
61
+ delivered: "success",
60
62
  } as const;
61
63
 
62
64
  export const columns: Column<Order>[] = [
@@ -67,7 +69,10 @@ export const columns: Column<Order>[] = [
67
69
  render: (row) => <Badge variant={statusVariant[row.status]}>{row.status}</Badge>,
68
70
  },
69
71
  {
72
+ // Numeric columns right-align so digits line up. `type: "money" | "number"`
73
+ // auto-right; with a custom `render` set `align` explicitly.
70
74
  label: "Amount",
75
+ align: "right",
71
76
  render: (row) => `${row.amount.toLocaleString()}`,
72
77
  },
73
78
  { label: "Created", accessor: (row) => row.createdAt },
@@ -145,10 +150,12 @@ Omit `fill` on pages that should flow and scroll naturally (forms, dashboards, a
145
150
  - Column count: 4-8 recommended
146
151
  - Must include pagination — never render unbounded lists
147
152
  - Table-first pages use `<Layout fill>` so title/toolbar/header/footer stay pinned and only rows scroll
148
- - Status Badge colors must use design system tokens (variant prop)
153
+ - 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
154
+ - 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)
149
155
  - Bulk actions toolbar appears only when ≥1 row is selected
150
- - Whole row is clickable via `onClickRow`; no per-row "View" / "Open" buttons
156
+ - Whole row is clickable via `onClickRow`; wrap the primary identifier cell in `<Link>` for keyboard/SR access. No per-row "View" / "Open" buttons
151
157
  - Per-row `Menu` (overflow `…`) is reserved for non-navigation actions (Archive, Duplicate, Delete)
158
+ - 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
152
159
 
153
160
  ## Anti-patterns
154
161