@uxelle/skills 0.2.1-beta.0 → 0.2.3

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 (53) hide show
  1. package/README.md +3 -5
  2. package/dist/index.js +106 -96
  3. package/index.json +106 -96
  4. package/package.json +1 -1
  5. package/skills/uxelle-components/ChoiceChip.md +1 -1
  6. package/skills/uxelle-components/ChoiceChipGroup.md +3 -3
  7. package/skills/uxelle-components/FilterChip.md +2 -2
  8. package/skills/uxelle-components/FilterChipGroup.md +1 -1
  9. package/skills/uxelle-components/Hero.md +2 -2
  10. package/skills/uxelle-components/Icon.md +4 -4
  11. package/skills/uxelle-components/Image.md +10 -3
  12. package/skills/uxelle-components/MultiSelect.md +3 -2
  13. package/skills/uxelle-components/NavigationSide.md +3 -3
  14. package/skills/uxelle-components/NavigationSideItem.md +4 -4
  15. package/skills/uxelle-components/NavigationSideSubItem.md +3 -3
  16. package/skills/uxelle-components/SKILL.md +9 -2
  17. package/skills/uxelle-components/Select.md +2 -1
  18. package/skills/uxelle-components/StatTile.md +13 -42
  19. package/skills/uxelle-components/Table.md +7 -4
  20. package/skills/uxelle-components/getting-started.md +85 -0
  21. package/skills/uxelle-design-harness/SKILL.md +44 -20
  22. package/skills/uxelle-design-harness/a2ui.md +31 -2
  23. package/skills/uxelle-design-harness/density.md +138 -0
  24. package/skills/uxelle-design-harness/how-to-accessibility.md +3 -1
  25. package/skills/uxelle-design-harness/how-to-color.md +6 -3
  26. package/skills/uxelle-design-harness/how-to-host.md +8 -21
  27. package/skills/uxelle-design-harness/how-to-page-layout.md +51 -6
  28. package/skills/uxelle-design-harness/principles.md +10 -6
  29. package/skills/uxelle-design-harness/recipe-app-chrome.md +20 -16
  30. package/skills/uxelle-design-harness/recipe-card-grid.md +14 -5
  31. package/skills/uxelle-design-harness/recipe-cta-band.md +16 -6
  32. package/skills/uxelle-design-harness/recipe-dashboard-overview.md +21 -7
  33. package/skills/uxelle-design-harness/recipe-data-table-page.md +30 -137
  34. package/skills/uxelle-design-harness/recipe-feature-section.md +9 -7
  35. package/skills/uxelle-design-harness/recipe-footer.md +7 -4
  36. package/skills/uxelle-design-harness/recipe-form-section.md +2 -2
  37. package/skills/uxelle-design-harness/recipe-hero.md +35 -15
  38. package/skills/uxelle-design-harness/recipe-landing-page.md +27 -4
  39. package/skills/uxelle-design-harness/recipe-logo-wall.md +11 -9
  40. package/skills/uxelle-design-harness/recipe-multi-step-flow.md +5 -3
  41. package/skills/uxelle-design-harness/recipe-page-header.md +14 -4
  42. package/skills/uxelle-design-harness/recipe-page-shell.md +2 -2
  43. package/skills/uxelle-design-harness/recipe-pricing.md +5 -2
  44. package/skills/uxelle-design-harness/recipe-query-bar.md +21 -5
  45. package/skills/uxelle-design-harness/recipe-record-detail.md +5 -2
  46. package/skills/uxelle-design-harness/recipe-settings-page.md +3 -3
  47. package/skills/uxelle-design-harness/recipe-stat-callouts.md +14 -7
  48. package/skills/uxelle-design-harness/recipe-states.md +9 -4
  49. package/skills/uxelle-design-harness/recipe-summary-list.md +5 -3
  50. package/skills/uxelle-design-harness/recipe-template.md +6 -0
  51. package/skills/uxelle-design-harness/recipe-testimonial.md +5 -3
  52. package/skills/uxelle-design-harness/spacing-steps.md +17 -8
  53. package/skills/uxelle-design-harness/tokens.md +45 -4
