@tailor-platform/app-shell 1.17.0 → 1.19.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 (36) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +503 -137
  3. package/dist/app-shell.js +8847 -7378
  4. package/dist/{appshell-CahhZy7v.js → appshell-DQTz-QwK.js} +489 -379
  5. package/dist/style-entry.d.ts +1 -656
  6. package/dist/testing.d.ts +118 -113
  7. package/dist/testing.js +1 -1
  8. package/dist/theme.bridge.css +127 -40
  9. package/dist/theme.css +4 -3
  10. package/dist/themes/_template.css +5 -6
  11. package/dist/themes/bloom.css +0 -2
  12. package/dist/themes/cream.css +0 -2
  13. package/dist/themes/default.css +112 -77
  14. package/dist/vite-plugin.d.ts +118 -113
  15. package/package.json +8 -5
  16. package/skills/app-shell-patterns/SKILL.md +51 -32
  17. package/skills/app-shell-patterns/references/api/aggregations.md +131 -0
  18. package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
  19. package/skills/app-shell-patterns/references/api/define-module.md +2 -4
  20. package/skills/app-shell-patterns/references/components/action-panel.md +55 -23
  21. package/skills/app-shell-patterns/references/components/badge.md +7 -3
  22. package/skills/app-shell-patterns/references/components/data-table.md +724 -36
  23. package/skills/app-shell-patterns/references/components/form.md +19 -9
  24. package/skills/app-shell-patterns/references/components/item-list.md +191 -0
  25. package/skills/app-shell-patterns/references/components/select.md +10 -0
  26. package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -6
  27. package/skills/app-shell-patterns/references/components/text.md +235 -0
  28. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +16 -14
  29. package/skills/app-shell-patterns/references/concepts/styling-theming.md +46 -22
  30. package/skills/app-shell-patterns/references/migrations.md +23 -2
  31. package/skills/app-shell-patterns/references/pages/document-detail.md +36 -25
  32. package/skills/app-shell-patterns/references/patterns/form-modal.md +4 -1
  33. package/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
  34. package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +3 -2
  35. package/dist/semantic-contrast.test.ts +0 -154
  36. package/dist/theme-bridge.test.ts +0 -159
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  name: app-shell-patterns
3
3
  description: "Best-practice UI patterns and correct component usage for building pages in apps that use @tailor-platform/app-shell. Use when: building or editing any screen, page, list, table, detail view, form, modal, dialog, wizard, or bulk/confirm/toast interaction in an app with @tailor-platform/app-shell installed — when choosing the right AppShell component, layout, or design token for a UI — or when upgrading @tailor-platform/app-shell, or diagnosing styling, theming, or dark-mode breakage that appeared after a version bump."
4
+ metadata:
5
+ app-shell-version: "1.19.0"
4
6
  ---
5
7
 
6
8
  # App-Shell Patterns
@@ -9,6 +11,10 @@ description: "Best-practice UI patterns and correct component usage for building
9
11
 
10
12
  Select and implement the correct UI pattern using @tailor-platform/app-shell components.
11
13
 
14
+ ## Version
15
+
16
+ This skill was generated from `@tailor-platform/app-shell` **1.19.0**. Before relying on it, compare that with the version installed in the app you're working on (resolve `@tailor-platform/app-shell/package.json` from that app, not the monorepo root). If they differ, this copy is stale: tell the user and suggest re-syncing it from the installed package's `skills/app-shell-patterns/` directory.
17
+
12
18
  ## Concepts
13
19
 
14
20
  Cross-cutting guides that underpin every screen — styling & theming and the token system, GraphQL data access, building custom components, plus auth and routing. Patterns and components build on these.
@@ -64,6 +70,7 @@ Every AppShell component, its API surface, and usage guidance. Read the entry fo
64
70
  | [GlobalHeaderLayout](references/components/global-header-layout.md) | Opinionated layout with an app-wide header above the whole shell and a sidebar that collapses to a persistent icon rail |
65
71
  | [Grid](references/components/grid.md) | Generic CSS-Grid layout primitive for equal or custom-width columns with responsive reflow, auto-fit, and spanning |
66
72
  | [Input](references/components/input.md) | Styled text input with consistent theming |
73
+ | [ItemList](references/components/item-list.md) | Compact icon-and-label lists for consistent navigation inside and outside the Sidebar |
67
74
  | [Layout](references/components/layout.md) | Responsive column layout component for organizing page content in 1, 2, or 3 columns |
68
75
  | [Menu](references/components/menu.md) | Dropdown menu with a compound component API, supporting checkbox/radio items, groups, separators, and nested sub-menus |
69
76
  | [MetricCard](references/components/metric-card.md) | Compact card for dashboard KPI summaries with title, value, optional trend and description |
@@ -76,6 +83,7 @@ Every AppShell component, its API surface, and usage guidance. Read the entry fo
76
83
  | [Spinner](references/components/spinner.md) | Lightweight loading indicator component with accessible defaults and preset sizes |
