@squadbase/vantage 0.6.2-dev1.5aae04e → 0.6.2-dev1.81c1bbe

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 (140) hide show
  1. package/dist/chunk-2IAPIOPS.js +152 -0
  2. package/dist/chunk-2IAPIOPS.js.map +1 -0
  3. package/dist/chunk-57GHR5LN.js +163 -0
  4. package/dist/chunk-57GHR5LN.js.map +1 -0
  5. package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
  6. package/dist/chunk-7S52M2SL.js.map +1 -0
  7. package/dist/chunk-JXFCSF3K.js +673 -0
  8. package/dist/chunk-JXFCSF3K.js.map +1 -0
  9. package/dist/{chunk-HKSVYMYJ.js → chunk-RU6WKEOG.js} +56 -927
  10. package/dist/chunk-RU6WKEOG.js.map +1 -0
  11. package/dist/components/index.d.ts +3 -243
  12. package/dist/components/index.js +5 -2
  13. package/dist/doc/index.d.ts +194 -2
  14. package/dist/doc/index.js +334 -3
  15. package/dist/doc/index.js.map +1 -1
  16. package/dist/format-U0cQl069.d.ts +243 -0
  17. package/dist/slide/index.d.ts +177 -1
  18. package/dist/slide/index.js +864 -1
  19. package/dist/slide/index.js.map +1 -1
  20. package/dist/table/index.js +5 -2
  21. package/dist/table/index.js.map +1 -1
  22. package/dist/ui/index.d.ts +4 -1
  23. package/dist/ui/index.js +3 -3
  24. package/dist/ui/index.js.map +1 -1
  25. package/docs/en/components.md +11 -2
  26. package/docs/en/doc/doc-action-items.md +49 -0
  27. package/docs/en/doc/doc-callout.md +38 -0
  28. package/docs/en/doc/doc-data-table.md +60 -0
  29. package/docs/en/doc/doc-executive-summary.md +64 -0
  30. package/docs/en/doc/doc-figure.md +56 -0
  31. package/docs/en/doc/doc-key-findings.md +61 -0
  32. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  33. package/docs/en/doc/doc-report-header.md +46 -0
  34. package/docs/en/parts/area-chart.md +99 -0
  35. package/docs/en/parts/bar-chart.md +91 -0
  36. package/docs/en/parts/bar-list.md +50 -0
  37. package/docs/en/parts/category-bar.md +37 -0
  38. package/docs/en/parts/chart-frame.md +39 -0
  39. package/docs/en/parts/donut-chart.md +72 -0
  40. package/docs/en/parts/funnel-steps.md +2 -1
  41. package/docs/en/parts/heatmap.md +53 -0
  42. package/docs/en/parts/legend.md +35 -0
  43. package/docs/en/parts/line-chart.md +94 -0
  44. package/docs/en/parts/progress-circle.md +33 -0
  45. package/docs/en/parts/sample-data.md +39 -0
  46. package/docs/en/parts/scatter-chart.md +56 -0
  47. package/docs/en/parts/tracker.md +36 -0
  48. package/docs/en/parts/updated-at.md +30 -0
  49. package/docs/en/parts/waterfall-chart.md +56 -0
  50. package/docs/en/slide/slide-chart-insight.md +52 -0
  51. package/docs/en/slide/slide-cover.md +39 -0
  52. package/docs/en/slide/slide-exec-summary.md +47 -0
  53. package/docs/en/slide/slide-kpi-grid.md +55 -0
  54. package/docs/en/slide/slide-kpi-hero.md +58 -0
  55. package/docs/en/slide/slide-next-actions.md +49 -0
  56. package/docs/en/slide/slide-scorecard-table.md +54 -0
  57. package/docs/en/ui/alert-dialog.md +42 -0
  58. package/docs/en/ui/avatar.md +31 -0
  59. package/docs/en/ui/combobox.md +74 -0
  60. package/docs/en/ui/drawer.md +41 -0
  61. package/docs/en/ui/empty.md +1 -1
  62. package/docs/en/ui/error-state.md +4 -3
  63. package/docs/en/ui/field.md +42 -0
  64. package/docs/en/ui/hover-card.md +21 -0
  65. package/docs/en/ui/kbd.md +17 -0
  66. package/docs/en/ui/loading.md +1 -1
  67. package/docs/en/ui/pagination.md +36 -0
  68. package/docs/en/ui/radio-group.md +21 -0
  69. package/docs/en/ui/toast.md +44 -0
  70. package/docs/en/ui/toggle-group.md +4 -3
  71. package/docs/index.json +697 -77
  72. package/docs/ja/components.md +10 -1
  73. package/docs/ja/doc/doc-action-items.md +49 -0
  74. package/docs/ja/doc/doc-callout.md +38 -0
  75. package/docs/ja/doc/doc-data-table.md +60 -0
  76. package/docs/ja/doc/doc-executive-summary.md +64 -0
  77. package/docs/ja/doc/doc-figure.md +56 -0
  78. package/docs/ja/doc/doc-key-findings.md +61 -0
  79. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  80. package/docs/ja/doc/doc-report-header.md +46 -0
  81. package/docs/ja/parts/area-chart.md +49 -25
  82. package/docs/ja/parts/bar-chart.md +50 -24
  83. package/docs/ja/parts/bar-list.md +31 -8
  84. package/docs/ja/parts/chart-frame.md +36 -0
  85. package/docs/ja/parts/donut-chart.md +45 -26
  86. package/docs/ja/parts/funnel-steps.md +27 -8
  87. package/docs/ja/parts/heatmap.md +45 -0
  88. package/docs/ja/parts/legend.md +36 -0
  89. package/docs/ja/parts/line-chart.md +48 -22
  90. package/docs/ja/parts/sample-data.md +61 -0
  91. package/docs/ja/parts/scatter-chart.md +69 -0
  92. package/docs/ja/parts/updated-at.md +27 -0
  93. package/docs/ja/parts/waterfall-chart.md +61 -0
  94. package/docs/ja/slide/slide-chart-insight.md +52 -0
  95. package/docs/ja/slide/slide-cover.md +39 -0
  96. package/docs/ja/slide/slide-exec-summary.md +47 -0
  97. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  98. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  99. package/docs/ja/slide/slide-next-actions.md +49 -0
  100. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  101. package/docs/ja/ui/empty.md +1 -1
  102. package/docs/ja/ui/error-state.md +4 -3
  103. package/docs/ja/ui/loading.md +1 -1
  104. package/docs/ja/ui/toggle-group.md +4 -3
  105. package/package.json +2 -2
  106. package/registry/ui/area-chart.tsx +45 -19
  107. package/registry/ui/bar-chart.tsx +45 -19
  108. package/registry/ui/bar-list.tsx +44 -17
  109. package/registry/ui/box-plot-chart.tsx +45 -19
  110. package/registry/ui/category-bar.tsx +44 -17
  111. package/registry/ui/chart-frame.tsx +44 -17
  112. package/registry/ui/delta.tsx +4 -4
  113. package/registry/ui/donut-chart.tsx +46 -19
  114. package/registry/ui/echart.tsx +171 -54
  115. package/registry/ui/heatmap.tsx +45 -19
  116. package/registry/ui/histogram-chart.tsx +45 -19
  117. package/registry/ui/legend.tsx +44 -17
  118. package/registry/ui/line-chart.tsx +45 -19
  119. package/registry/ui/page-shell.tsx +7 -7
  120. package/registry/ui/paired-chart.tsx +45 -19
  121. package/registry/ui/sample-data.tsx +1 -1
  122. package/registry/ui/scatter-chart.tsx +45 -19
  123. package/registry/ui/slider.tsx +1 -1
  124. package/registry/ui/status-badge.tsx +8 -8
  125. package/registry/ui/tooltip.tsx +6 -2
  126. package/registry/ui/tracker.tsx +9 -3
  127. package/registry/ui/trend-indicator.tsx +2 -2
  128. package/registry/ui/waterfall-chart.tsx +45 -19
  129. package/skills/vantage-design/SKILL.md +27 -25
  130. package/skills/vantage-design/references/extract.md +16 -14
  131. package/skills/vantage-design/references/format.md +6 -5
  132. package/skills/vantage-design/references/presets.md +60 -110
  133. package/skills/vantage-slide/SKILL.md +54 -36
  134. package/templates/AGENTS.md +2 -2
  135. package/templates/agents/doc.md +45 -0
  136. package/templates/agents/page.md +22 -1
  137. package/templates/agents/slide.md +55 -2
  138. package/theme.css +16 -15
  139. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  140. package/dist/chunk-XPQSTZUU.js.map +0 -1
