@tailor-platform/app-shell 1.15.0 → 1.16.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 (98) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +208 -114
  3. package/dist/app-shell.js +3449 -3446
  4. package/dist/{appshell-DLVBFo5J.js → appshell-B0wh5Lq0.js} +625 -512
  5. package/dist/style-entry.d.ts +116 -96
  6. package/dist/testing.d.ts +116 -96
  7. package/dist/testing.js +1 -1
  8. package/dist/vite-plugin.d.ts +116 -96
  9. package/package.json +8 -8
  10. package/skills/app-shell-patterns/SKILL.md +101 -14
  11. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
  12. package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
  13. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
  14. package/skills/app-shell-patterns/references/api/define-module.md +247 -0
  15. package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
  16. package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
  17. package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
  18. package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
  19. package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
  20. package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
  21. package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
  22. package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
  23. package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
  24. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +126 -0
  25. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +146 -0
  26. package/skills/app-shell-patterns/references/api/use-app-shell.md +143 -0
  27. package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
  28. package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
  29. package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
  30. package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
  31. package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
  32. package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
  33. package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
  34. package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
  35. package/skills/app-shell-patterns/references/api/use-toast.md +107 -0
  36. package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
  37. package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
  38. package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
  39. package/skills/app-shell-patterns/references/components/alert.md +202 -0
  40. package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
  41. package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
  42. package/skills/app-shell-patterns/references/components/attachment.md +138 -0
  43. package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
  44. package/skills/app-shell-patterns/references/components/avatar.md +80 -0
  45. package/skills/app-shell-patterns/references/components/badge.md +309 -0
  46. package/skills/app-shell-patterns/references/components/button.md +164 -0
  47. package/skills/app-shell-patterns/references/components/card.md +109 -0
  48. package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
  49. package/skills/app-shell-patterns/references/components/combobox.md +318 -0
  50. package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
  51. package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
  52. package/skills/app-shell-patterns/references/components/data-table.md +953 -0
  53. package/skills/app-shell-patterns/references/components/date-picker.md +248 -0
  54. package/skills/app-shell-patterns/references/components/default-header.md +75 -0
  55. package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
  56. package/skills/app-shell-patterns/references/components/description-card.md +569 -0
  57. package/skills/app-shell-patterns/references/components/dialog.md +164 -0
  58. package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
  59. package/skills/app-shell-patterns/references/components/form.md +378 -0
  60. package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
  61. package/skills/app-shell-patterns/references/components/grid.md +184 -0
  62. package/skills/app-shell-patterns/references/components/input.md +83 -0
  63. package/skills/app-shell-patterns/references/components/layout.md +539 -0
  64. package/skills/app-shell-patterns/references/components/menu.md +218 -0
  65. package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
  66. package/skills/app-shell-patterns/references/components/select.md +283 -0
  67. package/skills/app-shell-patterns/references/components/sheet.md +237 -0
  68. package/skills/app-shell-patterns/references/components/sidebar-group.md +466 -0
  69. package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
  70. package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
  71. package/skills/app-shell-patterns/references/components/spinner.md +81 -0
  72. package/skills/app-shell-patterns/references/components/table.md +188 -0
  73. package/skills/app-shell-patterns/references/components/tabs.md +158 -0
  74. package/skills/app-shell-patterns/references/components/textarea.md +122 -0
  75. package/skills/app-shell-patterns/references/components/timeline.md +317 -0
  76. package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
  77. package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
  78. package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
  79. package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
  80. package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
  81. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +350 -0
  82. package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +11 -4
  83. package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
  84. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +387 -0
  85. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +388 -0
  86. package/skills/app-shell-patterns/references/{fundamental/design-system.md → concepts/styling-theming.md} +258 -110
  87. package/skills/app-shell-patterns/references/migrations.md +34 -0
  88. package/skills/app-shell-patterns/references/pages/{detail.md → document-detail.md} +4 -27
  89. package/skills/app-shell-patterns/references/patterns/form-composer.md +43 -45
  90. package/skills/app-shell-patterns/references/patterns/form-modal.md +39 -100
  91. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
  92. package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
  93. package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
  94. package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
  95. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +13 -27
  96. package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
  97. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +11 -86
  98. package/skills/app-shell-patterns/references/fundamental/components.md +0 -1009
@@ -1,16 +1,21 @@
1
- # Design System
1
+ ---
2
+ title: Styling and Theming
3
+ description: The AppShell design system — Tailwind CSS v4 setup, theming and palettes, the token catalog, typography, and the styling boundary
4
+ ---
2
5
 
3
- Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `components.md`; that split avoids duplicating tables and lengthy examples across both docs.
6
+ <!-- GENERATED by docs-kit from docs-src/concepts/styling-theming.docs.outline.md — do not edit by hand. -->
4
7
 
