@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
@@ -1,127 +1,176 @@
1
1
  # Design System
2
2
 
3
- Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, `**astw:`** rules, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `**components.md`\*\*; that split avoids duplicating tables and lengthy examples across both docs.
3
+ Authority for **visual-only** decisions — tokens, theme imports, breakpoints intent, the `astw:` prefix, and custom-component conformance. For **React component APIs** (imports, props, JSX composition), pair this file with `components.md`; that split avoids duplicating tables and lengthy examples across both docs.
4
4
 
5
- `@tailor-platform/app-shell` (ERP scaffolds target **≥0.36**; bump your app’s pinned version deliberately) ships an opinionated design system via CSS variables and a `theme.css` import. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
5
+ `@tailor-platform/app-shell` ships an opinionated design system as CSS variables, bridged into Tailwind v4's token namespace. Use it whether you are consuming AppShell components (most cases) or building a custom component to fill a gap.
6
6
 
7
- **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. Every visual value you reach for must resolve to a token in this file. If a token is missing, add one; never inline.
7
+ **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
+
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.
8
10
 
9
11
  ## 1. Setup
10
12
 
11
- Authoritative app wiring also lives in `**project-setup.md`**; **scaffold `index.css`\*\* currently does:
13
+ The app's CSS entrypoint (`index.css` / `globals.css`) needs exactly this:
12
14
 
13
15
  ```css
14
16
  @import "tailwindcss";
15
- @import "tw-animate-css";
16
-
17
17
  @import "@tailor-platform/app-shell/styles";
18
- @import "@tailor-platform/app-shell/theme.css";
19
- ```
20
18
 
21
- Adjust if your App Shell version documents a different barrel filename, but keep this split:
19
+ /* Optional: at most one palette override, imported AFTER styles */
20
+ @import "@tailor-platform/app-shell/themes/bloom";
21
+ ```
22
22
 
23
- - `**theme.css**` — design tokens as CSS variables on `:root`.
24
- - `**styles**` (package export) — bundled component styles AppShell ships for primitives.
25
- - `**tailwindcss**` — utilities; token-backed classes (`bg-surface-1`, `text-fg-muted`) resolve through the theme.
23
+ - **`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.
25
+ - **`@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
26
 
27
- Older docs referred to `app-shell.css`; prefer the `**styles**` import the template uses.
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`.
28
28
 
29
- Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `**project-setup.md**`.
29
+ Tailwind v4 stays CSS-first; minimal `vite` / PostCSS wiring is in `project-setup.md`.
30
30
 
31
31
  ## 2. Theming via CSS variables
32
32
 
33
- AppShell controls its theme through CSS variables. Override them in `:root` (global) or a scoped selector (per-section, per-tenant, dark mode) to customize. Any token defined in `theme.css` can be overridden after the import.
33
+ Tokens exist in two layers, and knowing which one you are touching matters:
34
+
35
+ 1. **Raw CSS variables** (`--background`, `--primary`, `--radius`) — defined on `:root` in `themes/*.css`. These are what you **override**.
36
+ 2. **The Tailwind bridge** (`@theme inline` in `theme.bridge.css`) — maps each raw variable into Tailwind's namespace (`--background` → `--color-background`), which is what makes `bg-background` a real utility. You do **not** edit this layer.
37
+
38
+ Override raw variables after the `styles` import, using `:root` for light and `:root.dark` for dark. Set every override in both modes — overriding just `:root` misbehaves either way: on the default palette the light value carries into dark mode, and on a branded palette the override stops applying in dark mode altogether.
34
39
 
35
40
  ```css
36
41
  :root {
37
- --color-primary: #3b82f6;
38
- --color-background: #ffffff;
42
+ --primary: #3b82f6;
39
43
  }
40
44
 
41
- [data-theme="dark"] {
42
- --color-background: #0a0a0a;
43
- --color-foreground: #fafafa;
45
+ :root.dark {
46
+ --primary: #60a5fa;
44
47
  }
45
48
  ```
46
49
 