@@ -20,7 +20,7 @@ import { ChartFrame } from "@squadbase/vantage/components"
20
20
  import { EChart } from "@squadbase/vantage/components"
21
21
  import { EMPTY_VALUE } from "@squadbase/vantage/components"
22
22
  import type { ChartCommonProps, ValueFormatter } from "@squadbase/vantage/components"
23
- import { createContext, useCallback, useEffect, useRef, useState } from "react"
23
+ import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
24
24
  import type { ChartTone } from "@squadbase/vantage/components"
25
25
  import { type ReactNode } from "react"
26
26
  import { computeComparison, defaultChangeDisplay, formatChange, toneForValue, type ComparisonKind, type PeriodGrain } from "@squadbase/vantage/components"
@@ -63,10 +63,23 @@ interface ChartTokens {
63
63
  border: string | null
64
64
  /** Tooltip surface. */
65
65
  popover: string | null
66
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
67
+ colors?: Record<string, string>
68
+ /** Actual inherited painting surface (may change without a token changing). */
69
+ surface?: string | null
66
70
  /** Tooltip text. */
67
71
  popoverForeground: string | null
68
72
  }
69
73
 
74
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
75
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
76
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
77
+ const OPTION_COLOR_TOKENS = [
78
+ "--background", "--card", "--muted", "--primary", "--destructive",
79
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
80
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
81
+ ]
82
+
70
83
  let normalizeContext: CanvasRenderingContext2D | null | undefined
