@adia-ai/web-components 0.6.49 → 0.7.0

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 (113) hide show
  1. package/CHANGELOG.md +128 -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 +52 -1
  6. package/components/agent-reasoning/agent-reasoning.css +49 -22
  7. package/components/agent-trace/agent-trace.css +10 -10
  8. package/components/agent-trace/agent-trace.js +3 -3
  9. package/components/alert/alert.class.js +8 -1
  10. package/components/alert/alert.css +13 -1
  11. package/components/avatar/avatar.a2ui.json +2 -14
  12. package/components/avatar/avatar.class.js +3 -15
  13. package/components/avatar/avatar.d.ts +2 -4
  14. package/components/avatar/avatar.yaml +1 -18
  15. package/components/breadcrumb/breadcrumb.css +4 -1
  16. package/components/button/button.a2ui.json +3 -0
  17. package/components/button/button.css +14 -3
  18. package/components/button/button.yaml +5 -0
  19. package/components/calendar-grid/calendar-grid.css +1 -1
  20. package/components/calendar-picker/calendar-picker.css +5 -2
  21. package/components/chart/chart.a2ui.json +0 -18
  22. package/components/chart/chart.class.js +8 -50
  23. package/components/chart/chart.css +1 -15
  24. package/components/chart/chart.d.ts +0 -4
  25. package/components/chart/chart.yaml +0 -24
  26. package/components/color-input/color-input.css +4 -1
  27. package/components/combobox/combobox.class.js +11 -0
  28. package/components/combobox/combobox.css +8 -0
  29. package/components/date-range-picker/date-range-picker.class.js +5 -1
  30. package/components/date-range-picker/date-range-picker.css +12 -2
  31. package/components/datetime-picker/datetime-picker.class.js +3 -0
  32. package/components/datetime-picker/datetime-picker.css +16 -2
  33. package/components/empty-state/empty-state.css +11 -4
  34. package/components/field/field.css +17 -6
  35. package/components/grid/grid.a2ui.json +5 -0
  36. package/components/grid/grid.class.js +16 -6
  37. package/components/grid/grid.css +17 -3
  38. package/components/grid/grid.d.ts +2 -0
  39. package/components/grid/grid.yaml +9 -0
  40. package/components/heatmap/heatmap.class.js +9 -3
  41. package/components/heatmap/heatmap.css +19 -2
  42. package/components/image/image.css +4 -1
  43. package/components/input/input.css +1 -1
  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.css +4 -1
  52. package/components/modal/modal.class.js +10 -1
  53. package/components/modal/modal.css +9 -0
  54. package/components/option-card/option-card.a2ui.json +3 -0
  55. package/components/option-card/option-card.css +44 -19
  56. package/components/option-card/option-card.yaml +5 -0
  57. package/components/otp-input/otp-input.css +25 -10
  58. package/components/page/page.css +64 -11
  59. package/components/pagination/pagination.class.js +1 -1
  60. package/components/pagination/pagination.css +9 -1
  61. package/components/pane/pane.a2ui.json +3 -0
  62. package/components/pane/pane.class.js +7 -6
  63. package/components/pane/pane.css +5 -5
  64. package/components/pane/pane.yaml +6 -0
  65. package/components/pipeline-status/pipeline-status.css +6 -0
  66. package/components/popover/popover.css +12 -1
  67. package/components/preview/preview.css +30 -3
  68. package/components/progress-row/progress-row.css +3 -1
  69. package/components/qr-code/qr-code.css +4 -1
  70. package/components/segmented/segmented.css +4 -1
  71. package/components/select/select.a2ui.json +1 -1
  72. package/components/select/select.class.js +63 -7
  73. package/components/select/select.css +18 -0
  74. package/components/select/select.yaml +9 -2
  75. package/components/stack/stack.a2ui.json +12 -1
  76. package/components/stack/stack.d.ts +2 -2
  77. package/components/stack/stack.yaml +13 -1
  78. package/components/stat/stat.a2ui.json +5 -0
  79. package/components/stat/stat.css +55 -0
  80. package/components/stat/stat.d.ts +2 -0
  81. package/components/stat/stat.js +4 -0
  82. package/components/stat/stat.yaml +9 -0
  83. package/components/swiper/swiper.class.js +14 -6
  84. package/components/switch/switch.css +13 -0
  85. package/components/table/table.a2ui.json +2 -2
  86. package/components/table/table.css +13 -1
  87. package/components/table/table.yaml +2 -2
  88. package/components/time-picker/time-picker.css +4 -1
  89. package/components/timeline/timeline.class.js +3 -3
  90. package/components/timeline/timeline.css +23 -5
  91. package/components/toggle-group/toggle-group.css +4 -1
  92. package/components/toggle-scheme/toggle-scheme.css +4 -1
  93. package/components/tree/tree-item.a2ui.json +6 -0
  94. package/components/tree/tree-item.yaml +13 -1
  95. package/components/tree/tree.a2ui.json +3 -3
  96. package/components/tree/tree.class.js +24 -9
  97. package/components/tree/tree.css +8 -8
  98. package/components/tree/tree.test.js +52 -0
  99. package/components/tree/tree.yaml +4 -4
  100. package/dist/web-components.min.css +1 -1
  101. package/dist/web-components.min.js +84 -84
  102. package/package.json +3 -3
  103. package/styles/api/layout.css +7 -0
  104. package/styles/api/text.css +9 -5
  105. package/styles/index.css +11 -2
  106. package/styles/prose.css +8 -0
  107. package/styles/resets.css +5 -5
  108. package/styles/themes.css +8 -1
  109. package/styles/tokens.css +3 -3
  110. package/styles/type/elements.css +73 -0
  111. package/styles/type/roles.css +14 -49
  112. package/styles/type/scale.css +0 -5
  113. package/styles/typography.css +3 -3
