@squadbase/vantage 0.6.2-dev1.b28a4df → 0.6.2-dev1.b40ce36

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 (213) hide show
  1. package/dist/{build-X5NVCQKT.js → build-A23M4GCH.js} +7 -7
  2. package/dist/{build-X5NVCQKT.js.map → build-A23M4GCH.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-XPQSTZUU.js → chunk-7S52M2SL.js} +4 -3
  14. package/dist/chunk-7S52M2SL.js.map +1 -0
  15. package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
  16. package/dist/chunk-ANZLVSEP.js.map +1 -0
  17. package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
  18. package/dist/chunk-CKWPK4RO.js.map +1 -0
  19. package/dist/chunk-JXFCSF3K.js +673 -0
  20. package/dist/chunk-JXFCSF3K.js.map +1 -0
  21. package/dist/{chunk-UNCSBSO4.js → chunk-QOXXOFGL.js} +112 -927
  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 +13 -249
  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-H4I2JL63.js} +7 -7
  39. package/dist/{dev-4XEDTELV.js.map → dev-H4I2JL63.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-B2O3GJDU.js} +5 -5
  44. package/dist/{doctor-5E2VEZPK.js.map → doctor-B2O3GJDU.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/changelog.md +2 -0
  61. package/docs/en/components.md +13 -2
  62. package/docs/en/doc/delta-text.md +40 -0
  63. package/docs/en/doc/doc-action-items.md +49 -0
  64. package/docs/en/doc/doc-callout.md +38 -0
  65. package/docs/en/doc/doc-data-table.md +60 -0
  66. package/docs/en/doc/doc-executive-summary.md +64 -0
  67. package/docs/en/doc/doc-figure.md +56 -0
  68. package/docs/en/doc/doc-key-findings.md +61 -0
  69. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  70. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  71. package/docs/en/doc/doc-report-header.md +46 -0
  72. package/docs/en/pages-and-metadata.md +15 -0
  73. package/docs/en/parts/area-chart.md +99 -0
  74. package/docs/en/parts/bar-chart.md +91 -0
  75. package/docs/en/parts/bar-list.md +50 -0
  76. package/docs/en/parts/category-bar.md +37 -0
  77. package/docs/en/parts/chart-frame.md +39 -0
  78. package/docs/en/parts/delta.md +4 -3
  79. package/docs/en/parts/donut-chart.md +72 -0
  80. package/docs/en/parts/funnel-steps.md +2 -1
  81. package/docs/en/parts/heatmap.md +53 -0
  82. package/docs/en/parts/kpi-card.md +2 -1
  83. package/docs/en/parts/legend.md +35 -0
  84. package/docs/en/parts/line-chart.md +94 -0
  85. package/docs/en/parts/progress-circle.md +33 -0
  86. package/docs/en/parts/sample-data.md +39 -0
  87. package/docs/en/parts/scatter-chart.md +56 -0
  88. package/docs/en/parts/tracker.md +36 -0
  89. package/docs/en/parts/updated-at.md +30 -0
  90. package/docs/en/parts/waterfall-chart.md +56 -0
  91. package/docs/en/slide/slide-chart-full.md +48 -0
  92. package/docs/en/slide/slide-chart-insight.md +52 -0
  93. package/docs/en/slide/slide-columns.md +46 -0
  94. package/docs/en/slide/slide-comparison.md +68 -0
  95. package/docs/en/slide/slide-cover.md +39 -0
  96. package/docs/en/slide/slide-exec-summary.md +47 -0
  97. package/docs/en/slide/slide-heading.md +38 -0
  98. package/docs/en/slide/slide-kpi-grid.md +55 -0
  99. package/docs/en/slide/slide-kpi-hero.md +58 -0
  100. package/docs/en/slide/slide-next-actions.md +49 -0
  101. package/docs/en/slide/slide-scorecard-table.md +54 -0
  102. package/docs/en/slide/slide-section.md +44 -0
  103. package/docs/en/slide/slide-source-line.md +34 -0
  104. package/docs/en/ui/alert-dialog.md +42 -0
  105. package/docs/en/ui/avatar.md +31 -0
  106. package/docs/en/ui/combobox.md +74 -0
  107. package/docs/en/ui/drawer.md +41 -0
  108. package/docs/en/ui/empty.md +1 -1
  109. package/docs/en/ui/error-state.md +4 -3
  110. package/docs/en/ui/field.md +42 -0
  111. package/docs/en/ui/hover-card.md +21 -0
  112. package/docs/en/ui/kbd.md +17 -0
  113. package/docs/en/ui/loading.md +1 -1
  114. package/docs/en/ui/pagination.md +36 -0
  115. package/docs/en/ui/radio-group.md +21 -0
  116. package/docs/en/ui/toast.md +44 -0
  117. package/docs/en/ui/toggle-group.md +4 -3
  118. package/docs/index.json +857 -77
  119. package/docs/ja/changelog.md +2 -0
  120. package/docs/ja/components.md +12 -1
  121. package/docs/ja/doc/delta-text.md +40 -0
  122. package/docs/ja/doc/doc-action-items.md +49 -0
  123. package/docs/ja/doc/doc-callout.md +38 -0
  124. package/docs/ja/doc/doc-data-table.md +60 -0
  125. package/docs/ja/doc/doc-executive-summary.md +64 -0
  126. package/docs/ja/doc/doc-figure.md +56 -0
  127. package/docs/ja/doc/doc-key-findings.md +61 -0
  128. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  129. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  130. package/docs/ja/doc/doc-report-header.md +46 -0
  131. package/docs/ja/page-types/slide.md +1 -0
  132. package/docs/ja/pages-and-metadata.md +15 -0
  133. package/docs/ja/parts/area-chart.md +49 -25
  134. package/docs/ja/parts/bar-chart.md +50 -24
  135. package/docs/ja/parts/bar-list.md +31 -8
  136. package/docs/ja/parts/chart-frame.md +36 -0
  137. package/docs/ja/parts/delta.md +5 -4
  138. package/docs/ja/parts/donut-chart.md +45 -26
  139. package/docs/ja/parts/funnel-steps.md +27 -8
  140. package/docs/ja/parts/heatmap.md +45 -0
  141. package/docs/ja/parts/kpi-card.md +2 -1
  142. package/docs/ja/parts/legend.md +36 -0
  143. package/docs/ja/parts/line-chart.md +48 -22
  144. package/docs/ja/parts/sample-data.md +61 -0
  145. package/docs/ja/parts/scatter-chart.md +69 -0
  146. package/docs/ja/parts/updated-at.md +27 -0
  147. package/docs/ja/parts/waterfall-chart.md +61 -0
  148. package/docs/ja/slide/slide-chart-full.md +48 -0
  149. package/docs/ja/slide/slide-chart-insight.md +52 -0
  150. package/docs/ja/slide/slide-columns.md +46 -0
  151. package/docs/ja/slide/slide-comparison.md +68 -0
  152. package/docs/ja/slide/slide-cover.md +39 -0
  153. package/docs/ja/slide/slide-exec-summary.md +47 -0
  154. package/docs/ja/slide/slide-heading.md +38 -0
  155. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  156. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  157. package/docs/ja/slide/slide-next-actions.md +49 -0
  158. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  159. package/docs/ja/slide/slide-section.md +44 -0
  160. package/docs/ja/slide/slide-source-line.md +34 -0
  161. package/docs/ja/ui/empty.md +1 -1
  162. package/docs/ja/ui/error-state.md +4 -3
  163. package/docs/ja/ui/loading.md +1 -1
  164. package/docs/ja/ui/toggle-group.md +4 -3
  165. package/package.json +2 -2
  166. package/registry/ui/area-chart.tsx +88 -26
  167. package/registry/ui/attention-list.tsx +303 -0
  168. package/registry/ui/bar-chart.tsx +88 -26
  169. package/registry/ui/bar-list.tsx +87 -24
  170. package/registry/ui/box-plot-chart.tsx +3906 -0
  171. package/registry/ui/category-bar.tsx +54 -19
  172. package/registry/ui/chart-frame.tsx +56 -21
  173. package/registry/ui/dashboard-card.tsx +2 -2
  174. package/registry/ui/delta.tsx +11 -8
  175. package/registry/ui/donut-chart.tsx +89 -26
  176. package/registry/ui/echart.tsx +194 -44
  177. package/registry/ui/funnel-steps.tsx +33 -5
  178. package/registry/ui/heatmap.tsx +55 -21
  179. package/registry/ui/histogram-chart.tsx +3986 -0
  180. package/registry/ui/kpi-card.tsx +41 -14
  181. package/registry/ui/legend.tsx +87 -24
  182. package/registry/ui/line-chart.tsx +88 -26
  183. package/registry/ui/page-shell.tsx +9 -9
  184. package/registry/ui/paired-chart.tsx +3827 -0
  185. package/registry/ui/sample-data.tsx +1 -1
  186. package/registry/ui/scatter-chart.tsx +88 -26
  187. package/registry/ui/section-header.tsx +2 -2
  188. package/registry/ui/slider.tsx +1 -1
  189. package/registry/ui/status-badge.tsx +8 -8
  190. package/registry/ui/tooltip.tsx +6 -2
  191. package/registry/ui/tracker.tsx +9 -3
  192. package/registry/ui/trend-indicator.tsx +2 -2
  193. package/registry/ui/waterfall-chart.tsx +88 -26
  194. package/skills/vantage-design/SKILL.md +27 -25
  195. package/skills/vantage-design/references/extract.md +16 -14
  196. package/skills/vantage-design/references/format.md +6 -5
  197. package/skills/vantage-design/references/presets.md +60 -110
  198. package/skills/vantage-pitfalls/SKILL.md +5 -0
  199. package/skills/vantage-slide/SKILL.md +54 -36
  200. package/templates/AGENTS.md +4 -2
  201. package/templates/agents/doc.md +48 -0
  202. package/templates/agents/page.md +22 -1
  203. package/templates/agents/slide.md +70 -2
  204. package/theme.css +24 -20
  205. package/dist/chunk-2ICNHUG6.js.map +0 -1
  206. package/dist/chunk-7DZGJQ3P.js.map +0 -1
  207. package/dist/chunk-CEXAQQ5W.js.map +0 -1
  208. package/dist/chunk-JKLLFVEU.js.map +0 -1
  209. package/dist/chunk-UNCSBSO4.js.map +0 -1
  210. package/dist/chunk-V3LF7BE7.js.map +0 -1
  211. package/dist/chunk-WAMZWZJB.js.map +0 -1
  212. package/dist/chunk-XPQSTZUU.js.map +0 -1
  213. package/dist/chunk-XQUSGIDG.js.map +0 -1
@@ -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"
@@ -42,6 +42,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
42
42
  interface ChartTokens {
43
43
  /** Resolved `--chart-1..5`, in order. */
44
44
  series: string[]
45
+ /** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
46
+ fontFamily?: string
45
47
  /** Body text: titles and the default `textStyle`. */
46
48
  foreground: string | null
47
49
  /** Secondary text: axis labels, legend, subtitles. */
@@ -50,10 +52,23 @@ interface ChartTokens {
50
52
  border: string | null
51
53
  /** Tooltip surface. */
52
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
53
59
  /** Tooltip text. */
54
60
  popoverForeground: string | null
55
61
  }
56
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
+
57
72
  let normalizeContext: CanvasRenderingContext2D | null | undefined
58
73
 
59
74
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -109,14 +124,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
109
124
  const color = read(token)
110
125
  if (color) series.push(color)
111
126
  }
112
- return {
127
+ const tokens: ChartTokens = {
113
128
  series,
129
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
130
+ surface: readChartSurface(target),
131
+ ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
114
132
  foreground: read("--foreground"),
115
133
  mutedForeground: read("--muted-foreground"),
116
134
  border: read("--border"),
117
135
  popover: read("--popover"),
118
136
  popoverForeground: read("--popover-foreground"),
119
137
  }
138
+ tokenScopes.set(tokens, target)
139
+ return tokens
120
140
  }
121
141
 
122
142
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -124,8 +144,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
124
144
  if (a === b) return true
125
145
  if (!a || !b) return false
126
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) &&
127
151
  a.series.length === b.series.length &&