@@ -0,0 +1,138 @@
1
+ # Density: spacing and type by experience
2
+
3
+ The same structural role wants different space in different experiences. A dense
4
+ console packs sections together so more data fits on screen; a marketing page opens
5
+ them up so one idea lands per band. Both are correct — on the *same* grid, with the
6
+ *same* ramp.
7
+
8
+ Density is the lever. Pick a **mode** for the experience, then resolve each spacing
9
+ **role** and each **heading rank** through that mode. Roles are the vocabulary the
10
+ recipes speak in ("section gap", "field stack"); the mode decides which ramp step
11
+ each one lands on ([spacing-steps.md](spacing-steps.md)).
12
+
13
+ ## 1. Pick the mode
14
+
15
+ One mode per experience, chosen once before you write any layout:
16
+
17
+ - **`compact`** — data-dense product surfaces where screen real estate is the
18
+ scarce resource: dashboards, data tables, consoles, admin and ops tooling,
19
+ monitoring, inbox/queue views.
20
+ - **`standard`** — task product surfaces where comprehension beats density:
21
+ forms, settings, record detail, wizards, generic page bodies. **This is the
22
+ default** when the brief is unclear.
23
+ - **`spacious`** — public marketing and editorial surfaces that sell an idea:
24
+ landing pages, heroes, pricing, closing CTAs, feature bands.
25
+
26
+ State the mode in a comment at the top of the surface so the choice is auditable:
27
+
28
+ ```tsx
29
+ // Density: compact (data-dense dashboard) — see density.md
30
+ ```
31
+
32
+ ## 2. Resolve the roles
33
+
34
+ | Role — what it separates | `compact` | `standard` | `spacious` |
35
+ |---|---|---|---|
36
+ | **Flush** — lines already separated by their line box | `0` | `0` | `0` |
37
+ | **Tight** — lockup internals, chip rows, label over value | `micro-1` | `micro-2` | `micro-3` |
38
+ | **Control** — related fields, same-row clusters, toolbars | `micro-3` | `small-4` | `small-6` |
39
+ | **Group** — subgroups within a section, bespoke section padding | `small-6` | `medium-8` | `medium-10` |
40
+ | **Section** — between page sections | `medium-9` | `medium-12` | `large-14` |
41
+ | **Band** — block padding on a full-bleed band | `medium-12` | `large-13` | `large-15` |
42
+
43
+ `large-16` is reserved for a deliberate editorial statement — a full-viewport hero
44
+ band — not the default `spacious` band. `small-5`, `medium-7`, and `medium-11` are
45
+ tuning room between modes; reach for them only to fix a specific rhythm problem.
46
+
47
+ Emit the resolved token, not the role name — a compact section gap is
48
+ `gap="var(--uxl-theme-layout-spacing-medium-9)"`. There is no `--uxl-*` density
49
+ variable and no app-owned alias layer; resolve at generation time.
50
+
51
+ `standard` is the harness baseline — it is the value every recipe used before
52
+ density existed, so a `standard` surface looks exactly as it always did.
53
+
54
+ ## 3. Resolve the heading ranks
55
+
56
+ Rank is semantics; **type** is what makes rank visible. Map every heading through
57
+ the same mode so size and weight carry hierarchy — never color
58
+ ([principles.md](principles.md)).
59
+
60
+ | Rank | `compact` | `standard` | `spacious` |
61
+ |---|---|---|---|
62
+ | `h1` — one per page | `Display Small` | `Display Small` | `Display Large` |
63
+ | `h2` — page section | `Display Extra Small` | `Display Extra Small` | `Display Medium` |
64
+ | `h3` — subsection | `Component Medium` | `Component Medium` | `Display Small` |
65
+ | Body copy | `Body Medium` | `Body Medium` | `Body Medium` |
66
+
67
+ **Consecutive ranks must never share a type.** The `Display` scale is `Large`
68
+ (64px) → `Medium` (45) → `Small` (32) → `Extra Small` (22), then `Component Medium`
69
+ (18) and `Body Medium` (16) on desktop. An `h1` and `h2` both set to
70
+ `Display Extra Small` render identically and flatten the page, which reads as one
71
+ undifferentiated block no matter how correct the markup is.
72
+
73
+ `compact` and `standard` share the heading ramp on purpose: density is a *spacing*
74
+ concern, while Display-led headlines are a *marketing* one. On a genuinely dense
75
+ console where vertical space is critical, `compact` may drop to `h1`
76
+ `Display Extra Small` / `h2` `Component Medium` — still one full stop apart.
77
+
78
+ **Step down one stop inside a bounded container, but never below
79
+ `Display Extra Small`.** A heading inside a `Card`, `ProductCard`, or tile takes the
80
+ type one stop below its rank's default, because the container's border and padding
81
+ already supply the separation the larger type would carry — a `spacious` feature
82
+ card's `h2` is `Display Small`, not `Display Medium`. `Display Extra Small` is the
83
+ floor for any heading: a product `h2` already sits there, so a product card heading
84
+ stays put rather than dropping to `Component Medium`, which would read as a label
85
+ instead of a heading.
86
+
87
+ Dense supporting text — table cells, metadata, KPI labels — may use `Body Small`
88
+ (14px) under `compact`. Never go below `Body Small` for reading copy.
89
+
90
+ ## 4. Guardrails
91
+
92
+ **The grid never moves.** Page chrome — `--uxl-breakpoints-max-container-width`,
93
+ `-margin`, and `-gutter` — is **density-invariant**. Density modulates rhythm
94
+ *within* and *between* blocks, never the column cap, the page inset, or column
95
+ gaps. That is what keeps a compact page and a spacious page on one grid, and it is
96
+ the Swiss commitment in [principles.md](principles.md). Grid gaps stay
97
+ `--uxl-breakpoints-gutter` in every mode.
98
+
99
+ **Keep the gap roles at least ~1.5x apart.** A reader infers grouping by comparing
100
+ gaps, so **Tight → Control → Group → Section** must stay visibly distinct or the
101
+ page reads as undifferentiated mush. Every mode above satisfies this (`compact`
102
+ runs 4 → 10 → 16 → 24px on desktop). This is the failure mode of `compact`: tighten
103
+ the section gap to `medium-9` while leaving the group gap at `medium-8` and sections
104
+ stop being legible as sections. Tighten *the whole column* or none of it — never one
105
+ role in isolation.
106
+
107
+ **Band** is exempt: it is padding inset from a band edge, not a gap between
108
+ siblings, so it is not compared against the gap roles. A `spacious` band's `large-15`
109
+ padding sitting near the `large-14` section gap is correct, not a collision.
110
+
111
+ **`compact` has an accessibility floor.** Tightening must never bring touch targets
112
+ below 24 CSS px or let interactive rows collide. Catalog controls already meet the
113
+ floor — do not claw space back by shrinking hit areas
114
+ ([how-to-accessibility.md](how-to-accessibility.md)).
115
+
116
+ **No breakpoint logic for density.** The ramp already remaps per breakpoint (a
117
+ `spacious` section gap is 72px on desktop and 48px on mobile on its own), so density
118
+ and responsiveness compose for free. Never add `matchMedia` or a second mode for
119
+ small screens ([how-to-page-layout.md](how-to-page-layout.md#responsiveness)).
120
+
121
+ **Do not mix modes in one experience.** A dense surface nested inside a spacious
122
+ page (a pricing comparison on a landing page, a table in a marketing section) keeps
123
+ its *own internals* compact, but the surrounding **section** rhythm stays the page's
124
+ mode. Density describes the experience, not each component.
125
+
126
+ ## 5. Out of scope
127
+
128
+ Density does not reach into **component internals**. `Card` padding, `Table` row
129
+ height, `Button` insets, `Lockup` internals, and `Footer` band padding are
130
+ component-owned and already tuned — do not wrap a component in compensating padding
131
+ or override its spacing to hit a mode.
132
+
133
+ ## A2UI
134
+
135
+ Chat surfaces are inherently narrow and have no full-bleed bands, so **`spacious`
136
+ never applies** and the `Band` role has no meaning. Use `standard` by default and
137
+ `compact` for data-heavy runtime output, resolving the same
138
+ `var(--uxl-theme-layout-spacing-*)` strings ([a2ui.md](a2ui.md)).
@@ -2,6 +2,8 @@
2
2
 
3
3
  WCAG 2.x **Level AA** for pages and composed experiences. Prefer catalog components (`Button`, `Textfield`, `Textarea`, `Dialog`, …) so keyboard, focus rings, and field associations come for free. Contrast is theme-owned — use `--uxl-color-switcher-*`.
4
4
 
5
+ This file covers **composition**: landmarks, heading ranks, names, and live regions across a whole page. Component-level patterns (keyboard behavior, focus management, ARIA inside a single component) live in the workspace rule `.cursor/rules/accessibility.mdc`, and token/color relationships are owned by the theme layer. Read that rule when building or fixing a component rather than a page.
6
+
5
7
  ## Baseline
6
8
 
7
9
  - One `h1`, then `h2`… with no skipped ranks (`Text as="h1"` / `as="h2"`).
@@ -24,7 +26,7 @@ WCAG 2.x **Level AA** for pages and composed experiences. Prefer catalog compone
24
26
  - `aria-describedby` — extra help, not the name (`fieldDescription`, instructions).
25
27
  - `aria-hidden="true"` — decorative / redundant graphics (default `Icon`).
26
28
  - `aria-live="polite"` — status that appears without moving focus. `assertive` only for urgent interruptions.
27
- - `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.
28
30
  - `aria-expanded` — only on a real disclosure control, not on every section.
29
31
  - `aria-pressed` — toggle buttons (`IconButton` `activated`); `Switch` uses `checked` instead.
30
32
 
@@ -10,8 +10,10 @@ in [tokens.md](tokens.md)); choose which palette those roles resolve to with
10
10
  `data-color-switcher="<palette>"` on a region remaps the `--uxl-color-switcher-*`
11
11
  roles for that subtree; nested components inherit from the nearest ancestor.
12
12
 
13
- - Set it on a **region** — a nav band, hero, section band, banner, or badge — not
14
- on `<html>`. The host owns the page-level default (`default`).
13
+ - Set it on a **region** — a nav band, hero, section band, banner, or badge. The
14
+ **host** owns the one page-level default on `<html>`
15
+ (`data-color-switcher="default"`, set once during host setup —
16
+ [how-to-host.md](how-to-host.md)); recipes and page content never touch `<html>`.
15
17
  - Some components take a `dataColorSwitcher` prop *or* the `data-color-switcher`
16
18
  attribute (e.g. `LabelBadge`, `BannerAnnouncement`, `Button`, `DynamicAngle*`).
17
19
  The attribute wins when both are set; for buttons rendered into another
@@ -72,7 +74,8 @@ To choose:
72
74
 
73
75
  - Do apply a palette on the region, and style children with role tokens.
74
76
  - Do keep status palettes for real status, paired with text or an icon.
75
- - Don't set `data-color-switcher` on `<html>` or invent palette names.
77
+ - Don't re-set `data-color-switcher` on `<html>` from page content or a recipe
78
+ (host setup only), and don't invent palette names.
76
79
  - Don't hardcode hex/rgb, and don't stack multiple loud brand bands in one view.
77
80
 
78
81
  ## A2UI
@@ -10,35 +10,22 @@ Agents consume **published packages**, not repo filesystem paths. Do not import
10
10
 
11
11
  ## Load CSS
12
12
 
13
- Theme CSS is a **placeholder import** — use the theme id the host documents.
14
- Open-source example:
13
+ Load component and theme CSS as in
14
+ [getting-started.md](../uxelle-components/getting-started.md). Do not set
15
+ `font-family` in app CSS — `Text` uses theme tokens.
15
16
 
16
- ```ts
17
- import "@uxelle/themes/open-source";
18
- import "@uxelle/components/styles.css";
19
- ```
20
-
21
- Until `@uxelle/components/styles.css` is the full bundle, also load extracted
22
- component CSS:
23
-
24
- ```ts
25
- import "@uxelle/components/index.css";
26
- ```
27
-
28
- Enterprise hosts swap the theme module (`@uxelle/themes/<theme-id>`) and the root
29
- data attribute. Do not set `font-family` in app CSS — `Text` uses theme tokens.
30
- Load the typeface the theme expects (open-source: Open Sans).
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.
31
20
 
32
21
  On `<html>`:
33
22
 
34
23
  - Theme mode (host-owned): `data-open-source="light" | "dark"` (other themes use
35
24
  `data-<theme-id>`).
36
- - Color switcher default: `data-color-switcher="default"` on `<html>`. Apply other
25
+ - Color switcher default: `data-color-switcher="default"` on `<html>` — this is the
26
+ one place `<html>` carries a palette, and only the host sets it. Apply other
37
27
  palettes per region, not here ([how-to-color.md](how-to-color.md)).
38
28
 
39
- Optional: `UXelleThemeProvider` from `@uxelle/themes/react` with
40
- `theme="open-source"` keeps those attributes in sync.
41
-
42
29
  ## Skip link
43
30
 
44
31
  First focusable node in the DOM, targeting `id="main-content"` on `<main>`:
@@ -11,6 +11,24 @@ Page chrome comes from theme variables (do not invent `uxl-breakpoint-*` singula
11
11
 
12
12
  Stacks inside the column use `--uxl-theme-layout-spacing-*` ([spacing-steps.md](spacing-steps.md)).
13
13
 
14
+ ## Who owns the content column
15
+
16
+ **The experience owns it — chrome does not.** [recipe-app-chrome.md](recipe-app-chrome.md)
17
+ renders the skip link, `Navigation`, and a scrolling `<main>`, and stops there.
18
+ `<main>` is a scrollport with no padding and no cap. The experience recipe
19
+ (dashboard, data-table page, settings, page shell) renders the one capped column
20
+ inside it.
21
+
22
+ The column carries `maxWidth`, `mh="auto"`, `p`, and `gap`, and all four are
23
+ experience-level decisions: the `gap` is the density **Section** step
24
+ ([density.md](density.md)) and the cap may be the `600px` reading measure instead of
25
+ the container token. Chrome cannot know either, which is why it does not emit them.
26
+
27
+ So exactly one element in the tree sets `maxWidth="var(--uxl-breakpoints-max-container-width)"`
28
+ plus `p="var(--uxl-breakpoints-margin)"`. If you find yourself writing that pair
29
+ twice — once from chrome and once from the experience — the page is capped and
30
+ padded twice; delete the chrome one.
31
+
14
32
  ## Content column
15
33
 
16
34
  One capped, centered column carries the page:
@@ -27,9 +45,12 @@ One capped, centered column carries the page:
27
45
  >
28
46
  ```
29
47
 
30
- `mh="auto"` centers the column; `medium-12` is the gap between page sections. Cap
31
- with the token, not a literal (`1120px` / `1600px`), and keep page internals off
32
- the theme-internal `--uxl-theme-layout-*-gutter` / `*-margin`.
48
+ `mh="auto"` centers the column; the `gap` is the **Section** step, shown here at its
49
+ `standard` value — a `compact` dashboard uses `medium-9` and a `spacious` landing
50
+ page `large-14` ([density.md](density.md)). Cap with the token, not a literal
51
+ (`1120px` / `1600px`), and keep page internals off the theme-internal
52
+ `--uxl-theme-layout-*-gutter` / `*-margin`. The cap, the margin, and the gutter are
53
+ density-invariant — only the `gap` moves.
33
54
 
34
55
  If the host already renders `<main>` (the skip-link target), keep this column a
35
56
  `div`; otherwise set `as="main"`.
@@ -38,6 +59,23 @@ If the host already renders `<main>` (the skip-link target), keep this column a
38
59
  prose section inside a wider page) caps at the **reading measure** rather than the
39
60
  container token — see [Reading measure](#reading-measure).
40
61
 
62
+ ## Intrinsic sizes in rem
63
+
64
+ The no-px rule targets **breakpoints** and **theme values** — things the theme
65
+ already owns and remaps. It does not forbid an intrinsic size that expresses
66
+ "how small may this box get before it should reflow", because no theme variable
67
+ carries that. Use `rem` (never `px`) for:
68
+
69
+ - **`minmax()` minimums** on `auto-fit` grids — the smallest comfortable cell.
70
+ `16rem` for a text card, `18rem` for a media/CTA tile, `12rem`-`14rem` for a
71
+ metric tile.
72
+ - **A bounded search field** — `width="16rem"` with `flexShrink={0}`.
73
+
74
+ These are not breakpoints and must never be used as one: no media query, no
75
+ `matchMedia`, and no `maxWidth` on the page column. Anything the theme *does* own —
76
+ the column cap, page inset, gutter, spacing, type, color — always comes from a
77
+ variable ([tokens.md](tokens.md)).
78
+
41
79
  ## Reading measure
42
80
 
43
81
  Text you read in a single column — a form, an FAQ, a paragraph block — is easiest
@@ -156,8 +194,9 @@ Bands: `tablet` (from `--uxl-theme-layout-tablet-screen-width-min`), `desktop`,
156
194
 
157
195
  ## Keep controls with what they operate
158
196
 
159
- Group a control with the surface it drives; `medium-12` is the gap between
160
- *sections*, not between a control and its target.
197
+ Group a control with the surface it drives; the **Section** step is the gap between
198
+ *sections*, not between a control and its target — a control sits with its target at
199
+ the **Control** step in every density mode ([density.md](density.md)).
161
200
 
162
201
  - **Page navigation** (`Navigation` / `NavLink`) changes routes — don't repeat
163
202
  those destinations as in-page tabs.
@@ -170,7 +209,7 @@ Group a control with the surface it drives; `medium-12` is the gap between
170
209
  To stack when tight, switch the header `flexDirection` to `column` below tablet —
171
210
  not `flexWrap="wrap"`.
172
211
  - **Related form fields** (first/last name, city/state/ZIP): `display="grid"` with
173
- `fr` tracks and `small-4` gap; collapse to one column when narrow. See
212
+ `fr` tracks and the **Control** gap; collapse to one column when narrow. See
174
213
  [recipe-form-section.md](recipe-form-section.md).
175
214
 
176
215
  ## Action clusters
@@ -196,6 +235,12 @@ Delete/Upload, banner CTAs with two+ text buttons.
196
235
  (Filter/Export beside search), icon-only clusters. Do not use the tablet switch to
197
236
  restyle card grids — those reflow intrinsically ([recipe-card-grid.md](recipe-card-grid.md)).
198
237
 
238
+ **One button** needs no `direction` at all. `ButtonGroup` already defaults to
239
+ `direction="row"` / `fullWidth={false}`, and a single child renders identically in
240
+ either direction, so pass neither — a lone `direction="row"` reads as a deliberate
241
+ override of the switch above and invites a false review flag. Add a second text
242
+ button and the cluster takes the switch like any other.
243
+
199
244
  ## A2UI
200
245
 
201
246
  Chat adapters allow only `var(--uxl-theme-layout-spacing-*)` (or `0`) — no
@@ -17,10 +17,13 @@ expressive, but within the *same* grid, hierarchy, and limited palette.
17
17
  - **Restraint / minimalism.** Prefer fewer elements. Few type roles, few
18
18
  accents, one dominant brand band per view. If a divider, box, or color is not
19
19
  doing a job, remove it. Whitespace is the default separator.
20
- - **Whitespace and rhythm.** Space is structural. Use the ramp deliberately:
21
- tight stacks `micro`, related controls `small-4`, card/section padding
22
- `medium-8`, page-section gaps `medium-12`, hero breathing `large-13`+.
23
- See [spacing-steps.md](spacing-steps.md).
20
+ - **Whitespace and rhythm.** Space is structural, and how much of it is *the*
21
+ expression of an experience's character. Use the ramp deliberately by **role** —
22
+ Tight stacks, Control clusters, Group padding, Section gaps, Band breathing —
23
+ and resolve those roles through one **density mode**: `compact` where data
24
+ volume is the point, `spacious` where a single idea must land. The grid stays
25
+ fixed while the rhythm changes. See [spacing-steps.md](spacing-steps.md) and
26
+ [density.md](density.md).
24
27
  - **Clarity and legibility.** Objective, plain language. Render copy with `Text`
25
28
  so type, line-height, and contrast come from the theme; never set font-size or
26
29
  hex in app CSS.
@@ -63,10 +66,11 @@ The whole flow is assembled in [recipe-landing-page.md](recipe-landing-page.md).
63
66
  | | Product | Marketing |
64
67
  |---|---|---|
65
68
  | Palette | `default` / `default-subtle` + status | + brand bands for emphasis |
66
- | Density | Compact, information-dense | Airy, one idea per band |
69
+ | Density mode | `compact` (data-dense) or `standard` (task) | `spacious` |
67
70
  | Type | Restrained; Body-led | Expressive; Display-led headlines |
68
71
  | Motion / accents | Minimal | `DynamicAngle*` accents, still restrained |
69
72
  | Goal | Get the task done | Build trust and convert |
70
73
 
71
74
  Same grid, same tokens, same components. The difference is emphasis, not a
72
- different system.
75
+ different system — density shifts which ramp step each role lands on, never the
76
+ column cap, page inset, or gutter ([density.md](density.md)).
@@ -7,18 +7,25 @@ React app frame: skip link, [`Navigation`](../uxelle-components/Navigation.md),
7
7
  ## When not
8
8
 
9
9
  - Not an `AppChrome` / `PageShell` component.
10
+ - **Chrome does not render the content column.** `<main>` here is only the
11
+ scrollport — the **experience** inside it owns the capped column
12
+ ([how-to-page-layout.md](how-to-page-layout.md#who-owns-the-content-column)). Do
13
+ not emit a `maxWidth` + `--uxl-breakpoints-margin` wrapper from this recipe *and*
14
+ from the experience recipe, or the page is padded and capped twice.
10
15
  - Theme CSS and `data-*`: [how-to-host.md](how-to-host.md).
11
16
  - A2UI chat surfaces: skip link, 100% viewport shell, and html `data-*` are host-owned. Do not emit this recipe as A2UI. Optional `A2uiNavigation` / `A2uiFooter` only when the host already has a surface.
12
17
 
13
18
  ## Regions
14
19
 
15
- skip link → `Navigation` (header) and/or `NavigationSide` (side rail wrapping `NavigationSideGroup` rows) → `<main id="main-content">` → content column ([how-to-page-layout.md](how-to-page-layout.md)) → optional `Footer` **inside that same `<main>`**
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>`**
16
21
 
17
22
  ## Spacing
18
23
 
19
- Nav is a catalog component (its own tokens). Main is the scrollport (no page padding). The **page column inside main** uses breakpoint margin / max-container-width (one-column settings may use `600px` — [recipe-settings-page.md](recipe-settings-page.md)). When present, `Footer` sits after that column in the same scrollport.
24
+ Chrome is **density-invariant**: nav and footer are catalog components with their own tokens, and the margin / max-container-width on the experience's column never shift with the mode either. The experience picks the density mode and renders that column ([density.md](density.md)) — chrome does neither.
20
25
 
21
- Do not wrap `Footer` in extra `Layout` padding. Band padding is catalog-owned: two-band footers use `--uxl-breakpoints-padding-large-13`; a **single** band (legal-only, unused slot `null`) uses `--uxl-theme-layout-spacing-small-4` on the block axis. That is the related-controls step, not page-section (`medium-12`) or hero (`large-13`).
26
+ Nav is a catalog component (its own tokens). Main is the scrollport — **no page padding and no column cap here**; the experience's own column supplies breakpoint margin / max-container-width (one-column settings may use `600px` — [recipe-settings-page.md](recipe-settings-page.md)). When present, `Footer` sits after that column in the same scrollport.
27
+
28
+ Do not wrap `Footer` in extra `Layout` padding. Band padding is catalog-owned: two-band footers use `--uxl-breakpoints-padding-large-13`; a **single** band (legal-only, unused slot `null`) uses `--uxl-theme-layout-spacing-small-4` on the block axis. That is the **Control** step, not **Section** or **Band**.
22
29
 
23
30
  ## A11y
24
31
 
@@ -73,17 +80,14 @@ Import from `@uxelle/components`. Put skip-link CSS in the host stylesheet.
73
80
  />
74
81
  </Layout>
75
82
  <main id="main-content" tabIndex={-1} style={{ flexGrow: 1, minHeight: 0, overflow: "auto" }}>
76
- <Layout
77
- display="flex"
78
- flexDirection="column"
79
- width="100%"
80
- maxWidth="var(--uxl-breakpoints-max-container-width)"
81
- mh="auto"
82
- p="var(--uxl-breakpoints-margin)"
83
- gap="var(--uxl-theme-layout-spacing-medium-12)"
84
- >
85
- {/* page body — recipe-page-shell.md / recipe-form-section.md / recipe-data-table-page.md / recipe-settings-page.md */}
86
- </Layout>
83
+ {/*
84
+ The experience renders its own capped column here — chrome does not.
85
+ It owns maxWidth / mh / p / gap so it can set its density mode and, for a
86
+ one-column form or settings view, the 600px reading measure.
87
+ recipe-page-shell.md / recipe-dashboard-overview.md / recipe-data-table-page.md /
88
+ recipe-settings-page.md
89
+ */}
90
+ <PageBody />
87
91
  <Footer
88
92
  topMainContent={null}
89
93
  bottomMainContent={
@@ -108,7 +112,7 @@ When-not for this recipe. Host owns chrome (skip link, viewport shell, html `dat
108
112
 
109
113
  ## Help and marketing chrome
110
114
 
111
- 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 — omitted (`undefined`) or `null` on leading/center/trailing still fill catalog demo content (`??`). Pass `centerSlot={false}` when there is no center nav.
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.
112
116
 
113
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.
114
118
 
@@ -122,7 +126,7 @@ Nav trailing (search, language, Log in / Register) stays a **row** at every widt
122
126
  <Logo name="generic" width={40} interactive aria-hidden />
123
127
  </a>
124
128
  }
125
- centerSlot={false}
129
+ centerSlot={null}
126
130
  trailingSlot={
127
131
  <>
128
132
  <LanguageSelector value="EN" />
@@ -35,13 +35,20 @@ minmax(MIN, 1fr))` drops from 3-up to 2-up to 1-up as width shrinks; pick `MIN`
35
35
 
36
36
  ## Spacing
37
37
 
38
- `gap="var(--uxl-breakpoints-gutter)"`. Card inner padding is component-owned. The
39
- page column still uses [how-to-page-layout.md](how-to-page-layout.md).
38
+ `gap="var(--uxl-breakpoints-gutter)"`. Card inner padding is component-owned. Both
39
+ are **density-invariant** — a card grid looks the same in every mode, so do not
40
+ tighten the gutter for a `compact` dashboard or pad it out for a marketing page
41
+ ([density.md](density.md)). Stacks *inside* a card use the host's **Control** or
42
+ **Group** step. The page column still uses [how-to-page-layout.md](how-to-page-layout.md).
40
43
 
41
44
  ## A11y
42
45
 
43
46
  Name the group when the cards are a set (`aria-label` on the grid `Layout`).
44
- Headings inside cards continue page rank (`h2` under the page `h1`). A `ProductCard`
47
+ Headings inside cards continue page rank (`h2` under the page `h1`) but take the
48
+ type **one stop below** that rank's default, since the card already supplies the
49
+ separation — with `Display Extra Small` as the floor. A `spacious` card `h2` is
50
+ `Display Small`; a product card `h2` is already at the floor and stays
51
+ `Display Extra Small`, as shown ([density.md](density.md)). A `ProductCard`
45
52
  CTA names itself; if the whole card is the link, the heading text is the name. See
46
53
  [how-to-accessibility.md](how-to-accessibility.md).
47
54
 
@@ -100,5 +107,7 @@ Case-study / product tiles (`ProductCard`, media + CTA):
100
107
  ## A2UI
101
108
 
102
109
  `A2uiLayout` has no `gridTemplateColumns` — stack `A2uiCard` / `A2uiProductCard` in
103
- a column with `var(--uxl-theme-layout-spacing-*)` `gap`. No
104
- `getUxelleRecipe("card-grid")` ([a2ui.md](a2ui.md)).
110
+ a column with `var(--uxl-theme-layout-spacing-*)` `gap`. Media in
111
+ `A2uiProductCard` `centerSlotContent` is `A2uiImage` with `aspectRatio` (omit
112
+ `width` so it fills the card). No `getUxelleRecipe("card-grid")`
113
+ ([a2ui.md](a2ui.md)).
@@ -16,8 +16,10 @@ A single, decisive conversion moment: "Start free," "Talk to sales."
16
16
 
17
17
  - One primary action (an optional secondary is fine) — not a menu of links.
18
18
  - Section heading is `h2`, not another `h1`.
19
- - Keep it to one dominant brand band per page; if the hero is loud, this can be
20
- quieter, or vice versa.
19
+ - Keep it to one dominant brand band per page, and make that concrete rather than
20
+ vague: if the hero already carries a brand palette, set this band to
21
+ `default-subtle`. Only one of the two gets a `brand-*` palette
22
+ ([recipe-landing-page.md](recipe-landing-page.md)).
21
23
 
22
24
  ## Regions
23
25
 
@@ -33,8 +35,10 @@ viewports and stack `fullWidth` below tablet (`useBreakpointUp("tablet")`). See
33
35
 
34
36
  ## Spacing
35
37
 
36
- `large-13`+ block padding for the band; `medium-8` between the lockup and actions
37
- ([spacing-steps.md](spacing-steps.md)).
38
+ **Band** step for the band's block padding and **Group** between the lockup and the
39
+ actions — `large-15` and `medium-10` on a `spacious` marketing page, its usual home
40
+ ([density.md](density.md), [spacing-steps.md](spacing-steps.md)). A CTA closing a
41
+ `standard` product surface resolves to `large-13` / `medium-8`.
38
42
 
39
43
  ## A11y
40
44
 
@@ -50,20 +54,26 @@ role tokens — never hardcode hex. Pick the brand palette by the look you want
50
54
 
51
55
  ## React
52
56
 
57
+ `tabletUp` is the shared `useBreakpointUp("tablet")` helper — copy it from
58
+ [how-to-page-layout.md](how-to-page-layout.md#3-token-driven-js-narrow-fallback),
59
+ do not reimplement it or inline a px literal.
60
+
53
61
  ```tsx
62
+ const tabletUp = useBreakpointUp("tablet");
63
+
54
64
  <Layout
55
65
  as="section"
56
66
  data-color-switcher="brand-1"
57
67
  display="flex"
58
68
  justifyContent="center"
59
- p="var(--uxl-theme-layout-spacing-large-13)"
69
+ p="var(--uxl-theme-layout-spacing-large-15)"
60
70
  style={{ backgroundColor: "var(--uxl-color-switcher-background)" }}
61
71
  >
62
72
  <Layout
63
73
  display="flex"
64
74
  flexDirection="column"
65
75
  alignItems="center"
66
- gap="var(--uxl-theme-layout-spacing-medium-8)"
76
+ gap="var(--uxl-theme-layout-spacing-medium-10)"
67
77
  width="100%"
68
78
  maxWidth="var(--uxl-breakpoints-max-container-width)"
69
79
  >
@@ -32,18 +32,26 @@ 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
 
39
- `medium-12` between page sections (header / KPIs / primary / supporting);
40
- `--uxl-breakpoints-gutter` inside the grids. Section intros are `micro-2`
40
+ **Density: `compact`** — a dashboard's job is to show more at a glance, so the
41
+ column tightens ([density.md](density.md)). **Section** gap between page sections
42
+ (header / KPIs / primary / supporting) is `medium-9`; subgroups inside a section
43
+ use **Group** (`small-6`); section intros use **Tight** (`micro-1`).
44
+ `--uxl-breakpoints-gutter` stays the grid gap in every mode
41
45
  ([spacing-steps.md](spacing-steps.md)).
42
46
 
47
+ Step up to `standard` when the overview is a light landing page with two or three
48
+ KPIs rather than a dense monitoring surface — but pick one mode and hold it.
49
+
43
50
  ## A11y
44
51
 
45
52
  One `main`, one `h1`. Name each region (KPIs `aria-label="Key metrics"`, the primary
46
- 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
47
55
  for the primary surface follow [states](recipe-states.md). See
48
56
  [how-to-accessibility.md](how-to-accessibility.md).
49
57
 
@@ -55,6 +63,7 @@ alerts, never as decoration ([how-to-color.md](how-to-color.md)).
55
63
  ## React
56
64
 
57
65
  ```tsx
66
+ // Density: compact (data-dense dashboard) — see density.md
58
67
  <Layout
59
68
  display="flex"
60
69
  flexDirection="column"
@@ -62,7 +71,7 @@ alerts, never as decoration ([how-to-color.md](how-to-color.md)).
62
71
  maxWidth="var(--uxl-breakpoints-max-container-width)"
63
72
  mh="auto"
64
73
  p="var(--uxl-breakpoints-margin)"
65
- gap="var(--uxl-theme-layout-spacing-medium-12)"
74
+ gap="var(--uxl-theme-layout-spacing-medium-9)"
66
75
  >
67
76
  {/* Page header — recipe-page-header.md */}
68
77
  <DashboardHeader />
@@ -90,18 +99,23 @@ alerts, never as decoration ([how-to-color.md](how-to-color.md)).
90
99
  <LabelBadge label="+4" dataColorSwitcher="success" emphasis="low" leadingIcon leadingIconName="trending_up" />
91
100
  }
92
101
  />
102
+ {/*
103
+ Palette tracks whether the change is *good*, not whether the number rose.
104
+ Fewer open tickets is an improvement, so this delta is `success` while
105
+ trending down — say so in the label rather than relying on the arrow.
106
+ */}
93
107
  <StatTile
94
108
  label="Open tickets"
95
109
  value="18"
96
110
  trailingSlot
97
111
  trailingSlotContent={
98
- <LabelBadge label="-6" dataColorSwitcher="success" emphasis="low" leadingIcon leadingIconName="trending_down" />
112
+ <LabelBadge label="6 fewer" dataColorSwitcher="success" emphasis="low" leadingIcon leadingIconName="trending_down" />
99
113
  }
100
114
  />
101
115
  </Layout>
102
116
 
103
117
  {/* Primary surface — recipe-data-table-page.md (with states) */}
104
- <Layout as="section" display="flex" flexDirection="column" gap="var(--uxl-theme-layout-spacing-small-4)">
118
+ <Layout as="section" display="flex" flexDirection="column" gap="var(--uxl-theme-layout-spacing-micro-3)">
105
119
  <Text type="Display Extra Small" as="h2" width={false}>Recent activity</Text>
106
120
  {isLoading ? <ActivitySkeleton /> : rows.length ? <ActivityTable rows={rows} /> : <ActivityEmpty />}
107
121
  </Layout>