77
84
  | [Table](references/components/table.md) | Semantic HTML table with pre-styled sub-components |
78
85
  | [Tabs](references/components/tabs.md) | Tab navigation with a compound component API |
86
+ | [Text](references/components/text.md) | Text with a typography role, rendered as an element you choose |
79
87
  | [Textarea](references/components/textarea.md) | Styled multi-line text input sized by rows, with automatic Field / React Hook Form integration |
80
88
  | [Timeline](references/components/timeline.md) | Composable primitives for building time-based layouts with axis guides, row backgrounds, intervals, and dependency links |
81
89
  | [Toolbar](references/components/toolbar.md) | Full-width action bars with composable rows, grouped controls, and keyboard navigation |
@@ -86,33 +94,34 @@ Every AppShell component, its API surface, and usage guidance. Read the entry fo
86
94
 
87
95
  Hooks, factory functions, guards, and router utilities exported by AppShell.
88
96
 
89
- | Name | Description |
90
- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- |
91
- | [createAIGatewayClient](references/api/create-ai-gateway-client.md) | Create a low-level AI Gateway client that reuses AppShell authentication |
92
- | [createTypedPaths](references/api/create-typed-paths.md) | Generate type-safe route paths from file-based routing |
93
- | [defineI18nLabels](references/api/define-i18n-labels.md) | Define internationalized labels for multi-language support |
94
- | [defineModule](references/api/define-module.md) | Define a top-level module that appears in the main navigation |
95
- | [defineResource](references/api/define-resource.md) | Define a nested resource (page) within a module |
96
- | [hidden](references/api/guards/hidden.md) | Guard function that denies access and returns 404 Not Found |
97
- | [Guards Overview](references/api/guards/overview.md) | Access control system for routes and components using guard functions |
98
- | [pass](references/api/guards/pass.md) | Guard function that allows access and continues to the next guard |
99
- | [redirectTo](references/api/guards/redirect-to.md) | Guard function that redirects to another page |
100
- | [useLocation](references/api/router/use-location.md) | Hook to access current location object with pathname, search, and state |
101
- | [useNavigate](references/api/router/use-navigate.md) | Hook for programmatic navigation between routes |
102
- | [useParams](references/api/router/use-params.md) | Hook to access dynamic route parameters |
103
- | [useAIChat](references/api/use-ai-chat.md) | Simple text-only chat hook for AI Gateway |
104
- | [useAppShell](references/api/use-app-shell.md) | Hook to access AppShell context data and configuration |
105
- | [useAppShellConfig](references/api/use-app-shell-config.md) | Hook to access AppShell configuration data |
106
- | [useAppShellData](references/api/use-app-shell-data.md) | Hook to access custom context data passed to AppShell |
107
- | [useAuth](references/api/use-auth.md) | Hook for accessing authentication state and methods |
108
- | [useOverrideBreadcrumb](references/api/use-override-breadcrumb.md) | Hook for dynamically overriding breadcrumb titles from within page components |
109
- | [usePageMeta](references/api/use-page-meta.md) | Hook to resolve page metadata from route path |
110
- | [useRegisterCommandPaletteActions](references/api/use-register-command-palette-actions.md) | Hook for registering contextual page actions into the CommandPalette |
111
- | [useResolvedLocale](references/api/use-resolved-locale.md) | Hook to access the full BCP-47 locale and language code from AppShell context |
112
- | [useRouteError](references/api/use-route-error.md) | Hook to access error details in custom error boundaries |
113
- | [useTheme](references/api/use-theme.md) | Hook for accessing and controlling theme (light/dark mode) |
114
- | [useTimeZone](references/api/use-time-zone.md) | Hook to access the configured IANA timezone from AppShell context |
115
- | [useToast](references/api/use-toast.md) | Hook for displaying toast notifications |
97
+ | Name | Description |
98
+ | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | [Value aggregations](references/api/aggregations.md) | Reusable selector-based sum, average, minimum, maximum, and non-null count over row collections, with optional result-only transformation |
100
+ | [createAIGatewayClient](references/api/create-ai-gateway-client.md) | Create a low-level AI Gateway client that reuses AppShell authentication |
101
+ | [createTypedPaths](references/api/create-typed-paths.md) | Generate type-safe route paths from file-based routing |
102
+ | [defineI18nLabels](references/api/define-i18n-labels.md) | Type-safe locale-first labels for component text and localized navigation metadata |
103
+ | [defineModule](references/api/define-module.md) | Define a top-level module that appears in the main navigation |
104
+ | [defineResource](references/api/define-resource.md) | Define a nested resource (page) within a module |
105
+ | [hidden](references/api/guards/hidden.md) | Guard function that denies access and returns 404 Not Found |
106
+ | [Guards Overview](references/api/guards/overview.md) | Access control system for routes and components using guard functions |
107
+ | [pass](references/api/guards/pass.md) | Guard function that allows access and continues to the next guard |
108
+ | [redirectTo](references/api/guards/redirect-to.md) | Guard function that redirects to another page |
109
+ | [useLocation](references/api/router/use-location.md) | Hook to access current location object with pathname, search, and state |
110
+ | [useNavigate](references/api/router/use-navigate.md) | Hook for programmatic navigation between routes |
111
+ | [useParams](references/api/router/use-params.md) | Hook to access dynamic route parameters |
112
+ | [useAIChat](references/api/use-ai-chat.md) | Simple text-only chat hook for AI Gateway |
113
+ | [useAppShell](references/api/use-app-shell.md) | Hook to access AppShell context data and configuration |
114
+ | [useAppShellConfig](references/api/use-app-shell-config.md) | Hook to access AppShell configuration data |
115
+ | [useAppShellData](references/api/use-app-shell-data.md) | Hook to access custom context data passed to AppShell |
116
+ | [useAuth](references/api/use-auth.md) | Hook for accessing authentication state and methods |
117
+ | [useOverrideBreadcrumb](references/api/use-override-breadcrumb.md) | Hook for dynamically overriding breadcrumb titles from within page components |
118
+ | [usePageMeta](references/api/use-page-meta.md) | Hook to resolve page metadata from route path |
119
+ | [useRegisterCommandPaletteActions](references/api/use-register-command-palette-actions.md) | Hook for registering contextual page actions into the CommandPalette |
120
+ | [useResolvedLocale](references/api/use-resolved-locale.md) | Hook to access the full BCP-47 locale and language code from AppShell context |
121
+ | [useRouteError](references/api/use-route-error.md) | Hook to access error details in custom error boundaries |
122
+ | [useTheme](references/api/use-theme.md) | Hook for accessing and controlling theme (light/dark mode) |
123
+ | [useTimeZone](references/api/use-time-zone.md) | Hook to access the configured IANA timezone from AppShell context |
124
+ | [useToast](references/api/use-toast.md) | Hook for displaying toast notifications |
116
125
 