128
152
  a.series.every((color, i) => color === b.series[i]) &&
153
+ a.fontFamily === b.fontFamily &&
129
154
  a.foreground === b.foreground &&
130
155
  a.mutedForeground === b.mutedForeground &&
131
156
  a.border === b.border &&
@@ -148,7 +173,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
148
173
  * attributes alone leaves the chart painted in the previous palette until a
149
174
  * reload — the tokens in the DOM are new, the canvas is not.
150
175
  */
151
- function watchChartTokens(onChange: () => void): () => void {
176
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
152
177
  if (typeof document === "undefined") return () => {}
153
178
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
154
179
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -163,8 +188,11 @@ function watchChartTokens(onChange: () => void): () => void {
163
188
 
164
189
  const themeObserver = new MutationObserver(schedule)
165
190
  const attributeFilter = ["class", "style", "data-theme"]
166
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
167
- 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
+ }
168
196
 
169
197
  const styleObserver = new MutationObserver(schedule)
170
198
  if (document.head) {
@@ -220,7 +248,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
220
248
  // Let the page background show through: charts sit on cards, not on a canvas.
221
249
  backgroundColor: "transparent",
222
250
  ...(tokens.series.length > 0 ? { color: tokens.series } : {}),
223
- textStyle: { color: text },
251
+ textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
224
252
  title: { textStyle: { color: text }, subtextStyle: { color: muted } },
225
253
  legend: { textStyle: { color: muted } },
226
254
  categoryAxis: axisTheme(tokens, false),
@@ -236,7 +264,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
236
264
  padding: [8, 12],
237
265
  // Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
238
266
  extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
239
- textStyle: { color: tokens.popoverForeground ?? undefined, fontSize: 12 },
267
+ textStyle: {
268
+ color: tokens.popoverForeground ?? undefined,
269
+ fontSize: 12,
270
+ ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
271
+ },
240
272
  axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
241
273
  },
242
274
  }