@@ -1,9 +1,13 @@
1
1
  @scope (otp-input-ui) {
2
2
  :where(:scope) {
3
3
  /* ── Tokens ── */
4
- --otp-input-size-default: var(--a-size);
4
+ /* Digit boxes grow to fill the row, capped at ~1.75× the base control size
5
+ (~52px) so they read as comfortable single-digit boxes. Radius is the
6
+ plain --a-radius-lg token (no min()/clamp expression — radius patterns
7
+ stay simple). Scales with [size] + density (relative to --a-size). */
8
+ --otp-input-size-default: calc(var(--a-size) * 1.75);
5
9
  --otp-input-gap-default: var(--a-space-2);
6
- --otp-input-radius-default: var(--a-radius-md);
10
+ --otp-input-radius-default: var(--a-radius-lg);
7
11
  --otp-input-border-default: var(--a-ui-border);
8
12
  --otp-input-border-hover-default: var(--a-ui-border-hover);
9
13
  --otp-input-border-focus-default: var(--a-accent);
@@ -24,15 +28,27 @@
24
28
  /* ── Base ── */
25
29
  box-sizing: border-box;
26
30
  display: flex;
27
- justify-content: space-around;
31
+ justify-content: center;
28
32
  gap: var(--otp-input-gap, var(--otp-input-gap-default));
33
+ /* Fill the container (a block-level control per ADR-0037) so the digit
34
+ boxes grow to use the available width instead of sitting content-width
35
+ on the left. Boxes flex to fill up to their cap; once capped (very wide
36
+ container) the group centers rather than packing left. (bug-37 follow-up) */
37
+ width: 100%;
29
38
  }
30
39
 
31
40
  /* ── Digit inputs ── */
41
+ /* Boxes grow equally to fill the row (flex), staying square (aspect-ratio)
42
+ and capped at --otp-input-size so they read as digit boxes — not huge on a
43
+ wide container, not cramped on a narrow one. The cap keeps --a-radius-md
44
+ proportional (a rounded square, ~25-30%), which is the whole point of the
45
+ bug-37 sizing. */
32
46
  [slot="digit"] {
33
47
  box-sizing: border-box;
34
- width: var(--otp-input-size, var(--otp-input-size-default));
35
- height: var(--otp-input-size, var(--otp-input-size-default));
48
+ flex: 1 1 0;
49
+ min-width: 0;
50
+ max-width: var(--otp-input-size, var(--otp-input-size-default));
51
+ aspect-ratio: 1;
36
52
  text-align: center;
37
53
  border: 1px solid var(--otp-input-border, var(--otp-input-border-default));
38
54
  border-radius: var(--otp-input-radius, var(--otp-input-radius-default));
@@ -67,12 +83,11 @@
67
83
  :scope[disabled] [slot="digit"] {
68
84
  background: var(--otp-input-bg-disabled, var(--otp-input-bg-disabled-default));
69
85
  color: var(--otp-input-fg-disabled, var(--otp-input-fg-disabled-default));
70
- /* Dashed border rendered via SVG border-image for consistent ~8px
71
- dashes across browsers (native `border-style: dashed` is too short
72
- and varies by engine). The border-color fallback is used when
73
- border-image isn't supported. */
86
+ /* Native dashed border it follows the box's border-radius (a rounded
87
+ dashed square, matching the enabled state). An SVG border-image would
88
+ give more uniform dashes but is clipped to a SQUARE — it ignores
89
+ border-radius — so the disabled corners wouldn't round. */
74
90
  border: 1px dashed var(--otp-input-border-disabled, var(--otp-input-border-disabled-default));
75
- border-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='28' height='28' preserveAspectRatio='none'><rect x='0.5' y='0.5' width='27' height='27' fill='none' stroke='%23999' stroke-width='1' stroke-dasharray='8 6'/></svg>") 1 repeat;
76
91
  cursor: not-allowed;
77
92
  }
78
93
  }
@@ -9,6 +9,15 @@
9
9
  /* ── Padding default (when [padding] is set without a value) ── */
10
10
  --page-padding-default: var(--a-space-6);
11
11
 
12
+ /* ── Region rhythm — vertical gap between header / section / footer,
13
+ mirroring card-ui's section inset. The page [padding] supplies the
14
+ outer frame; this is the inter-region spacing. ── */
15
+ --page-inset-default: var(--a-space-5);
16
+
17
+ /* ── Padded-region sub-surface (a section[padding] inside the page) ── */
18
+ --page-region-bg-default: var(--a-bg-subtle);
19
+ --page-region-radius-default: var(--a-radius-md);
20
+
12
21
  /* ── Surfaces ── */
13
22
  --page-bg-default: var(--a-canvas-0);
14
23
  --page-fg-default: var(--a-fg);
@@ -23,6 +32,9 @@
23
32
  box-sizing: border-box;
24
33
  display: block;
25
34
  width: 100%;
35
+ /* --page-pad carries the resolved [padding] value so the sticky header can
36
+ bleed back over it (card-ui pattern) — see the sticky-header rule. */
37
+ padding: var(--page-pad, 0);
26
38
  background: var(--page-bg, var(--page-bg-default));
27
39
  color: var(--page-fg, var(--page-fg-default));
28
40
  }
