@adia-ai/web-components 0.0.20 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -199,7 +199,7 @@ the `streams` registry export from `core/data-stream.js`.
199
199
 
200
200
  Implementation: `core/data-stream.js` (~360 lines). Full
201
201
  attribute table + live demos:
202
- [`/site/components/chart#data-stream`](./site/pages/components/chart/index.html).
202
+ [`/site/components/chart#data-stream`](../../site/pages/components/chart/index.html).
203
203
 
204
204
  ## Build
205
205
 
@@ -371,9 +371,12 @@ class AdiaAgentReasoning extends AdiaElement {
371
371
  #makeStepItem(step) {
372
372
  const item = document.createElement('timeline-item-ui');
373
373
  item.setAttribute('text', step.label || '');
374
- if (step.status === 'done') item.setAttribute('completed', '');
375
- if (step.status === 'error') item.setAttribute('error', '');
376
- if (step.status === 'active') { item.setAttribute('active', ''); item.setAttribute('spinner', ''); }
374
+ // Map step.status canonical timeline-item-ui [status] enum.
375
+ // The legacy [completed]/[active]/[error] Booleans were removed
376
+ // in the Phase 6 cut (2026-04-27); only [status] is honoured.
377
+ if (step.status === 'done') item.setAttribute('status', 'completed');
378
+ if (step.status === 'error') item.setAttribute('status', 'error');
379
+ if (step.status === 'active') { item.setAttribute('status', 'active'); item.setAttribute('spinner', ''); }
377
380
 
378
381
  const duration = step.durationLabel
379
382
  || (step.duration != null ? formatDuration(step.duration) : '');
@@ -5,8 +5,6 @@
5
5
  --alert-fg: var(--a-fg);
6
6
  --alert-border: var(--a-border-subtle);
7
7
  --alert-icon-fg: var(--a-fg-muted);
8
- --alert-close-fg: var(--a-fg-muted);
9
- --alert-close-fg-hover: var(--a-fg);
10
8
 
11
9
  /* ── Layout ── */
12
10
  --alert-radius: var(--a-radius-md);
@@ -15,19 +13,25 @@
15
13
  --alert-gap: var(--a-space-2);
16
14
 
17
15
  /* ── Typography ── */
18
- --alert-font: var(--a-ui-size);
16
+ --alert-font: var(--a-ui-size);
17
+ --alert-line-height: var(--a-ui-line-height, 1.5);
19
18
  }
20
19
 
21
20
  :scope {
22
21
  /* ── Base ── */
23
22
  box-sizing: border-box;
24
23
  display: flex;
25
- align-items: center;
24
+ /* `align-items: start` so the leading icon and close button anchor
25
+ to the top — keeps the icon visually paired with the first line of
26
+ text on multi-line alerts. The leading slot's own height = body
27
+ line-height (below) means the icon optically centers on line 1. */
28
+ align-items: start;
26
29
  gap: var(--alert-gap);
27
30
  padding: var(--alert-py) var(--alert-px);
28
31
  border: 1px solid var(--alert-border);
29
32
  border-radius: var(--alert-radius);
30
33
  font-size: var(--alert-font);
34
+ line-height: var(--alert-line-height);
31
35
  color: var(--alert-fg);
32
36
  background: var(--alert-bg);
33
37
  }
@@ -65,6 +69,11 @@
65
69
  :scope [slot="leading"] {
66
70
  flex-shrink: 0;
67
71
  color: var(--alert-icon-fg);
72
+ /* Box height = one line of body so the icon optical-centers on the
73
+ first line under `align-items: start`. */
74
+ display: inline-flex;
75
+ align-items: center;
76
+ min-height: calc(var(--alert-font) * var(--alert-line-height));
68
77
  /* `ensure()` appends the leading-slot icon to the host, which
69
78
  puts it after any consumer-provided content in DOM order.
70
79
  Force it to the visual lead via flex `order` so the icon
@@ -77,17 +86,12 @@
77
86
  min-width: 0;
78
87
  }
79
88
 
89
+ /* Close button is a `<button-ui icon="x" variant="ghost" size="sm">`
90
+ stamped by alert.js — it brings its own focus ring, hover state,
91
+ and transitions. Box height matches the leading slot so X and icon
92
+ sit on the same first-line baseline. */
80
93
  :scope [slot="close"] {
81
- background: none;
82
- border: none;
83
- cursor: pointer;
84
- color: var(--alert-close-fg);
85
- padding: 0;
86
- line-height: 1;
87
- font-size: 1rem;
88
- }
89
-
90
- :scope [slot="close"]:hover {
91
- color: var(--alert-close-fg-hover);
94
+ flex-shrink: 0;
95
+ min-height: calc(var(--alert-font) * var(--alert-line-height));
92
96
  }
93
97
  }
@@ -24,22 +24,19 @@ class AdiaAlert extends AdiaElement {
24
24
  static parts = {
25
25
  leading: '<icon-ui slot="leading"></icon-ui>',
26
26
  content: '<span slot="content"></span>',
27
- close: '<button slot="close" type="button" aria-label="Close">\u00d7</button>',
27
+ /* Close affordance is a real button-ui \u2014 inherits the system focus
28
+ ring, hover transition, and icon sizing automatically. Listen for
29
+ the canonical `press` event (button-ui dispatches on click + Enter
30
+ + Space, no extra keydown wiring needed). */
31
+ close: '<button-ui slot="close" icon="x" variant="ghost" size="sm" aria-label="Close"></button-ui>',
28
32
  };
29
33
 
30
34
  static template = () => null;
31
35
 
32
- #onClick = (e) => {
36
+ #onPress = (e) => {
33
37
  if (e.target.closest('[slot="close"]')) this.#close();
34
38
  };
35
39
 
