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

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-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-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 +23 -19
  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-HKSVYMYJ.js.map +0 -1
  209. package/dist/chunk-JKLLFVEU.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
@@ -22,7 +22,7 @@ import { forwardRef, useEffect, useMemo, useState, type ForwardedRef, type React
22
22
  import { ChartFrame } from "@squadbase/vantage/components"
23
23
  import { EChart } from "@squadbase/vantage/components"
24
24
  import type { ChartCommonProps, ChartEventPayload, ChartSeriesColor } from "@squadbase/vantage/components"
25
- import { createContext, useCallback, useRef } from "react"
25
+ import { createContext, useCallback, useRef, type RefObject } from "react"
26
26
  import type { ChartTone } from "@squadbase/vantage/components"
27
27
  import { DEFAULT_LOCALE, EMPTY_VALUE, formatValue, toValueFormatter, type ValueFormat } from "@squadbase/vantage/components"
28
28
  import type { ChartColor, ChartColors, ValueFormatter } from "@squadbase/vantage/components"
@@ -54,6 +54,8 @@ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--ch
54
54
  interface ChartTokens {
55
55
  /** Resolved `--chart-1..5`, in order. */
56
56
  series: string[]
57
+ /** Computed `font-family` of the container, so canvas text matches the DOM. Optional. */
58
+ fontFamily?: string
57
59
  /** Body text: titles and the default `textStyle`. */
58
60
  foreground: string | null
59
61
  /** Secondary text: axis labels, legend, subtitles. */
@@ -62,10 +64,23 @@ interface ChartTokens {
62
64
  border: string | null
63
65
  /** Tooltip surface. */
64
66
  popover: string | null
67
+ /** Semantic and surface dependencies used by typed chart options. Optional for hand-built tokens. */
68
+ colors?: Record<string, string>
69
+ /** Actual inherited painting surface (may change without a token changing). */
70
+ surface?: string | null
65
71
  /** Tooltip text. */
66
72
  popoverForeground: string | null
67
73
  }
68
74
 
75
+ // Keep scope out of the public token shape. A resolved snapshot remembers where arbitrary
76
+ // var(--token) expressions must be read, without retaining unmounted charts globally.
77
+ const tokenScopes = new WeakMap<ChartTokens, Element>()
78
+ const OPTION_COLOR_TOKENS = [
79
+ "--background", "--card", "--muted", "--primary", "--destructive",
80
+ ...["positive", "negative", "warning", "neutral", "info"].flatMap((tone) =>
81
+ ["", "-fill", "-subtle"].map((suffix) => `--${tone}${suffix}`)),
82
+ ]
83
+
69
84
  let normalizeContext: CanvasRenderingContext2D | null | undefined
70
85
 
71
86
  /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
@@ -121,14 +136,19 @@ function readChartTokens(el?: Element | null): ChartTokens | null {
121
136
  const color = read(token)
122
137
  if (color) series.push(color)
123
138
  }
124
- return {
139
+ const tokens: ChartTokens = {
125
140
  series,
141
+ colors: Object.fromEntries(OPTION_COLOR_TOKENS.map((name) => [name, styles.getPropertyValue(name).trim()])),
142
+ surface: readChartSurface(target),
143
+ ...(styles.fontFamily ? { fontFamily: styles.fontFamily } : {}),
126
144
  foreground: read("--foreground"),
127
145
  mutedForeground: read("--muted-foreground"),
128
146
  border: read("--border"),
129
147
  popover: read("--popover"),
130
148
  popoverForeground: read("--popover-foreground"),
131
149
  }
150
+ tokenScopes.set(tokens, target)
151
+ return tokens
132
152
  }
133
153
 
134
154
  /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
@@ -136,8 +156,13 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
136
156
  if (a === b) return true
137
157
  if (!a || !b) return false
138
158
  return (
159
+ tokenScopes.get(a) === tokenScopes.get(b) &&
160
+ a.surface === b.surface &&
161
+ Object.keys(a.colors ?? {}).length === Object.keys(b.colors ?? {}).length &&
162
+ Object.entries(a.colors ?? {}).every(([name, value]) => b.colors?.[name] === value) &&
139
163
  a.series.length === b.series.length &&
140
164
  a.series.every((color, i) => color === b.series[i]) &&
165
+ a.fontFamily === b.fontFamily &&
141
166
  a.foreground === b.foreground &&
142
167
  a.mutedForeground === b.mutedForeground &&
143
168
  a.border === b.border &&
@@ -160,7 +185,7 @@ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean
160
185
  * attributes alone leaves the chart painted in the previous palette until a
161
186
  * reload — the tokens in the DOM are new, the canvas is not.
162
187
  */
163
- function watchChartTokens(onChange: () => void): () => void {
188
+ function watchChartTokens(onChange: () => void, scope?: Element | null): () => void {
164
189
  if (typeof document === "undefined") return () => {}
165
190
  // Style edits arrive as bursts of mutations; coalesce them into one read, and
166
191
  // take it after the next frame so a swapped stylesheet has been applied.
@@ -175,8 +200,11 @@ function watchChartTokens(onChange: () => void): () => void {
175
200
 
176
201
  const themeObserver = new MutationObserver(schedule)
177
202
  const attributeFilter = ["class", "style", "data-theme"]
178
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
179
- if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
203
+ // Only the chart and its ancestors can change its inherited theme. Never observe
204
+ // the document subtree: canvas/tooltip/rendering mutations would trigger every chart.
205
+ for (let node: Element | null = scope ?? document.body ?? document.documentElement; node; node = node.parentElement) {
206
+ themeObserver.observe(node, { attributes: true, attributeFilter })
207
+ }
180
208
 
181
209
  const styleObserver = new MutationObserver(schedule)
182
210
  if (document.head) {
@@ -232,7 +260,7 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
232
260
  // Let the page background show through: charts sit on cards, not on a canvas.
233
261
  backgroundColor: "transparent",
234
262
  ...(tokens.series.length > 0 ? { color: tokens.series } : {}),
235
- textStyle: { color: text },
263
+ textStyle: { color: text, ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}) },
236
264
  title: { textStyle: { color: text }, subtextStyle: { color: muted } },
237
265
  legend: { textStyle: { color: muted } },
238
266
  categoryAxis: axisTheme(tokens, false),
@@ -248,7 +276,11 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
248
276
  padding: [8, 12],
249
277
  // Rounded card with a soft shadow; in dark mode the shadow barely shows, so the border carries the edge.
250
278
  extraCssText: "border-radius:8px;box-shadow:var(--shadow-card, 0 4px 12px rgb(0 0 0 / 0.08));",
251
- textStyle: { color: tokens.popoverForeground ?? undefined, fontSize: 12 },
279
+ textStyle: {
280
+ color: tokens.popoverForeground ?? undefined,
281
+ fontSize: 12,
282
+ ...(tokens.fontFamily ? { fontFamily: tokens.fontFamily } : {}),
283
+ },
252
284
  axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
253
285
  },
254
286
  }
@@ -260,22 +292,23 @@ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
260
292
  * components that build an ECharts option from token colours themselves (the
261
293
  * typed charts), so their palette follows light/dark like `EChart`'s theme.
262
294
  */
263
- function useChartTokens(): ChartTokens | null {
295
+ function useChartTokens(scope?: Element | null | RefObject<Element | null>): ChartTokens | null {
264
296
  const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
265
297
  // Compared here, not inside a `setTokens` updater: an updater that reads the DOM is
266
298
  // replayed by React on every rebase and returns a fresh object each time, so the state
267
299
  // never settles ("Maximum update depth exceeded" once the tokens change after mount).
268
300
  const tokensRef = useRef(tokens)
269
301
  useEffect(() => {
302
+ const target = scope && "current" in scope ? scope.current : scope
270
303
  const sync = () => {
271
- const next = readChartTokens()
304
+ const next = readChartTokens(target)
272
305
  if (sameChartTokens(tokensRef.current, next)) return
273
306
  tokensRef.current = next
274
307
  setTokens(next)
275
308
  }
276
309
  sync()
277
- return watchChartTokens(sync)
278
- }, [])
310
+ return watchChartTokens(sync, target)
311
+ }, [scope])
279
312
  return tokens
280
313
  }
281
314
 
@@ -287,7 +320,7 @@ const EMBEDDED_VAR_RE = /var\(\s*(--[\w-]+)\s*\)/g
287
320
  /**
288
321
  * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
289
322
  * the resolved series tokens; `"positive"` / `"warning"` / `"negative"` / `"neutral"` (and the area
290
- * variants `"positive-fill"`, …) are read off the document root (`--positive`, …); `var(--token)` is read off the document root; any
323
+ * variants `"positive-fill"`, …) are read in the snapshot's inherited scope (`--positive`, …); `var(--token)` uses the same scope; any
291
324
  * other CSS colour is normalized like the tokens are. A colour that can't be
292
325
  * resolved comes back as `""`, which ECharts treats as "use the palette".
293
326
  */
@@ -298,12 +331,13 @@ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | nul
298
331
  // Meaning colours are the theme tokens of the same name: same path as `var(--positive)`.
299
332
  if (TONE_NAME_RE.test(color.trim())) {
300
333
  if (typeof document === "undefined") return ""
301
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue(`--${color.trim()}`)) ?? ""
334
+ const target = (tokens && tokenScopes.get(tokens)) ?? document.documentElement
335
+ return toCanvasColor(getComputedStyle(target).getPropertyValue(`--${color.trim()}`)) ?? ""
302
336
  }
303
337
  if (typeof document === "undefined") return color
304
338
  let value = color.trim()
305
339
  const cssVar = CSS_VAR_RE.exec(value)
306
- const styles = getComputedStyle(document.documentElement)
340
+ const styles = getComputedStyle((tokens && tokenScopes.get(tokens)) ?? document.documentElement)
307
341
  if (cssVar) {
308
342
  value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
309
343
  } else if (value.includes("var(")) {
@@ -349,7 +383,7 @@ function readChartSurface(el: Element | null): string | null {
349
383
  if (color) return color
350
384
  }
351
385
  }
352
- return toCanvasColor(getComputedStyle(document.documentElement).getPropertyValue("--background"))
386
+ return toCanvasColor(getComputedStyle(el ?? document.documentElement).getPropertyValue("--background"))
353
387
  }