@@ -33,17 +45,47 @@
33
45
  :scope[max-width="wide"] { max-width: var(--page-max-width-wide, var(--page-max-width-wide-default)); margin-inline: auto; }
34
46
  :scope[max-width="full"] { max-width: var(--page-max-width-full, var(--page-max-width-full-default)); }
35
47
 
36
- /* ── Padding scale (mirrors --a-space-N) ── */
37
- :scope[padding=""] { padding: var(--page-padding-default); }
38
- :scope[padding="0"] { padding: 0; }
39
- :scope[padding="1"] { padding: var(--a-space-1); }
40
- :scope[padding="2"] { padding: var(--a-space-2); }
41
- :scope[padding="3"] { padding: var(--a-space-3); }
42
- :scope[padding="4"] { padding: var(--a-space-4); }
43
- :scope[padding="5"] { padding: var(--a-space-5); }
44
- :scope[padding="6"] { padding: var(--a-space-6); }
45
- :scope[padding="7"] { padding: var(--a-space-7); }
46
- :scope[padding="8"] { padding: var(--a-space-8); }
48
+ /* ── Padding scale (mirrors --a-space-N) — sets --page-pad, applied above ── */
49
+ :scope[padding=""] { --page-pad: var(--page-padding-default); }
50
+ :scope[padding="0"] { --page-pad: 0; }
51
+ :scope[padding="1"] { --page-pad: var(--a-space-1); }
52
+ :scope[padding="2"] { --page-pad: var(--a-space-2); }
53
+ :scope[padding="3"] { --page-pad: var(--a-space-3); }
54
+ :scope[padding="4"] { --page-pad: var(--a-space-4); }
55
+ :scope[padding="5"] { --page-pad: var(--a-space-5); }
56
+ :scope[padding="6"] { --page-pad: var(--a-space-6); }
57
+ :scope[padding="7"] { --page-pad: var(--a-space-7); }
58
+ :scope[padding="8"] { --page-pad: var(--a-space-8); }
59
+
60
+ /* ═══════ Region model — header / section / footer ═══════
61
+ Mirrors card-ui's section model, adapted to the page's [padding] frame:
62
+ the page [padding] is the OUTER inset; these rules add the vertical
63
+ rhythm BETWEEN regions plus the [bleed] / [padding] modifiers. The
64
+ page's own @scope styling the slot primitives is what the docs promise
65
+ ([<header>] / [<section>] / [<footer>] and their -ui variants). */
66
+
67
+ /* Vertical rhythm: every region after the first picks up a top margin so
68
+ header → body → footer breathe consistently (the first region hugs the
69
+ padding edge). */
70
+ :scope > :where(header, header-ui, section, section-ui, footer, footer-ui)
71
+ ~ :where(header, header-ui, section, section-ui, footer, footer-ui) {
72
+ margin-block-start: var(--page-inset, var(--page-inset-default));
73
+ }
74
+
75
+ /* [bleed] — edge-to-edge region: cancel the page's inline [padding] so
76
+ full-width content (hero, banner, table, chart) reaches the page edges.
77
+ Resolves to 0 (no-op) when the page has no padding. */
78
+ :scope > :where(header, header-ui, section, section-ui, footer, footer-ui)[bleed] {
79
+ margin-inline: calc(-1 * var(--page-pad, 0));
80
+ }
81
+
82
+ /* [padding] on a region — a padded sub-surface with its own background +
83
+ radius, like card-ui's section[padding]. */
84
+ :scope > :where(section, section-ui)[padding] {
85
+ padding: var(--page-inset, var(--page-inset-default));
86
+ background: var(--page-region-bg, var(--page-region-bg-default));
87
+ border-radius: var(--page-region-radius, var(--page-region-radius-default));
88
+ }
47
89
 
