@adia-ai/web-components 0.0.13 → 0.0.15

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
@@ -15,7 +15,7 @@ A2UI protocol messages into live DOM.
15
15
  <link rel="stylesheet" href="node_modules/@adia-ai/web-components/index.css" />
16
16
 
17
17
  <script type="module">
18
- import '@adia-ai/web-components'; // registers every *-n tag
18
+ import '@adia-ai/web-components'; // registers every *-ui tag
19
19
  </script>
20
20
 
21
21
  <card-ui>
@@ -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>/
@@ -59,7 +61,7 @@ web-components/
59
61
  │ │ with @adia-ai/a2ui-utils)
60
62
  │ └── index.js — registers all patterns
61
63
 
62
- ├── traits/ — 40 composable behaviors via defineTrait()
64
+ ├── traits/ — 42 composable behaviors via defineTrait()
63
65
  │ (pressable, focusTrap, confetti, resizable, …)
64
66
 
65
67
  ├── a2ui/ — deprecation shim for one release
@@ -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
@@ -323,6 +323,35 @@
323
323
  margin: 0;
324
324
  }
325
325
 
326
+ /* Footer heading — trend-stat line (symmetric with header's [slot="heading"]).
327
+ Used for chart trend-footer patterns like "Trending up by 5.2% this month".
328
+ Paired with [slot="description"] for a "Jan – Mar 2024" period caption. */
329
+ > footer > [slot="heading"] {
330
+ font-size: var(--card-font-size);
331
+ font-weight: var(--a-weight-medium);
332
+ color: var(--card-heading-fg);
333
+ line-height: 1.3;
334
+ margin: 0;
335
+ flex: 0 0 auto;
336
+ }
337
+
338
+ /* Footer with heading + description = column stack, heading on top. The
339
+ existing flex-row layout becomes a flex-column when a heading is present,
340
+ so the trend line and period caption stack naturally. */
341
+ > footer:has(> [slot="heading"]) {
342
+ flex-direction: column;
343
+ align-items: flex-start;
344
+ gap: var(--a-space-0-5);
345
+ }
346
+
347
+ > footer:has(> [slot="heading"])[justify="end"] {
348
+ align-items: flex-end;
349
+ }
350
+
351
+ > footer:has(> [slot="heading"])[justify="center"] {
352
+ align-items: center;
353
+ }
354
+
326
355
  > footer [slot="action"] {
327
356
  margin-inline-start: auto;
328
357
  display: flex;
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://adiaui.dev/a2ui/v0_9/components/Chart.json",
4
4
  "title": "Chart",
5
- "description": "Declarative SVG chart supporting bar, line, pie, donut, radar, sparkline, stacked-bar, grouped-bar, and multi-line types.",
5
+ "description": "Declarative SVG chart supporting 18 types: bar, line, pie, donut, radar, sparkline, segments, area, scatter, radial-bar, gauge, stacked-bar, grouped-bar, multi-line, funnel, treemap, sankey, and composed.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "properties": {
16
16
  "type": {
17
- "description": "Chart type. Only 'bar' is built-in.",
17
+ "description": "Chart type. All 18 enum values have dedicated render paths in chart.js.",
18
18
  "type": "string",
19
19
  "enum": [
20
20
  "bar",
@@ -23,14 +23,23 @@
23
23
  "donut",
24
24
  "radar",
25
25
  "sparkline",
26
+ "segments",
27
+ "area",
28
+ "scatter",
29
+ "radial-bar",
30
+ "gauge",
26
31
  "stacked-bar",
27
32
  "grouped-bar",
28
- "multi-line"
33
+ "multi-line",
34
+ "funnel",
35
+ "treemap",
36
+ "sankey",
37
+ "composed"
29
38
  ],
30
39
  "default": "bar"
31
40
  },
32
41
  "aspect": {
33
- "description": "Aspect ratio",
42
+ "description": "DEPRECATED (OD-CHART-02). Parents should size the chart directly (width/height on the enclosing card or container). Still honored for back-compat; emits a one-shot console.warn per instance when set.",
34
43
  "type": "string",
35
44
  "enum": [
36
45
  "std",
@@ -55,8 +64,19 @@
55
64
  "component": {
56
65
  "const": "Chart"
57
66
  },
67
+ "format": {
68
+ "description": "Number-format mode applied to axis labels + value overlays + donut total + gauge value + treemap value + funnel value + internal tooltip. `abbr` is the legacy 1.2K / 3M format; `decimal` fixes 2 decimals; `currency` prefixes via `--chart-currency-prefix` token (default \"$\"); `percent` multiplies × 100 and adds a % suffix.",
69
+ "type": "string",
70
+ "enum": [
71
+ "abbr",
72
+ "decimal",
73
+ "currency",
74
+ "percent"
75
+ ],
76
+ "default": "abbr"
77
+ },
58
78
  "heading": {
59
- "description": "Chart heading text rendered above the canvas.",
79
+ "description": "DEPRECATED (OD-CHART-02). Place chart titles in an enclosing card-ui's `<header><span slot=\"heading\">...</span></header>` instead. Still honored for back-compat; emits a one-shot console.warn per instance when set. Used as the chart's `aria-label` when no explicit label is provided.",
60
80
  "type": "string",
61
81
  "default": ""
62
82
  },
@@ -113,7 +133,17 @@
113
133
  "x-adiaui": {
114
134
  "anti_patterns": [],
115
135
  "category": "agent",
116
- "events": {},
136
+ "events": {
137
+ "chart-hover": {
138
+ "description": "Fires when the pointer enters a datum (bar / dot / slice). Detail includes {label, value, pct, series, slot, pointerX, pointerY}. Only re-fires when the hovered datum changes — moving inside the same datum is quiet."
139
+ },
140
+ "chart-leave": {
141
+ "description": "Fires when the pointer leaves the plot area or a previously-hovered datum with no new one entering."
142
+ },
143
+ "chart-select": {
144
+ "description": "Fires on click of a datum with the same detail shape as chart-hover."
145
+ }
146
+ },
117
147
  "examples": [
118
148
  {
119
149
  "description": "Dashboard with KPI stat grid and two chart cards.",
@@ -158,12 +188,19 @@
158
188
  },
159
189
  "canvas": {
160
190
  "description": "Chart rendering area"
191
+ },
192
+ "empty": {
193
+ "description": "Shown in place of the SVG when `.data` is empty or missing. Typically an `<empty-state-ui>` with icon + heading + description + optional action. Visibility is CSS-toggled via `[data-has-data]` on the host — authors don't need to manage it imperatively."
161
194
  }
162
195
  },
163
196
  "states": [
164
197
  {
165
198
  "description": "Default, ready for interaction.",
166
199
  "name": "idle"
200
+ },
201
+ {
202
+ "description": "No data — the empty slot (if provided) is rendered; otherwise the chart area renders nothing. Host carries [data-has-data] only when data is present.",
203
+ "name": "empty"
167
204
  }
168
205
  ],
169
206
  "synonyms": {
@@ -3,6 +3,15 @@
3
3
  /* ── Tokens ── */
4
4
  --chart-font: var(--a-font-family);
5
5
  --chart-font-size: var(--a-ui-sm);
6
+ /* NOTE — `--chart-currency-prefix` is intentionally NOT declared here.
7
+ Declaring a default at `:where(:scope)` would shadow ancestor values
8
+ via the cascade (the child's explicit declaration wins over an
9
+ inherited ancestor value, regardless of specificity). Instead,
10
+ chart.js's `#fmtValue()` falls back to `'$'` when the computed
11
+ value is empty — so ancestors can set `--chart-currency-prefix: "€"`
12
+ and the chart inherits that value naturally. To register the
13
+ default formally (and enable transitions), add an `@property` rule
14
+ at `:root` in a global stylesheet. */
6
15
  --chart-fg: var(--a-fg);
7
16
  --chart-bar: var(--a-accent);
8
17
  --chart-bar-hover: var(--a-accent-bg-hover);
@@ -69,6 +78,29 @@
69
78
  position: relative;
70
79
  container-type: inline-size;
71
80
  container-name: chart;
81
+ /* Suppress default focus ring — we paint our own via [data-a11y-focused]
82
+ below so the ring only shows when the chart has a focused datum
83
+ (keyboard-only indicator, not just "the chart is tabindex=0"). */
84
+ outline: none;
85
+ }
86
+
87
+ /* OD-CHART-06 — keyboard a11y focus ring. Painted on the host when a
88
+ datum is focused via keyboard. Uses the canonical --a-focus-ring
89
+ recipe so it matches every other focusable primitive. */
90
+ :scope[data-a11y-focused]:focus-visible {
91
+ box-shadow: var(--a-focus-ring);
92
+ border-radius: var(--a-radius-sm);
93
+ }
94
+
95
+ /* Per-datum focus indicator — marks the currently-focused datum
96
+ via a data-a11y-focus attr set by chart.js on keyboard nav.
97
+ SVG paintable elements can't take box-shadow, so use a stroke
98
+ + drop-shadow filter to approximate a ring. */
99
+ [data-a11y-focus] {
100
+ stroke: var(--a-focus-color);
101
+ stroke-width: 2;
102
+ paint-order: stroke fill;
103
+ filter: drop-shadow(0 0 2px var(--a-focus-color));
72
104
  }
73
105
 
74
106
  /* Title + legend are fixed-height siblings; SVG gets the remaining
@@ -76,6 +108,19 @@
76
108
  inside the max-height envelope instead of pushing the legend out. */
77
109
  [data-chart-heading] { flex: 0 0 auto; }
78
110
  [data-legend] { flex: 0 0 auto; }
111
+
112
+ /* Empty-state slot — author places <empty-state-ui slot="empty"> inside
113
+ chart-ui. CSS toggles visibility on [data-has-data] (set by chart.js
114
+ when .data is non-empty). No data ⇒ empty-state visible; data ⇒ hidden. */
115
+ > [slot="empty"] { display: none; }
116
+ :scope:not([data-has-data]) > [slot="empty"] {
117
+ display: flex;
118
+ flex: 1 1 auto;
119
+ align-items: center;
120
+ justify-content: center;
121
+ min-height: 0;
122
+ }
123
+
79
124
  :scope > div:not([data-chart-heading]):not([data-legend]) {
80
125
  flex: 1 1 auto;
81
126
  min-height: 0;
@@ -354,6 +399,15 @@
354
399
  font-family: var(--chart-font);
355
400
  }
356
401
 
402
+ /* Container query — the chart element provides `container-type: inline-size`
403
+ + `container-name: chart` (above). Hide the internal legend at small
404
+ widths where the rows would dominate the chart visual. Consumers who
405
+ want a legend at small sizes should place an external chart-legend-ui
406
+ adjacent in a larger container. */
407
+ @container chart (max-width: 200px) {
408
+ [data-legend] { display: none; }
409
+ }
410
+
357
411
  [data-legend-item] {
358
412
  display: flex;
359
413
  flex-direction: row;
@@ -379,6 +433,176 @@
379
433
  [data-legend-dot][data-slice="8"] { background: var(--chart-8); }
380
434
  [data-legend-dot][data-slice="9"] { background: var(--chart-9); }
381
435
 
436
+ /* ── Area chart ──
437
+ Reuses #renderLine() but emphasizes the filled region. Overrides the
438
+ default `[data-area]` opacity of 0.15 (line-chart aesthetic) to 0.35
439
+ so the fill becomes the dominant visual. The `--chart-area-fill-opacity`
440
+ token is the knob for consumers to tune; bump for more presence, dial
441
+ down for subtler. */
442
+ :scope[type="area"] {
443
+ --chart-area-fill-opacity: 0.35;
444
+ }
445
+ :scope[type="area"] [data-area] {
446
+ fill: var(--chart-line);
447
+ opacity: var(--chart-area-fill-opacity);
448
+ stroke: none;
449
+ }
450
+ :scope[type="area"] [data-line] {
451
+ stroke-width: var(--chart-line-width);
452
+ }
453
+
454
+ /* ── Scatter ──
455
+ Only dots emitted (no line/area paths). Dots are slightly larger than
456
+ line-chart dots by default for readability. */
457
+ :scope[type="scatter"] [data-dot][data-scatter] {
458
+ fill: var(--chart-dot);
459
+ stroke: var(--chart-dot-stroke);
460
+ stroke-width: 1.5;
461
+ }
462
+
463
+ /* ── Radial bar ──
464
+ Concentric rings — each ring stroked with bandwidth. Track is the
465
+ faint backing, filled arc is accent. Stroke-linecap:round in the path
466
+ gives rounded end-caps on the arcs. */
467
+ [data-radial-track] {
468
+ stroke: var(--a-border-subtle);
469
+ fill: none;
470
+ }
471
+ [data-radial-bar] {
472
+ stroke: var(--chart-bar);
473
+ }
474
+ [data-radial-bar][data-slice="0"] { stroke: var(--chart-0); }
475
+ [data-radial-bar][data-slice="1"] { stroke: var(--chart-1); }
476
+ [data-radial-bar][data-slice="2"] { stroke: var(--chart-2); }
477
+ [data-radial-bar][data-slice="3"] { stroke: var(--chart-3); }
478
+ [data-radial-bar][data-slice="4"] { stroke: var(--chart-4); }
479
+ [data-radial-bar][data-slice="5"] { stroke: var(--chart-5); }
480
+ [data-radial-bar][data-slice="6"] { stroke: var(--chart-6); }
481
+ [data-radial-bar][data-slice="7"] { stroke: var(--chart-7); }
482
+ [data-radial-bar][data-slice="8"] { stroke: var(--chart-8); }
483
+ [data-radial-bar][data-slice="9"] { stroke: var(--chart-9); }
484
+
485
+ /* ── Gauge ──
486
+ Half-donut — track is faint backing, fill reads as primary accent. */
487
+ :scope[type="gauge"] [data-radial-track] {
488
+ fill: var(--a-border-subtle);
489
+ stroke: none;
490
+ }
491
+ :scope[type="gauge"] [data-gauge-fill] {
492
+ fill: var(--chart-bar);
493
+ stroke: none;
494
+ }
495
+ [data-gauge-value] {
496
+ font-weight: var(--a-weight-semibold);
497
+ fill: var(--chart-fg);
498
+ }
499
+ [data-gauge-max] {
500
+ fill: var(--chart-label);
501
+ font-family: var(--chart-font);
502
+ }
503
+
504
+ /* ── Funnel ──
505
+ Stages stacked top-to-bottom. Trapezoid paths carry data-slice so
506
+ the data palette colors each stage. Labels live outside the
507
+ funnel body — left (stage name) and right (value + drop %). */
508
+ [data-funnel-stage] {
509
+ fill: var(--chart-bar);
510
+ }
511
+ [data-funnel-stage][data-slice="0"] { fill: var(--chart-0); }
512
+ [data-funnel-stage][data-slice="1"] { fill: var(--chart-1); }
513
+ [data-funnel-stage][data-slice="2"] { fill: var(--chart-2); }
514
+ [data-funnel-stage][data-slice="3"] { fill: var(--chart-3); }
515
+ [data-funnel-stage][data-slice="4"] { fill: var(--chart-4); }
516
+ [data-funnel-stage][data-slice="5"] { fill: var(--chart-5); }
517
+ [data-funnel-stage][data-slice="6"] { fill: var(--chart-6); }
518
+ [data-funnel-stage][data-slice="7"] { fill: var(--chart-7); }
519
+ [data-funnel-stage][data-slice="8"] { fill: var(--chart-8); }
520
+ [data-funnel-stage][data-slice="9"] { fill: var(--chart-9); }
521
+
522
+ [data-funnel-label] {
523
+ fill: var(--chart-fg);
524
+ font-weight: var(--a-weight-medium);
525
+ }
526
+ [data-funnel-value] {
527
+ fill: var(--chart-fg);
528
+ font-weight: var(--a-weight-semibold);
529
+ }
530
+ [data-funnel-drop] {
531
+ fill: var(--chart-label);
532
+ }
533
+
534
+ /* ── Treemap ──
535
+ Flat tiles — each rect filled by the data palette. Labels +
536
+ values appear only on tiles large enough to fit them (size
537
+ check done in JS). Rounded corners pick up the standard
538
+ --a-radius via #resolveRadius(). */
539
+ [data-treemap-tile] {
540
+ stroke: var(--chart-dot-stroke);
541
+ stroke-width: 0;
542
+ }
543
+ [data-treemap-tile][data-slice="0"] { fill: var(--chart-0); }
544
+ [data-treemap-tile][data-slice="1"] { fill: var(--chart-1); }
545
+ [data-treemap-tile][data-slice="2"] { fill: var(--chart-2); }
546
+ [data-treemap-tile][data-slice="3"] { fill: var(--chart-3); }
547
+ [data-treemap-tile][data-slice="4"] { fill: var(--chart-4); }
548
+ [data-treemap-tile][data-slice="5"] { fill: var(--chart-5); }
549
+ [data-treemap-tile][data-slice="6"] { fill: var(--chart-6); }
550
+ [data-treemap-tile][data-slice="7"] { fill: var(--chart-7); }
551
+ [data-treemap-tile][data-slice="8"] { fill: var(--chart-8); }
552
+ [data-treemap-tile][data-slice="9"] { fill: var(--chart-9); }
553
+
554
+ [data-treemap-label] {
555
+ fill: var(--a-canvas-0);
556
+ font-weight: var(--a-weight-semibold);
557
+ }
558
+ [data-treemap-value] {
559
+ fill: var(--a-canvas-0);
560
+ opacity: 0.85;
561
+ }
562
+
563
+ /* ── Sankey ──
564
+ Left + right column nodes, curved bezier flows between. Flow
565
+ bands render semi-transparent so overlapping routes remain
566
+ distinguishable. Nodes adopt the data palette. */
567
+ [data-sankey-node] {
568
+ fill: var(--chart-bar);
569
+ }
570
+ [data-sankey-node][data-slice="0"] { fill: var(--chart-0); }
571
+ [data-sankey-node][data-slice="1"] { fill: var(--chart-1); }
572
+ [data-sankey-node][data-slice="2"] { fill: var(--chart-2); }
573
+ [data-sankey-node][data-slice="3"] { fill: var(--chart-3); }
574
+ [data-sankey-node][data-slice="4"] { fill: var(--chart-4); }
575
+ [data-sankey-node][data-slice="5"] { fill: var(--chart-5); }
576
+ [data-sankey-node][data-slice="6"] { fill: var(--chart-6); }
577
+ [data-sankey-node][data-slice="7"] { fill: var(--chart-7); }
578
+ [data-sankey-node][data-slice="8"] { fill: var(--chart-8); }
579
+ [data-sankey-node][data-slice="9"] { fill: var(--chart-9); }
580
+
581
+ [data-sankey-link] {
582
+ fill: var(--a-fg-subtle);
583
+ opacity: 0.35;
584
+ }
585
+ [data-sankey-link]:hover {
586
+ opacity: 0.6;
587
+ }
588
+ [data-sankey-label] {
589
+ fill: var(--chart-fg);
590
+ font-weight: var(--a-weight-medium);
591
+ }
592
+
593
+ /* ── Composed (bar + line overlay) ──
594
+ The bar uses slot 0 + the line uses slot 1 by default via the
595
+ --color-{key} injection in chart.js. CSS here just makes sure
596
+ the line reads over the bar visually — the line's data-line
597
+ element already picks up existing [data-line] styling. */
598
+ :scope[type="composed"] [data-line] {
599
+ stroke-width: calc(var(--chart-line-width) + 1);
600
+ }
601
+ :scope[type="composed"] [data-dot] {
602
+ stroke: var(--chart-dot-stroke);
603
+ stroke-width: 2;
604
+ }
605
+
382
606
  /* ── Sparkline container ──
383
607
  When host has explicit height, fill it; otherwise give a sensible
384
608
  default of 2rem so the sparkline still has room to breathe. */