354
388
 
355
389
  /** Widths are bucketed so a resize only rebuilds the option when the label budget can change. */
@@ -363,9 +397,10 @@ const WIDTH_BUCKET = 40
363
397
  */
364
398
  function useChartSurface<E extends Element>(
365
399
  forwarded: ForwardedRef<E>,
366
- tokens: ChartTokens | null,
367
- ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined } {
400
+ tokens?: ChartTokens | null,
401
+ ): { ref: (el: E | null) => void; surface: string | undefined; width: number | undefined; tokens: ChartTokens | null } {
368
402
  const [node, setNode] = useState<E | null>(null)
403
+ const scopedTokens = useChartTokens(node)
369
404
  const [surface, setSurface] = useState<string | undefined>(undefined)
370
405
  const [width, setWidth] = useState<number | undefined>(undefined)
371
406
  const ref = useCallback(
@@ -379,7 +414,7 @@ function useChartSurface<E extends Element>(
379
414
  useEffect(() => {
380
415
  if (!node) return
381
416
  setSurface(readChartSurface(node) ?? undefined)
382
- }, [node, tokens])
417
+ }, [node, tokens, scopedTokens])
383
418
  useEffect(() => {
384
419
  if (!node || typeof ResizeObserver === "undefined") return
385
420
  const update = (w: number) => setWidth(Math.floor(w / WIDTH_BUCKET) * WIDTH_BUCKET)
@@ -388,7 +423,7 @@ function useChartSurface<E extends Element>(
388
423
  observer.observe(node)
389
424
  return () => observer.disconnect()
390
425
  }, [node])
391
- return { ref, surface, width }
426
+ return { ref, surface, width, tokens: scopedTokens }
392
427
  }
393
428
 
394
429
  /* ---------- inlined from @squadbase/squadkit registry/lib/chart-utils.ts (internal to @squadbase/vantage) ---------- */
@@ -1542,6 +1577,10 @@ const toDate = (t: ParsedTime) => new Date(t.year, t.month - 1, t.day)
1542
1577
 
1543
1578
  /** How many x labels a time axis shows at most (the step is picked so the labels never crowd). */
1544
1579
  const TIME_AXIS_MAX_LABELS = 6
1580
+ /** Under this plot width (px) the axis, legend and labels switch to their compact form. */
1581
+ const NARROW_PLOT_WIDTH = 400
1582
+ /** The most x labels a narrow plot shows (a 262px plot reads 4 at most). */
1583
+ const NARROW_TIME_AXIS_MAX_LABELS = 4
1545
1584
 
1546
1585
  /** Calendar-friendly label steps per granularity (every 3 months, every week …). */
1547
1586
  const TIME_AXIS_STEPS: Record<TimeGranularity, readonly number[]> = {
@@ -1987,7 +2026,9 @@ const LEGEND_LINE_BUDGET = 320
1987
2026
  /** How many lines the plain legend takes, estimated from its labels (12px text + 8px dot + gaps). */
1988
2027
  function legendLines(items: readonly string[], plotWidth?: number, reserve = 0): number {
1989
2028
  const width = items.reduce((sum, name) => sum + [...name].length * 12 + 8 + 5 + 16, 0)
1990
- const budget = Math.max(80, (plotWidth ? Math.min(LEGEND_LINE_BUDGET, plotWidth) : LEGEND_LINE_BUDGET) - reserve)
2029
+ // A narrow plot has no room for the PNG buttons beside the legend: they move off its line, so no reserve.
2030
+ const narrow = plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
2031
+ const budget = Math.max(80, (plotWidth ? (narrow ? plotWidth : Math.min(LEGEND_LINE_BUDGET, plotWidth)) : LEGEND_LINE_BUDGET) - (narrow ? 0 : reserve))
1991
2032
  return Math.min(3, Math.max(1, Math.ceil(width / budget)))
1992
2033
  }
1993
2034
 
@@ -2242,7 +2283,15 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2242
2283
  ? null
2243
2284
  : timeAxisLabelStep(
2244
2285
  rawIndex,
2245
- plotWidth ? Math.max(2, Math.min(TIME_AXIS_MAX_LABELS, Math.floor(plotWidth / TIME_LABEL_WIDTH))) : undefined,
2286
+ plotWidth
2287
+ ? Math.max(
2288
+ 2,
2289
+ Math.min(
2290
+ plotWidth < NARROW_PLOT_WIDTH ? NARROW_TIME_AXIS_MAX_LABELS : TIME_AXIS_MAX_LABELS,
2291
+ Math.floor(plotWidth / TIME_LABEL_WIDTH),
2292
+ ),
2293
+ )
2294
+ : undefined,
2246
2295
  )
2247
2296
  // Plain (non-date) x labels of a line chart: thinned to every n-th so they never touch, never rotated.
2248
2297
  const plainStep = (() => {
@@ -2288,8 +2337,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2288
2337
  : []
2289
2338
  // On a narrow plot the max and the latest label would print over each other: keep the latest.
2290
2339
  const pointGap = plotWidth && labels.length > 1 ? plotWidth / (kind === "bar" ? labels.length : labels.length - 1) : null
2340
+ // By default only the latest value is labelled; an explicit `highlight` keeps its max + latest.
2341
+ const latestOnly = cfg.highlight === undefined && !shareHighlight && picked.length === 2
2291
2342
  const highlightPoints =
2292
- picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
2343
+ latestOnly
2344
+ ? [picked[1]!]
2345
+ : picked.length === 2 && pointGap !== null && Math.abs(picked[1]!.index - picked[0]!.index) * pointGap < HIGHLIGHT_LABEL_WIDTH
2293
2346
  ? [picked[1]!]
2294
2347
  : picked
2295
2348
 
@@ -2500,6 +2553,8 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2500
2553
  ? { width: 0, color, opacity: 0 }
2501
2554
  : { width: 2, color, opacity: dimSeries ? DIMMED : 1 },
2502
2555
  itemStyle: { color, opacity: dimSeries ? DIMMED : 1 },
2556
+ // Hovering a series fades the others (not for a stack, where the bands read together).
2557
+ emphasis: stacked ? undefined : { focus: "series" as const },
2503
2558
  areaStyle: areaStyle && dimSeries ? { ...areaStyle, opacity: DIMMED * 0.3 } : areaStyle,
2504
2559
  markPoint: markPointFor(c, color),
2505
2560
  markLine: undefined,
@@ -2946,7 +3001,10 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
2946
3001
  legend: {
2947
3002
  show: showLegend,
2948
3003
  top: 0,
2949
- right: reserveActions ? ACTIONS_RESERVE : 0,
3004
+ // Narrow: left-aligned and wrapping above the plot, so nothing is cut off at the right edge.
3005
+ ...(plotWidth !== undefined && plotWidth < NARROW_PLOT_WIDTH
3006
+ ? { left: 0, right: 0 }
3007
+ : { right: reserveActions ? ACTIONS_RESERVE : 0 }),
2950
3008
  // Plain wraps onto a second line at narrow widths instead of hiding items behind arrows.
2951
3009
  type: "plain",
2952
3010
  icon: "circle",
@@ -3007,7 +3065,12 @@ function buildCartesianOption(cfg: CartesianChartConfig): EChartsOption {
3007
3065
  }
3008
3066
  }
3009
3067
  const shares = percent ? cartesianSeriesValues(i <= lastActual ? [row] : [], categories, "percent") : null
3010
- for (const p of list) {
3068
+ // Rows follow the legend order (categories first), not ECharts' own series order.
3069
+ const legendRank = (p: AxisTooltipParam) => {
3070
+ const at = categories.indexOf(p.seriesName ?? "")
3071
+ return at === -1 ? categories.length : at
3072
+ }
3073
+ for (const p of [...list].sort((a, b) => legendRank(a) - legendRank(b))) {
3011
3074
  const color = typeof p.color === "string" ? p.color : "currentColor"
3012
3075
  if (p.seriesId === FORECAST_SERIES_ID) {
3013
3076
  // At and before the boundary the forecast repeats the actual value.
@@ -3543,7 +3606,7 @@ function DonutChartImpl<T extends Record<string, unknown>>(
3543
3606
  const showLegend = donutOptions?.showLegend ?? true
3544
3607
  const valueFormatter = useMemo(() => resolveValueFormatter(valueFormatterProp, format), [valueFormatterProp, format])
3545
3608
 
3546
- const tokens = useChartTokens()
3609
+ const { ref: frameRef, tokens } = useChartSurface(ref)
3547
3610
  const slices = useMemo(
3548
3611
  () => donutSlices(data, { index: labelKey, value: valueKey, colors, maxSlices, otherColor: CHART_OTHER_COLOR }),
3549
3612
  [data, labelKey, valueKey, colors, maxSlices],
@@ -3625,7 +3688,7 @@ function DonutChartImpl<T extends Record<string, unknown>>(
3625
3688
  return (
3626
3689
  <ChartFrame
3627
3690
  {...chartRootProps(rest)}
3628
- ref={ref}
3691
+ ref={frameRef}
3629
3692
  title={title}
3630
3693
  summary={resolvedSummary}
3631
3694
  isLoading={isLoading ?? loading ?? false}