@adia-ai/web-components 0.0.18 → 0.0.20

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 (90) hide show
  1. package/components/accordion/accordion.css +101 -102
  2. package/components/agent-feedback-bar/agent-feedback-bar.js +8 -8
  3. package/components/agent-questions/agent-questions.css +2 -1
  4. package/components/agent-questions/agent-questions.js +6 -6
  5. package/components/agent-reasoning/agent-reasoning.js +20 -5
  6. package/components/agent-trace/agent-trace.a2ui.json +5 -5
  7. package/components/agent-trace/agent-trace.js +7 -5
  8. package/components/agent-trace/agent-trace.yaml +2 -2
  9. package/components/alert/alert.a2ui.json +1 -2
  10. package/components/alert/alert.css +4 -4
  11. package/components/alert/alert.yaml +1 -2
  12. package/components/avatar/avatar.a2ui.json +3 -3
  13. package/components/avatar/avatar.js +10 -0
  14. package/components/avatar/avatar.yaml +6 -6
  15. package/components/button/button.a2ui.json +14 -2
  16. package/components/button/button.css +19 -2
  17. package/components/button/button.js +1 -0
  18. package/components/button/button.yaml +20 -2
  19. package/components/calendar-picker/calendar-picker.css +2 -1
  20. package/components/calendar-picker/calendar-picker.js +12 -1
  21. package/components/chart/chart.css +11 -11
  22. package/components/chart/chart.js +26 -18
  23. package/components/chart-legend/chart-legend.a2ui.json +2 -2
  24. package/components/chart-legend/chart-legend.js +4 -1
  25. package/components/chart-legend/chart-legend.yaml +2 -2
  26. package/components/chat/chat-input.js +13 -5
  27. package/components/chat/chat.a2ui.json +2 -2
  28. package/components/chat/chat.js +14 -3
  29. package/components/chat/chat.yaml +2 -2
  30. package/components/code/code.css +16 -6
  31. package/components/command/command.js +9 -1
  32. package/components/field/field.a2ui.json +0 -5
  33. package/components/field/field.css +2 -2
  34. package/components/field/field.js +53 -5
  35. package/components/field/field.yaml +5 -8
  36. package/components/heatmap/heatmap.css +32 -23
  37. package/components/input/input.js +30 -1
  38. package/components/kbd/kbd.a2ui.json +5 -1
  39. package/components/kbd/kbd.yaml +5 -1
  40. package/components/menu/menu.css +20 -8
  41. package/components/menu/menu.js +9 -1
  42. package/components/modal/modal.css +101 -108
  43. package/components/noodles/noodles.js +25 -8
  44. package/components/pipeline-status/pipeline-status.css +4 -4
  45. package/components/pipeline-status/pipeline-status.js +6 -4
  46. package/components/popover/popover.js +4 -0
  47. package/components/progress-row/progress-row.a2ui.json +3 -2
  48. package/components/progress-row/progress-row.yaml +2 -1
  49. package/components/range/range.js +7 -0
  50. package/components/richtext/richtext.css +2 -2
  51. package/components/richtext/richtext.js +4 -1
  52. package/components/segment/segment.css +1 -1
  53. package/components/segmented/segmented.js +7 -1
  54. package/components/select/select.css +7 -4
  55. package/components/slider/slider.js +15 -8
  56. package/components/stepper/stepper.css +181 -144
  57. package/components/stepper/stepper.js +5 -2
  58. package/components/swiper/swiper.a2ui.json +3 -3
  59. package/components/swiper/swiper.css +11 -77
  60. package/components/swiper/swiper.js +6 -5
  61. package/components/swiper/swiper.yaml +3 -3
  62. package/components/switch/switch.a2ui.json +8 -1
  63. package/components/switch/switch.yaml +8 -1
  64. package/components/table/table.js +9 -1
  65. package/components/table-toolbar/table-toolbar.a2ui.json +21 -21
  66. package/components/table-toolbar/table-toolbar.css +32 -91
  67. package/components/table-toolbar/table-toolbar.js +219 -86
  68. package/components/table-toolbar/table-toolbar.yaml +21 -12
  69. package/components/tabs/tabs.css +3 -2
  70. package/components/tabs/tabs.js +7 -1
  71. package/components/tag/tag.a2ui.json +2 -2
  72. package/components/tag/tag.yaml +2 -2
  73. package/components/timeline/timeline.css +244 -204
  74. package/components/timeline/timeline.js +1 -3
  75. package/components/toast/toast.a2ui.json +2 -3
  76. package/components/toast/toast.yaml +5 -3
  77. package/components/toolbar/toolbar.css +6 -1
  78. package/components/toolbar/toolbar.js +10 -2
  79. package/components/tooltip/tooltip.css +8 -2
  80. package/components/tooltip/tooltip.js +12 -14
  81. package/components/tree/tree.css +21 -0
  82. package/core/icons.js +14 -0
  83. package/core/polyfills.js +17 -7
  84. package/package.json +1 -1
  85. package/patterns/a2ui-root/a2ui-root.js +21 -14
  86. package/patterns/app-shell/css/app-shell.main.css +30 -1
  87. package/patterns/app-shell/css/app-shell.tokens.css +1 -0
  88. package/patterns/gen-ui/gen-ui.js +1 -1
  89. package/styles/colors/semantics.css +59 -2
  90. package/styles/tokens.css +16 -12