36
- #onKeydown = (e) => {
37
- if (e.target.closest('[slot="close"]') && (e.key === 'Enter' || e.key === ' ')) {
38
- e.preventDefault();
39
- this.#close();
40
- }
41
- };
42
-
43
40
  connected() {
44
41
  this.#updateRole();
45
42
 
@@ -48,13 +45,11 @@ class AdiaAlert extends AdiaElement {
48
45
  this.ensure('content');
49
46
  if (this.closable) this.ensure('close');
50
47
 
51
- this.addEventListener('click', this.#onClick);
52
- this.addEventListener('keydown', this.#onKeydown);
48
+ this.addEventListener('press', this.#onPress);
53
49
  }
54
50
 
55
51
  disconnected() {
56
- this.removeEventListener('click', this.#onClick);
57
- this.removeEventListener('keydown', this.#onKeydown);
52
+ this.removeEventListener('press', this.#onPress);
58
53
  }
59
54
 
60
55
  render() {
@@ -1,12 +1,13 @@
1
1
  /**
2
- * <avatar-ui src="photo.jpg" name="Alice Smith" size="md" shape="circle"></avatar-ui>
2
+ * <avatar-ui src="photo.jpg" text="Alice Smith" size="md" shape="circle"></avatar-ui>
3
3
  *
4
4
  * Fallback chain: image → initials → empty.
5
- * If src fails to load, falls back to initials derived from name.
5
+ * If src fails to load, falls back to initials derived from text.
6
+ * `name` is a deprecated alias for `text` (warns on use).
6
7
  *
7
8
  * <avatar-group-ui max="4" size="md">
8
- * <avatar-ui name="Alice"></avatar-ui>
9
- * <avatar-ui name="Bob"></avatar-ui>
9
+ * <avatar-ui text="Alice"></avatar-ui>
10
+ * <avatar-ui text="Bob"></avatar-ui>
10
11
  * ...
11
12
  * </avatar-group-ui>
12
13
  *
@@ -61,6 +61,7 @@
61
61
  box-shadow: var(--card-shadow);
62
62
  overflow: hidden;
63
63
  corner-shape: superellipse(1.1);
64
+ width: stretch;
64
65
  }
65
66
 
66
67
  /* ═══════ Variants — token-only overrides ═══════ */
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Standalone legend primitive for the AdiaUI chart family. Renders a row
5
5
  * of badge-ui chips (swatch + label) that are keyboard-focusable and
6
- * click-toggleable — clicking emits a `legend-toggle` event that consumers
7
- * (or chart-ui via [for]) wire to series visibility.
6
+ * click-toggleable — clicking emits a canonical `toggle` event that
7
+ * consumers (or chart-ui via [for]) wire to series visibility.
8
8
  *
9
9
  * Attributes:
10
10
  * for — id-ref of a chart-ui / heatmap-ui to mirror. When set, the
@@ -23,12 +23,12 @@
23
23
  * static — when set, rows render as non-interactive <span>s. Default
24
24
  * is interactive <button>s that toggle.
25
25
  * on-toggle — hide | opacity. Default hide. Escape hatch per OD-CHART-09.
26
- * Reported via legend-toggle event detail; chart-ui reads it
26
+ * Reported via the `toggle` event detail; chart-ui reads it
27
27
  * when wired via [for].
28
28
  *
29
29
  * Events:
30
- * legend-toggle — detail: {key, active, mode}. Fires on row click when
31
- * not [static]. `active` is the new state (true=visible).
30
+ * toggle — detail: {key, active, mode}. Fires on row click when
31
+ * not [static]. `active` is the new state (true=visible).
32
32
  */
33
33
 
34
34
  import { AdiaElement } from '../../core/element.js';
@@ -12,6 +12,9 @@
12
12
  gap: var(--col-gap);
13
13
  justify-content: var(--col-justify);
14
14
  align-items: var(--col-align);
15
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
16
+ padding: var(--a-padding, 0);
17
+ margin: var(--a-margin, 0);
15
18
  /* Neutralize legacy UA rule that maps HTML `align=` attr to `text-align`.
16
19
  Our [align="center"] sets flex cross-axis alignment, not text alignment. */
17
20
  text-align: inherit;
@@ -17,19 +17,15 @@
17
17
  }
18
18
 
19
19
  :scope {
20
- /* ── Base ── */
20
+ /* ── Base ── stacked layout (term above desc, single column). */
21
21
  box-sizing: border-box;
22
22
  display: grid;
23
+ grid-template-columns: 1fr;
23
24
  gap: var(--description-list-gap-row) var(--description-list-gap-column);
24
25
  margin: 0;
25
26
  padding: 0;
26
27
  }
27
28
 
28
- /* Default stacked: term above desc. Each pair in its own implicit row. */
29
- :scope {
30
- grid-template-columns: 1fr;
31
- }
32
-
33
29
  [data-dl-term],
34
30
  dt {
35
31
  margin: 0;
@@ -48,9 +44,13 @@
48
44
  line-height: 1.4;
49
45
  }
50
46
 
51
- /* Separate pairs with extra gap when stacked */
52
- :scope[layout="stacked"] [data-dl-desc]:not(:last-child),
53
- :scope[layout="stacked"] dd:not(:last-child) {
47
+ /* Pair separator: extra margin under each non-last dd so DD→DT (between
48
+ pairs) reads as a wider gap than DT→DD (within a pair). Match anything
49
+ that isn't `[layout="inline"]` so the default case (no attribute set —
50
+ `layout` defaults to "stacked" but only reflects when explicitly set)
51
+ and the explicit `[layout="stacked"]` both get the same rhythm. */
52
+ :scope:not([layout="inline"]) [data-dl-desc]:not(:last-child),
53
+ :scope:not([layout="inline"]) dd:not(:last-child) {
54
54
  margin-bottom: var(--description-list-gap-row);
55
55
  }
56
56
 
@@ -1,6 +1,8 @@
1
1
  @scope (grid-ui) {
2
2
  :where(:scope) {
3
- --grid-gap: var(--a-gap-md);
3
+ /* Read `--a-gap` so the universal `[gap="N"]` rules (tokens.css)
4
+ reach the grid; defaults to `--a-gap-md` at :root. */
5
+ --grid-gap: var(--a-gap);
4
6
  --grid-column-gap: var(--grid-gap);
5
7
  --grid-row-gap: var(--grid-gap);
6
8
  }
@@ -13,6 +15,9 @@
13
15
  grid-auto-columns: 1fr;
14
16
  column-gap: var(--grid-column-gap);
15
17
  row-gap: var(--grid-row-gap);
18
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
19
+ padding: var(--a-padding, 0);
20
+ margin: var(--a-margin, 0);
16
21
  }
17
22
 
18
23
  /* Explicit column count — switches to template mode */
@@ -69,7 +69,13 @@
69
69
  baseline centered regardless of line-height. */
70
70
  line-height: 1.4;
71
71
  cursor: text;
72
- transition: border-color var(--input-duration) var(--input-easing);
72
+ /* Cover every property the hover / focus / invalid states change so
73
+ the field doesn't half-snap (border slides, but bg/colour/ring snap). */
74
+ transition:
75
+ background var(--input-duration) var(--input-easing),
76
+ border-color var(--input-duration) var(--input-easing),
77
+ color var(--input-duration) var(--input-easing),
78
+ box-shadow var(--input-duration) var(--input-easing);
73
79
  }
74
80
  :scope:not([disabled]) [slot="field"]:hover {
75
81
  background: var(--input-bg-hover);
@@ -57,6 +57,14 @@
57
57
  border-inline-start: 2px solid transparent;
58
58
  padding-inline-start: var(--a-space-2);
59
59
  outline: none;
60
+ transition:
61
+ background var(--a-duration-fast) var(--a-easing),
62
+ color var(--a-duration-fast) var(--a-easing),
63
+ border-color var(--a-duration-fast) var(--a-easing);
64
+ }
65
+
66
+ :scope[selectable] > [role="listitem"]:hover {
67
+ background: var(--a-bg-hover);
60
68
  }
61
69
 
62
70
  :scope[selectable] > [role="listitem"][aria-selected="true"] {
@@ -143,11 +143,47 @@ class AdiaModal extends AdiaElement {
143
143
  this.drop('close');
144
144
  }
145
145
 
146
- const body = this.ensure('body');
147
- if (body.parentElement !== panel) panel.appendChild(body);
146
+ // Body — accept bare `<section>` tags, explicit `[slot="body"]`, OR
147
+ // unslotted light-DOM children (so `<modal-ui><p>…</p></modal-ui>` works
148
+ // with no markup ceremony). Mirrors drawer-ui's authoring contract.
149
+ const authorBody = [...this.children].find(c =>
150
+ c.getAttribute('slot') === 'body' || (!c.getAttribute('slot') && c.localName === 'section')
151
+ );
152
+ const looseChildren = [...this.children].filter(c =>
153
+ !c.getAttribute('slot') && c.localName !== 'section' && c.localName !== 'dialog' && c.localName !== 'footer'
154
+ );
155
+
156
+ let body;
157
+ if (authorBody) {
158
+ if (authorBody.getAttribute('slot') !== 'body') authorBody.setAttribute('slot', 'body');
159
+ if (authorBody.parentElement !== panel) panel.appendChild(authorBody);
160
+ panel.querySelector(':scope > [slot="body"][data-stamped]')?.remove();
161
+ body = authorBody;
162
+ } else {
163
+ body = this.ensure('body');
164
+ body.setAttribute('data-stamped', '');
165
+ if (body.parentElement !== panel) panel.appendChild(body);
166
+ }
167
+
168
+ // Migrate any remaining unslotted light-DOM children into the body so
169
+ // `<modal-ui><p>…</p></modal-ui>` lands in the visible region.
170
+ for (const child of looseChildren) {
171
+ if (child !== body) body.appendChild(child);
172
+ }
148
173
 
149
- const footer = this.ensure('footer');
150
- if (footer.parentElement !== panel) panel.appendChild(footer);
174
+ // Footer — opt-in via [slot="footer"] or bare <footer> tag (card-ui style).
175
+ const userFooter = [...this.children].find(c =>
176
+ c.getAttribute('slot') === 'footer' || (!c.getAttribute('slot') && c.localName === 'footer')
177
+ );
178
+ if (userFooter) {
179
+ if (userFooter.getAttribute('slot') !== 'footer') userFooter.setAttribute('slot', 'footer');
180
+ if (userFooter.parentElement !== panel) panel.appendChild(userFooter);
181
+ panel.querySelector(':scope > [slot="footer"][data-stamped]')?.remove();
182
+ } else {
183
+ const footer = this.ensure('footer');
184
+ footer.setAttribute('data-stamped', '');
185
+ if (footer.parentElement !== panel) panel.appendChild(footer);
186
+ }
151
187
 
152
188
  // Sync open state
153
189
  if (this.open && !dialog.open) {
@@ -7,9 +7,11 @@ import { AdiaElement } from '../../core/element.js';
7
7
  * an expandable history log. Auto-collapses when the pipeline finishes.
8
8
  *
9
9
  * Properties:
10
- * stage : String — current stage name (interpret, analyze, plan, generate, validate, render)
11
- * message : String — current stage description
12
- * complete : Booleanwhen true, shows final state and collapses history
10
+ * stage : String — current stage name (interpret, analyze, plan, generate, validate, render)
11
+ * message : String — current stage description
12
+ * status : String'idle' | 'active' | 'completed' | 'error'. Set to
13
+ * 'completed' to render the final state and collapse
14
+ * the history log.
13
15
  */
14
16
 
15
17
  const STAGE_LABELS = {
@@ -37,6 +37,14 @@
37
37
  border-radius: var(--progress-radius);
38
38
  background: var(--progress-fill);
39
39
  transition: width var(--progress-duration) var(--progress-easing);
40
+
41
+ /* Enter animation: paint the fill at 0 on first frame, then transition
42
+ to the inline width set by JS — so a freshly-mounted determinate bar
43
+ animates 0 → value rather than snapping in (or, in some cascades,
44
+ briefly painting at the indeterminate width:100% before settling). */
45
+ @starting-style {
46
+ width: 0;
47
+ }
40
48
  }
41
49
 
42
50
  /* Indeterminate bar shimmer */
@@ -53,6 +53,15 @@ class AdiaProgressRow extends AdiaElement {
53
53
  this.#progressEl = this.querySelector(':scope > progress-ui');
54
54
  if (!this.#progressEl) {
55
55
  this.#progressEl = document.createElement('progress-ui');
56
+ // Pre-set value BEFORE append so progress-ui's first paint is
57
+ // determinate at the target value — `@starting-style { width: 0 }`
58
+ // in progress.css then transitions 0 → value on mount. Without this,
59
+ // the bar mounts indeterminate (width: 100% from the shimmer rule),
60
+ // and `render()` setting value: N triggers a backwards 100% → N
61
+ // animation, which reads as the bar shrinking right-to-left.
62
+ if (this.value != null && this.value >= 0) {
63
+ this.#progressEl.setAttribute('value', String(this.value));
64
+ }
56
65
  this.appendChild(this.#progressEl);
57
66
  }
58
67
  }
@@ -128,6 +128,29 @@
128
128
  margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
129
129
  }
130
130
 
131
+ [data-richtext-body] h5 {
132
+ font-size: var(--richtext-body-size);
133
+ font-weight: var(--richtext-h4-weight);
134
+ color: var(--richtext-fg-muted);
135
+ text-transform: uppercase;
136
+ letter-spacing: 0.04em;
137
+ margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
138
+ }
139
+
140
+ [data-richtext-body] h6 {
141
+ font-size: var(--richtext-code-size);
142
+ font-weight: var(--richtext-h4-weight);
143
+ color: var(--richtext-fg-muted);
144
+ text-transform: uppercase;
145
+ letter-spacing: 0.06em;
146
+ margin: var(--richtext-h4-margin-top) 0 var(--richtext-h4-margin-bottom);
147
+ }
148
+
149
+ /* First-element rhythm reset — first heading shouldn't push content
150
+ down with its top margin (already controlled at the body inset). */
151
+ [data-richtext-body] > :first-child { margin-top: 0; }
152
+ [data-richtext-body] > :last-child { margin-bottom: 0; }
153
+
131
154
  /* ── Body text ── */
132
155
 
133
156
  [data-richtext-body] p {
@@ -180,6 +203,112 @@
180
203
  margin-bottom: var(--richtext-li-margin-bottom);
181
204
  }
182
205
 
206
+ /* Nested lists — collapse the vertical block margin so children
207
+ don't add a full paragraph gap before/after the parent <li>. */
208
+ [data-richtext-body] li > ul,
209
+ [data-richtext-body] li > ol {
210
+ margin-block: var(--a-space-1);
211
+ }
212
+
213
+ /* ── Definition lists ── */
214
+ [data-richtext-body] dl {
215
+ margin: var(--richtext-list-my) 0;
216
+ }
217
+ [data-richtext-body] dt {
218
+ font-weight: var(--richtext-strong-weight);
219
+ color: var(--richtext-fg);
220
+ margin-top: var(--a-space-2);
221
+ }
222
+ [data-richtext-body] dd {
223
+ margin: 0 0 var(--richtext-li-margin-bottom) var(--richtext-list-indent);
224
+ color: var(--richtext-fg-muted);
225
+ }
226
+
227
+ /* ── Blockquote ── */
228
+ [data-richtext-body] blockquote {
229
+ margin: var(--richtext-block-my) 0;
230
+ padding: var(--a-space-1) var(--a-space-4);
231
+ border-inline-start: 3px solid var(--richtext-border);
232
+ color: var(--richtext-fg-muted);
233
+ font-style: italic;
234
+ }
235
+ [data-richtext-body] blockquote > :last-child { margin-bottom: 0; }
236
+
237
+ /* ── Inline emphasis & semantic glyphs ── */
238
+ [data-richtext-body] mark {
239
+ background: var(--a-warning-muted, var(--richtext-code-bg));
240
+ color: inherit;
241
+ padding: 0 0.2em;
242
+ border-radius: var(--richtext-code-radius);
243
+ }
244
+ [data-richtext-body] del,
245
+ [data-richtext-body] s {
246
+ color: var(--richtext-fg-muted);
247
+ text-decoration: line-through;
248
+ }
249
+ [data-richtext-body] kbd {
250
+ font-family: var(--richtext-font-code);
251
+ font-size: 0.85em;
252
+ background: var(--richtext-code-bg);
253
+ border: 1px solid var(--richtext-border);
254
+ border-bottom-width: 2px;
255
+ border-radius: var(--richtext-code-radius);
256
+ padding: 0.05em 0.4em;
257
+ box-shadow: inset 0 -1px 0 var(--richtext-border);
258
+ white-space: nowrap;
259
+ }
260
+ [data-richtext-body] sup,
261
+ [data-richtext-body] sub {
262
+ font-size: 0.75em;
263
+ line-height: 0;
264
+ }
265
+ [data-richtext-body] abbr[title] {
266
+ text-decoration: underline dotted;
267
+ text-underline-offset: 2px;
268
+ cursor: help;
269
+ }
270
+
271
+ /* ── Media ── */
272
+ [data-richtext-body] img,
273
+ [data-richtext-body] video {
274
+ max-width: 100%;
275
+ height: auto;
276
+ display: block;
277
+ margin: var(--richtext-block-my) auto;
278
+ border-radius: var(--richtext-pre-radius);
279
+ }
280
+ [data-richtext-body] figure {
281
+ margin: var(--richtext-block-my) 0;
282
+ }
283
+ [data-richtext-body] figcaption {
284
+ font-size: var(--richtext-code-size);
285
+ color: var(--richtext-fg-muted);
286
+ text-align: center;
287
+ margin-top: var(--a-space-1);
288
+ }
289
+
290
+ /* ── Disclosure ── */
291
+ [data-richtext-body] details {
292
+ margin: var(--richtext-block-my) 0;
293
+ border: 1px solid var(--richtext-border);
294
+ border-radius: var(--richtext-pre-radius);
295
+ padding: var(--a-space-3) var(--a-space-4);
296
+ }
297
+ [data-richtext-body] details[open] {
298
+ background: var(--richtext-code-bg);
299
+ }
300
+ [data-richtext-body] summary {
301
+ cursor: pointer;
302
+ font-weight: var(--richtext-strong-weight);
303
+ margin: calc(-1 * var(--a-space-3)) calc(-1 * var(--a-space-4));
304
+ padding: var(--a-space-3) var(--a-space-4);
305
+ list-style-position: inside;
306
+ }
307
+ [data-richtext-body] details[open] summary {
308
+ margin-bottom: var(--a-space-2);
309
+ border-bottom: 1px solid var(--richtext-border);
310
+ }
311
+
183
312
  /* ── Tables ── */
184
313
 
185
314
  [data-richtext-body] table {
@@ -217,9 +346,18 @@
217
346
  color: var(--richtext-link);
218
347
  text-decoration: underline;
219
348
  text-underline-offset: 2px;
349
+ transition: color var(--a-duration-fast) var(--a-easing);
220
350
  }
221
351
 
222
352
  [data-richtext-body] a:hover {
223
353
  color: var(--richtext-link-hover);
224
354
  }
355
+
356
+ [data-richtext-body] a:focus-visible {
357
+ outline: var(--a-focus-ring);
358
+ outline-offset: 2px;
359
+ border-radius: var(--a-radius-sm);
360
+ }
361
+
362
+ [data-richtext-body] small { font-size: var(--richtext-code-size); color: var(--richtext-fg-muted); }
225
363
  }
@@ -1,6 +1,9 @@
1
1
  @scope (row-ui) {
2
2
  :where(:scope) {
3
- --row-gap: var(--a-gap-md);
3
+ /* `--a-gap` defaults to `--a-gap-md` at :root in tokens.css; the
4
+ universal `[gap="N"]` rules override it on the element, so reading
5
+ `--a-gap` here picks up `<row-ui gap="…">` automatically. */
6
+ --row-gap: var(--a-gap);
4
7
  --row-justify: flex-start;
5
8
  --row-align: center;
6
9
  --row-drag-bg-active: var(--a-accent-muted);
@@ -13,6 +16,9 @@
13
16
  gap: var(--row-gap);
14
17
  justify-content: var(--row-justify);
15
18
  align-items: var(--row-align);
19
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
20
+ padding: var(--a-padding, 0);
21
+ margin: var(--a-margin, 0);
16
22
  text-align: inherit;
17
23
  }
18
24
 
@@ -84,9 +84,12 @@
84
84
  background: var(--select-bg);
85
85
  line-height: 1;
86
86
  cursor: pointer;
87
+ /* Match every property the hover / focus / invalid states change. */
87
88
  transition:
89
+ background var(--select-duration) var(--select-easing),
88
90
  border-color var(--select-duration) var(--select-easing),
89
- background var(--select-duration) var(--select-easing);
91
+ color var(--select-duration) var(--select-easing),
92
+ box-shadow var(--select-duration) var(--select-easing);
90
93
  }
91
94
  [slot="trigger"]:hover {
92
95
  border-color: var(--select-border-hover);
@@ -7,6 +7,9 @@
7
7
  box-sizing: border-box;
8
8
  display: grid;
9
9
  place-items: var(--stack-align);
10
+ /* Universal [padding] / [margin] opt-in — see tokens.css for scale. */
11
+ padding: var(--a-padding, 0);
12
+ margin: var(--a-margin, 0);
10
13
  /* Same UA override as row-ui/col-ui: [align=...] is for flex/grid, not text. */
11
14
  text-align: inherit;
12
15
  }
@@ -32,10 +32,17 @@
32
32
  --stepper-line-done: var(--a-accent);
33
33
  --stepper-line-size: 2px;
34
34
 
35
- /* ── Layout ── */
35
+ /* ── Layout ──
36
+ Parent tokens use the bare `--stepper-*` name (no `-item-` infix)
37
+ so the child's `:where(stepper-item-ui)` block can pull from
38
+ parent → child via `var(--stepper-X, …)` without name collisions.
39
+ A self-named child declaration like
40
+ `--stepper-item-gap-sm: var(--stepper-item-gap-sm, …)` resolves
41
+ to invalid (cycle), which silently breaks any calc() that
42
+ consumes it. */
36
43
  --stepper-radius-full: var(--a-radius-full);
37
- --stepper-item-gap: var(--a-space-3);
38
- --stepper-item-gap-sm: var(--a-space-2);
44
+ --stepper-gap: var(--a-space-3);
45
+ --stepper-gap-sm: var(--a-space-2);
39
46
  --stepper-pad-x: var(--a-space-4);
40
47
  --stepper-pad-y: var(--a-space-6);
41
48
  --stepper-offset-xs: var(--a-space-1);
@@ -82,7 +89,8 @@
82
89
  --stepper-item-line-size: var(--stepper-line-size, 2px);
83
90
 
84
91
  --stepper-item-radius-full: var(--a-radius-full);
85
- --stepper-item-gap-sm: var(--stepper-item-gap-sm, var(--a-space-2));
92
+ --stepper-item-gap: var(--stepper-gap, var(--a-space-3));
93
+ --stepper-item-gap-sm: var(--stepper-gap-sm, var(--a-space-2));
86
94
  --stepper-item-pad-x: var(--stepper-pad-x, var(--a-space-4));
87
95
  --stepper-item-pad-y: var(--stepper-pad-y, var(--a-space-6));
88
96
  --stepper-item-offset-xs: var(--stepper-offset-xs, var(--a-space-1));
@@ -38,18 +38,18 @@ class AdiaStepper extends AdiaElement {
38
38
  const last = items[items.length - 1];
39
39
  if (last) last.setAttribute('data-last', '');
40
40
 
41
- // Parent drives child state via step index
41
+ // Parent drives child state via step index. Canonical Phase 6
42
+ // contract: `status` enum (`completed` / `active` / `idle`).
43
+ // CSS only matches `[status="…"]`; the legacy `[completed]` /
44
+ // `[active]` Booleans were removed in 0.0.20.
42
45
  items.forEach((item, i) => {
43
46
  item.setAttribute('data-index', i);
44
47
  if (i < this.step) {
45
- item.setAttribute('completed', '');
46
- item.removeAttribute('active');
48
+ item.setAttribute('status', 'completed');
47
49
  } else if (i === this.step) {
48
- item.removeAttribute('completed');
49
- item.setAttribute('active', '');
50
+ item.setAttribute('status', 'active');
50
51
  } else {
51
- item.removeAttribute('completed');
52
- item.removeAttribute('active');
52
+ item.setAttribute('status', 'idle');
53
53
  }
54
54
  });
55
55
  }
@@ -89,6 +89,16 @@
89
89
 
90
90
  :scope > [data-swiper-track]::-webkit-scrollbar { display: none; }
91
91
 
92
+ /* Click-and-drag affordance for cursor users — touch keeps native pan. */
93
+ @media (hover: hover) {
94
+ :scope > [data-swiper-track] { cursor: grab; }
95
+ :scope > [data-swiper-track][data-dragging] {
96
+ cursor: grabbing;
97
+ scroll-snap-type: none; /* let the pointer track 1:1 during drag */
98
+ user-select: none;
99
+ }
100
+ }
101
+
92
102
  /* ── Slides ── */
93
103
 
94
104
  :scope > [data-swiper-track] > * {
@@ -43,6 +43,7 @@ class AdiaSwiper extends AdiaElement {
43
43
  #activeIndex = 0;
44
44
  #bound = false;
45
45
  #fallbackNav = false;
46
+ #drag = null; /* { pointerId, startX, startScrollLeft, hasMoved } */
46
47
 
47
48
  get slides() {
48
49
  if (!this.#track) return [];
@@ -88,6 +89,14 @@ class AdiaSwiper extends AdiaElement {
88
89
 
89
90
  // Keyboard navigation
90
91
  this.addEventListener('keydown', this.#onKeydown);
92
+
93
+ // Click + drag (mouse / pen). Touch keeps native pan via scroll-snap.
94
+ this.#track.addEventListener('pointerdown', this.#onPointerDown);
95
+ this.#track.addEventListener('pointermove', this.#onPointerMove);
96
+ this.#track.addEventListener('pointerup', this.#onPointerUp);
97
+ this.#track.addEventListener('pointercancel', this.#onPointerUp);
98
+ // Capture-phase click suppression so a drag-on-button doesn't activate it.
99
+ this.#track.addEventListener('click', this.#onClickCapture, true);
91
100
  }
92
101
 
93
102
  if (this.autoplay) this.play();
@@ -109,6 +118,14 @@ class AdiaSwiper extends AdiaElement {
109
118
  this.removeEventListener('focusin', this.#onPauseFocus);
110
119
  this.removeEventListener('focusout', this.#onResumeFocus);
111
120
  this.removeEventListener('keydown', this.#onKeydown);
121
+ if (this.#track) {
122
+ this.#track.removeEventListener('pointerdown', this.#onPointerDown);
123
+ this.#track.removeEventListener('pointermove', this.#onPointerMove);
124
+ this.#track.removeEventListener('pointerup', this.#onPointerUp);
125
+ this.#track.removeEventListener('pointercancel', this.#onPointerUp);
126
+ this.#track.removeEventListener('click', this.#onClickCapture, true);
127
+ }
128
+ this.#drag = null;
112
129
  this.#track = null;
113
130
  this.#bound = false;
114
131
  }
@@ -280,6 +297,66 @@ class AdiaSwiper extends AdiaElement {
280
297
  if (e.key === 'ArrowRight') { e.preventDefault(); this.next(); }
281
298
  if (e.key === 'ArrowLeft') { e.preventDefault(); this.prev(); }
282
299
  };
300
+
301
+ // ── Click + drag (mouse / pen) ──
302
+ // Native touch already scroll-pans the track; we only intercept mouse and
303
+ // pen so cursor users get the same swipe affordance as touch users.
304
+
305
+ #DRAG_THRESHOLD_PX = 5;
306
+
307
+ #onPointerDown = (e) => {
308
+ if (e.pointerType === 'touch') return; /* native pan handles touch */
309
+ if (e.button !== 0) return; /* primary button only */
310
+ if (e.target.closest('button, button-ui, a, input, select, textarea')) return;
311
+ this.#drag = {
312
+ pointerId: e.pointerId,
313
+ startX: e.clientX,
314
+ startScrollLeft: this.#track.scrollLeft,
315
+ hasMoved: false,
316
+ };
317
+ /* Disable smooth scroll during drag so the bar tracks the pointer 1:1. */
318
+ this.#track.style.scrollBehavior = 'auto';
319
+ this.#track.setAttribute('data-dragging', '');
320
+ };
321
+
322
+ #onPointerMove = (e) => {
323
+ if (!this.#drag || e.pointerId !== this.#drag.pointerId) return;
324
+ const dx = e.clientX - this.#drag.startX;
325
+ if (Math.abs(dx) >= this.#DRAG_THRESHOLD_PX) {
326
+ if (!this.#drag.hasMoved) {
327
+ this.#drag.hasMoved = true;
328
+ /* Capture pointer so we keep tracking even if it leaves the track. */
329
+ try { this.#track.setPointerCapture(e.pointerId); } catch { /* noop */ }
330
+ }
331
+ this.#track.scrollLeft = this.#drag.startScrollLeft - dx;
332
+ e.preventDefault(); /* suppress text selection while dragging */
333
+ }
334
+ };
335
+
336
+ #onPointerUp = (e) => {
337
+ if (!this.#drag || e.pointerId !== this.#drag.pointerId) return;
338
+ const wasMoved = this.#drag.hasMoved;
339
+ if (wasMoved) {
340
+ try { this.#track.releasePointerCapture(e.pointerId); } catch { /* noop */ }
341
+ }
342
+ this.#drag = null;
343
+ /* Re-enable smooth scrolling; scroll-snap will glide to the nearest slide. */
344
+ this.#track.style.scrollBehavior = '';
345
+ this.#track.removeAttribute('data-dragging');
346
+ /* Ensure dragging stays "active" through the click event that follows
347
+ on the same gesture so #onClickCapture can suppress it. */
348
+ if (wasMoved) {
349
+ this.#track.setAttribute('data-just-dragged', '');
350
+ requestAnimationFrame(() => this.#track?.removeAttribute('data-just-dragged'));
351
+ }
352
+ };
353
+
354
+ #onClickCapture = (e) => {
355
+ if (this.#track?.hasAttribute('data-just-dragged')) {
356
+ e.preventDefault();
357
+ e.stopPropagation();
358
+ }
359
+ };
283
360
  }
284
361
 
285
362
  customElements.define('swiper-ui', AdiaSwiper);
@@ -89,7 +89,9 @@
89
89
  border-radius: var(--switch-radius);
90
90
  background: var(--switch-thumb-bg);
91
91
  box-shadow: var(--switch-thumb-shadow);
92
- transition: transform var(--switch-duration) var(--switch-easing);
92
+ transition:
93
+ transform var(--switch-duration) var(--switch-easing),
94
+ background var(--switch-duration) var(--switch-easing);
93
95
  }
94
96
  :scope[checked] [slot="thumb"] {
95
97
  transform: translateX(var(--switch-thumb-travel));
@@ -1,21 +1,23 @@
1
1
  /**
2
2
  * <timeline-ui> — Vertical (or horizontal) event / progress log.
3
3
  *
4
- * Each <timeline-item-ui> is one entry, with its own state (completed / active
5
- * / error), optional icon, duration, and an expandable outcomes list.
4
+ * Each <timeline-item-ui> is one entry, with its own state via the `status`
5
+ * enum (idle | active | completed | error), optional icon, duration, and an
6
+ * expandable outcomes list. `[spinner]` is an orthogonal presentation hint
7
+ * that animates the dot as a ring when combined with `status="active"`.
6
8
  *
7
9
  * <timeline-ui>
8
- * <timeline-item-ui text="Shipped" time="2h ago" completed></timeline-item-ui>
9
- * <timeline-item-ui text="Delivery" time="now" active spinner></timeline-item-ui>
10
- * <timeline-item-ui text="Received" ></timeline-item-ui>
10
+ * <timeline-item-ui text="Shipped" time="2h ago" status="completed"></timeline-item-ui>
11
+ * <timeline-item-ui text="Delivery" time="now" status="active" spinner></timeline-item-ui>
12
+ * <timeline-item-ui text="Received"></timeline-item-ui>
11
13
  * </timeline-ui>
12
14
  *
13
15
  * For agent reasoning / pipeline views, set size="sm" and use
14
- * duration, outcomes, spinner, error
16
+ * duration, outcomes, spinner, status="error"
15
17
  *
16
18
  * <timeline-ui size="sm">
17
- * <timeline-item-ui text="search" duration="850ms" completed></timeline-item-ui>
18
- * <timeline-item-ui text="generate" active spinner></timeline-item-ui>
19
+ * <timeline-item-ui text="search" duration="850ms" status="completed"></timeline-item-ui>
20
+ * <timeline-item-ui text="generate" status="active" spinner></timeline-item-ui>
19
21
  * </timeline-ui>
20
22
  *
21
23
  * For wizard / numbered-circle patterns, use the dedicated <stepper-ui>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -83,10 +83,10 @@
83
83
  compresses from 10 to 5 step-fractions — visual hierarchy
84
84
  leans more on font-weight + size and less on shade contrast. */
85
85
  --a-canvas-text-strong: light-dark(var(--a-neutral-05-shade), var(--a-neutral-05-tint));
86
- --a-canvas-text: light-dark(var(--a-neutral-30-shade), var(--a-neutral-30-tint));
86
+ --a-canvas-text: light-dark(var(--a-neutral-20-shade), var(--a-neutral-20-tint));
87
87
  --a-canvas-text-subtle: light-dark(var(--a-neutral-35-shade), var(--a-neutral-35-tint));
88
- --a-canvas-text-muted: light-dark(var(--a-neutral-40-shade), var(--a-neutral-40-tint));
89
- --a-canvas-text-disabled: light-dark(var(--a-neutral-60-shade), var(--a-neutral-60-tint));
88
+ --a-canvas-text-muted: light-dark(var(--a-neutral-50-shade), var(--a-neutral-50-tint));
89
+ --a-canvas-text-disabled: light-dark(var(--a-neutral-65-shade), var(--a-neutral-65-tint));
90
90
  --a-canvas-text-inverse: var(--a-neutral-10);
91
91
 
92
92
  /* Luminance-direction surfaces (always darken relative to canvas-0) */
@@ -154,9 +154,9 @@
154
154
  primitive — the naming inverts past the convergence point. Light
155
155
  mode wants dark-on-light, so we pick `tint` (deep accent); dark
156
156
  mode wants light-on-dark, so we pick `shade` (lifted accent). */
157
- --a-link: light-dark(var(--a-accent-65-tint), var(--a-accent-65-shade));
157
+ --a-link: light-dark(var(--a-accent-55-tint), var(--a-accent-55-shade));
158
158
  --a-link-hover: light-dark(var(--a-accent-70-tint), var(--a-accent-70-shade));
159
- --a-link-visited: light-dark(var(--a-accent-75-tint), var(--a-accent-75-shade));
159
+ --a-link-visited: light-dark(var(--a-accent-65-tint), var(--a-accent-65-shade));
160
160
 
161
161
  /* Accent text on accent solid bg — needs light text on dark-ish accent */
162
162
  --a-accent-text-strong: light-dark(var(--a-accent-05-shade), var(--a-accent-05-tint));
package/styles/prose.css CHANGED
@@ -24,9 +24,9 @@
24
24
  --a-inset: var(--a-inset-md);
25
25
 
26
26
  /* ── Gaps — shifted up the space scale ── */
27
- --a-gap-sm: var(--a-space-4);
28
- --a-gap-md: var(--a-space-5);
29
- --a-gap-lg: var(--a-space-6);
27
+ --a-gap-sm: var(--a-space-3);
28
+ --a-gap-md: var(--a-space-4);
29
+ --a-gap-lg: var(--a-space-5);
30
30
  --a-gap: var(--a-gap-md);
31
31
 
32
32
  /* ── Sizing — larger controls ── */
@@ -70,8 +70,8 @@
70
70
 
71
71
  /* body */
72
72
  --a-body-sm: 14px;
73
- --a-body-md: 15px;
74
- --a-body-lg: 16px;
73
+ --a-body-md: 16px;
74
+ --a-body-lg: 18px;
75
75
  --a-body-size: var(--a-body-md);
76
76
 
77
77
  /* deck */
package/styles/tokens.css CHANGED
@@ -327,3 +327,60 @@
327
327
  [gap="lg"] {
328
328
  --a-gap: var(--a-space-8);
329
329
  }
330
+
331
+ /* ── Universal [padding] / [margin] — same scale as [gap]. Layout
332
+ primitives (row-ui, col-ui, grid-ui, stack-ui) opt in by reading
333
+ var(--a-padding, 0) and var(--a-margin, 0). Components with their
334
+ own padding/margin contracts (block-ui, card-ui, drawer-ui) keep
335
+ their scoped declarations and aren't affected.
336
+
337
+ Registered as non-inheriting via @property so a parent's
338
+ [padding="6"] doesn't bleed into nested layout primitives —
339
+ children see the initial-value (0) unless they declare their own
340
+ [padding] / [margin]. */
341
+ @property --a-padding {
342
+ syntax: "<length>";
343
+ inherits: false;
344
+ initial-value: 0;
345
+ }
346
+ @property --a-margin {
347
+ syntax: "<length>";
348
+ inherits: false;
349
+ initial-value: 0;
350
+ }
351
+
352
+ [padding="0"] { --a-padding: 0; }
353
+ [padding="1"] { --a-padding: var(--a-space-1); }
354
+ [padding="2"] { --a-padding: var(--a-space-2); }
355
+ [padding="3"] { --a-padding: var(--a-space-3); }
356
+ [padding="4"] { --a-padding: var(--a-space-4); }
357
+ [padding="5"] { --a-padding: var(--a-space-5); }
358
+ [padding="6"] { --a-padding: var(--a-space-6); }
359
+ [padding="7"] { --a-padding: var(--a-space-7); }
360
+ [padding="8"] { --a-padding: var(--a-space-8); }
361
+ [padding="9"] { --a-padding: var(--a-space-9); }
362
+ [padding="10"] { --a-padding: var(--a-space-10); }
363
+ [padding="12"] { --a-padding: var(--a-space-12); }
364
+ [padding="16"] { --a-padding: var(--a-space-16); }
365
+
366
+ [padding="sm"] { --a-padding: var(--a-space-4); }
367
+ [padding="md"] { --a-padding: var(--a-space-6); }
368
+ [padding="lg"] { --a-padding: var(--a-space-8); }
369
+
370
+ [margin="0"] { --a-margin: 0; }
371
+ [margin="1"] { --a-margin: var(--a-space-1); }
372
+ [margin="2"] { --a-margin: var(--a-space-2); }
373
+ [margin="3"] { --a-margin: var(--a-space-3); }
374
+ [margin="4"] { --a-margin: var(--a-space-4); }
375
+ [margin="5"] { --a-margin: var(--a-space-5); }
376
+ [margin="6"] { --a-margin: var(--a-space-6); }
377
+ [margin="7"] { --a-margin: var(--a-space-7); }
378
+ [margin="8"] { --a-margin: var(--a-space-8); }
379
+ [margin="9"] { --a-margin: var(--a-space-9); }
380
+ [margin="10"] { --a-margin: var(--a-space-10); }
381
+ [margin="12"] { --a-margin: var(--a-space-12); }
382
+ [margin="16"] { --a-margin: var(--a-space-16); }
383
+
384
+ [margin="sm"] { --a-margin: var(--a-space-4); }
385
+ [margin="md"] { --a-margin: var(--a-space-6); }
386
+ [margin="lg"] { --a-margin: var(--a-space-8); }