@squadbase/vantage 0.6.2-dev1.a7c4a3e → 0.6.2-dev1.ac99e02

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 (186) hide show
  1. package/dist/add-4GHF2TX2.js +6 -0
  2. package/dist/add-4GHF2TX2.js.map +1 -0
  3. package/dist/{build-JB4VOMUI.js → build-MFWRPYKW.js} +9 -9
  4. package/dist/{build-JB4VOMUI.js.map → build-MFWRPYKW.js.map} +1 -1
  5. package/dist/{check-ZJYK6RLG.js → check-ZBIH422T.js} +7 -7
  6. package/dist/{check-ZJYK6RLG.js.map → check-ZBIH422T.js.map} +1 -1
  7. package/dist/{chunk-BCE22JSM.js → chunk-7DZGJQ3P.js} +33 -14
  8. package/dist/chunk-7DZGJQ3P.js.map +1 -0
  9. package/dist/{chunk-U2M6HGKQ.js → chunk-AABMKHEU.js} +4 -4
  10. package/dist/{chunk-U2M6HGKQ.js.map → chunk-AABMKHEU.js.map} +1 -1
  11. package/dist/{chunk-MP3QSOXL.js → chunk-AG6RZ2RR.js} +992 -16
  12. package/dist/chunk-AG6RZ2RR.js.map +1 -0
  13. package/dist/{chunk-KLEIVDUV.js → chunk-CEXAQQ5W.js} +150 -11
  14. package/dist/chunk-CEXAQQ5W.js.map +1 -0
  15. package/dist/{chunk-QI3XQKHS.js → chunk-GNXCX4WE.js} +10 -3
  16. package/dist/chunk-GNXCX4WE.js.map +1 -0
  17. package/dist/chunk-IBGOCWJB.js +1716 -0
  18. package/dist/chunk-IBGOCWJB.js.map +1 -0
  19. package/dist/{chunk-JGRMRXXZ.js → chunk-JKLLFVEU.js} +3 -3
  20. package/dist/{chunk-JGRMRXXZ.js.map → chunk-JKLLFVEU.js.map} +1 -1
  21. package/dist/{chunk-RPZAALCX.js → chunk-OB6UBJXF.js} +153 -153
  22. package/dist/{chunk-RPZAALCX.js.map → chunk-OB6UBJXF.js.map} +1 -1
  23. package/dist/{chunk-EP6JHXSR.js → chunk-QEWCYW3I.js} +44 -9
  24. package/dist/chunk-QEWCYW3I.js.map +1 -0
  25. package/dist/{chunk-ZVANIULZ.js → chunk-RNTDBX6S.js} +4 -4
  26. package/dist/{chunk-ZVANIULZ.js.map → chunk-RNTDBX6S.js.map} +1 -1
  27. package/dist/{chunk-PVTHHV7X.js → chunk-V3LF7BE7.js} +27 -5
  28. package/dist/chunk-V3LF7BE7.js.map +1 -0
  29. package/dist/{chunk-DTDVSFRY.js → chunk-XQUSGIDG.js} +17 -3
  30. package/dist/chunk-XQUSGIDG.js.map +1 -0
  31. package/dist/cli.js +12 -11
  32. package/dist/cli.js.map +1 -1
  33. package/dist/client/index.d.ts +32 -3
  34. package/dist/client/index.js +205 -12
  35. package/dist/client/index.js.map +1 -1
  36. package/dist/components/index.d.ts +216 -1
  37. package/dist/components/index.js +2 -2
  38. package/dist/{dev-FCEC3UKQ.js → dev-DCTOHQ6W.js} +10 -10
  39. package/dist/dev-DCTOHQ6W.js.map +1 -0
  40. package/dist/{docs-EJJ3BECT.js → docs-JVP4RPKT.js} +3 -3
  41. package/dist/{docs-EJJ3BECT.js.map → docs-JVP4RPKT.js.map} +1 -1
  42. package/dist/{doctor-WHZXPERA.js → doctor-5E2VEZPK.js} +7 -7
  43. package/dist/{doctor-WHZXPERA.js.map → doctor-5E2VEZPK.js.map} +1 -1
  44. package/dist/{preview-OGMCNQUO.js → preview-T6CLSQVX.js} +3 -3
  45. package/dist/{preview-OGMCNQUO.js.map → preview-T6CLSQVX.js.map} +1 -1
  46. package/dist/query/index.d.ts +0 -11
  47. package/dist/query/index.js +2 -2
  48. package/dist/{routes-GTFO5PUF.js → routes-3SGAFDMC.js} +6 -6
  49. package/dist/{routes-GTFO5PUF.js.map → routes-3SGAFDMC.js.map} +1 -1
  50. package/dist/{search-6Y422SV6.js → search-5LC4XPB5.js} +3 -3
  51. package/dist/{search-6Y422SV6.js.map → search-5LC4XPB5.js.map} +1 -1
  52. package/dist/slide/index.js +47 -10
  53. package/dist/slide/index.js.map +1 -1
  54. package/dist/table/index.js +8 -7
  55. package/dist/table/index.js.map +1 -1
  56. package/dist/ui/index.d.ts +140 -10
  57. package/dist/ui/index.js +1405 -63
  58. package/dist/ui/index.js.map +1 -1
  59. package/dist/{upgrade-AL4KZZR4.js → upgrade-DBBE6GIR.js} +7 -7
  60. package/dist/{upgrade-AL4KZZR4.js.map → upgrade-DBBE6GIR.js.map} +1 -1
  61. package/dist/vite/index.d.ts +10 -0
  62. package/dist/vite/index.js +4 -4
  63. package/docs/index.json +291 -61
  64. package/docs/ja/changelog.md +26 -0
  65. package/docs/ja/components.md +33 -89
  66. package/docs/ja/page-types/csv.md +83 -0
  67. package/docs/ja/page-types/doc.md +83 -0
  68. package/docs/ja/page-types/excel.md +82 -0
  69. package/docs/ja/page-types/slide.md +132 -0
  70. package/docs/ja/page-types.md +47 -0
  71. package/docs/ja/parts/area-chart.md +61 -0
  72. package/docs/ja/parts/bar-chart.md +69 -0
  73. package/docs/ja/parts/bar-list.md +35 -0
  74. package/docs/ja/parts/category-bar.md +36 -0
  75. package/docs/ja/parts/donut-chart.md +47 -0
  76. package/docs/ja/parts/echart.md +3 -0
  77. package/docs/ja/parts/line-chart.md +58 -0
  78. package/docs/ja/parts/progress-circle.md +34 -0
  79. package/docs/ja/parts/sparkline.md +1 -1
  80. package/docs/ja/parts/tracker.md +32 -0
  81. package/docs/ja/routing.md +8 -86
  82. package/docs/ja/ui/alert-dialog.md +41 -0
  83. package/docs/ja/ui/avatar.md +31 -0
  84. package/docs/ja/ui/combobox.md +74 -0
  85. package/docs/ja/ui/drawer.md +41 -0
  86. package/docs/ja/ui/field.md +42 -0
  87. package/docs/ja/ui/hover-card.md +21 -0
  88. package/docs/ja/ui/kbd.md +16 -0
  89. package/docs/ja/ui/pagination.md +36 -0
  90. package/docs/ja/ui/radio-group.md +21 -0
  91. package/docs/ja/ui/toast.md +46 -0
  92. package/docs/ja/ui-and-theming.md +14 -7
  93. package/package.json +25 -13
  94. package/registry/ui/accordion.tsx +74 -0
  95. package/registry/ui/alert-dialog.tsx +189 -0
  96. package/registry/ui/alert.tsx +78 -0
  97. package/registry/ui/app-shell.tsx +281 -0
  98. package/registry/ui/area-chart.tsx +1015 -0
  99. package/registry/ui/avatar.tsx +110 -0
  100. package/registry/ui/badge.tsx +54 -0
  101. package/registry/ui/bar-chart.tsx +1013 -0
  102. package/registry/ui/bar-list.tsx +302 -0
  103. package/registry/ui/breadcrumb.tsx +126 -0
  104. package/registry/ui/button.tsx +60 -0
  105. package/registry/ui/calendar.tsx +221 -0
  106. package/registry/ui/card.tsx +105 -0
  107. package/registry/ui/category-bar.tsx +281 -0
  108. package/registry/ui/checkbox.tsx +28 -0
  109. package/registry/ui/collapsible.tsx +21 -0
  110. package/registry/ui/combobox.tsx +310 -0
  111. package/registry/ui/command.tsx +196 -0
  112. package/registry/ui/dashboard-card.tsx +174 -0
  113. package/registry/ui/data-table.tsx +16 -91
  114. package/registry/ui/date-range-picker.tsx +209 -0
  115. package/registry/ui/dialog.tsx +159 -0
  116. package/registry/ui/donut-chart.tsx +1004 -0
  117. package/registry/ui/drawer.tsx +229 -0
  118. package/registry/ui/dropdown-menu.tsx +272 -0
  119. package/registry/ui/echart.tsx +486 -0
  120. package/registry/ui/field.tsx +240 -0
  121. package/registry/ui/filter-bar.tsx +365 -0
  122. package/registry/ui/funnel-steps.tsx +274 -0
  123. package/registry/ui/hover-card.tsx +52 -0
  124. package/registry/ui/input-group.tsx +158 -0
  125. package/registry/ui/input.tsx +22 -0
  126. package/registry/ui/kbd.tsx +28 -0
  127. package/registry/ui/label.tsx +20 -0
  128. package/registry/ui/line-chart.tsx +1007 -0
  129. package/registry/ui/metric-value.tsx +54 -0
  130. package/registry/ui/multi-select.tsx +226 -0
  131. package/registry/ui/page-shell.tsx +227 -0
  132. package/registry/ui/pagination.tsx +138 -0
  133. package/registry/ui/placeholder.tsx +30 -0
  134. package/registry/ui/popover.tsx +90 -0
  135. package/registry/ui/progress-circle.tsx +272 -0
  136. package/registry/ui/progress.tsx +83 -0
  137. package/registry/ui/radio-group.tsx +39 -0
  138. package/registry/ui/scroll-area.tsx +55 -0
  139. package/registry/ui/searchable-select.tsx +209 -0
  140. package/registry/ui/section-header.tsx +41 -0
  141. package/registry/ui/segmented-control.tsx +71 -0
  142. package/registry/ui/select.tsx +253 -0
  143. package/registry/ui/separator.tsx +25 -0
  144. package/registry/ui/sheet.tsx +137 -0
  145. package/registry/ui/sidebar.tsx +723 -0
  146. package/registry/ui/skeleton.tsx +15 -0
  147. package/registry/ui/slider.tsx +54 -0
  148. package/registry/ui/sparkline.tsx +155 -0
  149. package/registry/ui/spinner.tsx +12 -0
  150. package/registry/ui/states.tsx +61 -0
  151. package/registry/ui/status-badge.tsx +79 -0
  152. package/registry/ui/switch.tsx +32 -0
  153. package/registry/ui/table.tsx +116 -0
  154. package/registry/ui/tabs.tsx +82 -0
  155. package/registry/ui/textarea.tsx +20 -0
  156. package/registry/ui/toast.tsx +250 -0
  157. package/registry/ui/toggle-group.tsx +89 -0
  158. package/registry/ui/toggle.tsx +45 -0
  159. package/registry/ui/tooltip.tsx +66 -0
  160. package/registry/ui/tracker.tsx +236 -0
  161. package/registry/ui/trend-indicator.tsx +62 -0
  162. package/skills/vantage-add-feature/SKILL.md +9 -7
  163. package/skills/vantage-design/SKILL.md +3 -1
  164. package/skills/vantage-design/references/format.md +1 -1
  165. package/skills/vantage-pitfalls/SKILL.md +5 -1
  166. package/skills/vantage-slide/SKILL.md +17 -0
  167. package/templates/AGENTS.md +10 -1
  168. package/templates/agents/api.md +29 -0
  169. package/templates/agents/csv.md +2 -1
  170. package/templates/agents/doc.md +4 -3
  171. package/templates/agents/page.md +22 -8
  172. package/templates/agents/slide.md +13 -1
  173. package/templates/agents/xlsx.md +2 -1
  174. package/theme.css +7 -1
  175. package/dist/add-DY4AXANK.js +0 -6
  176. package/dist/add-DY4AXANK.js.map +0 -1
  177. package/dist/chunk-BCE22JSM.js.map +0 -1
  178. package/dist/chunk-DTDVSFRY.js.map +0 -1
  179. package/dist/chunk-EP6JHXSR.js.map +0 -1
  180. package/dist/chunk-KLEIVDUV.js.map +0 -1
  181. package/dist/chunk-MP3QSOXL.js.map +0 -1
  182. package/dist/chunk-PVTHHV7X.js.map +0 -1
  183. package/dist/chunk-QI3XQKHS.js.map +0 -1
  184. package/dist/chunk-T3FLAGI6.js +0 -316
  185. package/dist/chunk-T3FLAGI6.js.map +0 -1
  186. package/dist/dev-FCEC3UKQ.js.map +0 -1