47
- Override at the highest scope where the change applies. Do not duplicate token values across files — change them at the source.
50
+ Use `:root.dark`, not a bare `.dark`. The default palette is imported inside a cascade layer (`layer(theme.defaults)`), so any unlayered declaration of yours beats it — but the branded palettes (`cream`, `bloom`) are imported unlayered and define their dark values on `:root.dark`, which outranks `.dark`. A `.dark` override would silently lose against those.
51
+
52
+ Override at the highest scope where the change applies. A narrower scope (per-section, per-tenant) needs the same both-modes treatment, and its dark rule must still outrank a branded palette's `:root.dark` — pair `.tenant-a` with `:root.dark .tenant-a`. Do not duplicate token values across files — change them at the source. Never copy the palette wholesale; tokens you did not copy stay on AppShell's values, so the two halves drift apart and any surface AppShell adds later has no value in the copy at all.
48
53
 
49
- **Dark mode** is supported via `[data-theme="dark"]` on the root element. AppShell primitives respect it automatically. Custom components inherit dark-mode behaviour for free as long as they reference tokens (`bg-surface-1`, `text-fg-default`) and never inline literal colors.
54
+ **Do not paste a `@theme inline` block, a `@custom-variant dark` rule, or a copy of AppShell's palette into the app's entry CSS.** `styles` provides all three. A copy of any of them in the app's own CSS is unlayered, so it beats AppShell's layered palette and silently breaks dark mode — the build succeeds and nothing warns.
55
+
56
+ **Dark mode is a `.dark` class on `<html>`**, not a data attribute. AppShell's theme provider toggles `document.documentElement.classList` between `light` and `dark` and persists the choice under the `appshell-ui-theme` localStorage key; the bundled `AppearanceSwitcher` component drives it. The bridge registers `@custom-variant dark (&:where(.dark, .dark *))`, so the `dark:` variant works in your own markup.
57
+
58
+ 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.
50
59
 
51
60
  ## 3. Component styling with data attributes
52
61
 
53
- 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:
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:
54
63
 
55
64
  ```css
56
65
  /* Style a component based on its state */
57
66
  .SwitchThumb[data-checked] {
58
- background-color: green;
67
+ background-color: var(--status-completed);
59
68
  }
60
69
 
61
70
  .MenuItem[data-highlighted] {
62
- background-color: var(--color-primary);
63
- color: white;
71
+ background-color: var(--accent);
72
+ color: var(--accent-foreground);
64
73
  }
65
74
  ```
66
75
 
67
76
  This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays:
68
77
 
69
78
  ```tsx
70
- <Switch.Thumb className="bg-surface-3 data-[checked]:bg-primary" />
79
+ <Switch.Thumb className="bg-muted data-[checked]:bg-primary" />
71
80
  ```
72
81
 
73
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).
74
83
 
75
84
  ## 4. Tokens
76
85
 
77
- All values below are exposed by `theme.css`. **Use the token, never hand-type the value.** A hex literal or magic px in a PR is a review failure.
86
+ 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.
78
87
 
79
88
  ### Color
80
89
 