5
- `@tailor-platform/app-shell` ships an opinionated design system as CSS variables, bridged into Tailwind v4's token namespace. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
8
+ # Styling and Theming
9
+
10
+ Styling is done with **Tailwind CSS v4** against `@tailor-platform/app-shell`'s design system, which ships as CSS variables bridged into Tailwind's token namespace. Use the tokens whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
6
11
 
7
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.
8
13
 
9
- **Every token in this file 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 to you: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled. There is no error, no warning, and 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. 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.
10
15
 
11
- ## 1. Setup
16
+ ## Setup
12
17
 
13
- The app's CSS entrypoint (`index.css` / `globals.css`) needs exactly this:
18
+ AppShell exports `@tailor-platform/app-shell/styles`. Your app's CSS entrypoint (`index.css` / `globals.css`) needs exactly this:
14
19
 
15
20
  ```css
16
21
  @import "tailwindcss";
@@ -21,14 +26,14 @@ The app's CSS entrypoint (`index.css` / `globals.css`) needs exactly this:
21
26
  ```
22
27
 
23
28
  - **`tailwindcss`** — your app's own Tailwind build, which generates the utilities you write in your components.
24
- - **`@tailor-platform/app-shell/styles`** — the single required import. It pulls in both the design tokens (the `@theme inline` bridge, so `bg-background` / `text-muted-foreground` resolve in _your_ Tailwind build) and AppShell's precompiled component CSS.
29
+ - **`@tailor-platform/app-shell/styles`** — the single required import. It ships the palette (light **and** dark), the Tailwind v4 `@theme inline` bridge (so `bg-background` / `text-muted-foreground` resolve in _your_ Tailwind build), and AppShell's precompiled component CSS. Your entry CSS should declare none of these itself — a copy of any of them overrides AppShell's and silently breaks dark mode.
25
30
  - **`@tailor-platform/app-shell/themes/*`** — optional palette overrides (`default`, `cream`, `bloom`). Import at most one, after `styles`. Palette selection is by CSS import; there is no runtime palette prop.
26
31
 
27
- **Do not import `@tailor-platform/app-shell/theme.css`.** That export is a deprecated no-op shim, kept only so older apps keep building. It emits nothing. Older docs also referred to `app-shell.css`; prefer `styles`.
32
+ **Do not import `@tailor-platform/app-shell/theme.css`.** That export is a deprecated no-op shim kept only so older apps keep building; it emits nothing. Older docs also referred to `app-shell.css` — prefer `styles`.
28
33
 
29
- Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `project-setup.md`.
34
+ Tailwind v4 stays CSS-first; the `vite` / PostCSS wiring is minimal.
30
35
 
31
- ## 2. Theming via CSS variables
36
+ ## Theming with CSS variables
32
37
 
33
38
  Tokens exist in two layers, and knowing which one you are touching matters:
34
39
 
