@tailor-platform/app-shell 1.12.0 → 1.14.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.
Files changed (140) hide show
  1. package/README.md +1 -1
  2. package/dist/app-shell-core.css +3 -1
  3. package/dist/app-shell.d.ts +1038 -46
  4. package/dist/app-shell.js +10700 -14287
  5. package/dist/appshell-CtS8Y3LW.js +1823 -0
  6. package/dist/fonts/NOTO-SANS-JP-LICENSE.txt +93 -0
  7. package/dist/fonts/files/noto-sans-jp-0-wght-normal.woff2 +0 -0
  8. package/dist/fonts/files/noto-sans-jp-1-wght-normal.woff2 +0 -0
  9. package/dist/fonts/files/noto-sans-jp-10-wght-normal.woff2 +0 -0
  10. package/dist/fonts/files/noto-sans-jp-100-wght-normal.woff2 +0 -0
  11. package/dist/fonts/files/noto-sans-jp-101-wght-normal.woff2 +0 -0
  12. package/dist/fonts/files/noto-sans-jp-102-wght-normal.woff2 +0 -0
  13. package/dist/fonts/files/noto-sans-jp-103-wght-normal.woff2 +0 -0
  14. package/dist/fonts/files/noto-sans-jp-104-wght-normal.woff2 +0 -0
  15. package/dist/fonts/files/noto-sans-jp-105-wght-normal.woff2 +0 -0
  16. package/dist/fonts/files/noto-sans-jp-106-wght-normal.woff2 +0 -0
  17. package/dist/fonts/files/noto-sans-jp-107-wght-normal.woff2 +0 -0
  18. package/dist/fonts/files/noto-sans-jp-108-wght-normal.woff2 +0 -0
  19. package/dist/fonts/files/noto-sans-jp-109-wght-normal.woff2 +0 -0
  20. package/dist/fonts/files/noto-sans-jp-11-wght-normal.woff2 +0 -0
  21. package/dist/fonts/files/noto-sans-jp-110-wght-normal.woff2 +0 -0
  22. package/dist/fonts/files/noto-sans-jp-111-wght-normal.woff2 +0 -0
  23. package/dist/fonts/files/noto-sans-jp-112-wght-normal.woff2 +0 -0
  24. package/dist/fonts/files/noto-sans-jp-113-wght-normal.woff2 +0 -0
  25. package/dist/fonts/files/noto-sans-jp-114-wght-normal.woff2 +0 -0
  26. package/dist/fonts/files/noto-sans-jp-115-wght-normal.woff2 +0 -0
  27. package/dist/fonts/files/noto-sans-jp-116-wght-normal.woff2 +0 -0
  28. package/dist/fonts/files/noto-sans-jp-117-wght-normal.woff2 +0 -0
  29. package/dist/fonts/files/noto-sans-jp-118-wght-normal.woff2 +0 -0
  30. package/dist/fonts/files/noto-sans-jp-119-wght-normal.woff2 +0 -0
  31. package/dist/fonts/files/noto-sans-jp-12-wght-normal.woff2 +0 -0
  32. package/dist/fonts/files/noto-sans-jp-13-wght-normal.woff2 +0 -0
  33. package/dist/fonts/files/noto-sans-jp-14-wght-normal.woff2 +0 -0
  34. package/dist/fonts/files/noto-sans-jp-15-wght-normal.woff2 +0 -0
  35. package/dist/fonts/files/noto-sans-jp-16-wght-normal.woff2 +0 -0
  36. package/dist/fonts/files/noto-sans-jp-17-wght-normal.woff2 +0 -0
  37. package/dist/fonts/files/noto-sans-jp-18-wght-normal.woff2 +0 -0
  38. package/dist/fonts/files/noto-sans-jp-19-wght-normal.woff2 +0 -0
  39. package/dist/fonts/files/noto-sans-jp-2-wght-normal.woff2 +0 -0
  40. package/dist/fonts/files/noto-sans-jp-20-wght-normal.woff2 +0 -0
  41. package/dist/fonts/files/noto-sans-jp-21-wght-normal.woff2 +0 -0
  42. package/dist/fonts/files/noto-sans-jp-22-wght-normal.woff2 +0 -0
  43. package/dist/fonts/files/noto-sans-jp-23-wght-normal.woff2 +0 -0
  44. package/dist/fonts/files/noto-sans-jp-24-wght-normal.woff2 +0 -0
  45. package/dist/fonts/files/noto-sans-jp-25-wght-normal.woff2 +0 -0
  46. package/dist/fonts/files/noto-sans-jp-26-wght-normal.woff2 +0 -0
  47. package/dist/fonts/files/noto-sans-jp-27-wght-normal.woff2 +0 -0
  48. package/dist/fonts/files/noto-sans-jp-28-wght-normal.woff2 +0 -0
  49. package/dist/fonts/files/noto-sans-jp-29-wght-normal.woff2 +0 -0
  50. package/dist/fonts/files/noto-sans-jp-3-wght-normal.woff2 +0 -0
  51. package/dist/fonts/files/noto-sans-jp-30-wght-normal.woff2 +0 -0
  52. package/dist/fonts/files/noto-sans-jp-31-wght-normal.woff2 +0 -0
  53. package/dist/fonts/files/noto-sans-jp-32-wght-normal.woff2 +0 -0
  54. package/dist/fonts/files/noto-sans-jp-33-wght-normal.woff2 +0 -0
  55. package/dist/fonts/files/noto-sans-jp-34-wght-normal.woff2 +0 -0
  56. package/dist/fonts/files/noto-sans-jp-35-wght-normal.woff2 +0 -0
  57. package/dist/fonts/files/noto-sans-jp-36-wght-normal.woff2 +0 -0
  58. package/dist/fonts/files/noto-sans-jp-37-wght-normal.woff2 +0 -0
  59. package/dist/fonts/files/noto-sans-jp-38-wght-normal.woff2 +0 -0
  60. package/dist/fonts/files/noto-sans-jp-39-wght-normal.woff2 +0 -0
  61. package/dist/fonts/files/noto-sans-jp-4-wght-normal.woff2 +0 -0
  62. package/dist/fonts/files/noto-sans-jp-40-wght-normal.woff2 +0 -0
  63. package/dist/fonts/files/noto-sans-jp-41-wght-normal.woff2 +0 -0
  64. package/dist/fonts/files/noto-sans-jp-42-wght-normal.woff2 +0 -0
  65. package/dist/fonts/files/noto-sans-jp-43-wght-normal.woff2 +0 -0
  66. package/dist/fonts/files/noto-sans-jp-44-wght-normal.woff2 +0 -0
  67. package/dist/fonts/files/noto-sans-jp-45-wght-normal.woff2 +0 -0
  68. package/dist/fonts/files/noto-sans-jp-46-wght-normal.woff2 +0 -0
  69. package/dist/fonts/files/noto-sans-jp-47-wght-normal.woff2 +0 -0
  70. package/dist/fonts/files/noto-sans-jp-48-wght-normal.woff2 +0 -0
  71. package/dist/fonts/files/noto-sans-jp-49-wght-normal.woff2 +0 -0
  72. package/dist/fonts/files/noto-sans-jp-5-wght-normal.woff2 +0 -0
  73. package/dist/fonts/files/noto-sans-jp-50-wght-normal.woff2 +0 -0
  74. package/dist/fonts/files/noto-sans-jp-51-wght-normal.woff2 +0 -0
  75. package/dist/fonts/files/noto-sans-jp-52-wght-normal.woff2 +0 -0
  76. package/dist/fonts/files/noto-sans-jp-53-wght-normal.woff2 +0 -0
  77. package/dist/fonts/files/noto-sans-jp-54-wght-normal.woff2 +0 -0
  78. package/dist/fonts/files/noto-sans-jp-55-wght-normal.woff2 +0 -0
  79. package/dist/fonts/files/noto-sans-jp-6-wght-normal.woff2 +0 -0
  80. package/dist/fonts/files/noto-sans-jp-60-wght-normal.woff2 +0 -0
  81. package/dist/fonts/files/noto-sans-jp-61-wght-normal.woff2 +0 -0
  82. package/dist/fonts/files/noto-sans-jp-62-wght-normal.woff2 +0 -0
  83. package/dist/fonts/files/noto-sans-jp-63-wght-normal.woff2 +0 -0
  84. package/dist/fonts/files/noto-sans-jp-64-wght-normal.woff2 +0 -0
  85. package/dist/fonts/files/noto-sans-jp-65-wght-normal.woff2 +0 -0
  86. package/dist/fonts/files/noto-sans-jp-66-wght-normal.woff2 +0 -0
  87. package/dist/fonts/files/noto-sans-jp-67-wght-normal.woff2 +0 -0
  88. package/dist/fonts/files/noto-sans-jp-68-wght-normal.woff2 +0 -0
  89. package/dist/fonts/files/noto-sans-jp-69-wght-normal.woff2 +0 -0
  90. package/dist/fonts/files/noto-sans-jp-7-wght-normal.woff2 +0 -0
  91. package/dist/fonts/files/noto-sans-jp-70-wght-normal.woff2 +0 -0
  92. package/dist/fonts/files/noto-sans-jp-71-wght-normal.woff2 +0 -0
  93. package/dist/fonts/files/noto-sans-jp-72-wght-normal.woff2 +0 -0
  94. package/dist/fonts/files/noto-sans-jp-73-wght-normal.woff2 +0 -0
  95. package/dist/fonts/files/noto-sans-jp-74-wght-normal.woff2 +0 -0
  96. package/dist/fonts/files/noto-sans-jp-75-wght-normal.woff2 +0 -0
  97. package/dist/fonts/files/noto-sans-jp-76-wght-normal.woff2 +0 -0
  98. package/dist/fonts/files/noto-sans-jp-77-wght-normal.woff2 +0 -0
  99. package/dist/fonts/files/noto-sans-jp-78-wght-normal.woff2 +0 -0
  100. package/dist/fonts/files/noto-sans-jp-79-wght-normal.woff2 +0 -0
  101. package/dist/fonts/files/noto-sans-jp-8-wght-normal.woff2 +0 -0
  102. package/dist/fonts/files/noto-sans-jp-80-wght-normal.woff2 +0 -0
  103. package/dist/fonts/files/noto-sans-jp-81-wght-normal.woff2 +0 -0
  104. package/dist/fonts/files/noto-sans-jp-82-wght-normal.woff2 +0 -0
  105. package/dist/fonts/files/noto-sans-jp-83-wght-normal.woff2 +0 -0
  106. package/dist/fonts/files/noto-sans-jp-84-wght-normal.woff2 +0 -0
  107. package/dist/fonts/files/noto-sans-jp-85-wght-normal.woff2 +0 -0
  108. package/dist/fonts/files/noto-sans-jp-86-wght-normal.woff2 +0 -0
  109. package/dist/fonts/files/noto-sans-jp-87-wght-normal.woff2 +0 -0
  110. package/dist/fonts/files/noto-sans-jp-88-wght-normal.woff2 +0 -0
  111. package/dist/fonts/files/noto-sans-jp-89-wght-normal.woff2 +0 -0
  112. package/dist/fonts/files/noto-sans-jp-9-wght-normal.woff2 +0 -0
  113. package/dist/fonts/files/noto-sans-jp-90-wght-normal.woff2 +0 -0
  114. package/dist/fonts/files/noto-sans-jp-91-wght-normal.woff2 +0 -0
  115. package/dist/fonts/files/noto-sans-jp-92-wght-normal.woff2 +0 -0
  116. package/dist/fonts/files/noto-sans-jp-93-wght-normal.woff2 +0 -0
  117. package/dist/fonts/files/noto-sans-jp-94-wght-normal.woff2 +0 -0
  118. package/dist/fonts/files/noto-sans-jp-95-wght-normal.woff2 +0 -0
  119. package/dist/fonts/files/noto-sans-jp-96-wght-normal.woff2 +0 -0
  120. package/dist/fonts/files/noto-sans-jp-97-wght-normal.woff2 +0 -0
  121. package/dist/fonts/files/noto-sans-jp-98-wght-normal.woff2 +0 -0
  122. package/dist/fonts/files/noto-sans-jp-99-wght-normal.woff2 +0 -0
  123. package/dist/fonts/noto-sans-jp.css +2506 -0
  124. package/dist/style-entry.d.ts +600 -0
  125. package/dist/style-entry.js +0 -1
  126. package/dist/testing.d.ts +1140 -0
  127. package/dist/testing.js +14 -0
  128. package/dist/theme.bridge.css +29 -1
  129. package/dist/vite-plugin.d.ts +607 -1
  130. package/dist/vite-plugin.js +2 -4
  131. package/package.json +23 -15
  132. package/skills/app-shell-patterns/SKILL.md +14 -6
  133. package/skills/app-shell-patterns/references/fundamental/components.md +147 -4
  134. package/skills/app-shell-patterns/references/fundamental/design-system.md +1 -1
  135. package/skills/app-shell-patterns/references/fundamental/graphql.md +1 -1
  136. package/skills/app-shell-patterns/references/patterns/form-composer.md +142 -0
  137. package/skills/app-shell-patterns/references/patterns/form-modal.md +84 -30
  138. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +136 -64
  139. package/skills/app-shell-patterns/references/patterns/form-single-page.md +58 -26
  140. package/skills/app-shell-patterns/references/patterns/form-wizard.md +157 -69
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tailor-platform/app-shell",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "An opinionated React application framework for building ERP applications on Tailor Platform",
5
5
  "keywords": [
6
6
  "app-shell",
@@ -37,6 +37,10 @@
37
37
  "./vite-plugin": {
38
38
  "types": "./dist/vite-plugin.d.ts",
39
39
  "default": "./dist/vite-plugin.js"
40
+ },
41
+ "./testing": {
42
+ "types": "./dist/testing.d.ts",
43
+ "default": "./dist/testing.js"
40
44
  }
41
45
  },