81
- Four families. Pick by **intent**, not by visual taste.
82
-
83
- | Family | Token | Use | Tailwind |
84
- | ---------- | ------------------------ | ----------------------------------- | ----------------------------- |
85
- | Surface | `--color-surface-1` | page background | `bg-surface-1` |
86
- | Surface | `--color-surface-2` | card on page | `bg-surface-2` |
87
- | Surface | `--color-surface-3` | nested card on card | `bg-surface-3` |
88
- | Foreground | `--color-fg-default` | primary text | `text-fg-default` |
89
- | Foreground | `--color-fg-muted` | secondary text, descriptions | `text-fg-muted` |
90
- | Foreground | `--color-fg-subtle` | tertiary text, captions, timestamps | `text-fg-subtle` |
91
- | Brand | `--color-primary` | primary buttons, links, focus rings | `bg-primary` / `text-primary` |
92
- | Brand | `--color-primary-hover` | brand hover state | `hover:bg-primary-hover` |
93
- | Brand | `--color-primary-active` | brand pressed state | `active:bg-primary-active` |
94
- | Status | `--color-danger` | destructive actions, errors | `bg-danger` / `text-danger` |
95
- | Status | `--color-warning` | non-blocking caution | `bg-warning` / `text-warning` |
96
- | Status | `--color-success` | confirmations, completed states | `bg-success` / `text-success` |
97
- | Status | `--color-info` | neutral callouts | `bg-info` / `text-info` |
90
+ Colors follow shadcn-style semantic naming: a surface token and its matching `-foreground` pair. Pick by **intent**, not by visual taste. Pair a background with its own foreground — `bg-card` goes with `text-card-foreground`.
91
+
92
+ #### Surface & chrome
93
+
94
+ | Token | Use | Tailwind |
95
+ | ------------------------------------ | ----------------------------- | ---------------------------------------- |
96
+ | `--background` / `--foreground` | page background, default text | `bg-background` / `text-foreground` |
97
+ | `--card` / `--card-foreground` | card and panel surfaces | `bg-card` / `text-card-foreground` |
98
+ | `--popover` / `--popover-foreground` | menus, popovers, tooltips | `bg-popover` / `text-popover-foreground` |
99
+ | `--muted` / `--muted-foreground` | subtle fills; secondary text | `bg-muted` / `text-muted-foreground` |
100
+ | `--border` | hairlines, dividers | `border-border` |
101
+ | `--input` | form control borders | `border-input` |
102
+ | `--ring` | focus rings | `ring-ring` / `outline-ring` |
103
+
104
+ Surfaces are named by **role**, not by depth — there is no numbered `surface-1/2/3` ladder, and no third tier of body text below `muted-foreground`. Stack depth with `background` → `card` → `muted` plus a border or shadow, and reach for `popover` when the surface actually floats.
105
+
106
+ #### Brand & action
107
+
108
+ | Token | Use | Tailwind |
109
+ | -------------------------------------------- | -------------------------------- | -------------------------------------------- |
110
+ | `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` |
111
+ | `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` |
112
+ | `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` |
113
+ | `--destructive` / `--destructive-foreground` | destructive actions, errors | `bg-destructive` / `text-destructive` |
114
+
115
+ There are no `-hover` or `-active` brand tokens. Express interaction states with Tailwind variants and opacity — `hover:bg-primary/90`, `active:bg-primary/80` — which is what AppShell's own components do.
116
+
117
+ #### Status
118
+
119
+ Five status colors, used for badge fills and status dots:
120
+
121
+ | Token | Use | Tailwind |
122
+ | -------------------- | ------------------------ | --------------------- |
123
+ | `--status-default` | none / not applicable | `bg-status-default` |
124
+ | `--status-neutral` | informational | `bg-status-neutral` |
125
+ | `--status-completed` | success, completed | `bg-status-completed` |
126
+ | `--status-attention` | warning, needs attention | `bg-status-attention` |
127
+ | `--status-danger` | error, blocked | `bg-status-danger` |
128
+
129
+ Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `neutral`) over applying these directly — the variants already pair fill and foreground correctly. Reach for the raw token only on custom surfaces.
130
+
131
+ #### Sidebar & charts
132
+
133
+ `--sidebar`, `--sidebar-foreground`, `--sidebar-border`, `--sidebar-primary(-foreground)`, `--sidebar-accent(-foreground)`, `--sidebar-ring` → `bg-sidebar`, `text-sidebar-foreground`, and so on. These let a palette tint the shell independently of page content.
134
+
135
+ `--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
136
+
137
+ #### Alerts
138
+
139
+ `--alert-{neutral,success,warning,error,info}-{background,foreground,foreground-muted,border}` — five variants × four slots, each bridged into Tailwind:
140
+
141
+ | Slot | Tailwind | Use for |
142
+ | ------------------ | ---------------------------------- | -------------------------- |
143
+ | `background` | `bg-alert-info-background` | Callout fill |
144
+ | `foreground` | `text-alert-info-foreground` | Primary text and icons |
145
+ | `foreground-muted` | `text-alert-info-foreground-muted` | Secondary/description text |
146
+ | `border` | `border-alert-info-border` | Callout border |
147
+
148
+ Prefer the `Alert` component — it already pairs the four slots per variant and supplies the icon. Reach for the utilities directly only on custom surfaces `Alert` doesn't cover, such as a status-highlighted table row or an inline note:
98
149
 
99
150
  ```tsx