48
90
  /* ── Scroll container ── */
49
91
  :scope[scroll] {
@@ -53,11 +95,22 @@
53
95
  }
54
96
 
55
97
  /* ── Sticky-header support ── */
98
+ /* Drop the page's TOP padding when the header is sticky — that gap is where
99
+ scrolling content used to peek ABOVE the pinned header. The header then
100
+ sits flush at the scroll-container top and supplies its own top spacing
101
+ (padding-block below). */
102
+ :scope[sticky-header] { padding-top: 0; }
56
103
  :scope[sticky-header] > :where(header, header-ui) {
57
104
  position: sticky;
58
105
  top: 0;
59
106
  z-index: 1;
60
107
  background: var(--page-sticky-bg, var(--page-sticky-bg-default));
108
+ /* Bleed horizontally over the page's inline [padding] so the opaque band
109
+ spans the full scroll width, then re-inset the content so it stays
110
+ aligned with the body. Vertical = its own breathing room. Card-ui
111
+ header pattern. (No negative margin-top — that breaks position:sticky.) */
112
+ margin-inline: calc(-1 * var(--page-pad, 0));
113
+ padding: var(--a-space-3) var(--page-pad, 0);
61
114
  transition: border-color var(--a-duration-fast) var(--a-easing), box-shadow var(--a-duration-fast) var(--a-easing);
62
115
  }
63
116
 
@@ -37,7 +37,7 @@ export class UIPagination extends UIElement {
37
37
  size: { type: String, default: 'md', reflect: true },
38
38
  };
39
39
 
40
- // Phosphor icons stamped by this primitive (prev/next chevrons inside
40
+ // Phosphor icons stamped by this primitive (prev/next carets inside
41
41
  // the nested <button-ui>). Audited by check-required-icons.mjs.
42
42
  static requiredIcons = ['caret-left', 'caret-right'];
43
43
 
@@ -23,9 +23,12 @@
23
23
  :scope {
24
24
  /* ── Base ── */
25
25
  box-sizing: border-box;
26
- display: inline-flex;
26
+ display: flex;
27
27
  }