42
46
  "publishConfig": {
@@ -46,7 +50,7 @@
46
50
  "@base-ui/react": "1.6.0",
47
51
  "@internationalized/date": "3.12.2",
48
52
  "@standard-schema/spec": "^1.1.0",
49
- "@tailor-platform/auth-public-client": "^0.5.1",
53
+ "@tailor-platform/auth-public-client": "^0.6.1",
50
54
  "change-case": "^5.4.4",
51
55
  "class-variance-authority": "^0.7.1",
52
56
  "clsx": "^2.1.1",
@@ -57,29 +61,31 @@
57
61
  "react-router": "^8.3.0",
58
62
  "sonner": "^2.0.7",
59
63
  "tailwind-merge": "^3.6.0",
60
- "@tailor-platform/app-shell-vite-plugin": "0.2.3"
64
+ "@tailor-platform/vite-plugin-app-shell": "0.3.0"
61
65
  },
62
66
  "devDependencies": {
63
- "@tailwindcss/postcss": "^4.3.0",
64
- "@testing-library/react": "16.3.2",
65
- "@testing-library/user-event": "14.6.3",
66
- "@types/encoding-japanese": "^2.2.1",
67
+ "@fontsource-variable/noto-sans-jp": "5.3.0",
68
+ "@microsoft/api-extractor": "7.58.12",
69
+ "@tailwindcss/postcss": "^4.3.3",
70
+ "@testing-library/react": "16.3.3",
71
+ "@testing-library/user-event": "14.6.7",
72
+ "@types/encoding-japanese": "2.2.1",
67
73
  "@types/node": "^25.9.5",
68
- "@types/papaparse": "^5.5.2",
74
+ "@types/papaparse": "5.5.2",
69
75
  "@types/react": "^19",
70
76
  "@types/react-dom": "^19",
71
- "@vitejs/plugin-react": "^5.2.0",
77
+ "@vitejs/plugin-react": "^6.1.0",
78
+ "@vitest/coverage-v8": "4.1.10",
72
79
  "happy-dom": "20.9.0",
73
80
  "postcss": "8.5.26",
74
81
  "react": "^19.2.7",
75
82
  "react-dom": "^19.2.7",
76
- "tailwindcss": "^4.2.4",
77
- "tw-animate-css": "^1.4.0",
83
+ "react-hook-form": "7.71.2",
84
+ "tailwindcss": "^4.3.3",
85
+ "tw-animate-css": "1.4.0",
78
86
  "typescript": "5.9.3",
79
- "vite": "^7.3.2",
80
- "vite-plugin-dts": "^4.5.0",
81
- "vite-plugin-externalize-deps": "^0.10.0",
82
- "vite-tsconfig-paths": "^6.1.1",
87
+ "vite": "^8.2.2",
88
+ "vite-plugin-dts": "5.0.3",
83
89
  "vitest": "^4.1.10"
84
90
  },