71
84
 
72
85
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -122,8 +135,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
122
135
  const color = read(token)
123
136
  if (color) series.push(color)
124
137
  }
125
- return {
138
+ const tokens: ChartTokens = {
126
139
  series,
140
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
141
+ surface: readChartSurface(target),
127
142
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
128
143
  foreground: read("--foreground"),
129
144
  mutedForeground: read("--muted-foreground"),
@@ -131,6 +146,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
131
146
  popover: read("--popover"),
132
147
  popoverForeground: read("--popover-foreground"),
133
148
  }
149
+ tokenScopes.set(tokens, target)
150
+ return tokens
134
151
  }
135
152
 
136
153
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -138,6 +155,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
138
155
  if (a === b) return true
139
156
  if (!a || !b) return false
140
157
  return (
158
+ tokenScopes.get(a) === tokenScopes.get(b) &&
159
+ a.surface === b.surface &&
160
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
161
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
141
162
  a.series.length === b.series.length &&
142
163
  a.series.every((color, i) => color === b.series[i]) &&
143
164
  a.fontFamily === b.fontFamily &&
@@ -163,7 +184,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
163
184
  * attributes alone leaves the chart painted in the previous palette until a
164
185
  * reload — the tokens in the DOM are new, the canvas is not.
165
186
  */
166
- function watchChartTokens(onChange: () => void): () => void {
187
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
167
188
  if (typeof document === "undefined") return () => {}
168
189
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
169
190
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -178,8 +199,11 @@ function watchChartTokens(onChange: () => void): () => void {
178
199
 
179
200
  const themeObserver = new MutationObserver(schedule)
180
201
  const attributeFilter = ["class", "style", "data-theme"]
181
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
182
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
202
+ // Only the chart and its ancestors can change its inherited theme. Never observe
203
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
204
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
205
+ themeObserver.observe(node, { attributes: true, attributeFilter })
206
+ }
183
207
 
184
208
  const styleObserver = new MutationObserver(schedule)
185
209
  if (document.head) {
@@ -267,22 +291,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
267
291
  * components that build an ECharts option from token colours themselves (the
268
292
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
269
293
  */
270
- function useChartTokens(): ChartTokens | null {
294
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
271
295
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
272
296
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
273
297
  // replayed by React on every rebase and returns a fresh object each time, so the state
274
298
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
275
299
  const tokensRef = useRef(tokens)
276
300
  useEffect(() => {
301
+ const target = scope && "current" in scope ? scope.current : scope
277
302
  const sync = () => {
278
- const next = readChartTokens()
303
+ const next = readChartTokens(target)
279
304
  if (sameChartTokens(tokensRef.current, next)) return
280
305
  tokensRef.current = next
281
306
  setTokens(next)
282
307
  }
283
308
  sync()
284
- return watchChartTokens(sync)
285
- }, [])
309
+ return watchChartTokens(sync, target)
310
+ }, [scope])
286
311
  return tokens
287
312
  }
288
313
 
@@ -294,7 +319,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
294
319
  /**
295
320
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
296
321
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
297
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
322
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
298
323
  * other CSS colour is normalized like the tokens are. A colour that can't be
299
324
  * resolved comes back as `""`, which ECharts treats as "use the palette".
300
325
  */
@@ -305,12 +330,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
305
330
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
306
331
  if (TONE_NAME_RE.test(color.trim())) {
307
332
  if (typeof document === "undefined") return ""
308
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
333
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
334
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
309
335
  }
310
336
  if (typeof document === "undefined") return color
311
337
  let value = color.trim()
312
338
  const cssVar = CSS_VAR_RE.exec(value)
313
- const styles = getComputedStyle(document.documentElement)
339
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
314
340
  if (cssVar) {
315
341
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
316
342
  } else if (value.includes("var(")) {
@@ -356,7 +382,7 @@ function readChartSurface(el: Element | null): string | null {
356
382
  if (color) return color
357
383
  }
358
384
  }
359
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
385
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
360
386
  }
361
387
 
362
388
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -370,9 +396,10 @@ const WIDTH_BUCKET = 40
370
396
  */
371
397
  function useChartSurface<E extends Element>(
372
398
  forwarded: ForwardedRef<E>,
373
- tokens: ChartTokens | null,
374
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
399
+ tokens?: ChartTokens | null,
400
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
375
401
  const [node, setNode] = useState<E | null>(null)
402
+ const scopedTokens = useChartTokens(node)
376
403
  const [surface, setSurface] = useState<string | undefined>(undefined)
377
404
  const [width, setWidth] = useState<number | undefined>(undefined)
378
405
  const ref = useCallback(
@@ -386,7 +413,7 @@ function useChartSurface<E extends Element>(
386
413
  useEffect(() => {
387
414
  if (!node) return
388
415
  setSurface(readChartSurface(node) ?? undefined)
389
- }, [node, tokens])
416
+ }, [node, tokens, scopedTokens])
390
417
  useEffect(() => {
391
418
  if (!node || typeof ResizeObserver === "undefined") return
392
419
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -395,7 +422,7 @@ function useChartSurface<E extends Element>(
395
422
  observer.observe(node)
396
423
  return () => observer.disconnect()
397
424
  }, [node])
398
- return { ref, surface, width }
425
+ return { ref, surface, width, tokens: scopedTokens }
399
426
  }
400
427
 
401
428
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-options.ts (internal to @squadbase/vantage) ---------- */
@@ -3843,8 +3870,7 @@ export const BoxPlotChart = forwardRef(function BoxPlotChart(
3843
3870
  }: BoxPlotChartProps,
3844
3871
  ref: ForwardedRef<HTMLDivElement>,
3845
3872
  ) {
3846
- const tokens = useChartTokens()
3847
- const { ref: frameRef } = useChartSurface(ref, tokens)
3873
+ const { ref: frameRef, tokens } = useChartSurface(ref)
3848
3874
  const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format), [valueFormatterProp, format])
3849
3875
  const entries = useMemo(() => boxPlotEntries(groups, whisker), [groups, whisker])
3850
3876
  const [color = ""] = useMemo(() => resolveChartColors(["chart-1"], tokens), [tokens])
@@ -13,7 +13,7 @@ import { cn } from "@squadbase/vantage/ui"
13
13
  import { Tooltip, TooltipContent, TooltipTrigger } from "@squadbase/vantage/ui"
14
14
  import type { ValueFormat } from "@squadbase/vantage/components"
15
15
  import type { ChartColor, ValueFormatter } from "@squadbase/vantage/components"
16
- import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef } from "react"
16
+ import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
17
17
  import type { ChartTone } from "@squadbase/vantage/components"
