@squadbase/vantage 0.6.2-dev1.d594c56 → 0.6.2-dev1.e333781

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 (202) hide show
  1. package/dist/{build-X5NVCQKT.js → build-YOPOGPQB.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-YOPOGPQB.js.map} +1 -1
  3. package/dist/{check-ZBIH422T.js → check-FBHT6W6J.js} +5 -5
  4. package/dist/{check-ZBIH422T.js.map → check-FBHT6W6J.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-7DZGJQ3P.js → chunk-5C2SEDPH.js} +21 -4
  10. package/dist/chunk-5C2SEDPH.js.map +1 -0
  11. package/dist/chunk-6OTTGAPO.js +44 -0
  12. package/dist/chunk-6OTTGAPO.js.map +1 -0
  13. package/dist/{chunk-2ICNHUG6.js → chunk-7652CGJJ.js} +10 -3
  14. package/dist/{chunk-2ICNHUG6.js.map → chunk-7652CGJJ.js.map} +1 -1
  15. package/dist/{chunk-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
  16. package/dist/chunk-7S52M2SL.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-HKSVYMYJ.js → chunk-QOXXOFGL.js} +61 -929
  20. package/dist/chunk-QOXXOFGL.js.map +1 -0
  21. package/dist/{chunk-WAMZWZJB.js → chunk-R6MTJAHX.js} +6 -6
  22. package/dist/chunk-R6MTJAHX.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-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
  26. package/dist/chunk-UWSQUHX6.js.map +1 -0
  27. package/dist/cli.js +6 -6
  28. package/dist/components/index.d.ts +11 -247
  29. package/dist/components/index.js +5 -2
  30. package/dist/delta-text-CvwBGJ95.d.ts +15 -0
  31. package/dist/{dev-4XEDTELV.js → dev-GUKQT6U6.js} +7 -7
  32. package/dist/{dev-4XEDTELV.js.map → dev-GUKQT6U6.js.map} +1 -1
  33. package/dist/doc/index.d.ts +215 -2
  34. package/dist/doc/index.js +307 -3
  35. package/dist/doc/index.js.map +1 -1
  36. package/dist/{doctor-5E2VEZPK.js → doctor-B2O3GJDU.js} +5 -5
  37. package/dist/{doctor-5E2VEZPK.js.map → doctor-B2O3GJDU.js.map} +1 -1
  38. package/dist/format-U0cQl069.d.ts +243 -0
  39. package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
  40. package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
  41. package/dist/slide/index.d.ts +272 -1
  42. package/dist/slide/index.js +972 -2
  43. package/dist/slide/index.js.map +1 -1
  44. package/dist/table/index.js +5 -2
  45. package/dist/table/index.js.map +1 -1
  46. package/dist/ui/index.d.ts +4 -1
  47. package/dist/ui/index.js +3 -3
  48. package/dist/ui/index.js.map +1 -1
  49. package/dist/{upgrade-JHXZBGQS.js → upgrade-7ZXMCTTR.js} +4 -4
  50. package/dist/{upgrade-JHXZBGQS.js.map → upgrade-7ZXMCTTR.js.map} +1 -1
  51. package/dist/vite/index.js +3 -3
  52. package/docs/en/changelog.md +2 -0
  53. package/docs/en/components.md +13 -2
  54. package/docs/en/doc/delta-text.md +40 -0
  55. package/docs/en/doc/doc-action-items.md +49 -0
  56. package/docs/en/doc/doc-callout.md +38 -0
  57. package/docs/en/doc/doc-data-table.md +60 -0
  58. package/docs/en/doc/doc-executive-summary.md +64 -0
  59. package/docs/en/doc/doc-figure.md +56 -0
  60. package/docs/en/doc/doc-key-findings.md +61 -0
  61. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  62. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  63. package/docs/en/doc/doc-report-header.md +46 -0
  64. package/docs/en/pages-and-metadata.md +15 -0
  65. package/docs/en/parts/area-chart.md +99 -0
  66. package/docs/en/parts/bar-chart.md +91 -0
  67. package/docs/en/parts/bar-list.md +50 -0
  68. package/docs/en/parts/category-bar.md +37 -0
  69. package/docs/en/parts/chart-frame.md +39 -0
  70. package/docs/en/parts/delta.md +4 -3
  71. package/docs/en/parts/donut-chart.md +72 -0
  72. package/docs/en/parts/funnel-steps.md +2 -1
  73. package/docs/en/parts/heatmap.md +53 -0
  74. package/docs/en/parts/kpi-card.md +2 -1
  75. package/docs/en/parts/legend.md +35 -0
  76. package/docs/en/parts/line-chart.md +94 -0
  77. package/docs/en/parts/progress-circle.md +33 -0
  78. package/docs/en/parts/sample-data.md +39 -0
  79. package/docs/en/parts/scatter-chart.md +56 -0
  80. package/docs/en/parts/tracker.md +36 -0
  81. package/docs/en/parts/updated-at.md +30 -0
  82. package/docs/en/parts/waterfall-chart.md +56 -0
  83. package/docs/en/slide/slide-chart-full.md +48 -0
  84. package/docs/en/slide/slide-chart-insight.md +52 -0
  85. package/docs/en/slide/slide-columns.md +46 -0
  86. package/docs/en/slide/slide-comparison.md +68 -0
  87. package/docs/en/slide/slide-cover.md +39 -0
  88. package/docs/en/slide/slide-exec-summary.md +47 -0
  89. package/docs/en/slide/slide-heading.md +38 -0
  90. package/docs/en/slide/slide-kpi-grid.md +55 -0
  91. package/docs/en/slide/slide-kpi-hero.md +58 -0
  92. package/docs/en/slide/slide-next-actions.md +49 -0
  93. package/docs/en/slide/slide-scorecard-table.md +54 -0
  94. package/docs/en/slide/slide-section.md +44 -0
  95. package/docs/en/slide/slide-source-line.md +34 -0
  96. package/docs/en/ui/alert-dialog.md +42 -0
  97. package/docs/en/ui/avatar.md +31 -0
  98. package/docs/en/ui/combobox.md +74 -0
  99. package/docs/en/ui/drawer.md +41 -0
  100. package/docs/en/ui/empty.md +1 -1
  101. package/docs/en/ui/error-state.md +4 -3
  102. package/docs/en/ui/field.md +42 -0
  103. package/docs/en/ui/hover-card.md +21 -0
  104. package/docs/en/ui/kbd.md +17 -0
  105. package/docs/en/ui/loading.md +1 -1
  106. package/docs/en/ui/pagination.md +36 -0
  107. package/docs/en/ui/radio-group.md +21 -0
  108. package/docs/en/ui/toast.md +44 -0
  109. package/docs/en/ui/toggle-group.md +4 -3
  110. package/docs/index.json +857 -77
  111. package/docs/ja/changelog.md +2 -0
  112. package/docs/ja/components.md +12 -1
  113. package/docs/ja/doc/delta-text.md +40 -0
  114. package/docs/ja/doc/doc-action-items.md +49 -0
  115. package/docs/ja/doc/doc-callout.md +38 -0
  116. package/docs/ja/doc/doc-data-table.md +60 -0
  117. package/docs/ja/doc/doc-executive-summary.md +64 -0
  118. package/docs/ja/doc/doc-figure.md +56 -0
  119. package/docs/ja/doc/doc-key-findings.md +61 -0
  120. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  121. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  122. package/docs/ja/doc/doc-report-header.md +46 -0
  123. package/docs/ja/page-types/slide.md +1 -0
  124. package/docs/ja/pages-and-metadata.md +15 -0
  125. package/docs/ja/parts/area-chart.md +49 -25
  126. package/docs/ja/parts/bar-chart.md +50 -24
  127. package/docs/ja/parts/bar-list.md +31 -8
  128. package/docs/ja/parts/chart-frame.md +36 -0
  129. package/docs/ja/parts/delta.md +5 -4
  130. package/docs/ja/parts/donut-chart.md +45 -26
  131. package/docs/ja/parts/funnel-steps.md +27 -8
  132. package/docs/ja/parts/heatmap.md +45 -0
  133. package/docs/ja/parts/kpi-card.md +2 -1
  134. package/docs/ja/parts/legend.md +36 -0
  135. package/docs/ja/parts/line-chart.md +48 -22
  136. package/docs/ja/parts/sample-data.md +61 -0
  137. package/docs/ja/parts/scatter-chart.md +69 -0
  138. package/docs/ja/parts/updated-at.md +27 -0
  139. package/docs/ja/parts/waterfall-chart.md +61 -0
  140. package/docs/ja/slide/slide-chart-full.md +48 -0
  141. package/docs/ja/slide/slide-chart-insight.md +52 -0
  142. package/docs/ja/slide/slide-columns.md +46 -0
  143. package/docs/ja/slide/slide-comparison.md +68 -0
  144. package/docs/ja/slide/slide-cover.md +39 -0
  145. package/docs/ja/slide/slide-exec-summary.md +47 -0
  146. package/docs/ja/slide/slide-heading.md +38 -0
  147. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  148. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  149. package/docs/ja/slide/slide-next-actions.md +49 -0
  150. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  151. package/docs/ja/slide/slide-section.md +44 -0
  152. package/docs/ja/slide/slide-source-line.md +34 -0
  153. package/docs/ja/ui/empty.md +1 -1
  154. package/docs/ja/ui/error-state.md +4 -3
  155. package/docs/ja/ui/loading.md +1 -1
  156. package/docs/ja/ui/toggle-group.md +4 -3
  157. package/package.json +2 -2
  158. package/registry/ui/area-chart.tsx +88 -26
  159. package/registry/ui/attention-list.tsx +303 -0
  160. package/registry/ui/bar-chart.tsx +88 -26
  161. package/registry/ui/bar-list.tsx +87 -24
  162. package/registry/ui/box-plot-chart.tsx +3906 -0
  163. package/registry/ui/category-bar.tsx +54 -19
  164. package/registry/ui/chart-frame.tsx +56 -21
  165. package/registry/ui/dashboard-card.tsx +2 -2
  166. package/registry/ui/delta.tsx +11 -8
  167. package/registry/ui/donut-chart.tsx +89 -26
  168. package/registry/ui/echart.tsx +194 -44
  169. package/registry/ui/funnel-steps.tsx +33 -5
  170. package/registry/ui/heatmap.tsx +55 -21
  171. package/registry/ui/histogram-chart.tsx +3986 -0
  172. package/registry/ui/kpi-card.tsx +41 -14
  173. package/registry/ui/legend.tsx +87 -24
  174. package/registry/ui/line-chart.tsx +88 -26
  175. package/registry/ui/page-shell.tsx +9 -9
  176. package/registry/ui/paired-chart.tsx +3827 -0
  177. package/registry/ui/sample-data.tsx +1 -1
  178. package/registry/ui/scatter-chart.tsx +88 -26
  179. package/registry/ui/section-header.tsx +2 -2
  180. package/registry/ui/slider.tsx +1 -1
  181. package/registry/ui/status-badge.tsx +8 -8
  182. package/registry/ui/tooltip.tsx +6 -2
  183. package/registry/ui/tracker.tsx +9 -3
  184. package/registry/ui/trend-indicator.tsx +2 -2
  185. package/registry/ui/waterfall-chart.tsx +88 -26
  186. package/skills/vantage-design/SKILL.md +27 -25
  187. package/skills/vantage-design/references/extract.md +16 -14
  188. package/skills/vantage-design/references/format.md +6 -5
  189. package/skills/vantage-design/references/presets.md +60 -110
  190. package/skills/vantage-pitfalls/SKILL.md +5 -0
  191. package/skills/vantage-slide/SKILL.md +54 -36
  192. package/templates/AGENTS.md +4 -2
  193. package/templates/agents/doc.md +48 -0
  194. package/templates/agents/page.md +22 -1
  195. package/templates/agents/slide.md +70 -2
  196. package/theme.css +16 -15
  197. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  198. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  199. package/dist/chunk-HKSVYMYJ.js.map +0 -1
  200. package/dist/chunk-JKLLFVEU.js.map +0 -1
  201. package/dist/chunk-WAMZWZJB.js.map +0 -1
  202. package/dist/chunk-XPQSTZUU.js.map +0 -1
