@adia-ai/web-components 0.6.50 → 0.7.1

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 (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/components/action-list/action-list.css +1 -1
  3. package/components/agent-artifact/agent-artifact.class.js +10 -10
  4. package/components/agent-artifact/agent-artifact.css +1 -1
  5. package/components/agent-reasoning/agent-reasoning.class.js +51 -0
  6. package/components/agent-reasoning/agent-reasoning.css +49 -22
  7. package/components/alert/alert.class.js +8 -1
  8. package/components/alert/alert.css +13 -1
  9. package/components/avatar/avatar.a2ui.json +2 -14
  10. package/components/avatar/avatar.class.js +3 -15
  11. package/components/avatar/avatar.d.ts +2 -4
  12. package/components/avatar/avatar.yaml +1 -18
  13. package/components/breadcrumb/breadcrumb.css +4 -1
  14. package/components/button/button.a2ui.json +3 -0
  15. package/components/button/button.css +14 -3
  16. package/components/button/button.yaml +5 -0
  17. package/components/calendar-grid/calendar-grid.css +1 -1
  18. package/components/calendar-picker/calendar-picker.css +5 -2
  19. package/components/chart/chart.a2ui.json +0 -18
  20. package/components/chart/chart.class.js +8 -50
  21. package/components/chart/chart.css +1 -15
  22. package/components/chart/chart.d.ts +0 -4
  23. package/components/chart/chart.yaml +0 -24
  24. package/components/color-input/color-input.css +4 -1
  25. package/components/combobox/combobox.class.js +11 -0
  26. package/components/combobox/combobox.css +8 -0
  27. package/components/date-range-picker/date-range-picker.class.js +5 -1
  28. package/components/date-range-picker/date-range-picker.css +12 -2
  29. package/components/datetime-picker/datetime-picker.class.js +3 -0
  30. package/components/datetime-picker/datetime-picker.css +16 -2
  31. package/components/empty-state/empty-state.css +11 -4
  32. package/components/field/field.css +17 -6
  33. package/components/grid/grid.a2ui.json +5 -0
  34. package/components/grid/grid.class.js +16 -6
  35. package/components/grid/grid.css +17 -3
  36. package/components/grid/grid.d.ts +2 -0
  37. package/components/grid/grid.yaml +9 -0
  38. package/components/heatmap/heatmap.class.js +9 -3
  39. package/components/heatmap/heatmap.css +19 -2
  40. package/components/image/image.css +4 -1
  41. package/components/input/input.class.js +38 -0
  42. package/components/input/input.css +9 -5
  43. package/components/input/input.test.js +57 -0
  44. package/components/integration-card/integration-card.class.js +31 -7
  45. package/components/integration-card/integration-card.test.js +12 -1
  46. package/components/kbd/kbd.a2ui.json +3 -2
  47. package/components/kbd/kbd.css +7 -4
  48. package/components/kbd/kbd.d.ts +2 -2
  49. package/components/kbd/kbd.yaml +2 -1
  50. package/components/list/list.class.js +8 -1
  51. package/components/menu/menu.class.js +12 -3
  52. package/components/menu/menu.css +4 -1
  53. package/components/menu/menu.test.js +130 -0
  54. package/components/modal/modal.class.js +10 -1
  55. package/components/modal/modal.css +9 -0
  56. package/components/option-card/option-card.a2ui.json +3 -0
  57. package/components/option-card/option-card.css +44 -19
  58. package/components/option-card/option-card.yaml +5 -0
  59. package/components/otp-input/otp-input.css +25 -10
  60. package/components/page/page.css +64 -11
  61. package/components/pagination/pagination.class.js +1 -1
  62. package/components/pagination/pagination.css +9 -1
  63. package/components/pipeline-status/pipeline-status.css +6 -0
  64. package/components/popover/popover.css +12 -1
  65. package/components/preview/preview.css +30 -3
  66. package/components/progress-row/progress-row.css +3 -1
  67. package/components/qr-code/qr-code.css +4 -1
  68. package/components/segmented/segmented.css +4 -1
  69. package/components/select/select.a2ui.json +1 -1
  70. package/components/select/select.class.js +63 -7
  71. package/components/select/select.css +18 -0
  72. package/components/select/select.yaml +9 -2
  73. package/components/stack/stack.a2ui.json +12 -1
  74. package/components/stack/stack.d.ts +2 -2
  75. package/components/stack/stack.yaml +13 -1
  76. package/components/stat/stat.a2ui.json +5 -0
  77. package/components/stat/stat.css +55 -0
  78. package/components/stat/stat.d.ts +2 -0
  79. package/components/stat/stat.js +4 -0
  80. package/components/stat/stat.yaml +9 -0
  81. package/components/swiper/swiper.class.js +14 -6
  82. package/components/switch/switch.css +13 -0
  83. package/components/table/table.a2ui.json +2 -2
  84. package/components/table/table.css +13 -1
  85. package/components/table/table.yaml +2 -2
  86. package/components/time-picker/time-picker.css +4 -1
  87. package/components/timeline/timeline.class.js +3 -3
  88. package/components/timeline/timeline.css +23 -5
  89. package/components/toggle-group/toggle-group.css +4 -1
  90. package/components/toggle-scheme/toggle-scheme.css +4 -1
  91. package/components/tree/tree.class.js +24 -4
  92. package/components/tree/tree.test.js +108 -0
  93. package/dist/web-components.min.css +1 -1
  94. package/dist/web-components.min.js +83 -83
  95. package/package.json +3 -3
  96. package/styles/api/layout.css +7 -0
  97. package/styles/api/text.css +9 -5
  98. package/styles/index.css +11 -2
  99. package/styles/prose.css +8 -0
  100. package/styles/resets.css +5 -5
  101. package/styles/themes.css +8 -1
  102. package/styles/tokens.css +3 -3
  103. package/styles/type/elements.css +73 -0
  104. package/styles/type/roles.css +14 -49
  105. package/styles/type/scale.css +0 -5
  106. package/styles/typography.css +3 -3
@@ -71,6 +71,7 @@ export class UISelect extends UIFormElement {
71
71
  static template = () => null;
72
72
 
73
73
  #options = [];
74
+ #ownTrigger = false; // true when WE stamped the trigger (vs a consumer-custom one)
74
75
  #listbox = null;
75
76
  #anchorCleanup = null;
76
77
  #query = '';
@@ -386,14 +387,18 @@ export class UISelect extends UIFormElement {
386
387
 
387
388
  // Stamp default trigger if none provided
388
389
  if (!this.querySelector('[slot="trigger"]')) {
390
+ this.#ownTrigger = true;
389
391
  // Detach listbox before innerHTML wipe so it isn't destroyed
390
392
  const lb = this.#listbox;
391
393
  if (lb?.parentNode === this) this.removeChild(lb);
392
394
 
395
+ // Initial leading reflects the host [avatar]/[icon]; #syncLeading() then
396
+ // reconciles it to the SELECTED option's icon/avatar on every render. The
397
+ // `data-select-leading` marker scopes that reconciliation to our element.
393
398
  const leading = this.avatar
394
- ? `<img slot="leading" src="${this.avatar}" alt="" />`
399
+ ? `<img slot="leading" data-select-leading src="${escapeHTML(this.avatar)}" alt="" />`
395
400
  : this.icon
396
- ? `<icon-ui slot="leading" name="${this.icon}"></icon-ui>`
401
+ ? `<icon-ui slot="leading" data-select-leading name="${escapeHTML(this.icon)}"></icon-ui>`
397
402
  : '';
398
403
  const displayMarkup = this.searchable
399
404
  ? `<input slot="display" type="text" role="combobox" aria-autocomplete="list" autocomplete="off" placeholder="${escapeHTML(this.placeholder || '')}" value="${escapeHTML(this.#displayText() === this.placeholder ? '' : this.#displayText())}" />`
@@ -470,6 +475,9 @@ export class UISelect extends UIFormElement {
470
475
  }
471
476
  }
472
477
 
478
+ // Reflect the selected option's icon/avatar in the trigger leading.
479
+ this.#syncLeading();
480
+
473
481
  // SPEC-040 — stamp / reconcile chips + "+N more" pill on every render.
474
482
  if (this.multiple) this.#stampChips();
475
483
  // Show clear-all only in multi-select mode when [clearable] + chips present.
@@ -548,12 +556,12 @@ export class UISelect extends UIFormElement {
548
556
  if (child.tagName === 'OPTGROUP') {
549
557
  const group = { label: child.label || child.getAttribute('label') || '', options: [] };
550
558
  for (const opt of child.querySelectorAll('option')) {
551
- group.options.push({ value: opt.value, label: opt.textContent.trim(), disabled: opt.disabled });
559
+ group.options.push({ value: opt.value, label: opt.textContent.trim(), disabled: opt.disabled, icon: opt.getAttribute('icon') || '', avatar: opt.getAttribute('avatar') || '' });
552
560
  if (opt.hasAttribute('selected')) preSelectedArr.push(opt.value);
553
561
  }
554
562
  this.#options.push(group);
555
563
  } else if (child.tagName === 'OPTION') {
556
- this.#options.push({ value: child.value, label: child.textContent.trim(), disabled: child.disabled });
564
+ this.#options.push({ value: child.value, label: child.textContent.trim(), disabled: child.disabled, icon: child.getAttribute('icon') || '', avatar: child.getAttribute('avatar') || '' });
557
565
  if (child.hasAttribute('selected')) preSelectedArr.push(child.value);
558
566
  } else if (
559
567
  // §225: skip [slot="display"] / [slot="listbox"] / [slot="action"] etc. — these are
@@ -614,6 +622,52 @@ export class UISelect extends UIFormElement {
614
622
 
615
623
  get options() { return this.#options; }
616
624
 
625
+ // Per-option leading markup: avatar (img) beats icon (icon-ui). Shared by
626
+ // the listbox rows AND the trigger (resolved against the selected option).
627
+ static #optionLeadHTML(opt) {
628
+ if (!opt) return '';
629
+ if (opt.avatar) return `<img data-option-avatar src="${escapeHTML(opt.avatar)}" alt="" />`;
630
+ if (opt.icon) return `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>`;
631
+ return '';
632
+ }
633
+
634
+ /**
635
+ * Reflect the SELECTED option's icon/avatar in the trigger's leading slot.
636
+ * Single-select only (multi-select shows chips). Falls back to the host
637
+ * [avatar]/[icon] when the selected option carries neither. Only manages the
638
+ * leading WE stamped (`[data-select-leading]`) — a consumer-custom trigger
639
+ * owns its own leading.
640
+ */
641
+ #syncLeading() {
642
+ if (this.multiple || !this.#ownTrigger) return;
643
+ const trigger = this.querySelector('[slot="trigger"]');
644
+ if (!trigger) return;
645
+ const flat = this.#options.flatMap((o) => o.options || [o]);
646
+ const sel = flat.find((o) => !o.header && !o.separator && o.value === this.value);
647
+ const avatar = (sel && sel.avatar) || this.avatar || '';
648
+ const icon = (sel && sel.icon) || this.icon || '';
649
+ const existing = trigger.querySelector(':scope > [data-select-leading]');
650
+ let html = '';
651
+ if (avatar) html = `<img slot="leading" data-select-leading src="${escapeHTML(avatar)}" alt="" />`;
652
+ else if (icon) html = `<icon-ui slot="leading" data-select-leading name="${escapeHTML(icon)}"></icon-ui>`;
653
+ if (!html) { existing?.remove(); return; }
654
+ const tmp = document.createElement('template');
655
+ tmp.innerHTML = html;
656
+ const next = tmp.content.firstElementChild;
657
+ if (!existing) {
658
+ trigger.insertBefore(next, trigger.firstChild);
659
+ } else if (existing.tagName === next.tagName) {
660
+ // Same element type — update the changing attribute in place.
661
+ if (next.tagName === 'IMG') {
662
+ if (existing.getAttribute('src') !== next.getAttribute('src')) existing.setAttribute('src', next.getAttribute('src'));
663
+ } else if (existing.getAttribute('name') !== next.getAttribute('name')) {
664
+ existing.setAttribute('name', next.getAttribute('name'));
665
+ }
666
+ } else {
667
+ existing.replaceWith(next); // icon ↔ avatar switch
668
+ }
669
+ }
670
+
617
671
  #renderOptions() {
618
672
  if (!this.#listbox) return;
619
673
  this.#listbox.innerHTML = '';
@@ -653,6 +707,8 @@ export class UISelect extends UIFormElement {
653
707
  // SPEC-040 — multi-select option rows render a leading checkbox
654
708
  // indicator (CSS-driven via [data-multi-option]); the `check` icon
655
709
  // shows when aria-selected="true".
710
+ // Per-option leading glyph — avatar (img) wins over icon (icon-ui).
711
+ const lead = UISelect.#optionLeadHTML(opt);
656
712
  if (this.multiple) {
657
713
  el.setAttribute('data-multi-option', '');
658
714
  const box = document.createElement('span');
@@ -661,11 +717,11 @@ export class UISelect extends UIFormElement {
661
717
  el.appendChild(box);
662
718
  const label = document.createElement('span');
663
719
  label.setAttribute('data-option-label', '');
664
- if (opt.icon) label.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
720
+ if (lead) label.innerHTML = `${lead}${escapeHTML(opt.label)}`;
665
721
  else label.textContent = opt.label;
666
722
  el.appendChild(label);
667
- } else if (opt.icon) {
668
- el.innerHTML = `<icon-ui name="${escapeHTML(opt.icon)}"></icon-ui>${escapeHTML(opt.label)}`;
723
+ } else if (lead) {
724
+ el.innerHTML = `${lead}${escapeHTML(opt.label)}`;
669
725
  } else {
670
726
  el.textContent = opt.label;
671
727
  }
@@ -191,6 +191,11 @@
191
191
  :scope[data-multi-chips] [slot="trigger"] {
192
192
  flex-wrap: wrap;
193
193
  align-items: center;
194
+ /* Pack chips left with a small gap — override the base trigger's
195
+ `space-between` (which is for the single-select content↔caret split),
196
+ otherwise the chips spread across the full trigger width. The display
197
+ slot flex-grows (below) to push the caret to the trailing edge. */
198
+ justify-content: flex-start;
194
199
  gap: var(--a-space-1);
195
200
  /* min-height tracks single-row height when empty; flex-wrap allows
196
201
  it to grow as chips overflow. */
@@ -290,6 +295,8 @@ select-ui [slot="listbox"]:popover-open {
290
295
  }
291
296
 
292
297
  select-ui [role="option"] {
298
+ display: flex;
299
+ align-items: center;
293
300
  padding: var(--a-space-1) var(--a-ui-px);
294
301
  border-radius: var(--a-radius-sm);
295
302
  white-space: nowrap;
@@ -325,6 +332,17 @@ select-ui [role="option"] icon-ui {
325
332
  vertical-align: -0.125em;
326
333
  }
327
334
 
335
+ /* Option with avatar — small inline image (matches the trigger leading
336
+ avatar radius), sized to the option line-height. */
337
+ select-ui [role="option"] img[data-option-avatar] {
338
+ width: var(--a-ui-size);
339
+ height: var(--a-ui-size);
340
+ border-radius: var(--select-leading-radius, var(--select-leading-radius-default));
341
+ object-fit: cover;
342
+ margin-inline-end: var(--a-space-1);
343
+ vertical-align: -0.2em;
344
+ }
345
+
328
346
  /* Separator */
329
347
  select-ui [data-separator] {
330
348
  height: 1px;
@@ -23,7 +23,7 @@ description: |
23
23
  # Per ADR-0027 — primitives that programmatically create other primitives
24
24
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
25
25
  composes:
26
- - icon-ui # chevron + option-row affixes (created in render)
26
+ - icon-ui # caret + option-row affixes (created in render)
27
27
  - tag-ui # multi-select chip per selected option in the trigger
28
28
  props:
29
29
  name:
@@ -151,7 +151,7 @@ props:
151
151
  default: false
152
152
  reflect: true
153
153
  options:
154
- description: "Option list. Array of {value, label, disabled?} or grouped {label, options: [...]}. Alternative to declarative <option> / <optgroup> children."
154
+ description: "Option list. Array of {value, label, disabled?, icon?, avatar?} or grouped {label, options: [...]}. Alternative to declarative <option> / <optgroup> children. Per-option icon/avatar render in the list AND reflect in the trigger's selected state."
155
155
  type: array
156
156
  default: []
157
157
  pattern:
@@ -249,6 +249,13 @@ a2ui:
249
249
  tag names are silently ignored (per §225 v0.5.9) and warned once
250
250
  at runtime. Or set `.options` programmatically as an array of
251
251
  `{value, label, disabled?}` (grouped form: `{label, options:[…]}`).
252
+ - >-
253
+ Per-option visuals: give each <option> an `icon` (Phosphor name) or
254
+ `avatar` (image URL) — `<option value="light" icon="sun">`. Each row
255
+ renders its glyph in the list AND the trigger reflects the SELECTED
256
+ option's icon/avatar (theme pickers, assignee/account switchers).
257
+ `avatar` wins over `icon`; a host-level [icon]/[avatar] is the
258
+ fallback when the selected option carries neither.
252
259
  - >-
253
260
  For dynamic option lists rendered inside <editor-shell>, set the
254
261
  JSON via the [data-options] attribute — <editor-shell>'s
@@ -14,8 +14,19 @@
14
14
  ],
15
15
  "properties": {
16
16
  "align": {
17
- "description": "Alignment of stacked items",
17
+ "description": "Alignment of the layered children within the shared cell (maps to grid place-items). `center` (default) plus eight directional keywords.",
18
18
  "type": "string",
19
+ "enum": [
20
+ "center",
21
+ "top-left",
22
+ "top",
23
+ "top-right",
24
+ "left",
25
+ "right",
26
+ "bottom-left",
27
+ "bottom",
28
+ "bottom-right"
29
+ ],
19
30
  "default": "center"
20
31
  },
21
32
  "component": {
@@ -21,6 +21,6 @@ cell.
21
21
  import { UIElement } from '../../core/element.js';
22
22
 
23
23
  export class UIStack extends UIElement {
24
- /** Alignment of stacked items */
25
- align: string;
24
+ /** Alignment of the layered children within the shared cell (maps to grid place-items). `center` (default) plus eight directional keywords. */
25
+ align: 'center' | 'top-left' | 'top' | 'top-right' | 'left' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right';
26
26
  }
@@ -18,9 +18,21 @@ description: |
18
18
  cell.
19
19
  props:
20
20
  align:
21
- description: Alignment of stacked items
21
+ description: >-
22
+ Alignment of the layered children within the shared cell (maps to
23
+ grid place-items). `center` (default) plus eight directional keywords.
22
24
  type: string
23
25
  default: center
26
+ enum:
27
+ - center
28
+ - top-left
29
+ - top
30
+ - top-right
31
+ - left
32
+ - right
33
+ - bottom-left
34
+ - bottom
35
+ - bottom-right
24
36
  events: {}
25
37
  slots:
26
38
  default:
@@ -13,6 +13,11 @@
13
13
  }
14
14
  ],
15
15
  "properties": {
16
+ "bleed": {
17
+ "description": "Horizontal-bleed KPI layout. With a `slot=\"chart\"` child, the value / label / change stack on the left while the chart fills the right column at full height and bleeds to the card's top / right / bottom edges (the horizontal counterpart to a chart in a card `<section bleed>`). No effect without a chart slot.",
18
+ "type": "boolean",
19
+ "default": false
20
+ },
16
21
  "change": {
17
22
  "description": "Change indicator text (e.g. '+12%', '-3%')",
18
23
  "type": "string",
@@ -72,6 +72,61 @@
72
72
  grid-column: 1;
73
73
  }
74
74
 
75
+ /* ── Horizontal-bleed KPI tile ──
76
+ value / label / change stack on the LEFT; the chart fills the RIGHT
77
+ column at full height and bleeds to the card's top / right / bottom
78
+ edges (cancels the card-section inset via negative margins keyed to
79
+ the inherited --card-inset). The compact `slot="chart"` reflow above
80
+ keeps the chart resting on the value baseline; this opt-in spreads it
81
+ into a full-height trajectory panel. Compose inside a card section:
82
+ <card-ui><section>
83
+ <stat-ui bleed value=… label=… change=… trend=…>
84
+ <chart-ui slot="chart" type="area" …></chart-ui>
85
+ </stat-ui>
86
+ </section></card-ui> */
87
+ :scope[bleed]:has([slot="chart"]) {
88
+ grid-template-columns: minmax(0, 1fr) minmax(0, var(--stat-bleed-col, 46%));
89
+ grid-template-areas:
90
+ "label chart"
91
+ "value chart"
92
+ "change chart";
93
+ align-items: start;
94
+ }
95
+ :scope[bleed] [slot="value"],
96
+ :scope[bleed] [slot="change"] {
97
+ grid-column: 1;
98
+ }
99
+ /* Icon shares the label row, pinned to the text column's inner edge. */
100
+ :scope[bleed] [slot="label"] {
101
+ padding-inline-end: var(--a-space-5);
102
+ }
103
+ :scope[bleed] [slot="icon"] {
104
+ grid-area: label;
105
+ justify-self: end;
106
+ align-self: start;
107
+ }
108
+ :scope[bleed] [slot="chart"] {
109
+ grid-area: chart;
110
+ /* VERTICAL bleed via stretch: a stretched grid item's height is
111
+ `track − margins`, so a negative margin-block overflows the track by
112
+ that amount top + bottom. `height:auto` overrides the base
113
+ chart-ui[slot="chart"]{height:100%} so the stretch governs. */
114
+ align-self: stretch;
115
+ height: auto;
116
+ margin-block: calc(-1 * var(--card-inset, var(--card-inset-default, 0px)));
117
+ /* HORIZONTAL bleed via explicit width: chart-ui pins its own
118
+ `width:100%` (chart.css), which beats justify-self:stretch and would
119
+ freeze the box to the track — so a negative margin-inline-end can't
120
+ widen it. Instead size the box to `track + inset` and left-anchor it,
121
+ so the right edge overflows to the card edge while the left keeps the
122
+ column gap. The percentage resolves against the grid track. */
123
+ justify-self: start;
124
+ width: calc(100% + var(--card-inset, var(--card-inset-default, 0px)));
125
+ min-width: 0;
126
+ /* override the inline-chart 4:3 — fill the row span instead */
127
+ aspect-ratio: auto;
128
+ }
129
+
75
130
  /* ── Label (eyebrow) ── */
76
131
  [slot="label"] {
77
132
  grid-area: label;
@@ -13,6 +13,8 @@
13
13
  import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export class UIStat extends UIElement {
16
+ /** Horizontal-bleed KPI layout. With a `slot="chart"` child, the value / label / change stack on the left while the chart fills the right column at full height and bleeds to the card's top / right / bottom edges (the horizontal counterpart to a chart in a card `<section bleed>`). No effect without a chart slot. */
17
+ bleed: boolean;
16
18
  /** Change indicator text (e.g. '+12%', '-3%') */
17
19
  change: string;
18
20
  /** Icon name displayed in the icon slot */
@@ -18,6 +18,10 @@ class UIStat extends UIElement {
18
18
  trend: { type: String, default: '', reflect: true },
19
19
  icon: { type: String, default: '', reflect: true },
20
20
  loading: { type: Boolean, default: false, reflect: true },
21
+ // Horizontal-bleed KPI layout: value/label/change stack on the left, the
22
+ // slot="chart" child fills the right column and bleeds to the card edges.
23
+ // Reflected so the [bleed] CSS selector gates the layout.
24
+ bleed: { type: Boolean, default: false, reflect: true },
21
25
  };
22
26
 
23
27
  static template = () => null;
@@ -49,6 +49,15 @@ props:
49
49
  required: true
50
50
  type: string
51
51
  default: ""
52
+ bleed:
53
+ description: >-
54
+ Horizontal-bleed KPI layout. With a `slot="chart"` child, the value /
55
+ label / change stack on the left while the chart fills the right column
56
+ at full height and bleeds to the card's top / right / bottom edges (the
57
+ horizontal counterpart to a chart in a card `<section bleed>`). No effect
58
+ without a chart slot.
59
+ type: boolean
60
+ default: false
52
61
  events: {}
53
62
  slots:
54
63
  change:
@@ -249,14 +249,22 @@ export class UISwiper extends UIElement {
249
249
  }
250
250
 
251
251
  // ── Snap & page helpers ──
252
- // `--swiper-columns` is set on the host by `[slides-per-view]` and
253
- // cascades to the track; the responsive @container rules set it on
254
- // the track directly. Reading the computed value on the track sees
255
- // both paths.
256
-
252
+ // The column count drives the page/dot math. `[slides-per-view]` (and the
253
+ // responsive @container rules) set `--swiper-columns-default` on the host /
254
+ // track per the component-token contract; consumers may override via the
255
+ // public `--swiper-columns`. We must read the SAME fallback chain the CSS
256
+ // width calc uses — `var(--swiper-columns, var(--swiper-columns-default))` —
257
+ // not just the public token. Reading only `--swiper-columns` (unset unless a
258
+ // consumer overrides) made #getColumns() always fall back to 1, so the dot
259
+ // count was `total - 1 + 1 = total slides` instead of the real page count
260
+ // (bug-29: 5 slides at slides-per-view=3 stamped 5 dots, not 3). Regressed by
261
+ // the 2026-05-24 OD-5 `-default` token sweep, which renamed the CSS side but
262
+ // not this read site.
257
263
  #getColumns() {
258
264
  if (!this.#track) return 1;
259
- const v = getComputedStyle(this.#track).getPropertyValue('--swiper-columns').trim();
265
+ const cs = getComputedStyle(this.#track);
266
+ const v = cs.getPropertyValue('--swiper-columns').trim()
267
+ || cs.getPropertyValue('--swiper-columns-default').trim();
260
268
  const n = parseInt(v, 10);
261
269
  return Number.isFinite(n) && n > 0 ? n : 1;
262
270
  }
@@ -115,6 +115,19 @@ switch-ui[checked] [slot="thumb"] {
115
115
  color: var(--switch-hint-fg, var(--a-fg-muted));
116
116
  line-height: var(--switch-hint-lh, 1.4);
117
117
  }
118
+ /* The hint is documented (yaml + props table) as rendering BELOW the label,
119
+ but the base `inline-flex` row laid the stamped `[slot="hint"]` out as a
120
+ third inline item (beside the label). When a hint is present, let the row
121
+ wrap and force the hint onto its own full-width line, indented under the
122
+ label (past the track + gap). `:has()`-scoped so hint-less switches keep
123
+ the unchanged single-row layout (no long-label side effects); no display-
124
+ model change (stays inline-flex). The hint span is stamped only when
125
+ [hint] is set (switch.class.js), so this never adds a phantom line. */
126
+ :scope:has([slot="hint"]) { flex-wrap: wrap; }
127
+ :scope:has([slot="hint"]) [slot="hint"] {
128
+ flex-basis: 100%;
129
+ padding-inline-start: calc(var(--switch-track-width, var(--switch-track-width-default)) + var(--switch-gap, var(--switch-gap-default)));
130
+ }
118
131
 
119
132
  :scope:focus-visible { outline: none; }
120
133
  :scope:focus-visible [slot="track"] { box-shadow: var(--switch-focus-ring, var(--switch-focus-ring-default)); }
@@ -169,7 +169,7 @@
169
169
  }
170
170
  },
171
171
  "row-collapse": {
172
- "description": "Fired when an already-expanded row's chevron is activated (collapses the row). Mirror of row-expand.",
172
+ "description": "Fired when an already-expanded row's caret is activated (collapses the row). Mirror of row-expand.",
173
173
  "detail": {
174
174
  "index": {
175
175
  "description": "Row index in the underlying data array.",
@@ -182,7 +182,7 @@
182
182
  }
183
183
  },
184
184
  "row-expand": {
185
- "description": "Fired when an expandable row's chevron is activated. detail.index is the row position; detail.row is the row data.",
185
+ "description": "Fired when an expandable row's caret is activated. detail.index is the row position; detail.row is the row data.",
186
186
  "detail": {
187
187
  "index": {
188
188
  "description": "Row index in the underlying data array.",
@@ -323,7 +323,12 @@
323
323
  color: var(--table-fg-hover, var(--table-fg-hover-default));
324
324
  }
325
325
 
326
- [data-selected] {
326
+ /* Selected row — must out-specify the base row rule `[data-body] >
327
+ [role="row"]` (0,2,0) and the hover rule (0,2,1), or the highlight never
328
+ paints. Scoping to `[data-body] > [role="row"][data-selected]` lifts it to
329
+ (0,4,0). (The striped-even override below handles selected+striped, which
330
+ is higher still.) */
331
+ [data-body] > [role="row"][data-selected] {
327
332
  background: var(--table-row-bg-selected, var(--table-row-bg-selected-default));
328
333
  }
329
334
 
@@ -341,6 +346,13 @@
341
346
  background: var(--table-row-bg-hover, var(--table-row-bg-hover-default));
342
347
  }
343
348
 
349
+ /* Selected wins over the stripe — same (0,5,0) specificity as the striped-
350
+ even rule above, placed AFTER it so source order resolves the tie in
351
+ favor of selection (a selected row reads as selected regardless of stripe). */
352
+ :scope[striped] [data-body] > [role="row"][data-selected] {
353
+ background: var(--table-row-bg-selected, var(--table-row-bg-selected-default));
354
+ }
355
+
344
356
  /* ═══════ Selection checkboxes ═══════ */
345
357
 
346
358
  [data-check-col] {
@@ -146,7 +146,7 @@ events:
146
146
  type: number
147
147
  description: New column width in pixels.
148
148
  row-collapse:
149
- description: Fired when an already-expanded row's chevron is activated (collapses the row). Mirror of row-expand.
149
+ description: Fired when an already-expanded row's caret is activated (collapses the row). Mirror of row-expand.
150
150
  detail:
151
151
  index:
152
152
  type: number
@@ -155,7 +155,7 @@ events:
155
155
  type: object
156
156
  description: Row data object.
157
157
  row-expand:
158
- description: Fired when an expandable row's chevron is activated. detail.index is the row position; detail.row is the row data.
158
+ description: Fired when an expandable row's caret is activated. detail.index is the row position; detail.row is the row data.
159
159
  detail:
160
160
  index:
161
161
  type: number
@@ -30,7 +30,7 @@
30
30
  :scope {
31
31
  /* ── Base ── */
32
32
  box-sizing: border-box;
33
- display: inline-flex;
33
+ display: flex;
34
34
  align-items: center;
35
35
  gap: var(--time-picker-gap, var(--time-picker-gap-default));
36
36
  min-height: var(--time-picker-height, var(--time-picker-height-default));
@@ -50,6 +50,9 @@
50
50
  box-shadow var(--time-picker-duration, var(--time-picker-duration-default)) var(--time-picker-easing, var(--time-picker-easing-default));
51
51
  }
52
52
 
53
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
54
+ :scope[inline] { display: inline-flex; }
55
+
53
56
  /* ── Segments ── */
54
57
  :scope [data-segment] {
55
58
  display: inline-block;
@@ -87,7 +87,7 @@ export class UITimelineItem extends UIElement {
87
87
  spinner: { type: Boolean, default: false, reflect: true },
88
88
  };
89
89
 
90
- // §205 (v0.5.7): dynamic chevron icons stamped on expanded-state ternary
90
+ // §205 (v0.5.7): dynamic caret icons stamped on expanded-state ternary
91
91
  // (timeline.class.js:167). Per FEEDBACK-16 §1 + §209 slot-11 ternary-walker discovery.
92
92
  // Note: `this.icon` consumer-supplied — not declared here.
93
93
  static requiredIcons = ['caret-down', 'caret-right'];
@@ -134,7 +134,7 @@ export class UITimelineItem extends UIElement {
134
134
  this.querySelector(':scope > [slot="icon"]')?.remove();
135
135
  }
136
136
 
137
- // Outcomes sub-list + toggle chevron
137
+ // Outcomes sub-list + toggle caret
138
138
  let body = this.querySelector(':scope > [slot="outcomes"]');
139
139
  if (this.#outcomes.length > 0) {
140
140
  if (!body) {
@@ -150,7 +150,7 @@ export class UITimelineItem extends UIElement {
150
150
  }
151
151
  body.hidden = !this.#expanded;
152
152
 
153
- // Toggle chevron lives on the row — we stamp a button once
153
+ // Toggle caret lives on the row — we stamp a button once
154
154
  let toggle = this.querySelector(':scope > [data-timeline-toggle]');
155
155
  if (!toggle) {
156
156
  toggle = document.createElement('button');
@@ -39,10 +39,18 @@
39
39
  --timeline-line-fg-default: var(--a-border-subtle);
40
40
  --timeline-line-fg-done-default: var(--a-border);
41
41
 
42
+ /* Marker box — the shared circular footprint the icon marker fills and
43
+ the dot / spinner center within (the column track --timeline-marker-w
44
+ is a touch wider for breathing room). One token sizes the whole marker
45
+ system; consumers (agent-reasoning-ui, chat surfaces) override it to
46
+ scale every marker type coherently. */
47
+ --timeline-marker-box-size-default: 1rem;
48
+
42
49
  /* Icon (replaces dot) */
43
- --timeline-icon-size-default: 1rem;
50
+ --timeline-icon-size-default: var(--timeline-marker-box-size, var(--timeline-marker-box-size-default));
44
51
  --timeline-icon-bg-default: var(--a-bg-muted);
45
52
  --timeline-icon-fg-default: var(--a-fg-subtle);
53
+ --timeline-icon-border-default: var(--a-border-subtle);
46
54
  --timeline-icon-bg-success-default: var(--a-success-muted);
47
55
  --timeline-icon-fg-success-default: var(--a-success-strong);
48
56
  --timeline-icon-bg-accent-default: var(--a-accent-muted);
@@ -85,6 +93,7 @@
85
93
  /* Compact variant (agent-reasoning + chat-adjacent) */
86
94
  :scope[size="sm"] {
87
95
  --timeline-marker-w-default: 1rem;
96
+ --timeline-marker-box-size-default: 0.875rem;
88
97
  --timeline-dot-size-default: 6px;
89
98
  --timeline-gap-row-default: var(--a-space-2);
90
99
  --timeline-label-size-default: var(--a-ui-sm);
@@ -133,9 +142,14 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
133
142
  --timeline-item-line-fg: var(--timeline-line-fg, var(--a-border-subtle));
134
143
  --timeline-item-line-fg-done: var(--timeline-line-fg-done, var(--a-border));
135
144
 
136
- --timeline-item-icon-size: var(--timeline-icon-size, 1rem);
145
+ /* Reads the override THEN the inherited -default (set on timeline-ui,
146
+ and shrunk by [size="sm"]) THEN a literal — so the marker box scales
147
+ with both consumer overrides AND the sm compact variant. */
148
+ --timeline-item-marker-box-size: var(--timeline-marker-box-size, var(--timeline-marker-box-size-default, 1rem));
149
+ --timeline-item-icon-size: var(--timeline-icon-size, var(--timeline-item-marker-box-size));
137
150
  --timeline-item-icon-bg: var(--timeline-icon-bg, var(--a-bg-muted));
138
151
  --timeline-item-icon-fg: var(--timeline-icon-fg, var(--a-fg-subtle));
152
+ --timeline-item-icon-border: var(--timeline-icon-border, var(--a-border-subtle));
139
153
  --timeline-item-icon-bg-success: var(--timeline-icon-bg-success, var(--a-success-muted));
140
154
  --timeline-item-icon-fg-success: var(--timeline-icon-fg-success, var(--a-success-strong));
141
155
  --timeline-item-icon-bg-accent: var(--timeline-icon-bg-accent, var(--a-accent-muted));
@@ -245,6 +259,10 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
245
259
  border-radius: 50%;
246
260
  background: var(--timeline-item-icon-bg);
247
261
  color: var(--timeline-item-icon-fg);
262
+ /* Subtle ring so the default (no-variant) chip reads as a marker even
263
+ when its muted fill blends with the surrounding surface; the colored
264
+ variants below restate background only, keeping this ring. */
265
+ border: 1px solid var(--timeline-item-icon-border);
248
266
  display: inline-flex;
249
267
  align-items: center;
250
268
  justify-content: center;
@@ -342,12 +360,12 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
342
360
  background: var(--timeline-item-subdot-bg);
343
361
  }
344
362
 
345
- /* ═══════ Toggle chevron (outcomes) ═══════ */
363
+ /* ═══════ Toggle caret (outcomes) ═══════ */
346
364
 
347
365
  :scope > [data-timeline-toggle] {
348
366
  position: absolute;
349
367
  inset-inline-end: 0;
350
- /* Place the chevron's icon-center on row 1's optical center.
368
+ /* Place the caret's icon-center on row 1's optical center.
351
369
  The button has --a-space-0-5 vertical padding, so the icon
352
370
  sits that much lower inside the box; we subtract that so the
353
371
  icon (not the box) lines up with the time-slot text center. */
@@ -372,7 +390,7 @@ agent-reasoning-ui timeline-ui:not([orientation="horizontal"]),
372
390
  color: var(--timeline-item-label-fg, var(--a-fg));
373
391
  }
374
392
 
375
- /* Reserve room so the time isn't overlapped by the chevron */
393
+ /* Reserve room so the time isn't overlapped by the caret */
376
394
  :scope:has(> [data-timeline-toggle]) [slot="time"] {
377
395
  margin-inline-end: var(--timeline-item-toggle-time-margin);
378
396
  }
@@ -19,11 +19,14 @@ toggle-option-ui:not([disabled]):hover {
19
19
 
20
20
  :scope {
21
21
  box-sizing: border-box;
22
- display: inline-flex;
22
+ display: flex;
23
23
  border: var(--toggle-group-border-width, var(--toggle-group-border-width-default)) solid var(--toggle-group-border-color, var(--toggle-group-border-color-default));
24
24
  border-radius: var(--toggle-group-radius, var(--toggle-group-radius-default));
25
25
  overflow: hidden;
26
26
  }
27
+
28
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
29
+ :scope[inline] { display: inline-flex; }
27
30
  }
28
31
 
29
32
  @scope (toggle-option-ui) {
@@ -4,12 +4,15 @@
4
4
  }
5
5
 
6
6
  :scope {
7
- display: inline-flex;
7
+ display: flex;
8
8
  align-items: center;
9
9
  justify-content: center;
10
10
  line-height: 0;
11
11
  }
12
12
 
13
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
14
+ :scope[inline] { display: inline-flex; }
15
+
13
16
  :scope[disabled] {
14
17
  pointer-events: none;
15
18
  }