@@ -114,10 +114,27 @@
114
114
  --button-border: var(--button-border-ghost);
115
115
  }
116
116
 
117
- :scope[variant="danger"] {
117
+ /* ── Color (semantic axis) ──
118
+ `[color]` carries semantic intent; `variant` carries visual style.
119
+ Together they compose: `<button-ui variant="solid" color="danger">`
120
+ vs `<button-ui variant="outline" color="danger">`.
121
+ `variant="danger"` is a deprecated alias migrated to color via JS. */
122
+ :scope[color="danger"] {
118
123
  --button-bg: var(--button-bg-danger);
119
124
  --button-fg: var(--button-fg-danger);
120
125
  }
126
+ :scope[color="success"] {
127
+ --button-bg: var(--a-success-strong);
128
+ --button-fg: var(--a-success-fg);
129
+ }
130
+ :scope[color="info"] {
131
+ --button-bg: var(--a-info-strong);
132
+ --button-fg: var(--a-info-fg);
133
+ }
134
+ :scope[color="warning"] {
135
+ --button-bg: var(--a-warning-strong);
136
+ --button-fg: var(--a-warning-fg);
137
+ }
121
138
 
122
139
  /* ── Hover (after variants so it wins on specificity via :hover) ── */
123
140
  :scope:not([disabled]):hover,
@@ -138,7 +155,7 @@
138
155
  --button-border: var(--button-border-hover);
139
156
  }
140
157
 
