@adia-ai/web-components 0.8.32 → 0.8.34

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 (100) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/MIGRATION.md +136 -0
  3. package/components/accordion/accordion-item.a2ui.json +8 -0
  4. package/components/accordion/accordion-item.yaml +6 -0
  5. package/components/accordion/accordion.css +3 -1
  6. package/components/agent-reasoning/agent-reasoning.a2ui.json +1 -1
  7. package/components/agent-reasoning/agent-reasoning.yaml +3 -3
  8. package/components/alert/alert.a2ui.json +3 -5
  9. package/components/alert/alert.class.js +3 -3
  10. package/components/alert/alert.css +5 -12
  11. package/components/alert/alert.d.ts +2 -2
  12. package/components/alert/alert.yaml +6 -5
  13. package/components/badge/badge.a2ui.json +6 -9
  14. package/components/badge/badge.class.js +4 -4
  15. package/components/badge/badge.css +111 -77
  16. package/components/badge/badge.d.ts +34 -12
  17. package/components/badge/badge.examples.md +1 -1
  18. package/components/badge/badge.yaml +37 -16
  19. package/components/button/button.a2ui.json +1 -1
  20. package/components/button/button.css +16 -2
  21. package/components/button/button.d.ts +2 -2
  22. package/components/button/button.yaml +1 -1
  23. package/components/card/card.yaml +1 -1
  24. package/components/chart/chart.class.js +8 -4
  25. package/components/combobox/combobox.a2ui.json +1 -1
  26. package/components/combobox/combobox.yaml +3 -0
  27. package/components/command/command.a2ui.json +4 -0
  28. package/components/command/command.class.js +54 -4
  29. package/components/command/command.d.ts +2 -0
  30. package/components/command/command.yaml +25 -3
  31. package/components/date-range-picker/date-range-picker.a2ui.json +1 -1
  32. package/components/date-range-picker/date-range-picker.class.js +42 -43
  33. package/components/date-range-picker/date-range-picker.css +11 -0
  34. package/components/date-range-picker/date-range-picker.yaml +6 -3
  35. package/components/description-list/description-list.css +16 -4
  36. package/components/integration-card/integration-card.class.js +11 -2
  37. package/components/mark/mark.a2ui.json +6 -7
  38. package/components/mark/mark.class.js +1 -1
  39. package/components/mark/mark.css +7 -4
  40. package/components/mark/mark.d.ts +2 -2
  41. package/components/mark/mark.examples.md +0 -4
  42. package/components/mark/mark.yaml +16 -6
  43. package/components/modal/modal.class.js +43 -2
  44. package/components/modal/modal.css +23 -0
  45. package/components/nav-group/nav-group.css +15 -4
  46. package/components/nav-item/nav-item.css +11 -3
  47. package/components/option-card/option-card.examples.md +1 -1
  48. package/components/popover/popover.a2ui.json +5 -0
  49. package/components/popover/popover.class.js +9 -4
  50. package/components/popover/popover.css +5 -2
  51. package/components/popover/popover.yaml +8 -0
  52. package/components/preview/preview.examples.md +1 -1
  53. package/components/richtext/richtext.css +5 -1
  54. package/components/segment/segment.css +10 -0
  55. package/components/step-progress/step-progress.css +16 -3
  56. package/components/table/cell-types.js +20 -1
  57. package/components/table/table.a2ui.json +1 -1
  58. package/components/table/table.class.js +11 -0
  59. package/components/table/table.css +9 -0
  60. package/components/table/table.d.ts +1 -1
  61. package/components/table/table.yaml +1 -1
  62. package/components/table-toolbar/table-toolbar.class.js +1 -1
  63. package/components/tabs/tabs.css +3 -1
  64. package/components/tag/tag.a2ui.json +5 -5
  65. package/components/tag/tag.class.js +1 -1
  66. package/components/tag/tag.css +93 -77
  67. package/components/tag/tag.d.ts +17 -11
  68. package/components/tag/tag.yaml +30 -19
  69. package/components/toast/toast.a2ui.json +2 -4
  70. package/components/toast/toast.css +0 -12
  71. package/components/toast/toast.yaml +3 -4
  72. package/core/element.js +31 -0
  73. package/core/signals.js +3 -1
  74. package/dist/theme-provider.min.js +2 -2
  75. package/dist/web-components.min.css +1 -1
  76. package/dist/web-components.min.js +67 -67
  77. package/dist/web-components.sheet.js +1 -1
  78. package/package.json +1 -1
  79. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +1 -1
  80. package/patterns/agent-memory/agent-memory.examples.html +5 -5
  81. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +2 -2
  82. package/patterns/audit-log/audit-log.examples.js +12 -5
  83. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.js +4 -1
  84. package/patterns/changelog-feed/changelog-feed.examples.html +1 -1
  85. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +1 -1
  86. package/patterns/conversion-funnel/conversion-funnel.examples.html +1 -1
  87. package/patterns/diff-review/diff-review.examples.html +9 -9
  88. package/patterns/filter-bar/filter-bar.examples.html +12 -8
  89. package/patterns/filter-bar/filter-bar.examples.js +4 -1
  90. package/patterns/marketing-engagement/marketing-engagement.examples.html +1 -1
  91. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  92. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  93. package/patterns/permissions-sharing/permissions-sharing.examples.html +1 -1
  94. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +2 -2
  95. package/patterns/record-detail-drawer/record-detail-drawer.examples.html +1 -1
  96. package/patterns/responsive-shell-sidebar/responsive-shell-sidebar.examples.html +135 -147
  97. package/patterns/retention-cohort/retention-cohort.examples.html +1 -1
  98. package/patterns/retention-cohort/retention-cohort.examples.js +4 -1
  99. package/patterns/system-banners/system-banners.examples.html +3 -3
  100. package/styles/colors/semantics/core.css +22 -2