85
91
  "peerDependencies": {
@@ -89,6 +95,8 @@
89
95
  "scripts": {
90
96
  "dev": "vite build --watch --mode development",
91
97
  "build": "vite build",
98
+ "check-dts": "node scripts/check-dts.mjs",
99
+ "vendor:fonts": "node scripts/sync-noto-fonts.mjs",
92
100
  "lint": "oxlint -c .oxlintrc.jsonc",
93
101
  "test": "vitest run",
94
102
  "type-check": "tsc --incremental"
@@ -25,6 +25,12 @@ 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 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 compares the layouts a screen could take and says when each applies. Match the screen first, then use the patterns the entry cites for the pieces within it.
31
+
32
+
33
+
28
34
  ## Available Patterns
29
35
 
30
36
  ### detail
@@ -35,12 +41,13 @@ Read it when upgrading `@tailor-platform/app-shell`, and whenever styling, themi
35
41
 
36
42
  ### form
37
43
 
38
- | Slug | Name | Description |
39
- | ------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------- |
40
- | [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen |
41
- | [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections |
42
- | [`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 |
43
- | [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
44
+ | Slug | Name | Description |
45
+ | ------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ |
46
+ | [`form/composer`](references/patterns/form-composer.md) | Composer | Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place |
47
+ | [`form/modal`](references/patterns/form-modal.md) | Modal Form | Default form pattern for Create/Edit — keeps user in context on the parent screen |
48
+ | [`form/sectioned`](references/patterns/form-sectioned.md) | Sectioned Form | Complex form with 15+ fields organized into named fieldset sections |
49
+ | [`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 |
50
+ | [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
44
51
 
45
52
  ### interaction
46
53
 
@@ -82,4 +89,5 @@ Full rationale in [`design-system.md`](references/fundamental/design-system.md)
82
89
  - **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`.
83
90
  - **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row.
84
91
  - **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one.
92
+ - **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.
85
93
  - **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path.
@@ -194,6 +194,13 @@ import { Button, Link } from '@tailor-platform/app-shell';
194
194
  **API:** `to`, `replace`, `state`, etc. — same as `react-router`.
195
195
  **Used in patterns:** all (navigation).
196
196
 
197
+ ### `NavLink`
198
+
199
+ **Import:** `import { NavLink } from '@tailor-platform/app-shell'`
200
+ **Purpose:** `Link` that knows when it is active — for nav menus and tab bars.
201
+ **API:** `to`, plus `className` / `style` / `children` as render functions receiving `{ isActive, isPending }`.
202
+ **Used in patterns:** navigation shells.
203
+
197
204
  ### `Dialog`
198
205
 
199
206
  > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/dialog.md)
@@ -426,7 +433,7 @@ const table = useDataTable({
426
433
 
427
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"`.
428
435
 
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.
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.
430
437
 
431
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`**.
432
439
 
@@ -612,13 +619,41 @@ const table = useDataTable({
612
619
 
613
620
  ## Forms
614
621
 
622
+ > **Which form stack?** AppShell's `Form` / `Field` / `Fieldset` wrap Base UI primitives and are the
623
+ > default — they need no extra dependency. They own **accessibility wiring and visual state only**
624
+ > (`htmlFor`, `aria-describedby`, `data-invalid` / `data-dirty` / `data-touched`); they do **not** own
625
+ > your values. React Hook Form is **optional** and consumer-installed — reach for it only when a form
626
+ > genuinely needs cross-field validation, field arrays, or a Zod resolver. The two compose; they are
627
+ > not alternatives. `react-hook-form` stopped being a runtime dependency in 1.4.0; it is a dev-only
628
+ > dependency of the package today, used to test the RHF integration contracts — if you use it,
629
+ > install it in your own app.
630
+
615
631
  ### `Form`
616
632
 
617
633
  > 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)
618
634
 
619
635
  **Import:** `import { Form } from '@tailor-platform/app-shell'`
620
- **Purpose:** Form root wired to react-hook-form. Use with `Field`, `Fieldset`, and Zod for validation.
621
- **API:** `FormProps` — `errors`, `actionsRef`, `validationMode`, `noValidate`, plus a namespace exposing form-related sub-helpers. Generic over `FormValues`.
636
+ **Purpose:** Form root that wraps Base UI's form primitive. Provides shared validation context for
637
+ child `Field.Root`s, consolidated error display, and server-error routing by field `name`.
638
+ **API:** `FormProps` — `onFormSubmit`, `onSubmit`, `errors`, `actionsRef`, `validationMode`,
639
+ `noValidate`, `id`. Generic over `FormValues`.
640
+ **Actions outside the form:** give the `Form` an `id` and point a detached submit button at it with
641
+ the native `form` attribute. This is how a page-header Save reaches a body form (1.13.0+):
642
+
643
+ ```tsx
644
+ <Layout.Header
645
+ title="Create product"
646
+ actions={[<Button key="save" type="submit" form="product-form">Save</Button>]}
647
+ />
648
+ <Form id="product-form" onFormSubmit={save}>…</Form>
649
+ ```
650
+
651
+ **Submit:** use `onFormSubmit(values)` — it receives parsed form values and is the default. Use the
652
+ low-level `onSubmit` **only** when handing the native event to React Hook Form's `handleSubmit`.
653
+ **Never** hand-roll `<form onSubmit={e => new FormData(e.currentTarget)}>` — that skips validation
654
+ and error routing.
655
+ **Server errors:** pass `errors={{ fieldName: "message" }}`; they route to the matching
656
+ `Field.Error` and clear when the user edits that field.
622
657
  **Example:** see `form/single-page.md`.
623
658
  **Used in patterns:** all `form/*`.
624
659
 
@@ -626,9 +661,71 @@ const table = useDataTable({
626
661
 
627
662
  **Import:** `import { Field } from '@tailor-platform/app-shell'`
628
663
  **Purpose:** Single form field with label + control + error message wiring.
629
- **API:** Compound. Wraps any input control (`Input`, `Select`, `Combobox`, etc.) and binds it to react-hook-form via `name`.
664
+ **API:** Compound — `Field.Root`, `Field.Label`, `Field.Control`, `Field.Description`, `Field.Error`,
665
+ `Field.Validity`. `Field.Root` establishes a context boundary; every Base UI-backed AppShell control
666
+ placed inside it inherits label association, `aria-describedby`, `disabled`, and invalid state
667
+ automatically.
668
+ **`Field.Control` is already a styled input** — write `<Field.Control />`, not
669
+ `<Field.Control render={<Input />} />`. `Input` and `Field.Control` share the same base classes, so
670
+ the `render` form just double-wraps. Reach for `Input` directly when you need a control outside a
671
+ `Field.Root`, or when the value is React-controlled.
672
+ **RHF interop (optional):** `Field.Root` accepts `isTouched`, `isDirty`, `invalid`, and `error`,
673
+ matching RHF's `fieldState` shape — so `<Field.Root {...fieldState}>` works with no adapter.
630
674
  **Used in patterns:** all `form/*`.
631
675
 
676
+ ### How values reach your submit handler
677
+
678
+ There are three separate mechanisms. Picking the wrong mental model is the most common
679
+ form mistake, so be explicit about which one a screen uses.
680
+
681
+ **1. `Form` + `Field.Root` → `onFormSubmit` (the default).** `onFormSubmit` does **not** read
682
+ `FormData`; it collects values from the `Field.Root`s registered inside the `Form`, keyed by each
683
+ field's `name`. Every AppShell control works this way once wrapped in a `Field.Root name="…"` —
684
+ including `Select`, `Combobox`, and `Autocomplete`. They need **no `name` of their own and no React
685
+ state**:
686
+
687
+ ```tsx
688
+ <Form onFormSubmit={(values) => save(values)}>
689
+ <Field.Root name="category">
690
+ <Field.Label>Category</Field.Label>
691
+ <Select items={CATEGORIES} placeholder="Select category" />
692
+ </Field.Root>
693
+ </Form>
694
+ ```
695
+
696
+ **2. Native submission** — a plain `<form>`, `new FormData(form)`, or a server action. This reads the
697
+ DOM, so each control needs its own `name`. `Select`, `Combobox`, and `Autocomplete` gained `name`
698
+ (plus `form`, `required`, `inputRef`) in 1.13.0; before that they contributed nothing to a native
699
+ payload.
700
+
701
+ **3. React Hook Form** — optional, consumer-installed. RHF owns the values; drive the control with
702
+ `value` / `onValueChange` from a `Controller` and spread `fieldState` onto `Field.Root`. Warranted
703
+ for cross-field validation, field arrays, or a Zod resolver — not for ordinary CRUD forms.
704
+
705
+ #### Object items
706
+
707
+ Under mechanism 1, a non-string item is serialised into the submitted value:
708
+
709
+ | Item shape | Value in `onFormSubmit` |
710
+ | -------------------------------------- | ------------------------------------ |
711
+ | `string` | the string |
712
+ | `{ value, label }` | `value`, automatically |
713
+ | any other object | **JSON string** — usually not wanted |
714
+ | any other object + `itemToStringValue` | whatever that function returns |
715
+
716
+ So for arbitrary objects, pass `itemToStringValue` to choose the submitted key. It is distinct from
717
+ `mapItem`, which controls what the user sees:
718
+
719
+ ```tsx
720
+ <Combobox
721
+ items={warehouses}
722
+ mapItem={(w) => ({ label: w.name, key: String(w.id) })}
723
+ itemToStringValue={(w) => String(w.id)}
724
+ />
725
+ ```
726
+
727
+ Multi-select submits an array.
728
+
632
729
  ### `Fieldset`
633
730
 
634
731
  **Import:** `import { Fieldset } from '@tailor-platform/app-shell'`
@@ -643,6 +740,26 @@ const table = useDataTable({
643
740
  **API:** `InputProps` — extends native `<input>` props.
644
741
  **Used in patterns:** all `form/*`.
645
742
 
743
+ ### `Textarea`
744
+
745
+ > Full API: [https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/textarea.md](https://raw.githubusercontent.com/tailor-platform/app-shell/refs/heads/main/docs/components/textarea.md)
746
+
747
+ **Import:** `import { Textarea } from '@tailor-platform/app-shell'`
748
+ **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).
749
+ **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.
750
+ **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.
751
+ **Field integration:** drops straight into `Field.Root` for label/description/error wiring exactly like `Input` and `Checkbox` — no bespoke `error` prop.
752
+ **Example:**
753
+
754
+ ```tsx
755
+ <Field.Root name="reason">
756
+ <Field.Label>Reason</Field.Label>
757
+ <Textarea rows={4} placeholder="Why is this being rejected?" />
758
+ </Field.Root>
759
+ ```
760
+
761
+ **Used in patterns:** `form/composer`, `form/*` (long-text fields).
762
+
646
763
  ### `Checkbox`
647
764
 
648
765
  **Import:** `import { Checkbox } from '@tailor-platform/app-shell'`
@@ -730,6 +847,32 @@ Types for authoring guard functions used by `WithGuard` and `appShellPageProps.g
730
847
  **Purpose:** Re-exported from `react-router`. Use the AppShell barrel — never import from `react-router` directly.
731
848
  **Used in patterns:** all (navigation, route params, query state).
732
849
 
850
+ ### `useMatch`, `useResolvedPath`, `useNavigation`
851
+
852
+ **Import:** `import { useMatch, useResolvedPath, useNavigation } from '@tailor-platform/app-shell'`
853
+ **Purpose:** `useMatch` tests a pattern against the current location (active states); `useResolvedPath` resolves a relative path; `useNavigation` exposes the in-flight navigation for pending UI.
854
+ **Used in patterns:** navigation shells, list→detail transitions.
855
+
856
+ ### `useBlocker`, `useBeforeUnload`
857
+
858
+ **Import:** `import { useBlocker, useBeforeUnload } from '@tailor-platform/app-shell'`
859
+ **Purpose:** Guard navigation away from unsaved work — `useBlocker` for in-app navigation, `useBeforeUnload` for closing or reloading the tab.
860
+ **Used in patterns:** form-heavy screens.
861
+
862
+ ### Types
863
+
864
+ **Import:** `import type { Location, NavigateFunction, NavigateOptions, To, Params, PathMatch, LinkProps, NavLinkProps, Navigation, Blocker, BlockerFunction } from '@tailor-platform/app-shell'`
865
+ **Purpose:** The React Router types that go with the hooks above, so app code never imports types from `react-router` either.
866
+
867
+ ### Not exported
868
+
869
+ `createBrowserRouter`, `RouterProvider`, `MemoryRouter`, `Routes`, `Route` — AppShell builds the router; a second one nested inside it breaks navigation. `useLoaderData`, `Form`, `useSubmit`, `useFetcher`, `useActionData` — AppShell does not wire up React Router's data layer. Load data in the page component or the app's data layer.
870
+
871
+ ### Testing
872
+
873
+ **Import:** `import { AppShell, TestRouter } from '@tailor-platform/app-shell/testing'`
874
+ **Purpose:** Test routing without a `react-router` dependency. `AppShell` from this entry also accepts `memory` / `initialEntries`, for mounting a page at a fixed URL; `TestRouter` gives a single component router context, and takes `path` when that component reads the route (`useParams` is `{}` without it).
875
+
733
876
  ### `createTypedPaths`
734
877
 
735
878
  **Import:** `import { createTypedPaths } from '@tailor-platform/app-shell'`
@@ -184,7 +184,7 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth
184
184
 
185
185
  ### Typography
186
186
 
187
- **AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable) → `font-sans`, which the base layer already applies to `body`.
187
+ **AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable for Latin, Noto Sans JP Variable for Japanese) → `font-sans`, which the base layer already applies to `body`. Both are variable fonts, so every weight token is real in both scripts. Override the whole stack by setting `--app-shell-font-sans` on `:root`.
188
188
 
189
189
  Compose roles from stock Tailwind utilities. These pairings are what AppShell's own components use — match them so your screens sit consistently alongside the primitives:
190
190
 
@@ -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
 
@@ -0,0 +1,142 @@
1
+ ---
2
+ slug: pattern/form/composer
3
+ name: Composer
4
+ category: pattern
5
+ subcategory: form
6
+ description: Free-text composer — a multi-line input above a trailing action row, for notes, replies, and comments submitted in place
7
+ requiredImports: [Card, Textarea, Button, Checkbox]
8
+ tags: [composer, textarea, note, reply, comment, actions, discard, send]
9
+ do:
10
+ - A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason
11
+ - The prose IS the interaction; any other controls only qualify how the text is submitted
12
+ dont:
13
+ - The text is one field among many on a record form — use form/modal, form/single-page or form/sectioned with a Textarea field
14
+ - A chat input where Enter sends and there is no Discard — that is a different layout
15
+ ---
16
+
17
+ # pattern/form/composer
18
+
19
+ ## When to Use
20
+
21
+ - A free-text box the user fills and submits in place — add a note, reply on a thread, leave a comment, give a rejection reason
22
+ - The prose IS the interaction; any other controls only qualify how the text is submitted
23
+
24
+ ## Layout
25
+
26
+ ```
27
+ +---------------------------------------------------------+
28
+ | Card.Root / Card.Content |
29
+ | +-----------------------------------------------------+ |
30
+ | | Textarea | |
31
+ | | | |
32
+ | +-----------------------------------------------------+ |
33
+ | [ options slot … ] [Discard] [ Send ] |
34
+ +---------------------------------------------------------+
35
+ ```
36
+
37
+ Two rows inside one `Card`:
38
+
39
+ 1. **Body** — a single `Textarea`, sized with `rows`. Tall enough to read as "write here", short enough that the action row stays on screen.
40
+ 2. **Action row** — one `flex items-center justify-between` line with a fixed right side and a free left side.
41
+
42
+ ## The action row
43
+
44
+ This is the part the pattern actually fixes. Everything sits on **one line**, never stacked.
45
+
46
+ **Right side — fixed.** Reading left to right: an optional `variant="ghost"` **Discard**, then exactly **one** primary submit. Nothing else goes here.
47
+
48
+ - Discard clears the body; it does not close or navigate. Omit it when there is nothing to discard back to.
49
+ - The submit is the view's single primary `Button` — never a second filled button beside it.
50
+ - Both disable while `submitting`; the submit also disables on empty/whitespace-only input.
51
+ - Swap the submit's label for a pending label while in flight ("Send" → "Sending…"). Do not swap in a spinner-only button — the label is what tells the user what is happening.
52
+
53
+ **Left side — open.** Zero or more controls that qualify the submission, laid out `flex items-center gap-2` and allowed to shrink (`min-w-0`) so the buttons never get pushed off. Common occupants:
54
+
55
+ - a `Checkbox` toggle — visibility, "internal note", "notify watchers" (the worked example below)
56
+ - an attachment or template `Button` with `variant="ghost"`
57
+ - a small `Select` — reply-as, channel, canned response
58
+
59
+ Leave it empty and the buttons still sit correctly at the right; `justify-between` needs no placeholder element.
60
+
61
+ ## Page Implementation
62
+
63
+ ```tsx
64
+ /* pattern: form/composer */
65
+ import { useState } from "react";
66
+ import { Button, Card, Checkbox, Textarea } from "@tailor-platform/app-shell";
67
+
68
+ type Props = {
69
+ onSubmit: (body: string, internal: boolean) => void;
70
+ submitting?: boolean;
71
+ };
72
+
73
+ export default function Composer({ onSubmit, submitting = false }: Props) {
74
+ const [body, setBody] = useState("");
75
+ const [internal, setInternal] = useState(false);
76
+
77
+ return (
78
+ <Card.Root>
79
+ <Card.Content>
80
+ <div className="space-y-3">
81
+ <Textarea
82
+ aria-label="Reply"
83
+ value={body}
84
+ onChange={(e) => setBody(e.target.value)}
85
+ placeholder={internal ? "Internal context for teammates…" : "Reply to customer…"}
86
+ rows={4}
87
+ />
88
+ <div className="flex items-center justify-between gap-2">
89
+ {/* Options slot — 0..N controls that qualify the submission. An
90
+ internal-note toggle here; swap for an attachment button, a
91
+ visibility Select, a template picker, or drop it entirely. */}
92
+ <div className="flex min-w-0 items-center gap-2">
93
+ <Checkbox
94
+ label="Internal note (not sent to customer)"
95
+ checked={internal}
96
+ onCheckedChange={setInternal}
97
+ />
98
+ </div>
99
+ {/* Action slot — fixed shape: optional ghost Discard, then exactly
100
+ one primary submit. Never more than one primary. */}
101
+ <div className="flex shrink-0 gap-2">
102
+ <Button
103
+ variant="ghost"
104
+ onClick={() => setBody("")}
105
+ disabled={body.length === 0 || submitting}
106
+ >
107
+ Discard
108
+ </Button>
109
+ <Button
110
+ onClick={() => onSubmit(body, internal)}
111
+ disabled={body.trim().length === 0 || submitting}
112
+ >
113
+ {submitting ? "Sending…" : "Send"}
114
+ </Button>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </Card.Content>
119
+ </Card.Root>
120
+ );
121
+ }
122
+ ```
123
+
124
+ ## Constraints
125
+
126
+ - The body control is `Textarea`, never `Input` — `Input` is locked to `h-9` and clips prose to one 36px line.
127
+ - Size the body with `rows` (3–5 is the usual range). Don't reach for an `astw:h-*` / `astw:min-h-*` override: `astw:` utilities only resolve if that exact class was compiled into the AppShell package CSS, so an arbitrary one written in a consuming app silently does nothing.
128
+ - The `Textarea` needs an accessible name. There is usually no visible label in a composer, so pass `aria-label`; a placeholder is not a label.
129
+ - One primary `Button` in the action row. Discard is `ghost`, and any left-side action is `ghost` too.
130
+ - The action row is one row at every width — the left side shrinks, the buttons do not wrap under the body.
131
+ - Disable the submit on empty or whitespace-only input, and disable both buttons while `submitting`.
132
+ - Wrap the composer in `Card.Root` when it sits among other cards (a detail page, the end of a thread). A composer that is already inside a `Sheet` or `Dialog` body does not need its own card.
133
+
134
+ ## Anti-patterns
135
+
136
+ - `Input` (or `Field.Control` with no override) for the body — a single-line box for multi-line text.
137
+ - Discard rendered as `outline` or filled — it competes with the submit for the eye.
138
+ - Submit and Discard both filled, or a third action added to the right side.
139
+ - The action row stacked above the `Textarea`, or the buttons wrapped onto their own line — the composer stops reading as one unit.
140
+ - A left-side toggle that silently changes what the submit does without changing the submit's own affordance — if "internal note" changes the destination, the placeholder (or the label) should say so.
141
+ - Submit enabled on an empty body, so the user can post nothing.
142
+ - A body so tall (`rows={12}`) that the action row falls below the fold — the composer stops reading as one unit.