28
28
 
29
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
30
+ :scope[inline] { display: inline-flex; }
31
+
29
32
  /* ── Nav container ── */
30
33
  [slot="nav"] {
31
34
  display: flex;
@@ -66,5 +69,10 @@
66
69
  aspect-ratio 1 here for the bordered-cell look). */
67
70
  :scope[variant="button"] [slot="nav"] button-ui {
68
71
  aspect-ratio: 1;
72
+ /* 1:1 cells inherit button-ui's --a-radius-md (~13px), which on a square
73
+ cell is ~37% of the side → a circle, not the "square 1:1 bordered button"
74
+ the variant promises. Tighten to -sm so the bordered cells read as rounded
75
+ squares. Same square+large-radius trap as otp-input (bug-37 / bug-39). */
76
+ --button-radius: var(--a-radius-sm);
69
77
  }
70
78
  }
@@ -82,6 +82,9 @@
82
82
  "text-area"
83
83
  ],
84
84
  "slots": {
85
+ "caret": {
86
+ "description": "Collapse caret inside the header. Auto-stamped as `<icon-ui slot=\"caret\" name=\"caret-right\">` (rotates on `[collapsed]`). Supply your own `slot=\"caret\"` child in the header to customize — adopt-or-stamp honors a declarative caret instead of stamping."
87
+ },
85
88
  "header": {
86
89
  "description": "Auto-created header element with label text and toggle arrow"
87
90
  }
@@ -100,12 +100,13 @@ export class UIPane extends UIElement {
100
100
  header.setAttribute('tabindex', '0');
101
101
  header.setAttribute('aria-expanded', String(!this.collapsed));
102
102
 
103
- // Stamp chevron icon if not present
104
- if (!header.querySelector('[slot="chevron"]')) {
105
- const chevron = document.createElement('icon-ui');
106
- chevron.setAttribute('slot', 'chevron');
107
- chevron.setAttribute('name', 'caret-right');
108
- header.append(chevron);
103
+ // Stamp the caret icon if not present (adopt-or-stamp: a declarative
104
+ // [slot="caret"] child is honored, else we stamp the default).
105
+ if (!header.querySelector('[slot="caret"]')) {
106
+ const caret = document.createElement('icon-ui');
107
+ caret.setAttribute('slot', 'caret');
108
+ caret.setAttribute('name', 'caret-right');
109
+ header.append(caret);
109
110
  }
110
111
  }
111
112
  }
@@ -29,7 +29,7 @@
29
29
 
30
30
  /* ── Header interaction ── */
31
31
  --pane-header-bg-hover-default: var(--a-bg-subtle);
32
- --pane-chevron-fg-default: var(--a-fg-muted);
32
+ --pane-caret-fg-default: var(--a-fg-muted);
33
33
 
34
34
  /* ── Section header ── */
35
35
  --pane-section-header-weight-default: var(--a-weight-medium);
@@ -107,17 +107,17 @@
107
107
  box-shadow: var(--a-focus-ring) inset;
108
108
  }
109
109
 