100
- // Good — Button exposes a destructive variant; prefer variants over bolting tokens on className when available
101
- <div className="bg-surface-1 text-fg-default">…</div>
151
+ <div className="rounded-lg border bg-alert-warning-background text-alert-warning-foreground border-alert-warning-border">
152
+ …
153
+ </div>
154
+ ```
155
+
156
+ The `background` and `border` slots are already semi-transparent tints (~10% and ~20%), so an opacity modifier compounds rather than replaces — `bg-alert-info-background/50` lands near 5% alpha, not 50%.
157
+
158
+ ```tsx
159
+ // Good — semantic token pairs, and a variant where one exists
160
+ <div className="bg-card text-card-foreground">…</div>
102
161
  <Button variant="destructive">Delete</Button>
162
+ <Badge variant="warning">Pending review</Badge>
103
163
 
104
164
  // Bad — raw colors bypass the theme
105
165
  <div style={{ background: "#fff", color: "#111" }}>…</div>
106
- ```
107
166
 
108
- Never reach for raw color names. If a status doesn't fit, that's a content problem, not a token problem.
167
+ // Bad — these classes do not exist and render as nothing at all
168
+ <div className="bg-surface-1 text-fg-muted">…</div>
169
+ ```
109
170
 
110
171
  ### Spacing
111
172
 
112
- Linear scale on a 4px base. Padding, margin, and gap all come from this scale. Tailwind's `p-4`, `gap-2`, `mt-8` resolve to the same tokens.
113
-
114
- | Token | Value | Common use |
115
- | ------------ | ----- | ------------------------- |
116
- | `--space-0` | 0 | reset |
117
- | `--space-1` | 4px | tight icon-text gap |
118
- | `--space-2` | 8px | inline gap, small padding |
119
- | `--space-3` | 12px | row gap, button padding |
120
- | `--space-4` | 16px | card padding, default gap |
121
- | `--space-6` | 24px | section gap |
122
- | `--space-8` | 32px | major section gap |
123
- | `--space-12` | 48px | page section break |
124
- | `--space-16` | 64px | hero spacing |
173
+ **AppShell defines no spacing tokens.** Use Tailwind's default 4px-based scale (`p-4`, `gap-2`, `mt-8`) — the same scale AppShell's own components use internally. There is no `--space-*` variable.
125
174
 
126
175
  ```tsx
127
176
  // Good — scale step
@@ -135,106 +184,112 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth
135
184
 
136
185
  ### Typography
137
186
 
138
- Named roles. Each token bundles font-size + line-height + weight. Pick by **role**, not by size. Don't set `font-size` and `line-height` independently.
139
-
140
- | Token | Tailwind | Use |
141
- | --------- | -------------- | ---------------------------- |
142
- | `display` | `text-display` | hero / marketing surfaces |
143
- | `h1` | `text-h1` | page title |
144
- | `h2` | `text-h2` | section heading |
145
- | `h3` | `text-h3` | subsection / card title |
146
- | `h4` | `text-h4` | nested heading |
147
- | `body-lg` | `text-body-lg` | emphasised body |
148
- | `body` | `text-body` | default body copy |
149
- | `body-sm` | `text-body-sm` | dense rows, secondary copy |
150
- | `caption` | `text-caption` | timestamps, labels, metadata |
151
- | `mono` | `text-mono` | IDs, code, numbers in tables |
187
+ **AppShell defines no typography scale tokens.** There is no `text-h1`, `text-body`, or `text-caption`. The only typography token is `--font-sans` (Inter Variable for Latin, Noto Sans JP Variable for Japanese) → `font-sans`, which the base layer already applies to `body`. Both are variable fonts, so every weight token is real in both scripts. Override the whole stack by setting `--app-shell-font-sans` on `:root`.
188
+
189
+ Compose roles from stock Tailwind utilities. These pairings are what AppShell's own components use — match them so your screens sit consistently alongside the primitives:
190
+
191
+ | Role | Utilities |
192
+ | --------------------------------- | ----------------------------------------- |
193
+ | Page title (`Layout.Header`) | `text-2xl font-bold tracking-tight` |
194
+ | Section heading | `text-lg font-semibold` |
195
+ | Card title | `text-lg font-semibold leading-none` |
196
+ | Body copy | `text-sm` |
197
+ | Secondary copy, descriptions | `text-sm text-muted-foreground` |
198
+ | Caption, timestamp, metadata | `text-xs text-muted-foreground` |
199
+ | Numeric value in a table or field | `text-sm font-medium tabular-nums` |
200
+ | ID, code, keyboard hint | `font-mono text-xs text-muted-foreground` |
152
201
 
153
202
  ```tsx
