@tailor-platform/app-shell 1.16.0 → 1.17.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/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +692 -106
- package/dist/app-shell.js +6294 -4939
- package/dist/{appshell-B0wh5Lq0.js → appshell-CahhZy7v.js} +316 -297
- package/dist/semantic-contrast.test.ts +154 -0
- package/dist/style-entry.d.ts +117 -82
- package/dist/testing.d.ts +122 -85
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +101 -1
- package/dist/theme.bridge.css +91 -0
- package/dist/theme.css +6 -1
- package/dist/themes/_template.css +7 -1
- package/dist/themes/bloom.css +0 -6
- package/dist/themes/cream.css +0 -6
- package/dist/themes/default.css +389 -69
- package/dist/vite-plugin.d.ts +117 -82
- package/package.json +1 -1
- package/skills/app-shell-patterns/SKILL.md +7 -5
- package/skills/app-shell-patterns/references/api/define-module.md +2 -2
- package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
- package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
- package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
- package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
- package/skills/app-shell-patterns/references/components/card.md +79 -7
- package/skills/app-shell-patterns/references/components/data-table.md +267 -52
- package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
- package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -0
- package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
- package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +255 -58
- package/skills/app-shell-patterns/references/migrations.md +21 -0
- package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
- package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +2 -2
|
@@ -11,7 +11,7 @@ Styling is done with **Tailwind CSS v4** against `@tailor-platform/app-shell`'s
|
|
|
11
11
|
|
|
12
12
|
**The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies.
|
|
13
13
|
|
|
14
|
-
**Every token in this document is verified against the shipped CSS.** If a token is not listed here, assume it does not exist. Inventing a plausible-sounding token (`bg-surface-1`, `text-fg-muted`, `--space-4`) is the worst failure mode available: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled — no error, no warning, nothing in the console. When unsure, read `node_modules/@tailor-platform/app-shell/dist/themes/default.css`; it is the ground truth.
|
|
14
|
+
**Every token in this document is verified against the shipped CSS.** If a token is not listed here, assume it does not exist. The colour scales (`--primitive-*`) are internal: they are not bridged to Tailwind and are not listed. Inventing a plausible-sounding token (`bg-surface-1`, `text-fg-muted`, `--space-4`) is the worst failure mode available: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled — no error, no warning, nothing in the console. When unsure, read `node_modules/@tailor-platform/app-shell/dist/themes/default.css`; it is the ground truth.
|
|
15
15
|
|
|
16
16
|
## Setup
|
|
17
17
|
|
|
@@ -86,14 +86,14 @@ Only import one palette at a time.
|
|
|
86
86
|
|
|
87
87
|
Theme tokens live in `packages/core/src/assets/themes/`. Copy `_template.css` to start a new palette — it lists exactly which sections to fill in for light and dark mode.
|
|
88
88
|
|
|
89
|
-
| Section | Required? | What to set
|
|
90
|
-
| --------------------- | --------------------- |
|
|
91
|
-
| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes
|
|
92
|
-
| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint`
|
|
93
|
-
| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders
|
|
94
|
-
| **4. Palette** | Optional | Radius, chart colors, shadows
|
|
95
|
-
| **5. Semantic** |
|
|
96
|
-
| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed
|
|
89
|
+
| Section | Required? | What to set |
|
|
90
|
+
| --------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
91
|
+
| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes |
|
|
92
|
+
| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint` |
|
|
93
|
+
| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders |
|
|
94
|
+
| **4. Palette** | Optional | Radius, chart colors, shadows |
|
|
95
|
+
| **5. Semantic** | Inherit | Colour scales, status and alert tokens inherit from `default.css`. Override a colour role (`--{intent}-{role}`) only to recolour the status components |
|
|
96
|
+
| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed |
|
|
97
97
|
|
|
98
98
|
A palette is selected by CSS import, not by an AppShell prop. Import exactly one theme file after `@tailor-platform/app-shell/styles`; if you import none, the default palette from `styles` is used.
|
|
99
99
|
|
|
@@ -175,17 +175,44 @@ There are no `-hover` or `-active` brand tokens. Express interaction states with
|
|
|
175
175
|
|
|
176
176
|
#### Status
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
**Deprecated.** These five tokens and their utilities remain for existing code and will be removed in the next major. New code uses the [semantic color roles](#semantic-color-roles) below. In the default palette they are aliases of those roles, so status colors and Alert colors agree:
|
|
179
179
|
|
|
180
|
-
| Token | Use
|
|
181
|
-
| -------------------- |
|
|
182
|
-
| `--status-default` | none / not applicable
|
|
183
|
-
| `--status-neutral` | informational
|
|
184
|
-
| `--status-completed` | success, completed
|
|
185
|
-
| `--status-attention` | warning, needs attention
|
|
186
|
-
| `--status-danger` | error, blocked
|
|
180
|
+
| Token | Use | Tailwind | Use instead |
|
|
181
|
+
| -------------------- | ---------------------------------------------------- | --------------------- | ---------------------- |
|
|
182
|
+
| `--status-default` | none / not applicable (follows `--muted-foreground`) | `bg-status-default` | `bg-neutral-indicator` |
|
|
183
|
+
| `--status-neutral` | informational | `bg-status-neutral` | `bg-info-solid` |
|
|
184
|
+
| `--status-completed` | success, completed | `bg-status-completed` | `bg-success-solid` |
|
|
185
|
+
| `--status-attention` | warning, needs attention | `bg-status-attention` | `bg-warning-solid` |
|
|
186
|
+
| `--status-danger` | error, blocked | `bg-status-danger` | `bg-danger-solid` |
|
|
187
187
|
|
|
188
|
-
Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `neutral`) over applying
|
|
188
|
+
Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `neutral`) over applying a fill directly — the variants already pair fill and foreground correctly.
|
|
189
|
+
|
|
190
|
+
These are fill and indicator colors. `--status-default` follows `--muted-foreground`, so it is theme-dependent and translucent in `cream` and `bloom`; the four hue tokens are opaque in every palette. Do not use them as text color: `--status-completed`, `--status-danger` and `--status-neutral` are below 4.5:1 on a dark card, and `--status-attention` is below 3:1 on a light one. For text, use `text-{intent}-text`.
|
|
191
|
+
|
|
192
|
+
#### Semantic color roles
|
|
193
|
+
|
|
194
|
+
Each intent has eight roles. The intents are `info`, `success`, `warning`, `danger` and `neutral`. The hue intents read internal color scales; `neutral` reads the system tokens. Every role is a token named `--{intent}-{role}` and a Tailwind color named `{intent}-{role}`:
|
|
195
|
+
|
|
196
|
+
| Role | Tailwind | Use for |
|
|
197
|
+
| --------------- | ----------------------- | ----------------------------------- |
|
|
198
|
+
| `surface` | `bg-info-surface` | Soft background |
|
|
199
|
+
| `surface-hover` | `bg-info-surface-hover` | Hover state of a soft background |
|
|
200
|
+
| `border` | `border-info-border` | Border on a soft background |
|
|
201
|
+
| `solid` | `bg-info-solid` | Filled background |
|
|
202
|
+
| `solid-hover` | `bg-info-solid-hover` | Hover state of a filled background |
|
|
203
|
+
| `text` | `text-info-text` | Text and icons on a soft background |
|
|
204
|
+
| `contrast` | `text-info-contrast` | Text on a filled background |
|
|
205
|
+
| `indicator` | `bg-info-indicator` | Dots and small marks |
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
<span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
In the default palette, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, in light and dark mode, on `--card` and `--background`. A unit test checks this from the shipped CSS. `text` on `surface-hover` is held at 4.2:1 by the same test; it is below 4.5:1 for `danger` and `success` in light mode. The test does not cover the `cream` and `bloom` palettes or the `neutral` intent.
|
|
212
|
+
|
|
213
|
+
Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent.
|
|
214
|
+
|
|
215
|
+
`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. They no longer follow `--destructive`, which still drives `Button` and `text-destructive`.
|
|
189
216
|
|
|
190
217
|
#### Sidebar & charts
|
|
191
218
|
|
|
@@ -195,24 +222,17 @@ Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `
|
|
|
195
222
|
|
|
196
223
|
#### Alerts
|
|
197
224
|
|
|
198
|
-
`--alert-{neutral,success,warning,error,info}-{background,foreground,foreground-muted,border}`
|
|
199
|
-
|
|
200
|
-
| Slot | Tailwind | Use for |
|
|
201
|
-
| ------------------ | ---------------------------------- | -------------------------- |
|
|
202
|
-
| `background` | `bg-alert-info-background` | Callout fill |
|
|
203
|
-
| `foreground` | `text-alert-info-foreground` | Primary text and icons |
|
|
204
|
-
| `foreground-muted` | `text-alert-info-foreground-muted` | Secondary/description text |
|
|
205
|
-
| `border` | `border-alert-info-border` | Callout border |
|
|
225
|
+
**Deprecated.** `--alert-{neutral,success,warning,error,info}-{background,foreground,foreground-muted,border}` and the matching utilities (`bg-alert-info-background`, `text-alert-info-foreground`, ...) remain for existing code and will be removed in the next major. In the default palette they are aliases of the semantic color roles: `background` is `surface`, `border` is `border`, `foreground` is `text`, and `foreground-muted` follows `foreground`; `error` reads the `danger` intent. The `Alert` component reads the roles, so overriding `--alert-*` changes these utilities but not `Alert`.
|
|
206
226
|
|
|
207
|
-
|
|
227
|
+
For new custom surfaces, use the roles instead of the alert slots:
|
|
208
228
|
|
|
209
229
|
```tsx
|
|
210
|
-
<div className="rounded-lg border bg-
|
|
230
|
+
<div className="rounded-lg border bg-warning-surface text-warning-text border-warning-border">
|
|
211
231
|
…
|
|
212
232
|
</div>
|
|
213
233
|
```
|
|
214
234
|
|
|
215
|
-
|
|
235
|
+
Across all colour tokens:
|
|
216
236
|
|
|
217
237
|
```tsx
|
|
218
238
|
// Good — semantic token pairs, and a variant where one exists
|
|
@@ -243,29 +263,203 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth
|
|
|
243
263
|
|
|
244
264
|
### Typography
|
|
245
265
|
|
|
246
|
-
|
|
266
|
+
AppShell defines **composite typography role tokens**. A role sets font size, line height, font weight, and letter spacing together, so one utility such as `text-heading-md` replaces a set like `text-lg font-semibold`. The font family is a separate token, `--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`.
|
|
267
|
+
|
|
268
|
+
There are 8 core roles and 2 relaxed body variants:
|
|
269
|
+
|
|
270
|
+
| Role | Utility | Size / line height | Weight | Use |
|
|
271
|
+
| ----------------- | ---------------------- | ------------------ | ------ | ----------------------------------------------- |
|
|
272
|
+
| `heading-lg` | `text-heading-lg` | 24px / 32px | 700 | Page title (`Layout.Header`) |
|
|
273
|
+
| `heading-md` | `text-heading-md` | 18px / 28px | 600 | Section heading, card title |
|
|
274
|
+
| `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
|
|
275
|
+
| `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
|
|
276
|
+
| `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
|
|
277
|
+
| `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text, figures in a table |
|
|
278
|
+
| `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
|
|
279
|
+
| `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
|
|
280
|
+
| `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
|
|
281
|
+
| `body-sm-relaxed` | `text-body-sm-relaxed` | 12px / 20px | 400 | Long text at the small size |
|
|
282
|
+
|
|
283
|
+
`heading-lg` also sets letter spacing to `-0.025em`. All other roles set `0em`.
|
|
284
|
+
|
|
285
|
+
Pair a role with a color token from the Color section above. These pairings are the recommended combinations:
|
|
286
|
+
|
|
287
|
+
| Use | Utilities |
|
|
288
|
+
| --------------------------------- | ---------------------------------------------- |
|
|
289
|
+
| Page title (`Layout.Header`) | `text-heading-lg` |
|
|
290
|
+
| Section heading | `text-heading-md` |
|
|
291
|
+
| Card title (`div` or `span`) | `text-heading-md leading-none` |
|
|
292
|
+
| Body copy | `text-body-md` |
|
|
293
|
+
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
|
|
294
|
+
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
|
|
295
|
+
| Numeric value in a table or field | `text-label-md tabular-nums` |
|
|
296
|
+
| ID, code | `font-mono text-code-sm text-muted-foreground` |
|
|
297
|
+
|
|
298
|
+
<example-preview name="typography-roles"></example-preview>
|
|
247
299
|
|
|
248
|
-
|
|
300
|
+
```tsx
|
|
301
|
+
function TypographyRoles() {
|
|
302
|
+
const rows = [
|
|
303
|
+
{
|
|
304
|
+
role: "heading-lg",
|
|
305
|
+
className: "text-heading-lg",
|
|
306
|
+
en: "Purchase orders",
|
|
307
|
+
ja: "発注管理",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
role: "heading-md",
|
|
311
|
+
className: "text-heading-md",
|
|
312
|
+
en: "Purchase orders by supplier",
|
|
313
|
+
ja: "仕入先別の発注一覧",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
role: "heading-sm",
|
|
317
|
+
className: "text-heading-sm",
|
|
318
|
+
en: "Delivery address",
|
|
319
|
+
ja: "納品先",
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
role: "body-md",
|
|
323
|
+
className: "text-body-md",
|
|
324
|
+
en: "The order was sent to the supplier on 12 March.",
|
|
325
|
+
ja: "発注書は3月12日に仕入先へ送付されました。",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
role: "body-sm",
|
|
329
|
+
className: "text-body-sm text-muted-foreground",
|
|
330
|
+
en: "Updated 2 hours ago",
|
|
331
|
+
ja: "2時間前に更新",
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
role: "label-md",
|
|
335
|
+
className: "text-label-md",
|
|
336
|
+
en: "Order quantity",
|
|
337
|
+
ja: "発注数量",
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
role: "label-sm",
|
|
341
|
+
className: "text-label-sm",
|
|
342
|
+
en: "Draft",
|
|
343
|
+
ja: "下書き",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
role: "code-sm",
|
|
347
|
+
className: "font-mono text-code-sm",
|
|
348
|
+
en: "PO-2026-000184",
|
|
349
|
+
ja: "PO-2026-000184",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
role: "body-md-relaxed",
|
|
353
|
+
className: "text-body-md-relaxed",
|
|
354
|
+
compareWith: { role: "body-md", className: "text-body-md" },
|
|
355
|
+
en: "Please confirm the delivery date with the supplier before you approve this order. If the supplier cannot deliver by the requested date, change the date or choose another supplier. Approved orders are sent to the supplier by email.",
|
|
356
|
+
ja: "この発注を承認する前に、納期を仕入先に確認してください。希望日までに納品できない場合は、納期を変更するか、別の仕入先を選んでください。承認した発注は、メールで仕入先に送られます。",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
role: "body-sm-relaxed",
|
|
360
|
+
className: "text-body-sm-relaxed text-muted-foreground",
|
|
361
|
+
compareWith: { role: "body-sm", className: "text-body-sm text-muted-foreground" },
|
|
362
|
+
en: "Changes to a confirmed order are recorded in the order history. Each record shows who made the change, when it was made, and the values before and after the change.",
|
|
363
|
+
ja: "確定済みの発注への変更は、発注履歴に記録されます。記録には、変更した人、変更した日時、変更前と変更後の値が含まれます。",
|
|
364
|
+
},
|
|
365
|
+
];
|
|
249
366
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
367
|
+
return (
|
|
368
|
+
<div className="flex flex-col gap-4">
|
|
369
|
+
{rows.map(({ role, className, compareWith, en, ja }) => (
|
|
370
|
+
<div key={role} className="grid grid-cols-[9rem_1fr] items-baseline gap-4">
|
|
371
|
+
<span className="font-mono text-code-sm text-muted-foreground">{role}</span>
|
|
372
|
+
{compareWith ? (
|
|
373
|
+
// A relaxed role changes only line height, so show the same text in the base role first.
|
|
374
|
+
<div className="flex flex-col gap-4">
|
|
375
|
+
{[compareWith, { role, className }].map((variant) => (
|
|
376
|
+
<div key={variant.role} className="flex flex-col gap-1">
|
|
377
|
+
<span className="font-mono text-code-sm text-muted-foreground">
|
|
378
|
+
{variant.role}
|
|
379
|
+
</span>
|
|
380
|
+
<p className={variant.className}>{en}</p>
|
|
381
|
+
<p className={variant.className}>{ja}</p>
|
|
382
|
+
</div>
|
|
383
|
+
))}
|
|
384
|
+
</div>
|
|
385
|
+
) : (
|
|
386
|
+
<div className="flex flex-col gap-1">
|
|
387
|
+
<p className={className}>{en}</p>
|
|
388
|
+
<p className={className}>{ja}</p>
|
|
389
|
+
</div>
|
|
390
|
+
)}
|
|
391
|
+
</div>
|
|
392
|
+
))}
|
|
393
|
+
</div>
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
```
|
|
260
397
|
|
|
261
398
|
```tsx
|
|
262
|
-
<h2 className="text-
|
|
263
|
-
<p className="text-
|
|
264
|
-
<span className="text-
|
|
399
|
+
<h2 className="text-heading-md">Section</h2>
|
|
400
|
+
<p className="text-body-md text-muted-foreground">Description copy</p>
|
|
401
|
+
<span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
|
|
265
402
|
```
|
|
266
403
|
|
|
404
|
+
On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now.
|
|
405
|
+
|
|
406
|
+
**Rules**
|
|
407
|
+
|
|
408
|
+
- **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
|
|
409
|
+
- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size.
|
|
410
|
+
- **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent.
|
|
411
|
+
- **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
|
|
412
|
+
- Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`.
|
|
413
|
+
- Tabular figures: `tabular-nums`.
|
|
414
|
+
- Emphasis on inline text: `font-medium` or `font-semibold`.
|
|
415
|
+
- Uppercase: `uppercase`.
|
|
416
|
+
- **Do not combine a role with other `leading-*`, `tracking-*`, or `text-[Npx]` values.** If none of the roles fits, use the nearest role.
|
|
417
|
+
- **12px is the minimum size.** Do not use smaller text.
|
|
418
|
+
- **`code-sm` needs `font-mono`.** The role sets size, line height, weight, and letter spacing, but a role cannot set the font family.
|
|
419
|
+
- **Use the relaxed variants for long text only.** For labels, table cells, and other short text, use the regular roles.
|
|
420
|
+
|
|
267
421
|
Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
|
|
268
422
|
|
|
423
|
+
**Change a role.** Each role is four CSS variables named `--app-shell-type-<role>-<size|line-height|weight|letter-spacing>`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode.
|
|
424
|
+
|
|
425
|
+
```css
|
|
426
|
+
:root {
|
|
427
|
+
--app-shell-type-heading-lg-size: 1.75rem;
|
|
428
|
+
--app-shell-type-heading-lg-line-height: calc(36 / 28);
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The `cream` and `bloom` palettes set `line-height` and `letter-spacing` on `h1` to `h6`. These rules are outside any `@layer`, so they win over a role utility, and over any `leading-*` or `tracking-*` class, on those elements. Under these palettes, an `h1` to `h6` element takes its line height (1.2) and letter spacing (`-0.03em`) from the palette. This includes `leading-none`: `<h3 className="text-heading-md leading-none">` has a line height of 1.2, while a `div` with the same classes has 1.0. Font size and weight still come from the role. The `default` palette has no such rule.
|
|
433
|
+
|
|
434
|
+
**`tailwind-merge`.** AppShell's `cn()` keeps a role together with a text color class, for example when you pass `className="text-label-md text-muted-foreground"` to an AppShell component. It cannot replace a size that a component sets itself, because component classes carry the internal `astw:` prefix (see "Why a plain utility can't override an AppShell default" below). If your app has its own `cn()` built on `tailwind-merge`, register the role names. Without this, `twMerge("text-label-md text-muted-foreground")` removes the role and keeps only the color.
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
438
|
+
|
|
439
|
+
const twMerge = extendTailwindMerge({
|
|
440
|
+
extend: {
|
|
441
|
+
classGroups: {
|
|
442
|
+
"font-size": [
|
|
443
|
+
{
|
|
444
|
+
text: [
|
|
445
|
+
"heading-lg",
|
|
446
|
+
"heading-md",
|
|
447
|
+
"heading-sm",
|
|
448
|
+
"body-md",
|
|
449
|
+
"body-sm",
|
|
450
|
+
"body-md-relaxed",
|
|
451
|
+
"body-sm-relaxed",
|
|
452
|
+
"label-md",
|
|
453
|
+
"label-sm",
|
|
454
|
+
"code-sm",
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
```
|
|
462
|
+
|
|
269
463
|
### Radius
|
|
270
464
|
|
|
271
465
|
`--radius` (`0.625rem`) is the base; the bridge derives four steps from it. Pick by component role — a card is always `md`, regardless of its size on screen.
|
|
@@ -332,12 +526,12 @@ Never invent a z value — `z-index: 9999` is always wrong. Popups and overlays
|
|
|
332
526
|
|
|
333
527
|
**AppShell exports no `Icon` component and defines no `--icon-*` tokens.** Icons come from [`lucide-react`](https://lucide.dev), which AppShell already depends on. Size them with Tailwind, pairing icon size to the adjacent text:
|
|
334
528
|
|
|
335
|
-
| Text size
|
|
336
|
-
|
|
|
337
|
-
| `text-xs`
|
|
338
|
-
| `text-sm`
|
|
339
|
-
| `text-lg`
|
|
340
|
-
| `text-2xl`
|
|
529
|
+
| Text size | Icon class |
|
|
530
|
+
| ------------------------------------------------------------------- | ---------- |
|
|
531
|
+
| 12px: `text-xs`, `text-body-sm`, `text-label-sm`, `text-code-sm` | `size-3` |
|
|
532
|
+
| 14px: `text-sm`, `text-body-md`, `text-label-md`, `text-heading-sm` | `size-4` |
|
|
533
|
+
| 18px: `text-lg`, `text-heading-md` | `size-5` |
|
|
534
|
+
| 24px (title): `text-2xl`, `text-heading-lg` | `size-6` |
|
|
341
535
|
|
|
342
536
|
```tsx
|
|
343
537
|
import { Check } from "lucide-react";
|
|
@@ -462,12 +656,12 @@ Write **plain Tailwind utilities** in your application code. Three rules cover e
|
|
|
462
656
|
</div>
|
|
463
657
|
```
|
|
464
658
|
|
|
465
|
-
When no prop exists and you genuinely have to override a value a component sets, add Tailwind's [importance modifier](https://tailwindcss.com/docs/styling-with-utility-classes#important-modifier) — a trailing `!`. It is the last resort, not the first: a plain utility silently loses (see below)
|
|
659
|
+
When no prop exists and you genuinely have to override a value a component sets, add Tailwind's [importance modifier](https://tailwindcss.com/docs/styling-with-utility-classes#important-modifier) — a trailing `!`. It is the last resort, not the first: a plain utility silently loses (see below). Check for a prop before reaching for it. Sitting a table flush inside a card, for example, used to need `className="px-0!"`; it is now a prop:
|
|
466
660
|
|
|
467
661
|
```tsx
|
|
468
662
|
<Card.Root>
|
|
469
663
|
<Card.Header title="Line items" />
|
|
470
|
-
<Card.Content
|
|
664
|
+
<Card.Content padding="none">
|
|
471
665
|
<Table.Root>{/* … */}</Table.Root>
|
|
472
666
|
</Card.Content>
|
|
473
667
|
</Card.Root>
|
|
@@ -494,6 +688,8 @@ This is the reason rule 3 sends you to props rather than to a more specific clas
|
|
|
494
688
|
<Card.Content className="px-0" />
|
|
495
689
|
```
|
|
496
690
|
|
|
691
|
+
(For this particular case, use `padding="none"`.)
|
|
692
|
+
|
|
497
693
|
State variants make it worse rather than better: `Button`'s `ghost` variant sets a hover color, and a `:hover` rule outranks an unprefixed utility on **specificity**, not merely order — so a plain `text-destructive` on a ghost button is red only until the pointer reaches it. `variant="destructive"` is the supported way to express that; where no such prop exists, `!` wins in every state because importance beats specificity.
|
|
498
694
|
|
|
499
695
|
### Styling by state (data attributes)
|
|
@@ -503,7 +699,7 @@ AppShell's UI components support data-attribute-based styling, following the [Ba
|
|
|
503
699
|
```css
|
|
504
700
|
/* Style a component based on its state */
|
|
505
701
|
.SwitchThumb[data-checked] {
|
|
506
|
-
background-color: var(--
|
|
702
|
+
background-color: var(--success-solid);
|
|
507
703
|
}
|
|
508
704
|
|
|
509
705
|
.MenuItem[data-highlighted] {
|
|
@@ -574,9 +770,10 @@ These are visual-composition rules every screen must follow, regardless of patte
|
|
|
574
770
|
| Intent | Pick |
|
|
575
771
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
576
772
|
| Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` |
|
|
577
|
-
| Non-blocking caution | `Badge variant="warning"`, or `bg-
|
|
578
|
-
| Confirmation / completed state | `Badge variant="success"`, or `bg-
|
|
579
|
-
|
|
|
773
|
+
| Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` |
|
|
774
|
+
| Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` |
|
|
775
|
+
| Informational callout | `Badge variant="info"`, or `Alert variant="info"` |
|
|
776
|
+
| Neutral callout | `Badge variant="neutral"`, or `Alert variant="neutral"` |
|
|
580
777
|
| Persistent panel (sidebar, header) | `shadow-sm` |
|
|
581
778
|
| Hovered / sticky surface | `shadow-md` |
|
|
582
779
|
| Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
|
|
@@ -584,5 +781,5 @@ These are visual-composition rules every screen must follow, regardless of patte
|
|
|
584
781
|
| Hover / focus transition | `duration-150 ease-out` |
|
|
585
782
|
| State change (toggle, select) | `duration-200 ease-in-out` |
|
|
586
783
|
| Two-column detail at <1024 | right column collapses below main — do not override |
|
|
587
|
-
| Inline ID, code, table number | `font-mono text-
|
|
588
|
-
| Timestamp, label, subtle metadata | `text-
|
|
784
|
+
| Inline ID, code, table number | `font-mono text-code-sm` (identifiers) / `text-label-md tabular-nums` (figures) |
|
|
785
|
+
| Timestamp, label, subtle metadata | `text-body-sm text-muted-foreground` |
|
|
@@ -10,6 +10,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
10
10
|
|
|
11
11
|
| Version | Change |
|
|
12
12
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
13
|
+
| 1.17.0 | [Status and alert tokens now resolve to the colour roles](#1170-status-and-alert-tokens-now-resolve-to-the-colour-roles) |
|
|
13
14
|
| 1.15.0 | [Base UI 1.8.0 breaks closed-`Select` and dialog-click tests](#1150-base-ui-180-breaks-closed-select-and-dialog-click-tests) |
|
|
14
15
|
| 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) |
|
|
15
16
|
| 1.11.0 | [React 19.2.7 and React Router v8 required](#1110-react-1927-and-react-router-v8-are-now-required) |
|
|
@@ -21,6 +22,26 @@ Each entry states which versions are affected, what breaks, how to detect it, an
|
|
|
21
22
|
| 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) |
|
|
22
23
|
| before 1.0 | [Pre-1.0 breaking changes](#before-10) |
|
|
23
24
|
|
|
25
|
+
## 1.17.0: status and alert tokens now resolve to the colour roles
|
|
26
|
+
|
|
27
|
+
**Applies to:** apps that use `bg-status-*`, `text-status-*` or `bg-alert-*` utilities, and apps whose theme overrides `--status-*`, `--alert-*` or `--destructive` to recolour `Badge`, `Alert`, `CsvImporter` or `MetricCard`.
|
|
28
|
+
|
|
29
|
+
1.17.0 adds semantic colour roles, `--{info|success|warning|danger|neutral}-{surface|surface-hover|border|solid|solid-hover|text|contrast|indicator}`, bridged as Tailwind colours such as `bg-success-surface` and `text-danger-text`. The old `--status-*` and `--alert-*` tokens still exist and still work, but they no longer hold their own colours: each one now resolves to a role (for example `--status-completed` is `var(--success-solid)`). They are deprecated and will be removed in the next major. Nothing errors. Three things change silently:
|
|
30
|
+
|
|
31
|
+
- **Colours change.** `bg-status-*` and `bg-alert-*` resolve to the role values. For example `bg-status-completed` is `#15803d` instead of `#22c55e`. `--status-default` follows `--muted-foreground` instead of a fixed `#737373`.
|
|
32
|
+
- **Component recolouring moves to the roles.** `Badge`, `Alert`, `CsvImporter` and `MetricCard` read the roles directly. A theme that overrides `--status-*` or `--alert-*` no longer changes these components; override `--{intent}-*` instead. `Badge` `error` / `subtle-error` and `Alert` `error` read `--danger-*`, so overriding `--destructive` no longer reaches them either. `Button variant="destructive"` and `text-destructive` still follow `--destructive`.
|
|
33
|
+
- **`text-status-*` loses contrast in dark mode.** The `--status-*` values are now the step-9 fills, which are below 4.5:1 as text on the dark card.
|
|
34
|
+
|
|
35
|
+
How to detect it: search the app for `status-`, `alert-` and theme overrides of `--status-`, `--alert-` and `--destructive`. Compare `Badge` and `Alert` in light and dark mode after upgrading.
|
|
36
|
+
|
|
37
|
+
What to change:
|
|
38
|
+
|
|
39
|
+
- Replace `text-status-completed` with `text-success-text`, `text-status-attention` with `text-warning-text`, `text-status-danger` with `text-danger-text`, and `text-status-neutral` with `text-info-text`.
|
|
40
|
+
- Replace `bg-status-completed/10 border-status-completed/30` style tints with `bg-success-surface border-success-border`. The same applies to the other intents and to `bg-alert-*` / `border-alert-*`.
|
|
41
|
+
- In a theme that recolours the status components, set the roles (`--success-solid`, `--danger-text`, ...) instead of `--status-*`, `--alert-*` or `--destructive`.
|
|
42
|
+
|
|
43
|
+
The roles are documented in [Styling & theming](https://github.com/tailor-platform/app-shell/blob/main/docs/concepts/styling-theming.md#semantic-color-roles). The decision record is `decisions/semantic-color-roles.md`.
|
|
44
|
+
|
|
24
45
|
## 1.15.0: Base UI 1.8.0 breaks closed-`Select` and dialog-click tests
|
|
25
46
|
|
|
26
47
|
**Applies to:** apps with unit tests that assert `Select` options without opening the `Select`, and apps with Playwright tests that click buttons inside a `Dialog` or `AlertDialog`.
|
|
@@ -183,7 +183,7 @@ time, and the record is unblocked only once every one is released.
|
|
|
183
183
|
### 4. Line items
|
|
184
184
|
|
|
185
185
|
`Card.Root` → `Card.Header` (title, plus a one-line description where the
|
|
186
|
-
columns need explaining) → `Card.Content
|
|
186
|
+
columns need explaining) → `Card.Content padding="none"` → `Table.Root`.
|
|
187
187
|
|
|
188
188
|
Fetch the line items with the record, sorted explicitly so the order is stable
|
|
189
189
|
across reloads, and render them as a plain `Table`. For the typical document —
|
|
@@ -773,7 +773,7 @@ export default function PurchaseOrderDetailPage({
|
|
|
773
773
|
would have been a link field on the summary instead. */}
|
|
774
774
|
<Card.Root>
|
|
775
775
|
<Card.Header title="Source requisitions" />
|
|
776
|
-
<Card.Content
|
|
776
|
+
<Card.Content padding="none">
|
|
777
777
|
<Table.Root>
|
|
778
778
|
<Table.Header>
|
|
779
779
|
<Table.Row>
|
|
@@ -808,7 +808,7 @@ export default function PurchaseOrderDetailPage({
|
|
|
808
808
|
enough to page. No container padding; the cells inset themselves. */}
|
|
809
809
|
<Card.Root>
|
|
810
810
|
<Card.Header title="Line items" />
|
|
811
|
-
<Card.Content
|
|
811
|
+
<Card.Content padding="none">
|
|
812
812
|
{order.lineItems.length === 0 ? (
|
|
813
813
|
<p className="px-6 text-sm text-muted-foreground">No line items on this order.</p>
|
|
814
814
|
) : (
|
|
@@ -875,7 +875,7 @@ export default function PurchaseOrderDetailPage({
|
|
|
875
875
|
relationship is possible, so the reader learns it exists. */}
|
|
876
876
|
<Card.Root>
|
|
877
877
|
<Card.Header title="Goods receipts" />
|
|
878
|
-
<Card.Content
|
|
878
|
+
<Card.Content padding="none">
|
|
879
879
|
{order.goodsReceipts.length === 0 ? (
|
|
880
880
|
<p className="px-6 text-sm text-muted-foreground">
|
|
881
881
|
No goods receipts linked to this order.
|
|
@@ -997,7 +997,7 @@ export default function PurchaseOrderDetailPage({
|
|
|
997
997
|
title="Journal entry"
|
|
998
998
|
description="Booked when this order was posted."
|
|
999
999
|
/>
|
|
1000
|
-
<Card.Content
|
|
1000
|
+
<Card.Content padding="none">
|
|
1001
1001
|
<Table.Root>
|
|
1002
1002
|
<Table.Header>
|
|
1003
1003
|
<Table.Row>
|
|
@@ -1096,7 +1096,7 @@ enforced across every detail screen once settled.
|
|
|
1096
1096
|
## Constraints
|
|
1097
1097
|
|
|
1098
1098
|
- **Every main-column section sits in a `Card.Root`** — except `DescriptionCard`, which contains itself, and `Alert`, which is a banner. No bare `<div>` sections.
|
|
1099
|
-
- **A table in a card
|
|
1099
|
+
- **A table in a card goes in `Card.Content padding="none"`, and nothing else changes.** Don't drop `Card.Content` or zero its padding with a class instead: the prop also keeps row backgrounds inside the card's rounded corners and removes a nested `DataTable`'s own border. Leave the table container alone too. `Table.Head` and `Table.Cell` already inset their own first and last cells by 24px; padding on the table container stacks on top of that and pushes the first column 24px right of the card title.
|
|
1100
1100
|
- **Bare `YYYY-MM-DD` dates must not use `type: "date"`.** `DescriptionCard` hands the value to `new Date(...)`, which reads a date-only string as UTC midnight and renders the previous day west of Greenwich. Pre-format those as text via `render`. Real timestamps keep `type: "date"`, with `emptyBehavior: "hide"` when nullable.
|
|
1101
1101
|
- **`ActionPanel` is workflow-only.** No navigation, no "view related record" — those are links in the cards that hold them.
|
|
1102
1102
|
- **Never rely on a query cap to bound the line-items table.** If the cap can be reached, page the lines.
|
|
@@ -28,32 +28,36 @@ function ModalForm() {
|
|
|
28
28
|
<Dialog.Title>Add address</Dialog.Title>
|
|
29
29
|
<Dialog.Description>Add a shipping address to this order.</Dialog.Description>
|
|
30
30
|
</Dialog.Header>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
{/*
|
|
32
|
+
* `onFormSubmit` fires only after validation passes and receives the
|
|
33
|
+
* registered field values — no `<form onSubmit>` + `new FormData(...)`.
|
|
34
|
+
*/}
|
|
35
|
+
<Form<AddressDraft>
|
|
36
|
+
noValidate
|
|
37
|
+
onFormSubmit={(values) => window.alert(`Saving ${values.label}`)}
|
|
37
38
|
>
|
|
38
39
|
<div className="flex flex-col gap-4 py-4">
|
|
39
40
|
<Field.Root name="label">
|
|
40
41
|
<Field.Label>Label</Field.Label>
|
|
41
|
-
<Field.Control
|
|
42
|
+
<Field.Control required />
|
|
43
|
+
<Field.Error match="valueMissing">Label is required.</Field.Error>
|
|
42
44
|
</Field.Root>
|
|
43
45
|
<Field.Root name="street">
|
|
44
46
|
<Field.Label>Street</Field.Label>
|
|
45
|
-
<Field.Control
|
|
47
|
+
<Field.Control required />
|
|
48
|
+
<Field.Error match="valueMissing">Street is required.</Field.Error>
|
|
46
49
|
</Field.Root>
|
|
47
50
|
<Field.Root name="city">
|
|
48
51
|
<Field.Label>City</Field.Label>
|
|
49
|
-
<Field.Control
|
|
52
|
+
<Field.Control required />
|
|
53
|
+
<Field.Error match="valueMissing">City is required.</Field.Error>
|
|
50
54
|
</Field.Root>
|
|
51
55
|
</div>
|
|
52
56
|
<Dialog.Footer>
|
|
53
|
-
<Dialog.Close render={<Button variant="ghost" />}>Cancel</Dialog.Close>
|
|
57
|
+
<Dialog.Close render={<Button type="button" variant="ghost" />}>Cancel</Dialog.Close>
|
|
54
58
|
<Button type="submit">Save</Button>
|
|
55
59
|
</Dialog.Footer>
|
|
56
|
-
</
|
|
60
|
+
</Form>
|
|
57
61
|
</Dialog.Content>
|
|
58
62
|
</Dialog.Root>
|
|
59
63
|
);
|
|
@@ -86,27 +90,28 @@ function ModalFormRouted() {
|
|
|
86
90
|
<Dialog.Header>
|
|
87
91
|
<Dialog.Title>Create product</Dialog.Title>
|
|
88
92
|
</Dialog.Header>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
window.alert(`Saving ${data.get("name") ?? ""}`);
|
|
93
|
+
<Form<ProductDraft>
|
|
94
|
+
noValidate
|
|
95
|
+
onFormSubmit={(values) => {
|
|
96
|
+
window.alert(`Saving ${values.name}`);
|
|
94
97
|
setCreateOpen(false);
|
|
95
98
|
}}
|
|
96
99
|
>
|
|
97
100
|
<div className="flex flex-col gap-4 py-4">
|
|
98
101
|
<Field.Root name="name">
|
|
99
102
|
<Field.Label>Name</Field.Label>
|
|
100
|
-
<Field.Control
|
|
103
|
+
<Field.Control required />
|
|
104
|
+
<Field.Error match="valueMissing">Name is required.</Field.Error>
|
|
101
105
|
</Field.Root>
|
|
102
106
|
</div>
|
|
103
107
|
<Dialog.Footer>
|
|
104
|
-
|
|
108
|
+
{/* Inside a `<Form>` an untyped button submits — Cancel must be `type="button"`. */}
|
|
109
|
+
<Button type="button" variant="ghost" onClick={() => setCreateOpen(false)}>
|
|
105
110
|
Cancel
|
|
106
111
|
</Button>
|
|
107
112
|
<Button type="submit">Save</Button>
|
|
108
113
|
</Dialog.Footer>
|
|
109
|
-
</
|
|
114
|
+
</Form>
|
|
110
115
|
</Dialog.Content>
|
|
111
116
|
</Dialog.Root>
|
|
112
117
|
</Layout>
|
|
@@ -133,7 +138,8 @@ placeholder.
|
|
|
133
138
|
- **`Form` + `Field` is the default stack.** They wrap Base UI and ship with AppShell — no extra
|
|
134
139
|
dependency.
|
|
135
140
|
- **Submit via `onFormSubmit(values)`.** It fires only after validation passes. Do not hand-roll
|
|
136
|
-
`<form onSubmit>` + `new FormData(...)` — that
|
|
141
|
+
`<form onSubmit>` + `new FormData(...)` — that bypasses `Form`'s field validation and server-error
|
|
142
|
+
routing.
|
|
137
143
|
- **`onFormSubmit` reads registered `Field.Root`s, not the DOM.** So every control — including
|
|
138
144
|
`Select`, `Combobox`, and `Autocomplete` — just needs wrapping in a `Field.Root name="…"`. They
|
|
139
145
|
need **no `name` of their own and no `useState`**. Mirroring field values into React state is the
|