@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
@@ -0,0 +1,130 @@
1
+ /**
2
+ * <menu-ui> behavioral tests.
3
+ *
4
+ * FEEDBACK-92 (2026-05-31): `#show()` collected items with a direct-child
5
+ * `:scope >` query, so items rendered through the template engine's
6
+ * `.map()` / `repeat()` (each wrapped in a `display:contents` <span>) were
7
+ * skipped → the popover opened empty. The fix uses a descendant query
8
+ * (mirroring `#hide()`). These tests pin the dynamic + static paths.
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
12
+ import { html, stamp, repeat } from '../../core/template.js';
13
+
14
+ beforeAll(async () => {
15
+ await import('../../core/element.js');
16
+ await import('./menu.js');
17
+ await import('../button/button.js');
18
+ });
19
+
20
+ const settle = () => new Promise((r) => setTimeout(r, 30));
21
+
22
+ describe('<menu-ui> collects dynamically-rendered items (FEEDBACK-92)', () => {
23
+ let host;
24
+
25
+ beforeEach(() => {
26
+ host = document.createElement('div');
27
+ document.body.appendChild(host);
28
+ });
29
+ afterEach(() => host.remove());
30
+
31
+ function popItems(menu) {
32
+ const pop = menu.querySelector('[data-menu-popover]');
33
+ return pop ? pop.querySelectorAll('menu-item-ui') : [];
34
+ }
35
+
36
+ it('populates the popover from `.map()`-rendered items (the bug)', async () => {
37
+ const ITEMS = [{ id: 'a', text: 'Alpha' }, { id: 'b', text: 'Beta' }];
38
+ // Render the menu (closed) the documented way — items via `.map()`. The
39
+ // consumer opens it later via interaction, after the template has rendered.
40
+ stamp(html`
41
+ <menu-ui>
42
+ <button-ui slot="trigger" text="Open"></button-ui>
43
+ ${ITEMS.map((i) => html`<menu-item-ui .text=${i.text} .value=${i.id}></menu-item-ui>`)}
44
+ </menu-ui>
45
+ `, host);
46
+ await settle();
47
+
48
+ const menu = host.querySelector('menu-ui');
49
+ // The exact bug condition: items exist under the host but are NOT direct
50
+ // children — the template engine nested them inside display:contents spans,
51
+ // which the old `:scope >` query in #show() could not see.
52
+ expect(menu.querySelectorAll('menu-item-ui').length).toBe(2);
53
+ expect(menu.querySelector(':scope > menu-item-ui')).toBeNull();
54
+
55
+ menu.open = true; // user opens the menu post-render
56
+ await settle();
57
+
58
+ const items = popItems(menu);
59
+ expect(items.length).toBe(2);
60
+ expect([...items].map((el) => el.value)).toEqual(['a', 'b']);
61
+ });
62
+
63
+ it('populates the popover from `repeat()`-rendered items', async () => {
64
+ const ITEMS = [{ id: 'x', text: 'Ex' }, { id: 'y', text: 'Why' }, { id: 'z', text: 'Zee' }];
65
+ stamp(html`
66
+ <menu-ui>
67
+ <button-ui slot="trigger" text="Open"></button-ui>
68
+ ${repeat(ITEMS, (i) => i.id, (i) => html`<menu-item-ui .text=${i.text} .value=${i.id}></menu-item-ui>`)}
69
+ </menu-ui>
70
+ `, host);
71
+ await settle();
72
+
73
+ const menu = host.querySelector('menu-ui');
74
+ expect(menu.querySelector(':scope > menu-item-ui')).toBeNull(); // behind wrappers
75
+ menu.open = true;
76
+ await settle();
77
+ expect(popItems(menu).length).toBe(3);
78
+ });
79
+
80
+ it('still populates from static literal children (no regression)', async () => {
81
+ const menu = document.createElement('menu-ui');
82
+ menu.innerHTML = `
83
+ <button-ui slot="trigger" text="Open"></button-ui>
84
+ <menu-item-ui text="Edit" value="edit"></menu-item-ui>
85
+ <menu-divider-ui></menu-divider-ui>
86
+ <menu-item-ui text="Delete" value="delete"></menu-item-ui>`;
87
+ host.appendChild(menu);
88
+ await settle();
89
+ menu.open = true;
90
+ await settle();
91
+
92
+ expect(popItems(menu).length).toBe(2);
93
+ const pop = menu.querySelector('[data-menu-popover]');
94
+ expect(pop.querySelectorAll('menu-divider-ui').length).toBe(1);
95
+ });
96
+
97
+ it('does not absorb the trigger into the popover', async () => {
98
+ const menu = document.createElement('menu-ui');
99
+ menu.innerHTML = `
100
+ <button-ui slot="trigger" text="Open"></button-ui>
101
+ <menu-item-ui text="Edit" value="edit"></menu-item-ui>`;
102
+ host.appendChild(menu);
103
+ await settle();
104
+ menu.open = true;
105
+ await settle();
106
+
107
+ const pop = menu.querySelector('[data-menu-popover]');
108
+ expect(pop.querySelector('[slot="trigger"]')).toBeNull();
109
+ expect(menu.querySelector(':scope > [slot="trigger"]')).not.toBeNull();
110
+ });
111
+
112
+ it('does not reorder items on a re-entrant #show() (open → re-render → still open)', async () => {
113
+ const menu = document.createElement('menu-ui');
114
+ menu.innerHTML = `
115
+ <button-ui slot="trigger" text="Open"></button-ui>
116
+ <menu-item-ui text="One" value="1"></menu-item-ui>
117
+ <menu-item-ui text="Two" value="2"></menu-item-ui>`;
118
+ host.appendChild(menu);
119
+ await settle();
120
+ menu.open = true;
121
+ await settle();
122
+ // Force another render() pass while still open (re-invokes #show()).
123
+ menu.placement = 'top-start';
124
+ await settle();
125
+
126
+ const pop = menu.querySelector('[data-menu-popover]');
127
+ const order = [...pop.querySelectorAll('menu-item-ui')].map((el) => el.value);
128
+ expect(order).toEqual(['1', '2']);
129
+ });
130
+ });
@@ -122,7 +122,16 @@ export class UIModal extends UIElement {
122
122
  }
123
123
 
124
124
  #getDuration() {
125
- const raw = getComputedStyle(this).getPropertyValue('--modal-duration').trim();
125
+ // Read the same fallback chain the CSS transitions use —
126
+ // var(--modal-duration, var(--modal-duration-default)). Reading only the
127
+ // public token (unset unless a consumer overrides) made this always fall
128
+ // back to 200ms while the CSS exit animation ran for --modal-duration-default
129
+ // (= --a-duration = 250ms), so the dialog closed 50ms early, clipping the
130
+ // animation. Same class as swiper bug-29 (the OD-5 -default token sweep left
131
+ // JS read-sites pointed at the now-unset public token).
132
+ const cs = getComputedStyle(this);
133
+ const raw = cs.getPropertyValue('--modal-duration').trim()
134
+ || cs.getPropertyValue('--modal-duration-default').trim();
126
135
  return parseFloat(raw) || 200;
127
136
  }
128
137
 
@@ -142,6 +142,12 @@
142
142
  flex: 1;
143
143
  overflow: auto;
144
144
  }
145
+ /* Form controls in the modal body fill the available width (a modal body is
146
+ a form column, not a content-width row) — matches the field/input family's
147
+ full-width behaviour in a real form. */
148
+ :scope [slot="body"] :is(field-ui, input-ui, select-ui, textarea-ui, combobox-ui, tags-input-ui, color-input-ui, slider-ui) {
149
+ width: 100%;
150
+ }
145
151
 