154
- <h2 className="text-h2">Section</h2>
155
- <p className="text-body text-fg-muted">Description copy</p>
156
- <span className="text-caption text-fg-subtle">Updated 2h ago</span>
203
+ <h2 className="text-lg font-semibold">Section</h2>
204
+ <p className="text-sm text-muted-foreground">Description copy</p>
205
+ <span className="text-xs text-muted-foreground">Updated 2h ago</span>
157
206
  ```
158
207
 
208
+ Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
209
+
159
210
  ### Radius
160
211
 
161
- Pick by component role. A card is always `md`, regardless of its size on screen.
212
+ `--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.
162
213
 
163
- | Token | Use |
164
- | --------------- | ------------------- |
165
- | `--radius-sm` | inputs, small chips |
166
- | `--radius-md` | cards, buttons |
167
- | `--radius-lg` | modals, sheets |
168
- | `--radius-xl` | large surfaces |
169
- | `--radius-full` | pills, avatars |
214
+ | Token | Value | Use | Tailwind |
215
+ | ------------- | -------------- | ------------------- | ------------ |
216
+ | `--radius-sm` | `radius − 4px` | inputs, small chips | `rounded-sm` |
217
+ | `--radius-md` | `radius − 2px` | buttons, cards | `rounded-md` |
218
+ | `--radius-lg` | `radius` | modals, sheets | `rounded-lg` |
219
+ | `--radius-xl` | `radius + 4px` | large surfaces | `rounded-xl` |
170
220
 
171
- ### Elevation
221
+ `rounded-full` (pills, avatars) is a stock Tailwind utility — there is no `--radius-full` token, but the class works.
172
222
 
173
- Higher elevation reads as "more transient" — match the component's lifetime. Never hand-craft a `box-shadow`.
223
+ Changing `--radius` alone rescales all four steps together.
174
224
 
175
- | Token | Use |
176
- | --------------- | ------------------------ |
177
- | `--elevation-0` | flat surface |
178
- | `--elevation-1` | persistent panel, card |
179
- | `--elevation-2` | sticky bar, hovered card |
180
- | `--elevation-3` | popover, menu |
181
- | `--elevation-4` | modal, dialog, sheet |
225
+ ### Shadow
182
226
 
183
- ### Motion
227
+ Four mode-aware shadows. There are **no `--elevation-*` tokens**; the scale is expressed as shadows. Never hand-craft a `box-shadow`.
184
228
 
185
- Duration paired with easing. Match motion to the change's lifetime — short events get short durations.
229
+ | Token | Bridged to | Use |
230
+ | ---------------------- | ----------- | --------------------------------- |
231
+ | `--semantic-shadow-xs` | `shadow-xs` | hairline lift, active nav item |
232
+ | `--semantic-shadow-sm` | `shadow-sm` | cards, persistent panels |
233
+ | `--semantic-shadow-md` | `shadow-md` | popovers, menus, hovered surfaces |
234
+ | `--semantic-shadow-lg` | `shadow-lg` | modals, dialogs, sheets |
186
235
 
187
- | Token | ~Duration | Use |
188
- | ----------------- | --------- | ------------------------------ |
189
- | `--motion-fast` | 120ms | hover, focus, button press |
190
- | `--motion-base` | 200ms | state changes (toggle, select) |
191
- | `--motion-slow` | 320ms | entrance, dialog open |
192
- | `--motion-slower` | 500ms | full-page transitions |
193
- | `--ease-out` | — | entrances, reveals |
194
- | `--ease-in-out` | — | symmetric state changes |
236
+ Higher shadow reads as "more transient" — match it to the component's lifetime. Each token carries a different value in dark mode, so using the token (rather than a literal) is what keeps depth legible on both backgrounds.
195
237
 
