@tailor-platform/app-shell 1.16.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.
- package/dist/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +692 -106
- package/dist/app-shell.js +6294 -4939
- package/dist/{appshell-B0wh5Lq0.js → appshell-CahhZy7v.js} +316 -297
- package/dist/semantic-contrast.test.ts +154 -0
- package/dist/style-entry.d.ts +117 -82
- package/dist/testing.d.ts +122 -85
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +101 -1
- package/dist/theme.bridge.css +91 -0
- package/dist/theme.css +6 -1
- package/dist/themes/_template.css +7 -1
- package/dist/themes/bloom.css +0 -6
- package/dist/themes/cream.css +0 -6
- package/dist/themes/default.css +389 -69
- package/dist/vite-plugin.d.ts +117 -82
- package/package.json +1 -1
- package/skills/app-shell-patterns/SKILL.md +7 -5
- package/skills/app-shell-patterns/references/api/define-module.md +2 -2
- 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 +267 -52
- 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 +6 -0
- package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -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 +255 -58
- package/skills/app-shell-patterns/references/migrations.md +21 -0
- 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/interaction-multi-select.md +129 -118
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +2 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: FilterRail
|
|
3
|
+
description: An always-visible faceted filter rail with user layout settings, driven by the same CollectionControl as DataTable
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/filter-rail.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# FilterRail
|
|
9
|
+
|
|
10
|
+
`FilterRail` shows every filterable axis beside the results instead of hiding it behind a filter dropdown. Each section shows its options up front, so users see what they can filter by without opening anything.
|
|
11
|
+
|
|
12
|
+
It writes the same `CollectionControl` a `DataTable` reads, so there is no second filter engine: a checkbox section **is** the `in` operator, a radio section is `eq`, a range is `between`.
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { FilterRail, type FilterRailSection } from "@tailor-platform/app-shell";
|
|
16
|
+
|
|
17
|
+
const sections: FilterRailSection[] = [
|
|
18
|
+
{
|
|
19
|
+
id: "category",
|
|
20
|
+
label: "Category",
|
|
21
|
+
control: "checkbox",
|
|
22
|
+
field: "category",
|
|
23
|
+
options: categories,
|
|
24
|
+
},
|
|
25
|
+
{ id: "status", label: "Status", control: "radio", field: "status", options: statuses },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
<FilterRail.Root control={control} sections={sections}>
|
|
29
|
+
<FilterRail.Sections />
|
|
30
|
+
</FilterRail.Root>;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Sections and parts
|
|
34
|
+
|
|
35
|
+
What the rail shows is **config**: `sections`, one entry per axis, in screen order. How the rail is assembled is **parts**: leave a part out and that feature is not rendered.
|
|
36
|
+
|
|
37
|
+
| Part | Renders |
|
|
38
|
+
| --------------------- | ----------------------------------------------------------------------------- |
|
|
39
|
+
| `FilterRail.Root` | The rail. Takes `control`, `sections`, `layout`. |
|
|
40
|
+
| `FilterRail.Header` | A title row with a slot for actions. |
|
|
41
|
+
| `FilterRail.ClearAll` | Clears the rail's own filters only. Hidden while none are active. |
|
|
42
|
+
| `FilterRail.Settings` | Show / hide, reorder and sort sections. Needs `layout`. |
|
|
43
|
+
| `FilterRail.Sections` | The sections, in the user's layout. Scrolls while the header stays put. |
|
|
44
|
+
| `FilterRail.Provider` | Holds the config when a `Trigger` sits outside the rail (see Narrow screens). |
|
|
45
|
+
| `FilterRail.Trigger` | The narrow-screen "Filters" button, placed anywhere inside `Provider`. |
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<FilterRail.Root control={control} sections={sections} layout={layout}>
|
|
49
|
+
<FilterRail.Header>
|
|
50
|
+
<FilterRail.ClearAll />
|
|
51
|
+
<FilterRail.Settings />
|
|
52
|
+
</FilterRail.Header>
|
|
53
|
+
<FilterRail.Sections />
|
|
54
|
+
</FilterRail.Root>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Section types
|
|
58
|
+
|
|
59
|
+
Every section has an `id`, a `label` and an optional `hint`. The operator is fixed per section — the user picks values, never operators.
|
|
60
|
+
|
|
61
|
+
| `control` | Commits | Notes |
|
|
62
|
+
| ------------- | ------------------------------------------ | ----------------------------------------------------------- |
|
|
63
|
+
| `checkbox` | `in` (or `nin`) with the ticked values | `sort`, `truncate` (default 8), `pinned`, `searchThreshold` |
|
|
64
|
+
| `radio` | `eq` (or `ne`) with one value | An "Any" row removes the filter; `anyLabel: null` hides it |
|
|
65
|
+
| `boolean` | `eq` with `true` / `false` | Three states: Any, `trueLabel`, `falseLabel` |
|
|
66
|
+
| `text` | `contains`, `hasPrefix` or `eq` | Debounced (`debounceMs`, default 250) |
|
|
67
|
+
| `numberRange` | `between` `{ min, max }`, or `gte` / `lte` | Commits on blur / Enter; one end alone is half-open |
|
|
68
|
+
| `date` | `eq`, `gte` or `lte` a `YYYY-MM-DD` | Uses `DatePicker` |
|
|
69
|
+
| `dateRange` | `between` `{ min, max }` | Uses `DateRangePicker`; optional `presets` |
|
|
70
|
+
| `custom` | Whatever `render` writes | Declares the `fields` it owns so Clear all includes them |
|
|
71
|
+
|
|
72
|
+
A checkbox list long enough to be truncated also gets a search box, so a value can be found without scrolling (`searchThreshold` overrides when it appears).
|
|
73
|
+
|
|
74
|
+
A field holds one filter, so two sections must not share a field; the rail warns in the console when they do.
|
|
75
|
+
|
|
76
|
+
#### Typing fields
|
|
77
|
+
|
|
78
|
+
Give `FilterRailSection` the row's keys so a `field` that is not on the table is a type error, the same way `createColumnHelper<Row>()` types columns. This works with a typed control or a plain one:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
const sections: FilterRailSection<keyof Product & string>[] = [
|
|
82
|
+
{ id: "category", label: "Category", control: "checkbox", field: "category", options },
|
|
83
|
+
// field: "categroy" → type error
|
|
84
|
+
];
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
With a typed control (from `useCollectionVariables({ tableMetadata })`), a section whose field the control does not have is also rejected.
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
const stockLevel: FilterRailSection = {
|
|
91
|
+
id: "stockLevel",
|
|
92
|
+
label: "Stock level",
|
|
93
|
+
control: "custom",
|
|
94
|
+
fields: ["stock"],
|
|
95
|
+
render: ({ getFilter, setFilter }) => (
|
|
96
|
+
<Button
|
|
97
|
+
variant="outline"
|
|
98
|
+
size="xs"
|
|
99
|
+
onClick={() => setFilter("stock", { operator: "between", value: { min: 1, max: 20 } })}
|
|
100
|
+
>
|
|
101
|
+
Low stock
|
|
102
|
+
</Button>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The rail never hides state: a selected option is never truncated away, never disabled and never hidden by a section's search box.
|
|
108
|
+
|
|
109
|
+
### Collapsing sections
|
|
110
|
+
|
|
111
|
+
Each section collapses and expands from the chevron by its title. Sections start expanded; set `defaultCollapsed: true` to start one collapsed (for a rarely used axis). While a section is collapsed, its header still shows how many values are selected and keeps its **Clear**, so a collapsed section never hides an applied filter. The open / closed state is kept while the page is open, including across the narrow-screen sheet opening and closing.
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
{ id: "warehouse", label: "Warehouse", control: "checkbox", field: "warehouse", options, defaultCollapsed: true }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Choosing a control
|
|
118
|
+
|
|
119
|
+
Checkbox and radio sections list every option, so they are only for **small, fixed sets** whose values you know up front — status, category, size, a handful of warehouses. Their `options` are a static array; the rail does not fetch them.
|
|
120
|
+
|
|
121
|
+
Do **not** use a checkbox or radio list for a field whose values **keep growing** — product names, SKUs, customers, suppliers, order numbers. The list gets longer with every new record, takes over the rail, and becomes a scroll hunt. Give those fields a search instead:
|
|
122
|
+
|
|
123
|
+
| The field's values are… | Use |
|
|
124
|
+
| ------------------------------------------------ | --------------------------------------------------------------------------- |
|
|
125
|
+
| A small fixed set (≈ up to 20), pick any | `checkbox` |
|
|
126
|
+
| A small fixed set, pick one | `radio` (or `boolean` for yes / no) |
|
|
127
|
+
| Open-ended and growing (names, codes, customers) | `text` — a search that commits `contains`, `hasPrefix` or `eq` |
|
|
128
|
+
| Open-ended, but users must pick exact records | a `custom` section rendering a searchable picker (e.g. an async `Combobox`) |
|
|
129
|
+
| Numbers or dates | `numberRange`, `date`, `dateRange` |
|
|
130
|
+
|
|
131
|
+
In development the rail warns in the console when a checkbox or radio section lists more than 50 options, as a nudge towards a search.
|
|
132
|
+
|
|
133
|
+
## User layout
|
|
134
|
+
|
|
135
|
+
`useFilterRailLayout(storageKey)` persists the user's section order, hidden sections and option sort to localStorage. Pass it as `layout` and add `FilterRail.Settings`. Hiding a section that carries a filter also clears that filter. To share a layout across a team, pass your own `{ value, onChange, onReset }` binding instead.
|
|
136
|
+
|
|
137
|
+
## Beside a DataTable
|
|
138
|
+
|
|
139
|
+
The rail sits in its own `Layout.Column`, outside `DataTable.Root`, which is why `control` is a prop. Do not also render `DataTable.Filters` for the same fields — one fact in two places can drift.
|
|
140
|
+
|
|
141
|
+
## Narrow screens
|
|
142
|
+
|
|
143
|
+
Below the `lg` breakpoint there is no room for a side column, so the rail turns itself into a left sheet behind a **Filters** button carrying the active count — the same way the Sidebar handles small screens. Apps write no breakpoint code.
|
|
144
|
+
|
|
145
|
+
By default the button appears where the rail would have been. To put it somewhere else — a table toolbar, a page header — wrap the page in `FilterRail.Provider`, move the config there, and place `FilterRail.Trigger` wherever you want it. The Trigger renders nothing on wide screens, and while one is mounted the rail skips its own button.
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
<FilterRail.Provider control={control} sections={sections}>
|
|
149
|
+
<Layout fill>
|
|
150
|
+
<Layout.Column area="left">
|
|
151
|
+
<FilterRail.Root>
|
|
152
|
+
<FilterRail.Sections />
|
|
153
|
+
</FilterRail.Root>
|
|
154
|
+
</Layout.Column>
|
|
155
|
+
<Layout.Column area="main">
|
|
156
|
+
<DataTable.Root value={table}>
|
|
157
|
+
<DataTable.Toolbar>
|
|
158
|
+
<FilterRail.Trigger />
|
|
159
|
+
</DataTable.Toolbar>
|
|
160
|
+
<DataTable.Table />
|
|
161
|
+
</DataTable.Root>
|
|
162
|
+
</Layout.Column>
|
|
163
|
+
</Layout>
|
|
164
|
+
</FilterRail.Provider>
|
|
165
|
+
```
|
|
@@ -381,6 +381,11 @@ The title styling is controlled by AppShell's theme. `SidebarGroup` takes no `cl
|
|
|
381
381
|
- You want to organize a large navigation menu
|
|
382
382
|
- Users need to focus on specific sections
|
|
383
383
|
|
|
384
|
+
**Use [SidebarSection](./sidebar-section.md) when:**
|
|
385
|
+
|
|
386
|
+
- You only need a heading above items that should always stay visible
|
|
387
|
+
- The items should keep top-level styling instead of an indented submenu
|
|
388
|
+
|
|
384
389
|
**Use SidebarItem directly when:**
|
|
385
390
|
|
|
386
391
|
- You have standalone top-level navigation
|
|
@@ -453,6 +458,7 @@ Without `to`, only the chevron is clickable (for expand/collapse).
|
|
|
453
458
|
## Related Components
|
|
454
459
|
|
|
455
460
|
- [SidebarItem](./sidebar-item.md) - Individual navigation items
|
|
461
|
+
- [SidebarSection](./sidebar-section.md) - Static headings for flat items
|
|
456
462
|
- [DefaultSidebar](./sidebar-layout.md) - Container for groups
|
|
457
463
|
- [WithGuard](./with-guard.md) - Conditional rendering
|
|
458
464
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: SidebarSection
|
|
3
|
+
description: Static, non-collapsible heading for grouping flat sidebar navigation items
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/sidebar-section.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# SidebarSection
|
|
9
|
+
|
|
10
|
+
`SidebarSection` places a static heading above a run of flat navigation items. Unlike [`SidebarGroup`](./sidebar-group.md), a section is neither collapsible nor navigable, and its items keep the same styling and indentation as top-level items.
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { SidebarSection } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import {
|
|
22
|
+
DefaultSidebar,
|
|
23
|
+
SidebarItem,
|
|
24
|
+
SidebarSection,
|
|
25
|
+
SidebarSeparator,
|
|
26
|
+
} from "@tailor-platform/app-shell";
|
|
27
|
+
|
|
28
|
+
<DefaultSidebar>
|
|
29
|
+
<SidebarItem to="/overview" />
|
|
30
|
+
|
|
31
|
+
<SidebarSeparator />
|
|
32
|
+
|
|
33
|
+
<SidebarSection title="Services">
|
|
34
|
+
<SidebarItem to="/ai-gateways" />
|
|
35
|
+
<SidebarItem to="/applications" />
|
|
36
|
+
<SidebarItem to="/auth-connections" />
|
|
37
|
+
</SidebarSection>
|
|
38
|
+
|
|
39
|
+
<SidebarSeparator />
|
|
40
|
+
|
|
41
|
+
<SidebarSection title="Logs">
|
|
42
|
+
<SidebarItem to="/activity-logs" />
|
|
43
|
+
<SidebarItem to="/dataplane-logs" />
|
|
44
|
+
</SidebarSection>
|
|
45
|
+
</DefaultSidebar>;
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Props
|
|
49
|
+
|
|
50
|
+
| Prop | Type | Default | Description |
|
|
51
|
+
| ---------- | --------------------------- | ------------ | --------------------------------- |
|
|
52
|
+
| `title` | `string \| LocalizedString` | **Required** | Section heading (supports i18n) |
|
|
53
|
+
| `children` | `React.ReactNode` | **Required** | Child items (usually SidebarItem) |
|
|
54
|
+
|
|
55
|
+
## Behavior
|
|
56
|
+
|
|
57
|
+
- **Not interactive.** The heading is plain text: it is not a link and has no expand/collapse toggle.
|
|
58
|
+
- **Flat items.** Children render at the same level as top-level `SidebarItem`s, not as an indented submenu.
|
|
59
|
+
- **Icon rail.** When the sidebar collapses to an icon rail, the heading is hidden and the items stay in the rail as ordinary icons. Unlike `SidebarGroup`, there is no hover flyout.
|
|
60
|
+
- **No automatic dividers.** Add a `SidebarSeparator` between sections where you want a line.
|
|
61
|
+
|
|
62
|
+
## Internationalization
|
|
63
|
+
|
|
64
|
+
Titles support i18n with `defineI18nLabels`:
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { defineI18nLabels } from "@tailor-platform/app-shell";
|
|
68
|
+
|
|
69
|
+
const labels = defineI18nLabels({
|
|
70
|
+
en: { services: "Services" },
|
|
71
|
+
ja: { services: "サービス" },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
<SidebarSection title={labels.t("services")}>
|
|
75
|
+
<SidebarItem to="/ai-gateways" />
|
|
76
|
+
</SidebarSection>;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## When to Use
|
|
80
|
+
|
|
81
|
+
**Use SidebarSection when:**
|
|
82
|
+
|
|
83
|
+
- You want to label a run of items with a heading, but keep them always visible
|
|
84
|
+
- The items are peers of your top-level items, not children of a parent page
|
|
85
|
+
- You want the items to stay as individual icons in the icon rail
|
|
86
|
+
|
|
87
|
+
**Use SidebarGroup when:**
|
|
88
|
+
|
|
89
|
+
- Users should be able to collapse the items
|
|
90
|
+
- The items belong under a parent with its own icon (or landing page via `to`)
|
|
91
|
+
- You need nesting
|
|
92
|
+
|
|
93
|
+
| Feature | SidebarSection | SidebarGroup |
|
|
94
|
+
| ------------------- | ----------------- | ------------------------- |
|
|
95
|
+
| **Collapsible** | ❌ No | ✅ Yes |
|
|
96
|
+
| **Heading is link** | ❌ No | Optional (`to` prop) |
|
|
97
|
+
| **Icon** | ❌ No | Optional |
|
|
98
|
+
| **Child styling** | Same as top level | Indented submenu |
|
|
99
|
+
| **Icon rail** | Heading hidden | Group icon opens a flyout |
|
|
100
|
+
| **Nesting** | Not intended | Can contain groups |
|
|
101
|
+
|
|
102
|
+
## Accessibility
|
|
103
|
+
|
|
104
|
+
The children are rendered as their own list, labelled by the heading, so screen readers announce the section name (for example, "Services, list, 3 items") when entering it.
|
|
105
|
+
|
|
106
|
+
## Related Components
|
|
107
|
+
|
|
108
|
+
- [SidebarGroup](./sidebar-group.md) - Collapsible, nested navigation groups
|
|
109
|
+
- [SidebarItem](./sidebar-item.md) - Individual navigation items
|
|
110
|
+
- [DefaultSidebar](./default-sidebar.md) - Container for sidebar navigation
|
|
111
|
+
|
|
112
|
+
## Related Concepts
|
|
113
|
+
|
|
114
|
+
- [Sidebar Navigation](../concepts/sidebar-navigation.md) - Sidebar customization guide
|
|
@@ -44,7 +44,7 @@ export function StepIndicator({ steps, current }: Props) {
|
|
|
44
44
|
flex items-center gap-2 rounded-md px-3 py-2
|
|
45
45
|
bg-muted text-sm text-muted-foreground
|
|
46
46
|
data-[active]:bg-primary data-[active]:text-primary-foreground
|
|
47
|
-
data-[completed]:text-
|
|
47
|
+
data-[completed]:text-success-text
|
|
48
48
|
"
|
|
49
49
|
>
|
|
50
50
|
{i < current && <Check className="size-4" />}
|
|
@@ -143,12 +143,18 @@ type AppShellPageProps = {
|
|
|
143
143
|
meta?: {
|
|
144
144
|
title?: LocalizedString;
|
|
145
145
|
icon?: ReactNode;
|
|
146
|
-
breadcrumbTitle?: string | ((segment: string) =>
|
|
146
|
+
breadcrumbTitle?: string | ((segment: string) => LocalizedString);
|
|
147
147
|
};
|
|
148
148
|
guards?: Guard[];
|
|
149
149
|
};
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
For a localized breadcrumb, use `meta: { breadcrumbTitle: labels.t("orders") }`, just like `meta.title`.
|
|
153
|
+
AppShell recognizes translation functions created by `labels.t()` and resolves them with the current locale.
|
|
154
|
+
For a dynamic label, use `breadcrumbTitle: (segment) => labels.t("order", { id: segment })`.
|
|
155
|
+
Other callbacks receive the URL segment; AppShell resolves any returned `LocalizedString` with the current locale.
|
|
156
|
+
The existing `() => labels.t("orders")` form remains supported. Wrap handwritten locale functions in an outer callback rather than passing them directly.
|
|
157
|
+
|
|
152
158
|
## Path Conventions
|
|
153
159
|
|
|
154
160
|
| Directory Name | Converts To | Description |
|
|
@@ -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.
|