@@ -0,0 +1,486 @@
1
+ // Copied by `vantage add ui echart`. This file is yours to edit.
2
+ // Generated from @squadbase/vantage src/components/echart.tsx by `npm run registry` (in Vantage, edit src/, not registry/).
3
+ /**
4
+ * EChart — a thin Apache ECharts wrapper. It owns the canvas lifecycle (init,
5
+ * resize, dispose), forwards `option` and event handlers, drives the loading
6
+ * spinner, and can copy/download the chart as a PNG.
7
+ *
8
+ * Colours follow the design tokens: `--chart-1..5` become the series palette,
9
+ * and the text/border/popover tokens style axes, legend and tooltip. They're
10
+ * resolved once at init (see `chart-theme.ts`) and re-read whenever they can
11
+ * have changed — a light/dark flip, or a stylesheet swap such as editing
12
+ * `styles.css` while the dev server is running. `theme` opts out entirely;
13
+ * `option.color` overrides just the palette and keeps the rest.
14
+ */
15
+ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react"
16
+ import * as echarts from "echarts"
17
+ import type { ECharts, EChartsOption } from "echarts"
18
+ import { Check, Copy, Download } from "lucide-react"
19
+
20
+ import { cn } from "@squadbase/vantage/ui"
21
+ import { Button } from "@squadbase/vantage/ui"
22
+
23
+ /* ---------- inlined from src/components/chart-theme.ts (internal to @squadbase/vantage) ---------- */
24
+
25
+ /** Series palette, in order. Missing tokens are skipped, not defaulted. */
26
+ const SERIES_TOKENS = ["--chart-1", "--chart-2", "--chart-3", "--chart-4", "--chart-5"] as const
27
+
28
+ interface ChartTokens {
29
+ /** Resolved `--chart-1..5`, in order. */
30
+ series: string[]
31
+ /** Body text: titles and the default `textStyle`. */
32
+ foreground: string | null
33
+ /** Secondary text: axis labels, legend, subtitles. */
34
+ mutedForeground: string | null
35
+ /** Axis lines, ticks, split lines, tooltip border. */
36
+ border: string | null
37
+ /** Tooltip surface. */
38
+ popover: string | null
39
+ /** Tooltip text. */
40
+ popoverForeground: string | null
41
+ }
42
+
43
+ let normalizeContext: CanvasRenderingContext2D | null | undefined
44
+
45
+ /** The forms `fillStyle` serializes legacy colours to, and zrender can parse. */
46
+ const ZRENDER_COLOR_RE = /^(?:#[0-9a-f]{6}|rgba?\()/i
47
+
48
+ /**
49
+ * Serialize a CSS colour into a canvas-safe form, or `null` if the browser
50
+ * rejects it. Assigning an invalid value to `fillStyle` is a no-op, so the same
51
+ * input is written over two different known colours: agreeing results mean the
52
+ * value parsed, differing ones mean it was dropped.
53
+ */
54
+ function toCanvasColor(value: string): string | null {
55
+ const raw = value.trim()
56
+ if (!raw) return null
57
+ if (normalizeContext === undefined) {
58
+ const canvas = document.createElement("canvas")
59
+ canvas.width = 1
60
+ canvas.height = 1
61
+ normalizeContext = canvas.getContext("2d", { willReadFrequently: true })
62
+ }
63
+ const ctx = normalizeContext
64
+ if (!ctx) return raw
65
+ ctx.fillStyle = "#000000"
66
+ ctx.fillStyle = raw
67
+ const onBlack = ctx.fillStyle
68
+ ctx.fillStyle = "#ffffff"
69
+ ctx.fillStyle = raw
70
+ if (onBlack !== ctx.fillStyle || typeof onBlack !== "string") return null
71
+ if (ZRENDER_COLOR_RE.test(onBlack)) return onBlack
72
+ // Still `oklch()` / `lab()` / `color(...)`: paint it and read the pixel back.
73
+ // Wide-gamut colours clip to sRGB, which is all zrender can express anyway.
74
+ ctx.clearRect(0, 0, 1, 1)
75
+ ctx.fillRect(0, 0, 1, 1)
76
+ const [r = 0, g = 0, b = 0, a = 0] = ctx.getImageData(0, 0, 1, 1).data
77
+ return a === 255
78
+ ? `rgb(${r}, ${g}, ${b})`
79
+ : `rgba(${r}, ${g}, ${b}, ${Math.round((a / 255) * 1000) / 1000})`
80
+ }
81
+
82
+ /**
83
+ * Read the chart tokens off `el` (the document root by default — tokens live on
84
+ * `:root` and inherit). Returns `null` outside the browser so callers fall back
85
+ * to plain ECharts defaults.
86
+ */
87
+ function readChartTokens(el?: Element | null): ChartTokens | null {
88
+ if (typeof document === "undefined") return null
89
+ const target = el ?? document.documentElement
90
+ if (!target) return null
91
+ const styles = getComputedStyle(target)
92
+ const read = (name: string) => toCanvasColor(styles.getPropertyValue(name))
93
+ const series: string[] = []
94
+ for (const token of SERIES_TOKENS) {
95
+ const color = read(token)
96
+ if (color) series.push(color)
97
+ }
98
+ return {
99
+ series,
100
+ foreground: read("--foreground"),
101
+ mutedForeground: read("--muted-foreground"),
102
+ border: read("--border"),
103
+ popover: read("--popover"),
104
+ popoverForeground: read("--popover-foreground"),
105
+ }
106
+ }
107
+
108
+ /** Structural equality, so a theme toggle that changes nothing doesn't re-init. */
109
+ function sameChartTokens(a: ChartTokens | null, b: ChartTokens | null): boolean {
110
+ if (a === b) return true
111
+ if (!a || !b) return false
112
+ return (
113
+ a.series.length === b.series.length &&
114
+ a.series.every((color, i) => color === b.series[i]) &&
115
+ a.foreground === b.foreground &&
116
+ a.mutedForeground === b.mutedForeground &&
117
+ a.border === b.border &&
118
+ a.popover === b.popover &&
119
+ a.popoverForeground === b.popoverForeground
120
+ )
121
+ }
122
+
123
+ /**
124
+ * Watch everything that can change the resolved tokens and call `onChange` when
125
+ * something might have. It only ever says "re-read" — the caller compares with
126
+ * `sameChartTokens` and decides whether anything actually moved. Returns a
127
+ * disposer.
128
+ *
129
+ * Two sources, not one:
130
+ * - light/dark flips, which land as `class` / `data-theme` (or an inline
131
+ * `style`) on `<html>`/`<body>`, or as an OS preference change
132
+ * - stylesheet changes. In dev, editing `styles.css` rewrites the injected
133
+ * `<style>` in `<head>` and touches nothing else, so watching the theme
134
+ * attributes alone leaves the chart painted in the previous palette until a
135
+ * reload — the tokens in the DOM are new, the canvas is not.
136
+ */
137
+ function watchChartTokens(onChange: () => void): () => void {
138
+ if (typeof document === "undefined") return () => {}
139
+ // Style edits arrive as bursts of mutations; coalesce them into one read, and
140
+ // take it after the next frame so a swapped stylesheet has been applied.
141
+ let frame = 0
142
+ const schedule = () => {
143
+ if (frame) return
144
+ frame = requestAnimationFrame(() => {
145
+ frame = 0
146
+ onChange()
147
+ })
148
+ }
149
+
150
+ const themeObserver = new MutationObserver(schedule)
151
+ const attributeFilter = ["class", "style", "data-theme"]
152
+ themeObserver.observe(document.documentElement, { attributes: true, attributeFilter })
153
+ if (document.body) themeObserver.observe(document.body, { attributes: true, attributeFilter })
154
+
155
+ const styleObserver = new MutationObserver(schedule)
156
+ if (document.head) {
157
+ styleObserver.observe(document.head, {
158
+ childList: true,
159
+ subtree: true,
160
+ characterData: true,
161
+ attributes: true,
162
+ attributeFilter: ["href", "media", "disabled"],
163
+ })
164
+ }
165
+
166
+ // A swapped `<link>` only applies once the sheet has loaded, after its mutation.
167
+ const onLoad = (event: Event) => {
168
+ if (event.target instanceof HTMLLinkElement) schedule()
169
+ }
170
+ document.addEventListener("load", onLoad, true)
171
+
172
+ const media = window.matchMedia("(prefers-color-scheme: dark)")
173
+ media.addEventListener("change", schedule)
174
+
175
+ return () => {
176
+ if (frame) cancelAnimationFrame(frame)
177
+ themeObserver.disconnect()
178
+ styleObserver.disconnect()
179
+ document.removeEventListener("load", onLoad, true)
180
+ media.removeEventListener("change", schedule)
181
+ }
182
+ }
183
+
184
+ /** Shared shape for the four axis types ECharts themes address separately. */
185
+ function axisTheme(tokens: ChartTokens, splitLine: boolean) {
186
+ const line = tokens.border ?? undefined
187
+ return {
188
+ axisLine: { show: true, lineStyle: { color: line } },
189
+ axisTick: { show: true, lineStyle: { color: line } },
190
+ axisLabel: { show: true, color: tokens.mutedForeground ?? undefined },
191
+ splitLine: { show: splitLine, lineStyle: { color: line ? [line] : undefined } },
192
+ splitArea: { show: false },
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Build the ECharts theme. `option.color` still wins over `theme.color`, so an
198
+ * app that sets its own palette keeps it.
199
+ */
200
+ function buildChartTheme(tokens: ChartTokens): Record<string, unknown> {
201
+ const text = tokens.foreground ?? undefined
202
+ const muted = tokens.mutedForeground ?? undefined
203
+ const line = tokens.border ?? undefined
204
+ return {
205
+ // Let the page background show through: charts sit on cards, not on a canvas.
206
+ backgroundColor: "transparent",
207
+ ...(tokens.series.length > 0 ? { color: tokens.series } : {}),
208
+ textStyle: { color: text },
209
+ title: { textStyle: { color: text }, subtextStyle: { color: muted } },
210
+ legend: { textStyle: { color: muted } },
211
+ categoryAxis: axisTheme(tokens, false),
212
+ valueAxis: axisTheme(tokens, true),
213
+ logAxis: axisTheme(tokens, true),
214
+ timeAxis: axisTheme(tokens, true),
215
+ tooltip: {
216
+ backgroundColor: tokens.popover ?? undefined,
217
+ borderColor: line,
218
+ borderWidth: 1,
219
+ textStyle: { color: tokens.popoverForeground ?? undefined },
220
+ axisPointer: { lineStyle: { color: line }, crossStyle: { color: line } },
221
+ },
222
+ }
223
+ }
224
+
225
+ /**
226
+ * The resolved tokens, kept current: re-read whenever `watchChartTokens` says
227
+ * they may have moved, and only replaced when a value actually changed. For
228
+ * components that build an ECharts option from token colours themselves (the
229
+ * typed charts), so their palette follows light/dark like `EChart`'s theme.
230
+ */
231
+ function useChartTokens(): ChartTokens | null {
232
+ const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
233
+ useEffect(() => {
234
+ const sync = () => {
235
+ setTokens((prev) => {
236
+ const next = readChartTokens()
237
+ return sameChartTokens(prev, next) ? prev : next
238
+ })
239
+ }
240
+ sync()
241
+ return watchChartTokens(sync)
242
+ }, [])
243
+ return tokens
244
+ }
245
+
246
+ const SERIES_TOKEN_NAME_RE = /^chart-([1-5])$/
247
+ const CSS_VAR_RE = /^var\(\s*(--[\w-]+)\s*(?:,\s*(.+))?\)$/
248
+
249
+ /**
250
+ * Turn chart colours into canvas-safe strings. `"chart-1"`〜`"chart-5"` come from
251
+ * the resolved series tokens; `var(--token)` is read off the document root; any
252
+ * other CSS colour is normalized like the tokens are. A colour that can't be
253
+ * resolved comes back as `""`, which ECharts treats as "use the palette".
254
+ */
255
+ function resolveChartColors(colors: readonly string[], tokens: ChartTokens | null): string[] {
256
+ return colors.map((color) => {
257
+ const token = SERIES_TOKEN_NAME_RE.exec(color.trim())
258
+ if (token) return tokens?.series[Number(token[1]) - 1] ?? ""
259
+ if (typeof document === "undefined") return color
260
+ let value = color.trim()
261
+ const cssVar = CSS_VAR_RE.exec(value)
262
+ if (cssVar) {
263
+ const styles = getComputedStyle(document.documentElement)
264
+ value = styles.getPropertyValue(cssVar[1]!).trim() || (cssVar[2] ?? "").trim()
265
+ }
266
+ return toCanvasColor(value) ?? ""
267
+ })
268
+ }
269
+
270
+ /* ---------- end of inlined code ---------- */
271
+
272
+ export interface EChartProps {
273
+ /**
274
+ * ECharts のオプション。
275
+ *
276
+ * 型推論を効かせるため、必ず `EChartsOption` を annotation するか
277
+ * `satisfies EChartsOption` を付けること。付けないと `type: "category"` などの
278
+ * literal が `string` に拡張され、ビルドが失敗する。
279
+ *
280
+ * @example
281
+ * import type { EChartsOption } from "@squadbase/vantage/components"
282
+ *
283
+ * const option: EChartsOption = {
284
+ * xAxis: { type: "category", data: ["A", "B"] },
285
+ * yAxis: { type: "value" },
286
+ * series: [{ type: "bar", data: [1, 2] }],
287
+ * }
288
+ * <EChart option={option} />
289
+ */
290
+ option: EChartsOption
291
+ /**
292
+ * チャートの高さ。数値は px、文字列は CSS 値(例: `"50vh"`)として扱う。
293
+ *
294
+ * 省略した場合は外側ラッパーに `h-[400px]` がデフォルトで当たるが、`className` に
295
+ * `h-full` や `h-[360px]` などの高さクラスを渡せばそちらが優先される
296
+ * (`h-full` を使う場合は親要素に確定した高さが必要)。
297
+ * `height` と `className` の高さを両方指定した場合は `height`(インライン style)が勝つ。
298
+ */
299
+ height?: string | number
300
+ loading?: boolean
301
+ /**
302
+ * `echarts.init` にそのまま渡すテーマ(名前または登録済みテーマオブジェクト)。
303
+ *
304
+ * 渡すとデザイントークン追従は**完全に無効**になり、明暗の切り替えにも追従しなく
305
+ * なる。系列色だけ変えたいなら `option.color` を使う ― option はテーマより優先
306
+ * されるので、軸・凡例・ツールチップのトークン追従は残る。
307
+ */
308
+ theme?: string | object
309
+ onEvents?: Record<string, (params: unknown) => void>
310
+ ariaLabel?: string
311
+ className?: string
312
+ /** true にするとチャート右上に PNG コピー / ダウンロードボタンを表示する */
313
+ actions?: boolean
314
+ /** ダウンロード時のファイル名(拡張子含む)。省略時は "chart.png" */
315
+ fileName?: string
316
+ }
317
+
318
+ type EChartsEventHandler = (params: unknown) => void
319
+
320
+ export const EChart = forwardRef<HTMLDivElement, EChartProps>(function EChart(
321
+ {
322
+ option,
323
+ height,
324
+ loading = false,
325
+ theme,
326
+ onEvents,
327
+ ariaLabel,
328
+ className,
329
+ actions = false,
330
+ fileName = "chart.png",
331
+ },
332
+ ref,
333
+ ) {
334
+ const containerRef = useRef<HTMLDivElement | null>(null)
335
+ const [instance, setInstance] = useState<ECharts | null>(null)
336
+ const [copied, setCopied] = useState(false)
337
+ // 初回はレンダー前に読めるので、マウント直後の作り直しが起きない
338
+ const [tokens, setTokens] = useState<ChartTokens | null>(() => readChartTokens())
339
+
340
+ // 明暗の切り替えとスタイルシートの差し替え(dev で styles.css を書き換えたときなど)を
341
+ // 拾ってトークンを読み直す ― 何を見張るかは `watchChartTokens` 側が持つ。値が変わら
342
+ // ないときは同じオブジェクトを返して再 init を防ぐ。
343
+ useEffect(() => {
344
+ if (theme !== undefined) return
345
+ const sync = () => {
346
+ setTokens((prev) => {
347
+ const next = readChartTokens()
348
+ return sameChartTokens(prev, next) ? prev : next
349
+ })
350
+ }
351
+ sync()
352
+ return watchChartTokens(sync)
353
+ }, [theme])
354
+
355
+ const autoTheme = useMemo(() => (tokens ? buildChartTheme(tokens) : undefined), [tokens])
356
+ const resolvedTheme = theme ?? autoTheme
357
+
358
+ // init / re-init: ECharts はテーマの差し替えに対応しないので、テーマが変わったら
359
+ // インスタンスを破棄して作り直す(トークンが実際に変わったときだけここに来る)
360
+ useEffect(() => {
361
+ const container = containerRef.current
362
+ if (!container) return
363
+ const inst = echarts.init(container, resolvedTheme, { renderer: "canvas" })
364
+ // `data-rendering` stays on until the (animated) paint is done, so anything
365
+ // capturing the page — printing a document — can wait for real pixels.
366
+ container.dataset.rendering = ""
367
+ inst.on("finished", () => {
368
+ delete container.dataset.rendering
369
+ })
370
+ setInstance(inst)
371
+
372
+ const observer = new ResizeObserver(() => inst.resize())
373
+ observer.observe(container)
374
+
375
+ return () => {
376
+ observer.disconnect()
377
+ inst.dispose()
378
+ }
379
+ }, [resolvedTheme])
380
+
381
+ useEffect(() => {
382
+ if (!instance) return
383
+ if (containerRef.current) containerRef.current.dataset.rendering = ""
384
+ instance.setOption(option, { notMerge: true, lazyUpdate: true })
385
+ }, [instance, option])
386
+
387
+ useEffect(() => {
388
+ if (!instance) return
389
+ if (loading) instance.showLoading()
390
+ else instance.hideLoading()
391
+ }, [instance, loading])
392
+
393
+ useEffect(() => {
394
+ if (!instance || !onEvents) return
395
+ const entries = Object.entries(onEvents)
396
+ for (const [event, handler] of entries) {
397
+ instance.on(event, handler as EChartsEventHandler)
398
+ }
399
+ return () => {
400
+ if (instance.isDisposed()) return
401
+ for (const [event, handler] of entries) {
402
+ instance.off(event, handler as EChartsEventHandler)
403
+ }
404
+ }
405
+ }, [instance, onEvents])
406
+
407
+ const getPngDataUrl = useCallback(() => {
408
+ if (!instance) return null
409
+ // Export over the page background so the PNG isn't transparent. Computed
410
+ // styles resolve to rgb(a), so this stays canvas-safe whatever the token.
411
+ const pageBg = getComputedStyle(document.documentElement).backgroundColor
412
+ return instance.getDataURL({
413
+ type: "png",
414
+ pixelRatio: 2,
415
+ backgroundColor: pageBg && pageBg !== "rgba(0, 0, 0, 0)" ? pageBg : "#ffffff",
416
+ })
417
+ }, [instance])
418
+
419
+ const handleCopy = useCallback(async () => {
420
+ const dataUrl = getPngDataUrl()
421
+ if (!dataUrl) return
422
+ try {
423
+ const blob = await (await fetch(dataUrl)).blob()
424
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })])
425
+ setCopied(true)
426
+ setTimeout(() => setCopied(false), 2000)
427
+ } catch {
428
+ // ClipboardItem 未対応ブラウザでは silent fail
429
+ }
430
+ }, [getPngDataUrl])
431
+
432
+ const handleDownload = useCallback(() => {
433
+ const dataUrl = getPngDataUrl()
434
+ if (!dataUrl) return
435
+ const link = document.createElement("a")
436
+ link.href = dataUrl
437
+ link.download = fileName
438
+ link.click()
439
+ }, [getPngDataUrl, fileName])
440
+
441
+ // 高さの解決: height prop を渡した場合はインライン style が勝つ。未指定なら
442
+ // h-[400px] をデフォルトで当て、className の高さクラスに後勝ちさせる。
443
+ const styleHeight =
444
+ height == null ? undefined : typeof height === "number" ? `${height}px` : height
445
+
446
+ return (
447
+ <div
448
+ ref={ref}
449
+ data-slot="echart"
450
+ className={cn("group relative w-full", height == null && "h-[400px]", className)}
451
+ style={styleHeight != null ? { height: styleHeight } : undefined}
452
+ role="img"
453
+ aria-label={ariaLabel ?? "チャート"}
454
+ >
455
+ <div ref={containerRef} data-slot="echart-canvas" className="h-full w-full" />
456
+ {actions && (
457
+ <div
458
+ data-slot="echart-actions"
459
+ className="absolute top-2 right-2 z-10 flex gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100"
460
+ >
461
+ <Button
462
+ type="button"
463
+ variant="ghost"
464
+ size="icon-sm"
465
+ onClick={handleCopy}
466
+ aria-label={copied ? "コピーしました" : "PNG をクリップボードにコピー"}
467
+ >
468
+ {copied ? <Check className="size-4" /> : <Copy className="size-4" />}
469
+ </Button>
470
+ <Button
471
+ type="button"
472
+ variant="ghost"
473
+ size="icon-sm"
474
+ onClick={handleDownload}
475
+ aria-label="PNG をダウンロード"
476
+ >
477
+ <Download className="size-4" />
478
+ </Button>
479
+ </div>
480
+ )}
481
+ </div>
482
+ )
483
+ })
484
+ EChart.displayName = "EChart"
485
+
486
+ export type { EChartsOption } from "echarts"