@uxelle/skills 0.2.0-beta.2 → 0.2.2

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 (49) hide show
  1. package/README.md +3 -5
  2. package/dist/index.js +98 -88
  3. package/index.json +98 -88
  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/Image.md +10 -3
  11. package/skills/uxelle-components/Logo.md +1 -1
  12. package/skills/uxelle-components/MultiSelect.md +3 -2
  13. package/skills/uxelle-components/SKILL.md +3 -1
  14. package/skills/uxelle-components/Select.md +2 -1
  15. package/skills/uxelle-components/Table.md +7 -4
  16. package/skills/uxelle-components/getting-started.md +54 -0
  17. package/skills/uxelle-design-harness/SKILL.md +44 -20
  18. package/skills/uxelle-design-harness/a2ui.md +22 -2
  19. package/skills/uxelle-design-harness/density.md +138 -0
  20. package/skills/uxelle-design-harness/how-to-accessibility.md +2 -0
  21. package/skills/uxelle-design-harness/how-to-color.md +6 -3
  22. package/skills/uxelle-design-harness/how-to-host.md +5 -22
  23. package/skills/uxelle-design-harness/how-to-page-layout.md +51 -6
  24. package/skills/uxelle-design-harness/principles.md +10 -6
  25. package/skills/uxelle-design-harness/recipe-app-chrome.md +20 -16
  26. package/skills/uxelle-design-harness/recipe-card-grid.md +14 -5
  27. package/skills/uxelle-design-harness/recipe-cta-band.md +16 -6
  28. package/skills/uxelle-design-harness/recipe-dashboard-overview.md +17 -5
  29. package/skills/uxelle-design-harness/recipe-data-table-page.md +30 -137
  30. package/skills/uxelle-design-harness/recipe-feature-section.md +9 -7
  31. package/skills/uxelle-design-harness/recipe-footer.md +7 -4
  32. package/skills/uxelle-design-harness/recipe-form-section.md +2 -2
  33. package/skills/uxelle-design-harness/recipe-hero.md +35 -15
  34. package/skills/uxelle-design-harness/recipe-landing-page.md +27 -4
  35. package/skills/uxelle-design-harness/recipe-logo-wall.md +11 -9
  36. package/skills/uxelle-design-harness/recipe-multi-step-flow.md +5 -3
  37. package/skills/uxelle-design-harness/recipe-page-header.md +14 -4
  38. package/skills/uxelle-design-harness/recipe-page-shell.md +2 -2
  39. package/skills/uxelle-design-harness/recipe-pricing.md +5 -2
  40. package/skills/uxelle-design-harness/recipe-query-bar.md +21 -5
  41. package/skills/uxelle-design-harness/recipe-record-detail.md +5 -2
  42. package/skills/uxelle-design-harness/recipe-settings-page.md +3 -3
  43. package/skills/uxelle-design-harness/recipe-stat-callouts.md +8 -3
  44. package/skills/uxelle-design-harness/recipe-states.md +9 -4
  45. package/skills/uxelle-design-harness/recipe-summary-list.md +5 -3
  46. package/skills/uxelle-design-harness/recipe-template.md +6 -0
  47. package/skills/uxelle-design-harness/recipe-testimonial.md +5 -3
  48. package/skills/uxelle-design-harness/spacing-steps.md +17 -8
  49. package/skills/uxelle-design-harness/tokens.md +45 -4