18
18
  import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
19
19
  import type { ChartColors, ChartSeriesColor } from "@squadbase/vantage/components"
@@ -52,10 +52,23 @@ interface ChartTokens {
52
52
  border: string | null
53
53
  /** Tooltip surface. */
54
54
  popover: string | null
55
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
56
+ colors?: Record<string, string>
57
+ /** Actual inherited painting surface (may change without a token changing). */
58
+ surface?: string | null
55
59
  /** Tooltip text. */
56
60
  popoverForeground: string | null
57
61
  }
58
62
 
63
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
64
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
65
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
66
+ const OPTION_COLOR_TOKENS = [
67
+ "--background", "--card", "--muted", "--primary", "--destructive",
68
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
69
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
70
+ ]
71
+
59
72
  let normalizeContext: CanvasRenderingContext2D | null | undefined
60
73
 
61
74
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -111,8 +124,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
111
124
  const color = read(token)
112
125
  if (color) series.push(color)
113
126
  }
114
- return {
127
+ const tokens: ChartTokens = {
115
128
  series,
129
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
130
+ surface: readChartSurface(target),
116
131
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
117
132
  foreground: read("--foreground"),
118
133
  mutedForeground: read("--muted-foreground"),
@@ -120,6 +135,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
120
135
  popover: read("--popover"),
121
136
  popoverForeground: read("--popover-foreground"),
122
137
  }
138
+ tokenScopes.set(tokens, target)
139
+ return tokens
123
140
  }