117
126
  ## Available Pages
118
127
 
@@ -134,6 +143,12 @@ A page is the shape of a whole screen — the outer choice, made before picking
134
143
  | [`form/single-page`](references/patterns/form-single-page.md) | Single Page Form | Routed full-page form for moderate field count (6-15) without natural sectioning |
135
144
  | [`form/wizard`](references/patterns/form-wizard.md) | Wizard Form | Multi-stage create flow with 3-7 steps and per-step validation gates |
136
145
 
146
+ ### i18n
147
+
148
+ | Slug | Name | Description |
149
+ | --------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------ |
150
+ | [`i18n/labels`](references/patterns/i18n-labels.md) | i18n Labels | Decide UI language before implementation and keep display labels separate from stable data |
151
+
137
152
  ### interaction
138
153
 
139
154
  | Slug | Name | Description |
@@ -151,11 +166,12 @@ A page is the shape of a whole screen — the outer choice, made before picking
151
166
  ## How to Use
152
167
 
153
168
  1. Identify the user's intent (list, detail, form, interaction, screen composition, recipe)
154
- 2. Building a whole screen? Match it to a **page** entry first and read that. A page fixes the screen's shape — its layout, what goes where, and in what order — before any of the parts inside it are chosen.
155
- 3. Match each part you are building to a **pattern** entry slug from the tables above. A one-off recipe rather than a screen skips straight to this step.
156
- 4. Read the entry's detailed spec: `references/<category>/<slug>.md` (relative to this file)
157
- 5. Read the concept references for styling, design tokens, GraphQL, and custom components: `references/concepts/`; component and hook APIs live in `references/components/` and `references/api/`
158
- 6. Implement using ONLY the imports listed in the entry's `requiredImports` — for a page, the `requiredImports` of the pattern it sent you to
169
+ 2. Before implementing screen text, read [`i18n/labels`](references/patterns/i18n-labels.md) and establish the app's UI language independently of the specification's language. Use feature label catalogs by default, unless the project explicitly opts out for a single-language app. The API reference is [`defineI18nLabels`](references/api/define-i18n-labels.md).
170
+ 3. Building a whole screen? Match it to a **page** entry first and read that. A page fixes the screen's shape — its layout, what goes where, and in what order — before any of the parts inside it are chosen.
171
+ 4. Match each part you are building to a **pattern** entry slug from the tables above. A one-off recipe rather than a screen skips straight to this step.
172
+ 5. Read the entry's detailed spec: `references/<category>/<slug>.md` (relative to this file)
173
+ 6. Read the concept references for styling, design tokens, GraphQL, and custom components: `references/concepts/`; component and hook APIs live in `references/components/` and `references/api/`
174
+ 7. Implement using ONLY the imports listed in the selected entries' `requiredImports` — for a page, use those of its patterns; for localization, include the `i18n/labels` entry
159
175
 