@@ -0,0 +1,54 @@
1
+ <!-- Generated from README.md (getting-started region). Do not edit. -->
2
+
3
+ # Getting started
4
+
5
+ New app or host without uxElle packages and CSS? Read this file first, then set up page chrome in [how-to-host.md](../uxelle-design-harness/how-to-host.md). Component APIs live in [SKILL.md](SKILL.md). Optional A2UI runtime: [a2ui.md](../uxelle-design-harness/a2ui.md). Do not preload every component file.
6
+
7
+ ## Quick Start
8
+
9
+ Peer dependencies: React 18 or 19.
10
+
11
+ ```bash
12
+ npm install @uxelle/components @uxelle/themes
13
+ ```
14
+
15
+ ```tsx
16
+ import "@uxelle/components/styles.css";
17
+ import "@uxelle/components/index.css";
18
+ import "@uxelle/themes/themes/open-source/open-source.css";
19
+
20
+ import { Button } from "@uxelle/components";
21
+
22
+ function App() {
23
+ return <Button emphasis="high">Get Started</Button>;
24
+ }
25
+ ```
26
+
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
+
29
+ ### Next.js / SSR
30
+
31
+ - Import the three stylesheets in `app/layout.tsx` (or a top-level provider). JS side effects will not style App Router.
32
+ - Published `@uxelle/components` JS is prefixed with `"use client"`. Importing any component places that file in the client graph.
33
+ - Pass `mobile={true}` or `mobile={false}` on `NavigationSide` so server and client markup match. Omit `mobile` only in client-only surfaces.
34
+
35
+ Optional: for an agent-driven UI surface, also install `@uxelle/a2ui`.
36
+
37
+ ## Themes
38
+
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.
40
+
41
+ ```tsx
42
+ import "@uxelle/themes/themes/open-source/open-source.css";
43
+ import { UXelleThemeProvider } from "@uxelle/themes/react";
44
+
45
+ function AppShell({ children }: { children: React.ReactNode }) {
46
+ return (
47
+ <UXelleThemeProvider theme="open-source" darkMode={false} colorSwitcher="default">
48
+ {children}
49
+ </UXelleThemeProvider>
50
+ );
51
+ }
52
+ ```
53
+
54
+ Open Source uses **Open Sans** (self-hosted in the theme package). Do not set `font-family` in app CSS — `Text` uses theme tokens.
@@ -37,29 +37,38 @@ palette. The full mapping of principles to uxElle mechanisms lives in
37
37
  1. Load this `SKILL.md`. The foundations below always apply.
38
38
  2. Read the variable foundation once: [tokens.md](tokens.md) and
39
39
  [spacing-steps.md](spacing-steps.md).
40
- 3. Creating an app shell? [how-to-host.md](how-to-host.md). Building a page body?
41
- [how-to-page-layout.md](how-to-page-layout.md) (layout **and** responsiveness),
42
- [how-to-color.md](how-to-color.md), and [how-to-accessibility.md](how-to-accessibility.md).
43
- 4. Prefer an existing component. Link its doc at point of use, e.g.
40
+ 3. Pick the **density mode** for the experience — `compact` (data-dense product),
41
+ `standard` (task product, the default), `spacious` (marketing). One mode per
42
+ experience, chosen before any layout: [density.md](density.md).
43
+ 4. Creating a **new** app/host? Install and load CSS from
44
+ [getting-started.md](../uxelle-components/getting-started.md), then the
45
+ shell in [how-to-host.md](how-to-host.md). Building a page body in an
46
+ existing host? [how-to-page-layout.md](how-to-page-layout.md) (layout
47
+ **and** responsiveness), [how-to-color.md](how-to-color.md), and
48
+ [how-to-accessibility.md](how-to-accessibility.md).
49
+ 5. Prefer an existing component. Link its doc at point of use, e.g.
44
50
  `Table` -> `../uxelle-components/Table.md`. Import from `@uxelle/components`.
45
- 5. Otherwise follow a recipe (see the index). If nothing fits, compose `Layout` +
51
+ 6. Otherwise follow a recipe (see the index). If nothing fits, compose `Layout` +
46
52
  `Text` + primitives on the grid.
47
- 6. Building a runtime surface? Read [a2ui.md](a2ui.md) and each recipe's **A2UI** note.
48
- 7. Before you finish, run the quality checklist below.
53
+ 7. Building a runtime surface? Read [a2ui.md](a2ui.md) and each recipe's **A2UI** note.
54
+ 8. Before you finish, run the quality checklist below.
49
55
 
50
56
  ## Decision tree
51
57
 