@@ -27,7 +27,7 @@ import { ChartFrame } from "@squadbase/vantage/components"
27
27
  import { EChart } from "@squadbase/vantage/components"
28
28
  import { EMPTY_VALUE, type ValueFormat } from "@squadbase/vantage/components"
29
29
  import type { ChartActionsOptions, ChartEmptyState, ChartRootAttributes, ChartSeriesColor, ValueFormatter } from "@squadbase/vantage/components"
30
- import { createContext, useCallback, useEffect, useRef, useState } from "react"
30
+ import { createContext, useCallback, useEffect, useRef, useState, type RefObject } from "react"
31
31
  import type { ChartTone } from "@squadbase/vantage/components"
32
32
  import { DEFAULT_LOCALE, formatValue, toValueFormatter } from "@squadbase/vantage/components"
33
33
  import type { ChartColor, ChartColors } from "@squadbase/vantage/components"
@@ -56,6 +56,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
56
56
  interface ChartTokens {
57
57
  /** Resolved `--chart-1..5`, in order. */
58
58
  series: string[]
59
+ /** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
60
+ fontFamily?: string
59
61
  /** Body text: titles and the default `textStyle`. */
60
62
  foreground: string | null
61
63
  /** Secondary text: axis labels, legend, subtitles. */
@@ -64,10 +66,23 @@ interface ChartTokens {
64
66
  border: string | null
65
67
  /** Tooltip surface. */
66
68
  popover: string | null
69
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
70
+ colors?: Record<string, string>
71
+ /** Actual inherited painting surface (may change without a token changing). */
72
+ surface?: string | null
67
73
  /** Tooltip text. */
68
74
  popoverForeground: string | null
69
75
  }
70
76
 
77
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
78
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
79
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
80
+ const OPTION_COLOR_TOKENS = [
81
+ "--background", "--card", "--muted", "--primary", "--destructive",
82
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
83
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
84
+ ]
85
+
71
86
  let normalizeContext: CanvasRenderingContext2D | null | undefined
72
87
 
73
88
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -123,14 +138,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
123
138
  const color = read(token)
124
139
  if (color) series.push(color)
125
140
  }