196
- ```css
197
- .menu-item {
198
- transition: background-color var(--motion-fast) var(--ease-out);
199
- }
238
+ ### Motion
200
239
 
201
- @media (prefers-reduced-motion: reduce) {
202
- .menu-item {
203
- transition: none;
204
- }
205
- }
240
+ **AppShell defines no motion tokens.** There is no `--motion-fast` or `--ease-out` variable. Use Tailwind's `duration-*` and `ease-*` utilities. AppShell uses `tw-animate-css` internally for its own enter/exit animations, but those classes are prefixed and are not available to your app — add `@import "tw-animate-css";` to your own entrypoint if you want them.
241
+
242
+ | Intent | Utilities |
243
+ | ----------------------------- | -------------------------- |
244
+ | Hover, focus, button press | `duration-150 ease-out` |
245
+ | State change (toggle, select) | `duration-200 ease-in-out` |
246
+ | Entrance, dialog open | `duration-300 ease-out` |
247
+
248
+ ```tsx
249
+ <div className="transition-colors duration-150 ease-out motion-reduce:transition-none" />
206
250
  ```
207
251
 
208
- Always wrap motion in `@media (prefers-reduced-motion: reduce)` and collapse to instant or near-instant transitions. AppShell components handle this internally; custom components must do the same.
252
+ Always give motion a reduced-motion escape (`motion-reduce:transition-none`, or a `@media (prefers-reduced-motion: reduce)` block in CSS). AppShell components handle this internally; custom components must do the same.
209
253
 
210
254
  ### Z-index
211
255
 
212
- Never invent a z value. If you need a new layer, add a token; never `z-index: 9999`. Popups and overlays share `50` intentionally — sequencing comes from DOM order, not z escalation.
256
+ These exist as raw `:root` variables in the shipped base layer, but are **not** bridged into Tailwind — `z-sidebar` is not a class. Use the stock numeric utility, or reference the variable when you need it to track AppShell's layering:
213
257
 
214
258
  | Token | Value | Use |
215
259
  | ------------------ | ----- | ----------------------------- |
216
260
  | `--z-sidebar` | 10 | persistent sidebar |
217
- | `--z-sidebar-rail` | 10 | sidebar collapsed rail |
261
+ | `--z-sidebar-rail` | 20 | sidebar collapsed rail |
218
262
  | `--z-popup` | 50 | menu, tooltip, popover |
219
263
  | `--z-overlay` | 50 | modal, sheet, dialog backdrop |
220
264
 
221
- ### Icon sizes
265
+ ```tsx
266
+ <div className="z-50" />
267
+ <div className="z-[var(--z-popup)]" />
268
+ ```
269
+
270
+ Never invent a z value — `z-index: 9999` is always wrong. Popups and overlays share `50` intentionally: sequencing comes from DOM order, not z escalation.
222
271
 
223
- Pair icon size with the surrounding text scale. Pass via the `size` prop, not raw width/height.
272
+ ### Icons
224
273
 
225
- | Token | Pairs with text |
226
- | ----------- | --------------------- |
227
- | `--icon-sm` | `body-sm`, `caption` |
228
- | `--icon-md` | `body`, `body-lg` |
229
- | `--icon-lg` | `h3`, `h4` |
230
- | `--icon-xl` | `h1`, `h2`, `display` |
274
+ **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:
275
+
276
+ | Text size | Icon class |
277
+ | ------------------ | ---------- |
278
+ | `text-xs` | `size-3` |
279
+ | `text-sm` | `size-4` |
280
+ | `text-lg` | `size-5` |
281
+ | `text-2xl` (title) | `size-6` |
231
282
 
232
283
  ```tsx
233
- <Icon name="check" size="md" />
284
+ import { Check } from "lucide-react";
285
+
286
+ <Check className="size-4" />;
234
287
  ```
235
288
 
236
289
  ### Breakpoints
237
290
 
291
+ Stock Tailwind breakpoints — AppShell does not change them.
292
+
238
293
  | Token | Width |
239
294
  | ----- | ------ |
240
295
  | `sm` | 640px |
@@ -245,26 +300,25 @@ Pair icon size with the surrounding text scale. Pass via the `size` prop, not ra
245
300
 
246
301
  **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.
247
302
 
248
- Two-column **behavior** (right rail stacks under `**lg`**): respect AppShell defaults — do not force side-by-side grids on narrow viewports. `**Layout`column width table** numbers live in`**components.md` → Layout**; reuse them instead of guessing rem values here.
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.
249
304
 
250
305
  ## 5. The `astw:` prefix
251
306
 
252
- AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName`, `contentClassName`, `className` on roots. Prefix those utilities with `**astw:`\*\* so they apply to the wrapper AppShell controls.
307
+ AppShell exposes **layout / sizing / overflow** escapes on some components via props like `containerClassName` and `className` on roots. Prefix those utilities with `astw:` so they apply to the wrapper AppShell controls.
253
308
 
