@tailor-platform/app-shell 1.15.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +811 -131
  3. package/dist/app-shell.js +7155 -5797
  4. package/dist/{appshell-DLVBFo5J.js → appshell-CahhZy7v.js} +656 -524
  5. package/dist/semantic-contrast.test.ts +154 -0
  6. package/dist/style-entry.d.ts +147 -92
  7. package/dist/testing.d.ts +152 -95
  8. package/dist/testing.js +1 -1
  9. package/dist/theme-bridge.test.ts +101 -1
  10. package/dist/theme.bridge.css +91 -0
  11. package/dist/theme.css +6 -1
  12. package/dist/themes/_template.css +7 -1
  13. package/dist/themes/bloom.css +0 -6
  14. package/dist/themes/cream.css +0 -6
  15. package/dist/themes/default.css +389 -69
  16. package/dist/vite-plugin.d.ts +147 -92
  17. package/package.json +8 -8
  18. package/skills/app-shell-patterns/SKILL.md +108 -19
  19. package/skills/app-shell-patterns/references/api/create-ai-gateway-client.md +115 -0
  20. package/skills/app-shell-patterns/references/api/create-typed-paths.md +117 -0
  21. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +143 -0
  22. package/skills/app-shell-patterns/references/api/define-module.md +247 -0
  23. package/skills/app-shell-patterns/references/api/define-resource.md +307 -0
  24. package/skills/app-shell-patterns/references/api/guards/hidden.md +202 -0
  25. package/skills/app-shell-patterns/references/api/guards/overview.md +354 -0
  26. package/skills/app-shell-patterns/references/api/guards/pass.md +129 -0
  27. package/skills/app-shell-patterns/references/api/guards/redirect-to.md +307 -0
  28. package/skills/app-shell-patterns/references/api/router/use-location.md +240 -0
  29. package/skills/app-shell-patterns/references/api/router/use-navigate.md +138 -0
  30. package/skills/app-shell-patterns/references/api/router/use-params.md +218 -0
  31. package/skills/app-shell-patterns/references/api/use-ai-chat.md +110 -0
  32. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +132 -0
  33. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +150 -0
  34. package/skills/app-shell-patterns/references/api/use-app-shell.md +150 -0
  35. package/skills/app-shell-patterns/references/api/use-auth.md +145 -0
  36. package/skills/app-shell-patterns/references/api/use-override-breadcrumb.md +74 -0
  37. package/skills/app-shell-patterns/references/api/use-page-meta.md +96 -0
  38. package/skills/app-shell-patterns/references/api/use-register-command-palette-actions.md +90 -0
  39. package/skills/app-shell-patterns/references/api/use-resolved-locale.md +67 -0
  40. package/skills/app-shell-patterns/references/api/use-route-error.md +214 -0
  41. package/skills/app-shell-patterns/references/api/use-theme.md +108 -0
  42. package/skills/app-shell-patterns/references/api/use-time-zone.md +80 -0
  43. package/skills/app-shell-patterns/references/api/use-toast.md +104 -0
  44. package/skills/app-shell-patterns/references/components/action-panel.md +168 -0
  45. package/skills/app-shell-patterns/references/components/activity-card.md +142 -0
  46. package/skills/app-shell-patterns/references/components/ai-chat.md +315 -0
  47. package/skills/app-shell-patterns/references/components/alert.md +202 -0
  48. package/skills/app-shell-patterns/references/components/app-shell.md +530 -0
  49. package/skills/app-shell-patterns/references/components/appearance-switcher.md +87 -0
  50. package/skills/app-shell-patterns/references/components/attachment.md +138 -0
  51. package/skills/app-shell-patterns/references/components/autocomplete.md +278 -0
  52. package/skills/app-shell-patterns/references/components/avatar.md +80 -0
  53. package/skills/app-shell-patterns/references/components/badge.md +309 -0
  54. package/skills/app-shell-patterns/references/components/button.md +164 -0
  55. package/skills/app-shell-patterns/references/components/card.md +181 -0
  56. package/skills/app-shell-patterns/references/components/checkbox.md +169 -0
  57. package/skills/app-shell-patterns/references/components/combobox.md +318 -0
  58. package/skills/app-shell-patterns/references/components/command-palette.md +413 -0
  59. package/skills/app-shell-patterns/references/components/csv-importer.md +214 -0
  60. package/skills/app-shell-patterns/references/components/data-table.md +1168 -0
  61. package/skills/app-shell-patterns/references/components/date-picker.md +259 -0
  62. package/skills/app-shell-patterns/references/components/default-header.md +75 -0
  63. package/skills/app-shell-patterns/references/components/default-sidebar.md +136 -0
  64. package/skills/app-shell-patterns/references/components/description-card.md +569 -0
  65. package/skills/app-shell-patterns/references/components/dialog.md +164 -0
  66. package/skills/app-shell-patterns/references/components/document-progress-card.md +154 -0
  67. package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
  68. package/skills/app-shell-patterns/references/components/form.md +378 -0
  69. package/skills/app-shell-patterns/references/components/global-header-layout.md +132 -0
  70. package/skills/app-shell-patterns/references/components/grid.md +184 -0
  71. package/skills/app-shell-patterns/references/components/input.md +83 -0
  72. package/skills/app-shell-patterns/references/components/layout.md +539 -0
  73. package/skills/app-shell-patterns/references/components/menu.md +218 -0
  74. package/skills/app-shell-patterns/references/components/metric-card.md +94 -0
  75. package/skills/app-shell-patterns/references/components/select.md +283 -0
  76. package/skills/app-shell-patterns/references/components/sheet.md +237 -0
  77. package/skills/app-shell-patterns/references/components/sidebar-group.md +472 -0
  78. package/skills/app-shell-patterns/references/components/sidebar-item.md +458 -0
  79. package/skills/app-shell-patterns/references/components/sidebar-layout.md +258 -0
  80. package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
  81. package/skills/app-shell-patterns/references/components/spinner.md +81 -0
  82. package/skills/app-shell-patterns/references/components/table.md +188 -0
  83. package/skills/app-shell-patterns/references/components/tabs.md +158 -0
  84. package/skills/app-shell-patterns/references/components/textarea.md +122 -0
  85. package/skills/app-shell-patterns/references/components/timeline.md +317 -0
  86. package/skills/app-shell-patterns/references/components/toolbar.md +138 -0
  87. package/skills/app-shell-patterns/references/components/tooltip.md +144 -0
  88. package/skills/app-shell-patterns/references/components/with-guard.md +445 -0
  89. package/skills/app-shell-patterns/references/concepts/authentication.md +301 -0
  90. package/skills/app-shell-patterns/references/concepts/custom-components.md +59 -0
  91. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +356 -0
  92. package/skills/app-shell-patterns/references/{fundamental → concepts}/graphql.md +31 -18
  93. package/skills/app-shell-patterns/references/concepts/modules-and-resources.md +229 -0
  94. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +425 -0
  95. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +420 -0
  96. package/skills/app-shell-patterns/references/concepts/styling-theming.md +785 -0
  97. package/skills/app-shell-patterns/references/migrations.md +55 -0
  98. package/skills/app-shell-patterns/references/pages/{detail.md → document-detail.md} +10 -33
  99. package/skills/app-shell-patterns/references/patterns/form-composer.md +43 -45
  100. package/skills/app-shell-patterns/references/patterns/form-modal.md +37 -92
  101. package/skills/app-shell-patterns/references/patterns/form-sectioned.md +12 -38
  102. package/skills/app-shell-patterns/references/patterns/form-single-page.md +11 -32
  103. package/skills/app-shell-patterns/references/patterns/form-wizard.md +12 -53
  104. package/skills/app-shell-patterns/references/patterns/interaction-confirm.md +11 -27
  105. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +136 -139
  106. package/skills/app-shell-patterns/references/patterns/interaction-toast.md +10 -34
  107. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +13 -88
  108. package/skills/app-shell-patterns/references/fundamental/components.md +0 -1009
  109. package/skills/app-shell-patterns/references/fundamental/design-system.md +0 -440
