@adia-ai/web-components 0.0.12 → 0.0.14

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 (46) hide show
  1. package/README.md +10 -10
  2. package/components/card/card.css +29 -0
  3. package/components/chart/chart.a2ui.json +43 -6
  4. package/components/chart/chart.css +224 -0
  5. package/components/chart/chart.js +1049 -27
  6. package/components/chart/chart.yaml +62 -6
  7. package/components/chart-legend/chart-legend.a2ui.json +139 -0
  8. package/components/chart-legend/chart-legend.css +124 -0
  9. package/components/chart-legend/chart-legend.js +185 -0
  10. package/components/chart-legend/chart-legend.yaml +133 -0
  11. package/components/code/code-editor.js +161 -0
  12. package/components/code/code.a2ui.json +59 -0
  13. package/components/code/code.css +78 -2
  14. package/components/code/code.js +147 -9
  15. package/components/code/code.yaml +42 -0
  16. package/components/heatmap/heatmap.js +62 -13
  17. package/components/index.js +1 -0
  18. package/components/select/select.css +1 -1
  19. package/components/slider/slider.js +8 -3
  20. package/components/stat/stat.a2ui.json +3 -0
  21. package/components/stat/stat.css +32 -0
  22. package/components/stat/stat.yaml +6 -0
  23. package/components/tooltip/tooltip.a2ui.json +29 -4
  24. package/components/tooltip/tooltip.css +111 -0
  25. package/components/tooltip/tooltip.js +200 -12
  26. package/components/tooltip/tooltip.yaml +38 -4
  27. package/core/icons.js +35 -1
  28. package/core/index.js +25 -0
  29. package/core/provider.js +1 -1
  30. package/index.css +26 -0
  31. package/index.js +18 -0
  32. package/package.json +14 -6
  33. package/patterns/adia-chat/adia-chat.js +1 -1
  34. package/styles/colors/semantics.css +6 -5
  35. package/styles/{styles.css → components.css} +9 -111
  36. package/styles/resets.css +116 -0
  37. package/styles/tokens.css +8 -2
  38. package/core/_cm-core.js +0 -38
  39. package/core/_cm-theme.js +0 -58
  40. package/core/_lang-css.js +0 -2
  41. package/core/_lang-html.js +0 -2
  42. package/core/_lang-javascript.js +0 -2
  43. package/core/_lang-json.js +0 -2
  44. package/core/_lang-markdown.js +0 -2
  45. package/core/_lang-yaml.js +0 -2
  46. package/core/code-editor-bundle.js +0 -63