146
152
  /* ═══════ Footer ═══════ */
147
153
  :scope [slot="footer"] {
@@ -153,4 +159,7 @@
153
159
  border-top: 1px solid var(--modal-border, var(--modal-border-default));
154
160
  flex-shrink: 0;
155
161
  }
162
+ /* No content → no footer chrome: an empty footer slot otherwise renders a
163
+ dead band (border-top + padding) under the body. */
164
+ :scope [slot="footer"]:empty { display: none; }
156
165
  }
@@ -114,6 +114,9 @@
114
114
  "default": {
115
115
  "description": "Spillover content revealed only when the card is checked — typically a follow-up form field (e.g. a textarea on an \"Other\" option, conditional inputs that depend on the selection). Aligns with the heading/description column; hidden via `display: none` when not checked."
116
116
  },
117
+ "action": {
118
+ "description": "CTA slot for `layout=\"tile\"` — a button-ui anchored to the card's bottom edge and stretched full-width, so a row of tile cards keeps their CTAs aligned regardless of body length (plan-picker-style pickers)."
119
+ },
117
120
  "heading": {
118
121
  "description": "Rich heading content. Overrides the `heading` attribute when present."
119
122
  },
@@ -173,34 +173,59 @@ option-card-ui[checked] [slot="icon"] {
173
173
  description below, all left-aligned. Used for hero pickers
174
174
  (data source, role, plan tiles) where the icon is a primary
175
175
  brand cue rather than secondary chrome. ── */
176
+ /* Tile layout — a vertical content stack (icon → heading → description →
177
+ body → action) with the radio indicator pinned top-right OUT of flow.
178
+ Flex (not grid) so content flows in DOM order with no reserved/empty
179
+ tracks: a no-icon plan card has no dead left column, and a slotted CTA
180
+ (slot="action") anchors to the card bottom regardless of how much body
181
+ content (price / feature list) precedes it. (Plan Picker option-card grid) */
176
182
  :scope[layout="tile"] {
177
- grid-template-columns: minmax(0, 1fr) auto;
178
- grid-template-areas:
179
- "icon indicator"
180
- "heading heading"
181
- "description description";
182
- column-gap: var(--option-card-gap-x, var(--option-card-gap-x-default));
183
- row-gap: var(--option-card-gap-y, var(--option-card-gap-y-default));
184
- padding: var(--a-space-4);
185
- align-items: start;
183
+ display: flex;
184
+ flex-direction: column;
185
+ gap: var(--option-card-gap-y, var(--option-card-gap-y-default));
186
+ position: relative;
187
+ /* padding inherited from base :scope (token-driven, --option-card-padding-*) */
188
+ }
189
+ /* Radio pinned to the card's top-right corner, OUT of flow. Clear the
190
+ base grid-area:indicator so the absolute containing block is the card
191
+ itself (position:relative) rather than a collapsed grid cell — otherwise
192
+ a grid-forcing host (plan-picker-ui) strands the indicator in an 8px
193
+ gutter and inset-inline-end resolves negative. Inset by the active
194
+ padding token so the dot aligns with the content edge. */
195
+ :scope[layout="tile"]::before {
196
+ position: absolute;
197
+ grid-area: auto;
198
+ inset-block-start: var(--option-card-padding-block, var(--option-card-padding-block-default));
199
+ inset-inline-end: var(--option-card-padding-inline, var(--option-card-padding-inline-default));
200
+ margin-block-start: 0;
186
201
  }
202
+ /* Visual order is fixed via `order`, NOT DOM order: composites
203
+ (plan-picker-ui) append default-slot body content BEFORE option-card
204
+ stamps the heading/description spans on connect, so a flex stack that
205
+ trusted DOM order would render the body above the title. */
187
206
  :scope[layout="tile"] [slot="icon"] {
188
- grid-area: icon;
189
- justify-self: start;
207
+ order: 1;
190
208
  align-self: start;
191
209
  --option-card-icon-size-default: 1.75rem;
192
210
  }
193
- :scope[layout="tile"]::before {
194
- grid-area: indicator;
195
- align-self: start;
196
- justify-self: end;
197
- margin-block-start: 0;
198
- }
199
211
  :scope[layout="tile"] [slot="heading"] {
200
- margin-block-start: var(--a-space-2);
212
+ order: 2;
213
+ /* Reserve the top-right radio's footprint so a long heading doesn't run
214
+ under it. */
215
+ padding-inline-end: var(--a-space-6);
216
+ }
217
+ :scope[layout="tile"] [slot="description"] {
218
+ order: 3;
201
219
  }
202
220
  :scope[layout="tile"] > :not([slot]) {
203
- grid-column: 1 / -1;
221
+ order: 4;
222
+ }
223
+ /* CTA anchors to the card bottom (auto top-margin) + full-width, so a row of
224
+ plan cards with different body lengths keeps their buttons aligned. */
225
+ :scope[layout="tile"] [slot="action"] {
226
+ order: 5;
227
+ width: 100%;
228
+ margin-block-start: auto;
204
229
  }
205
230
 
206
231
  /* ── State: disabled ── */
@@ -76,6 +76,11 @@ slots:
76
76
  description: Rich description content. Overrides the `description` attribute when present.
77
77
  icon:
78
78
  description: Custom icon element. Overrides the `icon` attribute when present.
79
+ action:
80
+ description: >-
81
+ CTA slot for `layout="tile"` — a button-ui anchored to the card's bottom
82
+ edge and stretched full-width, so a row of tile cards keeps their CTAs
83
+ aligned regardless of body length (plan-picker-style pickers).
79
84
  default:
80
85
  description: >-
81
86
  Spillover content revealed only when the card is checked — typically a
@@ -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
  }
@@ -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
  },