@adia-ai/web-components 0.0.14 → 0.0.16

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 (41) hide show
  1. package/README.md +43 -1
  2. package/components/alert/alert.css +5 -0
  3. package/components/alert/alert.js +4 -2
  4. package/components/button/button.js +4 -1
  5. package/components/chart/chart.js +7 -4
  6. package/components/chat/chat-input.js +13 -2
  7. package/components/description-list/description-list.js +4 -3
  8. package/components/field/field.css +113 -63
  9. package/components/field/field.js +44 -142
  10. package/components/icon/icon.a2ui.json +1 -1
  11. package/components/icon/icon.css +16 -0
  12. package/components/icon/icon.js +18 -0
  13. package/components/icon/icon.yaml +6 -2
  14. package/components/index.js +7 -0
  15. package/components/input/input.a2ui.json +1 -1
  16. package/components/input/input.css +21 -23
  17. package/components/input/input.js +36 -9
  18. package/components/input/input.yaml +3 -1
  19. package/components/option-card/option-card.a2ui.json +262 -0
  20. package/components/option-card/option-card.css +215 -0
  21. package/components/option-card/option-card.js +158 -0
  22. package/components/option-card/option-card.yaml +234 -0
  23. package/components/rating/rating.a2ui.json +10 -0
  24. package/components/rating/rating.yaml +8 -0
  25. package/components/segment/segment.a2ui.json +5 -0
  26. package/components/segment/segment.css +2 -0
  27. package/components/segment/segment.js +21 -1
  28. package/components/segment/segment.yaml +5 -0
  29. package/components/textarea/textarea.css +3 -1
  30. package/components/textarea/textarea.js +2 -2
  31. package/components/tooltip/tooltip.js +10 -3
  32. package/core/data-stream.js +486 -0
  33. package/core/form.js +5 -0
  34. package/core/index.js +2 -0
  35. package/core/streams-bridge.js +96 -0
  36. package/package.json +1 -1
  37. package/styles/colors/semantics.css +21 -3
  38. package/styles/components.css +1 -0
  39. package/styles/prose.css +3 -7
  40. package/styles/tokens.css +7 -4
  41. package/styles/typography.css +6 -1
package/README.md CHANGED
@@ -42,7 +42,9 @@ web-components/
42
42
  │ ├── provider.js global provider registration + router-ui
43
43
  │ ├── anchor.js popover + anchor-positioning
44
44
  │ ├── markdown.js lightweight markdown renderer
45
- └── transport.js SSE / streaming helpers for LLM adapters
45
+ ├── transport.js SSE / streaming helpers for LLM adapters
46
+ │ └── data-stream.js `data-stream-*` attribute trait (HTTP/SSE/WS,
47
+ │ signal-backed, refcounted shared transports)
46
48
 
47
49
  ├── components/ — 80 *-ui custom elements
48
50
  │ └── <tag>/
@@ -159,6 +161,46 @@ Accepts the four A2UI message kinds: `updateComponents`,
159
161
  normalizes LLM-emitted aliases (e.g. `Carousel` → `swiper-ui`) so generated
160
162
  output is robust to name drift.
161
163
 
164
+ ## Data streaming via `data-stream-*` attributes
165
+
166
+ Any element with a settable `.data` property — chart-ui, table-ui,
167
+ heatmap-ui, stat-ui, list-ui, etc. — can be fed from a backing
168
+ source via attributes alone. No per-component opt-in:
169
+
170
+ ```html
171
+ <!-- HTTP one-shot fetch, JSON -->
172
+ <chart-ui type="area" x="month" y="revenue"
173
+ data-stream-src="/api/revenue?range=3m"
174
+ data-stream-path="data"></chart-ui>
175
+
176
+ <!-- HTTP polling every 5s -->
177
+ <table-ui sortable striped
178
+ data-stream-src="/api/orders"
179
+ data-stream-interval="5000"></table-ui>
180
+
181
+ <!-- Server-Sent Events, append on each message -->
182
+ <heatmap-ui type="matrix" rows="7" cols="52"
183
+ data-stream-src="/sse/activity"
184
+ data-stream-mode="sse"
185
+ data-stream-merge="append"></heatmap-ui>
186
+
187
+ <!-- Spread a multi-property response onto the element -->
188
+ <stat-ui data-stream-src="/api/kpi"
189
+ data-stream-target="*"></stat-ui>
190
+ ```
191
+
192
+ Modes: HTTP (one-shot or polling), `sse` (`EventSource`), `ws`
193
+ (`WebSocket`). Formats: `json` (default), `csv`, `tsv`, `jsonl`,
194
+ `text` — auto-detected from URL extension or content-type. Two
195
+ elements with attribute-identical streams share one transport
196
+ (refcounted, signal-backed); explicit `data-stream-id` lets
197
+ unrelated configs share intentionally. Programmatic access via
198
+ the `streams` registry export from `core/data-stream.js`.
199
+
200
+ Implementation: `core/data-stream.js` (~360 lines). Full
201
+ attribute table + live demos:
202
+ [`/site/components/chart#data-stream`](./site/pages/components/chart/index.html).
203
+
162
204
  ## Build