110
- /* Collapse indicator — stamped by JS as icon-ui */
111
- & > header > [slot="chevron"] {
110
+ /* Collapse indicator (caret) — stamped by JS as icon-ui */
111
+ & > header > [slot="caret"] {
112
112
  --a-icon-size: var(--a-caret-size);
113
113
  flex-shrink: 0;
114
114
  margin-inline-start: auto;
115
- color: var(--pane-chevron-fg, var(--pane-chevron-fg-default));
115
+ color: var(--pane-caret-fg, var(--pane-caret-fg-default));
116
116
  transition: transform var(--pane-duration, var(--pane-duration-default)) var(--pane-easing, var(--pane-easing-default));
117
117
  transform: rotate(90deg);
118
118
  }
119
119
 
120
- :scope[collapsed] > header > [slot="chevron"] {
120
+ :scope[collapsed] > header > [slot="caret"] {
121
121
  transform: rotate(0deg);
122
122
  }
123
123
 
@@ -62,6 +62,12 @@ events:
62
62
  slots:
63
63
  header:
64
64
  description: Auto-created header element with label text and toggle arrow
65
+ caret:
66
+ description: >-
67
+ Collapse caret inside the header. Auto-stamped as
68
+ `<icon-ui slot="caret" name="caret-right">` (rotates on `[collapsed]`).
69
+ Supply your own `slot="caret"` child in the header to customize —
70
+ adopt-or-stamp honors a declarative caret instead of stamping.
65
71
  states:
66
72
  - name: idle
67
73
  description: Default, ready for interaction.
@@ -96,11 +96,17 @@
96
96
  transition: background var(--pipeline-status-duration, var(--pipeline-status-duration-default)) var(--pipeline-status-easing, var(--pipeline-status-easing-default));
97
97
  }
98
98
 
99
+ /* State colors: the -active (accent) / -complete (success) dot-bg tokens were
100
+ defined but never applied — these rules only set animation, so every dot
101
+ stayed the default gray (--a-border). Now active dots read accent + pulse,
102
+ complete dots read success. (bug-40 — "use color more") */
99
103
  [data-pipeline-dot="active"] {
104
+ background: var(--pipeline-status-dot-bg-active, var(--pipeline-status-dot-bg-active-default));
100
105
  animation: pipeline-pulse var(--pipeline-status-pulse-duration, var(--pipeline-status-pulse-duration-default)) var(--pipeline-status-pulse-easing, var(--pipeline-status-pulse-easing-default)) infinite;
101
106
  }
102
107
 
103
108
  [data-pipeline-dot="complete"] {
109
+ background: var(--pipeline-status-dot-bg-complete, var(--pipeline-status-dot-bg-complete-default));
104
110
  animation: none;
105
111
  }
106
112
 
@@ -14,10 +14,13 @@
14
14
 
15
15
  :scope {
16
16
  box-sizing: border-box;
17
- display: inline-flex;
17
+ display: flex;
18
18
  position: relative;
19
19
  }
20
20
 
21
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
22
+ :scope[inline] { display: inline-flex; }
23
+
21
24
  [slot="trigger"] {
22
25
  display: inline-flex;
23
26
  }
@@ -81,6 +84,14 @@
81
84
  box-shadow: var(--popover-shadow, var(--popover-shadow-default));
82
85
  }
83
86
 
