@pixerate/editor-react 0.36.0 → 0.37.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,71 @@
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";
77
-
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";
90
- 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 }) {
151
- 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);
293
- };
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)) })
600
- ] }),
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" })
605
- ] }),
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
- }
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-C2JWRD2B.js";
685
23
 
686
24
  // 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";
25
+ import { describeChartSelection } from "@pixerate/editor/chart";
26
+ import { jsx, jsxs } from "react/jsx-runtime";
689
27
  function ChartEditor({ controller, showInspector = true, showDataGrid = true, showToolbar = true, height = 380, canvasProps, className }) {
690
28
  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) })
29
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-2 ${className ?? ""}`, "data-chart-editor": "", children: [
30
+ showToolbar && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", "data-chart-toolbar": "", children: [
31
+ /* @__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" }),
32
+ /* @__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" }),
33
+ /* @__PURE__ */ jsx("span", { className: "ml-2 truncate text-xs text-muted-foreground", "data-chart-selection": "", "aria-live": "polite", children: describeChartSelection(controller, state.selection) })
696
34
  ] }),
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" })
35
+ 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(" ") }),
36
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-col gap-2 md:flex-row", children: [
37
+ /* @__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 }) }),
38
+ showInspector && /* @__PURE__ */ jsx(ChartInspector, { controller, className: "w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" })
701
39
  ] }),
702
- showDataGrid && /* @__PURE__ */ jsx4(ChartDataGrid, { controller })
40
+ showDataGrid && /* @__PURE__ */ jsx(ChartDataGrid, { controller })
703
41
  ] });
704
42
  }
705
43
 
706
44
  // src/chart/ChartStreamingView.tsx
707
- import { useEffect as useEffect4, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
45
+ import { useEffect, useMemo, useRef, useState } from "react";
708
46
  import {
709
- CSS_VARIABLE_CHART_THEME as CSS_VARIABLE_CHART_THEME2,
47
+ CSS_VARIABLE_CHART_THEME,
710
48
  ChartStream,
711
49
  blendChartScenes,
712
50
  easeChartTween,
713
- layoutChart as layoutChart3,
51
+ layoutChart,
714
52
  layoutSparkline
715
53
  } from "@pixerate/editor/chart";
716
- import { jsx as jsx5 } from "react/jsx-runtime";
54
+ import { jsx as jsx2 } from "react/jsx-runtime";
717
55
  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(() => {
56
+ const stream = useMemo(() => new ChartStream(format), [format]);
57
+ const fullTheme = useMemo(() => ({ ...CSS_VARIABLE_CHART_THEME, ...theme }), [theme]);
58
+ const [scene, setScene] = useState(null);
59
+ const shown = useRef(null);
60
+ const lastAdopt = useRef(0);
61
+ const frame = useRef(null);
62
+ const pending = useRef(null);
63
+ useEffect(() => {
726
64
  const adopt = () => {
727
65
  const read = stream.update(source, complete);
728
66
  if (complete && onIssues) onIssues(read.issues);
729
67
  if (!read.spec) return;
730
- const next = layoutChart3(read.spec, { width, height, theme: fullTheme, valueDomain: read.valueDomain });
68
+ const next = layoutChart(read.spec, { width, height, theme: fullTheme, valueDomain: read.valueDomain });
731
69
  const from = shown.current;
732
70
  lastAdopt.current = Date.now();
733
71
  if (frame.current !== null) cancelAnimationFrame(frame.current);
@@ -754,15 +92,15 @@ function ChartStreamingView({ source, complete = false, format = "auto", width =
754
92
  if (pending.current) clearTimeout(pending.current);
755
93
  };
756
94
  }, [source, complete, stream, width, height, fullTheme, adoptInterval, tween, onIssues]);
757
- useEffect4(() => () => {
95
+ useEffect(() => () => {
758
96
  if (frame.current !== null) cancelAnimationFrame(frame.current);
759
97
  }, []);
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" }) });
98
+ 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
99
  }
762
100
  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]);
101
+ 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
102
  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 ?? ""}` });
103
+ 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
104
  }
767
105
  export {
768
106
  ChartCanvas,