@uxelle/skills 0.2.2 → 0.2.4

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.
@@ -10,11 +10,11 @@ Card-styled readout for a headline figure with a label, optional qualifier, info
10
10
 
11
11
  | Prop | Type | Default | Description |
12
12
  |------|------|---------|-------------|
13
- | order | `"labelFirst" \| "valueFirst"` | `"labelFirst" as StatTileOrder` | Vertical order of the label row and the value row. |
14
- | label | `ReactNode` | `—` | Label in the label row. Occupies remaining width beside the info control. Pass a `string` (or `number`) to render with the default label typography (`Text type="Body Medium"`), or pass a `ReactNode` for full control. When you pass a `ReactNode` you own its accessibility: use meaningful text (not color or icon alone) and keep it non-interactive so it can name the group. |
15
- | value | `ReactNode` | `—` | Headline figure in the value row. Pass a `string` (or `number`) to render with the default value typography (`Text type="Display Medium"` as `<p>`), or pass a `ReactNode` for full control. A library `Text` node is rendered as `<p>` so a grid of StatTiles does not become a heading outline. When you pass any other `ReactNode`, make sure the figure reads as text and is not conveyed through color or an icon alone (WCAG 1.4.1). |
13
+ | order | `"labelFirst" \| "valueFirst"` | `"labelFirst"` | Vertical order of the label row and the value row. |
14
+ | label | `ReactNode` | `—` | Label in the label row. Occupies remaining width beside the info control and stays on one line: overflow ellipsizes. Hover or focus a truncated label to read the full name in a tooltip. A string (or number) is wrapped in **Text** with `type="Body Medium"`; pass a `Text` node for a different type (truncation is still applied). When you pass a `Text` or other `ReactNode` you own its accessibility: use meaningful text (not color or icon alone) and keep it non-interactive so it can name the group. |
15
+ | value | `ReactNode` | `—` | Headline figure in the value row. A string (or number) is wrapped in **Text** with `type="Display Small"` as `<p>`; pass a `Text` node for a different type. A `Text` keeps its `type` and is rendered as `<p>` so a grid of StatTiles does not become a heading outline. Any other `ReactNode` is allowed; make sure the figure reads as text and is not conveyed through color or an icon alone (WCAG 1.4.1). |
16
16
  | qualifier | `boolean` | `true` | When true, allows the qualifier to render when content is present. |
17
- | qualifierContent | `ReactNode` | `—` | Qualifier beside the value. Pass a `string` (or `number`) to render with the default qualifier typography (`Text type="Body Medium"`), or a `ReactNode` for full control. Only rendered when `qualifier` is true and content is present. When you pass a `ReactNode`, ensure it reads as text; do not rely on color alone to distinguish comparison periods. |
17
+ | qualifierContent | `ReactNode` | `—` | Qualifier beside the value. A string (or number) is wrapped in **Text** with `type="Body Medium"`; pass a `Text` node for a different type. Only rendered when `qualifier` is true and content is present. When you pass a `Text` or other `ReactNode`, ensure it reads as text; do not rely on color alone to distinguish comparison periods. |
18
18
  | tooltipSlot | `boolean` | `true` | When true, allows the tooltip slot to render when content is present. |
19
19
  | tooltipContent | `ReactNode` | `—` | Info control at the end of the label row (typically `Tooltip` wrapping `IconButton`). Only rendered when `tooltipSlot` is true and content is present. Icon-only triggers within this slot must carry an accessible name via `aria-label` on the interactive element (e.g. `<IconButton aria-label="More information" />`). |
20
20
  | trailingSlot | `boolean` | `false` | When true, allows the trailing region to render when content is present. The slot accepts any content. |
@@ -70,7 +70,7 @@ Card-styled readout for a headline figure with a label, optional qualifier, info
70
70
  // ReactNode override — caller owns accessibility (semantic element, text)
71
71
  <StatTile
72
72
  label={<Text type="Body Medium" width={false}>Fill rate</Text>}
73
- value={<Text type="Display Medium" as="p">94%</Text>}
73
+ value={<Text type="Display Small" as="p">94%</Text>}
74
74
  />
75
75
  ```
76
76
 
@@ -78,7 +78,7 @@ Card-styled readout for a headline figure with a label, optional qualifier, info
78
78
  // Polling dashboard — aria-live on the value announces only the change
79
79
  <StatTile
80
80
  label="Active sessions"
81
- value={<Text type="Display Medium" as="p" aria-live="polite">{count}</Text>}
81
+ value={<Text type="Display Small" as="p" aria-live="polite">{count}</Text>}
82
82
  />
83
83
  ```
