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