@@ -248,22 +280,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
248
280
  * components that build an ECharts option from token colours themselves (the
249
281
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
250
282
  */
251
- function useChartTokens(): ChartTokens | null {
283
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
252
284
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
253
285
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
254
286
  // replayed by React on every rebase and returns a fresh object each time, so the state
255
287
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
256
288
  const tokensRef = useRef(tokens)
257
289
  useEffect(() => {
290
+ const target = scope && "current" in scope ? scope.current : scope
258
291
  const sync = () => {
259
- const next = readChartTokens()
292
+ const next = readChartTokens(target)
260
293
  if (sameChartTokens(tokensRef.current, next)) return
261
294
  tokensRef.current = next
262
295
  setTokens(next)
263
296
  }
264
297
  sync()
265
- return watchChartTokens(sync)
266
- }, [])
298
+ return watchChartTokens(sync, target)
299
+ }, [scope])
267
300
  return tokens
268
301
  }
269
302
 
@@ -275,7 +308,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
275
308
  /**
276
309
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
277
310
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
278
- * 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
279
312
  * other CSS colour is normalized like the tokens are. A colour that can't be
280
313
  * resolved comes back as `""`, which ECharts treats as "use the palette".
281
314
  */
@@ -286,12 +319,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
286
319
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
287
320
  if (TONE_NAME_RE.test(color.trim())) {
288
321
  if (typeof document === "undefined") return ""
289
- 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()}`)) ?? ""
290
324
  }
291
325
  if (typeof document === "undefined") return color
292
326
  let value = color.trim()
293
327
  const cssVar = CSS_VAR_RE.exec(value)
294
- const styles = getComputedStyle(document.documentElement)
328
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
295
329
  if (cssVar) {
296
330
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
297
331
  } else if (value.includes("var(")) {
@@ -337,7 +371,7 @@ function readChartSurface(el: Element | null): string | null {
337
371
  if (color) return color
338
372
  }
339
373
  }
340
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
374
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
341
375
  }
342
376
 
343
377
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -351,9 +385,10 @@ const WIDTH_BUCKET = 40
351
385
  */
352
386
  function useChartSurface<E extends Element>(
353
387
  forwarded: ForwardedRef<E>,
354
- tokens: ChartTokens | null,
355
- ): { 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 } {
356
390
  const [node, setNode] = useState<E | null>(null)
391
+ const scopedTokens = useChartTokens(node)
357
392
  const [surface, setSurface] = useState<string | undefined>(undefined)
358
393
  const [width, setWidth] = useState<number | undefined>(undefined)
359
394
  const ref = useCallback(
@@ -367,7 +402,7 @@ function useChartSurface<E extends Element>(
367
402
  useEffect(() => {
368
403
  if (!node) return
369
404
  setSurface(readChartSurface(node) ?? undefined)
370
- }, [node, tokens])
405
+ }, [node, tokens, scopedTokens])
371
406
  useEffect(() => {
372
407
  if (!node || typeof ResizeObserver === "undefined") return
373
408
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -376,7 +411,7 @@ function useChartSurface<E extends Element>(
376
411
  observer.observe(node)
377
412
  return () => observer.disconnect()
378
413
  }, [node])
379
- return { ref, surface, width }
414
+ return { ref, surface, width, tokens: scopedTokens }
380
415
  }
381
416
 
382
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"
@@ -49,6 +49,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
49
49
  interface ChartTokens {
50
50
  /** Resolved `--chart-1..5`, in order. */
51
51
  series: string[]
52
+ /** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
53
+ fontFamily?: string
52
54
  /** Body text: titles and the default `textStyle`. */
53
55
  foreground: string | null
54
56
  /** Secondary text: axis labels, legend, subtitles. */
@@ -57,10 +59,23 @@ interface ChartTokens {
57
59
  border: string | null
58
60
  /** Tooltip surface. */
59
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
60
66
  /** Tooltip text. */
61
67
  popoverForeground: string | null
62
68
  }
63
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
+
64
79
  let normalizeContext: CanvasRenderingContext2D | null | undefined
65
80
 
66
81
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -116,14 +131,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
116
131
  const color = read(token)
117
132
  if (color) series.push(color)
118
133
  }
119
- return {
134
+ const tokens: ChartTokens = {
120
135
  series,
136
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
137
+ surface: readChartSurface(target),
138
+ ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
121
139
  foreground: read("--foreground"),
122
140
  mutedForeground: read("--muted-foreground"),
123
141
  border: read("--border"),
124
142
  popover: read("--popover"),
125
143
  popoverForeground: read("--popover-foreground"),
126
144
  }
145
+ tokenScopes.set(tokens, target)
146
+ return tokens
127
147
  }
128
148
 
129
149
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -131,8 +151,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
131
151
  if (a === b) return true
132
152
  if (!a || !b) return false
133
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) &&
134
158
  a.series.length === b.series.length &&
135
159
  a.series.every((color, i) => color === b.series[i]) &&
160
+ a.fontFamily === b.fontFamily &&
136
161
  a.foreground === b.foreground &&
137
162
  a.mutedForeground === b.mutedForeground &&
138
163
  a.border === b.border &&
@@ -155,7 +180,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
155
180
  * attributes alone leaves the chart painted in the previous palette until a
156
181
  * reload — the tokens in the DOM are new, the canvas is not.
157
182
  */
158
- function watchChartTokens(onChange: () => void): () => void {
183
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
159
184
  if (typeof document === "undefined") return () => {}
160
185
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
161
186
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -170,8 +195,11 @@ function watchChartTokens(onChange: () => void): () => void {
170
195
 
171
196
  const themeObserver = new MutationObserver(schedule)
172
197
  const attributeFilter = ["class", "style", "data-theme"]
173
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
174
- 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
+ }
175
203
 
176
204
  const styleObserver = new MutationObserver(schedule)
177
205
  if (document.head) {
@@ -227,7 +255,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
227
255
  // Let the page background show through: charts sit on cards, not on a canvas.
228
256
  backgroundColor: "transparent",
229
257
  ...(tokens.series.length > 0 ? { color: tokens.series } : {}),
230
- textStyle: { color: text },
258
+ textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
231
259
  title: { textStyle: { color: text }, subtextStyle: { color: muted } },
232
260
  legend: { textStyle: { color: muted } },
233
261
  categoryAxis: axisTheme(tokens, false),
@@ -243,7 +271,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
243
271
  padding: [8, 12],
244
272
  // Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
245
273
  extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
246
- textStyle: { color: tokens.popoverForeground ?? undefined, fontSize: 12 },
274
+ textStyle: {
275
+ color: tokens.popoverForeground ?? undefined,
276
+ fontSize: 12,
277
+ ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
278
+ },
247
279
  axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
248
280
  },
249
281
  }
@@ -255,22 +287,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
255
287
  * components that build an ECharts option from token colours themselves (the
256
288
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
257
289
  */
258
- function useChartTokens(): ChartTokens | null {
290
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
259
291
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
260
292
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
261
293
  // replayed by React on every rebase and returns a fresh object each time, so the state
262
294
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
263
295
  const tokensRef = useRef(tokens)
264
296
  useEffect(() => {
297
+ const target = scope && "current" in scope ? scope.current : scope
265
298
  const sync = () => {
266
- const next = readChartTokens()
299
+ const next = readChartTokens(target)
267
300
  if (sameChartTokens(tokensRef.current, next)) return
268
301
  tokensRef.current = next
269
302
  setTokens(next)
270
303
  }
271
304
  sync()
272
- return watchChartTokens(sync)
273
- }, [])
305
+ return watchChartTokens(sync, target)
306
+ }, [scope])
274
307
  return tokens
275
308
  }
276
309
 
@@ -282,7 +315,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
282
315
  /**
283
316
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
284
317
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
285
- * 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
286
319
  * other CSS colour is normalized like the tokens are. A colour that can't be
287
320
  * resolved comes back as `""`, which ECharts treats as "use the palette".
288
321
  */
@@ -293,12 +326,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
293
326
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
294
327
  if (TONE_NAME_RE.test(color.trim())) {
295
328
  if (typeof document === "undefined") return ""
296
- 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()}`)) ?? ""
297
331
  }