126
- return {
141
+ const tokens: ChartTokens = {
127
142
  series,
143
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
144
+ surface: readChartSurface(target),
145
+ ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
128
146
  foreground: read("--foreground"),
129
147
  mutedForeground: read("--muted-foreground"),
130
148
  border: read("--border"),
131
149
  popover: read("--popover"),
132
150
  popoverForeground: read("--popover-foreground"),
133
151
  }
152
+ tokenScopes.set(tokens, target)
153
+ return tokens
134
154
  }
135
155
 
136
156
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -138,8 +158,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
138
158
  if (a === b) return true
139
159
  if (!a || !b) return false
140
160
  return (
161
+ tokenScopes.get(a) === tokenScopes.get(b) &&
162
+ a.surface === b.surface &&
163
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
164
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
141
165
  a.series.length === b.series.length &&
142
166
  a.series.every((color, i) => color === b.series[i]) &&
167
+ a.fontFamily === b.fontFamily &&
143
168
  a.foreground === b.foreground &&
144
169
  a.mutedForeground === b.mutedForeground &&
145
170
  a.border === b.border &&
@@ -162,7 +187,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
162
187
  * attributes alone leaves the chart painted in the previous palette until a
163
188
  * reload — the tokens in the DOM are new, the canvas is not.
164
189
  */
165
- function watchChartTokens(onChange: () => void): () => void {
190
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
166
191
  if (typeof document === "undefined") return () => {}
167
192
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
168
193
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -177,8 +202,11 @@ function watchChartTokens(onChange: () => void): () => void {
177
202
 
178
203
  const themeObserver = new MutationObserver(schedule)
179
204
  const attributeFilter = ["class", "style", "data-theme"]
180
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
181
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
205
+ // Only the chart and its ancestors can change its inherited theme. Never observe
206
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
207
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
208
+ themeObserver.observe(node, { attributes: true, attributeFilter })
209
+ }
182
210
 
183
211
  const styleObserver = new MutationObserver(schedule)
184
212
  if (document.head) {
@@ -234,7 +262,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
234
262
  // Let the page background show through: charts sit on cards, not on a canvas.
235
263
  backgroundColor: "transparent",
236
264
  ...(tokens.series.length > 0 ? { color: tokens.series } : {}),
237
- textStyle: { color: text },
265
+ textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
238
266
  title: { textStyle: { color: text }, subtextStyle: { color: muted } },
239
267
  legend: { textStyle: { color: muted } },
240
268
  categoryAxis: axisTheme(tokens, false),
@@ -250,7 +278,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
250
278
  padding: [8, 12],
251
279
  // Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
252
280
  extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
253
- textStyle: { color: tokens.popoverForeground ?? undefined, fontSize: 12 },
281
+ textStyle: {
282
+ color: tokens.popoverForeground ?? undefined,
283
+ fontSize: 12,
284
+ ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
285
+ },
254
286
  axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
255
287
  },
256
288
  }
@@ -262,22 +294,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
262
294
  * components that build an ECharts option from token colours themselves (the
263
295
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
264
296
  */
265
- function useChartTokens(): ChartTokens | null {
297
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
266
298
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
267
299
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
268
300
  // replayed by React on every rebase and returns a fresh object each time, so the state
269
301
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
270
302
  const tokensRef = useRef(tokens)
271
303
  useEffect(() => {
304
+ const target = scope && "current" in scope ? scope.current : scope
272
305
  const sync = () => {
273
- const next = readChartTokens()
306
+ const next = readChartTokens(target)
274
307
  if (sameChartTokens(tokensRef.current, next)) return
275
308
  tokensRef.current = next
276
309
  setTokens(next)
277
310
  }
278
311
  sync()
279
- return watchChartTokens(sync)
280
- }, [])
312
+ return watchChartTokens(sync, target)
313
+ }, [scope])
281
314
  return tokens
282
315
  }