84
84
 
@@ -111,40 +111,11 @@ Card-styled readout for a headline figure with a label, optional qualifier, info
111
111
 
112
112
  ## Notes
113
113
 
114
- - Pass a `string` (or `number`) to `label`, `value`, and `qualifierContent` to
115
- - render with each region's default typography — no `Text` wrapper needed. Pass
116
- - a `ReactNode` instead when you need full control (e.g. truncation, a custom
117
- - element); in that case you own its accessibility (see each prop). A library
118
- - `Text` passed as `value` is rendered as `<p>`.
119
- - Pass a library `Tooltip` (typically wrapping `IconButton`) through
120
- - `tooltipContent`. Toggle regions with `qualifier`, `tooltipSlot`, and
121
- - `trailingSlot`; empty content is not rendered. `trailingSlotContent` is an
122
- - open slot — any content is allowed. Surface, elevation, radius, and
123
- - padding come from `Card`. StatTile is a standalone tile: place it next to
124
- - other StatTiles in a grid. Do not wrap it in `Card`. A figure inside a chart
125
- - card or table cell is a different surface — compose `Text` there.
126
- - **Accessibility.** The root is a `role="group"` region. It is named
127
- - automatically from `label` (via `aria-labelledby`), so the group is announced
128
- - with context in both orders — including `"valueFirst"`, where the value
129
- - precedes the label in the DOM. Supply your own `aria-label` or
130
- - `aria-labelledby` to override the derived name; this is **required** when
131
- - there is no `label` (e.g. `aria-label="Median first response time, 1 day 12
132
- - hours"`) so the figure is not announced without context. In development,
133
- - StatTile warns when content is present but the group has no name. The trailing
134
- - slot is open, so its accessibility is the caller's responsibility — see
135
- - `trailingSlotContent`.
136
- - **Navigation.** Do not put `onClick` on StatTile — the root is a named group,
137
- - not a control, and a wrapped link would nest interactives when a tooltip is
138
- - present. Put the destination in `trailingSlotContent` as a `Link` or
139
- - `IconButton`.
114
+ - `label`, `value`, and `qualifierContent` each accept a string (or number) or a **Text** node. A string is wrapped in that region's default `Text` type — no wrapper needed. Pass a `Text` when you need a different `type` or a live region. A `Text` passed as `value` keeps its `type` and is rendered as `<p>`. The label always truncates to one line so a row of tiles stays scannable; hover or focus a truncated label to read the full name (`Text` Body Small). This overflow tooltip is separate from `tooltipContent` (the info control).
115
+ - Pass a library `Tooltip` (typically wrapping `IconButton`) through `tooltipContent`. Toggle regions with `qualifier`, `tooltipSlot`, and `trailingSlot`; empty content is not rendered. `trailingSlotContent` is an open slot — any content is allowed. Surface, elevation, radius, and padding come from `Card`. StatTile is a standalone tile: place it next to other StatTiles in a grid. Do not wrap it in `Card`. A figure inside a chart card or table cell is a different surface — compose `Text` there.
116
+ - **Accessibility.** The root is a `role="group"` region. It is named automatically from `label` (via `aria-labelledby`), so the group is announced with context in both orders — including `"valueFirst"`, where the value precedes the label in the DOM. Supply your own `aria-label` or `aria-labelledby` to override the derived name; this is **required** when there is no `label` (e.g. `aria-label="Median first response time, 1 day 12 hours"`) so the figure is not announced without context. In development, StatTile warns when content is present but the group has no name. The trailing slot is open, so its accessibility is the caller's responsibility — see `trailingSlotContent`.
117
+ - **Navigation.** Do not put `onClick` on StatTile — the root is a named group, not a control, and a wrapped link would nest interactives when a tooltip is present. Put the destination in `trailingSlotContent` as a `Link` or `IconButton`.
140
118
  - Set `order` to `"valueFirst"` to put the value row above the label row.
