@adia-ai/web-components 0.8.15 → 0.8.17

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
@@ -1,5 +1,34 @@
1
1
  # Changelog — @adia-ai/web-components
2
2
 
3
+ ## [0.8.17] — 2026-07-27
4
+
5
+ ### Added
6
+ - **`button-ui` gains `variant="tonal"`** (`components/button/`; gh#434) — the filled-tonal emphasis tier between `solid` and `outline`: a translucent `<family>-container` fill with scheme-flipped in-family text, deepening one container step on hover. Default rides the primary family (the classic soft brand button beside a solid CTA); composes with `[color]` exactly like `solid` (`variant="tonal" color="danger"` → the danger container fill). Deliberately **not** named `secondary`: `secondary` is a color family in this system, and an unset variant already IS the secondary-rank look (compose-constitution) — `tonal` names the tier without colliding with the hue.
7
+ - **8 new `on-<family>-container` color roles** (`styles/colors/material-static.css`) — Material's own missing on-container text role: `light-dark(<family>-950, <family>-200)`. Neither naive pairing works alone (the `-950` leg fails dark at 1.4–2.8:1; the `-200` leg fails light at ~1.0:1); the scheme-split composition measures 5.6–15.2:1 everywhere. `verify:contrast` grows 90 → **138 pairs**: the 48 new on-container × composited-container pairs (containers are alpha-ladder stops, so unlike opaque fills they are composited over the page background before measuring) were rostered and green **before** the variant's CSS existed — the gate-first discipline gh#427 established.
8
+
9
+ ### Changed
10
+ - **`select-ui` single-select options carry a radio indicator** (`components/select/`; operator ruling 2026-07-27) — the sibling of SPEC-040's multi-select checkbox: every single-select option row now renders a leading radio (bordered circle; primary ring + center dot when selected), and selection is carried by the indicator instead of a row tint. `--select-bg-selected` / `--select-fg-selected` remain consumer-overridable API but their DEFAULTS move to the plain row colors (`transparent` / `--select-fg`) — set `--select-bg-selected` to re-enable a tinted selected row. Hover/focus feedback restated on the selected row (the 2-attribute selected rule outweighed the plain `:hover` rule). Multi-select is unchanged — its checkbox treatment already carried the signal.
11
+
12
+ - **Selected surfaces ride the plain `-container` stop — the same tier as `variant="tonal"`'s rest fill** (`styles/colors/semantics/core.css`; operator ruling 2026-07-27). `--a-bg-selected` and `--a-canvas-selected` move from `neutral-container-active` (40%) to `neutral-container` (20%), so "selected" and "tonal" speak one surface language and the interaction ladder reads hover `container-low` (10%) → selected / tonal rest `container` (20%) → tonal hover `container-hover` (30%). Reaches every selected-state consumer through the alias: `nav-item-ui`, table row selection, table-toolbar menus, and `--a-ui-bg-selected` (select/combobox options). Verified live: selected measures α0.2 against tonal's α0.2, hover α0.1.
13
+
14
+ ### Fixed
15
+ - **Nav rails no longer render as section headings** (`styles/type/roles.css`) — the typographic role grammar's `[variant="section"]` selector matches *any* element, and the nav family (`nav-ui`, `nav-item-ui`, `nav-group-ui`) uses `variant="section"` **structurally** for the flat subnav-rail tier. The whole heading treatment — family, weight, size, tracking, case, color — landed on the rail and inherited into every item, so under any `[theme]` the subnav rendered in the theme's display face at heading weight (velour: Playfair Display w600). The role rule now excludes the three nav elements. The exclusion must live in the role rule: it sits in `@layer utilities` while component sheets load in the earlier `@layer components` (`styles/index.css` order), so a later-layer match beats anything a component declares about itself. `text-ui` is deliberately *not* excluded — its variant enum aligns with the role grammar on purpose. Scanned all 125 primitives + modules for other structural collisions with the 13 role names: the nav family is the only one.
16
+
17
+ ### Maintenance
18
+ - **`dist/` bundles rebuilt** in this cut's window (6 file(s)) — regenerated from the source changes described above, not independent edits.
19
+
20
+ ## [0.8.16] — 2026-07-26
21
+
22
+ ### Fixed
23
+ - **Text on the default palette's filled interaction states now meets WCAG AA** (`styles/colors/material-static.css`; gh#427) — scope of the claim: the 48 verified pairs are `on-<family>` over `<family>`, `-hover`, and `-active` for all 8 families in both schemes. Container, disabled, and per-`[theme]` fills are *not* covered by that run. — `on-<family>` text is the near-white `<family>-050` stop, and the resting fill was `<family>-500`, which measured **1.97:1 for `warning`** and 2.90–4.28:1 for the other seven: all 8 families shipped sub-AA text at rest. Worse, dark-mode `-active` resolved to the *light* `250` stop under that same near-white text, measuring **1.32–1.76:1** across all 8 — effectively invisible text while a button is pressed. The fill roles move to one monotonically darkening ladder, identical in both schemes: rest `600` / hover `650` / active `700`, with `secondary` at `650`/`700`/`750` and `warning` at `700`/`750`/`800` (its 1.97 start needed three steps). Measured after the change: every family clears 4.5:1 in all three states, worst cell 4.73:1 (`primary` at rest).
24
+
25
+ Two deliberate consequences. Fills are visibly darker, `warning` most of all. And dark mode loses its lighter-on-press behaviour — that `250` stop *was* the 1.32:1 defect, so press feedback now darkens in both schemes, which also removes the light/dark stop divergence in the fill ladder entirely.
26
+
27
+ - **`styles/themes.css`'s contrast claim corrected** — its header stated the hue-rotation "preserves the verified WCAG AA 4.5:1 thresholds (`npm run verify:contrast`, 42/42 pairs, **every theme**)". Two things were wrong: that 42-pair roster never checked a fill at all (gh#427), and the gate resolves only the default ramps — it does not iterate `[theme]` blocks, so no theme was ever measured. The header now states the real scope: 90 pairs proving the default palette, with per-theme ratios resting on the rotation argument (per-stop lightness untouched, and WCAG luminance is lightness-dominated) rather than on a measured run. Mechanizing per-theme verification is not done here.
28
+
29
+ ### Maintenance
30
+ - **`dist/` bundles rebuilt** in this cut's window (5 file(s)) — regenerated from the source changes described above, not independent edits.
31
+
3
32
  ## [0.8.15] — 2026-07-26
4
33
 
5
34
  ### Changed
@@ -78,7 +78,7 @@
78
78
  "$ref": "common_types.json#/$defs/DynamicString"
79
79
  },