141
- :scope[variant="danger"]:not([disabled]):hover {
158
+ :scope[color="danger"]:not([disabled]):hover {
142
159
  --button-bg: var(--button-bg-hover);
143
160
  --button-fg: var(--button-fg-hover);
144
161
  --button-border: var(--button-border-hover);
@@ -5,6 +5,7 @@ class AdiaButton extends AdiaElement {
5
5
  static properties = {
6
6
  text: { type: String, default: '', reflect: true },
7
7
  variant: { type: String, default: 'solid', reflect: true },
8
+ color: { type: String, default: '', reflect: true },
8
9
  size: { type: String, default: 'md', reflect: true },
9
10
  disabled: { type: Boolean, default: false, reflect: true },
10
11
  stretch: { type: Boolean, default: false, reflect: true },
@@ -41,7 +41,11 @@ props:
41
41
  type: string
42
42
  default: ""
43
43
  variant:
44
- description: Visual style variant. `default` is the base style; `link` renders the button as underlined text for inline actions.
44
+ description: >-
45
+ Visual style — `solid` (default fill), `outline`, `ghost`, `link`.
46
+ `default` / `primary` are aliases of `solid`. Style is independent
47
+ of semantic intent — to express destructive / success / info /
48
+ warning intent, set [color="…"] alongside.
45
49
  type: string
46
50
  default: solid
47
51
  enum:
@@ -49,12 +53,26 @@ props:
49
53
  - solid
50
54
  - outline
51
55
  - ghost
52
- - danger
53
56
  - primary
54
57
  - secondary
55
58
  - soft
56
59
  - current
57
60
  - link
61
+ color:
62
+ description: >-
63
+ Semantic intent — composes with [variant]. `<button-ui variant="solid" color="danger">`
64
+ = filled destructive action; `<button-ui variant="outline" color="success">`
65
+ = outlined success affordance.
66
+ type: string
67
+ default: ""
68
+ enum:
69
+ - default
70
+ - accent
71
+ - info
72
+ - success
73
+ - warning
74
+ - danger
75
+ reflect: true
58
76
  events:
59
77
  press:
60
78
  description: Fired on complete press (pointer up or Enter/Space)
@@ -72,6 +72,7 @@
72
72
  --calendar-picker-day-today-border: var(--a-accent);
73
73
  --calendar-picker-day-today-dot-size: var(--a-space-0-5);
74
74
  --calendar-picker-day-today-dot-offset: var(--a-space-1);
75
+ --calendar-picker-day-grid-gap: var(--a-space-px); /* 1px hairline between cells */
75
76
  --calendar-picker-day-focus-ring: var(--a-focus-ring);
76
77
 
77
78
  /* Transitions */
@@ -244,7 +245,7 @@ calendar-picker-ui [data-cal-weekdays] span {
244
245
  calendar-picker-ui [data-cal-grid] {
245
246
  display: grid;
246
247
  grid-template-columns: repeat(7, 1fr);
247
- gap: 1px;
248
+ gap: var(--calendar-picker-day-grid-gap);
248
249
  }
249
250
 
250
251
  /* Day buttons */
@@ -59,6 +59,7 @@ class AdiaCalendarPicker extends AdiaFormElement {
59
59
  #viewMonth = new Date().getMonth();
60
60
  #focusedDay = null;
61
61
  #popoverClickBound = false;
62
+ #openRaf = null;
62
63
 
63
64
  #onPopoverClick = (e) => {
64
65
  const target = e.target instanceof Element ? e.target : null;
@@ -121,12 +122,18 @@ class AdiaCalendarPicker extends AdiaFormElement {
121
122
  this.#anchorCleanup = anchorPopover(this.#trigger, this.#popover, {
122
123
  placement: 'bottom-start', gap: 4,
123
124
  });
124
- requestAnimationFrame(() => {
125
+ this.#openRaf = requestAnimationFrame(() => {
126
+ this.#openRaf = null;
127
+ if (!this.isConnected || !this.open) return;
125
128
  document.addEventListener('pointerdown', this.#onOutside, { once: true });
126
129
  });
127
130
  } else {
128
131
  this.#anchorCleanup?.();
129
132
  this.#anchorCleanup = null;
133
+ if (this.#openRaf != null) {
134
+ cancelAnimationFrame(this.#openRaf);
135
+ this.#openRaf = null;
136
+ }
130
137
  this.#popover?.hidePopover?.();
131
138
  document.removeEventListener('pointerdown', this.#onOutside);
132
139
  }
@@ -311,6 +318,10 @@ class AdiaCalendarPicker extends AdiaFormElement {
311
318
 
312
319
  disconnected() {
313
320
  super.disconnected();
321
+ if (this.#openRaf != null) {
322
+ cancelAnimationFrame(this.#openRaf);
323
+ this.#openRaf = null;
324
+ }
314
325
  this.removeEventListener('click', this.#onClick);
315
326
  this.removeEventListener('keydown', this.#onKey);
316
327
  document.removeEventListener('pointerdown', this.#onOutside);
@@ -630,24 +630,24 @@
630
630
  /* ═══════ Color variants ═══════ */
631
631
 
632
632
  :scope[color="success"] {
633
- --chart-bar: var(--a-success);
633
+ --chart-bar: var(--a-success-strong);
634
634
  --chart-bar-hover: var(--a-success-bg-hover);
635
- --chart-line: var(--a-success);
636
- --chart-dot: var(--a-success);
635
+ --chart-line: var(--a-success-strong);
636
+ --chart-dot: var(--a-success-strong);
637
637
  }
638
638
 
639
639
  :scope[color="warning"] {
640
- --chart-bar: var(--a-warning);
640
+ --chart-bar: var(--a-warning-strong);
641
641
  --chart-bar-hover: var(--a-warning-bg-hover);
642
- --chart-line: var(--a-warning);
643
- --chart-dot: var(--a-warning);
642
+ --chart-line: var(--a-warning-strong);
643
+ --chart-dot: var(--a-warning-strong);
644
644
  }
645
645
 
646
646
  :scope[color="danger"] {
647
- --chart-bar: var(--a-danger);
647
+ --chart-bar: var(--a-danger-strong);
648
648
  --chart-bar-hover: var(--a-danger-bg-hover);
649
- --chart-line: var(--a-danger);
650
- --chart-dot: var(--a-danger);
649
+ --chart-line: var(--a-danger-strong);
650
+ --chart-dot: var(--a-danger-strong);
651
651
  }
652
652
 
653
653
  :scope[color="info"] {
@@ -682,8 +682,8 @@
682
682
  ───────────────────────────────────────────────────────────────── */
683
683
 
684
684
  .chart-tooltip-popup {
685
- --chart-tooltip-bg: var(--a-neutral-2-shade);
686
- --chart-tooltip-fg: var(--a-neutral-2-tint);
685
+ --chart-tooltip-bg: var(--a-fg);
686
+ --chart-tooltip-fg: var(--a-bg);
687
687
  --chart-tooltip-px: var(--a-space-2-5);
688
688
  --chart-tooltip-py: var(--a-space-1-5);
689
689
  --chart-tooltip-radius: var(--a-radius-sm);
@@ -196,9 +196,9 @@ class AdiaChart extends AdiaElement {
196
196
  hideValues: { type: Boolean, default: false, reflect: true, attribute: 'hide-values' },
197
197
  radius: { type: Number, default: null, reflect: true },
198
198
  smooth: { type: Number, default: 0.4, reflect: true },
199
- aspect: { type: String, default: 'std', reflect: true }, // std | wide | square | tall (deprecated — OD-CHART-02)
200
- size: { type: String, default: '', reflect: true }, // sm | md | lg
201
- format: { type: String, default: 'abbr', reflect: true }, // abbr | decimal | currency | percent (OD-CHART-03)
199
+ aspect: { type: String, default: 'std', reflect: true },
200
+ size: { type: String, default: '', reflect: true },
201
+ format: { type: String, default: 'abbr', reflect: true },
202
202
  };
203
203
 
204
204
  static template = () => null;
@@ -263,10 +263,11 @@ class AdiaChart extends AdiaElement {
263
263
  if (!this.hasAttribute('role')) this.setAttribute('role', 'img');
264
264
  if (!this.hasAttribute('aria-label')) this.setAttribute('aria-label', this.heading || `${this.type} chart`);
265
265
 
266
- /* Phase 1b listen for legend-toggle events from external
267
- chart-legend-ui[for=self] and hide the matching series. Events
268
- bubble so document-level listening catches descendant legends. */
269
- document.addEventListener('legend-toggle', this.#onLegendToggle);
266
+ /* Listen for canonical `toggle` events bubbled from external
267
+ chart-legend-ui[for=self] descendants. The handler filters by
268
+ target so other components dispatching `toggle` (accordion-ui,
269
+ agent-trace-ui, etc.) don't interfere. */
270
+ document.addEventListener('toggle', this.#onLegendToggle);
270
271
 
271
272
  /* OD-CHART-06 — keyboard a11y. Chart becomes focusable; arrow keys
272
273
  move a virtual focus across datums in DOM order, Enter/Space fires
@@ -279,6 +280,14 @@ class AdiaChart extends AdiaElement {
279
280
  this.addEventListener('keydown', this.#onKeydown);
280
281
  this.addEventListener('focus', this.#onFocus);
281
282
  this.addEventListener('blur', this.#onBlur);
283
+ /* Pointer/click handlers attached to the host (not per-render SVG) so
284
+ render() stays listener-graph-idempotent. Handlers use
285
+ e.target.closest('[data-tip-*]') so they only fire on real datums. */
286
+ this.addEventListener('pointerover', this.#onPointerOver);
287
+ this.addEventListener('pointermove', this.#onPointerMove);
288
+ this.addEventListener('pointerleave', this.#onPointerLeave);
289
+ this.addEventListener('pointerdown', this.#onPointerDown);
290
+ this.addEventListener('click', this.#onClick);
282
291
  this.#warnDeprecatedAttrs();
283
292
 
284
293
  this.#resizeObs = new ResizeObserver((entries) => {
@@ -443,16 +452,10 @@ class AdiaChart extends AdiaElement {
443
452
  /* Notify external legend/tooltip consumers that legendData has refreshed. */
444
453
  this.dispatchEvent(new CustomEvent('legend-update', { bubbles: true }));
445
454
 
446
- /* Wire hover tooltip + custom events. Internal tooltip (#tipEl) remains
447
- for back-compat; Phase 2 retires it when tooltip-ui[follows=pointer]
448
- lands. Custom events are additive fire regardless of tooltip state.
449
- OD-CHART-07: pointerdown handles touch (tap-to-pin); pointerover /
450
- move still handle mouse + pen (`pointerType` gating inside each). */
451
- svgEl.addEventListener('pointerover', this.#onPointerOver);
452
- svgEl.addEventListener('pointermove', this.#onPointerMove);
453
- svgEl.addEventListener('pointerleave', this.#onPointerLeave);
454
- svgEl.addEventListener('pointerdown', this.#onPointerDown);
455
- svgEl.addEventListener('click', this.#onClick);
455
+ /* Hover tooltip + custom events are wired in connected() — host-level
456
+ so they survive the innerHTML wipe at render. Internal tooltip
457
+ (#tipEl) remains for back-compat; Phase 2 retires it when
458
+ tooltip-ui[follows=pointer] lands. */
456
459
  }
457
460
 
458
461
  /* ── Per-series --color-{key} injection ──
@@ -491,11 +494,16 @@ class AdiaChart extends AdiaElement {
491
494
  this.#resizeObs?.disconnect();
492
495
  this.#resizeObs = null;
493
496
  if (this.#resizeRaf) { cancelAnimationFrame(this.#resizeRaf); this.#resizeRaf = null; }
494
- document.removeEventListener('legend-toggle', this.#onLegendToggle);
497
+ document.removeEventListener('toggle', this.#onLegendToggle);
495
498
  document.removeEventListener('pointerdown', this.#pinnedTouchDismiss);
496
499
  this.removeEventListener('keydown', this.#onKeydown);
497
500
  this.removeEventListener('focus', this.#onFocus);
498
501
  this.removeEventListener('blur', this.#onBlur);
502
+ this.removeEventListener('pointerover', this.#onPointerOver);
503
+ this.removeEventListener('pointermove', this.#onPointerMove);
504
+ this.removeEventListener('pointerleave', this.#onPointerLeave);
505
+ this.removeEventListener('pointerdown', this.#onPointerDown);
506
+ this.removeEventListener('click', this.#onClick);
499
507
  this.#hideTooltip();
500
508
  }
501
509
 
@@ -27,7 +27,7 @@
27
27
  "default": ""
28
28
  },
29
29
  "onToggle": {
30
- "description": "Series-toggle mode emitted via legend-toggle event. `hide` removes the series from the render; `opacity` fades it. Wired via [for] on chart-ui.",
30
+ "description": "Series-toggle mode emitted via the `toggle` event. `hide` removes the series from the render; `opacity` fades it. Wired via [for] on chart-ui.",
31
31
  "type": "string",
32
32
  "enum": [
33
33
  "hide",
@@ -71,7 +71,7 @@
71
71
  "anti_patterns": [],
72
72
  "category": "agent",
73
73
  "events": {
74
- "legend-toggle": {
74
+ "toggle": {
75
75
  "description": "Fires on row click (non-static). Detail: {key, active, mode}. `active` is the new state (true=visible). Consumers (or chart-ui via [for]) wire this to series visibility."
76
76
  }
77
77
  },
@@ -36,6 +36,9 @@ import { AdiaElement } from '../../core/element.js';
36
36
  class AdiaChartLegend extends AdiaElement {
37
37
  static properties = {
38
38
  for: { type: String, default: '', reflect: true },
39
+ // Items is a JSON-encoded array; reflecting to the attribute would
40
+ // serialize a potentially long array on every change. The property is
41
+ // the source of truth; the attribute is intentionally write-only.
39
42
  items: { type: String, default: '', reflect: false },
40
43
  shape: { type: String, default: 'dot', reflect: true },
41
44
  position: { type: String, default: 'bottom', reflect: true },
@@ -173,7 +176,7 @@ class AdiaChartLegend extends AdiaElement {
173
176
  if (newActive) row.setAttribute('data-active', '');
174
177
  else row.removeAttribute('data-active');
175
178
 
176
- this.dispatchEvent(new CustomEvent('legend-toggle', {
179
+ this.dispatchEvent(new CustomEvent('toggle', {
177
180
  bubbles: true,
178
181
  detail: { key, active: newActive, mode: this.onToggle || 'hide' },
179
182
  }));
@@ -48,7 +48,7 @@ props:
48
48
  reflect: true
49
49
  onToggle:
50
50
  description: >-
51
- Series-toggle mode emitted via legend-toggle event. `hide` removes the
51
+ Series-toggle mode emitted via the `toggle` event. `hide` removes the
52
52
  series from the render; `opacity` fades it. Wired via [for] on chart-ui.
53
53
  type: string
54
54
  default: hide
@@ -58,7 +58,7 @@ props:
58
58
  reflect: true
59
59
  attribute: on-toggle
60
60
  events:
61
- legend-toggle:
61
+ toggle:
62
62
  description: >-
63
63
  Fires on row click (non-static). Detail: {key, active, mode}. `active` is
64
64
  the new state (true=visible). Consumers (or chart-ui via [for]) wire
@@ -24,7 +24,7 @@ import { AdiaElement } from '../../core/element.js';
24
24
  * model — currently selected model value (reflected, two-way with select)
25
25
  * placeholder — textarea placeholder
26
26
  * disabled — disable entire input (textarea becomes contenteditable=false)
27
- * busy — in-flight / streaming state: send button disabled, submit
27
+ * loading — in-flight / streaming state: send button disabled, submit
28
28
  * events suppressed, but textarea stays editable so the user
29
29
  * can draft a follow-up while the model is still responding.
30
30
  *
@@ -40,7 +40,7 @@ import { AdiaElement } from '../../core/element.js';
40
40
  class AdiaChatInput extends AdiaElement {
41
41
  static properties = {
42
42
  disabled: { type: Boolean, default: false, reflect: true },
43
- busy: { type: Boolean, default: false, reflect: true },
43
+ loading: { type: Boolean, default: false, reflect: true },
44
44
  placeholder: { type: String, default: 'Type a message...', reflect: true },
45
45
  model: { type: String, default: '', reflect: true },
46
46
  };
@@ -56,6 +56,7 @@ class AdiaChatInput extends AdiaElement {
56
56
  #attachments = []; // [{ type: 'image', dataUrl, name }]
57
57
  #previewEl = null;
58
58
  #fileInput = null;
59
+ #focusRaf = null;
59
60
 
60
61
  #onAttachPress = () => this.#fileInput?.click();
61
62
  #onFileInputChange = () => {
@@ -154,7 +155,7 @@ class AdiaChatInput extends AdiaElement {
154
155
  this.#textareaEl.placeholder = this.placeholder;
155
156
  }
156
157
  if (this.#sendEl) {
157
- this.#sendEl.disabled = this.disabled || this.busy;
158
+ this.#sendEl.disabled = this.disabled || this.loading;
158
159
  }
159
160
  // Sync model value to select (handles late upgrades)
160
161
  if (this.#modelEl && this.model && this.#modelEl.value !== this.model) {
@@ -164,7 +165,10 @@ class AdiaChatInput extends AdiaElement {
164
165
 
165
166
  #onPointerDown = (e) => {
166
167
  if (e.target.closest('button-ui, select-ui, input-ui, a')) return;
167
- requestAnimationFrame(() => this.#textareaEl?.focus());
168
+ this.#focusRaf = requestAnimationFrame(() => {
169
+ this.#focusRaf = null;
170
+ this.#textareaEl?.focus();
171
+ });
168
172
  };
169
173
 
170
174
  #onModelChange = () => {
@@ -172,7 +176,7 @@ class AdiaChatInput extends AdiaElement {
172
176
  };
173
177
 
174
178
  #onSubmit = () => {
175
- if (this.disabled || this.busy) return;
179
+ if (this.disabled || this.loading) return;
176
180
  const text = this.value;
177
181
  if (!text && !this.#attachments.length) return;
178
182
  this.dispatchEvent(new CustomEvent('submit', {
@@ -240,6 +244,10 @@ class AdiaChatInput extends AdiaElement {
240
244
  }
241
245
 
242
246
  disconnected() {
247
+ if (this.#focusRaf != null) {
248
+ cancelAnimationFrame(this.#focusRaf);
249
+ this.#focusRaf = null;
250
+ }
243
251
  this.removeEventListener('paste', this.#onPaste);
244
252
  this.#sendEl?.removeEventListener('press', this.#onSubmit);
245
253
  this.#textareaEl?.removeEventListener('submit', this.#onSubmit);
@@ -30,8 +30,8 @@
30
30
  "anti_patterns": [],
31
31
  "category": "agent",
32
32
  "events": {
33
- "chat-submit": {
34
- "description": "Fired on chat-submit."
33
+ "submit": {
34
+ "description": "Fired when the user submits a chat message via Enter or send button. Detail: { text, model }."
35
35
  }
36
36
  },
37
37
  "examples": [
@@ -32,7 +32,7 @@ function escapeHTML(s) {
32
32
  * chat.messages // read all messages
33
33
  *
34
34
  * Events:
35
- * chat-submit — user pressed send (detail: { text, model })
35
+ * submit — user pressed send (detail: { text, model })
36
36
  */
37
37
  class AdiaChat extends AdiaElement {
38
38
  static properties = {
@@ -44,6 +44,7 @@ class AdiaChat extends AdiaElement {
44
44
  #messages = [];
45
45
  #messagesEl = null;
46
46
  #inputEl = null;
47
+ #scrollRaf = null;
47
48
 
48
49
  get messages() { return [...this.#messages]; }
49
50
 
@@ -69,6 +70,10 @@ class AdiaChat extends AdiaElement {
69
70
  }
70
71
 
71
72
  disconnected() {
73
+ if (this.#scrollRaf != null) {
74
+ cancelAnimationFrame(this.#scrollRaf);
75
+ this.#scrollRaf = null;
76
+ }
72
77
  this.#inputEl?.removeEventListener('submit', this.#onSubmit);
73
78
  this.#messagesEl = null;
74
79
  this.#inputEl = null;
@@ -79,7 +84,7 @@ class AdiaChat extends AdiaElement {
79
84
  const { text, model } = e.detail || {};
80
85
  if (!text) return;
81
86
  this.#inputEl.clear();
82
- this.dispatchEvent(new CustomEvent('chat-submit', { bubbles: true, detail: { text, model } }));
87
+ this.dispatchEvent(new CustomEvent('submit', { bubbles: true, detail: { text, model } }));
83
88
  };
84
89
 
85
90
  appendMessage({ role, content = '', render = false }) {
@@ -147,7 +152,13 @@ class AdiaChat extends AdiaElement {
147
152
 
148
153
  #scrollToBottom() {
149
154
  const el = this.#messagesEl;
150
- if (el) requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; });
155
+ if (!el) return;
156
+ if (this.#scrollRaf != null) cancelAnimationFrame(this.#scrollRaf);
157
+ this.#scrollRaf = requestAnimationFrame(() => {
158
+ this.#scrollRaf = null;
159
+ if (!this.isConnected) return;
160
+ el.scrollTop = el.scrollHeight;
161
+ });
151
162
  }
152
163
  }
153
164
  customElements.define('chat-ui', AdiaChat);
@@ -13,8 +13,8 @@ props:
13
13
  type: boolean
14
14
  default: false
15
15
  events:
16
- chat-submit:
17
- description: "Fired on chat-submit."
16
+ submit:
17
+ description: "Fired when the user submits a chat message via Enter or send button. Detail: { text, model }."
18
18
  slots:
19
19
  default:
20
20
  description: "Default slot — primary child content."
@@ -6,6 +6,8 @@
6
6
  --code-radius: var(--a-radius-md);
7
7
  --code-radius-sm: var(--a-radius-sm);
8
8
  --code-copy-px: var(--a-space-1);
9
+ --code-copy-py: var(--a-space-0-5);
10
+ --code-lint-marker-w: 3px; /* Vertical lint-marker rule; intrinsic measurement; no --a-space-* equivalent at this width. */
9
11
 
10
12
  /* ── Colors ── */
11
13
  --code-bg: var(--a-bg);
@@ -31,9 +33,12 @@
31
33
  DOM. See docs/specs/code-editor.md §6. */
32
34
  --code-gutter-bg: var(--a-bg-subtle);
33
35
  --code-gutter-fg: var(--a-fg-muted);
34
- --code-active-line-bg: color-mix(in oklch, var(--a-accent-muted) 40%, transparent);
35
- --code-selection-bg: color-mix(in oklch, var(--a-accent-muted) 60%, transparent);
36
- --code-selection-match: color-mix(in oklch, var(--a-accent-muted) 30%, transparent);
36
+ /* Transparency-mixing in oklab Safari < 18 has an OKLCH bug where
37
+ the transparent operand's hue resolves to 0, red-shifting the mix.
38
+ oklab is hue-agnostic and visually identical in fixed engines. */
39
+ --code-active-line-bg: color-mix(in oklab, var(--a-accent-muted) 40%, transparent);
40
+ --code-selection-bg: color-mix(in oklab, var(--a-accent-muted) 60%, transparent);
41
+ --code-selection-match: color-mix(in oklab, var(--a-accent-muted) 30%, transparent);
37
42
  --code-cursor: var(--a-accent-strong);
38
43
  --code-focus-ring: var(--a-focus-ring);
39
44
 
@@ -110,7 +115,7 @@
110
115
  cursor: pointer;
111
116
  font-size: var(--code-header-font);
112
117
  color: var(--code-header-fg);
113
- padding: 2px var(--code-copy-px);
118
+ padding: var(--code-copy-py) var(--code-copy-px);
114
119
  border-radius: var(--code-radius-sm);
115
120
  transition:
116
121
  background var(--code-duration) var(--code-easing),
@@ -258,7 +263,12 @@
258
263
  :scope .cm-scroller {
259
264
  font-family: inherit;
260
265
  line-height: inherit;
261
- padding: var(--code-py) var(--code-px);
266
+ /* No padding — the gutter band ([data-cm-mount] > .cm-gutters has its
267
+ own --code-gutter-bg) reads as the chrome it is when pinned to the
268
+ left edge, and the active-line highlight and selection block get
269
+ full bleed. The header / footer bands frame the editor; this content
270
+ region should not double-inset. */
271
+ padding: 0;
262
272
  }
263
273
 
264
274
  :scope .cm-content {
@@ -347,7 +357,7 @@
347
357
  }
348
358
  :scope .cm-diagnostic {
349
359
  padding: 0;
350
- border-inline-start: 3px solid transparent;
360
+ border-inline-start: var(--code-lint-marker-w) solid transparent;
351
361
  padding-inline-start: var(--a-space-2);
352
362
  }
353
363
  :scope .cm-diagnostic-error { border-inline-start-color: var(--code-lint-error); }
@@ -32,6 +32,7 @@ class AdiaCommand extends AdiaElement {
32
32
  #activeIdx = -1;
33
33
  #inputEl = null;
34
34
  #listEl = null;
35
+ #focusRaf = null;
35
36
  #footerEl = null;
36
37
  #bound = false;
37
38
  #itemByEl = new WeakMap();
@@ -88,7 +89,10 @@ class AdiaCommand extends AdiaElement {
88
89
  }
89
90
 
90
91
  if (this.open) {
91
- requestAnimationFrame(() => this.#inputEl?.focus());
92
+ this.#focusRaf = requestAnimationFrame(() => {
93
+ this.#focusRaf = null;
94
+ this.#inputEl?.focus();
95
+ });
92
96
  }
93
97
  }
94
98
 
@@ -280,6 +284,10 @@ class AdiaCommand extends AdiaElement {
280
284
  };
281
285
 
282
286
  disconnected() {
287
+ if (this.#focusRaf != null) {
288
+ cancelAnimationFrame(this.#focusRaf);
289
+ this.#focusRaf = null;
290
+ }
283
291
  this.removeEventListener('keydown', this.#onKeydown);
284
292
  this.#inputEl?.removeEventListener('input', this.#onInput);
285
293
  this.#listEl?.removeEventListener('click', this.#onListClick);
@@ -21,11 +21,6 @@
21
21
  "component": {
22
22
  "const": "Field"
23
23
  },
24
- "error": {
25
- "description": "Validation error message rendered below the control in danger style. Takes precedence over `hint` in the same row, and carries role=\"alert\" so screen readers announce changes.",
26
- "type": "string",
27
- "default": ""
28
- },
29
24
  "hint": {
30
25
  "description": "Help text rendered below the control in caption style. Wired into the slotted control's aria-describedby so screen readers announce it. Suppressed when `error` is set.",
31
26
  "type": "string",
@@ -5,12 +5,12 @@
5
5
  --field-label-color: var(--a-fg);
6
6
  --field-label-size: var(--a-ui-sm);
7
7
  --field-label-weight: var(--a-weight-medium);
8
- --field-required-color: var(--a-danger);
8
+ --field-required-color: var(--a-danger-strong);
9
9
  --field-trailing-color: var(--a-fg-subtle);
10
10
  --field-trailing-size: var(--a-ui-sm);
11
11
  --field-hint-color: var(--a-fg-muted);
12
12
  --field-hint-size: var(--a-ui-sm);
13
- --field-error-color: var(--a-danger);
13
+ --field-error-color: var(--a-danger-strong);
14
14
  --field-error-size: var(--a-ui-sm);
15
15
 
16
16
  /* In inline mode, the label column auto-sizes by default (each