163
205
 
164
206
  ```bash
@@ -65,6 +65,11 @@
65
65
  :scope [slot="leading"] {
66
66
  flex-shrink: 0;
67
67
  color: var(--alert-icon-fg);
68
+ /* `ensure()` appends the leading-slot icon to the host, which
69
+ puts it after any consumer-provided content in DOM order.
70
+ Force it to the visual lead via flex `order` so the icon
71
+ always reads first. */
72
+ order: -1;
68
73
  }
69
74
 
70
75
  :scope [slot="content"] {
@@ -66,9 +66,11 @@ class AdiaAlert extends AdiaElement {
66
66
  this.drop('leading');
67
67
  }
68
68
 
69
- // Text
69
+ // Text — only write from the `text` attribute when it's set, so
70
+ // consumers passing rich content via `<span slot="content">…</span>`
71
+ // (links, <strong>, etc.) aren't clobbered with empty textContent.
70
72
  const content = this.ensure('content');
71
- if (content) content.textContent = this.text;
73
+ if (content && this.text) content.textContent = this.text;
72
74
 
73
75
  // Close button
74
76
  if (this.closable) {
@@ -22,7 +22,10 @@ class AdiaButton extends AdiaElement {
22
22
  }
23
23
 
24
24
  render() {
25
- this.setAttribute('aria-label', this.text || '');
25
+ // Don't clobber a user-provided aria-label with an empty string when
26
+ // text is unset (e.g. icon-only button with author-set aria-label).
27
+ // Only auto-set when we have meaningful text to put there.
28
+ if (this.text) this.setAttribute('aria-label', this.text);
26
29
  if (this.icon) {
27
30
  const existing = this.querySelector('icon-ui');
28
31
  if (!existing || existing.name !== this.icon) {
@@ -885,15 +885,18 @@ class AdiaChart extends AdiaElement {
885
885
 
886
886
  try { this.#tipEl.showPopover(); } catch (_) { /* popover not supported */ }
887
887
 
888
- /* Follow the cursor — offset up-right, clamp to viewport */
888
+ /* Follow the cursor — centered horizontally above, clamp to viewport
889
+ with an 8px edge-pad, flip below when there's no room above. */
889
890
  const gap = 12;
891
+ const edgePad = 8;
890
892
  const { clientX, clientY } = event;
891
893
  const tw = this.#tipEl.offsetWidth || 0;
892
894
  const th = this.#tipEl.offsetHeight || 0;
893
- let x = clientX + gap;
895
+ let x = clientX - tw / 2;
894
896
  let y = clientY - th - gap;
895
- if (x + tw > window.innerWidth) x = clientX - tw - gap;
896
- if (y < 0) y = clientY + gap;
897
+ if (x < edgePad) x = edgePad;
898
+ if (x + tw > window.innerWidth - edgePad) x = window.innerWidth - tw - edgePad;
899
+ if (y < edgePad) y = clientY + gap;
897
900
  this.#tipEl.style.left = `${x}px`;
898
901
  this.#tipEl.style.top = `${y}px`;
899
902
  }
@@ -85,8 +85,8 @@ class AdiaChatInput extends AdiaElement {
85
85
  this.innerHTML = `
86
86
  <textarea-ui placeholder="${this.placeholder}" rows="1"></textarea-ui>
87
87
  <div slot="toolbar">