141
- - The root fills its parent by default (`fullWidth`). Set `fullWidth={false}`
142
- - to hug contents. Long label, value, and qualifier copy wrap inside the tile.
143
- - While data is loading, replace the tile with a `Skeleton` sized to the card
144
- - (full-width rectangle) inside `SkeletonGroup`. Do not pass a skeleton as
145
- - `value`.
146
- - For polling dashboards where the value updates in place, prefer putting
147
- - `aria-live="polite"` on the **value** node so only the changing figure is
148
- - announced (a root-level `aria-live` also works but re-announces the whole
149
- - group). When using a `string` `value`, wrap it yourself to place the live
150
- - region precisely.
119
+ - The root fills its parent by default (`fullWidth`). Set `fullWidth={false}` to hug contents. Long value and qualifier copy wrap inside the tile; the label ellipsizes instead of wrapping.
120
+ - While data is loading, replace the tile with a `Skeleton` sized to the card (full-width rectangle) inside `SkeletonGroup`. Do not pass a skeleton as `value`.
121
+ - For polling dashboards where the value updates in place, prefer putting `aria-live="polite"` on the **value** node so only the changing figure is announced (a root-level `aria-live` also works but re-announces the whole group). When using a `string` `value`, wrap it yourself to place the live region precisely.
@@ -12,10 +12,11 @@ Peer dependencies: React 18 or 19.
12
12
  npm install @uxelle/components @uxelle/themes
13
13
  ```
14
14
 
15
+ Import the two stylesheets:
16
+
15
17
  ```tsx
16
- import "@uxelle/components/styles.css";
17
- import "@uxelle/components/index.css";
18
- import "@uxelle/themes/themes/open-source/open-source.css";
18
+ import "@uxelle/components/base.css";
19
+ import "@uxelle/themes/open-source.css";
19
20
 
20
21
  import { Button } from "@uxelle/components";
21
22
 
@@ -24,22 +25,52 @@ function App() {
24
25
  }
25
26
  ```
26
27
 
27
- `styles.css` loads Material Symbols and the global baseline. `index.css` ships the bundled component rules. Load both stylesheets in the host app — the package JS does not import CSS automatically.
28
+ Then set the theme attributes on `<html>`:
29
+
30
+ ```html
31
+ <html data-open-source="light" data-color-switcher="default"></html>
32
+ ```
33
+
34
+ Both attributes are required. Every `--uxl-*` token is declared under
35
+ `[data-open-source="light"]` or `[data-open-source="dark"]`, so components render
36
+ unstyled without them. See [Themes](#themes) to switch modes at runtime.
37
+
38
+ `base.css` is the single component stylesheet. `@uxelle/components/styles.css`
39
+ (global baseline) and `@uxelle/components/index.css` (component rules)
40
+ stay exported if you need to load them separately.
28
41
 
29
42
  ### Next.js / SSR
30
43
 
31
- - Import the three stylesheets in `app/layout.tsx` (or a top-level provider). JS side effects will not style App Router.
44
+ - Import both stylesheets in `app/layout.tsx` (or a top-level provider). JS side effects will not style App Router.
32
45
  - Published `@uxelle/components` JS is prefixed with `"use client"`. Importing any component places that file in the client graph.
33
46
  - Pass `mobile={true}` or `mobile={false}` on `NavigationSide` so server and client markup match. Omit `mobile` only in client-only surfaces.
34
47
 
35
48
  Optional: for an agent-driven UI surface, also install `@uxelle/a2ui`.
36
49
 
50
+ ### Icons
51
+
52
+ `@uxelle/components` ships GPF chrome glyphs (`close`, `chevron_*`, `check`, and other names the library itself uses). For any other Material Symbol Sharp name, install `@uxelle/icons` and register once before the first `Icon` render:
53
+
54
+ ```bash
55
+ npm install @uxelle/icons
56
+ ```
57
+
58
+ ```tsx
59
+ import "@uxelle/icons/register";
60
+ ```
61
+
62
+ `@uxelle/a2ui` registers the full catalog when you import it. Unknown names render no glyph (an empty span). Development warns; production is silent, so an icon-only control with `aria-label` can still pass a11y tests while showing nothing. Register `@uxelle/icons` before the first render, or type names with `UxelleIconName` from `@uxelle/icons`.
63
+
37
64
  ## Themes
38
65
 
39
- Theme CSS is published as `@uxelle/themes` and generated from design tokens. Apply a theme by importing one CSS file. Set `data-open-source="light"` or `data-open-source="dark"` on `<html>`, plus `data-color-switcher="default"`. Optional: wrap the app with `UXelleThemeProvider` so those attributes stay in sync.
66
+ Theme CSS is published as `@uxelle/themes` and generated from design tokens. Quick Start covers the import and the two required attributes; this section covers changing them.
67
+
68
+ - `data-open-source` — `"light"` or `"dark"`
69
+ - `data-color-switcher` — `"default"`, `"default-subtle"`, `"success"`, `"warning"`, `"danger"`, or `"info"`
70
+
71
+ To drive both from React instead of hardcoding them, wrap the app with `UXelleThemeProvider`, which sets the attributes on `document.documentElement`:
40
72
 
41
73
  ```tsx