254
- **Do not duplicate full component trees here.** Typical patterns (full `**DataTable`** composition, `**Sheet`+ footer**,`**Table.Root` + card insets**) live in `**components.md`\*\* with JSX you can copy.
309
+ **Do not duplicate full component trees here.** Typical patterns (full `DataTable` composition, `Sheet` + footer, `Table.Root` + card insets) live in `components.md` with JSX you can copy.
255
310
 
256
- Minimal illustrations — same rules apply to other `*ClassName` hooks:
311
+ Minimal illustration — the same rules apply to other `*ClassName` hooks:
257
312
 
258
313
  ```tsx
259
- <Table.Root containerClassName="astw:px-6 astw:max-h-96 astw:overflow-y-auto" />
260
- <Sheet contentClassName="astw:w-[480px] astw:flex astw:flex-col astw:gap-4" />
314
+ <Table.Root containerClassName="astw:px-6 astw:overflow-y-auto" />
261
315
  ```
262
316
 
263
317
  Rules:
264
318
 
265
- - `**astw:**` only on AppShell `*ClassName` / root `className` hooks each component exposes. Use **plain** Tailwind (`flex`, `gap-4`, `bg-surface-1`, …) on **your** markup.
319
+ - `astw:` only on AppShell `*ClassName` / root `className` hooks each component exposes. Use **plain** Tailwind (`flex`, `gap-4`, `bg-background`, …) on **your** markup.
266
320
  - Stick to **layout** utilities (`flex`, `grid`, `max-h-*`, `min-h-0`, `overflow-*`, widths). Avoid painting over internal AppShell padding or colors via `astw:` — prefer an upstream prop or composition change.
267
- - Steps like `**astw:p-4`\*\* still resolve through tokens — never arbitrary `astw:p-[13px]`.
321
+ - Steps like `astw:p-4` still resolve through the scale — never arbitrary `astw:p-[13px]`.
268
322
 
269
323
  ## 6. When AppShell doesn't have a component you need
270
324
 
@@ -272,15 +326,15 @@ Most ERP screens compose entirely from AppShell primitives. When you hit a gap,
272
326
 
273
327
  ### Decision tree
274
328
 
275
- 1. **Can you compose existing AppShell primitives?** A "card with metric and trend arrow" is `Card` + `Stat` + `Icon`, not a new component. Compose first.
329
+ 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.
276
330
  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.
277
331
  3. **If it's already proven reusable across 2+ apps**, skip local entirely — use the `build-component` skill to add it to AppShell directly.
278
332
 
279
333
  ### Conformance rules (non-negotiable for any custom component)
280
334
 
281
- - **Tokens only.** No hex literals, no magic px values, no hand-rolled shadows. Every visual property maps to a token from Section 4.
335
+ - **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.
282
336
  - **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.
283
- - **Compose AppShell primitives inside.** If the custom needs a button, use `Button` — not raw `<button>`. Same for `Input`, `Badge`, `Icon`. The custom's job is composition, not reinvention.
337
+ - **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.
284
338
  - **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.
285
339
  - **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.
286
340
 
@@ -288,7 +342,7 @@ Example skeleton for a local custom component:
288
342
 
