@pixerate/editor-react 0.36.0 → 0.38.0

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.
@@ -1,733 +1,79 @@
1
- // src/chart/useChartEditor.ts
2
- import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
1
  import {
4
- ChartController,
5
- ChartDirector,
6
- ChartPresence,
7
- createChartCommands,
8
- createChartToolProvider,
9
- layoutChart
10
- } from "@pixerate/editor/chart";
11
- function useChartController(options = {}) {
12
- const ref = useRef(null);
13
- if (!ref.current) ref.current = new ChartController(options);
14
- return ref.current;
15
- }
16
- function useChartState(controller) {
17
- return useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
18
- }
19
- function useChartEditor(controllerOrOptions = {}) {
20
- const own = useRef(null);
21
- let controller;
22
- if (controllerOrOptions instanceof ChartController) controller = controllerOrOptions;
23
- else {
24
- if (!own.current) own.current = new ChartController(controllerOrOptions);
25
- controller = own.current;
26
- }
27
- const state = useChartState(controller);
28
- const actions = useMemo(
29
- () => ({
30
- update: (patch) => controller.update(patch),
31
- setType: (type) => controller.setType(type),
32
- setValue: (seriesId, index, value) => controller.setValue(seriesId, index, value),
33
- select: (selection) => controller.select(selection),
34
- undo: () => void controller.undo(),
35
- redo: () => void controller.redo()
36
- }),
37
- [controller]
38
- );
39
- return { controller, state, spec: state.spec, selection: state.selection, ...actions };
40
- }
41
- function useChartCommands(registry, controller, options = {}) {
42
- const { prefix, group, scope } = options;
43
- useEffect(() => {
44
- if (!registry) return;
45
- return registry.register(createChartCommands(controller, { prefix, group, scope }));
46
- }, [registry, controller, prefix, group, scope]);
47
- }
48
- function useChartToolProvider(registry, controller, options = {}) {
49
- const latest = useRef(options);
50
- latest.current = options;
51
- const { id, namespace } = options;
52
- useEffect(() => {
53
- if (!registry) return;
54
- return registry.register(createChartToolProvider(controller, { ...latest.current, id, namespace }));
55
- }, [registry, controller, id, namespace]);
56
- }
57
- function useChartDirector(controller, options = {}) {
58
- const sceneRef = useRef(null);
59
- const ref = useRef(null);
60
- if (!ref.current) {
61
- const presence = new ChartPresence();
62
- const size = options.size ?? { width: 640, height: 400 };
63
- const director = new ChartDirector(controller, presence, { scene: () => sceneRef.current ?? layoutChart(controller.getSpec(), size), speed: options.speed });
64
- ref.current = { presence, director };
65
- }
66
- return { ...ref.current, sceneRef };
67
- }
68
-
69
- // src/chart/styles.ts
70
- var chartInputClass = "h-7 w-full min-w-0 rounded-md border border-input bg-background px-2 text-xs text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:cursor-not-allowed disabled:opacity-50";
71
- var chartButtonClass = "inline-flex h-7 items-center justify-center gap-1 rounded-md border border-input bg-background px-2 text-xs text-foreground hover:bg-muted focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50";
72
- var chartIconButtonClass = "inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-xs text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-40";
73
- var chartSegmentClass = (active) => `inline-flex h-7 flex-1 items-center justify-center rounded px-2 text-xs focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary ${active ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`;
74
- var chartLabelClass = "text-[11px] font-medium text-muted-foreground";
75
- var chartSectionClass = "space-y-2 border-b border-border pb-3 last:border-b-0";
76
- var chartCellClass = "h-7 w-full min-w-[4.5rem] bg-transparent px-2 text-right text-xs tabular-nums text-foreground focus-visible:bg-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-primary";
2
+ ChartCanvas,
3
+ ChartCommitInput,
4
+ ChartDataGrid,
5
+ ChartInspector,
6
+ ChartPresenceLayer,
7
+ ChartSceneSvg,
8
+ chartButtonClass,
9
+ chartCellClass,
10
+ chartIconButtonClass,
11
+ chartInputClass,
12
+ chartLabelClass,
13
+ chartSectionClass,
14
+ chartSegmentClass,
15
+ renderChartMark,
16
+ useChartCommands,
17
+ useChartController,
18
+ useChartDirector,
19
+ useChartEditor,
20
+ useChartState,
21
+ useChartToolProvider
22
+ } from "../chunk-IH5ZIOK3.js";
77
23
 
78
- // src/chart/ChartCanvas.tsx
79
- import { useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState, useSyncExternalStore as useSyncExternalStore2 } from "react";
80
- import {
81
- CSS_VARIABLE_CHART_THEME,
82
- chartClientToScene,
83
- chartTextBaseline,
84
- chartTextTransform,
85
- describeChartSelection,
86
- handleChartKey,
87
- layoutChart as layoutChart2,
88
- startChartPointDrag
89
- } from "@pixerate/editor/chart";
24
+ // src/chart/ChartEditor.tsx
25
+ import { useState } from "react";
26
+ import { describeChartSelection, downloadChartPng } from "@pixerate/editor/chart";
90
27
  import { jsx, jsxs } from "react/jsx-runtime";