88
- <select-ui slot="model" placeholder="Model" divider></select-ui>
89
- <button-ui icon="paper-plane-right" variant="ghost" slot="send"></button-ui>
88
+ <select-ui slot="model" placeholder="Model" aria-label="Select model" divider></select-ui>
89
+ <button-ui icon="paper-plane-right" variant="ghost" slot="send" aria-label="Send message"></button-ui>
90
90
  </div>
91
91
  `;
92
92
  }
@@ -95,6 +95,16 @@ class AdiaChatInput extends AdiaElement {
95
95
  this.#sendEl = this.querySelector('[slot="send"]');
96
96
  this.#modelEl = this.querySelector('[slot="model"]');
97
97
 
98
+ // Default aria-labels on author-provided send/model when not set —
99
+ // these elements are screen-reader-relevant and shouldn't fall back
100
+ // to the icon-name announcement.
101
+ if (this.#sendEl && !this.#sendEl.hasAttribute('aria-label')) {
102
+ this.#sendEl.setAttribute('aria-label', 'Send message');
103
+ }
104
+ if (this.#modelEl && !this.#modelEl.hasAttribute('aria-label')) {
105
+ this.#modelEl.setAttribute('aria-label', 'Select model');
106
+ }
107
+
98
108
  // Apply models if set before connected (options first, then value)
99
109
  if (this.#models.length && this.#modelEl) {
100
110
  this.#modelEl.options = this.#models;
@@ -116,6 +126,7 @@ class AdiaChatInput extends AdiaElement {
116
126
  this.#attachBtn.setAttribute('variant', 'ghost');
117
127
  this.#attachBtn.setAttribute('slot', 'attach');
118
128
  this.#attachBtn.setAttribute('size', 'sm');
129
+ this.#attachBtn.setAttribute('aria-label', 'Attach image');
119
130
  const sendBtn = toolbar.querySelector('[slot="send"]');
120
131
  toolbar.insertBefore(this.#attachBtn, sendBtn);
121
132
  this.#attachBtn.addEventListener('press', this.#onAttachPress);
@@ -30,9 +30,10 @@ class AdiaDescriptionList extends AdiaElement {
30
30
  static template = () => null;
31
31
 
32
32
  connected() {
33
- // Use the native <dl> role by wrapping or using the tag directly —
34
- // since we're a custom element, we set role explicitly.
35
- this.setAttribute('role', 'list');
33
+ // ARIA 1.2: list role requires listitem children, but <dt>/<dd>
34
+ // aren't listitems. group role is the accurate fit for a
35
+ // labeled-pairs grouping.
36
+ this.setAttribute('role', 'group');
36
37
  }
37
38
 
38
39
  render() {
@@ -7,105 +7,155 @@
7
7
  --field-label-weight: var(--a-weight-medium);
8
8
  --field-required-color: var(--a-danger);
9
9
  --field-trailing-color: var(--a-fg-subtle);
10
- --field-trailing-size: var(--a-ui-tiny);
10
+ --field-trailing-size: var(--a-ui-sm);
11
11
  --field-hint-color: var(--a-fg-muted);
12
- --field-hint-size: var(--a-ui-tiny);
12
+ --field-hint-size: var(--a-ui-sm);
13
13
  --field-error-color: var(--a-danger);
14
- --field-error-size: var(--a-ui-tiny);
14
+ --field-error-size: var(--a-ui-sm);
15
+
16
+ /* In inline mode, the label column auto-sizes by default (each
17
+ field's label column is independent of its siblings). Consumers
18
+ that want shared label-column alignment across stacked inline
19
+ fields can raise this floor — e.g. 12rem in a multi-field form. */
20
+ --field-label-inline-min: 0;
15
21
  }
16
22
 
17
- /* ── Base — stacked: three flex rows stacked vertically.
18
- Each row is its own flex container so `trailing` and `action`
19
- size independently (the previous single-grid shared column 2
20
- across rows and made them co-sized). ── */
23
+ /* ── Base — single grid; children placed by [slot] attribute via
24
+ named grid-areas. No row wrappers; no DOM reparenting. The
25
+ template adapts via `:has()` to which slots are present so
26
+ empty tracks don't leak column-gap. ── */
21
27
  :scope {
22
28
  box-sizing: border-box;
23
- display: flex;
24
- flex-direction: column;
25
- gap: var(--field-gap);
29
+ display: grid;
30
+ grid-template-columns: minmax(0, 1fr);
31
+ grid-template-areas:
32
+ "label"
33
+ "control"
34
+ "message";
35
+ column-gap: var(--field-gap);
36
+ row-gap: var(--field-gap);
37
+ align-items: center;
26
38
  }
27
39
 
28
- :scope > [data-row] {
29
- display: flex;
30
- align-items: center;
31
- gap: var(--field-gap);
32
- min-width: 0;
40
+ /* Stacked + (trailing or action) → 2-col */
41
+ :scope:has(> :is([slot="trailing"], [slot="action"])) {
42
+ grid-template-columns: minmax(0, 1fr) auto;
43
+ grid-template-areas:
44
+ "label trailing"
45
+ "control action"
46
+ "message message";
47
+ }
48
+
49
+ /* Stacked + no label, no trailing → drop the empty top row. */
50
+ :scope:not([label]):not(:has(> [slot="trailing"])) {
51
+ grid-template-areas:
52
+ "control"
53
+ "message";
54
+ }
55
+ :scope:not([label]):not(:has(> [slot="trailing"])):has(> [slot="action"]) {
56
+ grid-template-columns: minmax(0, 1fr) auto;
57
+ grid-template-areas:
58
+ "control action"
59
+ "message message";
33
60
  }
34
61
 
35
- /* label-row label grows, trailing auto-sizes and right-aligns. */
36
- :scope > [data-row="label"] > [data-field-label] {
37
- flex: 1 1 auto;
62
+ /* Hide the label cell when the label attr is absent. */
63
+ :scope:not([label]) > [slot="label"] { display: none; }
64
+
65
+ /* ── Slot styling ── */
66
+ :scope > [slot="label"] {
67
+ grid-area: label;
38
68
  color: var(--field-label-color);
39
69
  font-size: var(--field-label-size);
40
70
  font-weight: var(--field-label-weight);
41
71
  cursor: pointer;
42
72
  min-width: 0;
43
73
  }
44
- :scope > [data-row="label"] > [data-field-label] > [data-field-required] {
74
+ :scope > [slot="label"] > [data-field-required] {
45
75
  color: var(--field-required-color);
46
76
  margin-inline-start: 0.15em;
47
77
  font-weight: var(--a-weight-bold);
48
78
  }
49
- :scope > [data-row="label"] > [slot="trailing"] {
50
- flex: 0 0 auto;
79
+ :scope > [slot="trailing"] {
80
+ grid-area: trailing;
81
+ justify-self: end;
51
82
  color: var(--field-trailing-color);
52
83
  font-size: var(--field-trailing-size);
53
84
  }
54
-
55
- /* control-row control grows, action auto-sizes. */
56
- :scope > [data-row="control"] > :not([slot="action"]) {
57
- flex: 1 1 auto;
85
+ :scope > :not([slot]) {
86
+ grid-area: control;
58
87
  min-width: 0;
59
88
  }
60
- :scope > [data-row="control"] > [slot="action"] {
61
- flex: 0 0 auto;
89
+ :scope > [slot="action"] {
90
+ grid-area: action;
91
+ justify-self: end;
62
92
  }
63
-
64
- /* message-row — hint or error; collapsed via [hidden] when both empty. */
65
- :scope > [data-row="message"] > [data-field-hint] {
93
+ :scope > [slot="hint"],
94
+ :scope > [slot="error"] {
95
+ grid-area: message;
96
+ line-height: 1.3;
97
+ }
98
+ :scope > [slot="hint"] {
66
99
  color: var(--field-hint-color);
67
100
  font-size: var(--field-hint-size);
68
- line-height: 1.3;
69
101
  }
70
- :scope > [data-row="message"] > [data-field-error] {
102
+ :scope > [slot="error"] {
71
103
  color: var(--field-error-color);
72
104
  font-size: var(--field-error-size);
73
- line-height: 1.3;
74
105
  font-weight: var(--a-weight-medium);
75
106
  }
76
107
 
77
- /* Hide the whole label-row when there's no label AND no trailing
78
- prevents an empty row stealing vertical gap. */
79
- :scope:not([label]) > [data-row="label"] > [data-field-label] {
80
- display: none;
108
+ /* ── Mode: inline content slots on row 1, message row below
109
+ (aligned with the control column so hint/error sit under the
110
+ input, not under the label). Templates branch by which
111
+ optional slots are present so we don't carry zero-width
112
+ tracks + their gaps. ── */
113
+ :scope[inline] {
114
+ grid-template-columns: minmax(var(--field-label-inline-min), auto) minmax(0, 1fr);
115
+ grid-template-areas:
116
+ "label control"
117
+ ". message";
81
118
  }
82
- :scope:not([label]) > [data-row="label"]:not(:has(> [slot="trailing"])) {
83
- display: none;
119
+ :scope[inline]:has(> [slot="trailing"]):not(:has(> [slot="action"])) {
120
+ grid-template-columns: minmax(var(--field-label-inline-min), auto) auto minmax(0, 1fr);
121
+ grid-template-areas:
122
+ "label trailing control"
123
+ ". . message";
84
124
  }
85
-
86
- /* ── Mode: inline — flatten label-row + control-row into a single
87
- grid row. `display: contents` promotes the row wrappers' children
88
- directly to :scope's grid, keeping per-cell independence. The
89
- message-row stays a block row below. ── */
90
- :scope[inline] {
91
- display: grid;
92
- grid-template-columns: auto auto 1fr auto;
93
- grid-template-rows: auto auto;
94
- gap: var(--field-gap);
95
- align-items: center;
125
+ :scope[inline]:not(:has(> [slot="trailing"])):has(> [slot="action"]) {
126
+ grid-template-columns: minmax(var(--field-label-inline-min), auto) minmax(0, 1fr) auto;
127
+ grid-template-areas:
128
+ "label control action"
129
+ ". message message";
130
+ }
131
+ :scope[inline]:has(> [slot="trailing"]):has(> [slot="action"]) {
132
+ grid-template-columns: minmax(var(--field-label-inline-min), auto) auto minmax(0, 1fr) auto;
133
+ grid-template-areas:
134
+ "label trailing control action"
135
+ ". . message message";
96
136
  }
97
- :scope[inline] > [data-row="label"],
98
- :scope[inline] > [data-row="control"] {
99
- display: contents;
100
- }
101
- :scope[inline] > [data-row="label"] > [data-field-label] { grid-column: 1; grid-row: 1; justify-self: start; }
102
- :scope[inline] > [data-row="label"] > [slot="trailing"] { grid-column: 2; grid-row: 1; justify-self: start; }
103
- :scope[inline] > [data-row="control"] > :not([slot="action"]) { grid-column: 3; grid-row: 1; }
104
- :scope[inline] > [data-row="control"] > [slot="action"] { grid-column: 4; grid-row: 1; justify-self: end; }
105
- :scope[inline] > [data-row="message"] {
106
- grid-column: 1 / -1;
107
- grid-row: 2;
108
- display: flex;
109
- gap: var(--field-gap);
137
+ :scope[inline]:not([label]):not(:has(> [slot="trailing"])):not(:has(> [slot="action"])) {
138
+ grid-template-columns: minmax(0, 1fr);
139
+ grid-template-areas:
140
+ "control"
141
+ "message";
142
+ }
143
+ :scope[inline]:not([label]):not(:has(> [slot="trailing"])):has(> [slot="action"]) {
144
+ grid-template-columns: minmax(0, 1fr) auto;
145
+ grid-template-areas:
146
+ "control action"
147
+ "message message";
148
+ }
149
+
150
+ /* In inline mode, push compact toggle controls (switch / check /
151
+ radio) to the row's end edge — settings rows then render label-
152
+ left, control-right regardless of label length. Wide controls
153
+ (input / textarea / select) keep their default stretch behavior
154
+ so they fill the trailing column. Already in HEAD as `356a39f`
155
+ against the row-wrapper model; the second selector here keeps
156
+ the rule alive once the v2 flat-DOM refactor (§18) lands. */
157
+ :scope[inline]:has(:is(switch-ui, check-ui, radio-ui)) > [data-row="control"] > :not([slot="action"]),
158
+ :scope[inline]:has(> :is(switch-ui, check-ui, radio-ui)) > :not([slot]) {
159
+ justify-self: end;
110
160
  }
111
161
  }