@tailor-platform/app-shell 1.13.0 → 1.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/app-shell",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "description": "An opinionated React application framework for building ERP applications on Tailor Platform",
5
5
  "keywords": [
6
6
  "app-shell",
@@ -47,37 +47,37 @@
47
47
  "access": "public"
48
48
  },
49
49
  "dependencies": {
50
- "@base-ui/react": "1.6.0",
50
+ "@base-ui/react": "1.8.0",
51
51
  "@internationalized/date": "3.12.2",
52
52
  "@standard-schema/spec": "^1.1.0",
53
53
  "@tailor-platform/auth-public-client": "^0.6.1",
54
+ "@tailor-platform/vite-plugin-app-shell": "0.3.0",
54
55
  "change-case": "^5.4.4",
55
56
  "class-variance-authority": "^0.7.1",
56
57
  "clsx": "^2.1.1",
57
58
  "encoding-japanese": "^2.2.0",
58
- "lucide-react": "^1.8.0",
59
+ "lucide-react": "1.8.0",
59
60
  "openai": "6.45.0",
60
61
  "papaparse": "^5.5.3",
61
62
  "react-router": "^8.3.0",
62
63
  "sonner": "^2.0.7",
63
- "tailwind-merge": "^3.6.0",
64
- "@tailor-platform/app-shell-vite-plugin": "0.2.3"
64
+ "tailwind-merge": "^3.6.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@fontsource-variable/noto-sans-jp": "5.3.0",
68
68
  "@microsoft/api-extractor": "7.58.12",
69
69
  "@tailwindcss/postcss": "^4.3.3",
70
70
  "@testing-library/react": "16.3.3",
71
- "@testing-library/user-event": "14.6.6",
71
+ "@testing-library/user-event": "14.6.7",
72
72
  "@types/encoding-japanese": "2.2.1",
73
- "@types/node": "^25.9.5",
73
+ "@types/node": "^25.9.6",
74
74
  "@types/papaparse": "5.5.2",
75
75
  "@types/react": "^19",
76
76
  "@types/react-dom": "^19",
77
77
  "@vitejs/plugin-react": "^6.1.0",
78
78
  "@vitest/coverage-v8": "4.1.10",
79
79
  "happy-dom": "20.9.0",
80
- "postcss": "8.5.26",
80
+ "postcss": "8.5.28",
81
81
  "react": "^19.2.7",
82
82
  "react-dom": "^19.2.7",
83
83
  "react-hook-form": "7.71.2",
@@ -25,13 +25,15 @@ These are the foundational rules that underpin all patterns. All patterns build
25
25
 
26
26
  Read it when upgrading `@tailor-platform/app-shell`, and whenever styling, theming, or dark mode looks wrong after a version bump. These breakages are silent: the build succeeds and nothing warns, so the cause is not discoverable from the error output. The package ships no CHANGELOG, so this file is the only migration record available locally.
27
27
 
28
- ## Available Patterns
28
+ ## Available Pages
29
+
30
+ A page is the shape of a whole screen — the outer choice, made before picking patterns for the parts inside it. Where a pattern is one recipe, a page fixes the layout of a whole screen and says what belongs in each part of it. Match the screen first, then use the patterns the entry cites for the pieces within it.
29
31
 
30
- ### detail
32
+ | Slug | Name | Description |
33
+ | -------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
34
+ | [`detail`](references/pages/detail.md) | Detail Page | The screen for a single record — its summary, its contents, the records around it, and the actions available on it |
31
35
 
32
- | Slug | Name | Description |
33
- | ----------------------------------------------------------------------------- | ------------------------ | --------------------------------------------------------------------- |
34
- | [`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 |
36
+ ## Available Patterns
35
37
 
36
38
  ### form
37
39
 
@@ -60,16 +62,17 @@ Read it when upgrading `@tailor-platform/app-shell`, and whenever styling, themi
60
62
  ## How to Use
61
63
 
62
64
  1. Identify the user's intent (list, detail, form, interaction, screen composition, recipe)
63
- 2. Match constraints to an entry slug from the tables above
64
- 3. Read the entry's detailed spec: `references/<category>/<slug>.md` (relative to this file)
65
- 4. Read fundamental references for component APIs, design tokens, and GraphQL conventions: `references/fundamental/`
66
- 5. Implement using ONLY the imports listed in the entry's `requiredImports`
65
+ 2. Building a whole screen? Match it to a **page** entry first and read that. A page fixes the screen's shape — its layout, what goes where, and in what order — before any of the parts inside it are chosen.
66
+ 3. Match each part you are building to a **pattern** entry slug from the tables above. A one-off recipe rather than a screen skips straight to this step.
67
+ 4. Read the entry's detailed spec: `references/<category>/<slug>.md` (relative to this file)
68
+ 5. Read fundamental references for component APIs, design tokens, and GraphQL conventions: `references/fundamental/`
69
+ 6. Implement using ONLY the imports listed in the entry's `requiredImports` — for a page, the `requiredImports` of the pattern it sent you to
67
70
 
68
71
  ## Rules
69
72
 
70
73
  - ALWAYS cite the entry slug in a comment at the top of the file:
71
- `/* pattern: list/dense-scan */`
72
- - NEVER mix patterns in a single page component
74
+ `/* pattern: list/dense-scan */`. Cite the pattern you implemented, even when a page entry sent you to it.
75
+ - NEVER mix two patterns that solve the same problem in one component — one part, one recipe. Composing patterns for _different_ parts of a screen is expected, and the screen's page entry says which.
73
76
  - ALWAYS use AppShell components — do NOT use raw HTML or third-party UI libraries
74
77
  - If no entry matches, compose directly from fundamental references
75
78
 
@@ -83,4 +86,5 @@ Full rationale in [`design-system.md`](references/fundamental/design-system.md)
83
86
  - **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`.
84
87
  - **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row.
85
88
  - **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one.
89
+ - **Forms use AppShell `Form` + `Field`**, submitting via `onFormSubmit` — never a bare `<form onSubmit>` with `FormData`. `onFormSubmit` reads registered `Field.Root`s, so every control (dropdowns included) just needs a wrapping `Field.Root name="…"` — no `name` on the control, no `useState`. React Hook Form is optional, consumer-installed, and only warranted for cross-field validation, field arrays, or a Zod resolver. Details in [`components.md`](references/fundamental/components.md) → Forms.
86
90
  - **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path.
@@ -9,7 +9,7 @@
9
9
  | This file (`components.md`) | `design-system.md` |
10
10
  | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
11
11
  | **Imports**, compound structure, hooks, canonical **composition** (`Card` + `Table`, `DataTable`, `Dialog`, etc.) | **Tokens**, theme imports, typography/spacing/radius/elevation **tables**, breakpoints **intent** |
12
- | JSX examples tied to ERP patterns (`patterns/*`) | **`astw:`** rules — only on AppShell `*ClassName` props; plain utilities on **your** elements |
12
+ | JSX examples tied to ERP patterns (`patterns/*`) | **AppShell styling boundary** — plain utilities and no internal `astw:` prefix |
13
13
  | Prop summaries + links to upstream `docs/components/*.md` | **Visual conformance**: no magic colors/px on custom markup, motion, dark mode |
14
14
 
15
15
  **Rule of thumb:** “Which component / prop?” → **here.** “Which token / spacing step / elevation?” → **`design-system.md`** §§4–6.
@@ -105,7 +105,7 @@ import { Button, Layout, Tabs } from "@tailor-platform/app-shell";
105
105
  </Layout>
106
106
  ```
107
107
 
108
- **Notes:** Children that aren't `Layout.Header` or `Layout.Column` are filtered out. Column gap overrides (`<Layout className="astw:gap-6" />`) → **`design-system.md`** §5 (`astw:` rules).
108
+ **Notes:** Children that aren't `Layout.Header` or `Layout.Column` are filtered out. Set column gaps with `<Layout gap={6} />` (supported steps: 4, 6, 8).
109
109
 
110
110
  **Used in patterns:** every page pattern (`list/*`, `detail/*`, `form/*`).
111
111
 
@@ -257,7 +257,7 @@ import { Button, Link } from '@tailor-platform/app-shell';
257
257
 
258
258
  **Used in patterns:** `list/*` (filter sheet variant).
259
259
 
260
- **Notes:** Size the panel with `Sheet.Content`'s **`size`** prop (`"sm" | "md" | "lg" | "xl" | "full"`, default `"sm"`) — not a width utility. `astw:` rules → **`design-system.md`** §5.
260
+ **Notes:** Size the panel with `Sheet.Content`'s **`size`** prop (`"sm" | "md" | "lg" | "xl" | "full"`, default `"sm"`) — not a width utility.
261
261
 
262
262
  ### `Menu`
263
263
 
@@ -355,7 +355,7 @@ Plus `badgeVariants` CVA for custom-styled siblings.
355
355
 
356
356
  **Import:** `import { Table } from '@tailor-platform/app-shell'`
357
357
  **Purpose:** Semantic data table with scrollable container.
358
- **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).
358
+ **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="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).
359
359
  **Example:**
360
360
 
361
361
  ```tsx
@@ -385,8 +385,8 @@ Plus `badgeVariants` CVA for custom-styled siblings.
385
385
 
386
386
  **Notes:**
387
387
 
388
- - **Inside a card?** Pass `containerClassName="astw:px-6"` on `Table.Root` for the horizontal inset, and either drop `Card.Content` (bare list form) or pass `Card.Content className="astw:px-0"` (header+content form). Skipping the `containerClassName` lands the first column flush against the card edge. See the `Card` entry for the two canonical forms and a DON'T example. Dense cell typography (**`text-sm`**, **`tabular-nums`** for figures, **`font-mono`** for identifiers) → **`design-system.md`** §4 Typography.
389
- - **Whole row is clickable.** Use `<Table.Row onClick={() => navigate(detailPath)} className="astw:cursor-pointer">`. For keyboard and screen-reader users, also wrap the primary identifier cell content in `<Link>` (so the row is reachable via Tab; `Table.Row` is a `<tr>` and cannot itself be a Link — wrapping a `<tr>` in `<a>` is invalid HTML). **No per-row "View" / "Open" / "→" buttons.** Per-row `Menu` (overflow `…`) is the only allowed per-row action surface and is reserved for non-navigation actions like Archive, Duplicate.
388
+ - **Inside a card?** Zero the card's horizontal padding — drop `Card.Content` (bare list form) or pass `Card.Content className="px-0!"` (header+content form) — and leave the table container's padding alone. `Table.Head` / `Table.Cell` carry their own `first:pl-6` / `last:pr-6`, so a `containerClassName="px-6"` double-pads and the first column lands 24px right of the card title. See the `Card` entry for the two canonical forms and a DON'T example. Dense cell typography (**`text-sm`**, **`tabular-nums`** for figures, **`font-mono`** for identifiers) → **`design-system.md`** §4 Typography.
389
+ - **Whole row is clickable.** Use `<Table.Row onClick={() => navigate(detailPath)} className="cursor-pointer">`. For keyboard and screen-reader users, also wrap the primary identifier cell content in `<Link>` (so the row is reachable via Tab; `Table.Row` is a `<tr>` and cannot itself be a Link — wrapping a `<tr>` in `<a>` is invalid HTML). **No per-row "View" / "Open" / "→" buttons.** Per-row `Menu` (overflow `…`) is the only allowed per-row action surface and is reserved for non-navigation actions like Archive, Duplicate.
390
390
 
391
391
  ### `DataTable`
392
392
 
@@ -433,7 +433,7 @@ const table = useDataTable({
433
433
 
434
434
  **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"`.
435
435
 
436
- **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.
436
+ **Metadata path:** Prefer `createColumnHelper` + `inferColumns(tableMetadata.order)` (`@tailor-platform/sdk-plugin-app-shell` codegen) when available so enum/datetime/string filters bind to the right editors.
437
437
 
438
438
  **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`**.
439
439
 
@@ -461,13 +461,13 @@ const table = useDataTable({
461
461
 
462
462
  **Notes:**
463
463
 
464
- - **Tables inside a card need TWO co-requisite geometry changes** (token-backed spacing rationale → **`design-system.md`** §4 Spacing): (a) Card stops imposing horizontal padding — drop `Card.Content` for the bare form, or pass `Card.Content className="astw:px-0"` for the header+content form. (b) `Table.Root` provides the inset itself via `containerClassName="astw:px-6"`. Skipping (b) lands the first column flush against the card edge — `Table.Cell`'s intrinsic `astw:first:pl-6` does NOT render reliably in this composition.
464
+ - **A table inside a card needs ONE geometry change, not two** (token-backed spacing rationale → **`design-system.md`** §4 Spacing): the card stops imposing horizontal padding. Drop `Card.Content` for the bare form, or pass `Card.Content className="px-0!"` for the header+content form. That is all. `Table.Head` and `Table.Cell` already carry an intrinsic `first:pl-6` / `last:pr-6`, which supplies the inset on its own — so the table container must add **none**. Padding on the container stacks on top of the cell's own and pushes the first column 24px past the card title.
465
465
 
466
466
  **Bare table-in-card (list pages, no card-level title):**
467
467
 
468
468
  ```tsx
469
469
  <Card.Root>
470
- <Table.Root containerClassName="astw:px-6">{/* … */}</Table.Root>
470
+ <Table.Root>{/* … */}</Table.Root>
471
471
  </Card.Root>
472
472
  ```
473
473
 
@@ -476,18 +476,19 @@ const table = useDataTable({
476
476
  ```tsx
477
477
  <Card.Root>
478
478
  <Card.Header title="Line items" />
479
- <Card.Content className="astw:px-0">
480
- <Table.Root containerClassName="astw:px-6">{/* … */}</Table.Root>
479
+ <Card.Content className="px-0!">
480
+ <Table.Root>{/* … */}</Table.Root>
481
481
  </Card.Content>
482
482
  </Card.Root>
483
483
  ```
484
484
 
485
- **DON'T — first column lands flush against the card edge:**
485
+ **DON'T — the first column sits 24px right of the card title:**
486
486
 
487
487
  ```tsx
488
488
  <Card.Root>
489
- <Card.Content className="astw:px-0">
490
- <Table.Root>{/* missing containerClassName="astw:px-6" */}</Table.Root>
489
+ <Card.Content className="px-0!">
490
+ {/* double-pads: the container's 24px adds to the cell's own first:pl-6 */}
491
+ <Table.Root containerClassName="px-6">{/* … */}</Table.Root>
491
492
  </Card.Content>
492
493
  </Card.Root>
493
494
  ```
@@ -565,26 +566,37 @@ const table = useDataTable({
565
566
  />
566
567
  ```
567
568
 
568
- **Used in patterns:** `detail/hero-with-actions` (body sections).
569
+ **Used in:** `page/detail` (body sections).
569
570
 
570
571
  ### `ActionPanel`
571
572
 
572
573
  **Import:** `import { ActionPanel } from '@tailor-platform/app-shell'`
573
574
  **Purpose:** Right-rail panel listing workflow actions for a detail page (approve, reject, archive, etc.).
574
- **API:** `ActionPanelProps` — `title`, `actions` (array of `{ label, onSelect, variant?, disabled?, hidden? }`), `className`.
575
+ **API:** `ActionPanelProps` — `title`, `actions`, `className`. Each action is `{ key, label, icon, onClick?, disabled?, loading?, variant? }`; `key`, `label` and `icon` are all **required**, the handler is `onClick` (not `onSelect`), and `variant` is `"default" | "destructive"`. There is no `hidden` — omit a row by not spreading it in. The row type is not exported on its own; annotate an array as `ActionPanelProps["actions"]`.
575
576
  **Example:**
576
577
 
577
578
  ```tsx
578
579
  <ActionPanel
579
580
  title="Actions"
580
581
  actions={[
581
- { label: "Approve", variant: "default", onSelect: handleApprove },
582
- { label: "Reject", variant: "destructive", onSelect: handleReject, disabled: !canReject },
582
+ { key: "approve", label: "Approve", icon: <Check />, onClick: handleApprove },
583
+ ...(canReject
584
+ ? [
585
+ {
586
+ key: "reject",
587
+ label: "Reject",
588
+ icon: <X />,
589
+ onClick: handleReject,
590
+ variant: "destructive" as const,
591
+ loading: rejecting,
592
+ },
593
+ ]
594
+ : []),
583
595
  ]}
584
596
  />
585
597
  ```
586
598
 
587
- **Used in patterns:** `detail/hero-with-actions`.
599
+ **Used in:** `page/detail`.
588
600
 
589
601
  **Notes:**
590
602
 
@@ -595,7 +607,7 @@ const table = useDataTable({
595
607
  **Import:** `import { ActivityCard } from '@tailor-platform/app-shell'`
596
608
  **Purpose:** Timeline of events on a record (audit log, status changes, comments).
597
609
  **API:** Compound — `ActivityCard.Root`, `ActivityCard.Items` (generic over item type), plus `ActivityCardProps`, `ActivityCardItem`, `ActivityCardItemProps`. Items render with timestamp + actor + description.
598
- **Used in patterns:** `detail/hero-with-actions` (right column or bottom section).
610
+ **Used in:** `page/detail` (right column, for revision history / audit trail).
599
611
 
600
612
  ### `Alert`
601
613
 
@@ -619,13 +631,41 @@ const table = useDataTable({
619
631
 
620
632
  ## Forms
621
633
 
634
+ > **Which form stack?** AppShell's `Form` / `Field` / `Fieldset` wrap Base UI primitives and are the
635
+ > default — they need no extra dependency. They own **accessibility wiring and visual state only**
636
+ > (`htmlFor`, `aria-describedby`, `data-invalid` / `data-dirty` / `data-touched`); they do **not** own
637
+ > your values. React Hook Form is **optional** and consumer-installed — reach for it only when a form
638
+ > genuinely needs cross-field validation, field arrays, or a Zod resolver. The two compose; they are
639
+ > not alternatives. `react-hook-form` stopped being a runtime dependency in 1.4.0; it is a dev-only
640
+ > dependency of the package today, used to test the RHF integration contracts — if you use it,
641
+ > install it in your own app.
642
+
622
643
  ### `Form`
623
644
 
624
645
  > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/form.md)
625
646
 
626
647
  **Import:** `import { Form } from '@tailor-platform/app-shell'`
627
- **Purpose:** Form root wired to react-hook-form. Use with `Field`, `Fieldset`, and Zod for validation.
628
- **API:** `FormProps` — `errors`, `actionsRef`, `validationMode`, `noValidate`, plus a namespace exposing form-related sub-helpers. Generic over `FormValues`.
648
+ **Purpose:** Form root that wraps Base UI's form primitive. Provides shared validation context for
649
+ child `Field.Root`s, consolidated error display, and server-error routing by field `name`.
650
+ **API:** `FormProps` — `onFormSubmit`, `onSubmit`, `errors`, `actionsRef`, `validationMode`,
651
+ `noValidate`, `id`. Generic over `FormValues`.
652
+ **Actions outside the form:** give the `Form` an `id` and point a detached submit button at it with
653
+ the native `form` attribute. This is how a page-header Save reaches a body form (1.13.0+):
654
+
655
+ ```tsx
656
+ <Layout.Header
657
+ title="Create product"
658
+ actions={[<Button key="save" type="submit" form="product-form">Save</Button>]}
659
+ />
660
+ <Form id="product-form" onFormSubmit={save}>…</Form>
661
+ ```
662
+
663
+ **Submit:** use `onFormSubmit(values)` — it receives parsed form values and is the default. Use the
664
+ low-level `onSubmit` **only** when handing the native event to React Hook Form's `handleSubmit`.
665
+ **Never** hand-roll `<form onSubmit={e => new FormData(e.currentTarget)}>` — that skips validation
666
+ and error routing.
667
+ **Server errors:** pass `errors={{ fieldName: "message" }}`; they route to the matching
668
+ `Field.Error` and clear when the user edits that field.
629
669
  **Example:** see `form/single-page.md`.
630
670
  **Used in patterns:** all `form/*`.
631
671
 
@@ -633,9 +673,71 @@ const table = useDataTable({
633
673
 
634
674
  **Import:** `import { Field } from '@tailor-platform/app-shell'`
635
675
  **Purpose:** Single form field with label + control + error message wiring.
636
- **API:** Compound. Wraps any input control (`Input`, `Select`, `Combobox`, etc.) and binds it to react-hook-form via `name`.
676
+ **API:** Compound — `Field.Root`, `Field.Label`, `Field.Control`, `Field.Description`, `Field.Error`,
677
+ `Field.Validity`. `Field.Root` establishes a context boundary; every Base UI-backed AppShell control
678
+ placed inside it inherits label association, `aria-describedby`, `disabled`, and invalid state
679
+ automatically.
680
+ **`Field.Control` is already a styled input** — write `<Field.Control />`, not
681
+ `<Field.Control render={<Input />} />`. `Input` and `Field.Control` share the same base classes, so
682
+ the `render` form just double-wraps. Reach for `Input` directly when you need a control outside a
683
+ `Field.Root`, or when the value is React-controlled.
684
+ **RHF interop (optional):** `Field.Root` accepts `isTouched`, `isDirty`, `invalid`, and `error`,
685
+ matching RHF's `fieldState` shape — so `<Field.Root {...fieldState}>` works with no adapter.
637
686
  **Used in patterns:** all `form/*`.
638
687
 
688
+ ### How values reach your submit handler
689
+
690
+ There are three separate mechanisms. Picking the wrong mental model is the most common
691
+ form mistake, so be explicit about which one a screen uses.
692
+
693
+ **1. `Form` + `Field.Root` → `onFormSubmit` (the default).** `onFormSubmit` does **not** read
694
+ `FormData`; it collects values from the `Field.Root`s registered inside the `Form`, keyed by each
695
+ field's `name`. Every AppShell control works this way once wrapped in a `Field.Root name="…"` —
696
+ including `Select`, `Combobox`, and `Autocomplete`. They need **no `name` of their own and no React
697
+ state**:
698
+
699
+ ```tsx
700
+ <Form onFormSubmit={(values) => save(values)}>
701
+ <Field.Root name="category">
702
+ <Field.Label>Category</Field.Label>
703
+ <Select items={CATEGORIES} placeholder="Select category" />
704
+ </Field.Root>
705
+ </Form>
706
+ ```
707
+
708
+ **2. Native submission** — a plain `<form>`, `new FormData(form)`, or a server action. This reads the
709
+ DOM, so each control needs its own `name`. `Select`, `Combobox`, and `Autocomplete` gained `name`
710
+ (plus `form`, `required`, `inputRef`) in 1.13.0; before that they contributed nothing to a native
711
+ payload.
712
+
713
+ **3. React Hook Form** — optional, consumer-installed. RHF owns the values; drive the control with
714
+ `value` / `onValueChange` from a `Controller` and spread `fieldState` onto `Field.Root`. Warranted
715
+ for cross-field validation, field arrays, or a Zod resolver — not for ordinary CRUD forms.
716
+
717
+ #### Object items
718
+
719
+ Under mechanism 1, a non-string item is serialised into the submitted value:
720
+
721
+ | Item shape | Value in `onFormSubmit` |
722
+ | -------------------------------------- | ------------------------------------ |
723
+ | `string` | the string |
724
+ | `{ value, label }` | `value`, automatically |
725
+ | any other object | **JSON string** — usually not wanted |
726
+ | any other object + `itemToStringValue` | whatever that function returns |
727
+
728
+ So for arbitrary objects, pass `itemToStringValue` to choose the submitted key. It is distinct from
729
+ `mapItem`, which controls what the user sees:
730
+
731
+ ```tsx
732
+ <Combobox
733
+ items={warehouses}
734
+ mapItem={(w) => ({ label: w.name, key: String(w.id) })}
735
+ itemToStringValue={(w) => String(w.id)}
736
+ />
737
+ ```
738
+
739
+ Multi-select submits an array.
740
+
639
741
  ### `Fieldset`
640
742
 
641
743
  **Import:** `import { Fieldset } from '@tailor-platform/app-shell'`
@@ -657,7 +759,7 @@ const table = useDataTable({
657
759
  **Import:** `import { Textarea } from '@tailor-platform/app-shell'`
658
760
  **Purpose:** Multi-line text input. Maps to spec field types: `text`, long-form `string`. Use instead of `Input` whenever the value is prose (notes, descriptions, replies, reasons).
659
761
  **API:** `TextareaProps` — Base UI's field control (`value`, `defaultValue`, `onChange`, `onValueChange`, `disabled`, `required`, `readOnly`, `name`, aria-\*) rendered as a `<textarea>`, plus `rows`. `cols` / `wrap` are not accepted — the control is `w-full`, so neither can take effect.
660
- **Sizing:** no fixed height — `rows` sets the visible line count and the user can drag it taller. `min-h-16` is a hard floor, so `rows` only takes effect from 3 up. Width is the parent's job: the control is `w-full`, and `cols` is not accepted because it could never win against that. Do **not** reach for `Input` and a height override; `Input` is locked to `h-9`, and an `astw:h-*` override from a consuming app resolves to nothing unless that exact utility was compiled into the package CSS.
762
+ **Sizing:** no fixed height — `rows` sets the visible line count and the user can drag it taller. `min-h-16` is a hard floor, so `rows` only takes effect from 3 up. Width is the parent's job: the control is `w-full`, and `cols` is not accepted because it could never win against that. Do **not** reach for `Input` and a height override; `Input` is locked to `h-9`, and an application must not use AppShell's internal `astw:` classes.
661
763
  **Field integration:** drops straight into `Field.Root` for label/description/error wiring exactly like `Input` and `Checkbox` — no bespoke `error` prop.
662
764
  **Example:**
663
765
 
@@ -702,6 +804,22 @@ const table = useDataTable({
702
804
 
703
805
  ---
704
806
 
807
+ ## AI
808
+
809
+ ### `AIChat`
810
+
811
+ > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/ai-chat.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/ai-chat.md)
812
+
813
+ **Import:** `import { AIChat } from '@tailor-platform/app-shell'`
814
+ **Purpose:** Building blocks for an LLM assistant UI — a streaming conversation view over a composer, plus reasoning, tool-call, and citation parts. The root places three regions in a fixed order, `Layout`-style: `AIChat.Header` (optional) over `AIChat.Conversation` over `AIChat.Composer` (optional). Each region carries its own props; the root carries `status`.
815
+ **API:** Root (`status`) + regions — `AIChat.Header` (`title`, `icon`, `actions`), `AIChat.Conversation` (`autoScroll`, transcript as children), `AIChat.Composer` (`onSubmit`, `onStop`, `value`/`defaultValue`/`onValueChange`, `placeholder`, `disabled`, `submitOnEnter`, `actions`) — + transcript parts: `AIChat.Message`, `.Response`, `.EmptyState`, `.Suggestions`/`.Suggestion`, `.Actions`/`.Action`, `.Reasoning`/`.ReasoningTrigger`/`.ReasoningContent`, `.ChainOfThought*`, `.Tool*`, `.Sources*`, `.History`.
816
+ **Composer:** Body is `Textarea` over one action row per `form/composer`'s layout. Enter submits (IME-safe), Shift+Enter newlines, and `status` from `useAIChat()` drives the busy/Stop state.
817
+ **Header:** a 48px strip (graphic + `text-sm font-semibold` title + right action slot) closed by a full-width rule; renders only when `title` or `actions` is set.
818
+ **Renders no chrome:** no border or background of its own — wrap in `Card`, `Sheet`, or a `Layout.Column`, with `overflow-hidden` so the header rule stays inside rounded corners.
819
+ **Used in patterns:** none yet — a component, not a catalogue pattern (platform-planning#1748).
820
+
821
+ ---
822
+
705
823
  ## Auth & access
706
824
 
707
825
  ### `AuthProvider`
@@ -1,6 +1,6 @@
1
1
  # Design System
2
2
 
3
- Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, the `astw:` prefix, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `components.md`; that split avoids duplicating tables and lengthy examples across both docs.
3
+ Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `components.md`; that split avoids duplicating tables and lengthy examples across both docs.
4
4
 
5
5
  `@tailor-platform/app-shell` ships an opinionated design system as CSS variables, bridged into Tailwind v4's token namespace. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
6
6
 
@@ -302,23 +302,30 @@ Stock Tailwind breakpoints — AppShell does not change them.
302
302
 
303
303
  Two-column **behavior** (right rail stacks under `lg`): respect AppShell defaults — do not force side-by-side grids on narrow viewports. The `Layout` column width table lives in `components.md` → Layout; reuse those numbers instead of guessing rem values here.
304
304
 
305
- ## 5. The `astw:` prefix
305
+ ## 4b. Links
306
306
 
307
- AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName` and `className` on roots. Prefix those utilities with `astw:` so they apply to the wrapper AppShell controls.
307
+ One treatment for every text link — inside `DescriptionCard`, in table rows,
308
+ in cards:
308
309
 
309
- **Do not duplicate full component trees here.** Typical patterns (full `DataTable` composition, `Sheet` + footer, `Table.Root` + card insets) live in `components.md` with JSX you can copy.
310
+ - **`text-primary` at rest, `hover:underline` only.** Colour carries the affordance; a resting underline is noise. Pair with `underline-offset-4`.
311
+ - **Internal routes** use app-shell's `Link` (`to`), never a bare `<a href>`.
312
+ - **External destinations** use `<a target="_blank" rel="noopener noreferrer">` with a lucide `ExternalLink` at `size-3` after the label. There is no dedicated component for this; that markup is the convention.
313
+ - **Identifiers** (document numbers, SKUs) keep `font-mono` on top of the link style.
310
314
 
311
- Minimal illustration — the same rules apply to other `*ClassName` hooks:
315
+ This is the treatment `DescriptionCard` already applies to its `type: "link"`
316
+ fields — match it rather than inventing a second one.
317
+
318
+ ## 5. AppShell styling boundary
319
+
320
+ `astw:` is AppShell's internal compiled CSS prefix. **Never write it in an application.** It is not a consumer customization API and arbitrary prefixed utilities may not exist in the shipped CSS.
321
+
322
+ Use ordinary Tailwind utilities on your own markup. An explicitly documented layout hook, such as `Table.Root`'s `containerClassName`, also accepts ordinary utilities from the application's Tailwind build:
312
323
 
313
324
  ```tsx
314
- <Table.Root containerClassName="astw:px-6 astw:overflow-y-auto" />
325
+ <Table.Root containerClassName="px-6 overflow-y-auto" />
315
326
  ```
316
327
 
317
- Rules:
318
-
319
- - `astw:` only on AppShell `*ClassName` / root `className` hooks each component exposes. Use **plain** Tailwind (`flex`, `gap-4`, `bg-background`, …) on **your** markup.
320
- - Stick to **layout** utilities (`flex`, `grid`, `max-h-*`, `min-h-0`, `overflow-*`, widths). Avoid painting over internal AppShell padding or colors via `astw:` — prefer an upstream prop or composition change.
321
- - Steps like `astw:p-4` still resolve through the scale — never arbitrary `astw:p-[13px]`.
328
+ For an AppShell component's appearance, use its documented props, variants, or composition instead of styling over its internals.
322
329
 
323
330
  ## 6. When AppShell doesn't have a component you need
324
331
 
@@ -380,13 +387,13 @@ When a custom component proves reusable across 2+ apps, promote it upstream into
380
387
 
381
388
  ### Where to look
382
389
 
383
- | Concern | File |
384
- | --------------------------------------------------- | ---------------------------------- |
385
- | Component imports, props, JSX composition | **`components.md`** |
386
- | Page / screen layout patterns | **`patterns/<type>/<slug>.md`** |
387
- | Design tokens, `astw:` rules, custom UI conformance | **this file** (`design-system.md`) |
390
+ | Concern | File |
391
+ | --------------------------------------------------------------- | ---------------------------------- |
392
+ | Component imports, props, JSX composition | **`components.md`** |
393
+ | Page / screen layout patterns | **`patterns/<type>/<slug>.md`** |
394
+ | Design tokens, AppShell styling boundary, custom UI conformance | **this file** (`design-system.md`) |
388
395
 
389
- **`components.md`** holds long JSX compositions (e.g. **`DataTable`**, **card + `Table`**). **Do not copy those trees into this file** — link back here only for tokens and `astw:` policy.
396
+ **`components.md`** holds long JSX compositions (e.g. **`DataTable`**, **card + `Table`**). **Do not copy those trees into this file** — link back here only for tokens and the AppShell styling boundary.
390
397
 
391
398
  `patterns/` is organised by page type: `patterns/list/`, `patterns/detail/`, `patterns/form/`, `patterns/interaction/`. Pick the slug matching the screen you're building.
392
399
 
@@ -108,7 +108,7 @@ For `pnpm gql-tada:check` to pass, always destructure with optional chaining and
108
108
 
109
109
  For **standard list pages**, do not hand-roll only `first` / `after`. Compose AppShell **`DataTable`** + **`useCollectionVariables`** + **`useDataTable`**: derive query variables from `variables.pagination`, `variables.query`, `variables.order`, map `edges` / `pageInfo` / `total` into `useDataTable`’s `data`. See **`patterns/list/dense-scan.md`** and **`components.md` → DataTable**.
110
110
 
111
- When `@tailor-platform/app-shell-sdk-plugin` codegen is enabled, pass generated **`tableMetadata`** into **`useCollectionVariables`** so `variables.query` / `variables.order` are typed against your Tailor GraphQL inputs (upstream **data-table.md** Typed query variables).
111
+ When `@tailor-platform/sdk-plugin-app-shell` codegen is enabled, pass generated **`tableMetadata`** into **`useCollectionVariables`** so `variables.query` / `variables.order` are typed against your Tailor GraphQL inputs (upstream **data-table.md** Typed query variables).
112
112
 
113
113
  ## Mutations and cache invalidation
114
114