91
- function renderChartMark(mark, fontFamily, interactive) {
92
- const common = {
93
- "data-role": mark.role,
94
- "data-chart-key": mark.target ? mark.key : void 0,
95
- "data-selected": mark.selected ? "" : void 0,
96
- opacity: mark.opacity,
97
- pointerEvents: mark.role === "selection" || mark.role === "drag-readout" || !mark.target && mark.role !== "background" ? "none" : void 0,
98
- cursor: interactive && mark.target && mark.role !== "background" ? "pointer" : void 0
99
- };
100
- switch (mark.kind) {
101
- case "rect":
102
- return /* @__PURE__ */ jsx("rect", { ...common, x: mark.x, y: mark.y, width: mark.width, height: mark.height, rx: mark.radius, style: { fill: mark.fill, stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
103
- case "path":
104
- return /* @__PURE__ */ jsx(
105
- "path",
106
- {
107
- ...common,
108
- d: mark.d,
109
- strokeLinejoin: "round",
110
- strokeLinecap: "round",
111
- strokeDasharray: mark.dash,
112
- style: { fill: mark.fill ?? "none", stroke: mark.stroke, strokeWidth: mark.strokeWidth }
113
- },
114
- mark.key
115
- );
116
- case "circle":
117
- return /* @__PURE__ */ jsx("circle", { ...common, cx: mark.cx, cy: mark.cy, r: mark.r, style: { fill: mark.fill, stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
118
- case "line":
119
- return /* @__PURE__ */ jsx("line", { ...common, x1: mark.x1, y1: mark.y1, x2: mark.x2, y2: mark.y2, strokeDasharray: mark.dash, style: { stroke: mark.stroke, strokeWidth: mark.strokeWidth } }, mark.key);
120
- case "text":
121
- return /* @__PURE__ */ jsx(
122
- "text",
123
- {
124
- ...common,
125
- x: mark.x,
126
- y: mark.y,
127
- textAnchor: mark.anchor,
128
- dominantBaseline: chartTextBaseline(mark.baseline),
129
- transform: chartTextTransform(mark),
130
- style: { fill: mark.fill, fontSize: mark.size, fontWeight: mark.weight, fontFamily, userSelect: "none" },
131
- children: mark.text
132
- },
133
- mark.key
134
- );
135
- }
136
- }
137
- function ChartPresenceLayer({ presence }) {
138
- const { actors } = useSyncExternalStore2(presence.subscribe, presence.getState, presence.getState);
139
- return /* @__PURE__ */ jsx("g", { "data-chart-presence": "", pointerEvents: "none", children: actors.map((a) => /* @__PURE__ */ jsxs("g", { transform: `translate(${a.x} ${a.y})`, "data-actor": a.id, children: [
140
- /* @__PURE__ */ jsx("path", { d: "M0,0 L0,14 L4,10.5 L7,17 L9.5,16 L6.5,9.5 L11.5,9.5 Z", style: { fill: a.color, stroke: "#ffffff", strokeWidth: 1 }, transform: a.pressed ? "scale(0.9)" : void 0 }),
141
- /* @__PURE__ */ jsxs("g", { transform: "translate(13 16)", children: [
142
- /* @__PURE__ */ jsx("rect", { x: 0, y: 0, rx: 4, height: 18, width: Math.max(28, (a.message ?? a.name).length * 6.4 + 12), style: { fill: a.color } }),
143
- /* @__PURE__ */ jsx("text", { x: 6, y: 9, dominantBaseline: "central", style: { fill: "#ffffff", fontSize: 11, fontWeight: 600, fontFamily: "inherit" }, children: a.message ?? a.name })
144
- ] })
145
- ] }, a.id)) });
146
- }
147
- function ChartSceneSvg({ scene, label, className }) {
148
- return /* @__PURE__ */ jsx("svg", { width: scene.width, height: scene.height, viewBox: `0 0 ${scene.width} ${scene.height}`, role: "img", "aria-label": label, className: `block max-w-full ${className ?? ""}`, children: scene.marks.map((m) => renderChartMark(m, scene.theme.fontFamily, false)) });
149
- }
150
- function ChartCanvas({ controller, width = 640, height = 400, responsive = true, theme, editable = true, measureText, emptyMessage, className, presence, sceneRef }) {
28
+ function ChartEditor({ controller, showInspector = true, showDataGrid = true, showToolbar = true, pngOptions, height = 380, canvasProps, className }) {
151
29
  const state = useChartState(controller);
152
- const wrapper = useRef2(null);
153
- const svg = useRef2(null);
154
- const drag = useRef2(null);
155
- const [measured, setMeasured] = useState(null);
156
- const [frozen, setFrozen] = useState(null);
157
- useEffect2(() => {
158
- const el = wrapper.current;
159
- if (!responsive || !el || typeof ResizeObserver === "undefined") return;
160
- const observer = new ResizeObserver(([entry]) => {
161
- const w = Math.round(entry.contentRect.width);
162
- if (w > 0) setMeasured(w);
163
- });
164
- observer.observe(el);
165
- return () => observer.disconnect();
166
- }, [responsive]);
167
- const sceneWidth = responsive && measured || width;
168
- const fullTheme = useMemo2(() => ({ ...CSS_VARIABLE_CHART_THEME, ...theme }), [theme]);
169
- const scene = useMemo2(
170
- () => layoutChart2(state.spec, {
171
- width: sceneWidth,
172
- height,
173
- theme: fullTheme,
174
- selection: editable ? state.selection : null,
175
- measureText,
176
- emptyMessage,
177
- valueDomain: frozen?.valueDomain,
178
- valueDomain2: frozen?.valueDomain2,
179
- xDomain: frozen?.xDomain,
180
- readout: frozen?.readout
181
- }),
182
- [state.spec, state.selection, sceneWidth, height, fullTheme, editable, measureText, emptyMessage, frozen]
183
- );
184
- if (sceneRef) sceneRef.current = scene;
185
- const byKey = useMemo2(() => new Map(scene.marks.map((m) => [m.key, m])), [scene]);
186
- useEffect2(() => () => drag.current?.end(), []);
187
- const scenePoint = (e) => chartClientToScene(scene, svg.current.getBoundingClientRect(), e.clientX, e.clientY);
188
- const onPointerDown = (e) => {
189
- if (!editable || e.button !== 0) return;
190
- const key = e.target.closest?.("[data-chart-key]")?.getAttribute("data-chart-key");
191
- const target = key ? byKey.get(key)?.target : void 0;
192
- controller.select(target ?? null);
193
- drag.current = startChartPointDrag(controller, scene, target, scenePoint(e));
194
- if (drag.current) {
195
- const d = drag.current;
196
- setFrozen({ valueDomain: d.valueDomain, valueDomain2: d.valueDomain2 ?? null, xDomain: d.xDomain ?? null, readout: d.target });
197
- svg.current?.setPointerCapture?.(e.pointerId);
198
- }
199
- };
200
- const onPointerMove = (e) => {
201
- if (drag.current) drag.current.move(scenePoint(e), { fine: e.altKey, lockAxis: e.shiftKey, noSnap: e.ctrlKey || e.metaKey });
202
- };
203
- const endDrag = () => {
204
- drag.current?.end();
205
- drag.current = null;
206
- setFrozen(null);
207
- };
208
- const onKeyDown = (e) => {
209
- if (drag.current && e.key === "Escape") {
210
- drag.current.cancel();
211
- drag.current = null;
212
- setFrozen(null);
213
- e.preventDefault();
214
- return;
215
- }
216
- if (editable && handleChartKey(controller, scene, e)) e.preventDefault();
217
- };
218
- const label = state.spec.title || "Chart";
219
- return /* @__PURE__ */ jsx("div", { ref: wrapper, className: `w-full ${className ?? ""}`, "data-chart-canvas": "", children: /* @__PURE__ */ jsxs(
220
- "svg",
221
- {
222
- ref: svg,
223
- width: sceneWidth,
224
- height,
225
- viewBox: `0 0 ${scene.width} ${scene.height}`,
226
- role: "img",
227
- "aria-label": editable ? `${label}. ${describeChartSelection(controller, state.selection)}` : label,
228
- tabIndex: editable ? 0 : void 0,
229
- className: "block max-w-full touch-none select-none rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
230
- onPointerDown,
231
- onPointerMove,
232
- onPointerUp: endDrag,
233
- onPointerCancel: endDrag,
234
- onKeyDown,
235
- children: [
236
- scene.marks.map((m) => renderChartMark(m, scene.theme.fontFamily, editable)),
237
- presence && /* @__PURE__ */ jsx(ChartPresenceLayer, { presence })
238
- ]
239
- }
240
- ) });
241
- }
242
-
243
- // src/chart/ChartDataGrid.tsx
244
- import { useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
245
- import { chartColorInputValue, chartSeriesColor, formatChartNumber } from "@pixerate/editor/chart";
246
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
247
- function ChartCommitInput({ value, onCommit, onKeyDown, onBlur, ...rest }) {
248
- const [draft, setDraft] = useState2(value);
249
- const typing = useRef3(false);
250
- useEffect3(() => {
251
- if (!typing.current) setDraft(value);
252
- }, [value]);
253
- const commit = () => {
254
- if (!typing.current) return;
255
- typing.current = false;
256
- if (draft === value) return;
257
- if (onCommit(draft) === false) setDraft(value);
258
- };
259
- return /* @__PURE__ */ jsx2(
260
- "input",
261
- {
262
- ...rest,
263
- value: draft,
264
- onChange: (e) => {
265
- typing.current = true;
266
- setDraft(e.currentTarget.value);
267
- },
268
- onBlur: (e) => {
269
- onBlur?.(e);
270
- commit();
271
- },
272
- onKeyDown: (e) => {
273
- onKeyDown?.(e);
274
- if (e.defaultPrevented) return;
275
- if (e.key === "Enter") {
276
- e.preventDefault();
277
- commit();
278
- } else if (e.key === "Escape") {
279
- typing.current = false;
280
- setDraft(value);
281
- }
282
- }
283
- }
284
- );
285
- }
286
- function ChartDataGrid({ controller, categoryHeader = "Category", readOnly = false, className }) {
287
- const { spec, selection } = useChartState(controller);
288
- const paste = (row, column) => (e) => {
289
- const text = e.clipboardData?.getData("text/plain") ?? "";
290
- if (!/[\t\n]/.test(text.replace(/\n$/, ""))) return;
291
- e.preventDefault();
292
- controller.pasteCells(row, column, text);
30
+ const [pngError, setPngError] = useState(null);
31
+ const downloadPng = () => {
32
+ setPngError(null);
33
+ downloadChartPng(controller.toJSON(), pngOptions).catch((error) => setPngError(error instanceof Error ? error.message : String(error)));
293
34
  };
294
- return /* @__PURE__ */ jsxs2("div", { className: `overflow-auto rounded-md border border-border ${className ?? ""}`, "data-chart-grid": "", children: [
295
- /* @__PURE__ */ jsxs2("table", { className: "w-full border-collapse text-xs", children: [
296
- /* @__PURE__ */ jsx2("thead", { className: "bg-muted/50", children: /* @__PURE__ */ jsxs2("tr", { children: [
297
- /* @__PURE__ */ jsx2("th", { className: "border-b border-r border-border px-2 py-1 text-left font-medium text-muted-foreground", children: categoryHeader }),
298
- spec.series.map((s, si) => {
299
- const color = chartSeriesColor(spec, si);
300
- const selected = selection?.kind === "series" && selection.seriesId === s.id;
301
- return /* @__PURE__ */ jsx2("th", { "data-series-id": s.id, className: `border-b border-r border-border px-1 py-1 font-medium ${selected ? "bg-primary/10" : ""}`, children: /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1", children: [
302
- /* @__PURE__ */ jsx2(
303
- "input",
304
- {
305
- type: "color",
306
- "aria-label": `${s.name} colour`,
307
- value: chartColorInputValue(color),
308
- disabled: readOnly,
309
- onPointerDown: () => controller.beginGesture(`color:${s.id}`),
310
- onChange: (e) => controller.updateSeries(s.id, { color: e.currentTarget.value }),
311
- onBlur: () => controller.endGesture(),
312
- className: "h-5 w-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
313
- }
314
- ),
315
- /* @__PURE__ */ jsx2(
316
- ChartCommitInput,
317
- {
318
- value: s.name,
319
- "aria-label": "Series name",
320
- onFocus: () => controller.select({ kind: "series", seriesId: s.id }),
321
- onCommit: (name) => void controller.updateSeries(s.id, { name }),
322
- className: `${chartInputClass} h-6 min-w-[5rem] border-transparent bg-transparent font-medium ${s.hidden ? "text-muted-foreground line-through" : ""}`
323
- }
324
- ),
325
- !readOnly && /* @__PURE__ */ jsxs2(Fragment, { children: [
326
- /* @__PURE__ */ jsx2(
327
- "button",
328
- {
329
- type: "button",
330
- className: chartIconButtonClass,
331
- title: s.hidden ? "Show series" : "Hide series",
332
- "aria-label": s.hidden ? `Show ${s.name}` : `Hide ${s.name}`,
333
- "aria-pressed": !s.hidden,
334
- "data-action": "toggle-series",
335
- onClick: () => controller.toggleSeries(s.id),
336
- children: s.hidden ? "\u25CB" : "\u25CF"
337
- }
338
- ),
339
- /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Delete series", "aria-label": `Delete ${s.name}`, "data-action": "remove-series", onClick: () => controller.removeSeries(s.id), children: "\xD7" })
340
- ] })
341
- ] }) }, s.id);
342
- }),
343
- !readOnly && /* @__PURE__ */ jsx2("th", { className: "w-8 border-b border-border px-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Add series", "aria-label": "Add series", "data-action": "add-series", onClick: () => controller.addSeries(), children: "+" }) })
344
- ] }) }),
345
- /* @__PURE__ */ jsx2("tbody", { children: spec.categories.map((category, i) => {
346
- const rowSelected = selection?.kind === "category" && selection.index === i;
347
- return /* @__PURE__ */ jsxs2("tr", { "data-category-index": i, className: rowSelected ? "bg-primary/10" : void 0, children: [
348
- /* @__PURE__ */ jsx2("th", { className: "border-b border-r border-border p-0 text-left font-normal", children: /* @__PURE__ */ jsx2(
349
- ChartCommitInput,
350
- {
351
- value: category,
352
- "aria-label": `Category ${i + 1}`,
353
- onFocus: () => controller.select({ kind: "category", index: i }),
354
- onPaste: paste(i, -1),
355
- onCommit: (label) => void controller.renameCategory(i, label),
356
- className: `${chartCellClass} text-left`
357
- }
358
- ) }),
359
- spec.series.map((s) => {
360
- const value = s.values[i];
361
- const selected = selection?.kind === "point" && selection.seriesId === s.id && selection.index === i;
362
- return /* @__PURE__ */ jsx2("td", { className: `border-b border-r border-border p-0 ${selected ? "bg-primary/10" : ""}`, children: /* @__PURE__ */ jsx2(
363
- ChartCommitInput,
364
- {
365
- value: value === null || value === void 0 ? "" : String(value),
366
- inputMode: "decimal",
367
- "aria-label": `${s.name}, ${category}`,
368
- title: value === null ? void 0 : formatChartNumber(value, spec.yAxis.format),
369
- "data-cell": `${s.id}:${i}`,
370
- onFocus: () => controller.select({ kind: "point", seriesId: s.id, index: i }),
371
- onPaste: paste(i, spec.series.indexOf(s)),
372
- onCommit: (text) => controller.setValue(s.id, i, text).ok,
373
- className: chartCellClass
374
- }
375
- ) }, s.id);
376
- }),
377
- !readOnly && /* @__PURE__ */ jsx2("td", { className: "border-b border-border px-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: chartIconButtonClass, title: "Delete category", "aria-label": `Delete ${category}`, "data-action": "remove-category", onClick: () => controller.removeCategory(i), children: "\xD7" }) })
378
- ] }, i);
379
- }) })
380
- ] }),
381
- !readOnly && /* @__PURE__ */ jsx2("div", { className: "p-1", children: /* @__PURE__ */ jsx2("button", { type: "button", className: `${chartButtonClass} h-6 border-dashed`, "data-action": "add-category", onClick: () => controller.addCategory(), children: "+ Add category" }) })
382
- ] });
383
- }
384
-
385
- // src/chart/ChartInspector.tsx
386
- import { useId } from "react";
387
- import {
388
- CHART_COLOR_SCHEMES,
389
- CHART_LABEL_POSITIONS,
390
- CHART_LAYOUT_PRESETS,
391
- CHART_MARKERS,
392
- CHART_TRENDLINE_TYPES,
393
- checkChartColors,
394
- chartFamily,
395
- chartSupportsTrendlines,
396
- chartTrendlineName,
397
- CHART_CURVES,
398
- CHART_LEGEND_POSITIONS,
399
- CHART_NUMBER_STYLES,
400
- CHART_SERIES_TYPES,
401
- CHART_STACKINGS,
402
- CHART_TYPE_LABELS,
403
- CHART_TYPES,
404
- chartColorInputValue as chartColorInputValue2,
405
- chartSeriesColor as chartSeriesColor2,
406
- chartSeriesType,
407
- isCartesianChart
408
- } from "@pixerate/editor/chart";
409
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
410
- var cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
411
- function Field({ label, htmlFor, children }) {
412
- return /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2", children: [
413
- /* @__PURE__ */ jsx3("label", { htmlFor, className: `${chartLabelClass} w-20 shrink-0`, children: label }),
414
- /* @__PURE__ */ jsx3("div", { className: "min-w-0 flex-1", children })
415
- ] });
416
- }
417
- function Toggle({ label, checked, onChange, name }) {
418
- return /* @__PURE__ */ jsxs3("label", { className: "flex cursor-pointer items-center gap-2 text-xs text-foreground", children: [
419
- /* @__PURE__ */ jsx3("input", { type: "checkbox", className: "h-3.5 w-3.5 accent-primary", checked, "data-option": name, onChange: (e) => onChange(e.currentTarget.checked) }),
420
- label
421
- ] });
422
- }
423
- function Select({ id, value, options, onChange, name, labels }) {
424
- return /* @__PURE__ */ jsx3("select", { id, className: chartInputClass, value, "data-option": name, onChange: (e) => onChange(e.currentTarget.value), children: options.map((o) => /* @__PURE__ */ jsx3("option", { value: o, children: labels?.[o] ?? cap(o) }, o)) });
425
- }
426
- function AxisSection({ controller, axis, value, idBase, horizontal, numeric = false }) {
427
- const set = (patch) => controller.setAxis(axis, patch);
428
- const setFormat = (patch) => set({ format: { ...value.format, ...patch } });
429
- const bound = (key2) => /* @__PURE__ */ jsx3(
430
- ChartCommitInput,
431
- {
432
- id: `${idBase}-${axis}-${key2}`,
433
- value: value[key2] === null ? "" : String(value[key2]),
434
- placeholder: "Auto",
435
- inputMode: "decimal",
436
- "data-option": `${axis}Axis.${key2}`,
437
- className: chartInputClass,
438
- onCommit: (text) => set({ [key2]: text.trim() === "" ? null : Number(text) }).ok
439
- }
440
- );
441
- const name = axis === "x" ? numeric ? "X axis" : "Category axis" : axis === "y" ? "Value axis" : "Secondary axis";
442
- const valueAxis = axis !== "x" || numeric;
443
- const key = axis === "y2" ? "yAxis2" : `${axis}Axis`;
444
- return /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": `axis-${axis}`, children: [
445
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
446
- /* @__PURE__ */ jsxs3("h4", { className: "text-xs font-semibold text-foreground", children: [
447
- name,
448
- /* @__PURE__ */ jsxs3("span", { className: "ml-1 font-normal text-muted-foreground", children: [
449
- "(",
450
- axis === "x" ? horizontal ? "left" : "bottom" : axis === "y2" ? horizontal ? "top" : "right" : horizontal ? "bottom" : "left",
451
- ")"
452
- ] })
453
- ] }),
454
- /* @__PURE__ */ jsx3(Toggle, { label: "Show", name: `${axis}Axis.show`, checked: value.show, onChange: (show) => set({ show }) })
455
- ] }),
456
- /* @__PURE__ */ jsx3(Field, { label: "Title", htmlFor: `${idBase}-${axis}-title`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-${axis}-title`, value: value.title, "data-option": `${axis}Axis.title`, className: chartInputClass, onCommit: (title) => void set({ title }) }) }),
457
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: [
458
- /* @__PURE__ */ jsx3(Toggle, { label: "Grid lines", name: `${key}.gridlines`, checked: value.gridlines, onChange: (gridlines) => set({ gridlines }) }),
459
- axis === "x" && !numeric && /* @__PURE__ */ jsx3(Toggle, { label: "Dates", name: "xAxis.kind", checked: value.kind === "date", onChange: (dates) => set({ kind: dates ? "date" : void 0 }) })
460
- ] }),
461
- valueAxis && /* @__PURE__ */ jsx3(Field, { label: "Step", htmlFor: `${idBase}-${axis}-step`, children: /* @__PURE__ */ jsx3(
462
- ChartCommitInput,
463
- {
464
- id: `${idBase}-${axis}-step`,
465
- value: value.step === null ? "" : String(value.step),
466
- placeholder: "Auto",
467
- inputMode: "decimal",
468
- "data-option": `${key}.step`,
469
- className: chartInputClass,
470
- onCommit: (text) => set({ step: text.trim() === "" ? null : Number(text) }).ok
471
- }
472
- ) }),
473
- valueAxis && /* @__PURE__ */ jsxs3(Fragment2, { children: [
474
- /* @__PURE__ */ jsx3(Field, { label: "Min / max", children: /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
475
- bound("min"),
476
- bound("max")
477
- ] }) }),
478
- /* @__PURE__ */ jsx3(Field, { label: "Format", htmlFor: `${idBase}-${axis}-format`, children: /* @__PURE__ */ jsxs3("div", { className: "flex gap-1", children: [
479
- /* @__PURE__ */ jsx3(Select, { id: `${idBase}-${axis}-format`, name: `${key}.format.style`, value: value.format.style ?? "number", options: CHART_NUMBER_STYLES, onChange: (style) => setFormat({ style }) }),
480
- /* @__PURE__ */ jsx3(
481
- ChartCommitInput,
482
- {
483
- value: value.format.decimals === void 0 ? "" : String(value.format.decimals),
484
- placeholder: "Auto",
485
- "aria-label": "Decimals",
486
- title: "Decimals",
487
- inputMode: "numeric",
488
- "data-option": `${key}.format.decimals`,
489
- className: `${chartInputClass} w-14 shrink-0`,
490
- onCommit: (text) => {
491
- const format = { ...value.format };
492
- if (text.trim() === "") delete format.decimals;
493
- else format.decimals = Number(text);
494
- return set({ format }).ok;
495
- }
496
- }
497
- )
498
- ] }) }),
499
- value.format.style === "currency" && /* @__PURE__ */ jsx3(Field, { label: "Currency", htmlFor: `${idBase}-${axis}-currency`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-${axis}-currency`, value: value.format.currency ?? "USD", "data-option": `${key}.format.currency`, className: chartInputClass, onCommit: (currency) => void setFormat({ currency: currency.toUpperCase() }) }) }),
500
- /* @__PURE__ */ jsx3(Field, { label: "Code", htmlFor: `${idBase}-${axis}-code`, children: /* @__PURE__ */ jsx3(
501
- ChartCommitInput,
502
- {
503
- id: `${idBase}-${axis}-code`,
504
- value: value.format.code ?? "",
505
- placeholder: 'e.g. #,##0.0,,"M"',
506
- "data-option": `${key}.format.code`,
507
- className: chartInputClass,
508
- onCommit: (code) => {
509
- const format = { ...value.format };
510
- if (code.trim()) format.code = code.trim();
511
- else delete format.code;
512
- return set({ format }).ok;
513
- }
514
- }
515
- ) })
516
- ] })
517
- ] });
518
- }
519
- function ChartInspector({ controller, sections, className }) {
520
- const { spec, selectedSeries } = useChartState(controller);
521
- const idBase = useId();
522
- const show = (section) => !sections || sections.includes(section);
523
- const cartesian = isCartesianChart(spec.type);
524
- const family = chartFamily(spec);
525
- const hasAxes = family === "cartesian" || family === "xy" || family === "waterfall" || family === "histogram" || family === "box";
526
- const types = spec.series.map((s) => chartSeriesType(spec, s));
527
- const usesSecondary = family === "cartesian" && spec.series.some((s) => s.axis === "secondary" && !s.hidden);
528
- const colorCheck = checkChartColors(spec.series.filter((s) => !s.hidden).map((s) => chartSeriesColor2(spec, spec.series.indexOf(s))));
529
- const hasBars = cartesian && types.includes("bar");
530
- const hasLines = cartesian && types.some((t) => t === "line" || t === "area");
531
- const seriesIndex = selectedSeries ? spec.series.findIndex((s) => s.id === selectedSeries.id) : -1;
532
- return /* @__PURE__ */ jsxs3("div", { className: `space-y-3 text-xs ${className ?? ""}`, "data-chart-inspector": "", children: [
533
- show("type") && /* @__PURE__ */ jsx3("div", { className: chartSectionClass, "data-section": "type", children: /* @__PURE__ */ jsx3("div", { className: "grid grid-cols-3 gap-0.5 rounded-md bg-muted p-0.5", role: "radiogroup", "aria-label": "Chart type", children: CHART_TYPES.map((type) => /* @__PURE__ */ jsx3("button", { type: "button", role: "radio", "aria-checked": spec.type === type, "data-chart-type": type, className: chartSegmentClass(spec.type === type), onClick: () => controller.setType(type), children: CHART_TYPE_LABELS[type] }, type)) }) }),
534
- show("style") && /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": "style", children: [
535
- /* @__PURE__ */ jsx3(Field, { label: "Layout", htmlFor: `${idBase}-layout`, children: /* @__PURE__ */ jsxs3("select", { id: `${idBase}-layout`, className: chartInputClass, value: "", "data-option": "preset.layout", onChange: (e) => e.currentTarget.value && controller.applyPreset({ layout: e.currentTarget.value }), children: [
536
- /* @__PURE__ */ jsx3("option", { value: "", children: "Quick layout\u2026" }),
537
- CHART_LAYOUT_PRESETS.map((p) => /* @__PURE__ */ jsx3("option", { value: p.id, title: p.description, children: p.label }, p.id))
538
- ] }) }),
539
- /* @__PURE__ */ jsx3(Field, { label: "Colours", htmlFor: `${idBase}-colors`, children: /* @__PURE__ */ jsxs3("select", { id: `${idBase}-colors`, className: chartInputClass, value: CHART_COLOR_SCHEMES.find((c) => c.palette.join() === spec.palette.join())?.id ?? "", "data-option": "preset.colors", onChange: (e) => e.currentTarget.value && controller.applyPreset({ colors: e.currentTarget.value }), children: [
540
- /* @__PURE__ */ jsx3("option", { value: "", children: "Custom" }),
541
- CHART_COLOR_SCHEMES.map((c) => /* @__PURE__ */ jsx3("option", { value: c.id, title: c.description, children: c.label }, c.id))
542
- ] }) }),
543
- !colorCheck.ok && /* @__PURE__ */ jsx3("p", { className: "text-[11px] leading-snug text-amber-600 dark:text-amber-400", "data-chart-color-warning": "", children: colorCheck.issues[0] }),
544
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, "data-action": "transpose", onClick: () => controller.transpose(), children: "Switch rows and columns" })
545
- ] }),
546
- show("titles") && /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": "titles", children: [
547
- /* @__PURE__ */ jsx3(Field, { label: "Title", htmlFor: `${idBase}-title`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-title`, value: spec.title, "data-option": "title", className: chartInputClass, onCommit: (title) => void controller.setTitle(title) }) }),
548
- /* @__PURE__ */ jsx3(Field, { label: "Subtitle", htmlFor: `${idBase}-subtitle`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-subtitle`, value: spec.subtitle, "data-option": "subtitle", className: chartInputClass, onCommit: (subtitle) => void controller.setSubtitle(subtitle) }) })
549
- ] }),
550
- show("options") && /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": "options", children: [
551
- cartesian && /* @__PURE__ */ jsx3(Field, { label: "Stacking", htmlFor: `${idBase}-stacking`, children: /* @__PURE__ */ jsx3(Select, { id: `${idBase}-stacking`, name: "stacking", value: spec.stacking, options: CHART_STACKINGS, labels: { none: "None", stacked: "Stacked", percent: "100%" }, onChange: (stacking) => controller.update({ stacking }) }) }),
552
- hasBars && /* @__PURE__ */ jsx3(Field, { label: "Bars", children: /* @__PURE__ */ jsx3("div", { className: "flex gap-0.5 rounded-md bg-muted p-0.5", children: ["vertical", "horizontal"].map((o) => /* @__PURE__ */ jsx3("button", { type: "button", "aria-pressed": spec.orientation === o, "data-option": `orientation.${o}`, className: chartSegmentClass(spec.orientation === o), onClick: () => controller.update({ orientation: o }), children: cap(o) }, o)) }) }),
553
- hasLines && /* @__PURE__ */ jsx3(Field, { label: "Lines", htmlFor: `${idBase}-curve`, children: /* @__PURE__ */ jsx3(Select, { id: `${idBase}-curve`, name: "curve", value: spec.curve, options: CHART_CURVES, onChange: (curve) => controller.update({ curve }) }) }),
554
- spec.type === "donut" && /* @__PURE__ */ jsx3(Field, { label: "Hole", htmlFor: `${idBase}-hole`, children: /* @__PURE__ */ jsx3(
555
- "input",
556
- {
557
- id: `${idBase}-hole`,
558
- type: "range",
559
- min: 0,
560
- max: 0.9,
561
- step: 0.05,
562
- value: spec.innerRadius,
563
- "data-option": "innerRadius",
564
- className: "w-full accent-primary",
565
- onPointerDown: () => controller.beginGesture("innerRadius"),
566
- onPointerUp: () => controller.endGesture(),
567
- onChange: (e) => controller.update({ innerRadius: Number(e.currentTarget.value) })
568
- }
569
- ) }),
570
- hasBars && /* @__PURE__ */ jsx3(Field, { label: "Spacing", htmlFor: `${idBase}-gap`, children: /* @__PURE__ */ jsx3(
571
- "input",
572
- {
573
- id: `${idBase}-gap`,
574
- type: "range",
575
- min: 0,
576
- max: 0.9,
577
- step: 0.02,
578
- value: spec.gapWidth,
579
- "data-option": "gapWidth",
580
- className: "w-full accent-primary",
581
- onPointerDown: () => controller.beginGesture("gapWidth"),
582
- onPointerUp: () => controller.endGesture(),
583
- onChange: (e) => controller.update({ gapWidth: Number(e.currentTarget.value) })
584
- }
585
- ) }),
586
- spec.dataLabels && hasBars && /* @__PURE__ */ jsx3(Field, { label: "Labels", htmlFor: `${idBase}-label-position`, children: /* @__PURE__ */ jsx3(Select, { id: `${idBase}-label-position`, name: "labelPosition", value: spec.labelPosition, options: CHART_LABEL_POSITIONS, onChange: (labelPosition) => controller.update({ labelPosition }) }) }),
587
- spec.type === "histogram" && /* @__PURE__ */ jsx3(Field, { label: "Bins", htmlFor: `${idBase}-bins`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-bins`, value: spec.bins === null ? "" : String(spec.bins), placeholder: "Auto", inputMode: "numeric", "data-option": "bins", className: chartInputClass, onCommit: (text) => controller.update({ bins: text.trim() === "" ? null : Number(text) }).ok }) }),
588
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: [
589
- spec.type === "waterfall" && /* @__PURE__ */ jsx3(Toggle, { label: "Total bar", name: "showTotal", checked: spec.showTotal, onChange: (showTotal) => controller.update({ showTotal }) }),
590
- hasLines && /* @__PURE__ */ jsx3(Toggle, { label: "Points", name: "points", checked: spec.points, onChange: (points) => controller.update({ points }) }),
591
- /* @__PURE__ */ jsx3(Toggle, { label: "Data labels", name: "dataLabels", checked: spec.dataLabels, onChange: (dataLabels) => controller.update({ dataLabels }) })
592
- ] })
593
- ] }),
594
- show("legend") && /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": "legend", children: [
595
- /* @__PURE__ */ jsxs3("div", { className: "flex items-center justify-between", children: [
596
- /* @__PURE__ */ jsx3("h4", { className: "text-xs font-semibold text-foreground", children: "Legend" }),
597
- /* @__PURE__ */ jsx3(Toggle, { label: "Show", name: "legend.show", checked: spec.legend.show, onChange: (v) => controller.setLegend({ show: v }) })
598
- ] }),
599
- spec.legend.show && /* @__PURE__ */ jsx3("div", { className: "flex gap-0.5 rounded-md bg-muted p-0.5", children: CHART_LEGEND_POSITIONS.map((p) => /* @__PURE__ */ jsx3("button", { type: "button", "aria-pressed": spec.legend.position === p, "data-option": `legend.${p}`, className: chartSegmentClass(spec.legend.position === p), onClick: () => controller.setLegend({ position: p }), children: cap(p) }, p)) })
35
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-chart-editor": "", children: [
36
+ showToolbar && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "data-chart-toolbar": "", children: [
37
+ /* @__PURE__ */ jsx("button", { type: "button", className: chartButtonClass, disabled: !state.canUndo, title: state.undoLabel ? `Undo ${state.undoLabel}` : "Undo", "data-action": "undo", onClick: () => controller.undo(), children: "Undo" }),
38
+ /* @__PURE__ */ jsx("button", { type: "button", className: chartButtonClass, disabled: !state.canRedo, title: state.redoLabel ? `Redo ${state.redoLabel}` : "Redo", "data-action": "redo", onClick: () => controller.redo(), children: "Redo" }),
39
+ /* @__PURE__ */ jsx("button", { type: "button", className: chartButtonClass, title: "Download the chart as a PNG image", "data-action": "download-png", onClick: downloadPng, children: "Download PNG" }),
40
+ /* @__PURE__ */ jsx("span", { className: "ml-2 truncate text-xs text-muted-foreground", "data-chart-selection": "", "aria-live": "polite", children: describeChartSelection(controller, state.selection) })
600
41
  ] }),
601
- show("axes") && hasAxes && /* @__PURE__ */ jsxs3(Fragment2, { children: [
602
- family !== "box" && family !== "histogram" && /* @__PURE__ */ jsx3(AxisSection, { controller, axis: "x", value: spec.xAxis, idBase, horizontal: spec.orientation === "horizontal", numeric: family === "xy" }),
603
- /* @__PURE__ */ jsx3(AxisSection, { controller, axis: "y", value: spec.yAxis, idBase, horizontal: spec.orientation === "horizontal" }),
604
- usesSecondary && /* @__PURE__ */ jsx3(AxisSection, { controller, axis: "y2", value: spec.yAxis2, idBase, horizontal: spec.orientation === "horizontal" })
42
+ pngError && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive", "data-chart-png-error": "", children: pngError }),
43
+ state.issues.length > 0 && /* @__PURE__ */ jsx("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive", "data-chart-issues": "", children: state.issues.map((i) => i.message).join(" ") }),
44
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
45
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 rounded-md border border-border bg-background p-1", children: /* @__PURE__ */ jsx(ChartCanvas, { controller, height, ...canvasProps }) }),
46
+ showInspector && /* @__PURE__ */ jsx(ChartInspector, { controller, className: "w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" })
605
47
  ] }),
606
- show("series") && selectedSeries && /* @__PURE__ */ jsxs3("div", { className: chartSectionClass, "data-section": "series", "data-series-id": selectedSeries.id, children: [
607
- /* @__PURE__ */ jsx3("h4", { className: "text-xs font-semibold text-foreground", children: "Series" }),
608
- /* @__PURE__ */ jsx3(Field, { label: "Name", htmlFor: `${idBase}-series-name`, children: /* @__PURE__ */ jsx3(ChartCommitInput, { id: `${idBase}-series-name`, value: selectedSeries.name, "data-option": "series.name", className: chartInputClass, onCommit: (name) => void controller.updateSeries(selectedSeries.id, { name }) }) }),
609
- /* @__PURE__ */ jsx3(Field, { label: "Colour", htmlFor: `${idBase}-series-color`, children: /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", children: [
610
- /* @__PURE__ */ jsx3(
611
- "input",
612
- {
613
- id: `${idBase}-series-color`,
614
- type: "color",
615
- value: chartColorInputValue2(chartSeriesColor2(spec, seriesIndex)),
616
- "data-option": "series.color",
617
- className: "h-7 w-9 shrink-0 cursor-pointer rounded border border-input bg-background p-0.5",
618
- onPointerDown: () => controller.beginGesture(`color:${selectedSeries.id}`),
619
- onBlur: () => controller.endGesture(),
620
- onChange: (e) => controller.updateSeries(selectedSeries.id, { color: e.currentTarget.value })
621
- }
622
- ),
623
- selectedSeries.color && /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, "data-action": "reset-color", onClick: () => controller.updateSeries(selectedSeries.id, { color: null }), children: "Palette" })
624
- ] }) }),
625
- cartesian && /* @__PURE__ */ jsx3(Field, { label: "Draw as", htmlFor: `${idBase}-series-type`, children: /* @__PURE__ */ jsxs3(
626
- "select",
627
- {
628
- id: `${idBase}-series-type`,
629
- className: chartInputClass,
630
- value: selectedSeries.type ?? "",
631
- "data-option": "series.type",
632
- onChange: (e) => controller.updateSeries(selectedSeries.id, { type: e.currentTarget.value || null }),
633
- children: [
634
- /* @__PURE__ */ jsx3("option", { value: "", children: "Chart type" }),
635
- CHART_SERIES_TYPES.map((t) => /* @__PURE__ */ jsx3("option", { value: t, children: CHART_TYPE_LABELS[t] }, t))
636
- ]
637
- }
638
- ) }),
639
- (cartesian || family === "xy") && ["line", "area", "scatter"].includes(chartSeriesType(spec, selectedSeries)) && /* @__PURE__ */ jsx3(Field, { label: "Marker", htmlFor: `${idBase}-series-marker`, children: /* @__PURE__ */ jsxs3(
640
- "select",
641
- {
642
- id: `${idBase}-series-marker`,
643
- className: chartInputClass,
644
- value: selectedSeries.marker ?? "",
645
- "data-option": "series.marker",
646
- onChange: (e) => controller.updateSeries(selectedSeries.id, { marker: e.currentTarget.value || null }),
647
- children: [
648
- /* @__PURE__ */ jsx3("option", { value: "", children: "Default" }),
649
- CHART_MARKERS.map((m) => /* @__PURE__ */ jsx3("option", { value: m, children: cap(m) }, m))
650
- ]
651
- }
652
- ) }),
653
- chartSupportsTrendlines(spec.type, spec.stacking) && /* @__PURE__ */ jsxs3("div", { className: "space-y-1", "data-section": "trendlines", children: [
654
- (selectedSeries.trendlines ?? []).map((t, i) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-1", "data-trendline": i, children: [
655
- /* @__PURE__ */ jsx3("span", { className: "flex-1 truncate text-muted-foreground", children: chartTrendlineName(t, selectedSeries.name) }),
656
- /* @__PURE__ */ jsx3(Toggle, { label: "R\xB2", name: `trendline.${i}.showR2`, checked: !!t.showR2, onChange: (showR2) => controller.updateTrendline(selectedSeries.id, i, { showR2 }) }),
657
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, "aria-label": "Delete trendline", "data-action": "trendline-delete", onClick: () => controller.removeTrendline(selectedSeries.id, i), children: "\xD7" })
658
- ] }, i)),
659
- /* @__PURE__ */ jsxs3(
660
- "select",
661
- {
662
- className: chartInputClass,
663
- value: "",
664
- "aria-label": "Add trendline",
665
- "data-option": "trendline.add",
666
- onChange: (e) => e.currentTarget.value && controller.addTrendline(selectedSeries.id, { type: e.currentTarget.value }),
667
- children: [
668
- /* @__PURE__ */ jsx3("option", { value: "", children: "Add trendline\u2026" }),
669
- CHART_TRENDLINE_TYPES.map((t) => /* @__PURE__ */ jsx3("option", { value: t, children: cap(t.replace("-", " ")) }, t))
670
- ]
671
- }
672
- )
673
- ] }),
674
- /* @__PURE__ */ jsxs3("div", { className: "flex flex-wrap items-center gap-1", children: [
675
- /* @__PURE__ */ jsx3(Toggle, { label: "Hidden", name: "series.hidden", checked: !!selectedSeries.hidden, onChange: (hidden) => controller.updateSeries(selectedSeries.id, { hidden }) }),
676
- family === "cartesian" && /* @__PURE__ */ jsx3(Toggle, { label: "Secondary axis", name: "series.axis", checked: selectedSeries.axis === "secondary", onChange: (secondary) => controller.updateSeries(selectedSeries.id, { axis: secondary ? "secondary" : null }) }),
677
- /* @__PURE__ */ jsx3("span", { className: "flex-1" }),
678
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, disabled: seriesIndex <= 0, "aria-label": "Move series earlier", "data-action": "series-up", onClick: () => controller.moveSeries(selectedSeries.id, seriesIndex - 1), children: "\u2191" }),
679
- /* @__PURE__ */ jsx3("button", { type: "button", className: chartButtonClass, disabled: seriesIndex >= spec.series.length - 1, "aria-label": "Move series later", "data-action": "series-down", onClick: () => controller.moveSeries(selectedSeries.id, seriesIndex + 1), children: "\u2193" }),
680
- /* @__PURE__ */ jsx3("button", { type: "button", className: `${chartButtonClass} text-destructive`, "data-action": "series-delete", onClick: () => controller.removeSeries(selectedSeries.id), children: "Delete" })
681
- ] })
682
- ] })
683
- ] });
684
- }
685
-
686
- // src/chart/ChartEditor.tsx
687
- import { describeChartSelection as describeChartSelection2 } from "@pixerate/editor/chart";
688
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
689
- function ChartEditor({ controller, showInspector = true, showDataGrid = true, showToolbar = true, height = 380, canvasProps, className }) {
690
- const state = useChartState(controller);
691
- return /* @__PURE__ */ jsxs4("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-chart-editor": "", children: [
692
- showToolbar && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", "data-chart-toolbar": "", children: [
693
- /* @__PURE__ */ jsx4("button", { type: "button", className: chartButtonClass, disabled: !state.canUndo, title: state.undoLabel ? `Undo ${state.undoLabel}` : "Undo", "data-action": "undo", onClick: () => controller.undo(), children: "Undo" }),
694
- /* @__PURE__ */ jsx4("button", { type: "button", className: chartButtonClass, disabled: !state.canRedo, title: state.redoLabel ? `Redo ${state.redoLabel}` : "Redo", "data-action": "redo", onClick: () => controller.redo(), children: "Redo" }),
695
- /* @__PURE__ */ jsx4("span", { className: "ml-2 truncate text-xs text-muted-foreground", "data-chart-selection": "", "aria-live": "polite", children: describeChartSelection2(controller, state.selection) })
696
- ] }),
697
- state.issues.length > 0 && /* @__PURE__ */ jsx4("p", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive", "data-chart-issues": "", children: state.issues.map((i) => i.message).join(" ") }),
698
- /* @__PURE__ */ jsxs4("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
699
- /* @__PURE__ */ jsx4("div", { className: "min-w-0 flex-1 rounded-md border border-border bg-background p-1", children: /* @__PURE__ */ jsx4(ChartCanvas, { controller, height, ...canvasProps }) }),
700
- showInspector && /* @__PURE__ */ jsx4(ChartInspector, { controller, className: "w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" })
701
- ] }),
702
- showDataGrid && /* @__PURE__ */ jsx4(ChartDataGrid, { controller })
48
+ showDataGrid && /* @__PURE__ */ jsx(ChartDataGrid, { controller })
703
49
  ] });
704
50
  }
705
51
 
706
52
  // src/chart/ChartStreamingView.tsx
707
- import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
53
+ import { useEffect, useMemo, useRef, useState as useState2 } from "react";
708
54
  import {
709
- CSS_VARIABLE_CHART_THEME as CSS_VARIABLE_CHART_THEME2,
55
+ CSS_VARIABLE_CHART_THEME,
710
56
  ChartStream,
711
57
  blendChartScenes,
712
58
  easeChartTween,
713
- layoutChart as layoutChart3,
59
+ layoutChart,
714
60
  layoutSparkline
715
61
  } from "@pixerate/editor/chart";
716
- import { jsx as jsx5 } from "react/jsx-runtime";
62
+ import { jsx as jsx2 } from "react/jsx-runtime";
717
63
  function ChartStreamingView({ source, complete = false, format = "auto", width = 640, height = 360, theme, adoptInterval = 300, tween = 450, placeholder, onIssues, className }) {
718
- const stream = useMemo3(() => new ChartStream(format), [format]);
719
- const fullTheme = useMemo3(() => ({ ...CSS_VARIABLE_CHART_THEME2, ...theme }), [theme]);
720
- const [scene, setScene] = useState3(null);
721
- const shown = useRef4(null);
722
- const lastAdopt = useRef4(0);
723
- const frame = useRef4(null);
724
- const pending = useRef4(null);
725
- useEffect4(() => {
64
+ const stream = useMemo(() => new ChartStream(format), [format]);
65
+ const fullTheme = useMemo(() => ({ ...CSS_VARIABLE_CHART_THEME, ...theme }), [theme]);
66
+ const [scene, setScene] = useState2(null);
67
+ const shown = useRef(null);
68
+ const lastAdopt = useRef(0);
69
+ const frame = useRef(null);
70
+ const pending = useRef(null);
71
+ useEffect(() => {
726
72
  const adopt = () => {
727
73
  const read = stream.update(source, complete);
728
74
  if (complete && onIssues) onIssues(read.issues);
729
75
  if (!read.spec) return;
730
- const next = layoutChart3(read.spec, { width, height, theme: fullTheme, valueDomain: read.valueDomain });
76
+ const next = layoutChart(read.spec, { width, height, theme: fullTheme, valueDomain: read.valueDomain });
731
77
  const from = shown.current;
732
78
  lastAdopt.current = Date.now();
733
79
  if (frame.current !== null) cancelAnimationFrame(frame.current);
@@ -754,15 +100,15 @@ function ChartStreamingView({ source, complete = false, format = "auto", width =
754
100
  if (pending.current) clearTimeout(pending.current);
755
101
  };
756
102
  }, [source, complete, stream, width, height, fullTheme, adoptInterval, tween, onIssues]);
757
- useEffect4(() => () => {
103
+ useEffect(() => () => {
758
104
  if (frame.current !== null) cancelAnimationFrame(frame.current);
759
105
  }, []);
760
- return /* @__PURE__ */ jsx5("div", { className, "data-chart-streaming": complete ? "complete" : "streaming", "aria-busy": !complete, children: scene ? /* @__PURE__ */ jsx5(ChartSceneSvg, { scene, label: complete ? void 0 : "Chart loading" }) : placeholder ?? /* @__PURE__ */ jsx5("div", { style: { height }, className: "flex items-center justify-center text-xs text-muted-foreground", children: "Drawing chart\u2026" }) });
106
+ return /* @__PURE__ */ jsx2("div", { className, "data-chart-streaming": complete ? "complete" : "streaming", "aria-busy": !complete, children: scene ? /* @__PURE__ */ jsx2(ChartSceneSvg, { scene, label: complete ? void 0 : "Chart loading" }) : placeholder ?? /* @__PURE__ */ jsx2("div", { style: { height }, className: "flex items-center justify-center text-xs text-muted-foreground", children: "Drawing chart\u2026" }) });
761
107
  }
762
108
  function ChartSparkline({ values, width = 80, height = 20, label, className, ...options }) {
763
- const scene = useMemo3(() => layoutSparkline(values, { ...options, width, height }), [values, width, height, options.kind, options.color, options.negativeColor, options.markers, options.highLow, options.min, options.max]);
109
+ const scene = useMemo(() => layoutSparkline(values, { ...options, width, height }), [values, width, height, options.kind, options.color, options.negativeColor, options.markers, options.highLow, options.min, options.max]);
764
110
  const present = values.filter((v) => v !== null);
765
- return /* @__PURE__ */ jsx5(ChartSceneSvg, { scene, label: label ?? (present.length ? `Trend from ${present[0]} to ${present[present.length - 1]}` : "No data"), className: `inline-block align-middle ${className ?? ""}` });
111
+ return /* @__PURE__ */ jsx2(ChartSceneSvg, { scene, label: label ?? (present.length ? `Trend from ${present[0]} to ${present[present.length - 1]}` : "No data"), className: `inline-block align-middle ${className ?? ""}` });
766
112
  }
767
113
  export {
768
114
  ChartCanvas,