@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.
Files changed (42) hide show
  1. package/dist/app-shell-core.css +1 -1
  2. package/dist/app-shell.d.ts +692 -106
  3. package/dist/app-shell.js +6294 -4939
  4. package/dist/{appshell-B0wh5Lq0.js → appshell-CahhZy7v.js} +316 -297
  5. package/dist/semantic-contrast.test.ts +154 -0
  6. package/dist/style-entry.d.ts +117 -82
  7. package/dist/testing.d.ts +122 -85
  8. package/dist/testing.js +1 -1
  9. package/dist/theme-bridge.test.ts +101 -1
  10. package/dist/theme.bridge.css +91 -0
  11. package/dist/theme.css +6 -1
  12. package/dist/themes/_template.css +7 -1
  13. package/dist/themes/bloom.css +0 -6
  14. package/dist/themes/cream.css +0 -6
  15. package/dist/themes/default.css +389 -69
  16. package/dist/vite-plugin.d.ts +117 -82
  17. package/package.json +1 -1
  18. package/skills/app-shell-patterns/SKILL.md +7 -5
  19. package/skills/app-shell-patterns/references/api/define-module.md +2 -2
  20. package/skills/app-shell-patterns/references/api/define-resource.md +2 -2
  21. package/skills/app-shell-patterns/references/api/use-app-shell-config.md +53 -47
  22. package/skills/app-shell-patterns/references/api/use-app-shell-data.md +30 -26
  23. package/skills/app-shell-patterns/references/api/use-app-shell.md +37 -30
  24. package/skills/app-shell-patterns/references/api/use-toast.md +28 -31
  25. package/skills/app-shell-patterns/references/components/app-shell.md +4 -4
  26. package/skills/app-shell-patterns/references/components/card.md +79 -7
  27. package/skills/app-shell-patterns/references/components/data-table.md +267 -52
  28. package/skills/app-shell-patterns/references/components/date-picker.md +32 -21
  29. package/skills/app-shell-patterns/references/components/filter-rail.md +165 -0
  30. package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -0
  31. package/skills/app-shell-patterns/references/components/sidebar-section.md +114 -0
  32. package/skills/app-shell-patterns/references/concepts/custom-components.md +1 -1
  33. package/skills/app-shell-patterns/references/concepts/file-based-routing.md +7 -1
  34. package/skills/app-shell-patterns/references/concepts/graphql.md +20 -14
  35. package/skills/app-shell-patterns/references/concepts/routing-navigation.md +41 -3
  36. package/skills/app-shell-patterns/references/concepts/sidebar-navigation.md +32 -0
  37. package/skills/app-shell-patterns/references/concepts/styling-theming.md +255 -58
  38. package/skills/app-shell-patterns/references/migrations.md +21 -0
  39. package/skills/app-shell-patterns/references/pages/document-detail.md +6 -6
  40. package/skills/app-shell-patterns/references/patterns/form-modal.md +26 -20
  41. package/skills/app-shell-patterns/references/patterns/interaction-multi-select.md +129 -118
  42. 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-status-completed
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) => 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
- // Query side — declares it cares about the Order typename
160
- const [{ data }] = useQuery({
161
- query: OrdersQuery,
162
- variables: { first: 25 },
163
- context: { additionalTypenames: ["Order"] },
164
- });
165
-
166
- // Mutation side — declares it produces Order writes
167
- const [, createOrder] = useMutation(CreateOrderMutation);
168
- await createOrder(
169
- { input },
170
- { additionalTypenames: ["Order"] }, // triggers refetch of any query subscribed to 'Order'
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 { currentUser } = useAppShellData();
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.