283
316
 
@@ -289,7 +322,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
289
322
  /**
290
323
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
291
324
  * 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
325
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
293
326
  * other CSS colour is normalized like the tokens are. A colour that can't be
294
327
  * resolved comes back as `""`, which ECharts treats as "use the palette".
295
328
  */
@@ -300,12 +333,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
300
333
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
301
334
  if (TONE_NAME_RE.test(color.trim())) {
302
335
  if (typeof document === "undefined") return ""
303
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
336
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
337
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
304
338
  }
305
339
  if (typeof document === "undefined") return color
306
340
  let value = color.trim()
307
341
  const cssVar = CSS_VAR_RE.exec(value)
308
- const styles = getComputedStyle(document.documentElement)
342
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
309
343
  if (cssVar) {
310
344
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
311
345
  } else if (value.includes("var(")) {
@@ -351,7 +385,7 @@ function readChartSurface(el: Element | null): string | null {
351
385
  if (color) return color
352
386
  }
353
387
  }
354
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
388
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
355
389
  }
356
390
 
357
391
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -365,9 +399,10 @@ const WIDTH_BUCKET = 40
365
399
  */
366
400
  function useChartSurface<E extends Element>(
367
401
  forwarded: ForwardedRef<E>,
368
- tokens: ChartTokens | null,
369
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
402
+ tokens?: ChartTokens | null,
403
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
370
404
  const [node, setNode] = useState<E | null>(null)
405
+ const scopedTokens = useChartTokens(node)
371
406
  const [surface, setSurface] = useState<string | undefined>(undefined)
372
407
  const [width, setWidth] = useState<number | undefined>(undefined)
373
408
  const ref = useCallback(
@@ -381,7 +416,7 @@ function useChartSurface<E extends Element>(
381
416
  useEffect(() => {
382
417
  if (!node) return
383
418
  setSurface(readChartSurface(node) ?? undefined)
384
- }, [node, tokens])
419
+ }, [node, tokens, scopedTokens])
385
420
  useEffect(() => {
386
421
  if (!node || typeof ResizeObserver === "undefined") return
387
422
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -390,7 +425,7 @@ function useChartSurface<E extends Element>(
390
425
  observer.observe(node)
391
426
  return () => observer.disconnect()
392
427
  }, [node])
393
- return { ref, surface, width }
428
+ return { ref, surface, width, tokens: scopedTokens }
394
429
  }
395
430
 
396
431
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -1657,8 +1692,7 @@ function HeatmapImpl<T extends Record<string, unknown>>(
1657
1692
  }: HeatmapProps<T>,
1658
1693
  ref: ForwardedRef<HTMLDivElement>,
1659
1694
  ) {
1660
- const tokens = useChartTokens()
1661
- const { ref: frameRef, surface, width: plotWidth } = useChartSurface(ref, tokens)
1695
+ const { ref: frameRef, surface, width: plotWidth, tokens } = useChartSurface(ref)
1662
1696
  const formatter = useMemo(() => resolveValueFormatter(valueFormatter, format ?? "number"), [valueFormatter, format])
1663
1697
  const model = useMemo(
1664
1698
  () => buildHeatmapModel({ data, x, y, value, xLabels, yLabels, flag, scale, minValue, maxValue }),