298
332
  if (typeof document === "undefined") return color
299
333
  let value = color.trim()
300
334
  const cssVar = CSS_VAR_RE.exec(value)
301
- const styles = getComputedStyle(document.documentElement)
335
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
302
336
  if (cssVar) {
303
337
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
304
338
  } else if (value.includes("var(")) {
@@ -344,7 +378,7 @@ function readChartSurface(el: Element | null): string | null {
344
378
  if (color) return color
345
379
  }
346
380
  }
347
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
381
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
348
382
  }
349
383
 
350
384
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -358,9 +392,10 @@ const WIDTH_BUCKET = 40
358
392
  */
359
393
  function useChartSurface<E extends Element>(
360
394
  forwarded: ForwardedRef<E>,
361
- tokens: ChartTokens | null,
362
- ): { 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 } {
363
397
  const [node, setNode] = useState<E | null>(null)
398
+ const scopedTokens = useChartTokens(node)
364
399
  const [surface, setSurface] = useState<string | undefined>(undefined)
365
400
  const [width, setWidth] = useState<number | undefined>(undefined)
366
401
  const ref = useCallback(
@@ -374,7 +409,7 @@ function useChartSurface<E extends Element>(
374
409
  useEffect(() => {
375
410
  if (!node) return
376
411
  setSurface(readChartSurface(node) ?? undefined)
377
- }, [node, tokens])
412
+ }, [node, tokens, scopedTokens])
378
413
  useEffect(() => {
379
414
  if (!node || typeof ResizeObserver === "undefined") return
380
415
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -383,7 +418,7 @@ function useChartSurface<E extends Element>(
383
418
  observer.observe(node)
384
419
  return () => observer.disconnect()
385
420
  }, [node])
386
- return { ref, surface, width }
421
+ return { ref, surface, width, tokens: scopedTokens }
387
422
  }
388
423
 
389
424
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -1219,12 +1254,12 @@ export const ChartFrame = forwardRef<HTMLDivElement, ChartFrameProps>(function C
1219
1254
  {(title || summary) && (
1220
1255
  <figcaption data-slot="chart-frame-header" className="flex flex-col gap-1">
1221
1256
  {title && (
1222
- <h3 id={titleId} data-slot="chart-frame-title" className="text-sm leading-none font-medium">
1257
+ <h3 id={titleId} data-slot="chart-frame-title" className="text-sm leading-snug font-semibold">
1223
1258
  {title}
1224
1259
  </h3>
1225
1260
  )}
1226
1261
  {summary && (
1227
- <p id={summaryId} data-slot="chart-frame-summary" className="text-sm text-muted-foreground">
1262
+ <p id={summaryId} data-slot="chart-frame-summary" className="text-sm leading-relaxed text-muted-foreground">
1228
1263
  {summary}
1229
1264
  </p>
1230
1265
  )}
@@ -75,7 +75,7 @@ export const DashboardCardTitle = forwardRef<HTMLHeadingElement, DashboardCardTi
75
75
  <h3
76
76
  ref={ref}
77
77
  data-slot="dashboard-card-title"
78
- className={cn("text-sm leading-none font-semibold tracking-tight", className)}
78
+ className={cn("text-sm leading-snug font-semibold tracking-tight", className)}
79
79
  {...props}
80
80
  />
81
81
  ),
@@ -91,7 +91,7 @@ export const DashboardCardDescription = forwardRef<
91
91
  <p
92
92
  ref={ref}
93
93
  data-slot="dashboard-card-description"
94
- className={cn("text-xs text-muted-foreground", className)}
94
+ className={cn("text-xs leading-normal text-muted-foreground", className)}
95
95
  {...props}
96
96
  />
97
97
  ))
@@ -9,7 +9,7 @@
9
9
  * Direction, rate, colour and wording are all derived here from `value` and
10
10
  * `comparison` (via `computeComparison`); none of them can be passed in, so they
11
11
  * cannot contradict each other. Colour is never the only signal: the arrow and
12
- * the sign always accompany it. Set `polarity="lower-is-better"` for metrics
12
+ * the sign always accompany it. Set `betterWhen="down"` for metrics
13
13
  * where a rise is bad (churn, error rate).
14
14
  *
15
15
  * - A flat change (inside `neutralBand`) uses a right arrow (→), never a minus-like dash, so it cannot
@@ -31,7 +31,7 @@
31
31
  * // Vantage: import { Delta } from "@squadbase/vantage/components"
32
32
  *
33
33
  * <Delta value={48_200_000} comparison={{ previous: 42_900_000, vs: "prior-period", grain: "month" }} />
34
- * <Delta value={0.021} format="percent" polarity="lower-is-better"
34
+ * <Delta value={0.021} format="percent" betterWhen="down"
35
35
  * comparison={{ previous: 0.026, vs: "prior-year", grain: "month" }} />
36
36
  */
37
37
  import { forwardRef, type HTMLAttributes } from "react"
@@ -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",
@@ -85,7 +85,9 @@ export interface DeltaProps
85
85
  value: number | null | undefined
86
86
  /** What the value is compared against. Required so the delta always carries its label. */
87
87
  comparison: Comparison
88
- /** Whether a rise is good. Default `"higher-is-better"`. */
88
+ /** Which direction is good: `"up"` (default) or `"down"` (churn, error rate). Wins over `polarity`. */
89
+ betterWhen?: "up" | "down"
90
+ /** @deprecated Older name of `betterWhen` (`"higher-is-better"` / `"lower-is-better"`); `"neutral"` is only reachable here. `betterWhen` wins if both are given. */
89
91
  polarity?: Polarity
90
92
  /** Absolute ratio below which the change reads as flat (0.01 = 1%). Default 0.01. */
91
93
  neutralBand?: number
@@ -134,6 +136,7 @@ export const Delta = forwardRef<HTMLSpanElement, DeltaProps>(
134
136
  {
135
137
  value,
136
138
  comparison,
139
+ betterWhen,
137
140
  polarity,
138
141
  neutralBand,
139
142
  format,
@@ -149,7 +152,7 @@ export const Delta = forwardRef<HTMLSpanElement, DeltaProps>(
149
152
  ) => {
150
153
  // Nothing to compare against yet (a draft fills props in steps).
151
154
  if (!comparison) return null
152
- const result = computeComparison(value, comparison, { polarity, neutralBand })
155
+ const result = computeComparison(value, comparison, { polarity: betterWhen ? (betterWhen === "down" ? "lower-is-better" : "higher-is-better") : polarity, neutralBand })
153
156
  const display = change ?? defaultChangeDisplay(format)
154
157
  const formatDigits = typeof format === "object" && format !== null && "digits" in format ? format.digits : undefined
155
158
  const changeDigits = display === "points" ? (digits ?? formatDigits) : digits