@@ -61,12 +61,16 @@ class AdiaHeatmap extends AdiaElement {
61
61
  if (!this.#bound) {
62
62
  this.#bound = true;
63
63
  this.addEventListener('pointerover', this.#onHover);
64
+ this.addEventListener('pointermove', this.#onMove);
65
+ this.addEventListener('pointerleave', this.#onLeave);
64
66
  this.addEventListener('click', this.#onClick);
65
67
  }
66
68
  }
67
69
 
68
70
  disconnected() {
69
71
  this.removeEventListener('pointerover', this.#onHover);
72
+ this.removeEventListener('pointermove', this.#onMove);
73
+ this.removeEventListener('pointerleave', this.#onLeave);
70
74
  this.removeEventListener('click', this.#onClick);
71
75
  this.#bound = false;
72
76
  }
@@ -217,28 +221,73 @@ class AdiaHeatmap extends AdiaElement {
217
221
  return target?.closest?.('[data-cell]');
218
222
  }
219
223
 
220
- #onHover = (e) => {
221
- const cell = this.#findCell(e.target);
222
- if (!cell || !cell.dataset.v) return;
224
+ /* Compose the cell-level event with the canonical chart-* shape so that
225
+ tooltip-ui[follows=pointer][for=this-heatmap] can render without caring
226
+ whether the source is a chart or a heatmap. */
227
+ #chartDetail(cell, event) {
223
228
  const r = Number(cell.dataset.r);
224
229
  const c = Number(cell.dataset.c);
225
230
  const v = Number(cell.dataset.v);
226
231
  const label = cell.getAttribute('aria-label') || '';
227
- this.dispatchEvent(new CustomEvent('cell-hover', {
228
- detail: { r, c, v, label }, bubbles: true,
229
- }));
232
+ return {
233
+ r, c,
234
+ label,
235
+ value: Number.isFinite(v) ? v : null,
236
+ pct: null,
237
+ series: null,
238
+ slot: 0,
239
+ pointerX: event?.clientX ?? null,
240
+ pointerY: event?.clientY ?? null,
241
+ };
242
+ }
243
+
244
+ #hoveredCell = null;
245
+
246
+ #onHover = (e) => {
247
+ const cell = this.#findCell(e.target);
248
+ if (!cell || !cell.dataset.v) return;
249
+ this.#hoveredCell = cell;
250
+ const detail = this.#chartDetail(cell, e);
251
+ /* Legacy cell-specific shape — kept for back-compat. */
252
+ this.dispatchEvent(new CustomEvent('cell-hover', { detail, bubbles: true }));
253
+ /* Canonical chart-hover shape for tooltip-ui[follows=pointer]. */
254
+ this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
255
+ };
256
+
257
+ #onMove = (e) => {
258
+ const cell = this.#findCell(e.target);
259
+ if (!cell || !cell.dataset.v) {
260
+ if (this.#hoveredCell) {
261
+ this.#hoveredCell = null;
262
+ this.dispatchEvent(new CustomEvent('chart-leave', { bubbles: true }));
263
+ }
264
+ return;
265
+ }
266
+ if (cell !== this.#hoveredCell) {
267
+ this.#hoveredCell = cell;
268
+ const detail = this.#chartDetail(cell, e);
269
+ this.dispatchEvent(new CustomEvent('cell-hover', { detail, bubbles: true }));
270
+ this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
271
+ } else {
272
+ /* Pointer still inside the same cell — re-fire chart-hover so the
273
+ pointer-follow tooltip can reposition without re-painting content. */
274
+ const detail = this.#chartDetail(cell, e);
275
+ this.dispatchEvent(new CustomEvent('chart-hover', { detail, bubbles: true }));
276
+ }
277
+ };
278
+
279
+ #onLeave = () => {
280
+ if (!this.#hoveredCell) return;
281
+ this.#hoveredCell = null;
282
+ this.dispatchEvent(new CustomEvent('chart-leave', { bubbles: true }));
230
283
  };
231
284
 
232
285
  #onClick = (e) => {
233
286
  const cell = this.#findCell(e.target);
234
287
  if (!cell || !cell.dataset.v) return;
235
- const r = Number(cell.dataset.r);
236
- const c = Number(cell.dataset.c);
237
- const v = Number(cell.dataset.v);
238
- const label = cell.getAttribute('aria-label') || '';
239
- this.dispatchEvent(new CustomEvent('cell-click', {
240
- detail: { r, c, v, label }, bubbles: true,
241
- }));
288
+ const detail = this.#chartDetail(cell, e);
289
+ this.dispatchEvent(new CustomEvent('cell-click', { detail, bubbles: true }));
290
+ this.dispatchEvent(new CustomEvent('chart-select', { detail, bubbles: true }));
242
291
  };
243
292
  }
244
293
 
@@ -48,6 +48,7 @@ export { AdiaRow } from './row/row.js';
48
48
  export { AdiaGrid } from './grid/grid.js';
49
49
  export { AdiaStack } from './stack/stack.js';
50
50
  export { AdiaChart } from './chart/chart.js';
51
+ export { AdiaChartLegend } from './chart-legend/chart-legend.js';
51
52
  export { AdiaPopover } from './popover/popover.js';
52
53
  export { AdiaAccordion, AdiaAccordionItem } from './accordion/accordion.js';
53
54
  export { AdiaDivider } from './divider/divider.js';
@@ -179,7 +179,7 @@ select-ui [slot="listbox"] {
179
179
  padding: var(--a-space-1);
180
180
  border: 1px solid var(--a-ui-border);
181
181
  border-radius: var(--a-radius);
182
- background: var(--a-bg-subtle);
182
+ background: var(--a-canvas-bright);
183
183
  box-shadow: var(--a-shadow-lg);
184
184
  max-height: 15rem;
185
185
  overflow-y: auto;
@@ -1,9 +1,14 @@
1
1
  /**
2
- * <slider-ui label="Width" value="63" min="0" max="200" step="1" suffix="rem"></slider-ui>
2
+ * <field-ui label="Width">
3
+ * <slider-ui value="63" min="0" max="200" step="1" suffix="rem"></slider-ui>
4
+ * </field-ui>
3
5
  *
4
- * Layout:
5
- * [label] [value] [suffix]
6
+ * Layout inside the field:
7
+ * [field label] [value] [suffix]
6
8
  * [====fill====●─────────────────track──────]
9
+ *
10
+ * Bare `<slider-ui label="…">` still works but logs a deprecation warning
11
+ * asking you to wrap in <field-ui> for proper label association.
7
12
  */
8
13
 
9
14
  import { AdiaFormElement } from '../../core/form.js';
@@ -83,6 +83,9 @@
83
83
  "change": {
84
84
  "description": "Child content region for the `change` slot."
85
85
  },
86
+ "chart": {
87
+ "description": "Inline sparkline / mini-chart slot. Typically a `<chart-ui slot=\"chart\" type=\"sparkline\">` positioned to the right of the value + change rows. The grid reflows to give the chart a sized right-column region; authors can use any chart type but sparklines / small bar variants read best."
88
+ },
86
89
  "icon": {
87
90
  "description": "Icon region — single icon-ui child."
88
91
  },
@@ -35,6 +35,38 @@
35
35
  align-items: baseline;
36
36
  }
37
37
 
38
+ /* ── Chart slot (inline sparkline) ──
39
+ When an author adds <chart-ui slot="chart"> (or any sparkline-shape
40
+ child), the grid reflows so the chart occupies the right column
41
+ across the value + change rows, fixed to the stat's right edge.
42
+ Hero number and chart read as a single visual unit. */
43
+ :scope:has([slot="chart"]) {
44
+ grid-template-columns: minmax(0, 1fr) minmax(5rem, 40%);
45
+ grid-template-areas:
46
+ "label icon"
47
+ "value chart"
48
+ "change chart";
49
+ align-items: end;
50
+ }
51
+
52
+ /* Cap the chart slot at a sparkline-appropriate size — otherwise the
53
+ chart's default aspect ratio stretches the row heights and bloats
54
+ the whole card. Consumers who want a taller inline chart can
55
+ override --stat-chart-max-height on the host. Uses `chart-ui[slot]`
56
+ compound selector to beat chart-ui's own `:scope { max-height: 28rem }`
57
+ (specificity 0,1,1 beats 0,0,1). `[slot="chart"]` alone is (0,1,0)
58
+ — enough on paper but empirically losing to chart-ui's own scope in
59
+ practice; the type selector makes it explicit. */
60
+ [slot="chart"] {
61
+ grid-area: chart;
62
+ align-self: center;
63
+ min-width: 0;
64
+ width: 100%;
65
+ }
66
+ chart-ui[slot="chart"] {
67
+ max-height: var(--stat-chart-max-height, 3rem);
68
+ }
69
+
38
70
  /* ── Label (eyebrow) ── */
39
71
  [slot="label"] {
40
72
  grid-area: label;
@@ -41,6 +41,12 @@ slots:
41
41
  description: "Label region — control label."
42
42
  value:
43
43
  description: "Child content region for the `value` slot."
44
+ chart:
45
+ description: >-
46
+ Inline sparkline / mini-chart slot. Typically a `<chart-ui slot="chart"
47
+ type="sparkline">` positioned to the right of the value + change rows.
48
+ The grid reflows to give the chart a sized right-column region; authors
49
+ can use any chart type but sparklines / small bar variants read best.
44
50
  states:
45
51
  - name: idle
46
52
  description: Default, ready for interaction.
@@ -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/Tooltip.json",
4
4
  "title": "Tooltip",
5
- "description": "Tooltip popup on hover/focus. Uses Popover API for top-layer rendering.",
5
+ "description": "Tooltip popup. Two modes — default `follows=\"trigger\"` shows on hover/focus anchored to wrapped children; `follows=\"pointer\"` subscribes to chart-hover events from [for] and renders a data-viz card that tracks the pointer.",
6
6
  "type": "object",
7
7
  "allOf": [
8
8
  {
@@ -17,12 +17,37 @@
17
17
  "const": "Tooltip"
18
18
  },
19
19
  "delay": {
20
- "description": "Delay in milliseconds before showing the tooltip on hover.",
20
+ "description": "Delay in milliseconds before showing the tooltip on hover (trigger mode only).",
21
21
  "type": "number",
22
22
  "default": 400
23
23
  },
24
+ "follows": {
25
+ "description": "`trigger` (default) pins to wrapped children via hover/focus. `pointer` subscribes to chart-hover/chart-leave events from [for] and positions at the pointer coordinates — used by chart-ui / heatmap-ui.",
26
+ "type": "string",
27
+ "enum": [
28
+ "trigger",
29
+ "pointer"
30
+ ],
31
+ "default": "trigger"
32
+ },
33
+ "for": {
34
+ "description": "id-ref of a chart-ui / heatmap-ui to follow. Required when follows=pointer. The target must dispatch `chart-hover` and `chart-leave` events.",
35
+ "type": "string",
36
+ "default": ""
37
+ },
38
+ "indicator": {
39
+ "description": "Color swatch shown beside each value row in pointer mode. `none` omits the swatch. Swatch color reads `--tooltip-indicator-color` which the host injects per-row from `--color-{seriesKey}`.",
40
+ "type": "string",
41
+ "enum": [
42
+ "none",
43
+ "dot",
44
+ "line",
45
+ "dashed"
46
+ ],
47
+ "default": "none"
48
+ },
24
49
  "placement": {
25
- "description": "Preferred position relative to the anchor element.",
50
+ "description": "Preferred position relative to the anchor element (trigger mode only).",
26
51
  "type": "string",
27
52
  "enum": [
28
53
  "top",
@@ -37,7 +62,7 @@
37
62
  "default": "top"
38
63
  },
39
64
  "text": {
40
- "description": "Tooltip text content displayed in the overlay.",
65
+ "description": "Tooltip text content displayed in the overlay (trigger mode only).",
41
66
  "type": "string",
42
67
  "default": ""
43
68
  }
@@ -37,3 +37,114 @@
37
37
  .tooltip-popup:popover-open {
38
38
  display: block;
39
39
  }
40
+
41
+ /* ── Pointer-follow mode (chart/heatmap) ──
42
+ Richer card: surface bg (not inverse), multi-row layout, indicator dot
43
+ per row reads `--tooltip-indicator-color` which the host injects per-row.
44
+ Copies `--color-{key}` + `--chart-0..9` from the target chart at show
45
+ time so series-keyed colors resolve despite the top-layer cascade break. */
46
+ .tooltip-popup[data-follows="pointer"] {
47
+ --tooltip-indicator-size: var(--a-space-2-5);
48
+ --tooltip-label-weight: var(--a-weight-medium);
49
+ --tooltip-value-weight: var(--a-weight-semibold);
50
+
51
+ /* Sized to its content — shared tooltip width reads as too wide for
52
+ short messages ("Mar 48") and doesn't help long ones either. max-width
53
+ prevents runaway text when values are long. */
54
+ width: max-content;
55
+ max-width: 18rem;
56
+ padding: var(--a-space-2) var(--a-space-3);
57
+ background: var(--a-canvas-0);
58
+ color: var(--a-fg);
59
+ border: 1px solid var(--a-border-subtle);
60
+ border-radius: var(--a-radius-md);
61
+ box-shadow: var(--a-shadow-md);
62
+ font-size: var(--a-ui-tiny);
63
+ white-space: normal;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: var(--a-space-0-5);
67
+
68
+ /* Elastic pointer follow — smooth the left/top updates so the tooltip
69
+ trails the pointer briefly rather than snapping on every mousemove.
70
+ Short ease-out duration keeps it snappy; prefers-reduced-motion drops
71
+ the transition entirely below. */
72
+ transition: left var(--a-duration-fast) var(--a-easing-out),
73
+ top var(--a-duration-fast) var(--a-easing-out);
74
+ }
75
+
76
+ @media (prefers-reduced-motion: reduce) {
77
+ .tooltip-popup[data-follows="pointer"] {
78
+ transition: none;
79
+ }
80
+ }
81
+
82
+ .tooltip-popup[data-follows="pointer"] [data-tip-role="label"] {
83
+ color: var(--a-fg-subtle);
84
+ font-weight: var(--tooltip-label-weight);
85
+ }
86
+
87
+ .tooltip-popup[data-follows="pointer"] [data-tip-role="series"] {
88
+ color: var(--a-fg-subtle);
89
+ font-size: calc(var(--a-ui-tiny) * 0.95);
90
+ }
91
+
92
+ .tooltip-popup[data-follows="pointer"] [data-tip-row] {
93
+ display: grid;
94
+ grid-template-columns: auto 1fr auto;
95
+ align-items: center;
96
+ gap: var(--a-space-1) var(--a-space-2);
97
+ padding: 1px 0;
98
+ }
99
+
100
+ /* Multi-series mode: the row the pointer is actually over gets a
101
+ brighter name weight to stand out from the other series rows. */
102
+ .tooltip-popup[data-follows="pointer"] [data-tip-row][data-hovered] [data-tip-role="name"],
103
+ .tooltip-popup[data-follows="pointer"] [data-tip-row][data-hovered] [data-tip-role="value"] {
104
+ color: var(--a-fg-strong);
105
+ font-weight: var(--a-weight-semibold);
106
+ }
107
+
108
+ .tooltip-popup[data-follows="pointer"] [data-tip-role="name"] {
109
+ color: var(--a-fg-subtle);
110
+ font-weight: var(--a-weight-regular);
111
+ }
112
+
113
+ .tooltip-popup[data-follows="pointer"] [data-tip-role="value"] {
114
+ font-weight: var(--tooltip-value-weight);
115
+ color: var(--a-fg);
116
+ }
117
+
118
+ .tooltip-popup[data-follows="pointer"] [data-tip-role="pct"] {
119
+ color: var(--a-fg-subtle);
120
+ font-weight: var(--a-weight-regular);
121
+ }
122
+
123
+ /* Indicator variants — dot / line / dashed */
124
+ .tooltip-popup[data-follows="pointer"][data-indicator="dot"] [data-indicator] {
125
+ display: inline-block;
126
+ width: var(--tooltip-indicator-size);
127
+ height: var(--tooltip-indicator-size);
128
+ border-radius: 50%;
129
+ background: var(--tooltip-indicator-color, var(--chart-0));
130
+ flex-shrink: 0;
131
+ }
132
+
133
+ .tooltip-popup[data-follows="pointer"][data-indicator="line"] [data-indicator] {
134
+ display: inline-block;
135
+ width: calc(var(--tooltip-indicator-size) * 1.6);
136
+ height: 2px;
137
+ background: var(--tooltip-indicator-color, var(--chart-0));
138
+ flex-shrink: 0;
139
+ }
140
+
141
+ .tooltip-popup[data-follows="pointer"][data-indicator="dashed"] [data-indicator] {
142
+ display: inline-block;
143
+ width: calc(var(--tooltip-indicator-size) * 1.6);
144
+ border-top: 2px dashed var(--tooltip-indicator-color, var(--chart-0));
145
+ flex-shrink: 0;
146
+ }
147
+
148
+ .tooltip-popup[data-follows="pointer"][data-indicator="none"] [data-indicator] {
149
+ display: none;
150
+ }
@@ -3,9 +3,17 @@
3
3
  * <button-ui text="Hover me"></button-ui>
4
4
  * </tooltip-ui>
5
5
  *
6
- * Tooltip popup. Wraps its children and shows a popover on hover/focus.
7
- * Uses Popover API (popover="manual") for top-layer rendering.
8
- * Positioned via anchorPopover() from @core/anchor.js.
6
+ * <!-- Pointer-follow mode: used by chart-ui to show a richer card that
7
+ * tracks the cursor across datums. -->
8
+ * <tooltip-ui follows="pointer" for="my-chart" indicator="dot"></tooltip-ui>
9
+ *
10
+ * Tooltip popup. Two modes:
11
+ *
12
+ * 1. `follows="trigger"` (default) — wraps children and shows on hover/focus,
13
+ * anchored to the trigger via @core/anchor.js + Popover API.
14
+ * 2. `follows="pointer"` — subscribes to `chart-hover`/`chart-leave` events
15
+ * from `[for]`, renders a card with label + indicator + value rows, and
16
+ * positions at the pointer coordinates. Used by chart-ui / heatmap-ui.
9
17
  *
10
18
  * No click interaction — hover/focus only.
11
19
  */
@@ -15,9 +23,12 @@ import { anchorPopover } from '../../core/anchor.js';
15
23
 
16
24
  class AdiaTooltip extends AdiaElement {
17
25
  static properties = {
18
- text: { type: String, default: '', reflect: true },
19
- placement: { type: String, default: 'top', reflect: true },
20
- delay: { type: Number, default: 400, reflect: true },
26
+ text: { type: String, default: '', reflect: true },
27
+ placement: { type: String, default: 'top', reflect: true },
28
+ delay: { type: Number, default: 400, reflect: true },
29
+ follows: { type: String, default: 'trigger', reflect: true }, // trigger | pointer
30
+ for: { type: String, default: '', reflect: true },
31
+ indicator: { type: String, default: 'none', reflect: true }, // none | dot | line | dashed
21
32
  };
22
33
 
23
34
  static template = () => null;
@@ -26,11 +37,21 @@ class AdiaTooltip extends AdiaElement {
26
37
  #timer = null;
27
38
  #cleanup = null;
28
39
 
40
+ /* Pointer-follow mode state */
41
+ #target = null;
42
+ #hoverHandler = null;
43
+ #leaveHandler = null;
44
+ #lastDetail = null;
45
+
29
46
  connected() {
30
- this.addEventListener('mouseenter', this.#onEnter);
31
- this.addEventListener('focusin', this.#onEnter);
32
- this.addEventListener('mouseleave', this.#onLeave);
33
- this.addEventListener('focusout', this.#onLeave);
47
+ if (this.follows === 'pointer') {
48
+ this.#attachPointerFollow();
49
+ } else {
50
+ this.addEventListener('mouseenter', this.#onEnter);
51
+ this.addEventListener('focusin', this.#onEnter);
52
+ this.addEventListener('mouseleave', this.#onLeave);
53
+ this.addEventListener('focusout', this.#onLeave);
54
+ }
34
55
  }
35
56
 
36
57
  disconnected() {
@@ -38,12 +59,19 @@ class AdiaTooltip extends AdiaElement {
38
59
  this.removeEventListener('focusin', this.#onEnter);
39
60
  this.removeEventListener('mouseleave', this.#onLeave);
40
61
  this.removeEventListener('focusout', this.#onLeave);
62
+ this.#detachPointerFollow();
41
63
  this.#hide();
42
64
  }
43
65
 
44
66
  render() {
45
- // Update popover text if already showing
46
- if (this.#popover) this.#popover.textContent = this.text;
67
+ // Trigger mode: update popover text if already showing
68
+ if (this.#popover && this.follows !== 'pointer') {
69
+ this.#popover.textContent = this.text;
70
+ }
71
+ // Pointer mode: re-paint content with current indicator if visible
72
+ if (this.#popover && this.follows === 'pointer' && this.#lastDetail) {
73
+ this.#paintPointerContent(this.#lastDetail);
74
+ }
47
75
  }
48
76
 
49
77
  #onEnter = () => {
@@ -89,6 +117,166 @@ class AdiaTooltip extends AdiaElement {
89
117
  try { this.#popover.hidePopover(); } catch (_) { /* popover not supported */ }
90
118
  this.#popover.remove();
91
119
  this.#popover = null;
120
+ this.#lastDetail = null;
121
+ }
122
+
123
+ /* ── Pointer-follow mode (chart/heatmap integration) ─────────────── */
124
+
125
+ #attachPointerFollow() {
126
+ this.#detachPointerFollow();
127
+ if (!this.for) return;
128
+
129
+ const root = this.getRootNode();
130
+ this.#target = (root && root.getElementById) ? root.getElementById(this.for) : document.getElementById(this.for);
131
+ if (!this.#target) {
132
+ // Warn once per (element, id) pair
133
+ if (!AdiaTooltip._warnedMissing) AdiaTooltip._warnedMissing = new WeakSet();
134
+ if (!AdiaTooltip._warnedMissing.has(this)) {
135
+ console.warn(`[tooltip-ui] follows="pointer" [for="${this.for}"] did not resolve to an element.`);
136
+ AdiaTooltip._warnedMissing.add(this);
137
+ }
138
+ return;
139
+ }
140
+
141
+ this.#hoverHandler = (e) => this.#onChartHover(e);
142
+ this.#leaveHandler = () => this.#hide();
143
+ this.#target.addEventListener('chart-hover', this.#hoverHandler);
144
+ this.#target.addEventListener('chart-leave', this.#leaveHandler);
145
+ }
146
+
147
+ #detachPointerFollow() {
148
+ if (this.#target && this.#hoverHandler) this.#target.removeEventListener('chart-hover', this.#hoverHandler);
149
+ if (this.#target && this.#leaveHandler) this.#target.removeEventListener('chart-leave', this.#leaveHandler);
150
+ this.#target = null;
151
+ this.#hoverHandler = null;
152
+ this.#leaveHandler = null;
153
+ }
154
+
155
+ #onChartHover(event) {
156
+ const detail = event.detail;
157
+ if (!detail) return;
158
+ this.#lastDetail = detail;
159
+
160
+ if (!this.#popover) {
161
+ this.#popover = this.#createPopover();
162
+ this.#copySeriesColorsFromTarget();
163
+ try { this.#popover.showPopover(); } catch (_) { /* unsupported */ }
164
+ }
165
+
166
+ this.#paintPointerContent(detail);
167
+ this.#positionAtPointer(detail.pointerX, detail.pointerY);
168
+ }
169
+
170
+ #createPopover() {
171
+ const el = document.createElement('div');
172
+ el.setAttribute('popover', 'manual');
173
+ el.setAttribute('role', 'tooltip');
174
+ el.setAttribute('data-follows', 'pointer');
175
+ el.setAttribute('data-indicator', this.indicator || 'none');
176
+ el.classList.add('tooltip-popup');
177
+ document.body.appendChild(el);
178
+ return el;
179
+ }
180
+
181
+ /* Copy per-series `--color-{key}` custom properties from the target chart
182
+ to the popover so inner rows can reference them. The popover lives in
183
+ the top layer and does NOT inherit custom properties from the host,
184
+ so we bridge them explicitly at show time. */
185
+ #copySeriesColorsFromTarget() {
186
+ if (!this.#target || !this.#popover) return;
187
+ /* Prefer inline style on the target (what chart-ui's #injectSeriesColors
188
+ writes). Fall back to computed style resolution for any --color-* vars
189
+ the author set on ancestors. */
190
+ const inline = this.#target.style;
191
+ for (let i = 0; i < inline.length; i++) {
192
+ const name = inline[i];
193
+ if (name.startsWith('--color-')) {
194
+ this.#popover.style.setProperty(name, inline.getPropertyValue(name));
195
+ }
196
+ }
197
+ /* Also copy the 10 categorical palette slots so the fallback
198
+ `var(--color-{key}, var(--chart-{slot}))` resolves. */
199
+ const cs = getComputedStyle(this.#target);
200
+ for (let i = 0; i < 10; i++) {
201
+ const v = cs.getPropertyValue(`--chart-${i}`).trim();
202
+ if (v) this.#popover.style.setProperty(`--chart-${i}`, v);
203
+ }
204
+ }
205
+
206
+ #paintPointerContent(detail) {
207
+ if (!this.#popover) return;
208
+ const { label, value, pct, series, slot, payload } = detail;
209
+ const indicator = this.indicator || 'none';
210
+
211
+ /* OD-CHART-05 — when `detail.payload` is an array (multi-series hover),
212
+ render one row per payload entry with the hovered series marked.
213
+ Fall back to the top-level single-datum shape for back-compat with
214
+ charts that don't emit a payload (categorical, single-series). */
215
+ const rows = Array.isArray(payload) && payload.length > 0
216
+ ? payload
217
+ : [{ series, label, value, pct, slot, hovered: true }];
218
+
219
+ const parts = [];
220
+
221
+ /* Label at the top — shared across all rows when a payload is
222
+ present (all series are at the same X column). */
223
+ if (label != null) {
224
+ parts.push(`<span data-tip-role="label">${this.#esc(String(label))}</span>`);
225
+ }
226
+
227
+ for (const row of rows) {
228
+ const rSeries = row.series ?? null;
229
+ const rValue = row.value;
230
+ const rPct = row.pct;
231
+ const rSlot = row.slot != null ? row.slot : 0;
232
+ const indicatorColor = rSeries
233
+ ? `var(--color-${rSeries}, var(--chart-${rSlot}))`
234
+ : `var(--chart-${rSlot})`;
235
+
236
+ if (rValue == null && rPct == null) continue;
237
+
238
+ const valueStr = rValue != null ? this.#esc(String(rValue)) : '';
239
+ const pctStr = rPct != null ? `<span data-tip-role="pct"> (${rPct}%)</span>` : '';
240
+ const indEl = indicator !== 'none'
241
+ ? `<span data-indicator style="--tooltip-indicator-color: ${indicatorColor}"></span>`
242
+ : '';
243
+ const nameEl = rSeries
244
+ ? `<span data-tip-role="name">${this.#esc(rSeries)}</span>`
245
+ : '';
246
+ const hoveredAttr = row.hovered ? ' data-hovered' : '';
247
+
248
+ parts.push(`<span data-tip-row${hoveredAttr}>${indEl}${nameEl}<span data-tip-role="value">${valueStr}${pctStr}</span></span>`);
249
+ }
250
+
251
+ this.#popover.setAttribute('data-indicator', indicator);
252
+ this.#popover.setAttribute('aria-live', 'polite');
253
+ this.#popover.innerHTML = parts.join('');
254
+ }
255
+
256
+ #positionAtPointer(x, y) {
257
+ if (!this.#popover || x == null || y == null) return;
258
+ const gap = 12;
259
+ const popover = this.#popover;
260
+ popover.style.position = 'fixed';
261
+ popover.style.left = '0';
262
+ popover.style.top = '0';
263
+ /* Force reflow to read offset dimensions now that content changed */
264
+ const tw = popover.offsetWidth || 0;
265
+ const th = popover.offsetHeight || 0;
266
+ let px = x + gap;
267
+ let py = y - th - gap;
268
+ if (px + tw > window.innerWidth) px = x - tw - gap;
269
+ if (py < 0) py = y + gap;
270
+ popover.style.left = `${px}px`;
271
+ popover.style.top = `${py}px`;
272
+ }
273
+
274
+ #esc(s) {
275
+ return String(s)
276
+ .replace(/&/g, '&amp;')
277
+ .replace(/</g, '&lt;')
278
+ .replace(/>/g, '&gt;')
279
+ .replace(/"/g, '&quot;');
92
280
  }
93
281
  }
94
282
  customElements.define('tooltip-ui', AdiaTooltip);