@@ -57,31 +62,85 @@ Override at the highest scope where the change applies. A narrower scope (per-se
57
62
 
58
63
  AppShell primitives respond to dark mode automatically. Custom components inherit it for free as long as they reference tokens (`bg-card`, `text-muted-foreground`) and never inline literal colors.
59
64
 
60
- ## 3. Component styling with data attributes
65
+ ## Theme Palettes
61
66
 
62
- AppShell's UI components support data-attribute-based styling, following the [Base UI data attributes](https://base-ui.com/react/handbook/styling#data-attributes) convention. Components expose `data-*` attributes that reflect their internal state, enabling CSS-only style control without JavaScript:
67
+ AppShell ships three palettes, each with light and dark variants:
68
+
69
+ | Palette | CSS import |
70
+ | --------- | -------------------------------------------------------------- |
71
+ | `default` | Included automatically via `@tailor-platform/app-shell/styles` |
72
+ | `cream` | `@tailor-platform/app-shell/themes/cream` |
73
+ | `bloom` | `@tailor-platform/app-shell/themes/bloom` |
74
+
75
+ Select a palette by importing its CSS file — no prop needed. Import it in your global CSS **after** `@tailor-platform/app-shell/styles`:
63
76
 
64
77
  ```css
65
- /* Style a component based on its state */
66
- .SwitchThumb[data-checked] {
67
- background-color: var(--status-completed);
68
- }
78
+ @import "tailwindcss";
79
+ @import "@tailor-platform/app-shell/styles";
80
+ @import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */
81
+ ```
69
82
 
70
- .MenuItem[data-highlighted] {
71
- background-color: var(--accent);
72
- color: var(--accent-foreground);
73
- }
83
+ Only import one palette at a time.
84
+
85
+ ## Adding a palette
86
+
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
+
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** | Do not duplicate | Status and alert tokens inherit from `default.css` |
96
+ | **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed |
97
+
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
+
100
+ Preview token values at `/showcase/colors` in the Vite example app.
101
+
102
+ Or skip the local setup: **[theme.tailor.tech](https://theme.tailor.tech/playground)**
103
+ takes a primary color, previews it on real AppShell components, and exports a
104
+ `themes/{name}.css` structured according to the tier table above.
105
+
106
+ ## Color modes: light / dark / system
107
+
108
+ AppShell supports three color modes: `light`, `dark`, and `system` (follows the OS setting).
109
+
110
+ Set the initial mode via the `defaultColorTheme` prop on `<AppShell>`. The user's selection is persisted to `localStorage` and restored on subsequent visits:
111
+
112
+ ```tsx
113
+ <AppShell defaultColorTheme="system" modules={modules}>
114
+ {/* ... */}
115
+ </AppShell>
74
116
  ```
75
117
 
76
- This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays:
118
+ Use the [`useTheme`](../api/use-theme.md) hook to read or change the theme at runtime:
119
+
120
+ <example-preview name="theme-toggle"></example-preview>
77
121
 
78
122
  ```tsx
79
- <Switch.Thumb className="bg-muted data-[checked]:bg-primary" />
123
+ function ThemeToggle() {
124
+ const { resolvedTheme, setTheme } = useTheme();
125
+ return (
126
+ <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
127
+ Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
128
+ </button>
129
+ );
130
+ }
80
131
  ```
81
132
 
82
- Check each component's API reference (`components.md`) for the data attributes it exposes. Custom components must follow the same convention (see Section 6).
133
+ Drop the pre-built [`AppearanceSwitcher`](../components/appearance-switcher.md) component anywhere in your layout for a ready-made light/dark/system toggle:
134
+
135
+ <example-preview name="appearance-toggle"></example-preview>
136
+
137
+ ```tsx
138
+ function AppearanceToggle() {
139
+ return <AppearanceSwitcher />;
140
+ }
141
+ ```
83
142
 
84
- ## 4. Tokens
143
+ ## Tokens
85
144
 
86
145
  Everything below is verified present in the shipped CSS. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure.
87
146
 
@@ -300,122 +359,181 @@ Stock Tailwind breakpoints — AppShell does not change them.
300
359
 
301
360
  **ERP target is `xl`/`2xl` desktop.** Pages should be designed for those widths first; smaller breakpoints exist for graceful degradation, not parity. Don't waste effort on mobile-first composition unless a screen explicitly calls for it. A list page that collapses gracefully at `md` is fine; a list page redesigned for `sm` is over-investment.
302
361
 
303
- Two-column **behavior** (right rail stacks under `lg`): respect AppShell defaults — do not force side-by-side grids on narrow viewports. The `Layout` column width table lives in `components.md` → Layout; reuse those numbers instead of guessing rem values here.
362
+ Two-column **behavior** (right rail stacks under `lg`): respect AppShell defaults — do not force side-by-side grids on narrow viewports. The `Layout` column width table lives in [`Layout`](../components/layout.md); reuse those numbers instead of guessing rem values here.
304
363
 
305
- ## 4b. Links
364
+ ## Fonts
306
365
 
307
- One treatment for every text link — inside `DescriptionCard`, in table rows,
308
- in cards:
366
+ AppShell bundles **Inter Variable** for Latin and **Noto Sans JP Variable** for Japanese, and
367
+ applies both via the `body` rule in `@tailor-platform/app-shell/styles`. No extra import:
309
368
 
310
- - **`text-primary` at rest, `hover:underline` only.** Colour carries the affordance; a resting underline is noise. Pair with `underline-offset-4`.
311
- - **Internal routes** use app-shell's `Link` (`to`), never a bare `<a href>`.
312
- - **External destinations** use `<a target="_blank" rel="noopener noreferrer">` with a lucide `ExternalLink` at `size-3` after the label. There is no dedicated component for this; that markup is the convention.
313
- - **Identifiers** (document numbers, SKUs) keep `font-mono` on top of the link style.
369
+ ```css
370
+ @import "tailwindcss";
371
+ @import "@tailor-platform/app-shell/styles";
372
+ ```
314
373
 
315
- This is the treatment `DescriptionCard` already applies to its `type: "link"`
316
- fields — match it rather than inventing a second one.
374
+ Both are variable fonts with a continuous 100–900 weight axis, so every weight the design
375
+ system names — `font-normal`, `font-medium`, `font-semibold`, `font-bold` — resolves to a real
376
+ weight in both scripts rather than to whatever faces happen to be installed.
317
377
 
318
- ## 5. AppShell styling boundary
378
+ ### Why Japanese needs its own font
319
379
 
320
- `astw:` is AppShell's internal compiled CSS prefix. **Never write it in an application.** It is not a consumer customization API and arbitrary prefixed utilities may not exist in the shipped CSS.
380
+ Inter has no CJK glyphs, so without a bundled Japanese font, Japanese characters fall through
381
+ to the operating system's. On Windows that font is Yu Gothic UI, which ships only
382
+ Light/Semilight/Regular/Semibold/Bold — no 500 — so CSS weight matching resolves a
383
+ `font-weight: 500` request down to Regular, and `font-medium` becomes **indistinguishable from
384
+ body text in Japanese**. Current macOS is not affected: it ships Hiragino Sans W0–W9 including
385
+ W5, which `font-weight: 500` resolves to correctly. Bundling the font makes the weight scale
386
+ hold on every platform rather than depending on what the OS happens to install.
321
387
 
322
- Use ordinary Tailwind utilities on your own markup. An explicitly documented layout hook, such as `Table.Root`'s `containerClassName`, also accepts ordinary utilities from the application's Tailwind build:
388
+ Noto Sans JP is metric-harmonised against Inter (`size-adjust: 94%` plus ascent/descent
389
+ overrides) so mixed Japanese/Latin strings read at one optical size, and a line containing
390
+ Japanese is exactly as tall as one without. A metric-matched `local()` fallback covers the
391
+ window before a subset arrives, so rows do not change height as fonts stream in.
323
392
 
324
- ```tsx
325
- <Table.Root containerClassName="px-6 overflow-y-auto" />
393
+ ### What it costs
394
+
395
+ The Japanese faces are roughly **5 MB of woff2 subsets**, and they land in your build output
396
+ whether or not your app renders Japanese — a bundler emits every subset it can see, because it
397
+ cannot know at build time which characters your data will contain. The stylesheet grows from
398
+ about 98 KB to 200 KB uncompressed, or 15 KB to 45 KB gzipped.
399
+
400
+ What your **users** download is much smaller, and proportional to what they actually read.
401
+ Each subset carries a `unicode-range`, so the browser fetches one only when it is about to
402
+ paint a character in that range. The faces are restricted to Japanese blocks — kana, kanji,
403
+ CJK punctuation, fullwidth forms and the compatibility blocks carrying ㈱ ㍿ ㎡ — so shared
404
+ symbols such as `✓` or `→` resolve to Inter or the system font as before, and
405
+ **an app that renders no Japanese downloads none of them**:
406
+
407
+ | what the user has seen | downloaded |
408
+ | ---------------------------------------- | ----------- |
409
+ | all hiragana + katakana | ~237 KB |
410
+ | + ~230 common ERP kanji | ~458 KB |
411
+ | + ~30 name kanji including variant forms | ~467 KB |
412
+ | **typical first Japanese screen** | **~910 KB** |
413
+
414
+ Files are content-hashed and cached, so the cost is front-loaded rather than per-navigation,
415
+ and it grows slowly as unusual characters appear in your data. Weight costs nothing extra —
416
+ every weight lives in the same file, so using `font-medium` and `font-bold` downloads no more
417
+ than `font-normal` alone.
418
+
419
+ To drop the Japanese faces entirely, override the stack without naming them (see below).
420
+ Nothing references them, so nothing is downloaded — though the files still ship in your build
421
+ output.
422
+
423
+ ### Using your own font
424
+
425
+ Set `--app-shell-font-sans` on `:root` **after** importing AppShell styles to replace the whole
426
+ stack:
427
+
428
+ ```css
429
+ @import "@tailor-platform/app-shell/styles";
430
+
431
+ :root {
432
+ --app-shell-font-sans: "Your Brand Sans", ui-sans-serif, system-ui, sans-serif;
433
+ }
326
434
  ```
327
435
 
328
- For an AppShell component's appearance, use its documented props, variants, or composition instead of styling over its internals.
436
+ A replacement should be a variable font, or otherwise supply real 400/500/600/700 faces —
437
+ AppShell's weight scale assumes all four exist. If your app renders Japanese, include a
438
+ Japanese family with a continuous weight axis for the same reason.
439
+
440
+ ## Styling AppShell components
329
441
 
330
- ## 6. When AppShell doesn't have a component you need
442
+ Write **plain Tailwind utilities** in your application code. Three rules cover every case:
331
443
 
332
- Most ERP screens compose entirely from AppShell primitives. When you hit a gap, work through this decision tree before building anything:
444
+ 1. **Your own markup** — ordinary utilities, exactly as in any Tailwind app.
445
+ 2. **A documented layout hook** — props like `Table.Root`'s `containerClassName` also take ordinary utilities from your Tailwind build:
333
446
 
334
- ### Decision tree
447
+ ```tsx
448
+ <Table.Root containerClassName="max-h-96 overflow-y-auto" />
449
+ ```
335
450
 
336
- 1. **Can you compose existing AppShell primitives?** A "card with a metric and a trend arrow" is `MetricCard`, or `Card` plus a lucide icon — not a new component. Compose first, and check `components.md` for what already exists before concluding there is a gap.
337
- 2. **If composition won't work, is the behavior one-off?** Build it locally under `src/components/<name>/` and flag it for the `build-component` skill, which promotes useful customs into AppShell upstream.
338
- 3. **If it's already proven reusable across 2+ apps**, skip local entirely — use the `build-component` skill to add it to AppShell directly.
451
+ 3. **An AppShell component's own appearance** — reach for its documented props, variants, or composition rather than styling over its internals. `Sheet.Content` takes `size`, `Table.Head` and `Table.Cell` take `align`, `Grid` takes `columns`, `Layout` takes `gap`, `Button` takes `variant`. A prop is a supported contract; a utility class aimed at a component's internals is not.
339
452
 
340
- ### Conformance rules (non-negotiable for any custom component)
453
+ ```tsx
454
+ // Props, not utilities, for a component's own appearance
455
+ <Sheet.Content size="lg" />
456
+ <Layout gap={6} />
457
+ <Button variant="destructive">Delete</Button>
341
458
 
342
- - **Tokens only.** No hex literals, no magic px values, no hand-rolled shadows. Every visual property maps to a token from Section 4 — and only to a token that actually exists there.
343
- - **Base UI data-attribute pattern for state.** Expose `data-*` attributes that reflect internal state; never style off React props alone. A custom toggle exposes `data-checked`; a custom step indicator exposes `data-active`, `data-completed`, etc.
344
- - **Compose AppShell primitives inside.** If the custom needs a button, use `Button` — not raw `<button>`. Same for `Input`, `Badge`, `Card`. The custom's job is composition, not reinvention.
345
- - **Document with a `README.md`** in the component folder listing: purpose, props, tokens used, and a brief justification for why this can't be composed from existing AppShell primitives.
346
- - **Match accessibility behavior** of the closest AppShell equivalent — focus management, ARIA roles/attributes, keyboard interactions. Reach for Base UI primitives if the behavior is non-trivial.
459
+ // Ordinary utilities on your own markup and on documented hooks
460
+ <div className="flex flex-col gap-4">
461
+ <Table.Root containerClassName="max-h-96 overflow-y-auto" />
462
+ </div>
463
+ ```
347
464
 
348
- Example skeleton for a local custom component:
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), and `!` is what the pattern catalogue uses for the one documented case, zeroing a card's padding so a table can sit flush inside it:
349
466
 
350
467
  ```tsx
351
- // src/components/StepIndicator/index.tsx
352
- import { Check } from "lucide-react";
468
+ <Card.Root>
469
+ <Card.Header title="Line items" />
470
+ <Card.Content className="px-0!">
471
+ <Table.Root>{/* … */}</Table.Root>
472
+ </Card.Content>
473
+ </Card.Root>
474
+ ```
353
475
 
354
- type Props = { steps: string[]; current: number };
476
+ Note the table container adds **no** horizontal padding of its own there: `Table.Head` and `Table.Cell` already carry `first:pl-6` / `last:pr-6`, so a `containerClassName="px-6"` would stack on top and push the first column 24px past the card title.
355
477
 
356
- export function StepIndicator({ steps, current }: Props) {
357
- return (
358
- <ol className="flex gap-2">
359
- {steps.map((label, i) => (
360
- <li
361
- key={label}
362
- data-active={i === current ? "" : undefined}
363
- data-completed={i < current ? "" : undefined}
364
- className="
365
- flex items-center gap-2 rounded-md px-3 py-2
366
- bg-muted text-sm text-muted-foreground
367
- data-[active]:bg-primary data-[active]:text-primary-foreground
368
- data-[completed]:text-status-completed
369
- "
370
- >
371
- {i < current && <Check className="size-4" />}
372
- {label}
373
- </li>
374
- ))}
375
- </ol>
376
- );
377
- }
478
+ ### Never write the `astw:` prefix in application code
479
+
480
+ AppShell's own components are styled with utilities carrying an `astw:` prefix (AppShell TailWind). This exists because Tailwind generates classes at build time: AppShell's stylesheet is compiled and published before your application's is generated, and Tailwind cannot reconcile two independently-generated stylesheets against each other. The prefix keeps the library's utilities from clashing with yours.
481
+
482
+ The prefix is **internal to the library**, and it is not a customization API. Your Tailwind build has no `astw` prefix configured, so it never generates `astw:*` classes — an `astw:` class written in application code only resolves if AppShell happens to already ship that exact utility for its own use. Many do not — 17 of the 55 classes this documentation used to recommend are absent from the shipped stylesheet — and Tailwind emits nothing for an unknown utility: no error, no warning, nothing in the console. The class lands in the DOM and does nothing.
483
+
484
+ Worse, one `className` string can be half-applied. AppShell merges class names with `tailwind-merge`, which is not configured with the `astw` prefix, so it strips a conflicting _internal_ class while leaving an unshipped `astw:` class in place — the element keeps a dead class and loses the style it had.
485
+
486
+ `@tailor-platform/eslint-plugin-app-shell` ships a `no-astw-prefix` rule that catches this in your own source. Enable it via the `recommended` preset in your `oxlint.config.ts`.
487
+
488
+ ### Why a plain utility can't override an AppShell default
489
+
490
+ This is the reason rule 3 sends you to props rather than to a more specific class. `tailwind-merge` groups `astw:px-6` and `px-0` separately — it reads the unknown `astw:` as a variant — so both survive on the element rather than the later one replacing the earlier. Both land in the same `@layer utilities` at equal specificity, and AppShell's precompiled sheet is imported after your Tailwind output, so its rule is later in the cascade and the library's value wins:
491
+
492
+ ```tsx
493
+ // Element keeps both classes; padding stays at AppShell's 24px
494
+ <Card.Content className="px-0" />
378
495
  ```
379
496
 
380
- Notice: every value is a real token, `data-*` attributes carry state, no hex literals, and the icon comes from `lucide-react` rather than a nonexistent `Icon` component.
497
+ 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.
381
498
 
382
- ### Promotion path
499
+ ### Styling by state (data attributes)
383
500
 
384
- When a custom component proves reusable across 2+ apps, promote it upstream into AppShell via the `build-component` skill (a separate skill at `~/.claude/skills/build-component/`). The skill handles API design, exports, test coverage, and the upstream PR. Local custom components should be considered staging — not a permanent home.
501
+ AppShell's UI components support data-attribute-based styling, following the [Base UI data attributes](https://base-ui.com/react/handbook/styling#data-attributes) convention. Components expose `data-*` attributes that reflect their internal state, enabling CSS-only style control without JavaScript:
385
502
 
386
- ## 7. Quick reference
503
+ ```css
504
+ /* Style a component based on its state */
505
+ .SwitchThumb[data-checked] {
506
+ background-color: var(--status-completed);
507
+ }
387
508
 
388
- ### Where to look
509
+ .MenuItem[data-highlighted] {
510
+ background-color: var(--accent);
511
+ color: var(--accent-foreground);
512
+ }
513
+ ```
389
514
 
390
- | Concern | File |
391
- | --------------------------------------------------------------- | ---------------------------------- |
392
- | Component imports, props, JSX composition | **`components.md`** |
393
- | Page / screen layout patterns | **`patterns/<type>/<slug>.md`** |
394
- | Design tokens, AppShell styling boundary, custom UI conformance | **this file** (`design-system.md`) |
515
+ This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays:
395
516
 
396
- **`components.md`** holds long JSX compositions (e.g. **`DataTable`**, **card + `Table`**). **Do not copy those trees into this file** — link back here only for tokens and the AppShell styling boundary.
517
+ ```tsx
518
+ <Switch.Thumb className="bg-muted data-[checked]:bg-primary" />
519
+ ```
397
520
 
398
- `patterns/` is organised by page type: `patterns/list/`, `patterns/detail/`, `patterns/form/`, `patterns/interaction/`. Pick the slug matching the screen you're building.
521
+ Check each component's own page under `docs/components/` for the data attributes it exposes. Custom components must follow the same convention (see [Custom components](./custom-components.md)).
399
522
 
400
- ### Semantic decisions
523
+ ## Links
401
524
 
402
- | Intent | Pick |
403
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
404
- | Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` |
405
- | Non-blocking caution | `Badge variant="warning"`, or `bg-status-attention` |
406
- | Confirmation / completed state | `Badge variant="success"`, or `bg-status-completed` |
407
- | Neutral callout | `Badge variant="info"`, or the `Alert` component |
408
- | Persistent panel (sidebar, header) | `shadow-sm` |
409
- | Hovered / sticky surface | `shadow-md` |
410
- | Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
411
- | Modal / sheet / dialog | `shadow-lg`, `duration-300` entrance |
412
- | Hover / focus transition | `duration-150 ease-out` |
413
- | State change (toggle, select) | `duration-200 ease-in-out` |
414
- | Two-column detail at <1024 | right column collapses below main — do not override |
415
- | Inline ID, code, table number | `font-mono text-xs` (identifiers) / `tabular-nums` (figures) |
416
- | Timestamp, label, subtle metadata | `text-xs text-muted-foreground` |
525
+ One treatment for every text link — inside `DescriptionCard`, in table rows,
526
+ in cards:
527
+
528
+ - **`text-primary` at rest, `hover:underline` only.** Colour carries the affordance; a resting underline is noise. Pair with `underline-offset-4`.
529
+ - **Internal routes** use app-shell's `Link` (`to`), never a bare `<a href>`.
530
+ - **External destinations** use `<a target="_blank" rel="noopener noreferrer">` with a lucide `ExternalLink` at `size-3` after the label. There is no dedicated component for this; that markup is the convention.
531
+ - **Identifiers** (document numbers, SKUs) keep `font-mono` on top of the link style.
532
+
533
+ This is the treatment `DescriptionCard` already applies to its `type: "link"`
534
+ fields — match it rather than inventing a second one.
417
535
 
418
- ### Composition & emphasis rules
536
+ ## Composition & emphasis rules
419
537
 
420
538
  These are visual-composition rules every screen must follow, regardless of pattern. They exist because emphasis only works when it is scarce.
421
539
 
@@ -426,10 +544,10 @@ These are visual-composition rules every screen must follow, regardless of patte
426
544
  - A record's **primary / lifecycle status** (PO status, SO status) → a **filled semantic** variant (`success` / `warning` / `error` / `info` / `neutral`) — one per row in a list, one in a detail header.
427
545
  - **Secondary statuses** (delivery, billing, fulfilment) and dense supporting columns → **`outline-*`** (with status dot).
428
546
  - **Tags / labels** ("New", "Returned") → **`subtle-*`**.
429
- - Reserve **`default`** (brand fill) for non-status emphasis — never the brand color as a routine status. The defect to avoid: making _every_ chip a loud fill, or giving secondary statuses the same weight as the primary one. (Variants: **`components.md`** → `Badge`.)
547
+ - Reserve **`default`** (brand fill) for non-status emphasis — never the brand color as a routine status. The defect to avoid: making _every_ chip a loud fill, or giving secondary statuses the same weight as the primary one. (Variants: [`Badge`](../components/badge.md).)
430
548
  - **Color:** status colors signal meaning, not decoration — don't tint neutral content.
431
549
 
432
- **Hierarchy.** One `h1` per page (the `Layout.Header` title). Section headings step down in weight and size; never skip levels for size — pick the role from §4 Typography, not the pixel size.
550
+ **Hierarchy.** One `h1` per page (the `Layout.Header` title). Section headings step down in weight and size; never skip levels for size — pick the role from the Tokens → Typography table, not the pixel size.
433
551
 
434
552
  **States — never ship only the happy path.** Every data-backed screen handles:
435
553
 
@@ -437,4 +555,34 @@ These are visual-composition rules every screen must follow, regardless of patte
437
555
  - **Empty** — a labelled empty state (what it is, how to add the first record), not a bare empty table.
438
556
  - **Error** — an inline error with a retry affordance, not a silent failure.
439
557
 
440
- **Spacing rhythm.** Use the spacing scale (§4) consistently — equal gaps between sibling sections, consistent card padding. A one-off `gap` or `padding` that doesn't match its siblings reads as a mistake.
558
+ **Spacing rhythm.** Use the spacing scale (see Tokens) consistently — equal gaps between sibling sections, consistent card padding. A one-off `gap` or `padding` that doesn't match its siblings reads as a mistake.
559
+
560
+ ## Quick reference
561
+
562
+ ### Where to look
563
+
564
+ | Concern | Where |
565
+ | ---------------------------------------------------------- | --------------------------------------------------------------- |
566
+ | Tokens, theming, the `astw:` styling boundary, conformance | **this document** |
567
+ | A component's imports, props, variants, composition | its page under [`docs/components/`](../components/) |
568
+ | A hook / function API | its page under [`docs/api/`](../api/) |
569
+ | Screen / page layout recipes | [`docs/patterns/`](../patterns/) and [`docs/pages/`](../pages/) |
570
+ | Building a custom component to fill a gap | [Custom components](./custom-components.md) |
571
+
572
+ ### Semantic decisions
573
+
574
+ | Intent | Pick |
575
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
576
+ | 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-status-attention` |
578
+ | Confirmation / completed state | `Badge variant="success"`, or `bg-status-completed` |
579
+ | Neutral callout | `Badge variant="info"`, or the `Alert` component |
580
+ | Persistent panel (sidebar, header) | `shadow-sm` |
581
+ | Hovered / sticky surface | `shadow-md` |
582
+ | Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
583
+ | Modal / sheet / dialog | `shadow-lg`, `duration-300` entrance |
584
+ | Hover / focus transition | `duration-150 ease-out` |
585
+ | State change (toggle, select) | `duration-200 ease-in-out` |
586
+ | Two-column detail at <1024 | right column collapses below main — do not override |
587
+ | Inline ID, code, table number | `font-mono text-xs` (identifiers) / `tabular-nums` (figures) |
588
+ | Timestamp, label, subtle metadata | `text-xs text-muted-foreground` |
@@ -1,3 +1,5 @@
1
+ <!-- GENERATED by docs-kit from docs-src/guides/migrations.docs.outline.md — do not edit by hand. -->
2
+
1
3
  # Migrations
2
4
 
3
5
  Every change that requires you to edit your application before or after upgrading, newest first.
@@ -8,6 +10,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an
8
10
 
9
11
  | Version | Change |
10
12
  | ------------- | ---------------------------------------------------------------------------------------------------------------------------- |
13
+ | 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) |
11
14
  | 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) |
12
15
  | 1.11.0 | [React 19.2.7 and React Router v8 required](#1110-react-1927-and-react-router-v8-are-now-required) |
13
16
  | 1.11.0 | [Non-modal `Sheet` renders no backdrop](#1110-non-modal-sheet-no-longer-renders-a-backdrop) |
@@ -18,6 +21,37 @@ Each entry states which versions are affected, what breaks, how to detect it, an
18
21
  | 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) |
19
22
  | before 1.0 | [Pre-1.0 breaking changes](#before-10) |
20
23
 
24
+ ## 1.15.0: Base UI 1.8.0 breaks closed-`Select` and dialog-click tests
25
+
26
+ **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`.
27
+
28
+ 1.15.0 updates the bundled `@base-ui/react` from 1.6.0 to 1.8.0 (1.12.0 to 1.14.0 used 1.6.0). The AppShell API is unchanged and the app behaves the same for the people using it. Two upstream behaviour changes do break tests that relied on the old behaviour, so an app that changes nothing can still go red in CI after upgrading. The fix is in the tests, not the app.
29
+
30
+ ### `Select` options are not in the DOM until it opens
31
+
32
+ A closed `Select` used to render its options into the DOM. It no longer does, so a test that asserts option text with `getByText` on a closed `Select` fails with "Unable to find an element".
33
+
34
+ Open the `Select` first, then wait for the option:
35
+
36
+ ```tsx
37
+ await userEvent.click(screen.getByRole("combobox", { name: "Auto Renewal Period" }));
38
+ expect(await screen.findByText("1 Month")).toBeInTheDocument();
39
+ ```
40
+
41
+ ### Dialog scroll lock can move a button under a Playwright click
42
+
43
+ Opening a `Dialog` or `AlertDialog` applies a scroll lock that shifts the layout slightly. Playwright can judge a button stable and then click where it used to be, so the click lands on the backdrop and closes the dialog. The test then times out waiting for the dialog, and the Playwright call log shows the click being retried after "element was detached from the DOM".
44
+
45
+ Press the button instead of clicking it. `press` does not use coordinates, so the layout shift cannot affect it:
46
+
47
+ ```ts
48
+ const confirm = page.getByRole("alertdialog").getByRole("button", { name: "Delete" });
49
+ await expect(confirm).toBeVisible();
50
+ await confirm.press("Enter");
51
+ ```
52
+
53
+ Do not reach for `click({ force: true })`. It only skips Playwright's stability check, and the click can still miss.
54
+
21
55
  ## 1.12.0: `DateField` / `DatePicker` field chrome moved to `Field.Root`
22
56
 
23
57
  **Applies to:** apps passing `label`, `description`, `errorMessage`, or `hideTimeZone` to `DateField` or `DatePicker`.
@@ -1,34 +1,11 @@
1
1
  ---
2
- slug: page/detail
3
- name: Detail Page
4
- category: page
2
+ title: Document Detail Page
5
3
  description: The screen for a single record — its summary, its contents, the records around it, and the actions available on it
6
- requiredImports:
7
- [
8
- Layout,
9
- DescriptionCard,
10
- Card,
11
- Table,
12
- Badge,
13
- Alert,
14
- ActionPanel,
15
- ActivityCard,
16
- Grid,
17
- MetricCard,
18
- DocumentProgressCard,
19
- ]
20
- tags: [detail, record, document, single-record, actions, related-records, two-column, line-items]
21
- do:
22
- - A screen showing exactly one record — an order, a receipt, an invoice, a supplier, an item
23
- - The record has a lifecycle someone moves it through, or other records created from it
24
- - Someone reading the record needs to understand its current state, and may want to act on it
25
- dont:
26
- - The screen shows a set of records — that is a collection
27
- - The record is a row's worth of fields, better opened in a Sheet from the list it belongs to
28
- - Creating a new record — that is a form; this pattern is for reading and amending one that already exists
29
4
  ---
30
5
 
31
- # page/detail
6
+ <!-- GENERATED by docs-kit from docs-src/pages/document-detail.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Document Detail Page
32
9
 
33
10
  The screen for a **single record**. Two columns: the record itself on the left,
34
11
  and on the right the actions available on it plus the context that sits around