160
176
  ## Rules
161
177
 
@@ -171,9 +187,12 @@ Full rationale in [`styling-theming`](references/concepts/styling-theming.md)
171
187
 
172
188
  - **One primary action per view:** at most one primary/filled `Button`; everything else is `outline`/`secondary`/`ghost`.
173
189
  - **Status badges by semantic color:** a **filled** semantic variant for the record's primary/lifecycle status, **`outline-*`** for secondary statuses, **`subtle-*`** for tags; reserve brand `default` for non-status emphasis.
190
+ - **Text uses `Text` and typography roles:** render headings, body copy, labels, and captions with [`Text`](references/components/text.md) and a `variant` (`heading-md`, `body-md`, `body-sm`, `label-md`, …) — never a bare `<p>`, `<h*>`, or `<span>` styled with size and weight utilities such as `text-lg font-semibold` or `text-sm`. Set the heading level with `Text.h1` to `Text.h6` (`<Text.h3 variant="heading-md">`), not by its size, and the color with a token in `className`. For other plain elements use `Text.p`, `Text.span` or `Text.div`; use `render` only to compose with another component. On an element you style yourself, such as a `th`, use the role utility (`text-label-md`). Pick the size with `variant`: a size class such as `text-sm` on `Text` has no effect. `Layout.Header` already renders the page title, and a form field label is [`Field.Label`](references/components/form.md), which already has the label role. Role table and color pairings: [`styling-theming`](references/concepts/styling-theming.md) → Typography.
191
+ - **Status and feedback color by semantic role:** use a component variant first (`Badge`, `Alert`, `Button variant="destructive"`). On your own element, use the roles `{info|success|warning|danger|neutral}-{surface|border|solid|text|contrast|indicator}`, for example `bg-warning-surface text-warning-text border-warning-border`. Error text is `text-danger-text`. Never write the deprecated `--status-*`, `--alert-*` or `--destructive` tokens or their utilities (`bg-status-*`, `text-alert-*`, `bg-destructive`, `text-destructive`) in new code; the next major removes them. When you edit a screen that still uses them, replace them: `text-destructive` → `text-danger-text`, `bg-status-completed` → `bg-success-solid`, `bg-alert-warning-background` → `bg-warning-surface`. Full mapping: [`styling-theming`](references/concepts/styling-theming.md) → Semantic color roles.
174
192
  - **No duplicate actions:** an action lives in exactly one place — never repeat the same action in both `Layout.Header` and `ActionPanel`.
175
193
  - **Action placement:** primary CTA + status in `Layout.Header`; workflow actions in `ActionPanel`; back/navigation in the breadcrumb — never in `ActionPanel`.
176
194
  - **Metric tiles always go in a `Grid`** (`columns={{ initial: 1, md: 2, xl: 4 }}`) — never one-per-row.
177
195
  - **Forms default to `form/modal`** — only build a routed full-page form when the design explicitly calls for one.
178
196
  - **Forms use AppShell `Form` + `Field`**, submitting via `onFormSubmit` — never a bare `<form onSubmit>` with `FormData`. `onFormSubmit` reads registered `Field.Root`s, so every control (dropdowns included) just needs a wrapping `Field.Root name="…"` — no `name` on the control, no `useState`. React Hook Form is optional, consumer-installed, and only warranted for cross-field validation, field arrays, or a Zod resolver. Details in [`Form`](references/components/form.md) and the `form/*` patterns.
179
197
  - **Handle every state:** loading (skeleton), empty (labelled empty state), and error (inline + retry) — never ship only the happy path.
