@tailor-platform/app-shell 1.16.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +762 -51
- package/dist/app-shell.js +7642 -5636
- package/dist/{appshell-B0wh5Lq0.js → appshell-Y1AYCuVJ.js} +323 -304
- package/dist/semantic-contrast.test.ts +227 -0
- package/dist/style-entry.d.ts +61 -26
- package/dist/testing.d.ts +66 -29
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +124 -1
- package/dist/theme.bridge.css +178 -0
- package/dist/theme.css +8 -2
- package/dist/themes/_template.css +8 -3
- package/dist/themes/bloom.css +0 -8
- package/dist/themes/cream.css +0 -8
- package/dist/themes/default.css +458 -103
- package/dist/vite-plugin.d.ts +61 -26
- package/package.json +2 -2
- package/skills/app-shell-patterns/SKILL.md +48 -37
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
- package/skills/app-shell-patterns/references/api/define-module.md +4 -6
- package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
- package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
- package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
- package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
- package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
- package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
- package/skills/app-shell-patterns/references/components/card.md +79 -7
- package/skills/app-shell-patterns/references/components/data-table.md +599 -77
- package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
- package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
- package/skills/app-shell-patterns/references/components/sidebar-group.md +12 -6
- package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
- package/skills/app-shell-patterns/references/components/text.md +198 -0
- package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
- package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
- package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
- package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
- package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +278 -65
- package/skills/app-shell-patterns/references/migrations.md +43 -1
- package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
- package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
- package/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
- package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +5 -4
|
@@ -153,22 +153,28 @@ function CreateOrderButton() {
|
|
|
153
153
|
|
|
154
154
|
### `additionalTypenames`
|
|
155
155
|
|
|
156
|
-
When a mutation creates/updates/deletes an entity that another query already has cached, urql won't know to refetch unless you pass `additionalTypenames`. Use it on **both** the query and the mutation when they share a typename
|
|
156
|
+
When a mutation creates/updates/deletes an entity that another query already has cached, urql won't know to refetch unless you pass `additionalTypenames`. Use it on **both** the query and the mutation when they share a typename.
|
|
157
|
+
|
|
158
|
+
Define the query's static `context` at module scope, outside the component, so its reference stays stable between renders. `useQuery` lists `context` as a hook dependency, so a fresh object on each render re-creates the query subscription and can cause query re-execution loops. The mutation's second argument is passed per call and is not a hook dependency, so it can stay inline.
|
|
157
159
|
|
|
158
160
|
```tsx
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
161
|
+
const ordersQueryContext = { additionalTypenames: ["Order"] };
|
|
162
|
+
|
|
163
|
+
function useOrders() {
|
|
164
|
+
// Query side — declares it cares about the Order typename
|
|
165
|
+
const [{ data }] = useQuery({
|
|
166
|
+
query: OrdersQuery,
|
|
167
|
+
variables: { first: 25 },
|
|
168
|
+
context: ordersQueryContext,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Mutation side — declares it produces Order writes
|
|
172
|
+
const [, executeCreateOrder] = useMutation(CreateOrderMutation);
|
|
173
|
+
const createOrder = (input: CreateOrderInput) =>
|
|
174
|
+
executeCreateOrder({ input }, { additionalTypenames: ["Order"] });
|
|
175
|
+
|
|
176
|
+
return { data, createOrder };
|
|
177
|
+
}
|
|
172
178
|
```
|
|
173
179
|
|
|
174
180
|
Skip `additionalTypenames` only when the mutation's return type already includes the affected entity _and_ every cached query for it touched the same `id` — urql will then refresh by id. When in doubt, add it.
|
|
@@ -159,15 +159,15 @@ const MyComponent = () => {
|
|
|
159
159
|
|
|
160
160
|
## Declarative Redirects
|
|
161
161
|
|
|
162
|
-
`Navigate` redirects as a render result, which avoids the `useEffect` + `navigate()` pattern (that pattern renders the old page for a frame before it redirects):
|
|
162
|
+
`Navigate` redirects as a render result, which avoids the `useEffect` + `navigate()` pattern (that pattern renders the old page for a frame before it redirects). `contextData` is typed through the `AppShellRegister` augmentation described in [useAppShell](../api/use-app-shell.md):
|
|
163
163
|
|
|
164
164
|
```tsx
|
|
165
165
|
import { Navigate, useAppShellData } from "@tailor-platform/app-shell";
|
|
166
166
|
|
|
167
167
|
const AdminPage = () => {
|
|
168
|
-
const {
|
|
168
|
+
const { contextData } = useAppShellData();
|
|
169
169
|
|
|
170
|
-
if (currentUser?.role !== "admin") {
|
|
170
|
+
if (contextData.currentUser?.role !== "admin") {
|
|
171
171
|
return <Navigate to="/dashboard" replace />;
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -207,6 +207,44 @@ defineResource({
|
|
|
207
207
|
// Breadcrumb shows: "Orders > Order #12345"
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
### Localized Breadcrumb Titles
|
|
211
|
+
|
|
212
|
+
Pass `labels.t(key)` directly to `meta.breadcrumbTitle`, just like `meta.title`.
|
|
213
|
+
AppShell recognizes these translation functions and resolves them using the current locale.
|
|
214
|
+
For labels that depend on the URL segment, return a `LocalizedString` from a segment callback:
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
import { defineI18nLabels, defineResource } from "@tailor-platform/app-shell";
|
|
218
|
+
|
|
219
|
+
const labels = defineI18nLabels({
|
|
220
|
+
en: {
|
|
221
|
+
orders: "All Orders",
|
|
222
|
+
order: ({ id }: { id: string }) => `Order #${id}`,
|
|
223
|
+
},
|
|
224
|
+
ja: {
|
|
225
|
+
orders: "すべての注文",
|
|
226
|
+
order: ({ id }: { id: string }) => `注文 #${id}`,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const ordersResource = defineResource({
|
|
231
|
+
path: "orders",
|
|
232
|
+
meta: { breadcrumbTitle: labels.t("orders") },
|
|
233
|
+
subResources: [
|
|
234
|
+
defineResource({
|
|
235
|
+
path: ":id",
|
|
236
|
+
meta: { breadcrumbTitle: (segment) => labels.t("order", { id: segment }) },
|
|
237
|
+
}),
|
|
238
|
+
],
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The same pattern works for `defineModule` and file-based `appShellPageProps.meta`.
|
|
243
|
+
Other callbacks still receive the URL segment. The existing `() => labels.t(key)` form remains supported.
|
|
244
|
+
Handwritten locale functions must be returned from an outer callback, for example `breadcrumbTitle: () => (locale) => locale === "ja" ? "注文" : "Orders"`.
|
|
245
|
+
If navigation and breadcrumbs share a label, omit `breadcrumbTitle` and use `title: labels.t(key)` instead.
|
|
246
|
+
Existing strings and `(segment) => string` callbacks remain supported; no `as string` cast is needed.
|
|
247
|
+
|
|
210
248
|
### Dynamic Breadcrumb Titles
|
|
211
249
|
|
|
212
250
|
Use the `useOverrideBreadcrumb` hook to replace a breadcrumb segment with a data-driven value from within the rendered page component:
|
|
@@ -182,6 +182,38 @@ const labels = defineI18nLabels({
|
|
|
182
182
|
|
|
183
183
|
---
|
|
184
184
|
|
|
185
|
+
### SidebarSection
|
|
186
|
+
|
|
187
|
+
A static heading for a run of flat navigation items. Unlike `SidebarGroup`, a section is neither collapsible nor navigable, and its items keep top-level styling. In the icon rail the heading is hidden.
|
|
188
|
+
|
|
189
|
+
#### Props
|
|
190
|
+
|
|
191
|
+
| Prop | Type | Required | Description |
|
|
192
|
+
| ---------- | ----------------- | -------- | --------------------------------- |
|
|
193
|
+
| `title` | `LocalizedString` | Yes | Section heading (supports i18n) |
|
|
194
|
+
| `children` | `ReactNode` | Yes | Child items (usually SidebarItem) |
|
|
195
|
+
|
|
196
|
+
#### Example
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<DefaultSidebar>
|
|
200
|
+
<SidebarItem to="/overview" />
|
|
201
|
+
<SidebarSeparator />
|
|
202
|
+
<SidebarSection title="Services">
|
|
203
|
+
<SidebarItem to="/ai-gateways" />
|
|
204
|
+
<SidebarItem to="/applications" />
|
|
205
|
+
</SidebarSection>
|
|
206
|
+
<SidebarSeparator />
|
|
207
|
+
<SidebarSection title="Logs">
|
|
208
|
+
<SidebarItem to="/activity-logs" />
|
|
209
|
+
</SidebarSection>
|
|
210
|
+
</DefaultSidebar>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Use `SidebarGroup` when users should be able to collapse the items or when they belong under a parent page; use `SidebarSection` when you only need a label above items that stay visible.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
185
217
|
### SidebarSeparator
|
|
186
218
|
|
|
187
219
|
A visual divider between sidebar sections.
|
|
@@ -11,7 +11,7 @@ Styling is done with **Tailwind CSS v4** against `@tailor-platform/app-shell`'s
|
|
|
11
11
|
|
|
12
12
|
**The tokens are the rails.** Consistency across customers, apps, and AI runs comes from the token system, not from rules written in prose. A hand-typed `#fff` or `padding: 13px` is not a "small deviation" — it is the mechanism by which consistency dies.
|
|
13
13
|
|
|
14
|
-
**Every token in this document is verified against the shipped CSS.** If a token is not listed here, assume it does not exist. Inventing a plausible-sounding token (`bg-surface-1`, `text-fg-muted`, `--space-4`) is the worst failure mode available: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled — no error, no warning, nothing in the console. When unsure, read `node_modules/@tailor-platform/app-shell/dist/themes/default.css`; it is the ground truth.
|
|
14
|
+
**Every token in this document is verified against the shipped CSS.** If a token is not listed here, assume it does not exist. The colour scales (`--primitive-*`) are internal: they are not bridged to Tailwind and are not listed. Inventing a plausible-sounding token (`bg-surface-1`, `text-fg-muted`, `--space-4`) is the worst failure mode available: Tailwind emits **no CSS at all** for an unknown utility, so the class is silently dropped and the element renders unstyled — no error, no warning, nothing in the console. When unsure, read `node_modules/@tailor-platform/app-shell/dist/themes/default.css`; it is the ground truth.
|
|
15
15
|
|
|
16
16
|
## Setup
|
|
17
17
|
|
|
@@ -86,14 +86,14 @@ Only import one palette at a time.
|
|
|
86
86
|
|
|
87
87
|
Theme tokens live in `packages/core/src/assets/themes/`. Copy `_template.css` to start a new palette — it lists exactly which sections to fill in for light and dark mode.
|
|
88
88
|
|
|
89
|
-
| Section | Required? | What to set
|
|
90
|
-
| --------------------- | --------------------- |
|
|
91
|
-
| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes
|
|
92
|
-
| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint`
|
|
93
|
-
| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders
|
|
94
|
-
| **4. Palette** | Optional | Radius, chart colors, shadows
|
|
95
|
-
| **5. Semantic** |
|
|
96
|
-
| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed
|
|
89
|
+
| Section | Required? | What to set |
|
|
90
|
+
| --------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
91
|
+
| **1. Brand** | Yes | `primary`, `secondary`, `accent` (+ foregrounds) — both modes |
|
|
92
|
+
| **2. Shell gradient** | Branded palettes only | `--shell-gradient-base`, `--shell-gradient-tint` |
|
|
93
|
+
| **3. System** | Tune or copy default | Surfaces: background, card, popover, muted, borders |
|
|
94
|
+
| **4. Palette** | Optional | Radius, chart colors, shadows |
|
|
95
|
+
| **5. Semantic** | Inherit | Colour scales, status and alert tokens inherit from `default.css`. Override a colour role (`--{intent}-{role}`) only to recolour the status components |
|
|
96
|
+
| **6. Structural** | Branded palettes | Copy the structural override block from `bloom.css` or `cream.css` when needed |
|
|
97
97
|
|
|
98
98
|
A palette is selected by CSS import, not by an AppShell prop. Import exactly one theme file after `@tailor-platform/app-shell/styles`; if you import none, the default palette from `styles` is used.
|
|
99
99
|
|
|
@@ -159,33 +159,69 @@ Colors follow shadcn-style semantic naming: a surface token and its matching `-f
|
|
|
159
159
|
| `--border` | hairlines, dividers | `border-border` |
|
|
160
160
|
| `--input` | form control borders | `border-input` |
|
|
161
161
|
| `--ring` | focus rings | `ring-ring` / `outline-ring` |
|
|
162
|
+
| `--backdrop` | dim layer behind a modal | `bg-backdrop` |
|
|
162
163
|
|
|
163
164
|
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
|
|
|
166
|
+
`--backdrop` is the dim layer behind `Dialog` and `Sheet` (black at 50% in both modes). Use `bg-backdrop` for your own modal layers instead of `bg-black/50`. Palettes inherit it; override it on `:root` to change every backdrop at once.
|
|
167
|
+
|
|
168
|
+
In the default palette, the surface, text and line tokens in this table read the internal gray scale, a white step, and white alpha steps (dark-mode lines), so their values match the gray used across the palette. `--backdrop` reads a black alpha step, and `--ring` is mixed from `--primary`. The other palettes set their own values.
|
|
169
|
+
|
|
165
170
|
#### Brand & action
|
|
166
171
|
|
|
167
|
-
| Token | Use
|
|
168
|
-
| -------------------------------------------- |
|
|
169
|
-
| `--primary` / `--primary-foreground` | primary buttons, emphasis
|
|
170
|
-
| `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips
|
|
171
|
-
| `--accent` / `--accent-foreground` | hover and selected nav states
|
|
172
|
-
| `--destructive` / `--destructive-foreground` |
|
|
172
|
+
| Token | Use | Tailwind |
|
|
173
|
+
| -------------------------------------------- | ------------------------------------------- | -------------------------------------------- |
|
|
174
|
+
| `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` |
|
|
175
|
+
| `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` |
|
|
176
|
+
| `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` |
|
|
177
|
+
| `--destructive` / `--destructive-foreground` | deprecated alias of danger solid / contrast | `bg-destructive` / `text-destructive` |
|
|
178
|
+
|
|
179
|
+
**Deprecated.** `--destructive`, `--destructive-foreground` and their utilities remain for existing code and will be removed in the next major. New code uses the danger roles: `bg-danger-solid`, `text-danger-contrast`, and `text-danger-text` for text. `Button variant="destructive"` is not deprecated yet; the next major renames it to `danger`.
|
|
180
|
+
|
|
181
|
+
`--destructive` and `--destructive-foreground` are aliases of the danger `solid` and `contrast` roles ([semantic color roles](#semantic-color-roles)), so `bg-destructive` is the danger solid fill in both modes and `Button variant="destructive"` reads the danger roles directly. `text-destructive` still works and resolves to the solid. For text, use `text-danger-text`, which meets 4.5:1 on `--card` and `--background`; the solid is `#dc2626` in dark mode, 3.71:1 on the dark card. To recolour destructive actions, override `--danger-solid` (and the other danger roles), not `--destructive`.
|
|
173
182
|
|
|
174
183
|
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
184
|
|
|
176
185
|
#### Status
|
|
177
186
|
|
|
178
|
-
|
|
187
|
+
**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:
|
|
188
|
+
|
|
189
|
+
| Token | Use | Tailwind | Use instead |
|
|
190
|
+
| -------------------- | ---------------------------------------------------- | --------------------- | ---------------------- |
|
|
191
|
+
| `--status-default` | none / not applicable (follows `--muted-foreground`) | `bg-status-default` | `bg-neutral-indicator` |
|
|
192
|
+
| `--status-neutral` | informational | `bg-status-neutral` | `bg-info-solid` |
|
|
193
|
+
| `--status-completed` | success, completed | `bg-status-completed` | `bg-success-solid` |
|
|
194
|
+
| `--status-attention` | warning, needs attention | `bg-status-attention` | `bg-warning-solid` |
|
|
195
|
+
| `--status-danger` | error, blocked | `bg-status-danger` | `bg-danger-solid` |
|
|
196
|
+
|
|
197
|
+
Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `neutral`) over applying a fill directly — the variants already pair fill and foreground correctly.
|
|
179
198
|
|
|
180
|
-
|
|
181
|
-
| -------------------- | ------------------------ | --------------------- |
|
|
182
|
-
| `--status-default` | none / not applicable | `bg-status-default` |
|
|
183
|
-
| `--status-neutral` | informational | `bg-status-neutral` |
|
|
184
|
-
| `--status-completed` | success, completed | `bg-status-completed` |
|
|
185
|
-
| `--status-attention` | warning, needs attention | `bg-status-attention` |
|
|
186
|
-
| `--status-danger` | error, blocked | `bg-status-danger` |
|
|
199
|
+
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`.
|
|
187
200
|
|
|
188
|
-
|
|
201
|
+
#### Semantic color roles
|
|
202
|
+
|
|
203
|
+
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}`:
|
|
204
|
+
|
|
205
|
+
| Role | Tailwind | Use for |
|
|
206
|
+
| --------------- | ----------------------- | ----------------------------------- |
|
|
207
|
+
| `surface` | `bg-info-surface` | Soft background |
|
|
208
|
+
| `surface-hover` | `bg-info-surface-hover` | Hover state of a soft background |
|
|
209
|
+
| `border` | `border-info-border` | Border on a soft background |
|
|
210
|
+
| `solid` | `bg-info-solid` | Filled background |
|
|
211
|
+
| `solid-hover` | `bg-info-solid-hover` | Hover state of a filled background |
|
|
212
|
+
| `text` | `text-info-text` | Text and icons on a soft background |
|
|
213
|
+
| `contrast` | `text-info-contrast` | Text on a filled background |
|
|
214
|
+
| `indicator` | `bg-info-indicator` | Dots and small marks |
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
<span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
222
|
+
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.
|
|
223
|
+
|
|
224
|
+
`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-*`. `Button variant="destructive"`, invalid input borders and error text follow `--danger-*` too, and `--destructive` is an alias of `--danger-solid`.
|
|
189
225
|
|
|
190
226
|
#### Sidebar & charts
|
|
191
227
|
|
|
@@ -195,24 +231,17 @@ Prefer `Badge` with a semantic variant (`success`, `warning`, `error`, `info`, `
|
|
|
195
231
|
|
|
196
232
|
#### Alerts
|
|
197
233
|
|
|
198
|
-
`--alert-{neutral,success,warning,error,info}-{background,foreground,foreground-muted,border}`
|
|
234
|
+
**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`.
|
|
199
235
|
|
|
200
|
-
|
|
201
|
-
| ------------------ | ---------------------------------- | -------------------------- |
|
|
202
|
-
| `background` | `bg-alert-info-background` | Callout fill |
|
|
203
|
-
| `foreground` | `text-alert-info-foreground` | Primary text and icons |
|
|
204
|
-
| `foreground-muted` | `text-alert-info-foreground-muted` | Secondary/description text |
|
|
205
|
-
| `border` | `border-alert-info-border` | Callout border |
|
|
206
|
-
|
|
207
|
-
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:
|
|
236
|
+
For new custom surfaces, use the roles instead of the alert slots:
|
|
208
237
|
|
|
209
238
|
```tsx
|
|
210
|
-
<div className="rounded-lg border bg-
|
|
239
|
+
<div className="rounded-lg border bg-warning-surface text-warning-text border-warning-border">
|
|
211
240
|
…
|
|
212
241
|
</div>
|
|
213
242
|
```
|
|
214
243
|
|
|
215
|
-
|
|
244
|
+
Across all colour tokens:
|
|
216
245
|
|
|
217
246
|
```tsx
|
|
218
247
|
// Good — semantic token pairs, and a variant where one exists
|
|
@@ -243,29 +272,210 @@ Hand-typing `padding: 13px` is a smell. Round to the nearest scale step; if noth
|
|
|
243
272
|
|
|
244
273
|
### Typography
|
|
245
274
|
|
|
246
|
-
|
|
275
|
+
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`.
|
|
276
|
+
|
|
277
|
+
There are 8 core roles and 2 relaxed body variants:
|
|
278
|
+
|
|
279
|
+
| Role | Utility | Size / line height | Weight | Use |
|
|
280
|
+
| ----------------- | ---------------------- | ------------------ | ------ | ----------------------------------------------- |
|
|
281
|
+
| `heading-lg` | `text-heading-lg` | 24px / 32px | 700 | Page title (`Layout.Header`) |
|
|
282
|
+
| `heading-md` | `text-heading-md` | 18px / 28px | 600 | Section heading, card title |
|
|
283
|
+
| `heading-sm` | `text-heading-sm` | 14px / 20px | 600 | Sub-section heading |
|
|
284
|
+
| `body-md` | `text-body-md` | 14px / 20px | 400 | Body copy, descriptions |
|
|
285
|
+
| `body-sm` | `text-body-sm` | 12px / 16px | 400 | Caption, timestamp, metadata |
|
|
286
|
+
| `label-md` | `text-label-md` | 14px / 20px | 500 | Field label, short UI text, figures in a table |
|
|
287
|
+
| `label-sm` | `text-label-sm` | 12px / 16px | 500 | Small label, badge text |
|
|
288
|
+
| `code-sm` | `text-code-sm` | 12px / 18px | 400 | ID and code. Use with `font-mono` |
|
|
289
|
+
| `body-md-relaxed` | `text-body-md-relaxed` | 14px / 24px | 400 | Long text, such as a chat message or a document |
|
|
290
|
+
| `body-sm-relaxed` | `text-body-sm-relaxed` | 12px / 20px | 400 | Long text at the small size |
|
|
291
|
+
|
|
292
|
+
`heading-lg` also sets letter spacing to `-0.025em`. All other roles set `0em`.
|
|
293
|
+
|
|
294
|
+
Pair a role with a color token from the Color section above. These pairings are the recommended combinations:
|
|
295
|
+
|
|
296
|
+
| Use | Utilities |
|
|
297
|
+
| --------------------------------- | ---------------------------------------------- |
|
|
298
|
+
| Page title (`Layout.Header`) | `text-heading-lg` |
|
|
299
|
+
| Section heading | `text-heading-md` |
|
|
300
|
+
| Card title | `text-heading-md leading-none` |
|
|
301
|
+
| Body copy | `text-body-md` |
|
|
302
|
+
| Secondary copy, descriptions | `text-body-md text-muted-foreground` |
|
|
303
|
+
| Caption, timestamp, metadata | `text-body-sm text-muted-foreground` |
|
|
304
|
+
| Numeric value in a table or field | `text-label-md tabular-nums` |
|
|
305
|
+
| ID, code | `font-mono text-code-sm text-muted-foreground` |
|
|
306
|
+
|
|
307
|
+
<example-preview name="typography-roles"></example-preview>
|
|
247
308
|
|
|
248
|
-
|
|
309
|
+
```tsx
|
|
310
|
+
function TypographyRoles() {
|
|
311
|
+
const rows = [
|
|
312
|
+
{
|
|
313
|
+
role: "heading-lg",
|
|
314
|
+
className: "text-heading-lg",
|
|
315
|
+
en: "Purchase orders",
|
|
316
|
+
ja: "発注管理",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
role: "heading-md",
|
|
320
|
+
className: "text-heading-md",
|
|
321
|
+
en: "Purchase orders by supplier",
|
|
322
|
+
ja: "仕入先別の発注一覧",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
role: "heading-sm",
|
|
326
|
+
className: "text-heading-sm",
|
|
327
|
+
en: "Delivery address",
|
|
328
|
+
ja: "納品先",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
role: "body-md",
|
|
332
|
+
className: "text-body-md",
|
|
333
|
+
en: "The order was sent to the supplier on 12 March.",
|
|
334
|
+
ja: "発注書は3月12日に仕入先へ送付されました。",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
role: "body-sm",
|
|
338
|
+
className: "text-body-sm text-muted-foreground",
|
|
339
|
+
en: "Updated 2 hours ago",
|
|
340
|
+
ja: "2時間前に更新",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
role: "label-md",
|
|
344
|
+
className: "text-label-md",
|
|
345
|
+
en: "Order quantity",
|
|
346
|
+
ja: "発注数量",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
role: "label-sm",
|
|
350
|
+
className: "text-label-sm",
|
|
351
|
+
en: "Draft",
|
|
352
|
+
ja: "下書き",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
role: "code-sm",
|
|
356
|
+
className: "font-mono text-code-sm",
|
|
357
|
+
en: "PO-2026-000184",
|
|
358
|
+
ja: "PO-2026-000184",
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
role: "body-md-relaxed",
|
|
362
|
+
className: "text-body-md-relaxed",
|
|
363
|
+
compareWith: { role: "body-md", className: "text-body-md" },
|
|
364
|
+
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.",
|
|
365
|
+
ja: "この発注を承認する前に、納期を仕入先に確認してください。希望日までに納品できない場合は、納期を変更するか、別の仕入先を選んでください。承認した発注は、メールで仕入先に送られます。",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
role: "body-sm-relaxed",
|
|
369
|
+
className: "text-body-sm-relaxed text-muted-foreground",
|
|
370
|
+
compareWith: { role: "body-sm", className: "text-body-sm text-muted-foreground" },
|
|
371
|
+
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.",
|
|
372
|
+
ja: "確定済みの発注への変更は、発注履歴に記録されます。記録には、変更した人、変更した日時、変更前と変更後の値が含まれます。",
|
|
373
|
+
},
|
|
374
|
+
];
|
|
249
375
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
376
|
+
return (
|
|
377
|
+
<div className="flex flex-col gap-4">
|
|
378
|
+
{rows.map(({ role, className, compareWith, en, ja }) => (
|
|
379
|
+
<div key={role} className="grid grid-cols-[9rem_1fr] items-baseline gap-4">
|
|
380
|
+
<span className="font-mono text-code-sm text-muted-foreground">{role}</span>
|
|
381
|
+
{compareWith ? (
|
|
382
|
+
// A relaxed role changes only line height, so show the same text in the base role first.
|
|
383
|
+
<div className="flex flex-col gap-4">
|
|
384
|
+
{[compareWith, { role, className }].map((variant) => (
|
|
385
|
+
<div key={variant.role} className="flex flex-col gap-1">
|
|
386
|
+
<span className="font-mono text-code-sm text-muted-foreground">
|
|
387
|
+
{variant.role}
|
|
388
|
+
</span>
|
|
389
|
+
<p className={variant.className}>{en}</p>
|
|
390
|
+
<p className={variant.className}>{ja}</p>
|
|
391
|
+
</div>
|
|
392
|
+
))}
|
|
393
|
+
</div>
|
|
394
|
+
) : (
|
|
395
|
+
<div className="flex flex-col gap-1">
|
|
396
|
+
<p className={className}>{en}</p>
|
|
397
|
+
<p className={className}>{ja}</p>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
</div>
|
|
401
|
+
))}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
```
|
|
260
406
|
|
|
261
407
|
```tsx
|
|
262
|
-
<h2 className="text-
|
|
263
|
-
<p className="text-
|
|
264
|
-
<span className="text-
|
|
408
|
+
<h2 className="text-heading-md">Section</h2>
|
|
409
|
+
<p className="text-body-md text-muted-foreground">Description copy</p>
|
|
410
|
+
<span className="text-body-sm text-muted-foreground">Updated 2h ago</span>
|
|
265
411
|
```
|
|
266
412
|
|
|
413
|
+
The [`Text`](../components/text.md) component applies the same roles with a `variant` prop. It renders a default element for each role (`h2` for headings, `p` for body text, `span` for the others), and its `render` prop sets the element, such as the heading level:
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
<Text variant="heading-md" render={<h3 />}>Line items</Text>
|
|
417
|
+
<Text variant="body-md" className="text-muted-foreground">Description copy</Text>
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
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.
|
|
421
|
+
|
|
422
|
+
**Rules**
|
|
423
|
+
|
|
424
|
+
- **A role carries no color.** Set the color with a token such as `text-muted-foreground`.
|
|
425
|
+
- **A role does not choose the HTML element.** Pick `h2`, `p`, or `span` from the document structure, not from the size. With `Text`, set the element with `render`.
|
|
426
|
+
- **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.
|
|
427
|
+
- **Four modifiers may be combined with a role.** All are stock Tailwind utilities:
|
|
428
|
+
- 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()`.
|
|
429
|
+
- Tabular figures: `tabular-nums`.
|
|
430
|
+
- Emphasis on inline text: `font-semibold`. Do not use `font-medium`; on a label role it makes no change.
|
|
431
|
+
- Uppercase: `uppercase`.
|
|
432
|
+
- **Do not combine a role with other `leading-*`, `tracking-*`, or `text-[Npx]` values.** If none of the roles fits, use the nearest role.
|
|
433
|
+
- **12px is the minimum size.** Do not use smaller text.
|
|
434
|
+
- **`code-sm` needs `font-mono`.** The role sets size, line height, weight, and letter spacing, but a role cannot set the font family.
|
|
435
|
+
- **Use the relaxed variants for long text only.** For labels, table cells, and other short text, use the regular roles.
|
|
436
|
+
|
|
267
437
|
Always use `tabular-nums` for numbers that stack in a column — without it, digits jitter between rows.
|
|
268
438
|
|
|
439
|
+
**Change a role.** Each role is four CSS variables named `--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. 1.17.0 named these variables `--app-shell-type-*`. The old names still work, but they are deprecated and will be removed in the next major; rename them to `--type-*`.
|
|
440
|
+
|
|
441
|
+
```css
|
|
442
|
+
:root {
|
|
443
|
+
--type-heading-lg-size: 1.75rem;
|
|
444
|
+
--type-heading-lg-line-height: calc(36 / 28);
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
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.
|
|
449
|
+
|
|
450
|
+
**`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.
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
454
|
+
|
|
455
|
+
const twMerge = extendTailwindMerge({
|
|
456
|
+
extend: {
|
|
457
|
+
classGroups: {
|
|
458
|
+
"font-size": [
|
|
459
|
+
{
|
|
460
|
+
text: [
|
|
461
|
+
"heading-lg",
|
|
462
|
+
"heading-md",
|
|
463
|
+
"heading-sm",
|
|
464
|
+
"body-md",
|
|
465
|
+
"body-sm",
|
|
466
|
+
"body-md-relaxed",
|
|
467
|
+
"body-sm-relaxed",
|
|
468
|
+
"label-md",
|
|
469
|
+
"label-sm",
|
|
470
|
+
"code-sm",
|
|
471
|
+
],
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
269
479
|
### Radius
|
|
270
480
|
|
|
271
481
|
`--radius` (`0.625rem`) is the base; the bridge derives four steps from it. Pick by component role — a card is always `md`, regardless of its size on screen.
|
|
@@ -332,12 +542,12 @@ Never invent a z value — `z-index: 9999` is always wrong. Popups and overlays
|
|
|
332
542
|
|
|
333
543
|
**AppShell exports no `Icon` component and defines no `--icon-*` tokens.** Icons come from [`lucide-react`](https://lucide.dev), which AppShell already depends on. Size them with Tailwind, pairing icon size to the adjacent text:
|
|
334
544
|
|
|
335
|
-
| Text size
|
|
336
|
-
|
|
|
337
|
-
| `text-xs`
|
|
338
|
-
| `text-sm`
|
|
339
|
-
| `text-lg`
|
|
340
|
-
| `text-2xl`
|
|
545
|
+
| Text size | Icon class |
|
|
546
|
+
| ------------------------------------------------------------------- | ---------- |
|
|
547
|
+
| 12px: `text-xs`, `text-body-sm`, `text-label-sm`, `text-code-sm` | `size-3` |
|
|
548
|
+
| 14px: `text-sm`, `text-body-md`, `text-label-md`, `text-heading-sm` | `size-4` |
|
|
549
|
+
| 18px: `text-lg`, `text-heading-md` | `size-5` |
|
|
550
|
+
| 24px (title): `text-2xl`, `text-heading-lg` | `size-6` |
|
|
341
551
|
|
|
342
552
|
```tsx
|
|
343
553
|
import { Check } from "lucide-react";
|
|
@@ -462,12 +672,12 @@ Write **plain Tailwind utilities** in your application code. Three rules cover e
|
|
|
462
672
|
</div>
|
|
463
673
|
```
|
|
464
674
|
|
|
465
|
-
When no prop exists and you genuinely have to override a value a component sets, add Tailwind's [importance modifier](https://tailwindcss.com/docs/styling-with-utility-classes#important-modifier) — a trailing `!`. It is the last resort, not the first: a plain utility silently loses (see below)
|
|
675
|
+
When no prop exists and you genuinely have to override a value a component sets, add Tailwind's [importance modifier](https://tailwindcss.com/docs/styling-with-utility-classes#important-modifier) — a trailing `!`. It is the last resort, not the first: a plain utility silently loses (see below). Check for a prop before reaching for it. Sitting a table flush inside a card, for example, used to need `className="px-0!"`; it is now a prop:
|
|
466
676
|
|
|
467
677
|
```tsx
|
|
468
678
|
<Card.Root>
|
|
469
679
|
<Card.Header title="Line items" />
|
|
470
|
-
<Card.Content
|
|
680
|
+
<Card.Content padding="none">
|
|
471
681
|
<Table.Root>{/* … */}</Table.Root>
|
|
472
682
|
</Card.Content>
|
|
473
683
|
</Card.Root>
|
|
@@ -494,6 +704,8 @@ This is the reason rule 3 sends you to props rather than to a more specific clas
|
|
|
494
704
|
<Card.Content className="px-0" />
|
|
495
705
|
```
|
|
496
706
|
|
|
707
|
+
(For this particular case, use `padding="none"`.)
|
|
708
|
+
|
|
497
709
|
State variants make it worse rather than better: `Button`'s `ghost` variant sets a hover color, and a `:hover` rule outranks an unprefixed utility on **specificity**, not merely order — so a plain `text-destructive` on a ghost button is red only until the pointer reaches it. `variant="destructive"` is the supported way to express that; where no such prop exists, `!` wins in every state because importance beats specificity.
|
|
498
710
|
|
|
499
711
|
### Styling by state (data attributes)
|
|
@@ -503,7 +715,7 @@ AppShell's UI components support data-attribute-based styling, following the [Ba
|
|
|
503
715
|
```css
|
|
504
716
|
/* Style a component based on its state */
|
|
505
717
|
.SwitchThumb[data-checked] {
|
|
506
|
-
background-color: var(--
|
|
718
|
+
background-color: var(--success-solid);
|
|
507
719
|
}
|
|
508
720
|
|
|
509
721
|
.MenuItem[data-highlighted] {
|
|
@@ -573,10 +785,11 @@ These are visual-composition rules every screen must follow, regardless of patte
|
|
|
573
785
|
|
|
574
786
|
| Intent | Pick |
|
|
575
787
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
576
|
-
| Destructive action (delete, void) | `Button variant="destructive"`; `bg-
|
|
577
|
-
| Non-blocking caution | `Badge variant="warning"`, or `bg-
|
|
578
|
-
| Confirmation / completed state | `Badge variant="success"`, or `bg-
|
|
579
|
-
|
|
|
788
|
+
| Destructive action (delete, void) | `Button variant="destructive"`; `bg-danger-solid` on custom surface; confirm in a dialog at `shadow-lg` |
|
|
789
|
+
| Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` |
|
|
790
|
+
| Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` |
|
|
791
|
+
| Informational callout | `Badge variant="info"`, or `Alert variant="info"` |
|
|
792
|
+
| Neutral callout | `Badge variant="neutral"`, or `Alert variant="neutral"` |
|
|
580
793
|
| Persistent panel (sidebar, header) | `shadow-sm` |
|
|
581
794
|
| Hovered / sticky surface | `shadow-md` |
|
|
582
795
|
| Popover / menu / tooltip | `bg-popover`, `shadow-md`, `duration-150` |
|
|
@@ -584,5 +797,5 @@ These are visual-composition rules every screen must follow, regardless of patte
|
|
|
584
797
|
| Hover / focus transition | `duration-150 ease-out` |
|
|
585
798
|
| State change (toggle, select) | `duration-200 ease-in-out` |
|
|
586
799
|
| Two-column detail at <1024 | right column collapses below main — do not override |
|
|
587
|
-
| Inline ID, code, table number | `font-mono text-
|
|
588
|
-
| Timestamp, label, subtle metadata | `text-
|
|
800
|
+
| Inline ID, code, table number | `font-mono text-code-sm` (identifiers) / `text-label-md tabular-nums` (figures) |
|
|
801
|
+
| Timestamp, label, subtle metadata | `text-body-sm text-muted-foreground` |
|