@squadbase/vantage 0.6.2-dev1.25d59a0 → 0.6.2-dev1.26a3cce

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