198
+ - **Keep UI language consistent:** apply the chosen language to navigation, component text, accessibility labels, and feedback. Use the catalog's `useT()` for rendered strings and `labels.t()` for `LocalizedString` metadata; keep server codes and comparison values untranslated. See [`i18n/labels`](references/patterns/i18n-labels.md).
@@ -0,0 +1,131 @@
1
+ ---
2
+ title: Value aggregations
3
+ description: Reusable selector-based sum, average, minimum, maximum, and non-null count over row collections, with optional result-only transformation
4
+ ---
5
+
6
+ <!-- GENERATED by docs-kit from docs-src/hooks/aggregations.docs.outline.md — do not edit by hand. -->
7
+
8
+ # Value aggregations
9
+
10
+ `aggregations` creates reusable callbacks that select values from a row collection and aggregate them. Each callback accepts `{ rows: readonly TRow[] }`; the module has no dependency on React, DataTable, or column definitions.
11
+
12
+ ```tsx
13
+ import { aggregations, type Aggregation } from "@tailor-platform/app-shell";
14
+
15
+ type Line = { quantity: number | null };
16
+
17
+ const sum = aggregations.sum((row: Line) => row.quantity);
18
+ sum({ rows: [{ quantity: 1 }, { quantity: 2 }, { quantity: null }] }); // 3
19
+ sum({ rows: [{ quantity: 4 }, { quantity: 5 }] }); // 9: no previous aggregate state
20
+
21
+ const summary: Aggregation<Line, string> = aggregations.sum(
22
+ (row: Line) => row.quantity,
23
+ (value) => (value === null ? "Unavailable" : `${value} units`),
24
+ );
25
+ summary({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // "3 units"
26
+ ```
27
+
28
+ ## Operations
29
+
30
+ | Factory | Result | Empty or all-nullish selected values |
31
+ | ------------------------------ | --------------------------------------- | ------------------------------------ |
32
+ | `aggregations.sum(selector)` | Sum of finite numbers | `0` |
33
+ | `aggregations.avg(selector)` | Average of finite numbers | `null` |
34
+ | `aggregations.min(selector)` | Minimum finite number | `null` |
35
+ | `aggregations.max(selector)` | Maximum finite number | `null` |
36
+ | `aggregations.count(selector)` | Count of non-nullish values of any type | `0` |
37
+
38
+ A selector is required. It receives one row at a time and determines the values to aggregate. Numeric selectors must return `number | null | undefined`; count selectors may return any type. There is no automatic field lookup or connection to a column's `id`, `accessor`, or rendered content.
39
+
40
+ Each factory returns an `Aggregation<TRow, TResult>` callback. Pass a read-only or mutable row array through `{ rows }`; aggregation does not mutate it or retain state between invocations. Selectors and transformers must be pure and must not mutate rows. Exceptions from either callback propagate to the caller.
41
+
42
+ All operations skip selected `null` and `undefined` values. Numeric operations also validate selected values at runtime and return `null` for the entire result if any other value is not a finite JavaScript number, including numeric strings, booleans, dates, `NaN`, and infinities. They do not coerce values, compare dates or strings, or return a partial result. Average divides by the number of non-nullish numeric values, not the row count.
43
+
44
+ Calculations use ordinary JavaScript numbers. A non-finite calculated result returns `null`; for average, this includes an intermediate sum overflow even when the mathematical average could be finite. These functions do not provide arbitrary-precision arithmetic.
45
+
46
+ `count(selector)` counts selected `0`, `false`, and `""` as present values. It counts non-nullish values, not truthy values; use `rows.length` when every row should count.
47
+
48
+ ## Optional transformer
49
+
50
+ Pass a second function to transform the final result:
51
+
52
+ ```tsx
53
+ const averageText = aggregations.avg(
54
+ (row: Line) => row.quantity,
55
+ (value) => value?.toFixed(2) ?? "–",
56
+ );
57
+ averageText({ rows: [{ quantity: 1 }, { quantity: null }, { quantity: 2 }] }); // "1.50"
58
+
59
+ const detailed = aggregations.sum(
60
+ (row: Line) => row.quantity,
61
+ (total) => ({ total, valid: total !== null }),
62
+ );
63
+ detailed({ rows: [{ quantity: 1 }, { quantity: 2 }] }); // { total: 3, valid: true }
64
+ ```
65
+
66
+ The transformer is called exactly once per aggregation invocation, including for empty or invalid input. Numeric transformers receive `number | null`; count transformers receive `number`. Its return type becomes the callback's result type, and may be text, JSX, an object, or another value. The aggregation library does not interpret the transformed result.
67
+
68
+ A transformer receives only the result, not rows, selected values, columns, or UI context. Filter `rows` before invoking the callback when the calculation needs a different input set.
69
+
70
+ For detached callbacks, annotate the selector's row parameter as above so the row and transformed result types can both be inferred. Row types are not restricted to objects; for example, `aggregations.sum<number>((value) => value)({ rows: [1, 2] })` returns `3`.
71
+
72
+ ## Composing with business validation
73
+
74
+ An aggregation is an ordinary callback, so a wrapper can validate the supplied rows before calling it. Keep business rules in the wrapper, numeric calculation in the aggregation, and result formatting in the transformer. The transformer is not an error handler: it receives `null` for invalid numeric results but has no row context or business-error details; selector exceptions still propagate.
75
+
76
+ For example, reject mixed currencies before adding monetary amounts:
77
+
78
+ ```tsx
79
+ type MoneyLine = { amount: number | null; currency: string };
80
+
81
+ const sumAmount = aggregations.sum(
82
+ (row: MoneyLine) => row.amount,
83
+ (total) => (total === null ? "Invalid amount" : total.toLocaleString("en-US")),
84
+ );
85
+
86
+ const moneyFooter = (ctx: { rows: readonly MoneyLine[] }) => {
87
+ const currencies = new Set(
88
+ ctx.rows.filter((row) => row.amount != null).map((row) => row.currency),
89
+ );
90
+ if (currencies.size > 1) {
91
+ return `Mixed currencies: ${[...currencies].join(", ")}`;
92
+ }
93
+ return sumAmount(ctx);
94
+ };
95
+
96
+ moneyFooter({
97
+ rows: [
98
+ { amount: 1000, currency: "JPY" },
99
+ { amount: 500, currency: "JPY" },
100
+ ],
101
+ }); // "1,500"
102
+ moneyFooter({
103
+ rows: [
104
+ { amount: 1000, currency: "JPY" },
105
+ { amount: 500, currency: "USD" },
106
+ ],
107
+ }); // "Mixed currencies: JPY, USD"
108
+ ```
109
+
110
+ The guard ignores nullish amounts because they do not contribute to the sum. A rejected currency combination returns directly without evaluating the selector or transformer. Neither the wrapper nor the helper converts currencies or provides arbitrary-precision arithmetic.
111
+
112
+ Pass `moneyFooter` directly as a column's `footer`. See the [runnable DataTable example](../components/data-table.md#composing-with-business-validation) to toggle mixed currencies and observe the composed behavior.
113
+
114
+ ## DataTable integration
115
+
116
+ Pass an aggregation callback directly to a column footer. It is an ordinary footer renderer that reads the current `ctx.rows`; DataTable needs no aggregation-specific handling:
117
+
118
+ ```tsx
119
+ import { aggregations, createColumnHelper } from "@tailor-platform/app-shell";
120
+
121
+ const { column } = createColumnHelper<{ quantity: number | null }>();
122
+ column({
123
+ id: "quantity",
124
+ type: "number",
125
+ footer: aggregations.sum((row) => row.quantity),
126
+ });
127
+ ```
128
+
129
+ Inline selectors infer the row type from the surrounding footer definition. The selector is the source of truth even when the column has an `accessor`; write the same calculation in the selector if the footer should aggregate a computed cell value.
130
+
131
+ Use a transformer for simple footer text or JSX. DataTable only accepts callback results it can render; object-producing aggregations remain available for non-UI use. For the current-page scope, loading behavior, and custom renderers, see [DataTable column footers](../components/data-table.md#column-footers).
@@ -1,143 +1,175 @@
1
1
  ---
2
2
  title: defineI18nLabels
3
- description: Define internationalized labels for multi-language support
3
+ description: Type-safe locale-first labels for component text and localized navigation metadata
4
4
  ---
5
5
 
6
6
  <!-- GENERATED by docs-kit from docs-src/hooks/define-i18n-labels.docs.outline.md — do not edit by hand. -->
7
7
 
8
8
  # defineI18nLabels
9
9
 
10
- Creates internationalized labels that automatically resolve based on the current locale.
10
+ Defines a label catalog and returns two resolvers: a React Hook for rendered text and a `LocalizedString` factory for AppShell configuration. No additional i18n package is required.
11
11
 
12
- ## Signature
12
+ ## Define a Catalog
13
13
 
14
- ```typescript
15
- function defineI18nLabels<T extends Record<string, Record<string, string>>>(
16
- labels: T,
17
- ): I18nLabels<T>;
18
- ```
14
+ Create the catalog once at module scope, not inside a component. The object is **locale-first**: `en` is the required base locale; each locale contains a flat map of label keys to strings or functions that return strings.
15
+
16
+ ```tsx
17
+ // i18n-labels.ts
18
+ import { defineI18nLabels } from "@tailor-platform/app-shell";
19
+
20
+ export const labels = defineI18nLabels({
21
+ en: {
22
+ title: "Requests",
23
+ save: "Save",
24
+ greeting: ({ name }: { name: string }) => `Hello, ${name}!`,
25
+ itemCount: ({ count }: { count: number }) => (count === 1 ? "1 item" : `${count} items`),
26
+ "status.PENDING": "Pending",
27
+ unknownStatus: "Unknown status",
28
+ },
29
+ ja: {
30
+ title: "申請一覧",
31
+ save: "保存",
32
+ greeting: ({ name }: { name: string }) => `こんにちは、${name}さん!`,
33
+ itemCount: ({ count }: { count: number }) => `${count}件`,
34
+ "status.PENDING": "承認待ち",
35
+ unknownStatus: "不明な状態",
36
+ },
37
+ });
19
38
 
20
- ## Parameters
39
+ export const useT = labels.useT;
40
+ ```
21
41
 
22
- ### `labels`
42
+ `useT` above is exported by **your catalog module**, not directly by `@tailor-platform/app-shell`. The component examples below use this shared setup; import `labels` and `useT` from `./i18n-labels` in application files.
23
43
 
24
- - **Type:** `Record<string, Record<string, string>>`
25
- - **Required:** Yes
26
- - **Description:** Object mapping label keys to locale-specific strings
44
+ Label keys are flat strings, not nested objects. A dotted key such as `"status.PENDING"` is an ordinary flat string key.
27
45
 
28
46
  ## Return Value
29
47
 
30
- ```typescript
31
- {
32
- t: (key: keyof T) => LocalizedString;
48
+ | Member | Returns | Use for |
49
+ | -------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------- |
50
+ | `labels.useT()` | A resolver `t` that returns a `string` | Text rendered by a React component, string-valued props, toasts, and validation messages |
51
+ | `t(key, props?)` | `string` | A known label key, with required props for function-valued labels |
52
+ | `t.dynamic(key, fallback)` | `string` | A runtime string key for a **static** label |
53
+ | `labels.t(key, props?)` | `LocalizedString` | `meta.title`, `meta.breadcrumbTitle`, and props explicitly accepting `LocalizedString` |
54
+
55
+ `LocalizedString` is `string | ((locale: string) => string)`. `labels.t()` creates a locale callback, **not rendered text**. Do not put its result directly into JSX children or pass it to a prop that accepts only `string`.
56
+
57
+ ## In Components: `useT()`
58
+
59
+ Call the Hook at the top level of a component under `AppShell`. The returned resolver reads the current AppShell locale.
60
+
61
+ <example-preview name="in-components"></example-preview>
62
+
63
+ ```tsx
64
+ function InComponents() {
65
+ const t = useT();
66
+
67
+ return <Button>{t("save")}</Button>;
33
68
  }
34
69
  ```
35
70
 
36
- ## Usage
71
+ Use the same resolver for string props such as `placeholder`, `aria-label`, or `Layout.Header`'s `title`, and inside event handlers for localized notifications. Do not call a Hook in an event handler; capture `t` during render.
37
72
 
38
- ### Basic Labels
73
+ ## Function-valued Labels: Typed Props
39
74
 
40
- ```typescript
41
- import { defineI18nLabels } from "@tailor-platform/app-shell";
75
+ Use a function for a whole sentence containing runtime values. Its props are inferred from the English definition and checked for the other declared locales. Keep pluralization and word order inside each locale's function rather than concatenating translated fragments.
42
76
 
43
- const labels = defineI18nLabels({
44
- dashboard: {
45
- en: "Dashboard",
46
- ja: "ダッシュボード",
47
- },
48
- products: {
49
- en: "Products",
50
- ja: "商品",
51
- },
52
- settings: {
53
- en: "Settings",
54
- ja: "設定",
55
- },
56
- });
77
+ <example-preview name="dynamic-labels"></example-preview>
57
78
 
58
- // Use in modules
59
- const dashboardModule = defineModule({
60
- path: "dashboard",
61
- meta: {
62
- title: labels.t("dashboard"),
63
- },
64
- component: DashboardPage,
65
- });
79
+ ```tsx
80
+ function DynamicLabels() {
81
+ const t = useT();
82
+
83
+ return (
84
+ <Layout>
85
+ <Layout.Header title={t("greeting", { name: "Alex" })} />
86
+ <Layout.Column>{t("itemCount", { count: 3 })}</Layout.Column>
87
+ </Layout>
88
+ );
89
+ }
66
90
  ```
67
91
 
68
- ### Multiple Locales
92
+ Both `t("greeting", { name })` and `labels.t("greeting", { name })` require the correct props. When using `labels.t()` for metadata, props are bound **when `labels.t()` is called**, not fetched at render time. For a breadcrumb depending on a URL segment, see [Routing & Navigation](../concepts/routing-navigation.md#dynamic-breadcrumb-titles).
69
93
 
70
- ```typescript
71
- const labels = defineI18nLabels({
72
- welcome: {
73
- en: "Welcome",
74
- ja: "ようこそ",
75
- es: "Bienvenido",
76
- fr: "Bienvenue",
77
- },
78
- });
79
- ```
94
+ ## Runtime Keys: `t.dynamic()`
80
95
 
81
- ### In Components
96
+ `t.dynamic(key: string, fallback: string)` accepts a key constructed at runtime, for example from a server enum. Keep the enum stable and translate only its display label.
82
97
 
83
- ```typescript
84
- function WelcomeMessage() {
85
- const labels = defineI18nLabels({
86
- greeting: {
87
- en: "Hello",
88
- ja: "こんにちは",
89
- },
90
- });
98
+ <example-preview name="runtime-keys"></example-preview>
91
99
 
92
- return <h1>{labels.t("greeting")}</h1>;
100
+ ```tsx
101
+ function RuntimeKeys() {
102
+ const t = useT();
103
+ const status: string = "PENDING";
104
+
105
+ return <Badge variant="warning">{t.dynamic(`status.${status}`, t("unknownStatus"))}</Badge>;
93
106
  }
94
107
  ```
95
108
 
96
- ### With SidebarGroup
109
+ Resolution order:
110
+
111
+ 1. Look up the key in the current locale.
112
+ 2. If absent, look it up in `en`.
113
+ 3. If the resolved value is missing **or is a function**, return the supplied fallback unchanged.
97
114
 
98
- ```typescript
99
- import { SidebarGroup } from "@tailor-platform/app-shell";
115
+ The fallback is not translated automatically. Prefer another translated label, as above, instead of an English literal. `t.dynamic()` does not accept label props; use typed `t(key, props)` for function-valued labels. Prefer typed `t(key)` whenever the key is known at compile time.
100
116
 
101
- const labels = defineI18nLabels({
102
- admin: {
103
- en: "Administration",
104
- ja: "管理",
117
+ ## Navigation Metadata: `labels.t()`
118
+
119
+ Module and resource definitions are usually outside React components, so they cannot call Hooks. Pass the locale callback to AppShell, which resolves it using the active locale:
120
+
121
+ ```tsx
122
+ import { defineModule } from "@tailor-platform/app-shell";
123
+ import { labels } from "./i18n-labels";
124
+
125
+ const requestModule = defineModule({
126
+ path: "requests",
127
+ meta: {
128
+ title: labels.t("title"),
129
+ breadcrumbTitle: labels.t("title"),
105
130
  },
131
+ component: InComponents,
132
+ resources: [],
106
133
  });
107
-
108
- <SidebarGroup title={labels.t("admin")}>
109
- <SidebarItem to="/admin/users" />
110
- </SidebarGroup>
111
134
  ```
112
135
 
113
- ## Setting Locale
136
+ The same approach works for `defineResource().meta` and `SidebarGroup`, `SidebarSection`, and `SidebarItem` title overrides. For ordinary string-valued component props, use the Hook resolver instead.
114
137
 
115
- The active locale is controlled by the `locale` prop on `AppShell`:
138
+ ## Setting Locale and Fallbacks
116
139
 
117
- ```typescript
118
- <AppShell locale="ja" modules={modules}>
140
+ Choose the app's UI language explicitly when it is fixed:
141
+
142
+ ```tsx
143
+ <AppShell locale="ja-JP" modules={[requestModule]}>
119
144
  <SidebarLayout />
120
145
  </AppShell>
121
146
  ```
122
147
 
123
- Supported locales: `en`, `ja`
148
+ - AppShell normalizes a BCP-47 tag such as `ja-JP` to `ja` for label lookup. Use language keys such as `ja` in the catalog, not `ja-JP`.
149
+ - If `locale` is omitted, AppShell uses the browser's preferred language. If browser locale information is unavailable or invalid, it uses `en`.
150
+ - If a catalog has no definition for the selected language, labels fall back to `en`. A missing localized key also falls back to its English value at runtime.
151
+ - Custom catalogs can define other languages such as `fr`; this does **not** translate AppShell's built-in UI. Its shared built-in catalog currently supplies English and Japanese, with English fallback for other languages.
152
+ - For regional date and number formatting, use the full tag from [useResolvedLocale](./use-resolved-locale.md); label translation does not format dates, numbers, or time zones automatically.
124
153
 
125
- If not specified, auto-detects from browser settings.
154
+ ## TypeScript Contract
126
155
 
127
- ## TypeScript
156
+ With an ordinary inferred definition such as the catalog above:
128
157
 
129
- Full type safety:
158
+ - `en` is required, even for an app whose selected UI language is Japanese.
159
+ - Other explicitly declared locales must supply all base keys and compatible function props. Translation omissions become compile errors.
160
+ - `t()` and `labels.t()` reject unknown keys, missing or wrongly typed function props, and props passed to static labels.
161
+ - `t.dynamic()` deliberately accepts any string key; unknown keys are handled by its required fallback instead of compile-time validation.
130
162
 
131
- ```typescript
132
- const labels = defineI18nLabels({
133
- dashboard: { en: "Dashboard", ja: "ダッシュボード" },
134
- products: { en: "Products", ja: "商品" },
135
- });
163
+ These checks validate structure and call sites, not translation quality or language consistency.
136
164
 
137
- labels.t("dashboard"); // ✅ Valid
138
- labels.t("invalid"); // ❌ Type error
139
- ```
165
+ ## Exports
166
+
167
+ - `I18nLabels` — type
168
+ - `defineI18nLabels` — const
140
169
 
141
170
  ## Related
142
171
 
143
- - [defineModule](./define-module.md) - Use labels in modules
172
+ - [i18n Labels Pattern](../patterns/i18n-labels.md) — UI language decisions, key naming, file splitting, and server-originated text
173
+ - [defineModule](./define-module.md) — localized navigation metadata
174
+ - [defineResource](./define-resource.md) — localized resource metadata
175
+ - [useResolvedLocale](./use-resolved-locale.md) — full locale for formatting
@@ -209,10 +209,8 @@ const productsModule = defineModule({
209
209
  import { defineI18nLabels } from "@tailor-platform/app-shell";
210
210
 
211
211
  const labels = defineI18nLabels({
212
- dashboard: {
213
- en: "Dashboard",
214
- ja: "ダッシュボード",
215
- },
212
+ en: { dashboard: "Dashboard" },
213
+ ja: { dashboard: "ダッシュボード" },
216
214
  });
217
215
 
218
216
  const dashboardModule = defineModule({