@tailor-platform/app-shell 1.11.0 → 1.13.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 -0
  2. package/dist/app-shell-core.css +3 -1
  3. package/dist/app-shell.d.ts +1192 -88
  4. package/dist/app-shell.js +10669 -13117
  5. package/dist/appshell-CSQWUk6q.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.test.ts +59 -0
  129. package/dist/theme.bridge.css +50 -1
  130. package/dist/vite-plugin.d.ts +600 -0
  131. package/dist/vite-plugin.js +2 -4
  132. package/package.json +27 -19
  133. package/skills/app-shell-patterns/SKILL.md +14 -7
  134. package/skills/app-shell-patterns/references/fundamental/components.md +56 -3
  135. package/skills/app-shell-patterns/references/fundamental/design-system.md +216 -162
  136. package/skills/app-shell-patterns/references/migrations.md +167 -0
  137. package/skills/app-shell-patterns/references/patterns/form-composer.md +142 -0
  138. package/skills/app-shell-patterns/references/patterns/form-single-page.md +3 -3
  139. package/skills/app-shell-patterns/references/patterns/form-wizard.md +5 -3
  140. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +1 -1
@@ -0,0 +1,167 @@
1
+ # Migrations
2
+
3
+ Every change that requires you to edit your application before or after upgrading, newest first.
4
+
5
+ This page is deliberately narrow. It is **not** a changelog — see [`packages/core/CHANGELOG.md`](https://github.com/tailor-platform/app-shell/blob/main/packages/core/CHANGELOG.md) for the full release history including features and fixes. A change belongs here only if an app that does nothing will break, misbehave, or silently drift.
6
+
7
+ Each entry states which versions are affected, what breaks, how to detect it, and what to change. Entries stay here permanently; they are not pruned when they get old, because apps upgrade across arbitrary version gaps.
8
+
9
+ | Version | Change |
10
+ | ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
11
+ | 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) |
12
+ | 1.11.0 | [React 19.2.7 and React Router v8 required](#1110-react-1927-and-react-router-v8-are-now-required) |
13
+ | 1.11.0 | [Non-modal `Sheet` renders no backdrop](#1110-non-modal-sheet-no-longer-renders-a-backdrop) |
14
+ | 1.8.0 | [`stream` removed from `useAIChat()`](#180-stream-removed-from-useaichat) |
15
+ | 1.5.0 → 1.7.0 | [Remove the theme bridge workaround](#150--170-remove-the-theme-bridge-workaround) |
16
+ | 1.5.0 | [`loader` removed from file-based pages](#150-loader-removed-from-file-based-page-definitions) |
17
+ | 1.3.0 | [Column inference and badge defaults changed](#130-column-inference-and-badge-defaults-changed) |
18
+ | 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) |
19
+ | before 1.0 | [Pre-1.0 breaking changes](#before-10) |
20
+
21
+ ## 1.12.0: `DateField` / `DatePicker` field chrome moved to `Field.Root`
22
+
23
+ **Applies to:** apps passing `label`, `description`, `errorMessage`, or `hideTimeZone` to `DateField` or `DatePicker`.
24
+
25
+ The date controls now follow the same composition model as `Field`, `Select`, `Combobox`, and `Autocomplete`. Field chrome (`label`, `description`, `errorMessage`) has moved out of the control and into `Field.Root`. `hideTimeZone` is removed — it was accepted by the prop types but had no effect.
26
+
27
+ TypeScript will error on the removed props; delete them and compose with `Field.Root` instead.
28
+
29
+ Before:
30
+
31
+ ```tsx
32
+ <DatePicker
33
+ label="Delivery date"
34
+ description="When should we ship your order?"
35
+ minValue={today(getLocalTimeZone())}
36
+ errorMessage={error}
37
+ isInvalid={!!error}
38
+ />
39
+ ```
40
+
41
+ After:
42
+
43
+ ```tsx
44
+ <Field.Root invalid={!!error}>
45
+ <Field.Label>Delivery date</Field.Label>
46
+ <DatePicker aria-label="Delivery date" minValue={today(getLocalTimeZone())} />
47
+ <Field.Description>When should we ship your order?</Field.Description>
48
+ <Field.Error match={!!error}>{error}</Field.Error>
49
+ </Field.Root>
50
+ ```
51
+
52
+ `isInvalid` remains a top-level prop for externally-controlled invalid styling. Semantic date props (`isRequired`, `isDisabled`, `isReadOnly`, `minValue`, `maxValue`, `isDateUnavailable`) remain top-level and unchanged. Standalone usage still works with an accessible name:
53
+
54
+ ```tsx
55
+ <DateField aria-label="Invoice date" />
56
+ ```
57
+
58
+ ## 1.11.0: React 19.2.7 and React Router v8 are now required
59
+
60
+ **Applies to:** every app upgrading to 1.11.0.
61
+
62
+ The minimum supported `react` and `react-dom` is raised to `19.2.7`, and AppShell moves to React Router v8. React 18 is no longer supported.
63
+
64
+ Upgrade `react` and `react-dom` to `>=19.2.7` in the same change. If your app imports from `react-router` directly, review the React Router v8 release notes for its own breaking changes — AppShell re-exports a subset (`useNavigate`, `useParams`, `useLocation`, and friends), and those are unaffected.
65
+
66
+ Note that 1.10.1 deliberately stayed on React Router v7 to pick up its security fixes while avoiding v8. Going 1.10.1 → 1.11.0 therefore crosses a router major.
67
+
68
+ ## 1.11.0: non-modal `Sheet` no longer renders a backdrop
69
+
70
+ **Applies to:** apps using `<Sheet.Root modal={false}>`.
71
+
72
+ A non-modal sheet previously still rendered a backdrop, dimming and blocking the page behind it. It now omits the backdrop, which is what `modal={false}` implies. Nothing errors — the page behind simply stays undimmed and interactive.
73
+
74
+ If you relied on the dimming, drop `modal={false}` and use a modal sheet.
75
+
76
+ ## 1.8.0: `stream` removed from `useAIChat()`
77
+
78
+ **Applies to:** apps passing `stream` to `useAIChat()`, or constructing an `AIGatewayChatRequest` by hand.
79
+
80
+ AppShell now selects streaming or JSON transport automatically from the model, so the option is gone. TypeScript errors on the removed property; delete it. There is no replacement.
81
+
82
+ ## 1.5.0 → 1.7.0: remove the theme bridge workaround
83
+
84
+ **Applies to:** apps that pasted the `@theme inline` block, `@custom-variant dark`, and AppShell's palette into their entry CSS — the workaround for `styles` shipping without the Tailwind bridge.
85
+
86
+ **`styles` regained the bridge in 1.7.0.** On 1.5.0–1.6.1 the workaround is load-bearing, so upgrade to 1.7.0 or later _before_ deleting any of it. Remove it earlier and every AppShell-token utility — `bg-card`, `bg-background`, `text-muted-foreground`, `border-border` — stops resolving, while `dark:` variants fall back to Tailwind's `prefers-color-scheme` default and stop tracking the `.dark` class.
87
+
88
+ From 1.7.0 the workaround is not merely redundant. It actively breaks dark mode, and the build succeeds with no warning:
89
+
90
+ - Your pasted `:root` and `.dark` blocks are unlayered, so they beat AppShell's layered default palette. Colours freeze at the values you copied, and any surface AppShell has added since has no dark value at all — so it renders light colours in dark mode: white text on white cards, unreadable disabled inputs.
91
+ - `@custom-variant dark (&:is(.dark *))` overrides AppShell's `&:where(.dark, .dark *)`. The `:is(.dark *)` form matches only _descendants_ of `.dark`, so `dark:` utilities stop applying to the `.dark` element itself.
92
+
93
+ ### Removing it
94
+
95
+ Delete from your entry CSS:
96
+
97
+ - the `@theme inline { … }` block,
98
+ - the `@custom-variant dark (…)` rule,
99
+ - every `:root` and `.dark` block copied from AppShell's palette — **all** of it, including the `*-foreground` pairs, `--status-*`, `--alert-*`, `--sidebar-*` and `--semantic-shadow-*`. The foregrounds are what leave text white on white, so a partial deletion reproduces the bug.
100
+ - any `@import "@tailor-platform/app-shell/theme.css"` (a no-op shim since 1.6.0, kept only so older apps keep building).
101
+
102
+ To find it, search every CSS file the app loads — not just the entry point, since `app/` and `styles/` are as common as `src/`:
103
+
104
+ ```bash
105
+ grep -rnE "@theme inline|@custom-variant|app-shell/theme\.css|--(card|popover|muted|sidebar|destructive|accent)(-foreground)?:" --include="*.css" --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=.next .
106
+ ```
107
+
108
+ Excluding `node_modules` matters: AppShell's own palette files declare these tokens too, and they must not be touched. In your own CSS, hits are either the workaround, which goes, or deliberate overrides, which should take the `:root` / `:root.dark` form described in [Overriding tokens](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#overriding-tokens).
109
+
110
+ What remains is short — [`examples/vite-app/src/index.css`](https://github.com/tailor-platform/app-shell/blob/main/examples/vite-app/src/index.css) is a working reference for the shape (it also imports a branded palette, which is optional):
111
+
112
+ ```css
113
+ @import "tailwindcss";
114
+ @import "@tailor-platform/app-shell/styles";
115
+
116
+ html,
117
+ body {
118
+ margin: 0;
119
+ padding: 0;
120
+ }
121
+ ```
122
+
123
+ ### Verifying
124
+
125
+ Toggle dark mode and confirm a real surface changes: inspect a `Card` and watch its computed `background-color` go from `rgb(255, 255, 255)` to `rgb(23, 23, 23)` on the default palette.
126
+
127
+ Reading the token directly also works — `getComputedStyle(document.documentElement).getPropertyValue("--card")` returns the winning declaration, so a stale copy shows up as its own value. Just compare against the authored notation: AppShell writes `rgba(23, 23, 23, 1)`, not `#171717`, and the computed value preserves that form.
128
+
129
+ ## 1.5.0: `loader` removed from file-based page definitions
130
+
131
+ **Applies to:** file-based routing apps that set `loader` in `Page.appShellPageProps`.
132
+
133
+ `loader` was an incomplete API exposed by accident, and `guards` is now the single source of page-level route behaviour. Move any access checks into a guard, and any data loading into the page component or your data layer.
134
+
135
+ ## 1.3.0: column inference and badge defaults changed
136
+
137
+ **Applies to:** apps using `inferColumns()`, or `DataTable`'s status badges.
138
+
139
+ Two changes that alter rendering without any error:
140
+
141
+ - `inferColumns()` no longer sets a default `render`. A column with neither an explicit `type` nor `render` now displays `—` for null and empty values, matching typed-column behaviour.
142
+ - Badge variant resolution moved into a shared helper whose default is `outline-neutral`. `DataTable` previously defaulted to `neutral`, so unstyled status badges change appearance.
143
+
144
+ Set `type` or `render` explicitly on any column whose previous rendering you want back, and pass an explicit variant where the old badge styling mattered. `BadgeVariantType` is deprecated in favour of `BadgeVariant`.
145
+
146
+ ## 1.0.2: `Toaster` no longer accepts `richColors`
147
+
148
+ **Applies to:** apps passing `richColors` to `<Toaster>`.
149
+
150
+ The prop is removed, and toasts no longer colour-code the success, error, warning, and info variants. TypeScript errors on the prop; delete it.
151
+
152
+ ## Before 1.0
153
+
154
+ Pre-1.0 releases changed the public API often, mostly around authentication and routing. If you are upgrading from a 0.x version, work through these in order — several supersede each other, so applying them out of sequence will not land you in the right place.
155
+
156
+ Each is summarised here; [`packages/core/CHANGELOG.md`](https://github.com/tailor-platform/app-shell/blob/main/packages/core/CHANGELOG.md) carries the full before/after code for every one.
157
+
158
+ | Version | Change |
159
+ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
+ | 0.33.0 | `AsyncFetcherFn` receives `string \| null` instead of `string`. It is called with `null` when the user has typed nothing — return initial items, or an empty array to show nothing until they type. |
161
+ | 0.28.0 | `EnhancedAuthClient.getAuthHeadersForQuery()` removed. Pass `fetch: authClient.fetch` to your GraphQL client instead; it handles DPoP proofs and token refresh transparently. |
162
+ | 0.27.0 | Module-level `guards` and `loaders` no longer cascade to child resources — declare them on each resource or page. A module without a `component` no longer auto-redirects to its first visible resource and must declare `guards`, or it throws at runtime. |
163
+ | 0.26.0 | Authentication moved to `@tailor-platform/auth-public-client` with DPoP. `AuthProvider` requires a `client` from `createAuthClient`, `apiEndpoint` is gone, `useAuth` returns its fields directly rather than under `authState`, and built-in user fetching (`meQuery`, `AuthState.user`, `DefaultUser`, `AuthRegister`) is removed — fetch the user with your own GraphQL client. |
164
+ | 0.24.0 | `accessControl` replaced by the `guards` array on `defineModule`/`defineResource`. `RedirectConfig` and `redirectToResource` removed — use `guards` with `redirectTo()`. |
165
+ | 0.19.0 | `BuiltinIdPAuthProvider` → `AuthProvider`, `useBuiltinIdpAuth` → `useAuth`. The `buildAuthorizationUrl`, `exchangeCodeForToken`, `prepareLogin`, and `handleOAuthCallback` utilities are no longer exported. |
166
+ | 0.13.0 | `defaultResourceRedirectPath` removed from `defineModule` in favour of a `redirectToResource` helper — which 0.24.0 then removed in turn. Coming from 0.13.0 or earlier, go straight to the 0.24.0 form: `guards` with `redirectTo()`. |
167
+ | 0.4.0 | `meta.title` no longer renders the page title automatically. The title is passed to the resource component via props (`ResourceComponentProps`); render it yourself. |
@@ -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.
@@ -4,7 +4,7 @@ name: Single Page Form
4
4
  category: pattern
5
5
  subcategory: form
6
6
  description: Routed full-page form for moderate field count (6-15) without natural sectioning
7
- requiredImports: [Layout, Form, Field, Fieldset, Input, Select, Button]
7
+ requiredImports: [Layout, Form, Field, Fieldset, Input, Textarea, Select, Button]
8
8
  tags: [form, page, create, edit, routed]
9
9
  do:
10
10
  - A routed Create or Edit page that the design has explicitly called out (e.g. /orders/create)
@@ -26,7 +26,7 @@ dont:
26
26
 
27
27
  ```tsx
28
28
  /* pattern: form/single-page */
29
- import { Button, Layout, Input, Select, Field } from "@tailor-platform/app-shell";
29
+ import { Button, Layout, Input, Textarea, Select, Field } from "@tailor-platform/app-shell";
30
30
 
31
31
  type Props = {
32
32
  onSave: (data: Record<string, string>) => void;
@@ -82,7 +82,7 @@ export default function SinglePageForm({ onSave, onCancel }: Props) {
82
82
  </Field.Root>
83
83
  <Field.Root name="description">
84
84
  <Field.Label>Description</Field.Label>
85
- <Field.Control render={<Input />} />
85
+ <Textarea rows={4} />
86
86
  </Field.Root>
87
87
  </form>
88
88
  </Layout.Column>
@@ -84,7 +84,7 @@ export default function WizardForm({ onComplete }: Props) {
84
84
  )}
85
85
  {currentStep === 1 && (
86
86
  <div className="space-y-4">
87
- <p className="text-fg-muted">Map CSV columns to product fields</p>
87
+ <p className="text-sm text-muted-foreground">Map CSV columns to product fields</p>
88
88
  <Field.Root name="nameColumn">
89
89
  <Field.Label>Name column</Field.Label>
90
90
  <Field.Control render={<Input placeholder="e.g. Column A" />} />
@@ -97,12 +97,14 @@ export default function WizardForm({ onComplete }: Props) {
97
97
  )}
98
98
  {currentStep === 2 && (
99
99
  <div className="space-y-4">
100
- <p className="text-fg-muted">Review your import — 42 products will be created.</p>
100
+ <p className="text-sm text-muted-foreground">
101
+ Review your import — 42 products will be created.
102
+ </p>
101
103
  </div>
102
104
  )}
103
105
  {currentStep === 3 && (
104
106
  <div className="space-y-4">
105
- <p className="text-fg-default">Import complete! 42 products created.</p>
107
+ <p className="text-sm text-foreground">Import complete! 42 products created.</p>
106
108
  </div>
107
109
  )}
108
110
  </Card.Content>
@@ -123,7 +123,7 @@ export default function MultiSelect({ orders, onArchive, onExport }: Props) {
123
123
  <div
124
124
  role="toolbar"
125
125
  aria-label="Bulk actions"
126
- className="fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-md border bg-surface-2 px-3 py-2 shadow-lg"
126
+ className="fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2 rounded-md border bg-popover px-3 py-2 shadow-lg"
127
127
  >
128
128
  <span className="text-sm font-medium">{selectedCount} selected</span>
129
129
  <Button size="sm" onClick={() => onArchive([...selectedIds])}>