@pixerate/editor-svelte 0.41.0 → 0.42.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.
package/README.md CHANGED
@@ -84,6 +84,7 @@ Tables are on by default in `createRichTextPreset` (`table: false` turns them of
84
84
  | `@pixerate/editor-svelte/persistence` | `createAutosave` (crash-recovery snapshots bound to a history / dirty flag, flushed on `pagehide`; call `destroy()` when done) and `<RecoveryPrompt>` (also re-exported from the root entry) |
85
85
  | `@pixerate/editor-svelte/generation` | `GenerationSettings` (controls generated from a model's capabilities, reference slots by role, plan adjustments; `onchange` / `onaddreference`), `createGenerationPlan(() => request, () => caps)`, `JobProgress`, `createJobProgress` (call `destroy()` when done) over `@pixerate/editor/generation` (also re-exported from the root entry) |
86
86
  | `@pixerate/editor-svelte/vector` | `VectorEditor` (toolbar, canvas and style panel), `VectorCanvas` (SVG canvas forwarding pointer, wheel, keyboard and clipboard events), `VectorToolbar`, `VectorStylePanel`, `VectorSceneNodes`, `VectorGradientDefs`, `createVectorEditor` (call `destroy()` when done) and `useVectorCommands` over `@pixerate/editor/vector` (see [the vector editor guide](../../docs/vector-editor.md)) |
87
+ | `@pixerate/editor-svelte/chart` | `ChartEditor` (toolbar, chart, inspector and data grid), `ChartCanvas` (SVG chart with selection, drag-to-edit and keyboard editing), `ChartDataGrid`, `ChartInspector`, `createChartEditor` (call `destroy()` when done) and `useChartCommands` over `@pixerate/editor/chart` (see [the chart editor guide](../../docs/chart-editor.md)) |
87
88
  | `@pixerate/editor-svelte/parameters` | `ParametersPanel` (a whole schema with groups, presets, undo / reset / randomize and keyframe diamonds), `createParameters` (call `destroy()` when done), `useParameterCommands`, and the field controls `ParameterFieldControl`, `ParameterSlider`, `ParameterToggle`, `ParameterSelect`, `ParameterColor`, `ParameterVector`, `ParameterImage`, `ParameterText`, `KeyframeDiamond` over `@pixerate/editor/parameters` (see [the parameters guide](../../docs/parameters.md)) |
88
89
  | `@pixerate/editor-svelte/canvas` | Runes for `@xyflow/svelte`: `createCanvasGraph`, `createCanvasClipboard`, `createCanvasDocking`, `createCanvasShortcuts`, `createCanvasInteractions`, `createCanvasExplosion`, `createCanvasNodeSync`, `createCanvasMultiDrag`, `createStreamingCanvas`, `AgentPresenceLayer`, `FloatingHorizontalScrollbar`, plus layout helpers |
89
90
 
@@ -0,0 +1,213 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import {
4
+ CSS_VARIABLE_CHART_THEME,
5
+ chartClientToScene,
6
+ chartTextBaseline,
7
+ chartTextTransform,
8
+ describeChartSelection,
9
+ handleChartKey,
10
+ layoutChart,
11
+ startChartPointDrag,
12
+ type ChartController,
13
+ type ChartMark,
14
+ type ChartPointDrag,
15
+ type ChartTextMeasure,
16
+ type ChartTheme
17
+ } from '@pixerate/editor/chart';
18
+ import { chartStateOf } from './createChartEditor.svelte.js';
19
+
20
+ interface Props {
21
+ controller: ChartController;
22
+ /** Scene width; with `responsive` this is only the fallback before the container is measured (default 640). */
23
+ width?: number;
24
+ /** Scene height in pixels (default 400). */
25
+ height?: number;
26
+ /** Follow the container's width (default `true`). */
27
+ responsive?: boolean;
28
+ /** Colours and type (default: shadcn-style CSS variables). */
29
+ theme?: Partial<ChartTheme>;
30
+ /** Click to select, drag bars and points, keyboard editing (default `true`). */
31
+ editable?: boolean;
32
+ /** Text width for layout (default: an estimate). */
33
+ measureText?: ChartTextMeasure;
34
+ emptyMessage?: string;
35
+ class?: string;
36
+ }
37
+
38
+ let {
39
+ controller,
40
+ width = 640,
41
+ height = 400,
42
+ responsive = true,
43
+ theme,
44
+ editable = true,
45
+ measureText,
46
+ emptyMessage,
47
+ class: className = ''
48
+ }: Props = $props();
49
+
50
+ const view = chartStateOf(() => controller);
51
+ let measured = $state(0);
52
+ let wrapper: HTMLDivElement | undefined = $state();
53
+ let svg: SVGSVGElement | undefined = $state();
54
+
55
+ // Guarded (not `bind:clientWidth`) so it also runs where ResizeObserver is missing (jsdom).
56
+ $effect(() => {
57
+ const el = wrapper;
58
+ if (!responsive || !el || typeof ResizeObserver === 'undefined') return;
59
+ const observer = new ResizeObserver(([entry]) => {
60
+ const w = Math.round(entry.contentRect.width);
61
+ if (w > 0) measured = w;
62
+ });
63
+ observer.observe(el);
64
+ return () => observer.disconnect();
65
+ });
66
+ let drag: ChartPointDrag | null = null;
67
+
68
+ const sceneWidth = $derived((responsive && measured) || width);
69
+ const fullTheme = $derived({ ...CSS_VARIABLE_CHART_THEME, ...theme });
70
+ const scene = $derived(
71
+ layoutChart(view.current.spec, {
72
+ width: sceneWidth,
73
+ height,
74
+ theme: fullTheme,
75
+ selection: editable ? view.current.selection : null,
76
+ measureText,
77
+ emptyMessage
78
+ })
79
+ );
80
+ const byKey = $derived(new Map(scene.marks.map((m) => [m.key, m])));
81
+ const label = $derived(view.current.spec.title || 'Chart');
82
+
83
+ onDestroy(() => drag?.end());
84
+
85
+ const pointerEvents = (mark: ChartMark) => (mark.role === 'selection' || (!mark.target && mark.role !== 'background') ? 'none' : undefined);
86
+ const cursor = (mark: ChartMark) => (editable && mark.target && mark.role !== 'background' ? 'pointer' : undefined);
87
+ const scenePoint = (e: PointerEvent) => chartClientToScene(scene, svg!.getBoundingClientRect(), e.clientX, e.clientY);
88
+
89
+ function pointerdown(e: PointerEvent) {
90
+ if (!editable || e.button !== 0) return;
91
+ const key = (e.target as Element).closest?.('[data-chart-key]')?.getAttribute('data-chart-key');
92
+ const target = key ? byKey.get(key)?.target : undefined;
93
+ controller.select(target ?? null);
94
+ drag = startChartPointDrag(controller, scene, target, scenePoint(e));
95
+ if (drag) svg?.setPointerCapture?.(e.pointerId);
96
+ }
97
+ function pointermove(e: PointerEvent) {
98
+ drag?.move(scenePoint(e));
99
+ }
100
+ function endDrag() {
101
+ drag?.end();
102
+ drag = null;
103
+ }
104
+ function keydown(e: KeyboardEvent) {
105
+ if (editable && handleChartKey(controller, scene, e)) e.preventDefault();
106
+ }
107
+ </script>
108
+
109
+ <div bind:this={wrapper} class="w-full {className}" data-chart-canvas="">
110
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
111
+ <svg
112
+ bind:this={svg}
113
+ width={sceneWidth}
114
+ {height}
115
+ viewBox="0 0 {scene.width} {scene.height}"
116
+ role="img"
117
+ aria-label={editable ? `${label}. ${describeChartSelection(controller, view.current.selection)}` : label}
118
+ tabindex={editable ? 0 : undefined}
119
+ class="block max-w-full touch-none select-none rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
120
+ onpointerdown={pointerdown}
121
+ onpointermove={pointermove}
122
+ onpointerup={endDrag}
123
+ onpointercancel={endDrag}
124
+ onkeydown={keydown}
125
+ >
126
+ {#each scene.marks as mark (mark.key)}
127
+ {#if mark.kind === 'rect'}
128
+ <rect
129
+ data-role={mark.role}
130
+ data-chart-key={mark.target ? mark.key : undefined}
131
+ data-selected={mark.selected ? '' : undefined}
132
+ x={mark.x}
133
+ y={mark.y}
134
+ width={mark.width}
135
+ height={mark.height}
136
+ rx={mark.radius}
137
+ opacity={mark.opacity}
138
+ pointer-events={pointerEvents(mark)}
139
+ cursor={cursor(mark)}
140
+ style:fill={mark.fill}
141
+ style:stroke={mark.stroke}
142
+ style:stroke-width={mark.strokeWidth}
143
+ />
144
+ {:else if mark.kind === 'path'}
145
+ <path
146
+ data-role={mark.role}
147
+ data-chart-key={mark.target ? mark.key : undefined}
148
+ data-selected={mark.selected ? '' : undefined}
149
+ d={mark.d}
150
+ stroke-linejoin="round"
151
+ stroke-linecap="round"
152
+ stroke-dasharray={mark.dash}
153
+ opacity={mark.opacity}
154
+ pointer-events={pointerEvents(mark)}
155
+ cursor={cursor(mark)}
156
+ style:fill={mark.fill ?? 'none'}
157
+ style:stroke={mark.stroke}
158
+ style:stroke-width={mark.strokeWidth}
159
+ />
160
+ {:else if mark.kind === 'circle'}
161
+ <circle
162
+ data-role={mark.role}
163
+ data-chart-key={mark.target ? mark.key : undefined}
164
+ data-selected={mark.selected ? '' : undefined}
165
+ cx={mark.cx}
166
+ cy={mark.cy}
167
+ r={mark.r}
168
+ opacity={mark.opacity}
169
+ pointer-events={pointerEvents(mark)}
170
+ cursor={cursor(mark)}
171
+ style:fill={mark.fill}
172
+ style:stroke={mark.stroke}
173
+ style:stroke-width={mark.strokeWidth}
174
+ />
175
+ {:else if mark.kind === 'line'}
176
+ <line
177
+ data-role={mark.role}
178
+ data-chart-key={mark.target ? mark.key : undefined}
179
+ data-selected={mark.selected ? '' : undefined}
180
+ x1={mark.x1}
181
+ y1={mark.y1}
182
+ x2={mark.x2}
183
+ y2={mark.y2}
184
+ stroke-dasharray={mark.dash}
185
+ opacity={mark.opacity}
186
+ pointer-events={pointerEvents(mark)}
187
+ cursor={cursor(mark)}
188
+ style:stroke={mark.stroke}
189
+ style:stroke-width={mark.strokeWidth}
190
+ />
191
+ {:else}
192
+ <text
193
+ data-role={mark.role}
194
+ data-chart-key={mark.target ? mark.key : undefined}
195
+ data-selected={mark.selected ? '' : undefined}
196
+ x={mark.x}
197
+ y={mark.y}
198
+ text-anchor={mark.anchor}
199
+ dominant-baseline={chartTextBaseline(mark.baseline)}
200
+ transform={chartTextTransform(mark)}
201
+ opacity={mark.opacity}
202
+ pointer-events={pointerEvents(mark)}
203
+ cursor={cursor(mark)}
204
+ style:fill={mark.fill}
205
+ style:font-size="{mark.size}px"
206
+ style:font-weight={mark.weight}
207
+ style:font-family={scene.theme.fontFamily}
208
+ style:user-select="none">{mark.text}</text
209
+ >
210
+ {/if}
211
+ {/each}
212
+ </svg>
213
+ </div>
@@ -0,0 +1,22 @@
1
+ import { type ChartController, type ChartTextMeasure, type ChartTheme } from '@pixerate/editor/chart';
2
+ interface Props {
3
+ controller: ChartController;
4
+ /** Scene width; with `responsive` this is only the fallback before the container is measured (default 640). */
5
+ width?: number;
6
+ /** Scene height in pixels (default 400). */
7
+ height?: number;
8
+ /** Follow the container's width (default `true`). */
9
+ responsive?: boolean;
10
+ /** Colours and type (default: shadcn-style CSS variables). */
11
+ theme?: Partial<ChartTheme>;
12
+ /** Click to select, drag bars and points, keyboard editing (default `true`). */
13
+ editable?: boolean;
14
+ /** Text width for layout (default: an estimate). */
15
+ measureText?: ChartTextMeasure;
16
+ emptyMessage?: string;
17
+ class?: string;
18
+ }
19
+ declare const ChartCanvas: import("svelte").Component<Props, {}, "">;
20
+ type ChartCanvas = ReturnType<typeof ChartCanvas>;
21
+ export default ChartCanvas;
22
+ //# sourceMappingURL=ChartCanvas.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartCanvas.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartCanvas.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EASH,KAAK,eAAe,EAGpB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAC;AAIhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,+GAA+G;IAC/G,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oDAAoD;IACpD,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAmGH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,56 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { chartInputClass } from './styles.js';
4
+
5
+ interface Props extends Omit<HTMLInputAttributes, 'value' | 'class'> {
6
+ value: string;
7
+ /** Called with the typed text. Return `false` to show the stored value again (a refused edit). */
8
+ oncommit: (value: string) => boolean | void;
9
+ class?: string;
10
+ }
11
+
12
+ let { value, oncommit, class: className = chartInputClass, onkeydown, onblur, ...rest }: Props = $props();
13
+
14
+ // Follows `value` unless the user is typing.
15
+ let draft = $state('');
16
+ let typing = false;
17
+ $effect.pre(() => {
18
+ const next = value;
19
+ if (!typing) draft = next;
20
+ });
21
+
22
+ function commit() {
23
+ if (!typing) return;
24
+ typing = false;
25
+ if (draft === value) return;
26
+ if (oncommit(draft) === false) draft = value;
27
+ }
28
+
29
+ function keydown(e: KeyboardEvent & { currentTarget: HTMLInputElement }) {
30
+ (onkeydown as ((e: KeyboardEvent) => void) | undefined)?.(e);
31
+ if (e.defaultPrevented) return;
32
+ if (e.key === 'Enter') {
33
+ e.preventDefault();
34
+ commit();
35
+ } else if (e.key === 'Escape') {
36
+ typing = false;
37
+ draft = value;
38
+ }
39
+ }
40
+ </script>
41
+
42
+ <input
43
+ type="text"
44
+ {...rest}
45
+ class={className}
46
+ value={draft}
47
+ oninput={(e) => {
48
+ typing = true;
49
+ draft = e.currentTarget.value;
50
+ }}
51
+ onblur={(e) => {
52
+ (onblur as ((e: FocusEvent) => void) | undefined)?.(e);
53
+ commit();
54
+ }}
55
+ onkeydown={keydown}
56
+ />
@@ -0,0 +1,11 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ interface Props extends Omit<HTMLInputAttributes, 'value' | 'class'> {
3
+ value: string;
4
+ /** Called with the typed text. Return `false` to show the stored value again (a refused edit). */
5
+ oncommit: (value: string) => boolean | void;
6
+ class?: string;
7
+ }
8
+ declare const ChartCommitInput: import("svelte").Component<Props, {}, "">;
9
+ type ChartCommitInput = ReturnType<typeof ChartCommitInput>;
10
+ export default ChartCommitInput;
11
+ //# sourceMappingURL=ChartCommitInput.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartCommitInput.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartCommitInput.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAIzD,UAAU,KAAM,SAAQ,IAAI,CAAC,mBAAmB,EAAE,OAAO,GAAG,OAAO,CAAC;IAClE,KAAK,EAAE,MAAM,CAAC;IACd,kGAAkG;IAClG,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8CH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,126 @@
1
+ <script lang="ts">
2
+ import { chartColorInputValue, chartSeriesColor, formatChartNumber, type ChartController } from '@pixerate/editor/chart';
3
+ import ChartCommitInput from './ChartCommitInput.svelte';
4
+ import { chartStateOf } from './createChartEditor.svelte.js';
5
+ import { chartButtonClass, chartCellClass, chartIconButtonClass, chartInputClass } from './styles.js';
6
+
7
+ interface Props {
8
+ controller: ChartController;
9
+ /** Header of the category column (default "Category"). */
10
+ categoryHeader?: string;
11
+ /** Hide the add / delete buttons. */
12
+ readOnly?: boolean;
13
+ class?: string;
14
+ }
15
+
16
+ let { controller, categoryHeader = 'Category', readOnly = false, class: className = '' }: Props = $props();
17
+
18
+ const view = chartStateOf(() => controller);
19
+ const spec = $derived(view.current.spec);
20
+ const selection = $derived(view.current.selection);
21
+
22
+ const cellText = (value: number | null | undefined) => (value === null || value === undefined ? '' : String(value));
23
+
24
+ // Pasting TSV / CSV fills and grows the grid from the cell.
25
+ const paste = (row: number, column: number) => (e: ClipboardEvent) => {
26
+ const text = e.clipboardData?.getData('text/plain') ?? '';
27
+ if (!/[\t\n]/.test(text.replace(/\n$/, ''))) return;
28
+ e.preventDefault();
29
+ controller.pasteCells(row, column, text);
30
+ };
31
+ </script>
32
+
33
+ <div class="overflow-auto rounded-md border border-border {className}" data-chart-grid="">
34
+ <table class="w-full border-collapse text-xs">
35
+ <thead class="bg-muted/50">
36
+ <tr>
37
+ <th class="border-b border-r border-border px-2 py-1 text-left font-medium text-muted-foreground">{categoryHeader}</th>
38
+ {#each spec.series as s, si (s.id)}
39
+ {@const selected = selection?.kind === 'series' && selection.seriesId === s.id}
40
+ <th data-series-id={s.id} class="border-b border-r border-border px-1 py-1 font-medium {selected ? 'bg-primary/10' : ''}">
41
+ <div class="flex items-center gap-1">
42
+ <input
43
+ type="color"
44
+ aria-label="{s.name} colour"
45
+ value={chartColorInputValue(chartSeriesColor(spec, si))}
46
+ disabled={readOnly}
47
+ onpointerdown={() => controller.beginGesture(`color:${s.id}`)}
48
+ oninput={(e) => controller.updateSeries(s.id, { color: e.currentTarget.value })}
49
+ onblur={() => controller.endGesture()}
50
+ class="h-5 w-5 shrink-0 cursor-pointer rounded border-0 bg-transparent p-0"
51
+ />
52
+ <ChartCommitInput
53
+ value={s.name}
54
+ aria-label="Series name"
55
+ onfocus={() => controller.select({ kind: 'series', seriesId: s.id })}
56
+ oncommit={(name) => void controller.updateSeries(s.id, { name })}
57
+ class="{chartInputClass} h-6 min-w-[5rem] border-transparent bg-transparent font-medium {s.hidden ? 'text-muted-foreground line-through' : ''}"
58
+ />
59
+ {#if !readOnly}
60
+ <button
61
+ type="button"
62
+ class={chartIconButtonClass}
63
+ title={s.hidden ? 'Show series' : 'Hide series'}
64
+ aria-label={s.hidden ? `Show ${s.name}` : `Hide ${s.name}`}
65
+ aria-pressed={!s.hidden}
66
+ data-action="toggle-series"
67
+ onclick={() => controller.toggleSeries(s.id)}>{s.hidden ? '○' : '●'}</button
68
+ >
69
+ <button type="button" class={chartIconButtonClass} title="Delete series" aria-label="Delete {s.name}" data-action="remove-series" onclick={() => controller.removeSeries(s.id)}>×</button>
70
+ {/if}
71
+ </div>
72
+ </th>
73
+ {/each}
74
+ {#if !readOnly}
75
+ <th class="w-8 border-b border-border px-1">
76
+ <button type="button" class={chartIconButtonClass} title="Add series" aria-label="Add series" data-action="add-series" onclick={() => controller.addSeries()}>+</button>
77
+ </th>
78
+ {/if}
79
+ </tr>
80
+ </thead>
81
+ <tbody>
82
+ {#each spec.categories as category, i (i)}
83
+ {@const rowSelected = selection?.kind === 'category' && selection.index === i}
84
+ <tr data-category-index={i} class={rowSelected ? 'bg-primary/10' : undefined}>
85
+ <th class="border-b border-r border-border p-0 text-left font-normal">
86
+ <ChartCommitInput
87
+ value={category}
88
+ aria-label="Category {i + 1}"
89
+ onfocus={() => controller.select({ kind: 'category', index: i })}
90
+ onpaste={paste(i, -1)}
91
+ oncommit={(label) => void controller.renameCategory(i, label)}
92
+ class="{chartCellClass} text-left"
93
+ />
94
+ </th>
95
+ {#each spec.series as s, si (s.id)}
96
+ {@const value = s.values[i]}
97
+ {@const selected = selection?.kind === 'point' && selection.seriesId === s.id && selection.index === i}
98
+ <td class="border-b border-r border-border p-0 {selected ? 'bg-primary/10' : ''}">
99
+ <ChartCommitInput
100
+ value={cellText(value)}
101
+ inputmode="decimal"
102
+ aria-label="{s.name}, {category}"
103
+ title={value === null ? undefined : formatChartNumber(value, spec.yAxis.format)}
104
+ data-cell="{s.id}:{i}"
105
+ onfocus={() => controller.select({ kind: 'point', seriesId: s.id, index: i })}
106
+ onpaste={paste(i, si)}
107
+ oncommit={(text) => controller.setValue(s.id, i, text).ok}
108
+ class={chartCellClass}
109
+ />
110
+ </td>
111
+ {/each}
112
+ {#if !readOnly}
113
+ <td class="border-b border-border px-1">
114
+ <button type="button" class={chartIconButtonClass} title="Delete category" aria-label="Delete {category}" data-action="remove-category" onclick={() => controller.removeCategory(i)}>×</button>
115
+ </td>
116
+ {/if}
117
+ </tr>
118
+ {/each}
119
+ </tbody>
120
+ </table>
121
+ {#if !readOnly}
122
+ <div class="p-1">
123
+ <button type="button" class="{chartButtonClass} h-6 border-dashed" data-action="add-category" onclick={() => controller.addCategory()}>+ Add category</button>
124
+ </div>
125
+ {/if}
126
+ </div>
@@ -0,0 +1,13 @@
1
+ import { type ChartController } from '@pixerate/editor/chart';
2
+ interface Props {
3
+ controller: ChartController;
4
+ /** Header of the category column (default "Category"). */
5
+ categoryHeader?: string;
6
+ /** Hide the add / delete buttons. */
7
+ readOnly?: boolean;
8
+ class?: string;
9
+ }
10
+ declare const ChartDataGrid: import("svelte").Component<Props, {}, "">;
11
+ type ChartDataGrid = ReturnType<typeof ChartDataGrid>;
12
+ export default ChartDataGrid;
13
+ //# sourceMappingURL=ChartDataGrid.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartDataGrid.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartDataGrid.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA6D,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAMvH,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkFH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
@@ -0,0 +1,54 @@
1
+ <script lang="ts">
2
+ import type { ComponentProps } from 'svelte';
3
+ import { describeChartSelection, type ChartController } from '@pixerate/editor/chart';
4
+ import ChartCanvas from './ChartCanvas.svelte';
5
+ import ChartDataGrid from './ChartDataGrid.svelte';
6
+ import ChartInspector from './ChartInspector.svelte';
7
+ import { chartStateOf } from './createChartEditor.svelte.js';
8
+ import { chartButtonClass } from './styles.js';
9
+
10
+ interface Props {
11
+ controller: ChartController;
12
+ /** The settings sidebar (default `true`). */
13
+ showInspector?: boolean;
14
+ /** The data grid under the chart (default `true`). */
15
+ showDataGrid?: boolean;
16
+ /** Undo / redo and the selection readout (default `true`). */
17
+ showToolbar?: boolean;
18
+ /** Chart height in pixels (default 380). */
19
+ height?: number;
20
+ canvasProps?: Omit<ComponentProps<typeof ChartCanvas>, 'controller' | 'height'>;
21
+ class?: string;
22
+ }
23
+
24
+ let { controller, showInspector = true, showDataGrid = true, showToolbar = true, height = 380, canvasProps = {}, class: className = '' }: Props = $props();
25
+
26
+ const view = chartStateOf(() => controller);
27
+ const state = $derived(view.current);
28
+ </script>
29
+
30
+ <div class="flex flex-col gap-2 {className}" data-chart-editor="">
31
+ {#if showToolbar}
32
+ <div class="flex items-center gap-1" data-chart-toolbar="">
33
+ <button type="button" class={chartButtonClass} disabled={!state.canUndo} title={state.undoLabel ? `Undo ${state.undoLabel}` : 'Undo'} data-action="undo" onclick={() => controller.undo()}>Undo</button>
34
+ <button type="button" class={chartButtonClass} disabled={!state.canRedo} title={state.redoLabel ? `Redo ${state.redoLabel}` : 'Redo'} data-action="redo" onclick={() => controller.redo()}>Redo</button>
35
+ <span class="ml-2 truncate text-xs text-muted-foreground" data-chart-selection="" aria-live="polite">{describeChartSelection(controller, state.selection)}</span>
36
+ </div>
37
+ {/if}
38
+ {#if state.issues.length > 0}
39
+ <p role="alert" class="rounded-md border border-destructive/40 bg-destructive/10 px-2 py-1 text-xs text-destructive" data-chart-issues="">
40
+ {state.issues.map((i) => i.message).join(' ')}
41
+ </p>
42
+ {/if}
43
+ <div class="flex min-h-0 flex-col gap-2 md:flex-row">
44
+ <div class="min-w-0 flex-1 rounded-md border border-border bg-background p-1">
45
+ <ChartCanvas {controller} {height} {...canvasProps} />
46
+ </div>
47
+ {#if showInspector}
48
+ <ChartInspector {controller} class="w-full shrink-0 overflow-y-auto rounded-md border border-border p-3 md:w-64" />
49
+ {/if}
50
+ </div>
51
+ {#if showDataGrid}
52
+ <ChartDataGrid {controller} />
53
+ {/if}
54
+ </div>
@@ -0,0 +1,20 @@
1
+ import type { ComponentProps } from 'svelte';
2
+ import { type ChartController } from '@pixerate/editor/chart';
3
+ import ChartCanvas from './ChartCanvas.svelte';
4
+ interface Props {
5
+ controller: ChartController;
6
+ /** The settings sidebar (default `true`). */
7
+ showInspector?: boolean;
8
+ /** The data grid under the chart (default `true`). */
9
+ showDataGrid?: boolean;
10
+ /** Undo / redo and the selection readout (default `true`). */
11
+ showToolbar?: boolean;
12
+ /** Chart height in pixels (default 380). */
13
+ height?: number;
14
+ canvasProps?: Omit<ComponentProps<typeof ChartCanvas>, 'controller' | 'height'>;
15
+ class?: string;
16
+ }
17
+ declare const ChartEditor: import("svelte").Component<Props, {}, "">;
18
+ type ChartEditor = ReturnType<typeof ChartEditor>;
19
+ export default ChartEditor;
20
+ //# sourceMappingURL=ChartEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartEditor.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACtF,OAAO,WAAW,MAAM,sBAAsB,CAAC;AAO7C,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,6CAA6C;IAC7C,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,sDAAsD;IACtD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC,OAAO,WAAW,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC,CAAC;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA6CH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,316 @@
1
+ <script lang="ts" module>
2
+ // A counter keeps label ids unique per inspector. Pass `idPrefix` for ids that match
3
+ // between server and client renders.
4
+ let nextId = 0;
5
+
6
+ export type ChartInspectorSection = 'type' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import {
11
+ CHART_CURVES,
12
+ CHART_LEGEND_POSITIONS,
13
+ CHART_NUMBER_STYLES,
14
+ CHART_SERIES_TYPES,
15
+ CHART_STACKINGS,
16
+ CHART_TYPE_LABELS,
17
+ CHART_TYPES,
18
+ chartColorInputValue,
19
+ chartSeriesColor,
20
+ chartSeriesType,
21
+ isCartesianChart,
22
+ type ChartAxis,
23
+ type ChartController,
24
+ type ChartNumberFormat,
25
+ type ChartSeriesType
26
+ } from '@pixerate/editor/chart';
27
+ import ChartCommitInput from './ChartCommitInput.svelte';
28
+ import { chartStateOf } from './createChartEditor.svelte.js';
29
+ import { chartButtonClass, chartInputClass, chartLabelClass, chartSectionClass, chartSegmentClass } from './styles.js';
30
+
31
+ interface Props {
32
+ controller: ChartController;
33
+ /** Which sections to show (default: all that apply to the chart). */
34
+ sections?: readonly ChartInspectorSection[];
35
+ /** Prefix for the inspector's element ids. */
36
+ idPrefix?: string;
37
+ class?: string;
38
+ }
39
+
40
+ let { controller, sections, idPrefix = `pixerate-chart-${++nextId}`, class: className = '' }: Props = $props();
41
+
42
+ const view = chartStateOf(() => controller);
43
+ const spec = $derived(view.current.spec);
44
+ const selectedSeries = $derived(view.current.selectedSeries);
45
+ const show = (section: ChartInspectorSection) => !sections || sections.includes(section);
46
+ const cartesian = $derived(isCartesianChart(spec.type));
47
+ const types = $derived(spec.series.map((s) => chartSeriesType(spec, s)));
48
+ const hasBars = $derived(cartesian && types.includes('bar'));
49
+ const hasLines = $derived(cartesian && types.some((t) => t === 'line' || t === 'area'));
50
+ const seriesIndex = $derived(selectedSeries ? spec.series.findIndex((s) => s.id === selectedSeries.id) : -1);
51
+ const horizontal = $derived(spec.orientation === 'horizontal');
52
+ const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
53
+ const stackingLabels: Record<string, string> = { none: 'None', stacked: 'Stacked', percent: '100%' };
54
+
55
+ const setAxis = (axis: 'x' | 'y', patch: Partial<ChartAxis>) => controller.setAxis(axis, patch);
56
+ const setFormat = (patch: Partial<ChartNumberFormat>) => setAxis('y', { format: { ...spec.yAxis.format, ...patch } });
57
+ const commitBound = (key: 'min' | 'max') => (text: string) => setAxis('y', { [key]: text.trim() === '' ? null : Number(text) }).ok;
58
+ function commitDecimals(text: string) {
59
+ const format = { ...spec.yAxis.format };
60
+ if (text.trim() === '') delete format.decimals;
61
+ else format.decimals = Number(text);
62
+ return setAxis('y', { format }).ok;
63
+ }
64
+ </script>
65
+
66
+ {#snippet toggle(label: string, name: string, checked: boolean, onchange: (checked: boolean) => void)}
67
+ <label class="flex cursor-pointer items-center gap-2 text-xs text-foreground">
68
+ <input type="checkbox" class="h-3.5 w-3.5 accent-primary" {checked} data-option={name} onchange={(e) => onchange(e.currentTarget.checked)} />
69
+ {label}
70
+ </label>
71
+ {/snippet}
72
+
73
+ {#snippet axisSection(axis: 'x' | 'y', value: ChartAxis)}
74
+ <div class={chartSectionClass} data-section="axis-{axis}">
75
+ <div class="flex items-center justify-between">
76
+ <h4 class="text-xs font-semibold text-foreground">
77
+ {axis === 'x' ? 'Category axis' : 'Value axis'}
78
+ <span class="ml-1 font-normal text-muted-foreground">({axis === 'x' ? (horizontal ? 'left' : 'bottom') : horizontal ? 'bottom' : 'left'})</span>
79
+ </h4>
80
+ {@render toggle('Show', `${axis}Axis.show`, value.show, (v) => setAxis(axis, { show: v }))}
81
+ </div>
82
+ <div class="flex items-center gap-2">
83
+ <label for="{idPrefix}-{axis}-title" class="{chartLabelClass} w-20 shrink-0">Title</label>
84
+ <div class="min-w-0 flex-1">
85
+ <ChartCommitInput id="{idPrefix}-{axis}-title" value={value.title} data-option="{axis}Axis.title" oncommit={(title) => void setAxis(axis, { title })} />
86
+ </div>
87
+ </div>
88
+ {@render toggle('Grid lines', `${axis}Axis.gridlines`, value.gridlines, (v) => setAxis(axis, { gridlines: v }))}
89
+ {#if axis === 'y'}
90
+ <div class="flex items-center gap-2">
91
+ <span class="{chartLabelClass} w-20 shrink-0">Min / max</span>
92
+ <div class="flex min-w-0 flex-1 gap-1">
93
+ <ChartCommitInput id="{idPrefix}-y-min" value={value.min === null ? '' : String(value.min)} placeholder="Auto" inputmode="decimal" data-option="yAxis.min" oncommit={commitBound('min')} />
94
+ <ChartCommitInput id="{idPrefix}-y-max" value={value.max === null ? '' : String(value.max)} placeholder="Auto" inputmode="decimal" data-option="yAxis.max" oncommit={commitBound('max')} />
95
+ </div>
96
+ </div>
97
+ <div class="flex items-center gap-2">
98
+ <label for="{idPrefix}-y-format" class="{chartLabelClass} w-20 shrink-0">Format</label>
99
+ <div class="flex min-w-0 flex-1 gap-1">
100
+ <select
101
+ id="{idPrefix}-y-format"
102
+ class={chartInputClass}
103
+ value={value.format.style ?? 'number'}
104
+ data-option="yAxis.format.style"
105
+ onchange={(e) => setFormat({ style: e.currentTarget.value as ChartNumberFormat['style'] })}
106
+ >
107
+ {#each CHART_NUMBER_STYLES as style (style)}
108
+ <option value={style}>{cap(style)}</option>
109
+ {/each}
110
+ </select>
111
+ <ChartCommitInput
112
+ value={value.format.decimals === undefined ? '' : String(value.format.decimals)}
113
+ placeholder="Auto"
114
+ aria-label="Decimals"
115
+ title="Decimals"
116
+ inputmode="numeric"
117
+ data-option="yAxis.format.decimals"
118
+ class="{chartInputClass} w-14 shrink-0"
119
+ oncommit={commitDecimals}
120
+ />
121
+ </div>
122
+ </div>
123
+ {#if value.format.style === 'currency'}
124
+ <div class="flex items-center gap-2">
125
+ <label for="{idPrefix}-y-currency" class="{chartLabelClass} w-20 shrink-0">Currency</label>
126
+ <div class="min-w-0 flex-1">
127
+ <ChartCommitInput id="{idPrefix}-y-currency" value={value.format.currency ?? 'USD'} data-option="yAxis.format.currency" oncommit={(c) => void setFormat({ currency: c.toUpperCase() })} />
128
+ </div>
129
+ </div>
130
+ {/if}
131
+ {/if}
132
+ </div>
133
+ {/snippet}
134
+
135
+ <div class="space-y-3 text-xs {className}" data-chart-inspector="">
136
+ {#if show('type')}
137
+ <div class={chartSectionClass} data-section="type">
138
+ <div class="grid grid-cols-3 gap-0.5 rounded-md bg-muted p-0.5" role="radiogroup" aria-label="Chart type">
139
+ {#each CHART_TYPES as type (type)}
140
+ <button type="button" role="radio" aria-checked={spec.type === type} data-chart-type={type} class={chartSegmentClass(spec.type === type)} onclick={() => controller.setType(type)}>
141
+ {CHART_TYPE_LABELS[type]}
142
+ </button>
143
+ {/each}
144
+ </div>
145
+ </div>
146
+ {/if}
147
+
148
+ {#if show('titles')}
149
+ <div class={chartSectionClass} data-section="titles">
150
+ <div class="flex items-center gap-2">
151
+ <label for="{idPrefix}-title" class="{chartLabelClass} w-20 shrink-0">Title</label>
152
+ <div class="min-w-0 flex-1">
153
+ <ChartCommitInput id="{idPrefix}-title" value={spec.title} data-option="title" oncommit={(title) => void controller.setTitle(title)} />
154
+ </div>
155
+ </div>
156
+ <div class="flex items-center gap-2">
157
+ <label for="{idPrefix}-subtitle" class="{chartLabelClass} w-20 shrink-0">Subtitle</label>
158
+ <div class="min-w-0 flex-1">
159
+ <ChartCommitInput id="{idPrefix}-subtitle" value={spec.subtitle} data-option="subtitle" oncommit={(subtitle) => void controller.setSubtitle(subtitle)} />
160
+ </div>
161
+ </div>
162
+ </div>
163
+ {/if}
164
+
165
+ {#if show('options')}
166
+ <div class={chartSectionClass} data-section="options">
167
+ {#if cartesian}
168
+ <div class="flex items-center gap-2">
169
+ <label for="{idPrefix}-stacking" class="{chartLabelClass} w-20 shrink-0">Stacking</label>
170
+ <select
171
+ id="{idPrefix}-stacking"
172
+ class={chartInputClass}
173
+ value={spec.stacking}
174
+ data-option="stacking"
175
+ onchange={(e) => controller.update({ stacking: e.currentTarget.value as (typeof CHART_STACKINGS)[number] })}
176
+ >
177
+ {#each CHART_STACKINGS as s (s)}
178
+ <option value={s}>{stackingLabels[s]}</option>
179
+ {/each}
180
+ </select>
181
+ </div>
182
+ {/if}
183
+ {#if hasBars}
184
+ <div class="flex items-center gap-2">
185
+ <span class="{chartLabelClass} w-20 shrink-0">Bars</span>
186
+ <div class="flex min-w-0 flex-1 gap-0.5 rounded-md bg-muted p-0.5">
187
+ {#each ['vertical', 'horizontal'] as const as o (o)}
188
+ <button type="button" aria-pressed={spec.orientation === o} data-option="orientation.{o}" class={chartSegmentClass(spec.orientation === o)} onclick={() => controller.update({ orientation: o })}>
189
+ {cap(o)}
190
+ </button>
191
+ {/each}
192
+ </div>
193
+ </div>
194
+ {/if}
195
+ {#if hasLines}
196
+ <div class="flex items-center gap-2">
197
+ <label for="{idPrefix}-curve" class="{chartLabelClass} w-20 shrink-0">Lines</label>
198
+ <select id="{idPrefix}-curve" class={chartInputClass} value={spec.curve} data-option="curve" onchange={(e) => controller.update({ curve: e.currentTarget.value as (typeof CHART_CURVES)[number] })}>
199
+ {#each CHART_CURVES as c (c)}
200
+ <option value={c}>{cap(c)}</option>
201
+ {/each}
202
+ </select>
203
+ </div>
204
+ {/if}
205
+ {#if spec.type === 'donut'}
206
+ <div class="flex items-center gap-2">
207
+ <label for="{idPrefix}-hole" class="{chartLabelClass} w-20 shrink-0">Hole</label>
208
+ <input
209
+ id="{idPrefix}-hole"
210
+ type="range"
211
+ min="0"
212
+ max="0.9"
213
+ step="0.05"
214
+ value={spec.innerRadius}
215
+ data-option="innerRadius"
216
+ class="w-full accent-primary"
217
+ onpointerdown={() => controller.beginGesture('innerRadius')}
218
+ onpointerup={() => controller.endGesture()}
219
+ oninput={(e) => controller.update({ innerRadius: Number(e.currentTarget.value) })}
220
+ />
221
+ </div>
222
+ {/if}
223
+ <div class="flex flex-wrap gap-x-4 gap-y-1">
224
+ {#if hasLines}
225
+ {@render toggle('Points', 'points', spec.points, (points) => controller.update({ points }))}
226
+ {/if}
227
+ {@render toggle('Data labels', 'dataLabels', spec.dataLabels, (dataLabels) => controller.update({ dataLabels }))}
228
+ </div>
229
+ </div>
230
+ {/if}
231
+
232
+ {#if show('legend')}
233
+ <div class={chartSectionClass} data-section="legend">
234
+ <div class="flex items-center justify-between">
235
+ <h4 class="text-xs font-semibold text-foreground">Legend</h4>
236
+ {@render toggle('Show', 'legend.show', spec.legend.show, (v) => controller.setLegend({ show: v }))}
237
+ </div>
238
+ {#if spec.legend.show}
239
+ <div class="flex gap-0.5 rounded-md bg-muted p-0.5">
240
+ {#each CHART_LEGEND_POSITIONS as p (p)}
241
+ <button type="button" aria-pressed={spec.legend.position === p} data-option="legend.{p}" class={chartSegmentClass(spec.legend.position === p)} onclick={() => controller.setLegend({ position: p })}>
242
+ {cap(p)}
243
+ </button>
244
+ {/each}
245
+ </div>
246
+ {/if}
247
+ </div>
248
+ {/if}
249
+
250
+ {#if show('axes') && cartesian}
251
+ {@render axisSection('x', spec.xAxis)}
252
+ {@render axisSection('y', spec.yAxis)}
253
+ {/if}
254
+
255
+ {#if show('series') && selectedSeries}
256
+ {@const series = selectedSeries}
257
+ <div class={chartSectionClass} data-section="series" data-series-id={series.id}>
258
+ <h4 class="text-xs font-semibold text-foreground">Series</h4>
259
+ <div class="flex items-center gap-2">
260
+ <label for="{idPrefix}-series-name" class="{chartLabelClass} w-20 shrink-0">Name</label>
261
+ <div class="min-w-0 flex-1">
262
+ <ChartCommitInput id="{idPrefix}-series-name" value={series.name} data-option="series.name" oncommit={(name) => void controller.updateSeries(series.id, { name })} />
263
+ </div>
264
+ </div>
265
+ <div class="flex items-center gap-2">
266
+ <label for="{idPrefix}-series-color" class="{chartLabelClass} w-20 shrink-0">Colour</label>
267
+ <div class="flex min-w-0 flex-1 items-center gap-1">
268
+ <input
269
+ id="{idPrefix}-series-color"
270
+ type="color"
271
+ value={chartColorInputValue(chartSeriesColor(spec, seriesIndex))}
272
+ data-option="series.color"
273
+ class="h-7 w-9 shrink-0 cursor-pointer rounded border border-input bg-background p-0.5"
274
+ onpointerdown={() => controller.beginGesture(`color:${series.id}`)}
275
+ onblur={() => controller.endGesture()}
276
+ oninput={(e) => controller.updateSeries(series.id, { color: e.currentTarget.value })}
277
+ />
278
+ {#if series.color}
279
+ <button type="button" class={chartButtonClass} data-action="reset-color" onclick={() => controller.updateSeries(series.id, { color: null })}>Palette</button>
280
+ {/if}
281
+ </div>
282
+ </div>
283
+ {#if cartesian}
284
+ <div class="flex items-center gap-2">
285
+ <label for="{idPrefix}-series-type" class="{chartLabelClass} w-20 shrink-0">Draw as</label>
286
+ <select
287
+ id="{idPrefix}-series-type"
288
+ class={chartInputClass}
289
+ value={series.type ?? ''}
290
+ data-option="series.type"
291
+ onchange={(e) => controller.updateSeries(series.id, { type: (e.currentTarget.value || null) as ChartSeriesType | null })}
292
+ >
293
+ <option value="">Chart type</option>
294
+ {#each CHART_SERIES_TYPES as t (t)}
295
+ <option value={t}>{CHART_TYPE_LABELS[t]}</option>
296
+ {/each}
297
+ </select>
298
+ </div>
299
+ {/if}
300
+ <div class="flex flex-wrap items-center gap-1">
301
+ {@render toggle('Hidden', 'series.hidden', !!series.hidden, (hidden) => controller.updateSeries(series.id, { hidden }))}
302
+ <span class="flex-1"></span>
303
+ <button type="button" class={chartButtonClass} disabled={seriesIndex <= 0} aria-label="Move series earlier" data-action="series-up" onclick={() => controller.moveSeries(series.id, seriesIndex - 1)}>↑</button>
304
+ <button
305
+ type="button"
306
+ class={chartButtonClass}
307
+ disabled={seriesIndex >= spec.series.length - 1}
308
+ aria-label="Move series later"
309
+ data-action="series-down"
310
+ onclick={() => controller.moveSeries(series.id, seriesIndex + 1)}>↓</button
311
+ >
312
+ <button type="button" class="{chartButtonClass} text-destructive" data-action="series-delete" onclick={() => controller.removeSeries(series.id)}>Delete</button>
313
+ </div>
314
+ </div>
315
+ {/if}
316
+ </div>
@@ -0,0 +1,14 @@
1
+ export type ChartInspectorSection = 'type' | 'titles' | 'options' | 'legend' | 'axes' | 'series';
2
+ import { type ChartController } from '@pixerate/editor/chart';
3
+ interface Props {
4
+ controller: ChartController;
5
+ /** Which sections to show (default: all that apply to the chart). */
6
+ sections?: readonly ChartInspectorSection[];
7
+ /** Prefix for the inspector's element ids. */
8
+ idPrefix?: string;
9
+ class?: string;
10
+ }
11
+ declare const ChartInspector: import("svelte").Component<Props, {}, "">;
12
+ type ChartInspector = ReturnType<typeof ChartInspector>;
13
+ export default ChartInspector;
14
+ //# sourceMappingURL=ChartInspector.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChartInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/ChartInspector.svelte.ts"],"names":[],"mappings":"AAOE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAGnG,OAAO,EAaH,KAAK,eAAe,EAGrB,MAAM,wBAAwB,CAAC;AAMhC,UAAU,KAAK;IACb,UAAU,EAAE,eAAe,CAAC;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC5C,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2OH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,38 @@
1
+ import { ChartController, type ChartActionResult, type ChartCommandOptions, type ChartControllerOptions, type ChartSelection, type ChartSpecPatch, type ChartState, type ChartType } from "@pixerate/editor/chart";
2
+ export interface SvelteChartEditor {
3
+ readonly controller: ChartController;
4
+ /** The controller's state (reactive). */
5
+ readonly state: ChartState;
6
+ /** Shorthand for `state.spec` (reactive). */
7
+ readonly spec: ChartState["spec"];
8
+ /** Shorthand for `state.selection` (reactive). */
9
+ readonly selection: ChartSelection | null;
10
+ update(patch: ChartSpecPatch): ChartActionResult;
11
+ setType(type: ChartType): ChartActionResult;
12
+ setValue(seriesId: string, index: number, value: unknown): ChartActionResult;
13
+ select(selection: ChartSelection | null): void;
14
+ undo(): void;
15
+ redo(): void;
16
+ /** Stops listening to the controller. */
17
+ destroy(): void;
18
+ }
19
+ /**
20
+ * Svelte 5 rune: subscribes to a `ChartController` (or creates one from options) and
21
+ * exposes its state reactively with bound actions.
22
+ */
23
+ export declare function createChartEditor(controllerOrOptions?: ChartController | ChartControllerOptions): SvelteChartEditor;
24
+ /** Reactive state of a (possibly replaced) controller prop, for use inside components. */
25
+ export declare function chartStateOf(getController: () => ChartController): {
26
+ readonly current: ChartState;
27
+ };
28
+ /** Anything with `register(specs) → unregister` (a `CommandRegistry`). */
29
+ export interface ChartCommandTarget {
30
+ register(specs: any): () => void;
31
+ }
32
+ /**
33
+ * Registers the controller's palette / agent commands (`chart.describe`, `chart.update`,
34
+ * `chart.setValues`, …) for the lifetime of the calling component (call it during
35
+ * initialisation). Pass a getter to follow a changing registry.
36
+ */
37
+ export declare function useChartCommands(registry: ChartCommandTarget | null | undefined | (() => ChartCommandTarget | null | undefined), controller: ChartController, options?: ChartCommandOptions): void;
38
+ //# sourceMappingURL=createChartEditor.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createChartEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/chart/createChartEditor.svelte.ts"],"names":[],"mappings":"AACA,OAAO,EACL,eAAe,EAEf,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,EACf,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAC;IACrC,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAC3B,6CAA6C;IAC7C,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;IAClC,kDAAkD;IAClD,QAAQ,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1C,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,iBAAiB,CAAC;IACjD,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,iBAAiB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,iBAAiB,CAAC;IAC7E,MAAM,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/C,IAAI,IAAI,IAAI,CAAC;IACb,IAAI,IAAI,IAAI,CAAC;IACb,yCAAyC;IACzC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,mBAAmB,GAAE,eAAe,GAAG,sBAA2B,GAAG,iBAAiB,CAyBvH;AAED,0FAA0F;AAC1F,wBAAgB,YAAY,CAAC,aAAa,EAAE,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAgBnG;AAED,0EAA0E;AAC1E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,MAAM,IAAI,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,kBAAkB,GAAG,IAAI,GAAG,SAAS,GAAG,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,SAAS,CAAC,EAC/F,UAAU,EAAE,eAAe,EAC3B,OAAO,GAAE,mBAAwB,GAChC,IAAI,CAMN"}
@@ -0,0 +1,61 @@
1
+ import { untrack } from "svelte";
2
+ import { ChartController, createChartCommands, } from "@pixerate/editor/chart";
3
+ /**
4
+ * Svelte 5 rune: subscribes to a `ChartController` (or creates one from options) and
5
+ * exposes its state reactively with bound actions.
6
+ */
7
+ export function createChartEditor(controllerOrOptions = {}) {
8
+ const controller = controllerOrOptions instanceof ChartController ? controllerOrOptions : new ChartController(controllerOrOptions);
9
+ let state = $state.raw(controller.getState());
10
+ const unsubscribe = controller.subscribe((next) => {
11
+ state = next;
12
+ });
13
+ return {
14
+ controller,
15
+ get state() {
16
+ return state;
17
+ },
18
+ get spec() {
19
+ return state.spec;
20
+ },
21
+ get selection() {
22
+ return state.selection;
23
+ },
24
+ update: (patch) => controller.update(patch),
25
+ setType: (type) => controller.setType(type),
26
+ setValue: (seriesId, index, value) => controller.setValue(seriesId, index, value),
27
+ select: (selection) => controller.select(selection),
28
+ undo: () => void controller.undo(),
29
+ redo: () => void controller.redo(),
30
+ destroy: unsubscribe,
31
+ };
32
+ }
33
+ /** Reactive state of a (possibly replaced) controller prop, for use inside components. */
34
+ export function chartStateOf(getController) {
35
+ let view = $state.raw(untrack(() => getController().getState()));
36
+ $effect(() => {
37
+ const controller = getController();
38
+ view = controller.getState();
39
+ return untrack(() => controller.subscribe((next) => {
40
+ view = next;
41
+ }));
42
+ });
43
+ return {
44
+ get current() {
45
+ return view;
46
+ },
47
+ };
48
+ }
49
+ /**
50
+ * Registers the controller's palette / agent commands (`chart.describe`, `chart.update`,
51
+ * `chart.setValues`, …) for the lifetime of the calling component (call it during
52
+ * initialisation). Pass a getter to follow a changing registry.
53
+ */
54
+ export function useChartCommands(registry, controller, options = {}) {
55
+ $effect(() => {
56
+ const target = typeof registry === "function" ? registry() : registry;
57
+ if (!target)
58
+ return;
59
+ return target.register(createChartCommands(controller, options));
60
+ });
61
+ }
@@ -0,0 +1,18 @@
1
+ import type { ComponentProps } from "svelte";
2
+ import type ChartCanvasComponent from "./ChartCanvas.svelte";
3
+ import type ChartDataGridComponent from "./ChartDataGrid.svelte";
4
+ import type ChartEditorComponent from "./ChartEditor.svelte";
5
+ import type ChartInspectorComponent from "./ChartInspector.svelte";
6
+ export { default as ChartEditor } from "./ChartEditor.svelte";
7
+ export { default as ChartCanvas } from "./ChartCanvas.svelte";
8
+ export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
9
+ export { default as ChartInspector } from "./ChartInspector.svelte";
10
+ export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
11
+ export type { ChartInspectorSection } from "./ChartInspector.svelte";
12
+ export * from "./createChartEditor.svelte.js";
13
+ export * from "./styles.js";
14
+ export type ChartCanvasProps = ComponentProps<typeof ChartCanvasComponent>;
15
+ export type ChartDataGridProps = ComponentProps<typeof ChartDataGridComponent>;
16
+ export type ChartEditorProps = ComponentProps<typeof ChartEditorComponent>;
17
+ export type ChartInspectorProps = ComponentProps<typeof ChartInspectorComponent>;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/chart/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,sBAAsB,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,oBAAoB,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,uBAAuB,MAAM,yBAAyB,CAAC;AAEnE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,YAAY,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACrE,cAAc,+BAA+B,CAAC;AAC9C,cAAc,aAAa,CAAC;AAE5B,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAC3E,MAAM,MAAM,mBAAmB,GAAG,cAAc,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ export { default as ChartEditor } from "./ChartEditor.svelte";
2
+ export { default as ChartCanvas } from "./ChartCanvas.svelte";
3
+ export { default as ChartDataGrid } from "./ChartDataGrid.svelte";
4
+ export { default as ChartInspector } from "./ChartInspector.svelte";
5
+ export { default as ChartCommitInput } from "./ChartCommitInput.svelte";
6
+ export * from "./createChartEditor.svelte.js";
7
+ export * from "./styles.js";
@@ -0,0 +1,8 @@
1
+ export declare const 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";
2
+ export declare const 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";
3
+ export declare const 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";
4
+ export declare const chartSegmentClass: (active: boolean) => string;
5
+ export declare const chartLabelClass = "text-[11px] font-medium text-muted-foreground";
6
+ export declare const chartSectionClass = "space-y-2 border-b border-border pb-3 last:border-b-0";
7
+ export declare const 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";
8
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/chart/styles.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,eAAe,4NAC+L,CAAC;AAC5N,eAAO,MAAM,gBAAgB,2QAC6O,CAAC;AAC3Q,eAAO,MAAM,oBAAoB,oQACkO,CAAC;AACpQ,eAAO,MAAM,iBAAiB,GAAI,QAAQ,OAAO,WAG7C,CAAC;AACL,eAAO,MAAM,eAAe,kDAAkD,CAAC;AAC/E,eAAO,MAAM,iBAAiB,0DAA0D,CAAC;AACzF,eAAO,MAAM,cAAc,kOACsM,CAAC"}
@@ -0,0 +1,8 @@
1
+ // Class names shared by the chart components (the same as the React package's).
2
+ export const 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";
3
+ export const 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";
4
+ export const 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";
5
+ export const 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"}`;
6
+ export const chartLabelClass = "text-[11px] font-medium text-muted-foreground";
7
+ export const chartSectionClass = "space-y-2 border-b border-border pb-3 last:border-b-0";
8
+ export const 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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor-svelte",
3
- "version": "0.41.0",
3
+ "version": "0.42.0",
4
4
  "description": "Svelte bindings and prompt/rich-text components for @pixerate/editor",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -63,6 +63,11 @@
63
63
  "svelte": "./dist/vector/index.js",
64
64
  "default": "./dist/vector/index.js"
65
65
  },
66
+ "./chart": {
67
+ "types": "./dist/chart/index.d.ts",
68
+ "svelte": "./dist/chart/index.js",
69
+ "default": "./dist/chart/index.js"
70
+ },
66
71
  "./package.json": "./package.json"
67
72
  },
68
73
  "typesVersions": {
@@ -93,6 +98,9 @@
93
98
  ],
94
99
  "vector": [
95
100
  "./dist/vector/index.d.ts"
101
+ ],
102
+ "chart": [
103
+ "./dist/chart/index.d.ts"
96
104
  ]
97
105
  }
98
106
  },
@@ -107,7 +115,7 @@
107
115
  "@tiptap/extension-bubble-menu": "^2.11.5",
108
116
  "@tiptap/extension-placeholder": "^2.11.5",
109
117
  "@tiptap/starter-kit": "^2.11.5",
110
- "@pixerate/editor": "0.44.0"
118
+ "@pixerate/editor": "0.46.0"
111
119
  },
112
120
  "peerDependencies": {
113
121
  "@dagrejs/dagre": "^1.1.4",