289
343
  ```tsx
290
344
  // src/components/StepIndicator/index.tsx
291
- import { Icon } from "@tailor-platform/app-shell";
345
+ import { Check } from "lucide-react";
292
346
 
293
347
  type Props = { steps: string[]; current: number };
294
348
 
@@ -301,13 +355,13 @@ export function StepIndicator({ steps, current }: Props) {
301
355
  data-active={i === current ? "" : undefined}
302
356
  data-completed={i < current ? "" : undefined}
303
357
  className="
304
- flex items-center gap-2 px-3 py-2 rounded-md
305
- bg-surface-2 text-fg-muted text-body-sm
306
- data-[active]:bg-primary data-[active]:text-fg-default
307
- data-[completed]:text-success
358
+ flex items-center gap-2 rounded-md px-3 py-2
359
+ bg-muted text-sm text-muted-foreground
360
+ data-[active]:bg-primary data-[active]:text-primary-foreground
361
+ data-[completed]:text-status-completed
308
362
  "
309
363
  >
310
- {i < current && <Icon name="check" size="sm" />}
364
+ {i < current && <Check className="size-4" />}
311
365
  {label}
312
366
  </li>
313
367
  ))}
@@ -316,7 +370,7 @@ export function StepIndicator({ steps, current }: Props) {
316
370
  }
317
371
  ```
318
372
 
319
- Notice: tokens for every value, `data-*` attributes for state, no hex literals, AppShell `Icon` composed inside.
373
+ 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.
320
374
 
321
375
  ### Promotion path
322
376
 
@@ -338,21 +392,21 @@ When a custom component proves reusable across 2+ apps, promote it upstream into
338
392
 
339
393
  ### Semantic decisions
340
394
 
341
- | Intent | Pick |
342
- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
343
- | Destructive action (delete, void) | Prefer **`Button`** `variant="destructive"`; use danger tokens on custom surfaces; confirm in dialog at elevation `4` |
344
- | Non-blocking caution | `bg-warning` |
345
- | Confirmation / completed state | `bg-success` |
346
- | Neutral callout | `bg-info` |
347
- | Persistent panel (sidebar, header) | elevation `1` |
348
- | Hovered / sticky surface | elevation `2` |
349
- | Popover / menu / tooltip | elevation `3`, `--motion-fast` |
350
- | Modal / sheet / dialog | elevation `4`, `--motion-slow` entrance |
351
- | Hover / focus transition | `--motion-fast` |
352
- | State change (toggle, select) | `--motion-base` |
353
- | Two-column detail at <1024 | right column collapses below main — do not override |
354
- | Inline ID, code, table number | `text-mono` |
355
- | Timestamp, label, subtle metadata | `text-caption text-fg-subtle` |
395
+ | Intent | Pick |
396
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
397
+ | Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` |
398
+ | Non-blocking caution | `Badge variant="warning"`, or `bg-status-attention` |
399
+ | Confirmation / completed state | `Badge variant="success"`, or `bg-status-completed` |
400
+ | Neutral callout | `Badge variant="info"`, or the `Alert` component |
401
+ | Persistent panel (sidebar, header) | `shadow-sm` |
402
+ | Hovered / sticky surface | `shadow-md` |
403
+ | Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
404
+ | Modal / sheet / dialog | `shadow-lg`, `duration-300` entrance |
405
+ | Hover / focus transition | `duration-150 ease-out` |
406
+ | State change (toggle, select) | `duration-200 ease-in-out` |
407
+ | Two-column detail at <1024 | right column collapses below main — do not override |
408
+ | Inline ID, code, table number | `font-mono text-xs` (identifiers) / `tabular-nums` (figures) |
409
+ | Timestamp, label, subtle metadata | `text-xs text-muted-foreground` |
356
410
 
357
411
  ### Composition & emphasis rules
358
412
 
@@ -366,9 +420,9 @@ These are visual-composition rules every screen must follow, regardless of patte
366
420
  - **Secondary statuses** (delivery, billing, fulfilment) and dense supporting columns → **`outline-*`** (with status dot).
367
421
  - **Tags / labels** ("New", "Returned") → **`subtle-*`**.
368
422
  - 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`.)
369
- - **Color:** status colors (`success`/`warning`/`error`/`info`) signal meaning, not decoration — don't tint neutral content.
423
+ - **Color:** status colors signal meaning, not decoration — don't tint neutral content.
370
424
 
371
- **Hierarchy.** One `h1` per page (the `Layout.Header` title). Section headings step down (`h2` → `h3`); never skip levels for size — pick the role token (`design-system.md` §4 Typography), not the pixel size.
425
+ **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.
372
426
 
373
427
  **States — never ship only the happy path.** Every data-backed screen handles:
374
428