52
- - **New app/host** (loads theme CSS, `data-*`, skip link, `main`) -> [how-to-host.md](how-to-host.md).
53
- Composing inside an existing host -> skip host setup; do not set theme `data-*` on `<html>`.
58
+ - **New app/host** (packages not installed, or CSS / `data-*` not loaded) ->
59
+ [getting-started.md](../uxelle-components/getting-started.md), then
60
+ [how-to-host.md](how-to-host.md) for skip link, `main`, and column chrome.
61
+ Composing inside an existing host -> skip both; do not set theme `data-*` on `<html>`.
54
62
  - **Product experience** (signed-in app):
55
63
  app frame -> [recipe-app-chrome.md](recipe-app-chrome.md);
56
- landing/overview -> [recipe-dashboard-overview.md](recipe-dashboard-overview.md);
57
- records list -> [recipe-data-table-page.md](recipe-data-table-page.md);
64
+ landing/overview -> [recipe-dashboard-overview.md](recipe-dashboard-overview.md) (`compact`);
65
+ records list -> [recipe-data-table-page.md](recipe-data-table-page.md) (`compact`);
58
66
  one record -> [recipe-record-detail.md](recipe-record-detail.md);
59
67
  settings -> [recipe-settings-page.md](recipe-settings-page.md);
60
68
  wizard/checkout -> [recipe-multi-step-flow.md](recipe-multi-step-flow.md);
61
69
  generic body -> [recipe-page-shell.md](recipe-page-shell.md).
62
- - **Marketing experience** (public): whole page -> [recipe-landing-page.md](recipe-landing-page.md),
70
+ Unmarked product recipes default to `standard` ([density.md](density.md)).
71
+ - **Marketing experience** (public, `spacious`): whole page -> [recipe-landing-page.md](recipe-landing-page.md),
63
72
  composed from the marketing pieces below.
64
73
  - **A piece** (part of an experience): page intro -> [recipe-page-header.md](recipe-page-header.md);
65
74
  filter/search toolbar -> [recipe-query-bar.md](recipe-query-bar.md);
@@ -79,6 +88,11 @@ palette. The full mapping of principles to uxElle mechanisms lives in
79
88
  padding / margin. Page chrome (inset, column cap, `fr` gaps) uses
80
89
  `--uxl-breakpoints-margin` / `-max-container-width` / `-gutter`. See
81
90
  [spacing-steps.md](spacing-steps.md) and [how-to-page-layout.md](how-to-page-layout.md).
91
+ - **Density**: choose the step by **role** (Flush / Tight / Control / Group /
92
+ Section / Band) and the heading type by **rank**, both resolved through one
93
+ **mode** for the whole experience — `compact` | `standard` | `spacious`. Page
94
+ chrome and component internals are density-invariant. Never mix modes or tighten
95
+ one role alone. See [density.md](density.md).
82
96
  - **Color**: style via `--uxl-color-switcher-*` roles; apply a palette with
83
97
  `data-color-switcher` on a **region** (never invent switcher names). See
84
98
  [how-to-color.md](how-to-color.md).
@@ -86,10 +100,12 @@ palette. The full mapping of principles to uxElle mechanisms lives in
86
100
  never use `--uxl-component-*` in app/recipe CSS (those belong inside components).
87
101
  - **No hardcoded values**: no hex, no font-family, no breakpoint px. Read
88
102
  breakpoint bounds from theme variables when JS is unavoidable