87
+ /* Prose margin reset on the content's edge children — a slotted <p> / <h*>
88
+ carries the UA stylesheet's margin-block (~1em), which lands inside the
89
+ panel padding and adds asymmetric top/bottom space (a single-line <p> looked
90
+ like it had a stray bottom margin). Zero the leading/trailing margins so the
91
+ panel padding alone frames the content. (bug — Popover single-line space) */
92
+ [slot="content"] > :first-child { margin-block-start: 0; }
93
+ [slot="content"] > :last-child { margin-block-end: 0; }
94
+
84
95
  [slot="content"]:popover-open {
85
96
  @starting-style {
86
97
  opacity: 0;
@@ -39,16 +39,42 @@
39
39
  by the frame's overflow:hidden. Most demos fit (the component's overflow
40
40
  check stacks split→full-width first), so the scrollbar only appears when a
41
41
  demo is genuinely wider than the docs column. */
42
+ /* Column flow is the default: each example stacks in DOM order and fills the
43
+ stage width (align-items: stretch), so block-level demos (progress / bars /
44
+ accordion / cards / pages …) read at full width without a per-component
45
+ list. Text-flow atoms (button / badge / tag / …) opt back to content-width
46
+ via align-self below. (bug-46 — replaces the old flex-row + :only-child
47
+ block-flow hack, which only filled single-child demos.) */
42
48
  [data-preview-render] {
43
49
  display: flex;
44
- flex-wrap: wrap;
45
- align-items: center;
50
+ flex-direction: column;
51
+ align-items: stretch;
46
52
  gap: var(--preview-render-gap, var(--preview-render-gap-default));
47
53
  padding: var(--preview-render-pad, var(--preview-render-pad-default));
48
54
  background: var(--preview-render-bg, var(--preview-render-bg-default));
49
55
  overflow-x: auto;
50
56
  }
51
-
57
+ /* Lone text-flow atoms shouldn't span the whole stage — keep them
58
+ content-width + leading-aligned (the ADR-0037 inline-display set). */
59
+ [data-preview-render] > :is(button-ui, badge-ui, tag-ui, chip-ui, kbd-ui, icon-ui, swatch-ui, spinner-ui, switch-ui, avatar-ui) {
60
+ align-self: start;
61
+ }
62
+ /* Form-field demos collapse to content-width as flex items in the render
63
+ cell — a placeholder-only combobox shrank to a ~52px "Sel…" stub — so
64
+ stretch form controls to the cell width with width:100%.
65
+ • <field-ui> is a full-width form ROW: let it fill the whole stage,
66
+ NO measure cap. The prior 28rem cap read as "not using available
67
+ space" on wide single-block demos (bug-55 follow-up).
68
+ • Bare atomic controls (input/select/…) keep the 28rem measure cap so
69
+ a lone control doesn't sprawl into an unrealistic bar on a wide stage.
70
+ Non-form demos (buttons, badges, etc.) are untouched — content-width. */
71
+ [data-preview-render] field-ui {
72
+ width: 100%;
73
+ }
74
+ [data-preview-render] > :is(input-ui, select-ui, combobox-ui, textarea-ui, tags-input-ui, color-input-ui) {
75
+ width: 100%;
76
+ max-width: 28rem;
77
+ }
52
78
  /* Code pane — divider line between panes; flatten the nested code-ui
53
79
  chrome so the preview frame owns the outer border + radius. */
54
80
  [data-preview-code] {
@@ -151,6 +177,7 @@
151
177
  color: var(--a-fg-muted);
152
178
  background: var(--a-bg-muted);
153
179
  padding: 0.125rem 0.375rem;
180
+ margin-bottom: 0.5rem;
154
181
  border-radius: var(--a-radius-sm);
155
182
  }
156
183
  /* Narrow: collapse each row to stacked render-over-code. */
@@ -1,7 +1,9 @@
1
1
  @scope (progress-row-ui) {
2
2
  :where(:scope) {
3
3
  /* ── Tokens ── */
4
- --progress-row-gap-default: var(--a-space-1);
4
+ /* Row-gap between the label/meta row and the bar. Bumped --a-space-1
5
+ --a-space-2 so the bar isn't crammed under the label. (bug-42) */
6
+ --progress-row-gap-default: var(--a-space-2);
5
7
  --progress-row-column-gap-default: var(--a-space-2);
6
8
  --progress-row-label-size-default: var(--a-ui-size);
7
9
  --progress-row-label-fg-default: var(--a-fg);
@@ -10,7 +10,7 @@
10
10
 
11
11
  :scope {
12
12
  box-sizing: border-box;
13
- display: inline-block;
13
+ display: block;
14
14
  /* The SVG itself carries explicit width/height; this is a fallback
15
15
  for cases where the SVG hasn't stamped yet (empty value/matrix). */
16
16
  line-height: 0;
@@ -18,6 +18,9 @@
18
18
  background: var(--qr-code-bg, var(--qr-code-bg-default));
19
19
  }
20
20
 
21
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
22
+ :scope[inline] { display: inline-block; }
23
+
21
24
  :scope svg {
22
25
  display: block;
23
26
  /* Width / height come from the explicit SVG attributes set by JS
@@ -28,7 +28,7 @@
28
28
  :scope {
29
29
  /* ── Base ── */
30
30
  box-sizing: border-box;
31
- display: inline-grid;
31
+ display: grid;
32
32
  grid-auto-flow: column;
33
33
  grid-auto-columns: 1fr;
34
34
  align-items: stretch;
@@ -55,6 +55,9 @@
55
55
  border-radius: var(--segmented-radius, var(--segmented-radius-default));
56
56
  }
57
57
 
58
+ /* Display convention (ADR-0037): block-level by default; [inline] opts back to inline-level. */
59
+ :scope[inline] { display: inline-grid; }
60
+
58
61
  /* -- Indicator (hidden until first selection) -- */
59
62
  :scope > [data-indicator] {
60
63
  display: none;
@@ -107,7 +107,7 @@
107
107
  "default": false
108
108
  },
109
109
  "options": {
110
- "description": "Option list. Array of {value, label, disabled?} or grouped {label, options: [...]}. Alternative to declarative <option> / <optgroup> children.",
110
+ "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.",
111
111
  "type": "array",
112
112
  "default": []
113
113
  },
@@ -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