@adia-ai/web-components 0.5.14 → 0.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -11,6 +11,193 @@ runtime ships in the sibling `@adia-ai/a2ui-runtime` package
11
11
 
12
12
  _No pending changes._
13
13
 
14
+ ## [0.5.15] - 2026-05-16
15
+
16
+ > **Scope note:** §325 + §326 were originally planned as a separate v0.5.16 cycle (see retired `docs/plans/0.5.16-release-plan.md`); absorbed into v0.5.15 since both landed within the same release window (same-day commits prior to bump). The peer's v0.5.16 plan-doc explicitly anticipated this absorption option ("Hermes's call per `/lockstep-release`"). v0.5.16 plan-doc marked SHIPPED-IN-V0.5.15.
17
+
18
+ ### v0.5.15 §326 — `<swatch-ui>` `[slot="chrome"]` lost in template contexts (FB-37; P1)
19
+
20
+ Closes FB-37. Pre-§326, `<swatch-ui>` consumed via any `html\`\`` template engine (or any engine that batches child appends separately from element creation) lost its `[slot="chrome"]` children entirely. Root cause: `#stamp()` is gated by a `#stamped` once-only guard + scans `this.children` for chrome slots at `connectedCallback` time. Template engines create the host via `createElement` + connect it FIRST (children empty), then batch-append children AFTER. `#stamp()`'s chrome scan returns `[]`; the `#stamped` guard prevents re-runs; chrome children landed at the END of host's children (after `data-copy`) instead of the canonical sibling-of-tile position. Blocked Tokens Studio's C1.3 dogfood migration (9 failing Playwright tests root-caused to this) — and >95% of AdiaUI consumers using `html\`\`` templates.
21
+
22
+ #### Fixed — `components/swatch/class.js`
23
+
24
+ - NEW `#absorbChromeSlot()` helper. Idempotent. Walks `this.children`; for any child with `slot="chrome"` that's not one of the internal stamped elements + not in the canonical position, calls `this.insertBefore(child, this.#badgeEl)` to move it to the slot between `#tileEl` + `#badgeEl`.
25
+ - `#syncCore()` calls `#absorbChromeSlot()` at the top of every render pass. Late-arriving chrome children (template-render path) get picked up on the next reactive cycle; pre-positioned chrome children (static-HTML path) are no-op-moves.
26
+ - `#stamp()`'s once-only eager scan stays intact for the static-HTML path; `#absorbChromeSlot()` is the safety net for late-append.
27
+
28
+ #### Added — `components/swatch/swatch.test.js`
29
+
30
+ - 2 NEW vitest cases under "FB-37 / §326":
31
+ - `absorbs chrome child appended AFTER connectedCallback (template-render path)` — mimics the bug's exact timing (create + connect with empty children → late append → property mutation triggers render → fix picks up the chrome child).
32
+ - `still handles chrome child appended BEFORE connect (static-HTML path)` — regression check that §314's original behavior still works.
33
+ - Total suite: 6 cases (4 from §325 + 2 from §326).
34
+
35
+ #### Migration
36
+
37
+ Consumers using `<swatch-ui>` via `html\`\`` template engines (lit-html, AdiaUI's own `html\`\``, Preact, Vue, etc.) get the §314 chrome-slot feature working as documented in v0.5.13's `swatch.yaml slots:` block. No consumer-side changes required — the §326 fix is invisible to consumers writing canonical templates.
38
+
39
+ ### v0.5.15 §325 — `<swatch-ui>` default-slot wiped by label-attr (FB-36)
40
+
41
+ Closes FB-36 + FB-36-followup. Pre-§325, `<swatch-ui label="500"><span>tip</span></swatch-ui>` written as indented multi-line HTML dropped the default-slot child entirely; only `label="500"` rendered. Root cause: `#stamp()`'s `Array.from(this.childNodes).filter(...)` captured pure-whitespace text nodes from the indentation; those landed in `#labelEl.firstChild`; `#syncCore()`'s "stale text-node" branch (designed for runtime `el.label = 'X'` mutations on attr-only swatches) then matched the whitespace + did `textContent = this.label`, wiping both the whitespace AND the consumer's `<span>`.
42
+
43
+ Pre-§325 the yaml spec ("Use the default slot for richer content" — `swatch.yaml:40`) and the implementation contradicted each other; post-§325 they agree.
44
+
45
+ #### Fixed — `components/swatch/class.js`
46
+
47
+ - `#stamp()`'s `slotted` filter chain gains `&& !(n.nodeType === 3 && !n.textContent.trim())` — strips pure-whitespace text nodes from the capture. `#labelEl.firstChild` now becomes the consumer's element (or non-whitespace text), so `#syncCore`'s stale-text-node branch fires only for genuinely attr-driven cases.
48
+
49
+ #### Added — `components/swatch/swatch.test.js`
50
+
51
+ - NEW 4-case vitest suite covering FB-36's 3 input axes (default-slot present/absent × label-attr present/absent × runtime mutation). Locks the §325 fix structurally — future regressions of any of the 4 paths fail CI.
52
+
53
+ #### Migration
54
+
55
+ `<swatch-ui label="500"><span>tip</span></swatch-ui>` now behaves per yaml spec: default-slot wins, label-attr drops. Consumers using the workaround `.label=` property binding (per FB-36-followup) to bypass the bug can switch back to the canonical `[label]` attribute form post-upgrade.
56
+
57
+ ### v0.5.15 §324 — Yaml `enum_descriptions:` codegen extension (v0.6.0 FB-23 §3 retire carry-forward, PATCH-eligible)
58
+
59
+ Closes the v0.6.0 carry-forward "Codegen yaml `enum:` per-member descriptions (FB-23 §3 retire)" from the RESPONSE-29 carry-forwards table. v0.5.15 ships the additive codegen surface on PATCH cadence; v0.6.0 no longer needs to carry this item.
60
+
61
+ **The surface change:** `scripts/schemas/component.yaml.schema.json` `Prop` definition gains an optional `enum_descriptions:` object field — when present alongside `enum:`, dts-codegen emits an exported type alias (e.g. `UITextVariant`) above the class declaration with per-variant JSDoc carrying each description. IDE hover surfaces the disambiguation at authoring time without requiring hand-authored .d.ts.
62
+
63
+ #### Added
64
+
65
+ - `scripts/schemas/component.yaml.schema.json` — NEW optional `enum_descriptions: { type: object, additionalProperties: { type: string } }` field per prop. Backward-compatible: existing yamls without the field continue to emit inline enum unions.
66
+ - `scripts/build/components.mjs` — forwards `enum_descriptions:` through `compileProp()` into the a2ui.json sidecar.
67
+ - `scripts/build/dts-codegen.mjs` — `emitEnumTypeAlias()` helper detects props with both `enum:` and `enum_descriptions:`; emits a typed union alias `${className}${PascalCase(propName)}` with per-variant JSDoc; class property references the alias instead of inlining the union.
68
+
69
+ #### Changed — `components/text/text.yaml`
70
+
71
+ - `variant:` prop gains 12-entry `enum_descriptions:` block (one description per variant). Prop `description:` block also gains the FB-23 §2 ARIA-role disclaimer that previously lived in the hand-authored .d.ts.
72
+
73
+ #### Changed — `scripts/build/dts-codegen.mjs` `HAND_AUTHORED_DTS`
74
+
75
+ - `text` removed from skip-list. `text.d.ts` now codegen-driven; per-variant JSDoc surfaces via the new `enum_descriptions:` field. Net effect: 1 of 3 remaining `HAND_AUTHORED_DTS` entries closed (`toast` + `feed` remain — both carry runtime API the yaml shape can't express).
76
+
77
+ #### Regenerated — `components/text/text.d.ts`
78
+
79
+ - Replaces hand-authored .d.ts with codegen output. `UITextVariant` type alias preserved as exported type; per-variant JSDoc preserved verbatim. Net result: zero consumer-facing diff in the .d.ts surface; codegen takes over maintenance.
80
+
81
+ ### v0.5.15 §322 — NEW audit slot 32: `check-dead-constants.mjs`
82
+
83
+ Graduates the FB-35 bug class (declared-but-unread module-level consts) to a CI gate. Walks every `.js` file under `packages/<pkg>/`; extracts SCREAMING_SNAKE_CASE / PascalCase `const NAME = ...;` declarations at module level; flags zero-reference cases. Skips `export const NAME` (consumed externally) + test/spec/examples/config files.
84
+
85
+ First sweep surfaced + removed 2 real dead constants:
86
+ - `packages/a2ui/validator/validator.js:38` — `WIRING_WEIGHTS` (vestigial from planned weighted-scoring system; safe deletion).
87
+ - `packages/llm/adapters/openai.js:10` — `DEFAULT_MAX_TOKENS = 4096` (replaced by explicit `opts.max_tokens` plumbing).
88
+
89
+ ### v0.5.15 §323 — NEW audit slot 33: `check-hover-guard-discipline.mjs`
90
+
91
+ Graduates the `analyze-css` bare-`:hover` discipline class to a CI gate. Detects `:hover` rules on host primitives + interactive slots/roles that lack a `:not([disabled])` guard AND set visual properties (background/color/border/box-shadow/etc.). Skips cursor-only / outline-style-only rule bodies.
92
+
93
+ First sweep surfaced 19 findings across 11 components — all baselined in EXPECTED_DRIFT for cycle-by-cycle close:
94
+ - **Real drift** (rely on `pointer-events: none` for click-suppression but hover styles still apply): action-list (4×), menu (1×), nav-item (2×), nav-group (1×).
95
+ - **Slotted-trigger drift** (mitigated via parent guard rules; cosmetic-OK but worth tightening): select (5×), calendar-picker (1×), range (2×), tag (1×).
96
+ - **Decorative-pop hover** (intentional): avatar (1×). Long-term whitelist candidate.
97
+ - **@scope-Safari-17.x-workaround**: action-list (3 of 4) + nav-item (2) + rating (1) — guard can't live inside @scope; lift alongside the hover rule.
98
+
99
+ #### Removed — dead module-level constants
100
+
101
+ - `packages/a2ui/validator/validator.js` — `WIRING_WEIGHTS` constant (zero refs).
102
+ - `packages/llm/adapters/openai.js` — `DEFAULT_MAX_TOKENS` constant (zero refs).
103
+
104
+ ### v0.5.15 §319 — NEW audit slot 29: `check-exports-map-resolves.mjs`
105
+
106
+ Graduates the FB-32 bug class to a CI gate. Closes the recurring drift where a package.json `exports` map entry resolves (via wildcard expansion or static path) to a file that doesn't exist on disk — consumer TS imports fail with `TS2882` even though Vite/Node can fall through `node_modules/<pkg>/<path>` directly.
107
+
108
+ #### Added — `scripts/release/check-exports-map-resolves.mjs`
109
+
110
+ - ~270 LOC. Walks every `packages/<pkg>/package.json` `exports` map. For each STATIC entry, verifies the referenced file path exists. For each WILDCARD entry matching the FB-32 shape (`./X/*` key + `./X/*/*.<ext>` value), enumerates filesystem candidates that would match the wildcard + verifies each resolved path exists.
111
+ - Skips wildcard expansions where an explicit non-wildcard entry overrides the resolution (Node exports-map resolution algorithm precedence).
112
+ - Skips CSS-only / no-JS components (header / footer / section / aside / various web-modules shells) — these intentionally ship no `.js`.
113
+ - Result: 9 packages / 52 static + 39 wildcard exports entries / 59 resolutions verified / 0 unresolved.
114
+
115
+ ### v0.5.15 §320 — NEW audit slot 30: `check-composes-passthrough.mjs`
116
+
117
+ Graduates the FB-33 bug class to a CI gate. Closes the recurring drift where a form-bearing primitive that programmatically creates ANOTHER form-bearing primitive in its lifecycle (light-DOM composition) doesn't expose / forward the composed primitive's documented prop + event-detail surface.
118
+
119
+ #### Added — `scripts/release/check-composes-passthrough.mjs`
120
+
121
+ - ~270 LOC. Static `COMPOSITION_PAIRS` list declares the required pass-through surface per pair. For each pair: verifies composing primitive's yaml `composes:` block lists the composed primitive; verifies yaml `props:` block exposes every required prop; verifies yaml `events:` block declares each event with required detail keys.
122
+ - Initial pair: `color-input → color-picker` (8 props + 12 event-detail keys across `change` + `input`).
123
+ - Adding a new pair: any new primitive that programmatically wraps a form-bearing primitive declares its contract in `COMPOSITION_PAIRS`; the audit fires at PR time.
124
+ - Companion to slot 23 (`check-form-bearing-dts.mjs`): that audit checks .d.ts type-coverage; this audit checks yaml-declared shape.
125
+
126
+ ### v0.5.15 §321 — NEW audit slot 31: `check-token-name-canonical-shape.mjs`
127
+
128
+ Graduates the stepper / list state-first naming drift class to a CI gate. Closes the recurring drift where a component-local CSS token uses state-first (`--stepper-active-bg`) or color-infix-state (`--link-color-hover`) naming instead of the canonical property-first state-suffix shape (`--<comp>(-<sub>)*-<prop>(-<state>)?`).
129
+
130
+ #### Added — `scripts/release/check-token-name-canonical-shape.mjs`
131
+
132
+ - ~210 LOC. Extracts `--<comp>-*` declarations from each component CSS file. Classifies each by segment shape: canonical-prop / canonical-prop-state / canonical-leaf / canonical-domain / drift-state-first / drift-infix-color-state.
133
+ - Vocabularies (PROP_TOKENS + STATE_TOKENS) capture the catalog's recognized property + state terms (bg/fg/border/ring/shadow/color/outline/size + hover/active/selected/disabled/focus/checked/done/...).
134
+ - Result: 92 components / 1809 component-local tokens audited / 0 drift findings.
135
+ - `link.css` `--link-color-hover` not flagged at v0.5.15 ship: `color` is a recognized PROP_TOKEN + `hover` is a recognized STATE_TOKEN — the shape matches canonical. A future, stricter audit (alternative-property-naming) could flag the `color` vs `fg` divergence; deferred to v0.6.0 BREAKING window since link.css's `--link-color-*` are documented public API.
136
+
137
+ ### v0.5.15 §316 — Naming-convention drift fix (stepper + list state-first → property-first)
138
+
139
+ `<stepper-ui>` and `<list-ui>` declared internal tokens with state-first naming (`--stepper-active-bg`, `--list-selected-fg`, etc.) — the catalog convention is property-first (`--<comp>-{prop}-{state}`). 18 token names renamed; usage sites updated in-place. `<link-ui>` skipped — its `--link-color-*` tokens are documented public API per `link.yaml tokens:` block; renaming is BREAKING and defers to v0.6.0.
140
+
141
+ #### Changed — `components/stepper/`, `components/list/`
142
+
143
+ - `stepper.css` — 12 tokens renamed: `--stepper-{active,done}-{bg,border,fg}` + `--stepper-item-{active,done}-{bg,border,fg}` → `--stepper-{bg,border,fg}-{active,done}` + `--stepper-item-{bg,border,fg}-{active,done}`.
144
+ - `list.css` — 3 tokens renamed: `--list-selected-{bg,fg,border}` → `--list-{bg,fg,border}-selected`.
145
+
146
+ ### v0.5.15 §316 — Hand-rolled transition-duration literals → token references (swatch + heatmap + page)
147
+
148
+ Three components hand-rolled `100ms ease-out` / `120ms ease-out` / `0.1s ease-out` / `150ms ease` instead of routing through the canonical `var(--a-duration-fast)` + `var(--a-easing*)` token chain. Theme designers couldn't swap transition cadence centrally.
149
+
150
+ #### Changed — `components/swatch/`, `components/heatmap/`, `components/page/`
151
+
152
+ - `swatch.css:294,311` — `100ms ease-out` + `120ms ease-out` → `var(--a-duration-fast) var(--a-easing-out)`.
153
+ - `heatmap.css:80` — `0.1s ease-out` → `var(--a-duration-fast) var(--a-easing-out)`.
154
+ - `page.css:61` — `150ms ease` → `var(--a-duration-fast) var(--a-easing)`.
155
+
156
+ ### v0.5.15 §317 — NEW audit slot 27: `check-raw-fg-in-component-css.mjs`
157
+
158
+ Graduates the §313/§315/§316 bug class to a CI gate. Closes the recurring drift class where component CSS rules bypass their own `--<comp>-<family>-<state>` semantic-layer token and reference the raw global `--a-<family>-<state>` token directly. The audit slot found 5 additional bypass instances across the codebase (nav-group + select + toolbar) — all routed through the component-local semantic layer in this commit.
159
+
160
+ #### Added — `scripts/release/check-raw-fg-in-component-css.mjs`
161
+
162
+ - ~190 LOC audit script. Parses each `<comp>.css` for the `:where(:scope) { ... }` declaration block + the set of `--<comp>-*` tokens defined therein. Scans the remainder for `<standard-css-property>: var(--a-<family>-<state>?)` usage where the parallel `--<comp>-<family>-<state>?` is defined — emits a finding per bypass.
163
+ - Whitelisted CSS properties: `color`, `background`, `background-color`, `border` (+ all sub-axis variants), `outline`, `fill`, `stroke`, `caret-color`, `accent-color`, `text-decoration-color`, `column-rule-color`.
164
+ - Distinguishes declaration sites (`--<comp>-X: var(--a-X)`) from usage sites mechanically — declaration sites are NOT flagged (those are correct semantic-layer aliasing).
165
+ - Companion to slot 25 (`check-component-css-vs-styles-tokens.mjs`): that audit catches orphan references (`var(--a-X)` where `--a-X` doesn't exist); this audit catches the inverse (`var(--a-X)` where it exists but `--<comp>-X` should be preferred).
166
+ - EXPECTED_FINDINGS baseline: 0 entries at v0.5.15 ship (sweep landed every concrete bypass).
167
+
168
+ #### Fixed — 5 component CSS bypass sites
169
+
170
+ - `nav-group.css:243,252` — `background: var(--a-bg-hover)` → `var(--nav-group-bg-hover)`.
171
+ - `select.css:199` — listbox container `color: var(--a-fg)` → `var(--select-fg)`.
172
+ - `select.css:319` — hint slot fallback `var(--a-fg-muted)` → `var(--select-fg-muted)`.
173
+ - `toolbar.css:107` — overflow popover `color: var(--a-fg)` → `var(--toolbar-fg)`.
174
+
175
+ ### v0.5.15 §317 — NEW audit slot 28: `check-form-bearing-events-symmetric.mjs`
176
+
177
+ Graduates the FB-31/§315 bug class to a CI gate. Closes the recurring drift class where UIFormElement-extending primitives emit only `input` (or only `complete`/`search`/`pick`) without the canonical `change` event with `detail.value`. The audit confirms all 17 form-bearing primitives are now symmetric post-v0.5.14 §315.
178
+
179
+ #### Added — `scripts/release/check-form-bearing-events-symmetric.mjs`
180
+
181
+ - ~145 LOC audit script. Walks every `class.js` for classes that `extends UIFormElement`. For each, verifies a `dispatchEvent(new CustomEvent('change', { detail: { value: ... } }))` occurs (statically or via dynamic-name dispatch + nearby `'change'` string literal + `detail = { value: ... }` construction).
182
+ - Heuristic limitations documented: false negatives possible if event name is computed via complex variable assignment; manual review at addition time required.
183
+ - ALLOW_LIST mechanism for primitives where the commit model legitimately omits `change` (none at v0.5.15 ship).
184
+ - Companion to slot 23 (`check-form-bearing-dts.mjs`): that audit verifies typed `<Comp>ChangeEvent` exists in `.d.ts`; this audit verifies the runtime dispatch.
185
+
186
+ ### v0.5.15 §318 — ADR-0028: L3 state-matrix orphan retirement (78 unused tokens deleted)
187
+
188
+ ADR-0028 ratifies the deletion of 78 orphaned L3 state-matrix tokens from `semantics.css`. The `maintain-tokens` skill audit during v0.5.14 §315 surfaced that 73 of ~80 L3 `--a-{family}-{prop}-{state}` tokens have zero consumers across the entire codebase. The per-primitive `--<comp>-*` token pattern (validated across v0.5.13/14/15 sweeps) is the actual consumer-facing API.
189
+
190
+ #### Removed — `styles/colors/semantics.css`
191
+
192
+ - 78 `--a-{accent,primary,brand,info,success,warning,danger,fg,border}-{fg,bg,border}-{active,selected,disabled,invalid}` declarations deleted. ~80 LOC reduction.
193
+ - Token count dropped 1301 → 1217 (-6.5%).
194
+ - Kept (have consumers): `--a-bg-{active,selected,disabled,invalid}`, `--a-fg-{selected,disabled}`, `--a-accent-bg-active`, `--a-primary-bg-active` (transitive via accent-bg-active).
195
+ - Kept (consumer-facing hover state): all `--a-*-hover` variants — hover is the only state where the generic-L3 layer is load-bearing.
196
+
197
+ #### Added — `.brain/adrs/0028-l3-state-matrix-orphan-retirement.md`
198
+
199
+ - ADR documents the audit-driven discovery, the retention rationale for the 7 used tokens, the deletion rationale for the 78 orphans, alternatives considered (keep all / delete only family-specific / add audit), and the per-primitive convention as the canonical consumer-facing override surface.
200
+
14
201
  ## [0.5.14] - 2026-05-15
15
202
 
16
203
  ### v0.5.14 — Form-control hover-fg token unification (precursor to §315; commit `178418525`)
@@ -77,7 +77,7 @@
77
77
  min-height: var(--heatmap-cell-min-size);
78
78
  aspect-ratio: 1 / 1;
79
79
  cursor: default;
80
- transition: transform 0.1s ease-out;
80
+ transition: transform var(--a-duration-fast) var(--a-easing-out);
81
81
  }
82
82
 
83
83
  :scope [data-cell][data-v] {
@@ -2,9 +2,9 @@
2
2
  :where(:scope) {
3
3
  /* ── Colors ── */
4
4
  --list-divider-color: var(--a-border-subtle);
5
- --list-selected-bg: var(--a-accent-muted);
6
- --list-selected-fg: var(--a-accent-strong);
7
- --list-selected-border: var(--a-accent-strong);
5
+ --list-bg-selected: var(--a-accent-muted);
6
+ --list-fg-selected: var(--a-accent-strong);
7
+ --list-border-selected: var(--a-accent-strong);
8
8
  }
9
9
 
10
10
  :scope {
@@ -68,9 +68,9 @@
68
68
  }
69
69
 
70
70
  :scope[selectable] > [role="listitem"][aria-selected="true"] {
71
- background: var(--list-selected-bg);
72
- color: var(--list-selected-fg);
73
- border-inline-start-color: var(--list-selected-border);
71
+ background: var(--list-bg-selected);
72
+ color: var(--list-fg-selected);
73
+ border-inline-start-color: var(--list-border-selected);
74
74
  }
75
75
 
76
76
  :scope[selectable] > [role="listitem"]:focus-visible {
@@ -240,7 +240,7 @@ nav-group-ui [slot="popover"] [role="option"] {
240
240
  }
241
241
 
242
242
  nav-group-ui [slot="popover"] [role="option"]:hover {
243
- background: var(--a-bg-hover);
243
+ background: var(--nav-group-bg-hover);
244
244
  color: var(--nav-group-fg-hover);
245
245
  }
246
246
 
@@ -249,7 +249,7 @@ nav-group-ui [slot="popover"] [role="option"]:hover {
249
249
  nav-group-ui [slot="popover"] [role="option"][aria-current="page"],
250
250
  nav-group-ui [slot="popover"] [role="option"][aria-selected="true"] {
251
251
  position: relative;
252
- background: var(--a-bg-hover);
252
+ background: var(--nav-group-bg-hover);
253
253
  color: var(--nav-group-fg-selected);
254
254
  font-weight: var(--a-weight-medium);
255
255
  }
@@ -58,7 +58,7 @@
58
58
  top: 0;
59
59
  z-index: 1;
60
60
  background: var(--page-sticky-bg);
61
- transition: border-color 150ms ease, box-shadow 150ms ease;
61
+ transition: border-color var(--a-duration-fast) var(--a-easing), box-shadow var(--a-duration-fast) var(--a-easing);
62
62
  }
63
63
 
64
64
  :scope[data-header-stuck] > :where(header, header-ui) {
@@ -196,7 +196,7 @@ select-ui [slot="listbox"] {
196
196
  overflow-y: auto;
197
197
  font-family: inherit;
198
198
  font-size: var(--a-ui-size);
199
- color: var(--a-fg);
199
+ color: var(--select-fg);
200
200
 
201
201
  /* Positioned by JS (#positionListbox) — fixed to viewport */
202
202
  width: max-content;
@@ -316,6 +316,6 @@ select-ui > [slot="hint"] {
316
316
  display: block;
317
317
  margin-top: var(--select-hint-mt, var(--a-space-1));
318
318
  font-size: var(--select-hint-size, var(--a-ui-xs));
319
- color: var(--select-hint-fg, var(--a-fg-muted));
319
+ color: var(--select-hint-fg, var(--select-fg-muted));
320
320
  line-height: var(--select-hint-lh, 1.4);
321
321
  }
@@ -20,13 +20,13 @@
20
20
  --stepper-weight: var(--a-weight-medium);
21
21
  --stepper-border-size: 2px;
22
22
 
23
- --stepper-active-bg: var(--a-bg);
24
- --stepper-active-border: var(--a-accent);
25
- --stepper-active-fg: var(--a-accent);
23
+ --stepper-bg-active: var(--a-bg);
24
+ --stepper-border-active: var(--a-accent);
25
+ --stepper-fg-active: var(--a-accent);
26
26
 
27
- --stepper-done-bg: var(--a-accent);
28
- --stepper-done-border: var(--a-accent);
29
- --stepper-done-fg: var(--a-accent-fg);
27
+ --stepper-bg-done: var(--a-accent);
28
+ --stepper-border-done: var(--a-accent);
29
+ --stepper-fg-done: var(--a-accent-fg);
30
30
 
31
31
  --stepper-line: var(--a-border-subtle);
32
32
  --stepper-line-done: var(--a-accent);
@@ -76,13 +76,13 @@
76
76
  --stepper-item-weight: var(--stepper-weight, var(--a-weight-medium));
77
77
  --stepper-item-border-size: var(--stepper-border-size, 2px);
78
78
 
79
- --stepper-item-active-bg: var(--stepper-active-bg, var(--a-bg));
80
- --stepper-item-active-border: var(--stepper-active-border, var(--a-accent));
81
- --stepper-item-active-fg: var(--stepper-active-fg, var(--a-accent));
79
+ --stepper-item-bg-active: var(--stepper-bg-active, var(--a-bg));
80
+ --stepper-item-border-active: var(--stepper-border-active, var(--a-accent));
81
+ --stepper-item-fg-active: var(--stepper-fg-active, var(--a-accent));
82
82
 
83
- --stepper-item-done-bg: var(--stepper-done-bg, var(--a-accent));
84
- --stepper-item-done-border: var(--stepper-done-border, var(--a-accent));
85
- --stepper-item-done-fg: var(--stepper-done-fg, var(--a-accent-fg));
83
+ --stepper-item-bg-done: var(--stepper-bg-done, var(--a-accent));
84
+ --stepper-item-border-done: var(--stepper-border-done, var(--a-accent));
85
+ --stepper-item-fg-done: var(--stepper-fg-done, var(--a-accent-fg));
86
86
 
87
87
  --stepper-item-line: var(--stepper-line, var(--a-border-subtle));
88
88
  --stepper-item-line-done: var(--stepper-line-done, var(--a-accent));
@@ -158,9 +158,9 @@
158
158
 
159
159
  /* Active state */
160
160
  :scope[status="active"]::after {
161
- border-color: var(--stepper-item-active-border);
162
- color: var(--stepper-item-active-fg);
163
- background: var(--stepper-item-active-bg);
161
+ border-color: var(--stepper-item-border-active);
162
+ color: var(--stepper-item-fg-active);
163
+ background: var(--stepper-item-bg-active);
164
164
  }
165
165
 
166
166
  :scope[status="active"] [slot="label"] {
@@ -171,9 +171,9 @@
171
171
  /* Completed state — checkmark replaces number */
172
172
  :scope[status="completed"]::after {
173
173
  content: '\2713';
174
- background: var(--stepper-item-done-bg);
175
- border-color: var(--stepper-item-done-border);
176
- color: var(--stepper-item-done-fg);
174
+ background: var(--stepper-item-bg-done);
175
+ border-color: var(--stepper-item-border-done);
176
+ color: var(--stepper-item-fg-done);
177
177
  }
178
178
 
179
179
  :scope[status="completed"]::before {
@@ -214,15 +214,15 @@
214
214
  }
215
215
 
216
216
  :scope[status="active"] [slot="icon"] {
217
- border-color: var(--stepper-item-active-border);
218
- color: var(--stepper-item-active-fg);
219
- background: var(--stepper-item-active-bg);
217
+ border-color: var(--stepper-item-border-active);
218
+ color: var(--stepper-item-fg-active);
219
+ background: var(--stepper-item-bg-active);
220
220
  }
221
221
 
222
222
  :scope[status="completed"] [slot="icon"] {
223
- background: var(--stepper-item-done-bg);
224
- border-color: var(--stepper-item-done-border);
225
- color: var(--stepper-item-done-fg);
223
+ background: var(--stepper-item-bg-done);
224
+ border-color: var(--stepper-item-border-done);
225
+ color: var(--stepper-item-fg-done);
226
226
  }
227
227
 
228
228
  /* ── Content slots ── */
@@ -226,6 +226,17 @@ export class UISwatch extends UIElement {
226
226
  // Capture pre-existing default-slot content so consumer-authored
227
227
  // children (e.g. <swatch-ui>Forecast</swatch-ui>) survive stamping.
228
228
  // `[slot="chrome"]` children are already removed above, so won't appear here.
229
+ //
230
+ // §325 (v0.5.16, FB-36): filter pure-whitespace text nodes from the
231
+ // capture. Pre-§325, indented multi-line HTML (`<swatch-ui
232
+ // label="500">\n <span>tip</span>\n</swatch-ui>`) captured the
233
+ // leading whitespace text node, which then made #labelEl.firstChild
234
+ // a nodeType=3 text node. #syncCore()'s "stale-text-node" branch
235
+ // matched the whitespace + did textContent = this.label, wiping
236
+ // BOTH the whitespace AND the consumer's <span>. Filter here so
237
+ // #labelEl.firstChild is the actual element/non-whitespace text,
238
+ // and #syncCore's stale-text branch only fires for genuinely
239
+ // attr-driven cases.
229
240
  const slotted = Array.from(this.childNodes).filter(n =>
230
241
  !(n.nodeType === 1 && n.dataset && (
231
242
  n.dataset.tile !== undefined ||
@@ -233,7 +244,7 @@ export class UISwatch extends UIElement {
233
244
  n.dataset.detail !== undefined ||
234
245
  n.dataset.badge !== undefined ||
235
246
  n.dataset.copy !== undefined
236
- ))
247
+ )) && !(n.nodeType === 3 && !n.textContent.trim())
237
248
  );
238
249
  this.innerHTML = '';
239
250
 
@@ -282,9 +293,53 @@ export class UISwatch extends UIElement {
282
293
 
283
294
  // ── Sync ──────────────────────────────────────────────────────────
284
295
 
296
+ /**
297
+ * §326 (v0.5.16, FB-37): Late-arriving `[slot="chrome"]` children may
298
+ * land on the host AFTER `#stamp()` runs — happens in any template
299
+ * engine (lit-html, AdiaUI `html\`\``, etc.) that batches child appends
300
+ * separately from element creation. `#stamp()` is gated by
301
+ * `this.#stamped` and won't re-run; this method picks up the late
302
+ * arrivals on every `render()` pass + moves them to the canonical
303
+ * sibling-of-tile position (between `#tileEl` and `#badgeEl`).
304
+ *
305
+ * Idempotent: chrome children already in the right slot are left
306
+ * alone (the `insertBefore` only moves them if they're not the
307
+ * #badgeEl's immediate previousSibling chain).
308
+ */
309
+ #absorbChromeSlot() {
310
+ if (!this.#tileEl || !this.#badgeEl) return;
311
+ const chromeChildren = [];
312
+ for (const child of this.children) {
313
+ if (child === this.#tileEl) continue;
314
+ if (child === this.#badgeEl) break; // hit the badge — done scanning the head region
315
+ if (child.getAttribute?.('slot') === 'chrome') chromeChildren.push(child);
316
+ }
317
+ // Walk the rest of the children looking for chrome children that landed
318
+ // AFTER the internal structure (the common template-late-append case).
319
+ for (const child of Array.from(this.children)) {
320
+ if (
321
+ child !== this.#tileEl &&
322
+ child !== this.#badgeEl &&
323
+ child !== this.#labelEl &&
324
+ child !== this.#detailEl &&
325
+ child !== this.#copyEl &&
326
+ child.getAttribute?.('slot') === 'chrome'
327
+ ) {
328
+ // Move to the canonical position: immediately after #tileEl.
329
+ // insertBefore is idempotent — moving an already-positioned child
330
+ // to the same spot is a no-op.
331
+ this.insertBefore(child, this.#badgeEl);
332
+ }
333
+ }
334
+ }
335
+
285
336
  #syncCore() {
286
337
  if (!this.#tileEl || !this.#labelEl) return;
287
338
 
339
+ // §326 (v0.5.16, FB-37): pick up `[slot="chrome"]` children that
340
+ // landed after #stamp() ran (template-render path).
341
+ this.#absorbChromeSlot();
342
+
288
343
  // Normalize enum values; fall back silently when a consumer sets a typo.
289
344
  const shape = SHAPES.has(this.shape) ? this.shape : 'square';
290
345
  const size = SIZES.has(this.size) ? this.size : 'md';
@@ -291,7 +291,7 @@
291
291
  line-height: 1;
292
292
  cursor: pointer;
293
293
  border-radius: var(--a-radius-xs);
294
- transition: color 100ms ease-out, background 100ms ease-out;
294
+ transition: color var(--a-duration-fast) var(--a-easing-out), background var(--a-duration-fast) var(--a-easing-out);
295
295
  }
296
296
  :scope > [data-copy]:hover { color: var(--a-fg); background: var(--a-bg-muted); }
297
297
  :scope > [data-copy]:focus-visible {
@@ -308,7 +308,7 @@
308
308
  :scope[selectable] {
309
309
  cursor: pointer;
310
310
  border-radius: var(--a-radius-sm);
311
- transition: box-shadow 120ms ease-out;
311
+ transition: box-shadow var(--a-duration-fast) var(--a-easing-out);
312
312
  }
313
313
  :scope[selectable]:focus { outline: none; }
314
314
  :scope[selectable]:focus-visible {
@@ -0,0 +1,163 @@
1
+ /**
2
+ * <swatch-ui> behavioral tests.
3
+ *
4
+ * Currently scoped to FB-36 / §325 regression coverage. Future tests for
5
+ * #stamp() lifecycle, chrome-slot composition, auto-contrast probe,
6
+ * etc. land here.
7
+ *
8
+ * Test setup note: happy-dom connects custom elements synchronously
9
+ * during innerHTML parsing — BEFORE sibling children inside the same
10
+ * element are appended. Real browsers connect after the full innerHTML
11
+ * is parsed. To exercise the FB-36 path (children present at connect
12
+ * time), tests use `document.createElement` + `appendChild` to build
13
+ * the full subtree FIRST, then attach to the document — guaranteeing
14
+ * the children are in `this.childNodes` when `connectedCallback()` fires.
15
+ */
16
+
17
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
18
+
19
+ beforeAll(async () => {
20
+ await import('./swatch.js');
21
+ });
22
+
23
+ function buildSwatch({ label, defaultSlot, shape = 'block', labelPosition = 'overlay', color = '#3b82f6' }) {
24
+ const swatch = document.createElement('swatch-ui');
25
+ swatch.setAttribute('shape', shape);
26
+ swatch.setAttribute('label-position', labelPosition);
27
+ swatch.setAttribute('color', color);
28
+ if (label !== undefined) swatch.setAttribute('label', label);
29
+ if (defaultSlot) {
30
+ // Mimic indented HTML's whitespace text nodes — the FB-36 trigger.
31
+ swatch.appendChild(document.createTextNode('\n '));
32
+ const child = document.createElement('span');
33
+ child.id = defaultSlot.id;
34
+ child.textContent = defaultSlot.text;
35
+ swatch.appendChild(child);
36
+ swatch.appendChild(document.createTextNode('\n '));
37
+ }
38
+ return swatch;
39
+ }
40
+
41
+ describe('<swatch-ui> default-slot + label-attr precedence (FB-36 / §325)', () => {
42
+ let host;
43
+
44
+ beforeEach(() => {
45
+ host = document.createElement('div');
46
+ document.body.appendChild(host);
47
+ });
48
+
49
+ it('preserves default-slot element when label="X" is also present (FB-36 §325 fix)', async () => {
50
+ const swatch = buildSwatch({
51
+ label: '500',
52
+ defaultSlot: { id: 'probe-default', text: 'richlabel' },
53
+ });
54
+ host.appendChild(swatch);
55
+ await new Promise((r) => setTimeout(r, 80));
56
+
57
+ const label = swatch.querySelector('[data-label]');
58
+ const span = swatch.querySelector('#probe-default');
59
+
60
+ // Pre-§325 the span was wiped + label="500" took precedence.
61
+ // Post-§325 the default-slot wins (yaml spec: "richer content").
62
+ expect(span).not.toBeNull();
63
+ expect(label.textContent.trim()).toBe('richlabel');
64
+ });
65
+
66
+ it('renders label-attr text when no default-slot is present', async () => {
67
+ const swatch = buildSwatch({ label: '500' });
68
+ host.appendChild(swatch);
69
+ await new Promise((r) => setTimeout(r, 80));
70
+ const label = swatch.querySelector('[data-label]');
71
+ expect(label.textContent.trim()).toBe('500');
72
+ });
73
+
74
+ it('renders default-slot text when no label-attr is present', async () => {
75
+ const swatch = buildSwatch({
76
+ defaultSlot: { id: 'probe-only', text: 'defaultonly' },
77
+ });
78
+ host.appendChild(swatch);
79
+ await new Promise((r) => setTimeout(r, 80));
80
+ const label = swatch.querySelector('[data-label]');
81
+ const span = swatch.querySelector('#probe-only');
82
+ expect(span).not.toBeNull();
83
+ expect(label.textContent.trim()).toBe('defaultonly');
84
+ });
85
+
86
+ it('syncs label-attr changes when there is no default-slot (no whitespace regression)', async () => {
87
+ const swatch = buildSwatch({ label: '500' });
88
+ host.appendChild(swatch);
89
+ await new Promise((r) => setTimeout(r, 80));
90
+ swatch.label = '600';
91
+ await new Promise((r) => setTimeout(r, 80));
92
+ const label = swatch.querySelector('[data-label]');
93
+ expect(label.textContent.trim()).toBe('600');
94
+ });
95
+ });
96
+
97
+ describe('<swatch-ui> late-arriving [slot="chrome"] children (FB-37 / §326)', () => {
98
+ let host;
99
+
100
+ beforeEach(() => {
101
+ host = document.createElement('div');
102
+ document.body.appendChild(host);
103
+ });
104
+
105
+ it('absorbs chrome child appended AFTER connectedCallback (template-render path)', async () => {
106
+ // Mimic the template-engine timing: create the swatch + connect it FIRST
107
+ // (children empty), then append the chrome child + trigger a render.
108
+ const swatch = document.createElement('swatch-ui');
109
+ swatch.setAttribute('shape', 'block');
110
+ swatch.setAttribute('label-position', 'overlay');
111
+ swatch.setAttribute('color', '#3b82f6');
112
+ swatch.setAttribute('label', '500');
113
+ host.appendChild(swatch);
114
+ await new Promise((r) => setTimeout(r, 80));
115
+
116
+ // At this point #stamp() has run + chromeSlot was empty.
117
+ // Append a chrome child LATER — this is what template engines do.
118
+ const chrome = document.createElement('span');
119
+ chrome.setAttribute('slot', 'chrome');
120
+ chrome.id = 'late-chrome';
121
+ chrome.textContent = '★';
122
+ swatch.appendChild(chrome);
123
+
124
+ // Trigger a re-render via property mutation (template engines typically
125
+ // set props after appending — this nudge fires render → #syncCore →
126
+ // #absorbChromeSlot picks up the late chrome child). Must use a value
127
+ // DIFFERENT from current to actually fire the signal write.
128
+ swatch.label = '999';
129
+ await new Promise((r) => setTimeout(r, 80));
130
+
131
+ const found = swatch.querySelector('#late-chrome');
132
+ expect(found).not.toBeNull();
133
+ // Chrome child should be a direct host sibling positioned between tile + badge.
134
+ const tile = swatch.querySelector('[data-tile]');
135
+ const badge = swatch.querySelector('[data-badge]');
136
+ expect(found.previousElementSibling).toBe(tile);
137
+ expect(found.nextElementSibling).toBe(badge);
138
+ });
139
+
140
+ it('still handles chrome child appended BEFORE connect (static-HTML path)', async () => {
141
+ // Build swatch with chrome child present at connect time — the canonical
142
+ // FB-34 / §314 path should still work post-§326.
143
+ const swatch = document.createElement('swatch-ui');
144
+ swatch.setAttribute('shape', 'block');
145
+ swatch.setAttribute('label-position', 'overlay');
146
+ swatch.setAttribute('color', '#3b82f6');
147
+ swatch.setAttribute('label', '500');
148
+ const chrome = document.createElement('span');
149
+ chrome.setAttribute('slot', 'chrome');
150
+ chrome.id = 'early-chrome';
151
+ chrome.textContent = '⚑';
152
+ swatch.appendChild(chrome);
153
+ host.appendChild(swatch);
154
+ await new Promise((r) => setTimeout(r, 80));
155
+
156
+ const found = swatch.querySelector('#early-chrome');
157
+ expect(found).not.toBeNull();
158
+ const tile = swatch.querySelector('[data-tile]');
159
+ const badge = swatch.querySelector('[data-badge]');
160
+ expect(found.previousElementSibling).toBe(tile);
161
+ expect(found.nextElementSibling).toBe(badge);
162
+ });
163
+ });
@@ -36,7 +36,7 @@
36
36
  "default": false
37
37
  },
38
38
  "variant": {
39
- "description": "Typography variant — sets role tokens (size/weight/tracking/color).",
39
+ "description": "Typography variant — sets design-role tokens (size / weight /\ntracking / color / leading) per the L0 typography token family.\n**PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant=\"heading\">`\ndoes NOT set `role=\"heading\"` + `aria-level` on the host. Document-\noutline assistive technology will treat the element as `role=\"generic\"`.\nFor semantic headings, wrap with native `<h1>`-`<h6>` OR add\n`role=\"heading\" aria-level=\"N\"` on the host directly. For visual-only\nlabels (eyebrows, kickers, captions, deck), the presentational default\nis correct. The §221k chooser guide in USAGE.md documents picker heuristics.",
40
40
  "type": "string",
41
41
  "enum": [
42
42
  "body",
@@ -52,7 +52,21 @@
52
52
  "metric",
53
53
  "code"
54
54
  ],
55
- "default": "body"
55
+ "default": "body",
56
+ "enum_descriptions": {
57
+ "title": "Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog.",
58
+ "body": "Default body copy. 14px / regular. Paragraphs, multi-line content, running prose.",
59
+ "caption": "Annotation under a primary line — smaller + muted. Use for image captions, footnotes.",
60
+ "code": "Inline monospace code reference. Use for inline code within prose.",
61
+ "deck": "Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title.",
62
+ "display": "Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners.",
63
+ "heading": "Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers.",
64
+ "kicker": "Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those).",
65
+ "label": "Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized + medium-weight. Use for field labels bound to form controls.",
66
+ "metric": "Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers.",
67
+ "section": "Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings.",
68
+ "subsection": "Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section."
69
+ }
56
70
  }
57
71
  },
58
72
  "required": [
@@ -1,41 +1,28 @@
1
1
  /**
2
- * `<text-ui>` — Typography wrapper that applies role tokens. Supports
3
- * truncation and line clamping.
2
+ * `<text-ui>` — Typography wrapper that applies role tokens. Supports truncation and line clamping.
4
3
  *
5
4
  * @see https://ui-kit.exe.xyz/site/components/text
6
5
  *
7
- * HAND-AUTHORED (not codegen'd) per §247 (v0.5.10) — the yaml `enum:` schema
8
- * carries variant names but not per-value descriptions; codegen emits a
9
- * single property-level JSDoc and loses the per-variant disambiguation that
10
- * the §221k chooser guide documents. Hand-authored .d.ts carries per-variant
11
- * JSDoc inline so IDE hover surfaces the disambiguation at authoring time.
12
- * Also carries the FB-23 §2 ARIA-role disclaimer on the property doc.
13
- * `dts-codegen.mjs` keeps this file in its `HAND_AUTHORED_DTS` skip list.
6
+ * Type declarations generated by scripts/build/dts-codegen.mjs from
7
+ * the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
8
+ * run `npm run build:components`, then `npm run codegen:dts` to
9
+ * regenerate; or hand-author this file fully if rich event types are
10
+ * needed beyond what the yaml `events:` block can express.
14
11
  */
15
12
 
16
13
  import { UIElement } from '../../core/element.js';
17
14
 
18
- /**
19
- * Typography variants — visual rank from `display` (largest, hero) to
20
- * `code` (inline monospace). Pick by intent, not by visual size — every
21
- * variant has a distinct typographic role per §221k chooser guide.
22
- *
23
- * See `packages/web-components/USAGE.md` "v0.5.9 — Consumer-feedback
24
- * discoverability sweep" §221k for the picker heuristics.
25
- */
26
15
  export type UITextVariant =
27
16
  /** Default body copy. 14px / regular. Paragraphs, multi-line content, running prose. */
28
17
  | 'body'
29
- /** Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners. */
30
- | 'display'
31
- /** Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog. */
32
- | 'title'
33
18
  /** Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers. */
34
19
  | 'heading'
20
+ /** Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog. */
21
+ | 'title'
35
22
  /** Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section. */
36
23
  | 'subsection'
37
- /** Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings. */
38
- | 'section'
24
+ /** Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners. */
25
+ | 'display'
39
26
  /** Annotation under a primary line — smaller + muted. Use for image captions, footnotes. */
40
27
  | 'caption'
41
28
  /** Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized + medium-weight. Use for field labels bound to form controls. */
@@ -44,34 +31,30 @@ export type UITextVariant =
44
31
  | 'kicker'
45
32
  /** Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title. */
46
33
  | 'deck'
34
+ /** Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings. */
35
+ | 'section'
47
36
  /** Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers. */
48
37
  | 'metric'
49
- /** Inline monospace code reference. Use for `\`code\`` inline within prose. */
38
+ /** Inline monospace code reference. Use for inline code within prose. */
50
39
  | 'code';
51
40
 
52
41
  export class UIText extends UIElement {
53
- /**
54
- * Typography variant — sets design-role tokens (size / weight / tracking /
55
- * color / leading) per the L0 typography token family.
56
- *
57
- * **PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant="heading">`
58
- * does NOT set `role="heading"` + `aria-level` on the host. Document-outline
59
- * assistive technology will treat the element as `role="generic"`. For
60
- * **semantic** headings (screen-reader heading-navigation, document
61
- * outline), wrap with native `<h1>`-`<h6>` OR add
62
- * `role="heading" aria-level="N"` on the host element directly.
63
- *
64
- * For visual-only labels (eyebrows, kickers, captions, deck), the
65
- * presentational default is correct. The §221k chooser guide in USAGE.md
66
- * documents the picker heuristics.
67
- */
68
- variant: UITextVariant;
69
- /** When true, applies stronger emphasis (heavier weight + accent color). Styled via `:scope[strong]` in text.css. Use instead of `variant="heading"` when you want a single emphasized word inline in body copy. */
70
- strong: boolean;
71
- /** Single-line truncation with ellipsis. Ignored when `lines` is set. */
72
- truncate: boolean;
73
- /** Multi-line clamp count (0 = no clamp). Sets `--_text-lines` for `-webkit-line-clamp`. */
42
+ /** Multi-line clamp count (0 = no clamp) */
74
43
  lines: number;
44
+ /** When true, applies stronger emphasis (heavier weight + accent color). Styled via :scope[strong] in text.css. Use instead of variant=heading when you want a single emphasized word inline in body copy. */
45
+ strong: boolean;
75
46
  /** Display text content. The main payload field for Text components extracted from HTML. */
76
47
  textContent: string;
48
+ /** Single-line truncation with ellipsis. Ignored when `lines` is set. */
49
+ truncate: boolean;
50
+ /** Typography variant — sets design-role tokens (size / weight /
51
+ tracking / color / leading) per the L0 typography token family.
52
+ **PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant="heading">`
53
+ does NOT set `role="heading"` + `aria-level` on the host. Document-
54
+ outline assistive technology will treat the element as `role="generic"`.
55
+ For semantic headings, wrap with native `<h1>`-`<h6>` OR add
56
+ `role="heading" aria-level="N"` on the host directly. For visual-only
57
+ labels (eyebrows, kickers, captions, deck), the presentational default
58
+ is correct. The §221k chooser guide in USAGE.md documents picker heuristics. */
59
+ variant: UITextVariant;
77
60
  }
@@ -28,7 +28,16 @@ props:
28
28
  default: ""
29
29
  dynamic: true
30
30
  variant:
31
- description: Typography variant — sets role tokens (size/weight/tracking/color).
31
+ description: |-
32
+ Typography variant — sets design-role tokens (size / weight /
33
+ tracking / color / leading) per the L0 typography token family.
34
+ **PRESENTATIONAL-ONLY (§247, FB-23 §2).** `<text-ui variant="heading">`
35
+ does NOT set `role="heading"` + `aria-level` on the host. Document-
36
+ outline assistive technology will treat the element as `role="generic"`.
37
+ For semantic headings, wrap with native `<h1>`-`<h6>` OR add
38
+ `role="heading" aria-level="N"` on the host directly. For visual-only
39
+ labels (eyebrows, kickers, captions, deck), the presentational default
40
+ is correct. The §221k chooser guide in USAGE.md documents picker heuristics.
32
41
  type: string
33
42
  default: body
34
43
  enum:
@@ -44,6 +53,21 @@ props:
44
53
  - section
45
54
  - metric
46
55
  - code
56
+ # §324 (v0.5.15, FB-23 §3 retire): per-variant JSDoc surfaces at IDE
57
+ # hover via codegen. Retires the HAND_AUTHORED_DTS skip for text.d.ts.
58
+ enum_descriptions:
59
+ body: Default body copy. 14px / regular. Paragraphs, multi-line content, running prose.
60
+ display: Top-level hero / brand display. Tallest visual rank. Use for page-level hero one-liners.
61
+ title: Page title (visual rank H1). Largest under display. Use at the top of an authoritative page or dialog.
62
+ heading: Major page heading (visual rank H2). 16-18px / bold. Use for major sub-section dividers.
63
+ subsection: Sub-landmark within a section (visual rank H3). 14px / semibold. Use for card titles within a section.
64
+ section: Inline form-group / navlist heading (visual rank H4). Small-cap. Use for form group labels, nav list headings.
65
+ caption: Annotation under a primary line — smaller + muted. Use for image captions, footnotes.
66
+ label: Form-control label (above an `<input-ui>` / `<select-ui>` etc). UI-sized + medium-weight. Use for field labels bound to form controls.
67
+ kicker: Eyebrow text above a `title`. UPPERCASE + small + tracking. Use for content eyebrows (NOT form labels — use `label` for those).
68
+ deck: Sub-title under a `title`. One-line lead, slightly larger than body. Use for the lead sentence after a title.
69
+ metric: Numeric KPI / big-number stat. Bold + large. Use for dashboard metric numbers.
70
+ code: Inline monospace code reference. Use for inline code within prose.
47
71
  events: {}
48
72
  slots: {}
49
73
  states:
@@ -104,7 +104,7 @@ toolbar-ui [data-toolbar-spillover-menu]:popover-open {
104
104
  min-width: 10rem;
105
105
  font-family: inherit;
106
106
  font-size: var(--a-ui-size);
107
- color: var(--a-fg);
107
+ color: var(--toolbar-fg);
108
108
  /* Stack overflow items as rows (each row can be a group with its own gap). */
109
109
  display: flex;
110
110
  flex-direction: column;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.5.14",
3
+ "version": "0.5.15",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
5
5
  "type": "module",
6
6
  "types": "./index.d.ts",
@@ -116,20 +116,21 @@
116
116
  --a-fg-muted: var(--a-canvas-text-muted);
117
117
  --a-fg-strong: var(--a-canvas-text-strong);
118
118
  --a-fg-hover: var(--a-canvas-text-strong);
119
- --a-fg-active: var(--a-canvas-text-strong);
119
+ /* ADR-0028: --a-fg-{active,invalid} retired (zero consumers). Use
120
+ `--a-ui-text-active` / `--a-ui-text-invalid` for form-control state,
121
+ or define a per-primitive `--<comp>-fg-{active,invalid}` token. */
120
122
  --a-fg-selected: var(--a-canvas-text-strong);
121
123
  --a-fg-disabled: var(--a-canvas-text-disabled);
122
- --a-fg-invalid: var(--a-danger-text);
123
124
  --a-fg-inverse: var(--a-canvas-text-inverse);
124
125
 
125
126
  --a-border: var(--a-canvas-border);
126
127
  --a-border-subtle: var(--a-canvas-border-subtle);
127
128
  --a-border-strong: var(--a-canvas-border-strong);
128
129
  --a-border-hover: var(--a-canvas-border-strong);
129
- --a-border-active: var(--a-accent-border);
130
- --a-border-selected: var(--a-accent-border);
131
- --a-border-disabled: var(--a-canvas-border-subtle);
132
- --a-border-invalid: var(--a-danger-border);
130
+ /* ADR-0028: --a-border-{active,selected,disabled,invalid} retired (zero
131
+ consumers). Per-primitive `--<comp>-border-{state}` is the override
132
+ surface; `--a-focus-ring` carries focus-active borders for form
133
+ controls. */
133
134
 
134
135
  /* ══════════════════════════════════════════════════════════════
135
136
  ACCENT — Interactive, links, focus (source for --a-primary-*)
@@ -172,23 +173,12 @@
172
173
  --a-primary-bg: var(--a-accent-strong);
173
174
  --a-primary-bg-hover: var(--a-accent-hover);
174
175
  --a-primary-bg-active: var(--a-accent-active);
175
- --a-primary-bg-selected: var(--a-accent-selected);
176
- --a-primary-bg-disabled: var(--a-accent-muted);
177
- --a-primary-bg-invalid: var(--a-danger-muted);
178
176
 
179
177
  --a-primary-fg: var(--a-accent-05-tint);
180
178
  --a-primary-fg-hover: var(--a-accent-text-strong);
181
- --a-primary-fg-active: var(--a-accent-text-strong);
182
- --a-primary-fg-selected: var(--a-accent-text-strong);
183
- --a-primary-fg-disabled: var(--a-accent-text-disabled);
184
- --a-primary-fg-invalid: var(--a-danger-text);
185
179
 
186
180
  --a-primary-border: var(--a-accent-border);
187
181
  --a-primary-border-hover: var(--a-accent-border-strong);
188
- --a-primary-border-active: var(--a-accent-border-strong);
189
- --a-primary-border-selected: var(--a-accent-border-strong);
190
- --a-primary-border-disabled: var(--a-accent-border-subtle);
191
- --a-primary-border-invalid: var(--a-danger-border);
192
182
 
193
183
  /* Legacy shorthand — keep until all components migrate to --a-primary-* */
194
184
  --a-primary: var(--a-primary-bg);
@@ -200,16 +190,9 @@
200
190
  --a-accent-bg: var(--a-primary-bg);
201
191
  --a-accent-bg-hover: var(--a-primary-bg-hover);
202
192
  --a-accent-bg-active: var(--a-primary-bg-active);
203
- --a-accent-bg-selected: var(--a-primary-bg-selected);
204
- --a-accent-bg-disabled: var(--a-primary-bg-disabled);
205
- --a-accent-bg-invalid: var(--a-primary-bg-invalid);
206
193
 
207
194
  --a-accent-fg: var(--a-primary-fg);
208
195
  --a-accent-fg-hover: var(--a-primary-fg-hover);
209
- --a-accent-fg-active: var(--a-primary-fg-active);
210
- --a-accent-fg-selected: var(--a-primary-fg-selected);
211
- --a-accent-fg-disabled: var(--a-primary-fg-disabled);
212
- --a-accent-fg-invalid: var(--a-primary-fg-invalid);
213
196
 
214
197
  /* accent border is already defined at L2 (--a-accent-border*);
215
198
  L3 matches L2 since both resolve to the same scheme-aware value. */
@@ -239,24 +222,12 @@
239
222
  /* L3 — Brand surface role matrix */
240
223
  --a-brand-bg: var(--a-brand-strong);
241
224
  --a-brand-bg-hover: var(--a-brand-hover);
242
- --a-brand-bg-active: var(--a-brand-active);
243
- --a-brand-bg-selected: var(--a-brand-selected);
244
- --a-brand-bg-disabled: var(--a-brand-muted);
245
- --a-brand-bg-invalid: var(--a-danger-muted);
246
225
 
247
226
  --a-brand-fg: var(--a-brand-text-strong);
248
227
  --a-brand-fg-hover: var(--a-brand-text-strong);
249
- --a-brand-fg-active: var(--a-brand-text-strong);
250
- --a-brand-fg-selected: var(--a-brand-text-strong);
251
- --a-brand-fg-disabled: var(--a-brand-text-disabled);
252
- --a-brand-fg-invalid: var(--a-danger-text);
253
228
 
254
229
  /* L3 border state matrix — rest state is --a-brand-border (L2 above). */
255
230
  --a-brand-border-hover: var(--a-brand-border-strong);
256
- --a-brand-border-active: var(--a-brand-border-strong);
257
- --a-brand-border-selected: var(--a-brand-border-strong);
258
- --a-brand-border-disabled: var(--a-brand-border-subtle);
259
- --a-brand-border-invalid: var(--a-danger-border);
260
231
 
261
232
  /* ══════════════════════════════════════════════════════════════
262
233
  INFO
@@ -283,24 +254,12 @@
283
254
  /* L3 — Info surface role matrix */
284
255
  --a-info-bg: var(--a-info-strong);
285
256
  --a-info-bg-hover: var(--a-info-hover);
286
- --a-info-bg-active: var(--a-info-active);
287
- --a-info-bg-selected: var(--a-info-selected);
288
- --a-info-bg-disabled: var(--a-info-muted);
289
- --a-info-bg-invalid: var(--a-danger-muted);
290
257
 
291
258
  --a-info-fg: var(--a-info-text-strong);
292
259
  --a-info-fg-hover: var(--a-info-text-strong);
293
- --a-info-fg-active: var(--a-info-text-strong);
294
- --a-info-fg-selected: var(--a-info-text-strong);
295
- --a-info-fg-disabled: var(--a-info-text-disabled);
296
- --a-info-fg-invalid: var(--a-danger-text);
297
260
 
298
261
  /* L3 border state matrix — rest state is --a-info-border (L2 above). */
299
262
  --a-info-border-hover: var(--a-info-border-strong);
300
- --a-info-border-active: var(--a-info-border-strong);
301
- --a-info-border-selected: var(--a-info-border-strong);
302
- --a-info-border-disabled: var(--a-info-border-subtle);
303
- --a-info-border-invalid: var(--a-danger-border);
304
263
 
305
264
  /* ══════════════════════════════════════════════════════════════
306
265
  SUCCESS
@@ -327,24 +286,12 @@
327
286
  /* L3 — Success surface role matrix */
328
287
  --a-success-bg: var(--a-success-strong);
329
288
  --a-success-bg-hover: var(--a-success-hover);
330
- --a-success-bg-active: var(--a-success-active);
331
- --a-success-bg-selected: var(--a-success-selected);
332
- --a-success-bg-disabled: var(--a-success-muted);
333
- --a-success-bg-invalid: var(--a-danger-muted);
334
289
 
335
290
  --a-success-fg: var(--a-success-text-strong);
336
291
  --a-success-fg-hover: var(--a-success-text-strong);
337
- --a-success-fg-active: var(--a-success-text-strong);
338
- --a-success-fg-selected: var(--a-success-text-strong);
339
- --a-success-fg-disabled: var(--a-success-text-disabled);
340
- --a-success-fg-invalid: var(--a-danger-text);
341
292
 
342
293
  /* L3 border state matrix — rest state is --a-success-border (L2 above). */
343
294
  --a-success-border-hover: var(--a-success-border-strong);
344
- --a-success-border-active: var(--a-success-border-strong);
345
- --a-success-border-selected: var(--a-success-border-strong);
346
- --a-success-border-disabled: var(--a-success-border-subtle);
347
- --a-success-border-invalid: var(--a-danger-border);
348
295
 
349
296
  /* ══════════════════════════════════════════════════════════════
350
297
  WARNING
@@ -372,24 +319,12 @@
372
319
  /* L3 — Warning surface role matrix */
373
320
  --a-warning-bg: var(--a-warning-strong);
374
321
  --a-warning-bg-hover: var(--a-warning-hover);
375
- --a-warning-bg-active: var(--a-warning-active);
376
- --a-warning-bg-selected: var(--a-warning-selected);
377
- --a-warning-bg-disabled: var(--a-warning-muted);
378
- --a-warning-bg-invalid: var(--a-danger-muted);
379
322
 
380
323
  --a-warning-fg: var(--a-warning-text-strong);
381
324
  --a-warning-fg-hover: var(--a-warning-text-strong);
382
- --a-warning-fg-active: var(--a-warning-text-strong);
383
- --a-warning-fg-selected: var(--a-warning-text-strong);
384
- --a-warning-fg-disabled: var(--a-warning-text-disabled);
385
- --a-warning-fg-invalid: var(--a-danger-text);
386
325
 
387
326
  /* L3 border state matrix — rest state is --a-warning-border (L2 above). */
388
327
  --a-warning-border-hover: var(--a-warning-border-strong);
389
- --a-warning-border-active: var(--a-warning-border-strong);
390
- --a-warning-border-selected: var(--a-warning-border-strong);
391
- --a-warning-border-disabled: var(--a-warning-border-subtle);
392
- --a-warning-border-invalid: var(--a-danger-border);
393
328
 
394
329
  /* ══════════════════════════════════════════════════════════════
395
330
  DANGER
@@ -418,24 +353,12 @@
418
353
  /* L3 — Danger surface role matrix */
419
354
  --a-danger-bg: var(--a-danger-strong);
420
355
  --a-danger-bg-hover: var(--a-danger-hover);
421
- --a-danger-bg-active: var(--a-danger-active);
422
- --a-danger-bg-selected: var(--a-danger-selected);
423
- --a-danger-bg-disabled: var(--a-danger-muted);
424
- --a-danger-bg-invalid: var(--a-danger-muted);
425
356
 
426
357
  --a-danger-fg: var(--a-danger-text-strong);
427
358
  --a-danger-fg-hover: var(--a-danger-text-strong);
428
- --a-danger-fg-active: var(--a-danger-text-strong);
429
- --a-danger-fg-selected: var(--a-danger-text-strong);
430
- --a-danger-fg-disabled: var(--a-danger-text-disabled);
431
- --a-danger-fg-invalid: var(--a-danger-text);
432
359
 
433
360
  /* L3 border state matrix — rest state is --a-danger-border (L2 above). */
434
361
  --a-danger-border-hover: var(--a-danger-border-strong);
435
- --a-danger-border-active: var(--a-danger-border-strong);
436
- --a-danger-border-selected: var(--a-danger-border-strong);
437
- --a-danger-border-disabled: var(--a-danger-border-subtle);
438
- --a-danger-border-invalid: var(--a-danger-border);
439
362
 
440
363
  /* ══════════════════════════════════════════════════════════════
441
364
  BUCKETS — sequential 5-step ramp per family for data-vis consumers