@@ -0,0 +1,785 @@
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
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/concepts/styling-theming.docs.outline.md — do not edit by hand. -->
7
+
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.
11
+
12
+ **The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies.
13
+
14
+ **Every token in this document is verified against the shipped CSS.** If a token is not listed here, assume it does not exist. The colour scales (`--primitive-*`) are internal: they are not bridged to Tailwind and are not listed. Inventing a plausible-sounding token (`bg-surface-1`, `text-fg-muted`, `--space-4`) is the worst failure mode available: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled — no error, no warning, nothing in the console. When unsure, read `node_modules/@tailor-platform/app-shell/dist/themes/default.css`; it is the ground truth.
15
+
16
+ ## Setup
17
+
18
+ AppShell exports `@tailor-platform/app-shell/styles`. Your app's CSS entrypoint (`index.css` / `globals.css`) needs exactly this:
19
+
20
+ ```css
21
+ @import "tailwindcss";
22
+ @import "@tailor-platform/app-shell/styles";
23
+
24
+ /* Optional: at most one palette override, imported AFTER styles */
25
+ @import "@tailor-platform/app-shell/themes/bloom";
26
+ ```
27
+
28
+ - **`tailwindcss`** — your app's own Tailwind build, which generates the utilities you write in your components.
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.
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.
31
+
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`.
33
+
34
+ Tailwind v4 stays CSS-first; the `vite` / PostCSS wiring is minimal.
35
+
36
+ ## Theming with CSS variables
37
+
38
+ Tokens exist in two layers, and knowing which one you are touching matters:
39
+
40
+ 1. **Raw CSS variables** (`--background`, `--primary`, `--radius`) — defined on `:root` in `themes/*.css`. These are what you **override**.
41
+ 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.
42
+
43
+ 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.
44
+
45
+ ```css
46
+ :root {
47
+ --primary: #3b82f6;
48
+ }
49
+
50
+ :root.dark {
51
+ --primary: #60a5fa;
52
+ }
53
+ ```
54
+
55
+ 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.
56
+
57
+ 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.
58
+
59
+ **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.
60
+
61
+ **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.
62
+
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.
64
+
65
+ ## Theme Palettes
66
+
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`:
76
+
77
+ ```css
78
+ @import "tailwindcss";
79
+ @import "@tailor-platform/app-shell/styles";
80
+ @import "@tailor-platform/app-shell/themes/cream"; /* overrides default palette */
81
+ ```
82
+
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** | Inherit | Colour scales, status and alert tokens inherit from `default.css`. Override a colour role (`--{intent}-{role}`) only to recolour the status components |
96
+ | **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed |
97
+
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>
116
+ ```
117
+
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>
121
+
122
+ ```tsx
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
+ }
131
+ ```
132
+
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
+ ```
142
+
143
+ ## Tokens
144
+
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.
146
+
147
+ ### Color
148
+
149
+ 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`.
150
+
151
+ #### Surface & chrome
152
+
153
+ | Token | Use | Tailwind |
154
+ | ------------------------------------ | ----------------------------- | ---------------------------------------- |
155
+ | `--background` / `--foreground` | page background, default text | `bg-background` / `text-foreground` |
156
+ | `--card` / `--card-foreground` | card and panel surfaces | `bg-card` / `text-card-foreground` |
157
+ | `--popover` / `--popover-foreground` | menus, popovers, tooltips | `bg-popover` / `text-popover-foreground` |
158
+ | `--muted` / `--muted-foreground` | subtle fills; secondary text | `bg-muted` / `text-muted-foreground` |
159
+ | `--border` | hairlines, dividers | `border-border` |
160
+ | `--input` | form control borders | `border-input` |
161
+ | `--ring` | focus rings | `ring-ring` / `outline-ring` |
162
+
163
+ 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.
164
+
165
+ #### Brand & action
166
+
167
+ | Token | Use | Tailwind |
168
+ | -------------------------------------------- | -------------------------------- | -------------------------------------------- |
169
+ | `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` |
170
+ | `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` |
171
+ | `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` |
172
+ | `--destructive` / `--destructive-foreground` | destructive actions, errors | `bg-destructive` / `text-destructive` |
173
+
174
+ 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.
175
+
176
+ #### Status
177
+
178
+ **Deprecated.** These five tokens and their utilities remain for existing code and will be removed in the next major. New code uses the [semantic color roles](#semantic-color-roles) below. In the default palette they are aliases of those roles, so status colors and Alert colors agree:
179
+
180
+ | Token | Use | Tailwind | Use instead |
181
+ | -------------------- | ---------------------------------------------------- | --------------------- | ---------------------- |
182
+ | `--status-default` | none / not applicable (follows `--muted-foreground`) | `bg-status-default` | `bg-neutral-indicator` |
183
+ | `--status-neutral` | informational | `bg-status-neutral` | `bg-info-solid` |
184
+ | `--status-completed` | success, completed | `bg-status-completed` | `bg-success-solid` |
185
+ | `--status-attention` | warning, needs attention | `bg-status-attention` | `bg-warning-solid` |
186
+ | `--status-danger` | error, blocked | `bg-status-danger` | `bg-danger-solid` |
187
+
188
+ Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `neutral`) over applying a fill directly — the variants already pair fill and foreground correctly.
189
+
190
+ These are fill and indicator colors. `--status-default` follows `--muted-foreground`, so it is theme-dependent and translucent in `cream` and `bloom`; the four hue tokens are opaque in every palette. Do not use them as text color: `--status-completed`, `--status-danger` and `--status-neutral` are below 4.5:1 on a dark card, and `--status-attention` is below 3:1 on a light one. For text, use `text-{intent}-text`.
191
+
192
+ #### Semantic color roles
193
+
194
+ Each intent has eight roles. The intents are `info`, `success`, `warning`, `danger` and `neutral`. The hue intents read internal color scales; `neutral` reads the system tokens. Every role is a token named `--{intent}-{role}` and a Tailwind color named `{intent}-{role}`:
195
+
196
+ | Role | Tailwind | Use for |
197
+ | --------------- | ----------------------- | ----------------------------------- |
198
+ | `surface` | `bg-info-surface` | Soft background |
199
+ | `surface-hover` | `bg-info-surface-hover` | Hover state of a soft background |
200
+ | `border` | `border-info-border` | Border on a soft background |
201
+ | `solid` | `bg-info-solid` | Filled background |
202
+ | `solid-hover` | `bg-info-solid-hover` | Hover state of a filled background |
203
+ | `text` | `text-info-text` | Text and icons on a soft background |
204
+ | `contrast` | `text-info-contrast` | Text on a filled background |
205
+ | `indicator` | `bg-info-indicator` | Dots and small marks |
206
+
207
+ ```tsx
208
+ <span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
209
+ ```
210
+
211
+ In the default palette, `text` on `surface` and `contrast` on `solid` and `solid-hover` are at least 4.5:1 for the four hue intents, in light and dark mode, on `--card` and `--background`. A unit test checks this from the shipped CSS. `text` on `surface-hover` is held at 4.2:1 by the same test; it is below 4.5:1 for `danger` and `success` in light mode. The test does not cover the `cream` and `bloom` palettes or the `neutral` intent.
212
+
213
+ Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent.
214
+
215
+ `Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. They no longer follow `--destructive`, which still drives `Button` and `text-destructive`.
216
+
217
+ #### Sidebar & charts
218
+
219
+ `--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.
220
+
221
+ `--chart-1` … `--chart-5` → `bg-chart-1`, `text-chart-1`, `fill-chart-1`. Use them in order for categorical series.
222
+
223
+ #### Alerts
224
+
225
+ **Deprecated.** `--alert-{neutral,success,warning,error,info}-{background,foreground,foreground-muted,border}` and the matching utilities (`bg-alert-info-background`, `text-alert-info-foreground`, ...) remain for existing code and will be removed in the next major. In the default palette they are aliases of the semantic color roles: `background` is `surface`, `border` is `border`, `foreground` is `text`, and `foreground-muted` follows `foreground`; `error` reads the `danger` intent. The `Alert` component reads the roles, so overriding `--alert-*` changes these utilities but not `Alert`.
226
+
227
+ For new custom surfaces, use the roles instead of the alert slots:
228
+
229
+ ```tsx
230
+ <div className="rounded-lg border bg-warning-surface text-warning-text border-warning-border">
231
+ …
232
+ </div>
233
+ ```
234
+
235
+ Across all colour tokens:
236
+
237
+ ```tsx
238
+ // Good — semantic token pairs, and a variant where one exists
239
+ <div className="bg-card text-card-foreground">…</div>
240
+ <Button variant="destructive">Delete</Button>
241
+ <Badge variant="warning">Pending review</Badge>
242
+
243
+ // Bad — raw colors bypass the theme
244
+ <div style={{ background: "#fff", color: "#111" }}>…</div>
245
+
246
+ // Bad — these classes do not exist and render as nothing at all
247
+ <div className="bg-surface-1 text-fg-muted">…</div>
248
+ ```
249
+
250
+ ### Spacing
251
+
252
+ **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.
253
+
254
+ ```tsx
255
+ // Good — scale step
256
+ <div className="flex gap-3 p-4">…</div>
257
+
258
+ // Bad — magic value
259
+ <div className="p-[13px]">…</div>
260
+ ```
261
+
262
+ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if nothing fits, the layout is wrong, not the scale.
263
+
264
+ ### Typography
265
+
266
+ AppShell defines **composite typography role tokens**. A role sets font size, line height, font weight, and letter spacing together, so one utility such as `text-heading-md` replaces a set like `text-lg font-semibold`. The font family is a separate token, `--font-sans` (Inter Variable for Latin, Noto Sans JP Variable for Japanese) → `font-sans`, which the base layer already applies to `body`. Both are variable fonts, so every weight token is real in both scripts. Override the whole stack by setting `--app-shell-font-sans` on `:root`.
267
+
268
+ There are 8 core roles and 2 relaxed body variants:
269
+
270
+ | Role | Utility | Size / line height | Weight | Use |
271
+ | ----------------- | ---------------------- | ------------------ | ------ | ----------------------------------------------- |
272
+ | `heading-lg` | `text-heading-lg` | 24px / 32px | 700 | Page title (`Layout.Header`) |
273
+ | `heading-md` | `text-heading-md` | 18px / 28px | 600 | Section heading, card title |
274
+ | `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
275
+ | `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
276
+ | `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
277
+ | `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text, figures in a table |
278
+ | `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
279
+ | `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
280
+ | `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
281
+ | `body-sm-relaxed` | `text-body-sm-relaxed` | 12px / 20px | 400 | Long text at the small size |
282
+
283
+ `heading-lg` also sets letter spacing to `-0.025em`. All other roles set `0em`.
284
+
285
+ Pair a role with a color token from the Color section above. These pairings are the recommended combinations:
286
+
287
+ | Use | Utilities |
288
+ | --------------------------------- | ---------------------------------------------- |
289
+ | Page title (`Layout.Header`) | `text-heading-lg` |
290
+ | Section heading | `text-heading-md` |
291
+ | Card title (`div` or `span`) | `text-heading-md leading-none` |
292
+ | Body copy | `text-body-md` |
293
+ | Secondary copy, descriptions | `text-body-md text-muted-foreground` |
294
+ | Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
295
+ | Numeric value in a table or field | `text-label-md tabular-nums` |
296
+ | ID, code | `font-mono text-code-sm text-muted-foreground` |
297
+
298
+ <example-preview name="typography-roles"></example-preview>
299
+
300
+ ```tsx
301
+ function TypographyRoles() {
302
+ const rows = [
303
+ {
304
+ role: "heading-lg",
305
+ className: "text-heading-lg",
306
+ en: "Purchase orders",
307
+ ja: "発注管理",
308
+ },
309
+ {
310
+ role: "heading-md",
311
+ className: "text-heading-md",
312
+ en: "Purchase orders by supplier",
313
+ ja: "仕入先別の発注一覧",
314
+ },
315
+ {
316
+ role: "heading-sm",
317
+ className: "text-heading-sm",
318
+ en: "Delivery address",
319
+ ja: "納品先",
320
+ },
321
+ {
322
+ role: "body-md",
323
+ className: "text-body-md",
324
+ en: "The order was sent to the supplier on 12 March.",
325
+ ja: "発注書は3月12日に仕入先へ送付されました。",
326
+ },
327
+ {
328
+ role: "body-sm",
329
+ className: "text-body-sm text-muted-foreground",
330
+ en: "Updated 2 hours ago",
331
+ ja: "2時間前に更新",
332
+ },
333
+ {
334
+ role: "label-md",
335
+ className: "text-label-md",
336
+ en: "Order quantity",
337
+ ja: "発注数量",
338
+ },
339
+ {
340
+ role: "label-sm",
341
+ className: "text-label-sm",
342
+ en: "Draft",
343
+ ja: "下書き",
344
+ },
345
+ {
346
+ role: "code-sm",
347
+ className: "font-mono text-code-sm",
348
+ en: "PO-2026-000184",
349
+ ja: "PO-2026-000184",
350
+ },
351
+ {
352
+ role: "body-md-relaxed",
353
+ className: "text-body-md-relaxed",
354
+ compareWith: { role: "body-md", className: "text-body-md" },
355
+ en: "Please confirm the delivery date with the supplier before you approve this order. If the supplier cannot deliver by the requested date, change the date or choose another supplier. Approved orders are sent to the supplier by email.",
356
+ ja: "この発注を承認する前に、納期を仕入先に確認してください。希望日までに納品できない場合は、納期を変更するか、別の仕入先を選んでください。承認した発注は、メールで仕入先に送られます。",
357
+ },
358
+ {
359
+ role: "body-sm-relaxed",
360
+ className: "text-body-sm-relaxed text-muted-foreground",
361
+ compareWith: { role: "body-sm", className: "text-body-sm text-muted-foreground" },
362
+ en: "Changes to a confirmed order are recorded in the order history. Each record shows who made the change, when it was made, and the values before and after the change.",
363
+ ja: "確定済みの発注への変更は、発注履歴に記録されます。記録には、変更した人、変更した日時、変更前と変更後の値が含まれます。",
364
+ },
365
+ ];
366
+
367
+ return (
368
+ <div className="flex flex-col gap-4">
369
+ {rows.map(({ role, className, compareWith, en, ja }) => (
370
+ <div key={role} className="grid grid-cols-[9rem_1fr] items-baseline gap-4">
371
+ <span className="font-mono text-code-sm text-muted-foreground">{role}</span>
372
+ {compareWith ? (
373
+ // A relaxed role changes only line height, so show the same text in the base role first.
374
+ <div className="flex flex-col gap-4">
375
+ {[compareWith, { role, className }].map((variant) => (
376
+ <div key={variant.role} className="flex flex-col gap-1">
377
+ <span className="font-mono text-code-sm text-muted-foreground">
378
+ {variant.role}
379
+ </span>
380
+ <p className={variant.className}>{en}</p>
381
+ <p className={variant.className}>{ja}</p>
382
+ </div>
383
+ ))}
384
+ </div>
385
+ ) : (
386
+ <div className="flex flex-col gap-1">
387
+ <p className={className}>{en}</p>
388
+ <p className={className}>{ja}</p>
389
+ </div>
390
+ )}
391
+ </div>
392
+ ))}
393
+ </div>
394
+ );
395
+ }
396
+ ```
397
+
398
+ ```tsx
399
+ <h2 className="text-heading-md">Section</h2>
400
+ <p className="text-body-md text-muted-foreground">Description copy</p>
401
+ <span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
402
+ ```
403
+
404
+ On an `h1` to `h6` element, the `cream` and `bloom` palettes replace the role's line height and letter spacing, including `leading-none`; see "Change a role" below. AppShell's own components still use the stock utilities (`text-sm`, `font-medium`, and so on) today. They will move to the roles in later changes, so the two sets can appear side by side for now.
405
+
406
+ **Rules**
407
+
408
+ - **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
409
+ - **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size.
410
+ - **A role sets weight and letter spacing on its own element.** It does not inherit them from a parent. Put an emphasis or tracking class on the element that has the role, not on its parent.
411
+ - **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
412
+ - Compact line height: `leading-none`. Use it only for single-line text, such as a card title. Write it after the role when you merge with `cn()`.
413
+ - Tabular figures: `tabular-nums`.
414
+ - Emphasis on inline text: `font-medium` or `font-semibold`.
415
+ - Uppercase: `uppercase`.
416
+ - **Do not combine a role with other `leading-*`, `tracking-*`, or `text-[Npx]` values.** If none of the roles fits, use the nearest role.
417
+ - **12px is the minimum size.** Do not use smaller text.
418
+ - **`code-sm` needs `font-mono`.** The role sets size, line height, weight, and letter spacing, but a role cannot set the font family.
419
+ - **Use the relaxed variants for long text only.** For labels, table cells, and other short text, use the regular roles.
420
+
421
+ Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
422
+
423
+ **Change a role.** Each role is four CSS variables named `--app-shell-type-<role>-<size|line-height|weight|letter-spacing>`. Override them on `:root` to change a role for the whole app. Typography has no dark variant, so one override applies to light and dark mode.
424
+
425
+ ```css
426
+ :root {
427
+ --app-shell-type-heading-lg-size: 1.75rem;
428
+ --app-shell-type-heading-lg-line-height: calc(36 / 28);
429
+ }
430
+ ```
431
+
432
+ The `cream` and `bloom` palettes set `line-height` and `letter-spacing` on `h1` to `h6`. These rules are outside any `@layer`, so they win over a role utility, and over any `leading-*` or `tracking-*` class, on those elements. Under these palettes, an `h1` to `h6` element takes its line height (1.2) and letter spacing (`-0.03em`) from the palette. This includes `leading-none`: `<h3 className="text-heading-md leading-none">` has a line height of 1.2, while a `div` with the same classes has 1.0. Font size and weight still come from the role. The `default` palette has no such rule.
433
+
434
+ **`tailwind-merge`.** AppShell's `cn()` keeps a role together with a text color class, for example when you pass `className="text-label-md text-muted-foreground"` to an AppShell component. It cannot replace a size that a component sets itself, because component classes carry the internal `astw:` prefix (see "Why a plain utility can't override an AppShell default" below). If your app has its own `cn()` built on `tailwind-merge`, register the role names. Without this, `twMerge("text-label-md text-muted-foreground")` removes the role and keeps only the color.
435
+
436
+ ```ts
437
+ import { extendTailwindMerge } from "tailwind-merge";
438
+
439
+ const twMerge = extendTailwindMerge({
440
+ extend: {
441
+ classGroups: {
442
+ "font-size": [
443
+ {
444
+ text: [
445
+ "heading-lg",
446
+ "heading-md",
447
+ "heading-sm",
448
+ "body-md",
449
+ "body-sm",
450
+ "body-md-relaxed",
451
+ "body-sm-relaxed",
452
+ "label-md",
453
+ "label-sm",
454
+ "code-sm",
455
+ ],
456
+ },
457
+ ],
458
+ },
459
+ },
460
+ });
461
+ ```
462
+
463
+ ### Radius
464
+
465
+ `--radius` (`0.625rem`) is the base; the bridge derives four steps from it. Pick by component role — a card is always `md`, regardless of its size on screen.
466
+
467
+ | Token | Value | Use | Tailwind |
468
+ | ------------- | -------------- | ------------------- | ------------ |
469
+ | `--radius-sm` | `radius − 4px` | inputs, small chips | `rounded-sm` |
470
+ | `--radius-md` | `radius − 2px` | buttons, cards | `rounded-md` |
471
+ | `--radius-lg` | `radius` | modals, sheets | `rounded-lg` |
472
+ | `--radius-xl` | `radius + 4px` | large surfaces | `rounded-xl` |
473
+
474
+ `rounded-full` (pills, avatars) is a stock Tailwind utility — there is no `--radius-full` token, but the class works.
475
+
476
+ Changing `--radius` alone rescales all four steps together.
477
+
478
+ ### Shadow
479
+
480
+ Four mode-aware shadows. There are **no `--elevation-*` tokens**; the scale is expressed as shadows. Never hand-craft a `box-shadow`.
481
+
482
+ | Token | Bridged to | Use |
483
+ | ---------------------- | ----------- | --------------------------------- |
484
+ | `--semantic-shadow-xs` | `shadow-xs` | hairline lift, active nav item |
485
+ | `--semantic-shadow-sm` | `shadow-sm` | cards, persistent panels |
486
+ | `--semantic-shadow-md` | `shadow-md` | popovers, menus, hovered surfaces |
487
+ | `--semantic-shadow-lg` | `shadow-lg` | modals, dialogs, sheets |
488
+
489
+ 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.
490
+
491
+ ### Motion
492
+
493
+ **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.
494
+
495
+ | Intent | Utilities |
496
+ | ----------------------------- | -------------------------- |
497
+ | Hover, focus, button press | `duration-150 ease-out` |
498
+ | State change (toggle, select) | `duration-200 ease-in-out` |
499
+ | Entrance, dialog open | `duration-300 ease-out` |
500
+
501
+ ```tsx
502
+ <div className="transition-colors duration-150 ease-out motion-reduce:transition-none" />
503
+ ```
504
+
505
+ 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.
506
+
507
+ ### Z-index
508
+
509
+ 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:
510
+
511
+ | Token | Value | Use |
512
+ | ------------------ | ----- | ----------------------------- |
513
+ | `--z-sidebar` | 10 | persistent sidebar |
514
+ | `--z-sidebar-rail` | 20 | sidebar collapsed rail |
515
+ | `--z-popup` | 50 | menu, tooltip, popover |
516
+ | `--z-overlay` | 50 | modal, sheet, dialog backdrop |
517
+
518
+ ```tsx
519
+ <div className="z-50" />
520
+ <div className="z-[var(--z-popup)]" />
521
+ ```
522
+
523
+ 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.
524
+
525
+ ### Icons
526
+
527
+ **AppShell exports no `Icon` component and defines no `--icon-*` tokens.** Icons come from [`lucide-react`](https://lucide.dev), which AppShell already depends on. Size them with Tailwind, pairing icon size to the adjacent text:
528
+
529
+ | Text size | Icon class |
530
+ | ------------------------------------------------------------------- | ---------- |
531
+ | 12px: `text-xs`, `text-body-sm`, `text-label-sm`, `text-code-sm` | `size-3` |
532
+ | 14px: `text-sm`, `text-body-md`, `text-label-md`, `text-heading-sm` | `size-4` |
533
+ | 18px: `text-lg`, `text-heading-md` | `size-5` |
534
+ | 24px (title): `text-2xl`, `text-heading-lg` | `size-6` |
535
+
536
+ ```tsx
537
+ import { Check } from "lucide-react";
538
+
539
+ <Check className="size-4" />;
540
+ ```
541
+
542
+ ### Breakpoints
543
+
544
+ Stock Tailwind breakpoints — AppShell does not change them.
545
+
546
+ | Token | Width |
547
+ | ----- | ------ |
548
+ | `sm` | 640px |
549
+ | `md` | 768px |
550
+ | `lg` | 1024px |
551
+ | `xl` | 1280px |
552
+ | `2xl` | 1536px |
553
+
554
+ **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.
555
+
556
+ 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.
557
+
558
+ ## Fonts
559
+
560
+ AppShell bundles **Inter Variable** for Latin and **Noto Sans JP Variable** for Japanese, and
561
+ applies both via the `body` rule in `@tailor-platform/app-shell/styles`. No extra import:
562
+
563
+ ```css
564
+ @import "tailwindcss";
565
+ @import "@tailor-platform/app-shell/styles";
566
+ ```
567
+
568
+ Both are variable fonts with a continuous 100–900 weight axis, so every weight the design
569
+ system names — `font-normal`, `font-medium`, `font-semibold`, `font-bold` — resolves to a real
570
+ weight in both scripts rather than to whatever faces happen to be installed.
571
+
572
+ ### Why Japanese needs its own font
573
+
574
+ Inter has no CJK glyphs, so without a bundled Japanese font, Japanese characters fall through
575
+ to the operating system's. On Windows that font is Yu Gothic UI, which ships only
576
+ Light/Semilight/Regular/Semibold/Bold — no 500 — so CSS weight matching resolves a
577
+ `font-weight: 500` request down to Regular, and `font-medium` becomes **indistinguishable from
578
+ body text in Japanese**. Current macOS is not affected: it ships Hiragino Sans W0–W9 including
579
+ W5, which `font-weight: 500` resolves to correctly. Bundling the font makes the weight scale
580
+ hold on every platform rather than depending on what the OS happens to install.
581
+
582
+ Noto Sans JP is metric-harmonised against Inter (`size-adjust: 94%` plus ascent/descent
583
+ overrides) so mixed Japanese/Latin strings read at one optical size, and a line containing
584
+ Japanese is exactly as tall as one without. A metric-matched `local()` fallback covers the
585
+ window before a subset arrives, so rows do not change height as fonts stream in.
586
+
587
+ ### What it costs
588
+
589
+ The Japanese faces are roughly **5 MB of woff2 subsets**, and they land in your build output
590
+ whether or not your app renders Japanese — a bundler emits every subset it can see, because it
591
+ cannot know at build time which characters your data will contain. The stylesheet grows from
592
+ about 98 KB to 200 KB uncompressed, or 15 KB to 45 KB gzipped.
593
+
594
+ What your **users** download is much smaller, and proportional to what they actually read.
595
+ Each subset carries a `unicode-range`, so the browser fetches one only when it is about to
596
+ paint a character in that range. The faces are restricted to Japanese blocks — kana, kanji,
597
+ CJK punctuation, fullwidth forms and the compatibility blocks carrying ㈱ ㍿ ㎡ — so shared
598
+ symbols such as `✓` or `→` resolve to Inter or the system font as before, and
599
+ **an app that renders no Japanese downloads none of them**:
600
+
601
+ | what the user has seen | downloaded |
602
+ | ---------------------------------------- | ----------- |
603
+ | all hiragana + katakana | ~237 KB |
604
+ | + ~230 common ERP kanji | ~458 KB |
605
+ | + ~30 name kanji including variant forms | ~467 KB |
606
+ | **typical first Japanese screen** | **~910 KB** |
607
+
608
+ Files are content-hashed and cached, so the cost is front-loaded rather than per-navigation,
609
+ and it grows slowly as unusual characters appear in your data. Weight costs nothing extra —
610
+ every weight lives in the same file, so using `font-medium` and `font-bold` downloads no more
611
+ than `font-normal` alone.
612
+
613
+ To drop the Japanese faces entirely, override the stack without naming them (see below).
614
+ Nothing references them, so nothing is downloaded — though the files still ship in your build
615
+ output.
616
+
617
+ ### Using your own font
618
+
619
+ Set `--app-shell-font-sans` on `:root` **after** importing AppShell styles to replace the whole
620
+ stack:
621
+
622
+ ```css
623
+ @import "@tailor-platform/app-shell/styles";
624
+
625
+ :root {
626
+ --app-shell-font-sans: "Your Brand Sans", ui-sans-serif, system-ui, sans-serif;
627
+ }
628
+ ```
629
+
630
+ A replacement should be a variable font, or otherwise supply real 400/500/600/700 faces —
631
+ AppShell's weight scale assumes all four exist. If your app renders Japanese, include a
632
+ Japanese family with a continuous weight axis for the same reason.
633
+
634
+ ## Styling AppShell components
635
+
636
+ Write **plain Tailwind utilities** in your application code. Three rules cover every case:
637
+
638
+ 1. **Your own markup** — ordinary utilities, exactly as in any Tailwind app.
639
+ 2. **A documented layout hook** — props like `Table.Root`'s `containerClassName` also take ordinary utilities from your Tailwind build:
640
+
641
+ ```tsx
642
+ <Table.Root containerClassName="max-h-96 overflow-y-auto" />
643
+ ```
644
+
645
+ 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.
646
+
647
+ ```tsx
648
+ // Props, not utilities, for a component's own appearance
649
+ <Sheet.Content size="lg" />
650
+ <Layout gap={6} />
651
+ <Button variant="destructive">Delete</Button>
652
+
653
+ // Ordinary utilities on your own markup and on documented hooks
654
+ <div className="flex flex-col gap-4">
655
+ <Table.Root containerClassName="max-h-96 overflow-y-auto" />
656
+ </div>
657
+ ```
658
+
659
+ When no prop exists and you genuinely have to override a value a component sets, add Tailwind's [importance modifier](https://tailwindcss.com/docs/styling-with-utility-classes#important-modifier) — a trailing `!`. It is the last resort, not the first: a plain utility silently loses (see below). Check for a prop before reaching for it. Sitting a table flush inside a card, for example, used to need `className="px-0!"`; it is now a prop:
660
+
661
+ ```tsx
662
+ <Card.Root>
663
+ <Card.Header title="Line items" />
664
+ <Card.Content padding="none">
665
+ <Table.Root>{/* … */}</Table.Root>
666
+ </Card.Content>
667
+ </Card.Root>
668
+ ```
669
+
670
+ 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.
671
+
672
+ ### Never write the `astw:` prefix in application code
673
+
674
+ 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.
675
+
676
+ 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.
677
+
678
+ 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.
679
+
680
+ `@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`.
681
+
682
+ ### Why a plain utility can't override an AppShell default
683
+
684
+ 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:
685
+
686
+ ```tsx
687
+ // Element keeps both classes; padding stays at AppShell's 24px
688
+ <Card.Content className="px-0" />
689
+ ```
690
+
691
+ (For this particular case, use `padding="none"`.)
692
+
693
+ State variants make it worse rather than better: `Button`'s `ghost` variant sets a hover color, and a `:hover` rule outranks an unprefixed utility on **specificity**, not merely order — so a plain `text-destructive` on a ghost button is red only until the pointer reaches it. `variant="destructive"` is the supported way to express that; where no such prop exists, `!` wins in every state because importance beats specificity.
694
+
695
+ ### Styling by state (data attributes)
696
+
697
+ 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:
698
+
699
+ ```css
700
+ /* Style a component based on its state */
701
+ .SwitchThumb[data-checked] {
702
+ background-color: var(--success-solid);
703
+ }
704
+
705
+ .MenuItem[data-highlighted] {
706
+ background-color: var(--accent);
707
+ color: var(--accent-foreground);
708
+ }
709
+ ```
710
+
711
+ This works with Tailwind as well — **use theme tokens**, not raw Tailwind grays:
712
+
713
+ ```tsx
714
+ <Switch.Thumb className="bg-muted data-[checked]:bg-primary" />
715
+ ```
716
+
717
+ 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)).
718
+
719
+ ## Links
720
+
721
+ One treatment for every text link — inside `DescriptionCard`, in table rows,
722
+ in cards:
723
+
724
+ - **`text-primary` at rest, `hover:underline` only.** Colour carries the affordance; a resting underline is noise. Pair with `underline-offset-4`.
725
+ - **Internal routes** use app-shell's `Link` (`to`), never a bare `<a href>`.
726
+ - **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.
727
+ - **Identifiers** (document numbers, SKUs) keep `font-mono` on top of the link style.
728
+
729
+ This is the treatment `DescriptionCard` already applies to its `type: "link"`
730
+ fields — match it rather than inventing a second one.
731
+
732
+ ## Composition & emphasis rules
733
+
734
+ These are visual-composition rules every screen must follow, regardless of pattern. They exist because emphasis only works when it is scarce.
735
+
736
+ **Emphasis budget.** Attention is a budget you spend once per scan region.
737
+
738
+ - **One primary action per view.** A screen (or a card/section) has at most one filled/primary `Button`; everything else is `outline`, `secondary`, or `ghost`. If two things look equally important, neither reads as important.
739
+ - **Badges** encode status by semantic color, with a clear primary/secondary split:
740
+ - 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.
741
+ - **Secondary statuses** (delivery, billing, fulfilment) and dense supporting columns → **`outline-*`** (with status dot).
742
+ - **Tags / labels** ("New", "Returned") → **`subtle-*`**.
743
+ - 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).)
744
+ - **Color:** status colors signal meaning, not decoration — don't tint neutral content.
745
+
746
+ **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.
747
+
748
+ **States — never ship only the happy path.** Every data-backed screen handles:
749
+
750
+ - **Loading** — skeleton/placeholder, not a blank flash.
751
+ - **Empty** — a labelled empty state (what it is, how to add the first record), not a bare empty table.
752
+ - **Error** — an inline error with a retry affordance, not a silent failure.
753
+
754
+ **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.
755
+
756
+ ## Quick reference
757
+
758
+ ### Where to look
759
+
760
+ | Concern | Where |
761
+ | ---------------------------------------------------------- | --------------------------------------------------------------- |
762
+ | Tokens, theming, the `astw:` styling boundary, conformance | **this document** |
763
+ | A component's imports, props, variants, composition | its page under [`docs/components/`](../components/) |
764
+ | A hook / function API | its page under [`docs/api/`](../api/) |
765
+ | Screen / page layout recipes | [`docs/patterns/`](../patterns/) and [`docs/pages/`](../pages/) |
766
+ | Building a custom component to fill a gap | [Custom components](./custom-components.md) |
767
+
768
+ ### Semantic decisions
769
+
770
+ | Intent | Pick |
771
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
772
+ | Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` |
773
+ | Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` |
774
+ | Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` |
775
+ | Informational callout | `Badge variant="info"`, or `Alert variant="info"` |
776
+ | Neutral callout | `Badge variant="neutral"`, or `Alert variant="neutral"` |
777
+ | Persistent panel (sidebar, header) | `shadow-sm` |
778
+ | Hovered / sticky surface | `shadow-md` |
779
+ | Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
780
+ | Modal / sheet / dialog | `shadow-lg`, `duration-300` entrance |
781
+ | Hover / focus transition | `duration-150 ease-out` |
782
+ | State change (toggle, select) | `duration-200 ease-in-out` |
783
+ | Two-column detail at <1024 | right column collapses below main — do not override |
784
+ | Inline ID, code, table number | `font-mono text-code-sm` (identifiers) / `text-label-md tabular-nums` (figures) |
785
+ | Timestamp, label, subtle metadata | `text-body-sm text-muted-foreground` |