@adia-ai/web-components 0.0.15 → 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 (37) hide show
  1. package/components/alert/alert.css +5 -0
  2. package/components/alert/alert.js +4 -2
  3. package/components/button/button.js +4 -1
  4. package/components/chat/chat-input.js +13 -2
  5. package/components/description-list/description-list.js +4 -3
  6. package/components/field/field.css +113 -63
  7. package/components/field/field.js +44 -142
  8. package/components/icon/icon.a2ui.json +1 -1
  9. package/components/icon/icon.css +16 -0
  10. package/components/icon/icon.js +18 -0
  11. package/components/icon/icon.yaml +6 -2
  12. package/components/index.js +1 -0
  13. package/components/input/input.a2ui.json +1 -1
  14. package/components/input/input.css +19 -23
  15. package/components/input/input.js +36 -9
  16. package/components/input/input.yaml +3 -1
  17. package/components/option-card/option-card.a2ui.json +262 -0
  18. package/components/option-card/option-card.css +215 -0
  19. package/components/option-card/option-card.js +158 -0
  20. package/components/option-card/option-card.yaml +234 -0
  21. package/components/rating/rating.a2ui.json +10 -0
  22. package/components/rating/rating.yaml +8 -0
  23. package/components/segment/segment.a2ui.json +5 -0
  24. package/components/segment/segment.css +2 -0
  25. package/components/segment/segment.js +21 -1
  26. package/components/segment/segment.yaml +5 -0
  27. package/components/textarea/textarea.css +1 -1
  28. package/components/textarea/textarea.js +2 -2
  29. package/core/data-stream.js +21 -0
  30. package/core/form.js +5 -0
  31. package/core/index.js +2 -0
  32. package/core/streams-bridge.js +96 -0
  33. package/package.json +1 -1
  34. package/styles/colors/semantics.css +8 -3
  35. package/styles/components.css +1 -0
  36. package/styles/prose.css +3 -7
  37. package/styles/tokens.css +7 -4
@@ -1,6 +1,9 @@
1
1
  /**
2
- * <segment-ui value="tab1" text="Tab 1"></segment-ui>
2
+ * <segment-ui value="tab1" text="Tab 1" icon="list"></segment-ui>
3
3
  * Child of segmented-ui. Represents one option.
4
+ *
5
+ * `icon=` stamps a leading <icon-ui name="…"> child on render.
6
+ * Mirrors the button-ui pattern.
4
7
  */
5
8
 
6
9
  import { AdiaElement } from '../../core/element.js';
@@ -9,6 +12,7 @@ class AdiaSegment extends AdiaElement {
9
12
  static properties = {
10
13
  value: { type: String, default: '', reflect: true },
11
14
  text: { type: String, default: '', reflect: true },
15
+ icon: { type: String, default: '', reflect: true },
12
16
  disabled: { type: Boolean, default: false, reflect: true },
13
17
  selected: { type: Boolean, default: false, reflect: true },
14
18
  };
@@ -25,6 +29,22 @@ class AdiaSegment extends AdiaElement {
25
29
  this.setAttribute('aria-checked', this.selected ? 'true' : 'false');
26
30
  if (this.disabled) this.setAttribute('aria-disabled', 'true');
27
31
  else this.removeAttribute('aria-disabled');
32
+
33
+ // Stamp/refresh leading icon-ui child when [icon] is set.
34
+ // Mirrors button-ui's pattern (button.js:29) — author code never
35
+ // hand-rolls an icon-ui inside a segment.
36
+ if (this.icon) {
37
+ const existing = this.querySelector(':scope > icon-ui');
38
+ if (!existing || existing.getAttribute('name') !== this.icon) {
39
+ existing?.remove();
40
+ const iconEl = document.createElement('icon-ui');
41
+ iconEl.setAttribute('name', this.icon);
42
+ iconEl.setAttribute('aria-hidden', 'true');
43
+ this.prepend(iconEl);
44
+ }
45
+ } else {
46
+ this.querySelector(':scope > icon-ui')?.remove();
47
+ }
28
48
  }
29
49
  }
30
50
  customElements.define('segment-ui', AdiaSegment);
@@ -13,6 +13,11 @@ props:
13
13
  type: boolean
14
14
  default: false
15
15
  reflect: true
16
+ icon:
17
+ description: Phosphor icon name. Rendered as a leading <icon-ui> child stamped on render.
18
+ type: string
19
+ default: ""
20
+ reflect: true
16
21
  selected:
17
22
  description: Whether this segment is currently selected. Managed by the parent Segmented container; don't set multiple siblings selected.
18
23
  type: boolean
@@ -91,7 +91,7 @@
91
91
 
92
92
  /* Placeholder */
93
93
  [slot="text"][data-empty]::before {
94
- content: attr(aria-placeholder);
94
+ content: attr(data-placeholder);
95
95
  color: var(--textarea-placeholder-fg);
96
96
  pointer-events: none;
97
97
  }
@@ -29,7 +29,7 @@ class AdiaTextarea extends AdiaFormElement {
29
29
  ${this.label ? `<label slot="label" label="${this.label}"></label>` : ''}
30
30
  <div slot="text" contenteditable="plaintext-only" tabindex="0"
31
31
  ${this.value ? '' : 'data-empty=""'}
32
- aria-placeholder="${this.placeholder}"></div>
32
+ data-placeholder="${this.placeholder}"></div>
33
33
  `;
34
34
  }
35
35
 
@@ -47,7 +47,7 @@ class AdiaTextarea extends AdiaFormElement {
47
47
  render() {
48
48
  if (!this.#textEl) return;
49
49
 
50
- this.#textEl.setAttribute('aria-placeholder', this.placeholder);
50
+ this.#textEl.setAttribute('data-placeholder', this.placeholder);
51
51
 
52
52
  if (this.disabled || this.readonly) {
53
53
  this.#textEl.contentEditable = 'false';
@@ -58,6 +58,7 @@ const ATTRS = {
58
58
 
59
59
  const STREAMS = new Map(); /* streamId → { signal, refs, transport, opts } */
60
60
  const ELEMENT_STREAMS = new WeakMap(); /* el → { streamId, dispose, loaded } */
61
+ const PENDING = new Map(); /* streamId → array of whenStream resolvers */
61
62
  const WARNED = new WeakSet();
62
63
 
63
64
  /* Read-only public view of the streams registry. App code can do:
@@ -70,6 +71,21 @@ export const streams = {
70
71
  size() { return STREAMS.size; },
71
72
  };
72
73
 
74
+ /* Resolves with the stream entry once it's registered. If the stream
75
+ already exists, resolves synchronously on the next microtask; if not,
76
+ resolves the moment the first DOM consumer (or any caller of
77
+ acquireStream) creates it. Useful for cross-package adapters
78
+ (streams-bridge, A2UI surfaces) that may run before the DOM source
79
+ has connected. The promise never rejects — pair with AbortSignal at
80
+ the caller if you need cancellation. */
81
+ export function whenStream(id) {
82
+ if (STREAMS.has(id)) return Promise.resolve(STREAMS.get(id));
83
+ return new Promise((resolve) => {
84
+ if (!PENDING.has(id)) PENDING.set(id, []);
85
+ PENDING.get(id).push(resolve);
86
+ });
87
+ }
88
+
73
89
  function attr(el, key) { return el.getAttribute(ATTRS[key]) ?? ''; }
74
90
  function dispatch(el, name, detail) {
75
91
  el.dispatchEvent(new CustomEvent(name, { bubbles: true, detail }));
@@ -309,6 +325,11 @@ function acquireStream(id, opts) {
309
325
  };
310
326
  STREAMS.set(id, stream);
311
327
  stream.transport = startTransport(stream);
328
+ if (PENDING.has(id)) {
329
+ const resolvers = PENDING.get(id);
330
+ PENDING.delete(id);
331
+ for (const r of resolvers) r(stream);
332
+ }
312
333
  return stream;
313
334
  }
314
335
 
package/core/form.js CHANGED
@@ -188,10 +188,15 @@ export class AdiaFormElement extends AdiaElement {
188
188
  * slots; the per-control `label` attr renders via an inert shadow
189
189
  * slot without `[for]`, so click-to-focus doesn't work.
190
190
  *
191
+ * Subclasses that have promoted `label` to a first-class API with
192
+ * proper a11y wiring (e.g. input-ui's inline-leading caption with
193
+ * aria-labelledby) opt out by setting `static labelDeprecated = false`.
194
+ *
191
195
  * One-shot per class so the console isn't spammed on docs pages.
192
196
  */
193
197
  #warnDeprecatedLabel() {
194
198
  const ctor = this.constructor;
199
+ if (ctor.labelDeprecated === false) return;
195
200
  if (!ctor.properties?.label) return;
196
201
  if (!this.hasAttribute('label')) return;
197
202
  if (AdiaFormElement.#labelWarned.has(ctor)) return;
package/core/index.js CHANGED
@@ -23,3 +23,5 @@ export * from './icons.js';
23
23
  export * from './markdown.js';
24
24
  export * from './transport.js';
25
25
  export * from './polyfills.js';
26
+ export { streams, whenStream } from './data-stream.js';
27
+ export * from './streams-bridge.js';
@@ -0,0 +1,96 @@
1
+ /**
2
+ * streams-bridge — proxy data-stream signals into A2UI surface data
3
+ * models (OD-CHART-16, option b).
4
+ *
5
+ * effect()
6
+ * data-stream signal ──────────────────────▶ renderer.process({
7
+ * (any consumer: type: 'updateDataModel',
8
+ * chart-ui, table-ui, surfaceId, path, value
9
+ * headless app code) })
10
+ *
11
+ * Lives in `web-components/core/` rather than `a2ui-utils/` because
12
+ * the dependency direction is web-components → a2ui-utils, not the
13
+ * reverse. The bridge duck-types the renderer (it needs `.process()`,
14
+ * nothing else), so it works with any A2UI-protocol consumer — the
15
+ * actual `A2UIRenderer` from `@adia-ai/a2ui-utils`, a custom renderer,
16
+ * or a test stub.
17
+ *
18
+ * Contract:
19
+ * - One-way only: stream → data model. A2UI writes back to its model
20
+ * (e.g. via `update-data-model` wiring actions) do NOT propagate
21
+ * into the stream signal. If you need bidirectional state, the
22
+ * stream is the wrong substrate — use a wiring action.
23
+ * - Tear down with the returned dispose function. Dispose detaches
24
+ * the effect; it does NOT release the stream's refcount, because
25
+ * the bridge isn't the source of truth for the stream's lifecycle
26
+ * (the DOM consumer or a separate `acquireStream` caller is).
27
+ * - `select` uses dot-separated path syntax (matches
28
+ * `data-stream-path` on the trait). `path` uses slash-separated
29
+ * syntax (matches A2UI bindings).
30
+ *
31
+ * Usage:
32
+ * const dispose = bridgeStream(renderer, {
33
+ * surfaceId: 'main',
34
+ * streamId: 'id:rev', // explicit data-stream-id
35
+ * path: 'metrics/revenue', // A2UI data-model path
36
+ * select: 'data', // optional: sub-path within stream
37
+ * });
38
+ * // ... later
39
+ * dispose();
40
+ *
41
+ * // For DOM-first sources where the stream may register after the
42
+ * // bridge call returns:
43
+ * const dispose = await bridgeStreamAsync(renderer, opts);
44
+ */
45
+
46
+ import { effect } from './signals.js';
47
+ import { streams, whenStream } from './data-stream.js';
48
+
49
+ function resolveSelect(obj, dottedPath) {
50
+ if (!dottedPath) return obj;
51
+ let cursor = obj;
52
+ for (const seg of dottedPath.split('.')) {
53
+ if (cursor == null) return null;
54
+ cursor = cursor[seg];
55
+ }
56
+ return cursor;
57
+ }
58
+
59
+ function attach(renderer, stream, { surfaceId, path, select }) {
60
+ return effect(() => {
61
+ const value = stream.signal.value;
62
+ if (value == null) return;
63
+ const out = select ? resolveSelect(value, select) : value;
64
+ renderer.process({ type: 'updateDataModel', surfaceId, path, value: out });
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Bridge a registered stream into a renderer's surface data model.
70
+ * If the stream is not yet registered, logs a warning and returns a
71
+ * no-op dispose. Use {@link bridgeStreamAsync} when the stream might
72
+ * register after this call (e.g. DOM source connecting on the same
73
+ * tick as the bridge wiring).
74
+ */
75
+ export function bridgeStream(renderer, opts) {
76
+ const { streamId } = opts;
77
+ const stream = streams.get(streamId);
78
+ if (!stream) {
79
+ console.warn(
80
+ `[streams-bridge] stream "${streamId}" not registered — ` +
81
+ `call bridgeStreamAsync() to wait, or ensure the DOM source is ` +
82
+ `connected before bridging.`,
83
+ );
84
+ return () => {};
85
+ }
86
+ return attach(renderer, stream, opts);
87
+ }
88
+
89
+ /**
90
+ * Async variant — awaits {@link whenStream} before attaching the
91
+ * effect. Resolves once the bridge is wired (returns the dispose fn).
92
+ */
93
+ export async function bridgeStreamAsync(renderer, opts) {
94
+ const stream = await whenStream(opts.streamId);
95
+ return attach(renderer, stream, opts);
96
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
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": {
@@ -149,9 +149,14 @@
149
149
  schemes. Anchor color in `typography.css` reads --a-link rather
150
150
  than --a-accent-strong; emphasis on hover comes from a tighter
151
151
  chroma + darker step rather than full saturation jump. */
152
- --a-link: light-dark(var(--a-accent-65-shade), var(--a-accent-65-tint));
153
- --a-link-hover: light-dark(var(--a-accent-70-shade), var(--a-accent-70-tint));
154
- --a-link-visited: light-dark(var(--a-accent-75-shade), var(--a-accent-75-tint));
152
+ /* Polarity matches `--a-accent-N` family wrappers: at fine steps >50,
153
+ `tint` is the lower-L (deeper) primitive and `shade` is the higher-L
154
+ primitive the naming inverts past the convergence point. Light
155
+ mode wants dark-on-light, so we pick `tint` (deep accent); dark
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));
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));
155
160
 
156
161
  /* Accent text on accent solid bg — needs light text on dark-ish accent */
157
162
  --a-accent-text-strong: light-dark(var(--a-accent-05-shade), var(--a-accent-05-tint));
@@ -17,6 +17,7 @@
17
17
  @import "../components/textarea/textarea.css";
18
18
  @import "../components/check/check.css";
19
19
  @import "../components/radio/radio.css";
20
+ @import "../components/option-card/option-card.css";
20
21
  @import "../components/switch/switch.css";
21
22
  @import "../components/slider/slider.css";
22
23
  @import "../components/select/select.css";
package/styles/prose.css CHANGED
@@ -12,10 +12,9 @@
12
12
 
13
13
  [prose] {
14
14
  /* ── Radius — larger bases than UI ── */
15
- --a-radius-max: 1.125rem;
16
- --a-radius-sm: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.50), var(--a-radius-max));
17
- --a-radius-md: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.625), var(--a-radius-max));
18
- --a-radius-lg: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.75), var(--a-radius-max));
15
+ --a-radius-sm: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-sm-k)), var(--a-radius-max));
16
+ --a-radius-md: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-md-k)), var(--a-radius-max));
17
+ --a-radius-lg: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-lg-k)), var(--a-radius-max));
19
18
  --a-radius: var(--a-radius-md);
20
19
 
21
20
  /* ── Inset — shifted up the space scale ── */
@@ -165,7 +164,6 @@
165
164
  }
166
165
 
167
166
  [prose] table {
168
- margin-block: var(--a-space-4);
169
167
  border: 1px solid var(--a-border-subtle);
170
168
  border-radius: var(--a-radius-md);
171
169
  overflow: hidden;
@@ -177,10 +175,8 @@
177
175
 
178
176
  [prose] pre {
179
177
  padding: var(--a-space-5);
180
- margin-block: var(--a-space-4);
181
178
  }
182
179
 
183
180
  [prose] img {
184
181
  border-radius: var(--a-radius-lg);
185
- margin-block: var(--a-space-3);
186
182
  }
package/styles/tokens.css CHANGED
@@ -82,10 +82,13 @@
82
82
  At k=1, values match the original fixed scale.
83
83
  k=0 → sharp (clamped to min). k=2 → extra round (clamped to max). */
84
84
  --a-radius-min: 0.25rem;
85
- --a-radius-max: 1.5rem;
86
- --a-radius-sm: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.25), var(--a-radius-max));
87
- --a-radius-md: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.50), var(--a-radius-max));
88
- --a-radius-lg: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * 0.75), var(--a-radius-max));
85
+ --a-radius-max: 1.25rem;
86
+ --a-radius-sm-k: 0.333;
87
+ --a-radius-md-k: 0.666;
88
+ --a-radius-lg-k: 1.000;
89
+ --a-radius-sm: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-sm-k)), var(--a-radius-max));
90
+ --a-radius-md: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-md-k)), var(--a-radius-max));
91
+ --a-radius-lg: clamp(var(--a-radius-min), calc(var(--a-radius-k) * var(--a-radius-max) * var(--a-radius-lg-k)), var(--a-radius-max));
89
92
  --a-radius-full: 100rem;
90
93
  --a-radius: var(--a-radius-md);
91
94