80
80
  "variant": {
81
- "description": "Visual style — `solid` (default fill), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color=\"…\"] alongside.\nFor **inline navigation** (Terms of Service, Privacy Policy, footer links, \"Sign in\" / \"Sign up\" cross-page affordances) use `<link-ui>` instead — it carries proper `<a href>` semantics, keyboard handling (Enter only, no Space), middle-click open-new-tab, and screen-reader announces \"link\" instead of \"button\". Mixing navigation and action affordances under the same primitive is a category error fixed at this junction.",
81
+ "description": "Visual style — `solid` (default fill), `tonal` (soft container fill with in-family text — the emphasis tier between solid and outline; default rides the primary family), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color=\"…\"] alongside (`variant=\"tonal\" color=\"danger\"` = the danger family's container fill). There is deliberately no `variant=\"secondary\"`: `secondary` names a color family in this system, and an unset variant already IS the secondary-rank look (compose-constitution); `tonal` is the named tier for a softer-than-solid filled action.\nFor **inline navigation** (Terms of Service, Privacy Policy, footer links, \"Sign in\" / \"Sign up\" cross-page affordances) use `<link-ui>` instead — it carries proper `<a href>` semantics, keyboard handling (Enter only, no Space), middle-click open-new-tab, and screen-reader announces \"link\" instead of \"button\". Mixing navigation and action affordances under the same primitive is a category error fixed at this junction.",
82
82
  "type": "string",
83
83
  "enum": [
84
84
  "default",
@@ -34,6 +34,15 @@ button-ui[color="info"]:not([disabled]):hover { --button-bg: var(--a-info-
34
34
  -20 tint (--a-warning-bg) for clean contrast. (dogfood-audit: warning-strong-vs-bg) */
35
35
  button-ui[color="warning"]:not([disabled]):hover { --button-bg: var(--a-warning-bg); }
36
36
 
37
+ /* Tonal — the fill deepens one container step (container → container-hover).
38
+ Text stays on-<family>-container (gh#434; scheme-flipped in-family text);
39
+ every on-container × container-hover pair is AA-gated in verify:contrast. */
40
+ button-ui[variant="tonal"]:not([disabled]):hover { --button-bg: var(--md-sys-color-primary-container-hover); }
41
+ button-ui[variant="tonal"][color="danger"]:not([disabled]):hover { --button-bg: var(--md-sys-color-danger-container-hover); }
42
+ button-ui[variant="tonal"][color="success"]:not([disabled]):hover { --button-bg: var(--md-sys-color-success-container-hover); }
43
+ button-ui[variant="tonal"][color="info"]:not([disabled]):hover { --button-bg: var(--md-sys-color-info-container-hover); }
44
+ button-ui[variant="tonal"][color="warning"]:not([disabled]):hover { --button-bg: var(--md-sys-color-warning-container-hover); }
45
+
37
46
  /* Outline / ghost — no fill on hover; fg goes rest-color → `-strong`.
38
47
  Ghost reads --button-fg-ghost-hover (default --a-fg-strong) so consumers
39
48
  like alert.css can retint the ghost-hover label, and --button-bg/border-
@@ -220,6 +229,20 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
220
229
  --button-border: var(--button-border-outline);
221
230
  }
222
231
 
232
+ /* Tonal (gh#434) — the filled-tonal tier between solid and outline:
233
+ a translucent container fill with dark in-family text. Default rides
234
+ the PRIMARY family (the classic soft brand button beside a solid CTA);
235
+ [color] swaps the family below. Named `tonal`, never `secondary`:
236
+ `secondary` is a color family in this system, and the compose-
237
+ constitution keeps "unset = the secondary rank". Text role is
238
+ on-<family>-container = light-dark(950, 200) — every pair over
239
+ container/-hover/-active is AA-gated in verify:contrast (48 pairs). */
240
+ :scope[variant="tonal"] {
241
+ --button-bg: var(--md-sys-color-primary-container);
242
+ --button-fg: var(--md-sys-color-primary-on-primary-container);
243
+ --button-border: transparent;
244
+ }
245
+
223
246
  :scope[variant="ghost"] {
224
247
  --button-bg: var(--button-bg-ghost);
225
248
  --button-fg: var(--button-fg-ghost);
@@ -279,6 +302,13 @@ button-ui[variant="ghost"][color="warning"]:not([disabled]):hover { --button-f
279
302
  :scope[variant="ghost"][color="success"] { --button-bg: transparent; --button-fg: var(--md-sys-color-success-on-surface); }
280
303
  :scope[variant="ghost"][color="warning"] { --button-bg: transparent; --button-fg: var(--md-sys-color-warning-on-surface); }
281
304
 
305
+ /* Tonal × color — same 0,3,0 override pattern as outline/ghost above:
306
+ the intent family's container fill + its own on-container text. */
307
+ :scope[variant="tonal"][color="danger"] { --button-bg: var(--md-sys-color-danger-container); --button-fg: var(--md-sys-color-danger-on-danger-container); }
308
+ :scope[variant="tonal"][color="info"] { --button-bg: var(--md-sys-color-info-container); --button-fg: var(--md-sys-color-info-on-info-container); }
309
+ :scope[variant="tonal"][color="success"] { --button-bg: var(--md-sys-color-success-container); --button-fg: var(--md-sys-color-success-on-success-container); }
310
+ :scope[variant="tonal"][color="warning"] { --button-bg: var(--md-sys-color-warning-container); --button-fg: var(--md-sys-color-warning-on-warning-container); }
311
+
282
312
  /* Hover rules moved outside @scope — see Safari 17.x bug note at
283
313
  top of file. */
284
314
 
@@ -31,7 +31,7 @@ export class UIButton extends UIElement {
31
31
  text: string;
32
32
  /** Button label. Renderer routes this to the `text` attribute, which is rendered via CSS attr(text) on ::after and mirrored to aria-label. */
33
33
  textContent: string;
34
- /** Visual style — `solid` (default fill), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color="…"] alongside.
34
+ /** Visual style — `solid` (default fill), `tonal` (soft container fill with in-family text — the emphasis tier between solid and outline; default rides the primary family), `outline`, `ghost`. `default` / `primary` are aliases of `solid`. Style is independent of semantic intent — to express destructive / success / info / warning intent, set [color="…"] alongside (`variant="tonal" color="danger"` = the danger family's container fill). There is deliberately no `variant="secondary"`: `secondary` names a color family in this system, and an unset variant already IS the secondary-rank look (compose-constitution); `tonal` is the named tier for a softer-than-solid filled action.
35
35
  For **inline navigation** (Terms of Service, Privacy Policy, footer links, "Sign in" / "Sign up" cross-page affordances) use `<link-ui>` instead — it carries proper `<a href>` semantics, keyboard handling (Enter only, no Space), middle-click open-new-tab, and screen-reader announces "link" instead of "button". Mixing navigation and action affordances under the same primitive is a category error fixed at this junction. */
36
36
  variant: 'default' | 'solid' | 'outline' | 'ghost' | 'primary';
37
37
 
@@ -59,10 +59,17 @@ props:
59
59
  dynamic: true
60
60
  variant:
61
61
  description: >-
62
- Visual style — `solid` (default fill), `outline`, `ghost`.
62
+ Visual style — `solid` (default fill), `tonal` (soft container
63
+ fill with in-family text — the emphasis tier between solid and
64
+ outline; default rides the primary family), `outline`, `ghost`.
63
65
  `default` / `primary` are aliases of `solid`. Style is independent
64
66
  of semantic intent — to express destructive / success / info /
65
- warning intent, set [color="…"] alongside.
67
+ warning intent, set [color="…"] alongside (`variant="tonal"
68
+ color="danger"` = the danger family's container fill). There is
69
+ deliberately no `variant="secondary"`: `secondary` names a color
70
+ family in this system, and an unset variant already IS the
71
+ secondary-rank look (compose-constitution); `tonal` is the named
72
+ tier for a softer-than-solid filled action.
66
73
 
67
74
  For **inline navigation** (Terms of Service, Privacy Policy,
68
75
  footer links, "Sign in" / "Sign up" cross-page affordances) use
@@ -143,8 +143,8 @@
143
143
  "wrong": "{\"component\": \"Combobox\", \"options\": [{\"value\": \"a\", \"label\": \"A\"}, {\"value\": \"b\", \"label\": \"B\"}]}\n"
144
144
  },
145
145
  {
146
- "fix": "{\"component\": \"MultiSelect\", \"options\": [...]}\n",
147
- "why": "Combobox is single-select. Multi-select goes through MultiSelect.\n",
146
+ "fix": "{\"component\": \"Select\", \"multiple\": true, \"options\": [...]}\n",
147
+ "why": "Combobox is single-select. Multi-select goes through Select with\nmultiple:true — \"MultiSelect\" is not a registered component.\n",
148
148
  "wrong": "{\"component\": \"Combobox\", \"multiple\": true, \"options\": [...]}\n"
149
149
  }
150
150
  ],
@@ -277,8 +277,9 @@ a2ui:
277
277
  footer affordance + `create` event. Consumer wires the create
278
278
  event to a backend flow.
279
279
  - >-
280
- Multi-select goes through <multi-select-ui> (SPEC-040), not
281
- combobox. Combobox is single-select.
280
+ Multi-select goes through <select-ui multiple> (chips in the
281
+ trigger, checkbox-style option rows), not combobox. Combobox is
282
+ single-select; there is no <multi-select-ui> tag.
282
283
  anti_patterns:
283
284
  - wrong: |
284
285
  {"component": "Combobox", "value": "xx", "options": [{"value": "us", "label": "United States"}]}
@@ -296,9 +297,10 @@ anti_patterns:
296
297
  - wrong: |
297
298
  {"component": "Combobox", "multiple": true, "options": [...]}
298
299
  why: |
299
- Combobox is single-select. Multi-select goes through MultiSelect.
300
+ Combobox is single-select. Multi-select goes through Select with
301
+ multiple:true — "MultiSelect" is not a registered component.
300
302
  fix: |
301
- {"component": "MultiSelect", "options": [...]}
303
+ {"component": "Select", "multiple": true, "options": [...]}
302
304
  examples:
303
305
  - name: country-picker
304
306
  description: Country picker — canonical declarative <option> children inside a <field-ui>.
@@ -46,11 +46,12 @@ nav-item-ui[selected] [slot="icon"]:empty::before {
46
46
  --nav-item-icon-fg: var(--a-ui-text-muted);
47
47
  --nav-item-icon-fg-selected: var(--nav-item-accent);
48
48
  /* Two real container roles, one mechanism — no opacity synth.
49
- hover = --a-bg-muted (--md-sys-color-neutral-container-low),
49
+ hover = --a-bg-muted (--md-sys-color-neutral-container-low, 10%),
50
50
  matching nav-group-ui's own --nav-group-bg-hover exactly.
51
- selected = --a-bg-selected (--md-sys-color-neutral-container-active),
52
- a visibly deeper fill so selected still reads as selected
53
- under the cursor.
51
+ selected = --a-bg-selected (--md-sys-color-neutral-container, 20% —
52
+ the same tier as variant="tonal"'s rest fill, operator
53
+ ruling 2026-07-27), one ladder step deeper than hover so
54
+ selected still reads as selected under the cursor.
54
55
  Hover was --a-ui-bg-hover (--a-canvas-0-scrim, an opacity synth the
55
56
  token laws forbid); that is what v0.8.14 removed. Collapsing BOTH to
56
57
  container-low made hover and selected pixel-identical wherever no
@@ -783,10 +783,20 @@ export class UISelect extends UIFormElement {
783
783
  if (lead) label.innerHTML = `${lead}${escapeHTML(opt.label)}`;
784
784
  else label.textContent = opt.label;
785
785
  el.appendChild(label);
786
- } else if (lead) {
787
- el.innerHTML = `${lead}${escapeHTML(opt.label)}`;
788
786
  } else {
789
- el.textContent = opt.label;
787
+ // Single-select mirrors the multi structure with a RADIO indicator
788
+ // (operator ruling 2026-07-27) — selection is carried by the
789
+ // indicator, not a row tint; CSS keys off [data-single-option].
790
+ el.setAttribute('data-single-option', '');
791
+ const dot = document.createElement('span');
792
+ dot.setAttribute('data-radio', '');
793
+ dot.setAttribute('aria-hidden', 'true');
794
+ el.appendChild(dot);
795
+ const label = document.createElement('span');
796
+ label.setAttribute('data-option-label', '');
797
+ if (lead) label.innerHTML = `${lead}${escapeHTML(opt.label)}`;
798
+ else label.textContent = opt.label;
799
+ el.appendChild(label);
790
800
  }
791
801
  if (isSelected) el.setAttribute('aria-selected', 'true');
792
802
  if (opt.disabled) el.setAttribute('aria-disabled', 'true');
@@ -14,7 +14,12 @@
14
14
 
15
15
  /* ── Colors ── */
16
16
  --select-bg: var(--a-ui-bg);
17
- --select-bg-selected: var(--a-ui-bg-selected);
17
+ /* Selected option rows are indicator-carried (operator ruling
18
+ 2026-07-27): the radio (single) / checkbox (multi) shows selection,
19
+ the row itself stays untinted — so these DEFAULT to the plain row
20
+ colors. Both tokens remain consumer-overridable API; set
21
+ --select-bg-selected to re-enable a tinted selected row. */
22
+ --select-bg-selected: transparent;
18
23
  --select-bg-hover: var(--a-ui-bg-hover);
19
24
  --select-border: var(--a-ui-border);
20
25
  --select-border-hover: var(--a-ui-border-hover);
@@ -25,7 +30,7 @@
25
30
  --select-caret-fg: var(--a-ui-text-muted);
26
31
  --select-fg: var(--a-ui-text);
27
32
  --select-fg-hover: var(--a-ui-text-hover);
28
- --select-fg-selected: var(--a-ui-text-selected);
33
+ --select-fg-selected: var(--select-fg); /* indicator-carried — see --select-bg-selected note */
29
34
  --select-fg-subtle: var(--a-ui-text-subtle);
30
35
  --select-fg-muted: var(--a-ui-text-muted);
31
36
  --select-fg-disabled: var(--a-ui-text-disabled);
@@ -323,6 +328,13 @@ select-ui [role="option"][aria-selected="true"] {
323
328
  background: var(--select-bg-selected);
324
329
  font-weight: var(--a-ui-weight);
325
330
  }
331
+ /* Hover/focus feedback must survive on the selected row: the 2-attribute
332
+ selected rule above outweighs the plain :hover rule, so restate it (the
333
+ multi block below does the same for [data-multi-option]). */
334
+ select-ui [role="option"][aria-selected="true"]:hover,
335
+ select-ui [role="option"][aria-selected="true"][data-focused] {
336
+ background: var(--select-option-bg-hover);
337
+ }
326
338
  select-ui [role="option"][aria-disabled="true"] {
327
339
  color: var(--select-option-fg-disabled);
328
340
  cursor: not-allowed;
@@ -435,11 +447,50 @@ select-ui [slot="listbox"] [data-select-all-btn] {
435
447
 
436
448
  /* Multi-select option rows — leading checkbox indicator + label.
437
449
  The check icon is hidden by default and revealed at aria-selected. */
438
- select-ui [role="option"][data-multi-option] {
450
+ select-ui [role="option"][data-multi-option],
451
+ select-ui [role="option"][data-single-option] {
439
452
  display: flex;
440
453
  align-items: center;
441
454
  gap: var(--a-space-2);
442
455
  }
456
+
457
+ /* Single-select option rows — leading RADIO indicator (operator ruling
458
+ 2026-07-27, sibling of SPEC-040's multi checkbox): selection is carried
459
+ by the control indicator, not a row tint. Rest = bordered circle on the
460
+ overlay surface; selected = primary ring with a primary center dot
461
+ (the dot is a ::after circle, scaled in — same transition length as
462
+ the checkbox's fill). */
463
+ select-ui [role="option"][data-single-option] [data-radio] {
464
+ flex-shrink: 0;
465
+ display: inline-flex;
466
+ align-items: center;
467
+ justify-content: center;
468
+ width: 1rem;
469
+ height: 1rem;
470
+ border: 1px solid var(--a-ui-border);
471
+ border-radius: 50%;
472
+ background: var(--md-sys-color-neutral-surface-bright);
473
+ transition: border-color var(--a-duration-fast) var(--a-easing);
474
+ }
475
+ select-ui [role="option"][data-single-option] [data-radio]::after {
476
+ content: '';
477
+ width: 0.5rem;
478
+ height: 0.5rem;
479
+ border-radius: 50%;
480
+ background: var(--md-sys-color-primary);
481
+ transform: scale(0);
482
+ transition: transform var(--a-duration-fast) var(--a-easing);
483
+ }
484
+ select-ui [role="option"][data-single-option][aria-selected="true"] [data-radio] {
485
+ border-color: var(--md-sys-color-primary);
486
+ }
487
+ select-ui [role="option"][data-single-option][aria-selected="true"] [data-radio]::after {
488
+ transform: scale(1);
489
+ }
490
+ select-ui [role="option"][data-single-option] [data-option-label] {
491
+ flex: 1;
492
+ min-width: 0;
493
+ }
443
494
  select-ui [role="option"][data-multi-option] [data-checkbox] {
444
495
  flex-shrink: 0;
445
496
  display: inline-flex;