124
141
 
125
142
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -127,6 +144,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
127
144
  if (a === b) return true
128
145
  if (!a || !b) return false
129
146
  return (
147
+ tokenScopes.get(a) === tokenScopes.get(b) &&
148
+ a.surface === b.surface &&
149
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
150
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
130
151
  a.series.length === b.series.length &&
131
152
  a.series.every((color, i) => color === b.series[i]) &&
132
153
  a.fontFamily === b.fontFamily &&
@@ -152,7 +173,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
152
173
  * attributes alone leaves the chart painted in the previous palette until a
153
174
  * reload — the tokens in the DOM are new, the canvas is not.
154
175
  */
155
- function watchChartTokens(onChange: () => void): () => void {
176
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
156
177
  if (typeof document === "undefined") return () => {}
157
178
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
158
179
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -167,8 +188,11 @@ function watchChartTokens(onChange: () => void): () => void {
167
188
 
168
189
  const themeObserver = new MutationObserver(schedule)
169
190
  const attributeFilter = ["class", "style", "data-theme"]
170
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
171
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
191
+ // Only the chart and its ancestors can change its inherited theme. Never observe
192
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
193
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
194
+ themeObserver.observe(node, { attributes: true, attributeFilter })
195
+ }
172
196
 
173
197
  const styleObserver = new MutationObserver(schedule)
174
198
  if (document.head) {
@@ -256,22 +280,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
256
280
  * components that build an ECharts option from token colours themselves (the
257
281
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
258
282
  */
259
- function useChartTokens(): ChartTokens | null {
283
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
260
284
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
261
285
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
262
286
  // replayed by React on every rebase and returns a fresh object each time, so the state
263
287
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
264
288
  const tokensRef = useRef(tokens)
265
289
  useEffect(() => {
290
+ const target = scope && "current" in scope ? scope.current : scope
266
291
  const sync = () => {
267
- const next = readChartTokens()
292
+ const next = readChartTokens(target)
268
293
  if (sameChartTokens(tokensRef.current, next)) return
269
294
  tokensRef.current = next
270
295
  setTokens(next)
271
296
  }
272
297
  sync()
273
- return watchChartTokens(sync)
274
- }, [])
298
+ return watchChartTokens(sync, target)
299
+ }, [scope])
275
300
  return tokens
276
301
  }
277
302
 
@@ -283,7 +308,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
283
308
  /**
284
309
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
285
310
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
286
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
311
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
287
312
  * other CSS colour is normalized like the tokens are. A colour that can't be
288
313
  * resolved comes back as `""`, which ECharts treats as "use the palette".
289
314
  */
@@ -294,12 +319,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
294
319
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
295
320
  if (TONE_NAME_RE.test(color.trim())) {
296
321
  if (typeof document === "undefined") return ""
297
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
322
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
323
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
298
324
  }
299
325
  if (typeof document === "undefined") return color
300
326
  let value = color.trim()
301
327
  const cssVar = CSS_VAR_RE.exec(value)
302
- const styles = getComputedStyle(document.documentElement)
328
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
303
329
  if (cssVar) {
304
330
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
305
331
  } else if (value.includes("var(")) {
@@ -345,7 +371,7 @@ function readChartSurface(el: Element | null): string | null {
345
371
  if (color) return color
346
372
  }
347
373
  }
348
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
374
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
349
375
  }
350
376
 
351
377
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -359,9 +385,10 @@ const WIDTH_BUCKET = 40
359
385
  */
360
386
  function useChartSurface<E extends Element>(
361
387
  forwarded: ForwardedRef<E>,
362
- tokens: ChartTokens | null,
363
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
388
+ tokens?: ChartTokens | null,
389
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
364
390
  const [node, setNode] = useState<E | null>(null)
391
+ const scopedTokens = useChartTokens(node)
365
392
  const [surface, setSurface] = useState<string | undefined>(undefined)
366
393
  const [width, setWidth] = useState<number | undefined>(undefined)
367
394
  const ref = useCallback(
@@ -375,7 +402,7 @@ function useChartSurface<E extends Element>(
375
402
  useEffect(() => {
376
403
  if (!node) return
377
404
  setSurface(readChartSurface(node) ?? undefined)
378
- }, [node, tokens])
405
+ }, [node, tokens, scopedTokens])
379
406
  useEffect(() => {
380
407
  if (!node || typeof ResizeObserver === "undefined") return
381
408
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -384,7 +411,7 @@ function useChartSurface<E extends Element>(
384
411
  observer.observe(node)
385
412
  return () => observer.disconnect()
386
413
  }, [node])
387
- return { ref, surface, width }
414
+ return { ref, surface, width, tokens: scopedTokens }
388
415
  }
389
416
 
390
417
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -20,7 +20,7 @@ import { forwardRef, useId, type HTMLAttributes, type ReactNode } from "react"
20
20
 
21
21
  import { cn } from "@squadbase/vantage/ui"
22
22
  import type { ChartEmptyState } from "@squadbase/vantage/components"
23
- import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef } from "react"
23
+ import { createContext, useCallback, useEffect, useRef, useState, type ForwardedRef, type RefObject } from "react"
24
24
  import type { ChartTone } from "@squadbase/vantage/components"
25
25
  import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
26
26
  import type { ChartColor, ChartColors, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
@@ -59,10 +59,23 @@ interface ChartTokens {
59
59
  border: string | null
60
60
  /** Tooltip surface. */
61
61
  popover: string | null
62
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
63
+ colors?: Record<string, string>
64
+ /** Actual inherited painting surface (may change without a token changing). */
65
+ surface?: string | null
62
66
  /** Tooltip text. */
63
67
  popoverForeground: string | null
64
68
  }
65
69
 
70
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
71
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
72
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
73
+ const OPTION_COLOR_TOKENS = [
74
+ "--background", "--card", "--muted", "--primary", "--destructive",
75
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
76
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
77
+ ]
78
+
66
79
  let normalizeContext: CanvasRenderingContext2D | null | undefined
67
80
 
68
81
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -118,8 +131,10 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
118
131
  const color = read(token)
119
132
  if (color) series.push(color)
120
133
  }
121
- return {
134
+ const tokens: ChartTokens = {
122
135
  series,
136
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
137
+ surface: readChartSurface(target),
123
138
  ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
124
139
  foreground: read("--foreground"),
125
140
  mutedForeground: read("--muted-foreground"),
@@ -127,6 +142,8 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
127
142
  popover: read("--popover"),
128
143
  popoverForeground: read("--popover-foreground"),
129
144
  }
145
+ tokenScopes.set(tokens, target)
146
+ return tokens
130
147
  }
131
148
 
132
149
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -134,6 +151,10 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
134
151
  if (a === b) return true
135
152
  if (!a || !b) return false
136
153
  return (
154
+ tokenScopes.get(a) === tokenScopes.get(b) &&
155
+ a.surface === b.surface &&
156
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
157
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
137
158
  a.series.length === b.series.length &&
138
159
  a.series.every((color, i) => color === b.series[i]) &&
139
160
  a.fontFamily === b.fontFamily &&
@@ -159,7 +180,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
159
180
  * attributes alone leaves the chart painted in the previous palette until a
160
181
  * reload — the tokens in the DOM are new, the canvas is not.
161
182
  */
162
- function watchChartTokens(onChange: () => void): () => void {
183
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
163
184
  if (typeof document === "undefined") return () => {}
164
185
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
165
186
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -174,8 +195,11 @@ function watchChartTokens(onChange: () => void): () => void {
174
195
 
175
196
  const themeObserver = new MutationObserver(schedule)
176
197
  const attributeFilter = ["class", "style", "data-theme"]
177
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
178
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
198
+ // Only the chart and its ancestors can change its inherited theme. Never observe
199
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
200
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
201
+ themeObserver.observe(node, { attributes: true, attributeFilter })
202
+ }
179
203
 
180
204
  const styleObserver = new MutationObserver(schedule)
181
205
  if (document.head) {
@@ -263,22 +287,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
263
287
  * components that build an ECharts option from token colours themselves (the
264
288
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
265
289
  */
266
- function useChartTokens(): ChartTokens | null {
290
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
267
291
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
268
292
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
269
293
  // replayed by React on every rebase and returns a fresh object each time, so the state
270
294
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
271
295
  const tokensRef = useRef(tokens)
272
296
  useEffect(() => {
297
+ const target = scope && "current" in scope ? scope.current : scope
273
298
  const sync = () => {
274
- const next = readChartTokens()
299
+ const next = readChartTokens(target)
275
300
  if (sameChartTokens(tokensRef.current, next)) return
276
301
  tokensRef.current = next
277
302
  setTokens(next)
278
303
  }
279
304
  sync()
280
- return watchChartTokens(sync)
281
- }, [])
305
+ return watchChartTokens(sync, target)
306
+ }, [scope])
282
307
  return tokens
283
308
  }
284
309
 
@@ -290,7 +315,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
290
315
  /**
291
316
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
292
317
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
293
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
318
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
294
319
  * other CSS colour is normalized like the tokens are. A colour that can't be
295
320
  * resolved comes back as `""`, which ECharts treats as "use the palette".
296
321
  */
@@ -301,12 +326,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
301
326
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
302
327
  if (TONE_NAME_RE.test(color.trim())) {
303
328
  if (typeof document === "undefined") return ""
304
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
329
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
330
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
305
331
  }
306
332
  if (typeof document === "undefined") return color
307
333
  let value = color.trim()
308
334
  const cssVar = CSS_VAR_RE.exec(value)
309
- const styles = getComputedStyle(document.documentElement)
335
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
310
336
  if (cssVar) {
311
337
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
312
338
  } else if (value.includes("var(")) {
@@ -352,7 +378,7 @@ function readChartSurface(el: Element | null): string | null {
352
378
  if (color) return color
353
379
  }
354
380
  }
355
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
381
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
356
382
  }
357
383
 
358
384
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -366,9 +392,10 @@ const WIDTH_BUCKET = 40
366
392
  */
367
393
  function useChartSurface<E extends Element>(
368
394
  forwarded: ForwardedRef<E>,
369
- tokens: ChartTokens | null,
370
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
395
+ tokens?: ChartTokens | null,
396
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
371
397
  const [node, setNode] = useState<E | null>(null)
398
+ const scopedTokens = useChartTokens(node)
372
399
  const [surface, setSurface] = useState<string | undefined>(undefined)
373
400
  const [width, setWidth] = useState<number | undefined>(undefined)
374
401
  const ref = useCallback(
@@ -382,7 +409,7 @@ function useChartSurface<E extends Element>(
382
409
  useEffect(() => {
383
410
  if (!node) return
384
411
  setSurface(readChartSurface(node) ?? undefined)
385
- }, [node, tokens])
412
+ }, [node, tokens, scopedTokens])
386
413
  useEffect(() => {
387
414
  if (!node || typeof ResizeObserver === "undefined") return
388
415
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -391,7 +418,7 @@ function useChartSurface<E extends Element>(
391
418
  observer.observe(node)
392
419
  return () => observer.disconnect()
393
420
  }, [node])
394
- return { ref, surface, width }
421
+ return { ref, surface, width, tokens: scopedTokens }
395
422
  }
396
423
 
397
424
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -56,7 +56,7 @@ const deltaVariants = cva(
56
56
  {
57
57
  variants: {
58
58
  variant: {
59
- badge: "h-5 rounded-4xl px-2 py-0.5 text-xs font-semibold [&>svg]:size-3",
59
+ badge: "h-5 rounded-md border px-2 py-0.5 text-xs font-medium [&>svg]:size-3",
60
60
  inline: "text-sm [&>svg]:size-3.5",
61
61
  },
62
62
  sentiment: {
@@ -66,9 +66,9 @@ const deltaVariants = cva(
66
66
  },
67
67
  },
68
68
  compoundVariants: [
69
- { variant: "badge", sentiment: "positive", className: "bg-positive-subtle" },
70
- { variant: "badge", sentiment: "negative", className: "bg-negative-subtle" },
71
- { variant: "badge", sentiment: "neutral", className: "bg-neutral-subtle" },
69
+ { variant: "badge", sentiment: "positive", className: "border-positive/20 bg-positive-subtle dark:border-positive/25" },
70
+ { variant: "badge", sentiment: "negative", className: "border-negative/20 bg-negative-subtle dark:border-negative/25" },
71
+ { variant: "badge", sentiment: "neutral", className: "border-neutral/20 bg-neutral-subtle dark:border-neutral/25" },
72
72
  ],
73
73
  defaultVariants: {
74
74
  variant: "badge",