89
- ([how-to-page-layout.md](how-to-page-layout.md)). Exception: one-column reading
90
- content (a form/settings page, or a form / FAQ / prose section on a wider page)
91
- may cap at the `maxWidth="600px"` **reading measure** — always centered with
92
- `mh="auto"` ([how-to-page-layout.md](how-to-page-layout.md#reading-measure)).
103
+ ([how-to-page-layout.md](how-to-page-layout.md)). Two exceptions: one-column
104
+ reading content (a form/settings page, or a form / FAQ / prose section on a wider
105
+ page) may cap at the `maxWidth="600px"` **reading measure**, always centered with
106
+ `mh="auto"` ([how-to-page-layout.md](how-to-page-layout.md#reading-measure)); and
107
+ **intrinsic sizes** — `minmax()` mins and a bounded search field — use `rem`
108
+ ([how-to-page-layout.md](how-to-page-layout.md#intrinsic-sizes-in-rem)).
93
109
  - **Components first**: prefer a catalog component over bespoke markup; link its
94
110
  doc at point of use (`../uxelle-components/<Name>.md`); do not open package source.
95
111
  - **Recipes are patterns, not exports**: never add `PageShell` / `AppChrome` /
@@ -104,14 +120,21 @@ Every generated screen must pass this. It is the definition of done.
104
120
 
105
121
  - **Grid & rhythm**: content sits on the grid; one capped content column via
106
122
  `--uxl-breakpoints-max-container-width` / `-gutter` / `-margin`; section spacing
107
- from the ramp (page sections use `medium-12`). One-column reading sections
123
+ from the ramp at the experience's **Section** step. One-column reading sections
108
124
  (form / FAQ / prose) share the `600px` reading measure and are centered
109
125
  (`mh="auto"`); a decision cluster (form actions) is separated from its fields by
110
- `medium-12`, not the field gap.
126
+ the **Section** step, not the field gap.
127
+ - **Density**: one mode declared for the surface and applied to every role; the gap
128
+ roles (Tight -> Control -> Group -> Section) stay ~1.5x apart; page chrome and
129
+ component internals unchanged; no breakpoint logic added for density
130
+ ([density.md](density.md)).
111
131
  - **Tokens only**: spacing from `--uxl-theme-layout-spacing-*`; color from
112
132
  `--uxl-color-switcher-*`; no hardcoded hex or px, no invented `--uxl-*`, no
113
133
  `--uxl-component-*` in app CSS, no `var(--x, fallback)`.
114
- - **Hierarchy**: exactly one `h1`; heading ranks unbroken; few `Text` type roles.
134
+ - **Hierarchy**: exactly one `h1`; heading ranks unbroken; each rank resolved to a
135
+ **distinct** `Text` type through the density mode — consecutive ranks never share
136
+ a type (an `h1` and `h2` both on `Display Extra Small` flatten the page), and
137
+ headings inside a `Card` step down one stop ([density.md](density.md)).
115
138
  - **Responsive**: reflows mobile -> up with no px literals; clusters stack, `Table`
116
139
  scrolls, nav collapses; touch targets stay >= 24 CSS px.
117
140
  - **Color discipline**: at most one dominant brand band per view; status palettes
@@ -126,9 +149,10 @@ Every generated screen must pass this. It is the definition of done.
126
149
 
127
150
  **Foundations** (always apply): [principles.md](principles.md) ·
128
151
  [tokens.md](tokens.md) · [spacing-steps.md](spacing-steps.md) ·
152
+ [density.md](density.md) ·
129
153
  [how-to-page-layout.md](how-to-page-layout.md) · [how-to-color.md](how-to-color.md) ·
130
154
  [how-to-accessibility.md](how-to-accessibility.md) · [how-to-host.md](how-to-host.md) ·
131
- [a2ui.md](a2ui.md)
155
+ [getting-started](../uxelle-components/getting-started.md) · [a2ui.md](a2ui.md)
132
156
 
133
157
  **Experiences** — product: [app-chrome](recipe-app-chrome.md) ·
134
158
  [page-shell](recipe-page-shell.md) · [dashboard-overview](recipe-dashboard-overview.md) ·
@@ -19,6 +19,10 @@ use the catalog schema the host provides.
19
19
  - **No `className` / `style`.** Styling comes from the theme and adapter props.
20
20
  - **Layout-spacing tokens only.** `A2uiLayout` accepts `0` or
21
21
  `var(--uxl-theme-layout-spacing-*)`; never emit page-chrome breakpoint tokens.
22
+ - **`standard` or `compact` density only.** Chat surfaces are narrow and have no
23
+ full-bleed bands, so `spacious` never applies and the **Band** role has no
24
+ meaning. Use `standard` by default, `compact` for data-heavy runtime output
25
+ ([density.md](density.md)).
22
26
  - **No responsive JS.** There is no `matchMedia`. Default to a single, narrow
23
27
  column (chat is mobile-like) and stack action clusters (`"direction": "column"`).
24
28
  - **Host owns theme.** Never set theme `data-*` or a skip link from adapters; the
@@ -45,12 +49,27 @@ recipe JSON into the customer app.
45
49
  Most catalog components map 1:1 to an `A2ui<Name>` adapter (e.g. `Button` ->
46
50
  `A2uiButton`, `Textfield` -> `A2uiTextfield`, `Textarea` -> `A2uiTextarea`,
47
51
  `Table` -> `A2uiTable`, `Sheet` -> `A2uiSheet`, `Hero` -> `A2uiHero`,
48
- `Stepper` -> `A2uiStepper` + `A2uiStepperItem`, `ListControls` -> `A2uiListControls`,
52
+ `Image` -> `A2uiImage`, `Stepper` -> `A2uiStepper` + `A2uiStepperItem`,
53
+ `ListControls` -> `A2uiListControls`,
49
54
  `Banner` -> `A2uiBanner`, `LabelBadge` -> `A2uiLabelBadge`, `StatTile` -> `A2uiStatTile`,
55
+ `ChoiceChip` -> `A2uiChoiceChip`, `ChoiceChipGroup` -> `A2uiChoiceChipGroup`,
56
+ `FilterChip` -> `A2uiFilterChip`, `FilterChipGroup` -> `A2uiFilterChipGroup`,
50
57
  `DynamicAngle*` -> `A2uiDynamicAngle*`). `Table` has the full family (`A2uiTableGrid`,
51
58
  `A2uiTableHead`, `A2uiTableBody`, `A2uiTableRow`, `A2uiTableCell`,
52
59
  `A2uiTableHeaderCell`, `A2uiTableColumnSizingProvider`).
53
60
 
61
+ `A2uiImage` box size (`width`, `height`, `minWidth`, `maxWidth`, `minHeight`,
62
+ `maxHeight`) is `number|string` — numbers are pixels, strings pass through as CSS
63
+ (including `var(--uxl-…)`). `aspectRatio` crops and computes the missing axis.
64
+ Omit size and `aspectRatio` on an `A2uiImage` nested in `A2uiHero`.
65
+
66
+ `A2uiChoiceChip` / `A2uiFilterChip` take a `label` string. Do not nest `A2uiText`
67
+ inside a chip. Choice-chip labels use Condensed when unchecked and Condensed Alt
68
+ when checked; filter-chip labels use Condensed. `A2uiFilterChipGroup` wraps whole
69
+ chips; a chip wider than its container ellipsizes so dismiss stays visible.
70
+ Exclusive `A2uiChoiceChip` selection is one checked value in host state (toggles,
71
+ not radios). Use `A2uiRadioGroup` or `A2uiSegmentedControl` for radio semantics.
72
+
54
73
  Known gaps — substitute rather than invent:
55
74
 
56
75
  | React | A2UI |
@@ -66,7 +85,8 @@ If a design needs a gap component as its backbone, say so in the recipe's
66
85
 
67
86
  ## Translating a React recipe
68
87
 
69
- 1. Keep the same region order and layout-spacing values.
88
+ 1. Keep the same region order. Keep the layout-spacing values too, unless the React
89
+ surface was `spacious` — then re-resolve its roles at `standard`.
70
90
  2. Swap each component for its adapter; expand `Lockup` to stacked `A2uiText`.
71
91
  3. Replace `useBreakpointUp` switches with a single narrow column.
72
92
  4. Replace React state with host bindings.
@@ -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"`).
@@ -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,18 @@ 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:
15
-
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).
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.
31
16
 
32
17
  On `<html>`:
33
18
 
34
19
  - Theme mode (host-owned): `data-open-source="light" | "dark"` (other themes use
35
20
  `data-<theme-id>`).
36
- - Color switcher default: `data-color-switcher="default"` on `<html>`. Apply other
21
+ - Color switcher default: `data-color-switcher="default"` on `<html>` — this is the
22
+ one place `<html>` carries a palette, and only the host sets it. Apply other
37
23
  palettes per region, not here ([how-to-color.md](how-to-color.md)).
38
24
 
39
- Optional: `UXelleThemeProvider` from `@uxelle/themes/react` with
40
- `theme="open-source"` keeps those attributes in sync.
41
-
42
25
  ## Skip link
43
26
 
44
27
  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)).