@@ -85,10 +85,15 @@ export class UICommand extends UIElement {
85
85
  render() {
86
86
  // Stamp internal structure once
87
87
  if (!this.#inputEl) {
88
- // Parse options + capture author-provided slot="empty" content
89
- // before replacing innerHTML (dogfood-status yaml-drift finding:
90
- // yaml documented an overridable empty state that was never wired).
91
- this.#parseOptions();
88
+ // Parse declarative <option>/<optgroup> children once, unless
89
+ // `.options` was already set programmatically before first render
90
+ // (mirrors combobox-ui's connected()-time guard) + capture
91
+ // author-provided slot="empty" content before replacing innerHTML
92
+ // (dogfood-status yaml-drift finding: yaml documented an overridable
93
+ // empty state that was never wired).
94
+ if (this.#items.length === 0) {
95
+ this.#parseOptions();
96
+ }
92
97
  this.#emptyNodes = logicalSlotted(this, 'empty');
93
98
 
94
99
  this.innerHTML = `
@@ -143,6 +148,51 @@ export class UICommand extends UIElement {
143
148
  this.#renderItems();
144
149
  }
145
150
 
151
+ // ── Programmatic options (gh#887) ──
152
+ //
153
+ // Public API mirroring combobox-ui's `options` prop: an array of flat
154
+ // {value, label, icon?, shortcut?, disabled?} entries, or grouped
155
+ // {label, options:[…]} entries — same field names as the data-icon/
156
+ // data-shortcut attributes on declarative <option> children. Normalized
157
+ // into the internal #items shape #renderItems() already consumes (flat
158
+ // items carry `category`; groups carry `.items` not `.options`), so
159
+ // programmatic and declarative authoring render identically.
160
+
161
+ set options(list) {
162
+ this.#items = this.#normalizeOptions(Array.isArray(list) ? list : []);
163
+ // Only re-render if the internal list has already been stamped —
164
+ // otherwise the pending #items feed the first render() pass directly.
165
+ if (this.#listEl) this.#renderItems(this.#inputEl?.value || '');
166
+ }
167
+
168
+ get options() { return this.#items; }
169
+
170
+ #normalizeOptions(list) {
171
+ const out = [];
172
+ for (const entry of list) {
173
+ if (entry && Array.isArray(entry.options)) {
174
+ const label = entry.label || '';
175
+ out.push({ label, items: entry.options.map((opt) => this.#optionObjectToItem(opt, label)) });
176
+ } else if (entry) {
177
+ out.push(this.#optionObjectToItem(entry, ''));
178
+ }
179
+ }
180
+ return out;
181
+ }
182
+
183
+ /** Plain-object counterpart of #optionToItem (which reads a DOM <option>). */
184
+ #optionObjectToItem(opt, category = '') {
185
+ return {
186
+ value: opt.value,
187
+ label: opt.label ?? opt.value,
188
+ icon: opt.icon || '',
189
+ shortcut: opt.shortcut || '',
190
+ keywords: opt.keywords || '',
191
+ disabled: !!opt.disabled,
192
+ category,
193
+ };
194
+ }
195
+
146
196
  // ── Parse declarative <option>/<optgroup> ──
147
197
 
148
198
  // Wrapper-piercing walk for consumer-authored <option>/<optgroup> children.
@@ -35,6 +35,8 @@ export type CommandSelectEvent = CustomEvent<CommandSelectEventDetail>;
35
35
  export class UICommand extends UIElement {
36
36
  /** Whether the command palette is visible */
37
37
  open: boolean;
38
+ /** Programmatic item list. Array of {value, label, icon?, shortcut?, disabled?} or grouped {label, options:[…]}. `icon` renders as a leading icon-ui, `shortcut` as a trailing keyboard-shortcut hint — the same fields as the data-icon/data-shortcut attributes on declarative <option> children. Alternative to declarative <option data-icon data-shortcut> / <optgroup label> children. */
39
+ options: string;
38
40
  /** Placeholder text for the search input */
39
41
  placeholder: string;
40
42
 
@@ -24,6 +24,24 @@ props:
24
24
  description: Placeholder text for the search input
25
25
  type: string
26
26
  default: Type a command...
27
+ options:
28
+ description: >-
29
+ Programmatic item list. Array of {value, label, icon?, shortcut?,
30
+ disabled?} or grouped {label, options:[…]}. `icon` renders as a
31
+ leading icon-ui, `shortcut` as a trailing keyboard-shortcut hint —
32
+ the same fields as the data-icon/data-shortcut attributes on
33
+ declarative <option> children. Alternative to declarative
34
+ <option data-icon data-shortcut> / <optgroup label> children.
35
+ type: array
36
+ # Rich option OBJECTS ({value, label, icon?, …} / grouped) — declares
37
+ # items.type so the a2ui catalog maps to DynamicObjectList, not the
38
+ # string-list default (components.mjs TKT-0010 mapping). Without this
39
+ # the dialect validator rejected every harvested rich-options palette
40
+ # (exit-gate.corpus caught it at release pre-flight).
41
+ items:
42
+ type: object
43
+ default: []
44
+ dynamic: true # JS-set collection prop with a custom setter — mirrors combobox-ui's `options`
27
45
  events:
28
46
  dismiss:
29
47
  description: Fired when Escape is pressed
@@ -69,9 +87,13 @@ a2ui:
69
87
  shortcut. Do NOT hand-roll a <dialog> + Cmd+K key listener.
70
88
  - >-
71
89
  Author items as native <option value data-icon data-shortcut>
72
- elements inside <optgroup label="…"> for grouped sections. The
73
- `select` event's detail.category mirrors the parent optgroup's
74
- label. Detail = { value, label, category }.
90
+ elements inside <optgroup label="…"> for grouped sections, OR set
91
+ `.options` programmatically as an array of
92
+ `{value, label, icon?, shortcut?, disabled?}` (grouped form:
93
+ `{label, options:[…]}`) — same fields, alternative to declarative
94
+ children. The `select` event's detail.category mirrors the parent
95
+ optgroup's label (or the group's `label` when authored via
96
+ `options`). Detail = { value, label, category }.
75
97
  - >-
76
98
  Decision rule vs adjacent surfaces. Use <menu-ui> for small
77
99
  NON-searchable popover menus (≤10 actions, triggered by a
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/DateRangePicker.json",
4
4
  "title": "DateRangePicker",
5
- "description": "Compound form primitive for selecting a start + end date pair with optional named preset shortcuts (\"Today\", \"Last 7 days\", \"This month\", ...). Default presentation is a trigger button showing the formatted range; on activation a popover opens with two synchronized calendar panes + a preset side rail. Composes <calendar-picker-ui> twice; emits {from, to} ISO 8601 dates. Optional comparison-range mode emits a secondary range for analytics surfaces.",
5
+ "description": "Compound form primitive for selecting a start + end date pair with optional named preset shortcuts (\"Today\", \"Last 7 days\", \"This month\", ...). Default presentation is a trigger button showing the formatted range; on activation a popover opens with two synchronized calendar panes + a preset side rail. Composes <calendar-grid-ui> twice; emits {from, to} ISO 8601 dates. Both panes are ONE selection surface: first click = start, second click = end, in either grid; a second click earlier than the start restarts the range there. Optional comparison-range mode emits a secondary range for analytics surfaces.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -15,9 +15,12 @@
15
15
  * <date-range-picker-ui>
16
16
  *
17
17
  * Compound form primitive for selecting a start + end date pair with
18
- * optional preset shortcuts. Composes <calendar-picker-ui> (two
19
- * instances start + end pane) + <popover-ui> + <button-ui> for the
20
- * trigger and preset rail.
18
+ * optional preset shortcuts. Composes <calendar-grid-ui> (two panes
19
+ * showing adjacent months) + <popover-ui> + <button-ui> for the
20
+ * trigger and preset rail. Selection is click-start-then-click-end
21
+ * across BOTH panes as one surface (gh#948) — the panes differ only
22
+ * in which month they display, never in which half of the range a
23
+ * click writes.
21
24
  *
22
25
  * Form participation:
23
26
  * ElementInternals serializes the selected range as JSON:
@@ -345,8 +348,8 @@ export class UIDateRangePicker extends UIFormElement {
345
348
  this.#triggerRef.addEventListener('keydown', this.#onTriggerKey, true);
346
349
  this.#popoverRef.addEventListener('click', this.#onPopoverClick);
347
350
  this.#popoverRef.addEventListener('keydown', this.#onPopoverKey);
348
- this.#calFromRef.addEventListener('change', this.#onCalFromChange);
349
- this.#calToRef.addEventListener('change', this.#onCalToChange);
351
+ this.#calFromRef.addEventListener('change', this.#onDaySelect);
352
+ this.#calToRef.addEventListener('change', this.#onDaySelect);
350
353
  }
351
354
  // Wire form value on first connect.
352
355
  this.syncValue();
@@ -364,8 +367,8 @@ export class UIDateRangePicker extends UIFormElement {
364
367
  this.#popoverRef.removeEventListener('keydown', this.#onPopoverKey);
365
368
  this.#popoverRef.hidePopover?.();
366
369
  }
367
- if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#onCalFromChange);
368
- if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#onCalToChange);
370
+ if (this.#calFromRef) this.#calFromRef.removeEventListener('change', this.#onDaySelect);
371
+ if (this.#calToRef) this.#calToRef.removeEventListener('change', this.#onDaySelect);
369
372
  document.removeEventListener('pointerdown', this.#onOutside);
370
373
  document.removeEventListener('keydown', this.#onDocKey);
371
374
  this.#anchorCleanup?.();
@@ -449,14 +452,16 @@ export class UIDateRangePicker extends UIFormElement {
449
452
  // Cascade min/max + selection state into the calendar panes.
450
453
  const range = parseRange(this.value);
451
454
  const pending = this.#pending;
452
- // Effective endpoints — pending (mid-click) takes precedence over
453
- // value. Push the SAME from/to onto BOTH grids so each independently
454
- // lights up its in-range cells via `[data-in-range]`. Without this,
455
- // only the two endpoint cells got `[data-selected]` and the days
456
- // between them rendered as plain background the "fill the cells
457
- // between start and end dates" trap.
458
- const effFrom = pending?.from || range?.from || '';
459
- const effTo = pending?.to || range?.to || '';
455
+ // Effective endpoints — an in-flight pending selection REPLACES the
456
+ // committed value wholesale (no per-half fallback: mixing the new
457
+ // start with the old committed end would light a range the user
458
+ // never picked, gh#948). Push the SAME from/to onto BOTH grids so
459
+ // each independently lights up its in-range cells via
460
+ // `[data-in-range]`. Without this, only the two endpoint cells got
461
+ // `[data-selected]` and the days between them rendered as plain
462
+ // background the "fill the cells between start and end dates" trap.
463
+ const effFrom = pending ? pending.from : (range?.from || '');
464
+ const effTo = pending ? pending.to : (range?.to || '');
460
465
  const setRangeAttrs = (cal) => {
461
466
  if (effFrom) cal.setAttribute('range-start', effFrom);
462
467
  else cal.removeAttribute('range-start');
@@ -466,13 +471,13 @@ export class UIDateRangePicker extends UIFormElement {
466
471
  if (this.#calFromRef) {
467
472
  if (this.min) this.#calFromRef.setAttribute('min', this.min);
468
473
  if (this.max) this.#calFromRef.setAttribute('max', this.max);
469
- this.#calFromRef.value = pending?.from || range?.from || '';
474
+ this.#calFromRef.value = effFrom;
470
475
  setRangeAttrs(this.#calFromRef);
471
476
  }
472
477
  if (this.#calToRef) {
473
478
  if (this.min) this.#calToRef.setAttribute('min', this.min);
474
479
  if (this.max) this.#calToRef.setAttribute('max', this.max);
475
- this.#calToRef.value = pending?.to || range?.to || '';
480
+ this.#calToRef.value = effTo;
476
481
  setRangeAttrs(this.#calToRef);
477
482
  }
478
483
 
@@ -680,37 +685,31 @@ export class UIDateRangePicker extends UIFormElement {
680
685
  }
681
686
  };
682
687
 
683
- #onCalFromChange = (e) => {
688
+ // Unified day selection across BOTH grids — first click = start,
689
+ // second click = end, whichever pane the click lands in (gh#948).
690
+ // The panes' [data-cal-from]/[data-cal-to] roles are display-only
691
+ // (left month / next month); they no longer lock which half of the
692
+ // range a click writes. A second click EARLIER than the pending start
693
+ // restarts the selection at that day (the industry-standard dual-pane
694
+ // model) instead of emitting a reversed-range `invalid`. A click
695
+ // while a range is already committed starts a fresh selection.
696
+ #onDaySelect = (e) => {
684
697
  if (this.readonly) return;
685
698
  e.stopPropagation();
686
- const iso = e.detail?.value || this.#calFromRef?.value || '';
699
+ const iso = e.detail?.value || '';
687
700
  if (!iso) return;
688
- const existing = parseRange(this.value);
689
- if (!this.#pending) this.#pending = { from: '', to: '' };
690
- this.#pending.from = iso;
691
- // Live emit `input` for pending state.
692
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: iso, to: null } } }));
693
- // Attempt commit when both halves are set (in either order). The
694
- // commit path handles reversed-range invalidation.
695
- const toCandidate = this.#pending.to || existing?.to || null;
696
- if (toCandidate) {
697
- this.#commitRange({ from: iso, to: toCandidate });
701
+ const startFresh =
702
+ !this.#pending?.from || // nothing pending (incl. committed-value state)
703
+ this.#pending.to || // previous selection fully picked
704
+ iso < this.#pending.from; // earlier than pending start → restart there
705
+ if (startFresh) {
706
+ this.#pending = { from: iso, to: '' };
707
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: iso, to: null } } }));
708
+ return;
698
709
  }
699
- };
700
-
701
- #onCalToChange = (e) => {
702
- if (this.readonly) return;
703
- e.stopPropagation();
704
- const iso = e.detail?.value || this.#calToRef?.value || '';
705
- if (!iso) return;
706
- const existing = parseRange(this.value);
707
- if (!this.#pending) this.#pending = { from: '', to: '' };
708
710
  this.#pending.to = iso;
709
- this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from || existing?.from || null, to: iso } } }));
710
- const fromCandidate = this.#pending.from || existing?.from || null;
711
- if (fromCandidate) {
712
- this.#commitRange({ from: fromCandidate, to: iso });
713
- }
711
+ this.dispatchEvent(new CustomEvent('input', { bubbles: true, detail: { value: { from: this.#pending.from, to: iso } } }));
712
+ this.#commitRange({ from: this.#pending.from, to: iso });
714
713
  };
715
714
 
716
715
  #onOutside = (e) => {
@@ -158,6 +158,17 @@ date-range-picker-ui [slot="popover"] {
158
158
  /* Opt into a wider anchor.js max-width cap so both month panes fit side-by-side
159
159
  (the 32rem default clipped the second month). Viewport safety still applies. */
160
160
  --popover-max-width: var(--date-range-picker-popover-max-width);
161
+ /* Content sizing lives in the STYLESHEET, not only in anchor.js inline
162
+ styles (gh#948): only the native-anchor path applies width/max-width
163
+ hints inline — the JS fallback (Safari/Firefox) and any placement
164
+ where anchoring fails otherwise leave the popover's size to whatever
165
+ the surrounding cascade produces, which is how the viewport-huge
166
+ mostly-empty panel renders. These caps make the popover content-sized
167
+ in every path; anchor.js inline styles still win where they apply. */
168
+ width: max-content;
169
+ max-width: min(calc(100vw - 1rem), var(--date-range-picker-popover-max-width));
170
+ max-height: calc(100vh - 2rem);
171
+ overflow: auto;
161
172
  padding: var(--date-range-picker-py) var(--date-range-picker-px);
162
173
  border: 1px solid var(--date-range-picker-popover-border);
163
174
  border-radius: var(--date-range-picker-popover-radius);
@@ -10,9 +10,12 @@ description: >-
10
10
  optional named preset shortcuts ("Today", "Last 7 days", "This month",
11
11
  ...). Default presentation is a trigger button showing the formatted
12
12
  range; on activation a popover opens with two synchronized calendar
13
- panes + a preset side rail. Composes <calendar-picker-ui> twice;
14
- emits {from, to} ISO 8601 dates. Optional comparison-range mode
15
- emits a secondary range for analytics surfaces.
13
+ panes + a preset side rail. Composes <calendar-grid-ui> twice;
14
+ emits {from, to} ISO 8601 dates. Both panes are ONE selection
15
+ surface: first click = start, second click = end, in either grid;
16
+ a second click earlier than the start restarts the range there.
17
+ Optional comparison-range mode emits a secondary range for
18
+ analytics surfaces.
16
19
  # Per ADR-0027 — primitives that programmatically create other primitives
17
20
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
18
21
  composes:
@@ -6,10 +6,21 @@
6
6
  --description-list-term-min-width: 8rem;
7
7
 
8
8
  /* ── Typography ── */
9
- --description-list-term-font-size: var(--a-ui-sm);
10
- --description-list-term-weight: var(--a-weight-medium);
11
- --description-list-desc-font-size: var(--a-ui-sm);
12
- --description-list-desc-weight: var(--a-weight-normal);
9
+ /* Term reads as a kicker/data-label (operator ruling 2026-08-11,
10
+ matching the docs site's [data-label] convention): code family at
11
+ label size, normal weight — a quiet technical eyebrow the medium-
12
+ weight desc answers. */
13
+ --description-list-term-font-family: var(--a-font-family-code);
14
+ --description-list-term-font-size: var(--a-label-size);
15
+ --description-list-term-weight: var(--a-weight-normal);
16
+ /* 1em, not a fixed step — the desc inherits the surrounding body/
17
+ context size (operator ruling 2026-08-11), so a dl inside prose
18
+ matches the prose and inside a compact card matches the card. */
19
+ --description-list-desc-font-size: 1em;
20
+ /* Semibold — the desc (value) carries the pair's content and reads
21
+ clearly bolder than the muted normal-weight kicker term (operator
22
+ rulings 2026-08-11: first normal → medium, then medium → semibold). */
23
+ --description-list-desc-weight: var(--a-weight-semibold);
13
24
 
14
25
  /* ── Colors ── */
15
26
  --description-list-term-fg: var(--a-fg-muted);
@@ -29,6 +40,7 @@
29
40
  [data-dl-term],
30
41
  dt {
31
42
  margin: 0;
43
+ font-family: var(--description-list-term-font-family);
32
44
  font-size: var(--description-list-term-font-size);
33
45
  font-weight: var(--description-list-term-weight);
34
46
  color: var(--description-list-term-fg);
@@ -48,11 +48,14 @@ const ACTION_FOR_STATUS = Object.freeze({
48
48
  pending: { event: 'press', emits: '', label: 'Connecting…', variant: 'outline', icon: '' },
49
49
  });
50
50
 
51
+ // `coming-soon` is a family-less badge, tone="muted" — `variant="muted"`
52
+ // retired (variant/color/tone convention cut, gh#873/874); `#renderStatusBadge()`
53
+ // below branches on `variant` vs. absent.
51
54
  const BADGE_FOR_STATUS = Object.freeze({
52
55
  connected: { variant: 'success', text: 'Connected', icon: 'check' },
53
56
  error: { variant: 'danger', text: 'Error', icon: 'warning-circle' },
54
57
  pending: { variant: 'info', text: 'Connecting…', icon: '' },
55
- 'coming-soon':{ variant: 'muted', text: 'Coming soon', icon: '' },
58
+ 'coming-soon':{ text: 'Coming soon', icon: '' },
56
59
  });
57
60
 
58
61
  // Fallback glyph per known provider when no explicit [logo] is supplied, so
@@ -312,7 +315,13 @@ export class UIIntegrationCard extends UIElement {
312
315
  return;
313
316
  }
314
317
  this.#statusEl.hidden = false;
315
- this.#statusEl.setAttribute('variant', cfg.variant);
318
+ if (cfg.variant) {
319
+ this.#statusEl.setAttribute('variant', cfg.variant);
320
+ this.#statusEl.removeAttribute('tone');
321
+ } else {
322
+ this.#statusEl.removeAttribute('variant');
323
+ this.#statusEl.setAttribute('tone', 'muted');
324
+ }
316
325
  this.#statusEl.setAttribute('text', cfg.text);
317
326
  this.#statusEl.setAttribute('size', 'sm');
318
327
  if (cfg.icon) this.#statusEl.setAttribute('icon', cfg.icon);
@@ -17,14 +17,13 @@
17
17
  "const": "Mark"
18
18
  },
19
19
  "variant": {
20
- "description": "Highlight color variant. Default `warning` is the conventional yellow-marker tone.",
20
+ "description": "Highlight color family — the only color axis mark exposes. Default `warning` is the conventional yellow-marker tone and IS mark's family-less identity, so there is no `default` member. For a neutral (non-semantic) highlight, override the public tokens instead of the variant: `--mark-bg: var(--a-bg-muted); --mark-fg: var(--md-sys-color-neutral-on-surface)` — the successor to the retired `variant=\"muted\"` (gh#881).",
21
21
  "type": "string",
22
22
  "enum": [
23
23
  "warning",
24
24
  "info",
25
25
  "success",
26
- "danger",
27
- "muted"
26
+ "danger"
28
27
  ],
29
28
  "default": "warning"
30
29
  }
@@ -87,12 +86,12 @@
87
86
  "tag": "mark-ui",
88
87
  "tokens": {
89
88
  "--mark-bg": {
90
- "description": "Background color of the highlight.",
91
- "default": "var(--a-warning-muted)"
89
+ "description": "Background color of the highlight. Override for a non-semantic (e.g. neutral) highlight — see [variant].",
90
+ "default": "var(--md-sys-color-warning-container)"
92
91
  },
93
92
  "--mark-fg": {
94
- "description": "Foreground (text) color inside the highlight.",
95
- "default": "var(--a-warning-text)"
93
+ "description": "Foreground (text) color inside the highlight. Pair every [--mark-bg] override with a matching fg so contrast holds in both schemes.",
94
+ "default": "var(--md-sys-color-warning-on-surface)"
96
95
  },
97
96
  "--mark-px": {
98
97
  "description": "Horizontal padding around the highlighted text.",
@@ -8,7 +8,7 @@ import { UIElement } from '../../core/element.js';
8
8
 
9
9
  export class UIMark extends UIElement {
10
10
  static properties = {
11
- variant: { type: String, default: 'warning', reflect: true }, // warning | info | success | danger | muted
11
+ variant: { type: String, default: 'warning', reflect: true }, // warning | info | success | danger
12
12
  };
13
13
 
14
14
  static template = () => null;
@@ -32,8 +32,11 @@
32
32
  --mark-bg: var(--md-sys-color-danger-container);
33
33
  --mark-fg: var(--md-sys-color-danger-on-surface);
34
34
  }
35
- :scope[variant="muted"] {
36
- --mark-bg: var(--a-bg-muted);
37
- --mark-fg: var(--md-sys-color-neutral-on-surface);
38
- }
35
+
36
+ /* NB `variant` is mark's ONLY color axis and it is family-only
37
+ (gh#881, the two-axis convention). The former `variant="muted"`
38
+ neutral highlight is retired; its successor is the public token
39
+ override — `--mark-bg: var(--a-bg-muted); --mark-fg:
40
+ var(--md-sys-color-neutral-on-surface)` — so a non-semantic
41
+ highlight no longer needs an enum member of its own. */
39
42
  }
@@ -22,6 +22,6 @@ background highlight behind the matched span.
22
22
  import { UIElement } from '../../core/element.js';
23
23
 
24
24
  export class UIMark extends UIElement {
25
- /** Highlight color variant. Default `warning` is the conventional yellow-marker tone. */
26
- variant: 'warning' | 'info' | 'success' | 'danger' | 'muted';
25
+ /** Highlight color family — the only color axis mark exposes. Default `warning` is the conventional yellow-marker tone and IS mark's family-less identity, so there is no `default` member. For a neutral (non-semantic) highlight, override the public tokens instead of the variant: `--mark-bg: var(--a-bg-muted); --mark-fg: var(--md-sys-color-neutral-on-surface)` — the successor to the retired `variant="muted"` (gh#881). */
26
+ variant: 'warning' | 'info' | 'success' | 'danger';
27
27
  }
@@ -42,10 +42,6 @@
42
42
  <mark-ui variant="danger">danger</mark-ui>
43
43
  — removed line, conflict.
44
44
  </text-ui>
45
- <text-ui>
46
- <mark-ui variant="muted">muted</mark-ui>
47
- — neutral emphasis.
48
- </text-ui>
49
45
  </col-ui>
50
46
  ```
51
47
 
@@ -17,7 +17,14 @@ description: |
17
17
  background highlight behind the matched span.
18
18
  props:
19
19
  variant:
20
- description: Highlight color variant. Default `warning` is the conventional yellow-marker tone.
20
+ description: >-
21
+ Highlight color family — the only color axis mark exposes. Default
22
+ `warning` is the conventional yellow-marker tone and IS mark's
23
+ family-less identity, so there is no `default` member. For a
24
+ neutral (non-semantic) highlight, override the public tokens
25
+ instead of the variant: `--mark-bg: var(--a-bg-muted);
26
+ --mark-fg: var(--md-sys-color-neutral-on-surface)` — the successor
27
+ to the retired `variant="muted"` (gh#881).
21
28
  type: string
22
29
  default: warning
23
30
  enum:
@@ -25,7 +32,6 @@ props:
25
32
  - info
26
33
  - success
27
34
  - danger
28
- - muted
29
35
  reflect: true
30
36
  events: {}
31
37
  slots:
@@ -37,11 +43,15 @@ states:
37
43
  traits: []
38
44
  tokens:
39
45
  --mark-bg:
40
- description: Background color of the highlight.
41
- default: var(--a-warning-muted)
46
+ description: >-
47
+ Background color of the highlight. Override for a non-semantic
48
+ (e.g. neutral) highlight — see [variant].
49
+ default: var(--md-sys-color-warning-container)
42
50
  --mark-fg:
43
- description: Foreground (text) color inside the highlight.
44
- default: var(--a-warning-text)
51
+ description: >-
52
+ Foreground (text) color inside the highlight. Pair every [--mark-bg]
53
+ override with a matching fg so contrast holds in both schemes.
54
+ default: var(--md-sys-color-warning-on-surface)
45
55
  --mark-px:
46
56
  description: Horizontal padding around the highlighted text.
47
57
  default: var(--a-space-0-5)
@@ -51,6 +51,12 @@ export class UIModal extends UIElement {
51
51
  #closeTimer = null;
52
52
  #dialogRef = null;
53
53
 
54
+ // Monotonic, instance-agnostic counter for the fallback aria-labelledby
55
+ // id stamped onto an author's [slot="heading"] (see render()) — multiple
56
+ // <modal-ui> instances on one page must not collide on the same id.
57
+ // (Extracted from PR #944 — gh#947.)
58
+ static #headingIdSeq = 0;
59
+
54
60
  // Phosphor icons this primitive auto-stamps (without consumer markup).
55
61
  // Aggregated by installIconLoadersForRegistered() across all defined
56
62
  // elements. Audited by check-required-icons.mjs.
@@ -150,8 +156,25 @@ export class UIModal extends UIElement {
150
156
  const panel = this.ensure('panel');
151
157
  if (panel.parentElement !== dialog) dialog.appendChild(panel);
152
158
 
153
- const header = this.ensure('header');
154
- if (header.parentElement !== panel) panel.prepend(header);
159
+ // Header — accept an authored bare <header> or [slot="header"] child
160
+ // (card-ui style), mirroring the footer contract below. Without this
161
+ // branch an authored <header> fell through to the loose-children
162
+ // migration and rendered inside the BODY, displacing icon/heading to
163
+ // the bottom of the modal (gh#941).
164
+ const userHeader = [...this.children].find(c =>
165
+ c.getAttribute('slot') === 'header' || (!c.getAttribute('slot') && c.localName === 'header')
166
+ );
167
+ let header;
168
+ if (userHeader) {
169
+ if (userHeader.getAttribute('slot') !== 'header') userHeader.setAttribute('slot', 'header');
170
+ if (userHeader.parentElement !== panel) panel.prepend(userHeader);
171
+ panel.querySelector(':scope > [slot="header"][data-stamped]')?.remove();
172
+ header = userHeader;
173
+ } else {
174
+ header = this.ensure('header');
175
+ header.setAttribute('data-stamped', '');
176
+ if (header.parentElement !== panel) panel.prepend(header);
177
+ }
155
178
  // Only set `text=` when actually present — the CSS rule
156
179
  // `:scope [slot="header"][text]:not([text=""])::before { content: attr(text); flex: 1 }`
157
180
  // generates a flex:1 pseudo-element. Setting `text=""` unconditionally
@@ -165,6 +188,24 @@ export class UIModal extends UIElement {
165
188
  header.removeAttribute('text');
166
189
  }
167
190
 
191
+ // Accessible name — [text] stamps aria-label (host + dialog), the
192
+ // long-standing contract above. Absent that, fall back to the author's
193
+ // [slot="heading"] via aria-labelledby so a heading-only modal (e.g.
194
+ // destructive-confirm-modal, gh#941) still satisfies the WAI-ARIA APG
195
+ // dialog-modal pattern's accessible-name requirement. Re-evaluated
196
+ // every render so a later `text=` set, or the heading being removed,
197
+ // clears the reference instead of leaving it stale. (gh#947, from
198
+ // PR #944.)
199
+ const authoredHeading = header.querySelector(':scope > [slot="heading"]');
200
+ if (!this.text && authoredHeading) {
201
+ if (!authoredHeading.id) authoredHeading.id = `modal-heading-${++UIModal.#headingIdSeq}`;
202
+ if (dialog.getAttribute('aria-labelledby') !== authoredHeading.id) {
203
+ dialog.setAttribute('aria-labelledby', authoredHeading.id);
204
+ }
205
+ } else {
206
+ dialog.removeAttribute('aria-labelledby');
207
+ }
208
+
168
209
  if (!this.permanent) {
169
210
  const close = this.ensure('close');
170
211
  if (close.parentElement !== header) header.appendChild(close);
@@ -131,6 +131,29 @@
131
131
  flex: 1;
132
132
  }
133
133
 
134
+ /* ═══════ Header icon + heading pair (drawer-ui/card-ui parity) ═══════
135
+ Author-supplied icon/heading content nested inside <header> — an
136
+ alternative to [text]. Positioned by tag + DOM order, not by reading
137
+ [slot="icon"]/[slot="heading"] (AGENTS.md: "slot= attributes are
138
+ decorative metadata; positioning is CSS by tag + ancestor + DOM
139
+ order") — `button-ui` identifies the stamped auto-close affordance by
140
+ TAG, and render() guarantees it's the header's trailing child when
141
+ present. The header's last non-button child claims the row's free
142
+ space (heading position); any earlier content child (a leading
143
+ icon/avatar) stays fixed-width. (gh#947, from PR #944.) */
144
+ :scope [slot="header"] > :not(button-ui):has(~ :not(button-ui)) {
145
+ display: flex;
146
+ align-items: center;
147
+ flex-shrink: 0;
148
+ }
149
+ :scope [slot="header"] > :not(button-ui):not(:has(~ :not(button-ui))) {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: var(--a-space-2);
153
+ flex: 1;
154
+ min-width: 0;
155
+ }
156
+
134
157
  /* ═══════ Close button (button-ui) ═══════ */
135
158
  :scope [slot="close"] {
136
159
  flex-shrink: 0;
@@ -10,13 +10,22 @@
10
10
  --nav-group-font-size-sm: var(--a-ui-sm);
11
11
  --nav-group-font-size-lg: var(--a-ui-lg);
12
12
  --nav-group-font-weight: var(--a-weight-normal);
13
- --nav-group-fg: var(--md-sys-color-neutral-on-surface);
13
+ /* State color spec (gh#968, operator ruling 2026-08-11) — one
14
+ language with nav-item-ui:
15
+ idle — bg transparent · icon NEUTRAL · text NEUTRAL-HIGH
16
+ hover — bg container-low · icon + text ON-SURFACE
17
+ selected — bg container · icon + text ON-SURFACE
18
+ The selected icon no longer takes primary (supersedes gh#522's
19
+ glyph ruling); Figma's neutral/surfaceBright was considered for the
20
+ selected fill and RULED OUT — container stays (gh#795's mirror). */
21
+ --nav-group-fg: var(--md-sys-color-neutral-high);
14
22
  --nav-group-fg-hover: var(--a-fg-strong);
15
23
  --nav-group-fg-muted: var(--a-fg-muted);
16
24
  --nav-group-fg-selected: var(--a-fg-strong);
17
- --nav-group-icon-fg-selected: var(--md-sys-color-primary); /* one selection color with --nav-item-accent (gh#522) */
25
+ --nav-group-icon-fg: var(--md-sys-color-neutral);
26
+ --nav-group-icon-fg-selected: var(--a-fg-strong);
18
27
  --nav-group-bg-hover: var(--a-bg-muted);
19
- --nav-group-bg-selected: var(--a-bg-selected); /* mirrors --nav-item-bg-selected — the missing container fill (gh#795) */
28
+ --nav-group-bg-selected: var(--a-bg-selected); /* mirrors --nav-item-bg-selected (gh#795) */
20
29
  --nav-group-icon-size: calc(var(--nav-group-row-height) - var(--a-space-2));
21
30
  --nav-group-badge-size: var(--a-ui-sm);
22
31
 
@@ -119,7 +128,9 @@
119
128
  width: var(--nav-group-icon-size);
120
129
  height: var(--nav-group-icon-size);
121
130
  flex-shrink: 0;
122
- color: var(--nav-group-fg-muted);
131
+ /* idle icon = NEUTRAL (gh#968 state spec), one step quieter than the
132
+ header's neutral-high text. */
133
+ color: var(--nav-group-icon-fg);
123
134
  transition: color var(--nav-duration-fast) var(--nav-easing);
124
135
  display: flex;
125
136
  align-items: center;