42
- import "@uxelle/themes/themes/open-source/open-source.css";
43
74
  import { UXelleThemeProvider } from "@uxelle/themes/react";
44
75
 
45
76
  function AppShell({ children }: { children: React.ReactNode }) {
@@ -77,6 +77,7 @@ palette. The full mapping of principles to uxElle mechanisms lives in
77
77
  card row -> [recipe-card-grid.md](recipe-card-grid.md);
78
78
  metrics -> [recipe-stat-callouts.md](recipe-stat-callouts.md);
79
79
  empty/loading/error -> [recipe-states.md](recipe-states.md);
80
+ header navigation -> [recipe-navigation.md](recipe-navigation.md);
80
81
  page footer -> [recipe-footer.md](recipe-footer.md);
81
82
  hero -> [recipe-hero.md](recipe-hero.md); feature -> [recipe-feature-section.md](recipe-feature-section.md);
82
83
  trust bar -> [recipe-logo-wall.md](recipe-logo-wall.md); testimonial -> [recipe-testimonial.md](recipe-testimonial.md);
@@ -136,7 +137,9 @@ Every generated screen must pass this. It is the definition of done.
136
137
  a type (an `h1` and `h2` both on `Display Extra Small` flatten the page), and
137
138
  headings inside a `Card` step down one stop ([density.md](density.md)).
138
139
  - **Responsive**: reflows mobile -> up with no px literals; clusters stack, `Table`
139
- scrolls, nav collapses; touch targets stay >= 24 CSS px.
140
+ scrolls, compact header menu sits in `Navigation` `trailingSlot` and opens a
141
+ `Sheet` from that side (panel interior matches IA depth); touch targets stay
142
+ >= 24 CSS px.
140
143
  - **Color discipline**: at most one dominant brand band per view; status palettes
141
144
  only for semantic status, never color-alone (paired with text or icon).
142
145
  - **Accessibility**: skip link + one `main`; landmarks named; focus visible; every
@@ -164,7 +167,8 @@ Marketing: [landing-page](recipe-landing-page.md).
164
167
  [form-section](recipe-form-section.md) · [query-bar](recipe-query-bar.md) ·
165
168
  [card-grid](recipe-card-grid.md) · [summary-list](recipe-summary-list.md) ·
166
169
  [states](recipe-states.md) · [stat-callouts](recipe-stat-callouts.md) ·
167
- [logo-wall](recipe-logo-wall.md) · [footer](recipe-footer.md). Marketing: [hero](recipe-hero.md) ·
170
+ [logo-wall](recipe-logo-wall.md) · [navigation](recipe-navigation.md) ·
171
+ [footer](recipe-footer.md). Marketing: [hero](recipe-hero.md) ·
168
172
  [feature-section](recipe-feature-section.md) · [testimonial](recipe-testimonial.md) ·
169
173
  [pricing](recipe-pricing.md) · [cta-band](recipe-cta-band.md).
170
174
 
@@ -13,6 +13,10 @@ Hosts inject `uxelleCatalogSchema` (and optionally a recipe's messages) into the
13
13
  agent prompt and render the result. Do not open package source to "discover" props —
14
14
  use the catalog schema the host provides.
15
15
 
16
+ Importing `@uxelle/a2ui` registers the full Material Symbols Sharp catalog. React
17
+ hosts that do not use A2UI must `import "@uxelle/icons/register"` themselves if
18
+ generated UI uses icon names beyond GPF chrome.
19
+
16
20
  ## Hard constraints
17
21
 
18
22
  - **Catalog adapters only.** No raw HTML, no custom components.
@@ -70,6 +74,11 @@ chips; a chip wider than its container ellipsizes so dismiss stays visible.
70
74
  Exclusive `A2uiChoiceChip` selection is one checked value in host state (toggles,
71
75
  not radios). Use `A2uiRadioGroup` or `A2uiSegmentedControl` for radio semantics.
72
76
 
77
+ `A2uiStatTile` labels stay on one line and ellipsize; hover or focus a truncated
78
+ label to read the full name. Pass `label` as a string — do not wrap it in
79
+ `A2uiText` or set truncation. Value and qualifier copy wrap. The overflow tooltip
80
+ is automatic and separate from `tooltipContent`.
81
+
73
82
  Known gaps — substitute rather than invent:
74
83
 
75
84
  | React | A2UI |
@@ -26,7 +26,7 @@ This file covers **composition**: landmarks, heading ranks, names, and live regi
26
26
  - `aria-describedby` — extra help, not the name (`fieldDescription`, instructions).
27
27
  - `aria-hidden="true"` — decorative / redundant graphics (default `Icon`).
28
28
  - `aria-live="polite"` — status that appears without moving focus. `assertive` only for urgent interruptions.
29
- - `aria-current="page"` — current item in nav (`NavLink`, or `activated` on `NavigationSideItem` / `NavigationSideSubItem` destinations — not accordion parents).
29
+ - `aria-current="page"` — current item in nav (`NavLink`, or `activated` on `NavigationSideItem` / `NavigationSideSubItem` destinations — not accordion parents). When the side rail is collapsed, an accordion parent of an activated nested row still shows the selected appearance so the current section remains visible on the icon-only rail.
30
30
  - `aria-expanded` — only on a real disclosure control, not on every section.
31
31
  - `aria-pressed` — toggle buttons (`IconButton` `activated`); `Switch` uses `checked` instead.
32
32
 
@@ -14,6 +14,10 @@ Load component and theme CSS as in
14
14
  [getting-started.md](../uxelle-components/getting-started.md). Do not set
15
15
  `font-family` in app CSS — `Text` uses theme tokens.
16
16
 
17
+ If generated UI uses Material Symbol names beyond GPF chrome (`home`, `mail`,
18
+ `campaign`, …), add `import "@uxelle/icons/register"` once at the app entry.
19
+ `@uxelle/a2ui` already registers the full catalog.
20
+
17
21
  On `<html>`:
18
22
 
19
23
  - Theme mode (host-owned): `data-open-source="light" | "dark"` (other themes use
@@ -74,7 +78,7 @@ First focusable node in the DOM, targeting `id="main-content"` on `<main>`:
74
78
  <a className="uxl-skip-link" href="#main-content">
75
79
  Skip to main content
76
80
  </a>
77
- {/* Navigation — recipe-app-chrome.md */}
81
+ {/* Navigation — recipe-navigation.md */}
78
82
  <main id="main-content" tabIndex={-1} style={{ flexGrow: 1, minHeight: 0, overflow: "auto" }}>
79
83
  {/* page column — how-to-page-layout.md */}
80
84
  {/* Footer — inside this scrollport; recipe-app-chrome.md */}
@@ -184,8 +184,10 @@ Bands: `tablet` (from `--uxl-theme-layout-tablet-screen-width-min`), `desktop`,
184
184
  when a component needs an explicit count.
185
185
  - **Header / query clusters**: title over actions, search full-width with
186
186
  filters/export below, when narrow; one row when wide (see below).
187
- - **Navigation**: collapse to an `IconButton` menu opening a `Sheet` / `Menu`
188
- drawer when narrow; verify `Navigation`'s built-in behavior first.
187
+ - **Navigation**: below desktop the menu `IconButton` defaults to `Navigation`
188
+ `trailingSlot` and opens a `Sheet` from that same side (`useBreakpointUp("desktop")`,
189
+ not the tablet band). From desktop up it may live in any slot. Destinations
190
+ follow IA depth ([recipe-navigation.md](recipe-navigation.md)).
189
191
  - **Tables**: `Table` scrolls horizontally when narrow (no stacked-row API); use
190
192
  `PaginationSimple` where full `Pagination` is too wide.
191
193
  - **Hero / media**: `Hero` `contentDirection` handles narrow stacking; related-field
@@ -2,7 +2,10 @@
2
2
 
3
3
  ## When
4
4
 
5
- React app frame: skip link, [`Navigation`](../uxelle-components/Navigation.md), scrolling `<main>`, optional [`Footer`](../uxelle-components/Footer.md). Serves both product and marketing shells.
5
+ React app frame: skip link, [`Navigation`](../uxelle-components/Navigation.md)
6
+ (composition in [recipe-navigation.md](recipe-navigation.md)), scrolling `<main>`,
7
+ optional [`Footer`](../uxelle-components/Footer.md). Serves both product and
8
+ marketing shells.
6
9
 
7
10
  ## When not
8
11
 
@@ -17,7 +20,11 @@ React app frame: skip link, [`Navigation`](../uxelle-components/Navigation.md),
17
20
 
18
21
  ## Regions
19
22
 
20
- skip link → `Navigation` (header) and/or `NavigationSide` (side rail wrapping `NavigationSideGroup` rows) → `<main id="main-content">` → *the experience's* content column ([how-to-page-layout.md](how-to-page-layout.md#who-owns-the-content-column)) → optional `Footer` **inside that same `<main>`**
23
+ skip link → optional [`BannerAnnouncement`](../uxelle-components/BannerAnnouncement.md) → `Navigation` (header) and/or `NavigationSide` (side rail wrapping `NavigationSideGroup` rows) → `<main id="main-content">` → *the experience's* content column ([how-to-page-layout.md](how-to-page-layout.md#who-owns-the-content-column)) → optional `Footer` **inside that same `<main>`**
24
+
25
+ Pick the **chrome pattern** before filling header slots (marketing header, signed-in
26
+ side rail, hybrid portal, or focused lockup-only) —
27
+ [recipe-navigation.md](recipe-navigation.md#chrome-pattern).
21
28
 
22
29
  ## Spacing
23
30
 
@@ -29,10 +36,20 @@ Do not wrap `Footer` in extra `Layout` padding. Band padding is catalog-owned: t
29
36
 
30
37
  ## A11y
31
38
 
32
- [how-to-accessibility.md](how-to-accessibility.md) and skip-link CSS in [how-to-host.md](how-to-host.md). One `main`. `aria-current="page"` on the current `NavLink` or `NavigationSideItem` / `NavigationSideSubItem` (`activated`). Decorative `Logo` / `Icon` beside a visible product name: `aria-hidden`.
39
+ [how-to-accessibility.md](how-to-accessibility.md) and skip-link CSS in [how-to-host.md](how-to-host.md). One `main`. `aria-current="page"` on the current `NavLink` or `NavigationSideItem` / `NavigationSideSubItem` (`activated`). Decorative `Logo` beside a visible product name: `aria-hidden`; name the home control on the wrapping `<a>` (`aria-label="Home"`).
33
40
 
34
41
  `NavLink` in `Navigation` is **header** page navigation. `NavigationSide` is the side-rail **chrome** (`<aside>`, or a `<header>` when `mobile`). `NavigationSideGroup` is the **side** navigation landmark for the experience (a named `<nav>` of destination rows) and belongs in `NavigationSide` `centerSlotContent`. Do not wrap `NavigationSide` or `NavigationSideGroup` in another `nav`. Pass `aria-label` when more than one group is on the page. Accordion parents are not destinations — put section landing pages on `NavigationSideSubItem` children. Tabs that only change which rows appear in a table are **data** navigation — put them on the table ([recipe-data-table-page.md](recipe-data-table-page.md)), not in `centerSlot` and not as a second nav under the page title.
35
42
 
43
+ ## Navigation
44
+
45
+ Header slot architecture — lockup (Logo as home, Condensed Alt name / Condensed
46
+ location or descriptor), empty `centerSlot` unless the IA is two or more levels
47
+ deep, compact hamburger in `trailingSlot` below desktop (omit it when
48
+ `NavigationSide` already owns the compact menu), Sheet `open` / `onOpenChange`
49
+ and `direction` matching the menu side, trailing search/session cluster,
50
+ IA-depth interiors — lives in [recipe-navigation.md](recipe-navigation.md). Pass
51
+ every slot explicitly (`null` to drop a region). Do not pass `false`.
52
+
36
53
  ## Side rail
37
54
 
38
55
  Compose `NavigationSide` with `centerSlotContent` of `NavigationSideGroup` + `NavigationSideItem` (optional `accordion` with `NavigationSideSubItem` children). Set `expanded={false}` on `NavigationSide` for the icon-only rail (groups inherit collapsed layout). SSR hosts must pass `mobile={true}` or `mobile={false}` so server and client markup match. Omit `mobile` only in client-only surfaces to follow the theme mobile range. Do not hard-code viewport widths. Trailing slot content on destination rows must be non-interactive. When the mobile layout is shown, the menu control opens a right `Sheet` with the same `centerSlotContent` and `bottomSlotContent` as the rail (labeled rows). Use `menuOpen` / `onMenuOpenChange` to close it after navigation.
@@ -47,6 +64,12 @@ Footer content architecture — the brand block, categorized link columns, the l
47
64
 
48
65
  Import from `@uxelle/components`. Put skip-link CSS in the host stylesheet.
49
66
 
67
+ Signed-in default: identity + utilities, **no header hamburger**. Destinations
68
+ and the compact menu live on `NavigationSide` ([Side rail](#side-rail)). Public
69
+ marketing chrome with a trailing Sheet is in
70
+ [recipe-navigation.md](recipe-navigation.md) and
71
+ [Help and marketing chrome](#help-and-marketing-chrome).
72
+
50
73
  ```tsx
51
74
  <Layout
52
75
  display="flex"
@@ -64,16 +87,25 @@ Import from `@uxelle/components`. Put skip-link CSS in the host stylesheet.
64
87
  <Navigation
65
88
  bottomSlot={null}
66
89
  leadingSlot={
67
- <Text type="Condensed Alt" truncation width={false}>
68
- Product name
69
- </Text>
70
- }
71
- centerSlot={
72
90
  <>
73
- <NavLink label="Home" href="/" aria-current="page" />
74
- <NavLink label="Settings" href="/settings" />
91
+ <a href="/" aria-label="Home">
92
+ <Logo name="generic" width={48} interactive aria-hidden />
93
+ </a>
94
+ <Layout
95
+ display="flex"
96
+ flexDirection="column"
97
+ gap={0}
98
+ >
99
+ <Text type="Condensed Alt" truncation width={false}>
100
+ Product name
101
+ </Text>
102
+ <Text type="Condensed" truncation width={false}>
103
+ Short descriptor
104
+ </Text>
105
+ </Layout>
75
106
  </>
76
107
  }
108
+ centerSlot={null}
77
109
  trailingSlot={
78
110
  <IconButton emphasis="low" size="small" iconName="search" aria-label="Search" />
79
111
  }
@@ -102,44 +134,89 @@ Import from `@uxelle/components`. Put skip-link CSS in the host stylesheet.
102
134
 
103
135
  ## Responsive
104
136
 
105
- - **Primary nav collapses on mobile.** Below tablet, swap the center [`NavLink`](../uxelle-components/NavLink.md) set for an [`IconButton`](../uxelle-components/IconButton.md) (`iconName="menu"`, `aria-label="Menu"`) in `trailingSlot` that opens a [`Sheet`](../uxelle-components/Sheet.md) (or [`Menu`](../uxelle-components/Menu.md)) holding the same destinations as a `List` of `NavLink`s. Confirm `Navigation`'s built-in behavior first and only add the drawer for what it does not handle; switch with `useBreakpointUp("tablet")` ([how-to-page-layout.md](how-to-page-layout.md#responsiveness)).
106
- - **Trailing utilities stay a row** at every width (search, language, Log in / Register) — [how-to-page-layout.md](how-to-page-layout.md#action-clusters).
137
+ - **Header navigation** — [recipe-navigation.md](recipe-navigation.md): below
138
+ desktop the visible menu control defaults to `trailingSlot` and opens a `Sheet`
139
+ from that same side. From desktop up the hamburger may live in any slot; omit
140
+ it when `centerSlot` is visible or when `NavigationSide` owns the compact menu.
141
+ When `centerSlot` holds category disclosures (two- or three-level IA), hide that
142
+ row below desktop (`useBreakpointUp("desktop")`). Trailing utilities stay a **row**
143
+ at every width — [how-to-page-layout.md](how-to-page-layout.md#action-clusters).
107
144
  - **Footer** columns wrap intrinsically ([recipe-footer.md](recipe-footer.md)); band padding is catalog-owned. **Content column** reflows through the chrome tokens with nothing to set per breakpoint.
108
145
 
109
146
  ## A2UI
110
147
 
111
- When-not for this recipe. Host owns chrome (skip link, viewport shell, html `data-*`). Catalog adapters: `A2uiNavigation`, `A2uiFooter`, `A2uiBannerAnnouncement` if the surface needs them — no `getUxelleRecipe("app-chrome")`. For the side rail only, `getUxelleRecipe("navigation-side")` from `@uxelle/a2ui`: trailing badges are `A2uiLabelBadge` with `"emphasis": "high"`; the account row is `A2uiList` + `A2uiListItem`. See [a2ui.md](a2ui.md).
148
+ When-not for this recipe. Host owns chrome (skip link, viewport shell, html `data-*`). Catalog adapters: `A2uiNavigation`, `A2uiFooter`, `A2uiBannerAnnouncement` if the surface needs them — no `getUxelleRecipe("app-chrome")`. Header slot defaults for `A2uiNavigation`: [recipe-navigation.md](recipe-navigation.md). For the side rail only, `getUxelleRecipe("navigation-side")` from `@uxelle/a2ui`: trailing badges are `A2uiLabelBadge` with `"emphasis": "high"`; the account row is `A2uiList` + `A2uiListItem`. See [a2ui.md](a2ui.md).
112
149
 
113
150
  ## Help and marketing chrome
114
151
 
115
- Use the same `Navigation` when there are **no** primary app links (help center, marketing). Pass `bottomSlot={null}` to hide the secondary bar. Unused **primary** slots must not be omitted — an omitted (`undefined`) `leadingSlot` / `centerSlot` / `trailingSlot` fills catalog demo content. Pass **`null`** to drop a slot entirely (`centerSlot={null}` when there is no center nav) — the same pattern as `bottomSlot={null}`. Do not pass `false`: it clears the content but still renders an empty slot wrapper in the flex row.
116
-
117
- Nav trailing (search, language, Log in / Register) stays a **row** at every width — [how-to-page-layout.md](how-to-page-layout.md#action-clusters). Do not stack those controls on mobile.
152
+ Use the same `Navigation` slot defaults as [recipe-navigation.md](recipe-navigation.md)
153
+ (help center, marketing). Pass `bottomSlot={null}` unless there is a persistent L2
154
+ bar. Unused **primary** slots must not be omitted — an omitted (`undefined`)
155
+ `leadingSlot` / `centerSlot` / `trailingSlot` fills catalog demo content. Pass
156
+ **`null`** to drop a slot. Do not pass `false`.
118
157
 
119
158
  `Breadcrumbs` belong in the **page column**, not in `Navigation`. Related articles are an in-page `Link` list under the article, not nav or crumbs.
120
159
 
160
+ Control the header Sheet with `open` / `onOpenChange` and close it from each leaf
161
+ `onClick` ([recipe-navigation.md](recipe-navigation.md)).
162
+
121
163
  ```tsx
164
+ const [menuOpen, setMenuOpen] = useState(false);
165
+
122
166
  <Navigation
123
167
  bottomSlot={null}
124
168
  leadingSlot={
125
- <a href="/" aria-label="Home">
126
- <Logo name="generic" width={40} interactive aria-hidden />
127
- </a>
169
+ <>
170
+ <a href="/" aria-label="Home">
171
+ <Logo name="generic" width={48} interactive aria-hidden />
172
+ </a>
173
+ <Layout
174
+ display="flex"
175
+ flexDirection="column"
176
+ gap={0}
177
+ >
178
+ <Text type="Condensed Alt" truncation width={false}>
179
+ Help center
180
+ </Text>
181
+ </Layout>
182
+ </>
128
183
  }
129
184
  centerSlot={null}
130
185
  trailingSlot={
131
186
  <>
132
187
  <LanguageSelector value="EN" />
133
188
  <Button emphasis="low" size="small">
134
- <Text type="Button" width>
135
- Log in
136
- </Text>
189
+ Log in
137
190
  </Button>
138
191
  <Button emphasis="high" size="small">
139
- <Text type="Button" width>
140
- Register
141
- </Text>
192
+ Register
142
193
  </Button>
194
+ <Sheet
195
+ open={menuOpen}
196
+ onOpenChange={setMenuOpen}
197
+ direction="Right"
198
+ title=""
199
+ aria-label="Menu"
200
+ trigger={
201
+ <IconButton
202
+ emphasis="low"
203
+ size="small"
204
+ iconName="menu"
205
+ iconVariant="sharpUnfilled"
206
+ aria-label="Menu"
207
+ />
208
+ }
209
+ >
210
+ <List>
211
+ <ListItem
212
+ interactive
213
+ href="/help"
214
+ centerText="Articles"
215
+ bottomText=""
216
+ onClick={() => setMenuOpen(false)}
217
+ />
218
+ </List>
219
+ </Sheet>
143
220
  </>
144
221
  }
145
222
  />
@@ -32,7 +32,8 @@ sections
32
32
 
33
33
  KPI and card grids reflow with `auto-fit` / `minmax` (no JS); the header stacks and
34
34
  its actions go `fullWidth` below tablet; the primary `Table` scrolls horizontally
35
- when narrow. See [how-to-page-layout.md](how-to-page-layout.md#responsiveness).
35
+ when narrow. StatTile labels ellipsize on one line so a KPI row stays scannable.
36
+ See [how-to-page-layout.md](how-to-page-layout.md#responsiveness).
36
37
 
37
38
  ## Spacing
38
39
 
@@ -49,7 +50,8 @@ KPIs rather than a dense monitoring surface — but pick one mode and hold it.
49
50
  ## A11y
50
51
 
51
52
  One `main`, one `h1`. Name each region (KPIs `aria-label="Key metrics"`, the primary
52
- surface by its heading). KPI deltas pair color with a sign or word. Loading and empty
53
+ surface by its heading). KPI deltas pair color with a sign or word. Truncated KPI
54
+ labels keep the full name on the group; hover or focus reveals it. Loading and empty
53
55
  for the primary surface follow [states](recipe-states.md). See
54
56
  [how-to-accessibility.md](how-to-accessibility.md).
55
57