@grida/svg-editor 1.0.0-alpha.1

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.
@@ -0,0 +1,461 @@
1
+ import { SVGPathData, SVGPathDataTransformer } from "svg-pathdata";
2
+ //#region src/core/intents.ts
3
+ function num(doc, id, name, fallback = 0) {
4
+ const v = doc.get_attr(id, name);
5
+ if (v === null || v === "") return fallback;
6
+ const n = parseFloat(v);
7
+ return Number.isFinite(n) ? n : fallback;
8
+ }
9
+ function capture_translate_baseline(doc, id) {
10
+ const tag = doc.tag_of(id);
11
+ const own_transform = doc.get_attr(id, "transform");
12
+ if (own_transform !== null || tag === "g") return {
13
+ type: "viaTransform",
14
+ transform: own_transform
15
+ };
16
+ switch (tag) {
17
+ case "rect": return {
18
+ type: "rect",
19
+ x: num(doc, id, "x"),
20
+ y: num(doc, id, "y")
21
+ };
22
+ case "circle": return {
23
+ type: "circle",
24
+ cx: num(doc, id, "cx"),
25
+ cy: num(doc, id, "cy")
26
+ };
27
+ case "ellipse": return {
28
+ type: "ellipse",
29
+ cx: num(doc, id, "cx"),
30
+ cy: num(doc, id, "cy")
31
+ };
32
+ case "line": return {
33
+ type: "line",
34
+ x1: num(doc, id, "x1"),
35
+ y1: num(doc, id, "y1"),
36
+ x2: num(doc, id, "x2"),
37
+ y2: num(doc, id, "y2")
38
+ };
39
+ case "polyline": return {
40
+ type: "polyline",
41
+ points: doc.get_attr(id, "points") ?? ""
42
+ };
43
+ case "polygon": return {
44
+ type: "polygon",
45
+ points: doc.get_attr(id, "points") ?? ""
46
+ };
47
+ case "path": return {
48
+ type: "path",
49
+ d: doc.get_attr(id, "d") ?? ""
50
+ };
51
+ case "text": return {
52
+ type: "text",
53
+ x: num(doc, id, "x"),
54
+ y: num(doc, id, "y")
55
+ };
56
+ case "tspan": return {
57
+ type: "tspan",
58
+ x: num(doc, id, "x"),
59
+ y: num(doc, id, "y")
60
+ };
61
+ case "image": return {
62
+ type: "image",
63
+ x: num(doc, id, "x"),
64
+ y: num(doc, id, "y")
65
+ };
66
+ case "use": return {
67
+ type: "use",
68
+ x: num(doc, id, "x"),
69
+ y: num(doc, id, "y")
70
+ };
71
+ default: return { type: "unsupported" };
72
+ }
73
+ }
74
+ function shift_points_string(points, dx, dy) {
75
+ const nums = points.split(/[\s,]+/).filter(Boolean).map(Number);
76
+ const out = [];
77
+ for (let i = 0; i + 1 < nums.length; i += 2) out.push(`${nums[i] + dx},${nums[i + 1] + dy}`);
78
+ return out.join(" ");
79
+ }
80
+ function compose_leading_translate(existing, dx, dy) {
81
+ if (dx === 0 && dy === 0) return existing ? existing : null;
82
+ if (!existing) return `translate(${dx} ${dy})`;
83
+ const N = "[+-]?(?:\\d+\\.?\\d*|\\.\\d+)(?:[eE][+-]?\\d+)?";
84
+ const re = new RegExp(`^\\s*translate\\(\\s*(${N})(?:\\s*,\\s*|\\s+)(${N})\\s*\\)\\s*(.*)$`);
85
+ const m = existing.match(re);
86
+ if (m) {
87
+ const tx = parseFloat(m[1]) + dx;
88
+ const ty = parseFloat(m[2]) + dy;
89
+ const rest = m[3].trim();
90
+ return rest ? `translate(${tx} ${ty}) ${rest}` : `translate(${tx} ${ty})`;
91
+ }
92
+ return `translate(${dx} ${dy}) ${existing}`;
93
+ }
94
+ function shift_path_d(d, dx, dy) {
95
+ if (dx === 0 && dy === 0) return d;
96
+ try {
97
+ return new SVGPathData(d).transform(SVGPathDataTransformer.TRANSLATE(dx, dy)).encode();
98
+ } catch {
99
+ return d;
100
+ }
101
+ }
102
+ function apply_translate(doc, id, baseline, dx, dy) {
103
+ switch (baseline.type) {
104
+ case "viaTransform":
105
+ doc.set_attr(id, "transform", compose_leading_translate(baseline.transform ?? "", dx, dy));
106
+ return;
107
+ case "rect":
108
+ case "image":
109
+ case "use":
110
+ case "text":
111
+ case "tspan":
112
+ doc.set_attr(id, "x", String(baseline.x + dx));
113
+ doc.set_attr(id, "y", String(baseline.y + dy));
114
+ return;
115
+ case "circle":
116
+ case "ellipse":
117
+ doc.set_attr(id, "cx", String(baseline.cx + dx));
118
+ doc.set_attr(id, "cy", String(baseline.cy + dy));
119
+ return;
120
+ case "line":
121
+ doc.set_attr(id, "x1", String(baseline.x1 + dx));
122
+ doc.set_attr(id, "y1", String(baseline.y1 + dy));
123
+ doc.set_attr(id, "x2", String(baseline.x2 + dx));
124
+ doc.set_attr(id, "y2", String(baseline.y2 + dy));
125
+ return;
126
+ case "polyline":
127
+ case "polygon":
128
+ doc.set_attr(id, "points", shift_points_string(baseline.points, dx, dy));
129
+ return;
130
+ case "path":
131
+ doc.set_attr(id, "d", shift_path_d(baseline.d, dx, dy));
132
+ return;
133
+ case "unsupported": return;
134
+ }
135
+ }
136
+ function is_resizable(tag) {
137
+ switch (tag) {
138
+ case "rect":
139
+ case "image":
140
+ case "use":
141
+ case "circle":
142
+ case "ellipse":
143
+ case "line":
144
+ case "polyline":
145
+ case "polygon":
146
+ case "path":
147
+ case "text": return true;
148
+ default: return false;
149
+ }
150
+ }
151
+ function capture_resize_baseline(doc, id, bbox) {
152
+ const tag = doc.tag_of(id);
153
+ let attrs;
154
+ switch (tag) {
155
+ case "rect":
156
+ attrs = {
157
+ kind: "rect",
158
+ x: num(doc, id, "x"),
159
+ y: num(doc, id, "y"),
160
+ w: num(doc, id, "width", bbox.width),
161
+ h: num(doc, id, "height", bbox.height)
162
+ };
163
+ break;
164
+ case "image":
165
+ attrs = {
166
+ kind: "image",
167
+ x: num(doc, id, "x"),
168
+ y: num(doc, id, "y"),
169
+ w: num(doc, id, "width", bbox.width),
170
+ h: num(doc, id, "height", bbox.height)
171
+ };
172
+ break;
173
+ case "use":
174
+ attrs = {
175
+ kind: "use",
176
+ x: num(doc, id, "x"),
177
+ y: num(doc, id, "y"),
178
+ w: num(doc, id, "width", bbox.width),
179
+ h: num(doc, id, "height", bbox.height)
180
+ };
181
+ break;
182
+ case "circle":
183
+ attrs = {
184
+ kind: "circle",
185
+ cx: num(doc, id, "cx"),
186
+ cy: num(doc, id, "cy"),
187
+ r: num(doc, id, "r")
188
+ };
189
+ break;
190
+ case "ellipse":
191
+ attrs = {
192
+ kind: "ellipse",
193
+ cx: num(doc, id, "cx"),
194
+ cy: num(doc, id, "cy"),
195
+ rx: num(doc, id, "rx"),
196
+ ry: num(doc, id, "ry")
197
+ };
198
+ break;
199
+ case "line":
200
+ attrs = {
201
+ kind: "line",
202
+ x1: num(doc, id, "x1"),
203
+ y1: num(doc, id, "y1"),
204
+ x2: num(doc, id, "x2"),
205
+ y2: num(doc, id, "y2")
206
+ };
207
+ break;
208
+ case "polyline":
209
+ attrs = {
210
+ kind: "polyline",
211
+ points: doc.get_attr(id, "points") ?? ""
212
+ };
213
+ break;
214
+ case "polygon":
215
+ attrs = {
216
+ kind: "polygon",
217
+ points: doc.get_attr(id, "points") ?? ""
218
+ };
219
+ break;
220
+ case "path":
221
+ attrs = {
222
+ kind: "path",
223
+ d: doc.get_attr(id, "d") ?? ""
224
+ };
225
+ break;
226
+ case "text":
227
+ attrs = {
228
+ kind: "text",
229
+ x: num(doc, id, "x"),
230
+ y: num(doc, id, "y"),
231
+ fontSize: parseFloat(doc.get_attr(id, "font-size") ?? "16") || 16
232
+ };
233
+ break;
234
+ default: attrs = { kind: "unsupported" };
235
+ }
236
+ return {
237
+ bbox,
238
+ attrs
239
+ };
240
+ }
241
+ function compute_resize_factors(baseline, dir, dx, dy, shift) {
242
+ const b = baseline.bbox;
243
+ let anchorX = 0;
244
+ let anchorY = 0;
245
+ let baseHX = 0;
246
+ let baseHY = 0;
247
+ let affectsX = true;
248
+ let affectsY = true;
249
+ switch (dir) {
250
+ case "nw":
251
+ anchorX = b.x + b.width;
252
+ anchorY = b.y + b.height;
253
+ baseHX = b.x;
254
+ baseHY = b.y;
255
+ break;
256
+ case "n":
257
+ anchorX = b.x + b.width / 2;
258
+ anchorY = b.y + b.height;
259
+ baseHX = b.x + b.width / 2;
260
+ baseHY = b.y;
261
+ affectsX = false;
262
+ break;
263
+ case "ne":
264
+ anchorX = b.x;
265
+ anchorY = b.y + b.height;
266
+ baseHX = b.x + b.width;
267
+ baseHY = b.y;
268
+ break;
269
+ case "e":
270
+ anchorX = b.x;
271
+ anchorY = b.y + b.height / 2;
272
+ baseHX = b.x + b.width;
273
+ baseHY = b.y + b.height / 2;
274
+ affectsY = false;
275
+ break;
276
+ case "se":
277
+ anchorX = b.x;
278
+ anchorY = b.y;
279
+ baseHX = b.x + b.width;
280
+ baseHY = b.y + b.height;
281
+ break;
282
+ case "s":
283
+ anchorX = b.x + b.width / 2;
284
+ anchorY = b.y;
285
+ baseHX = b.x + b.width / 2;
286
+ baseHY = b.y + b.height;
287
+ affectsX = false;
288
+ break;
289
+ case "sw":
290
+ anchorX = b.x + b.width;
291
+ anchorY = b.y;
292
+ baseHX = b.x;
293
+ baseHY = b.y + b.height;
294
+ break;
295
+ case "w":
296
+ anchorX = b.x + b.width;
297
+ anchorY = b.y + b.height / 2;
298
+ baseHX = b.x;
299
+ baseHY = b.y + b.height / 2;
300
+ affectsY = false;
301
+ break;
302
+ }
303
+ const newHX = baseHX + (affectsX ? dx : 0);
304
+ const newHY = baseHY + (affectsY ? dy : 0);
305
+ const denomX = baseHX - anchorX;
306
+ const denomY = baseHY - anchorY;
307
+ let sx = affectsX && denomX !== 0 ? (newHX - anchorX) / denomX : 1;
308
+ let sy = affectsY && denomY !== 0 ? (newHY - anchorY) / denomY : 1;
309
+ if (shift && affectsX && affectsY) {
310
+ const mag = Math.max(Math.abs(sx), Math.abs(sy));
311
+ sx = sx >= 0 ? mag : -mag;
312
+ sy = sy >= 0 ? mag : -mag;
313
+ }
314
+ sx = Math.max(.001, sx);
315
+ sy = Math.max(.001, sy);
316
+ return {
317
+ sx,
318
+ sy,
319
+ origin: {
320
+ x: anchorX,
321
+ y: anchorY
322
+ }
323
+ };
324
+ }
325
+ function scale_points_string(points, origin, sx, sy) {
326
+ const nums = points.split(/[\s,]+/).filter(Boolean).map(Number);
327
+ const out = [];
328
+ for (let i = 0; i + 1 < nums.length; i += 2) {
329
+ const x = origin.x + (nums[i] - origin.x) * sx;
330
+ const y = origin.y + (nums[i + 1] - origin.y) * sy;
331
+ out.push(`${x},${y}`);
332
+ }
333
+ return out.join(" ");
334
+ }
335
+ function scale_path_d(d, origin, sx, sy) {
336
+ try {
337
+ const e = origin.x * (1 - sx);
338
+ const f = origin.y * (1 - sy);
339
+ return new SVGPathData(d).transform(SVGPathDataTransformer.MATRIX(sx, 0, 0, sy, e, f)).encode();
340
+ } catch {
341
+ return d;
342
+ }
343
+ }
344
+ function apply_resize(doc, id, baseline, sx, sy, origin) {
345
+ const a = baseline.attrs;
346
+ switch (a.kind) {
347
+ case "rect":
348
+ case "image":
349
+ case "use":
350
+ doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * sx));
351
+ doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * sy));
352
+ doc.set_attr(id, "width", String(Math.max(.001, a.w * sx)));
353
+ doc.set_attr(id, "height", String(Math.max(.001, a.h * sy)));
354
+ return;
355
+ case "circle": {
356
+ const s = Math.min(sx, sy);
357
+ doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * s));
358
+ doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * s));
359
+ doc.set_attr(id, "r", String(Math.max(.001, a.r * s)));
360
+ return;
361
+ }
362
+ case "ellipse":
363
+ doc.set_attr(id, "cx", String(origin.x + (a.cx - origin.x) * sx));
364
+ doc.set_attr(id, "cy", String(origin.y + (a.cy - origin.y) * sy));
365
+ doc.set_attr(id, "rx", String(Math.max(.001, a.rx * sx)));
366
+ doc.set_attr(id, "ry", String(Math.max(.001, a.ry * sy)));
367
+ return;
368
+ case "line":
369
+ doc.set_attr(id, "x1", String(origin.x + (a.x1 - origin.x) * sx));
370
+ doc.set_attr(id, "y1", String(origin.y + (a.y1 - origin.y) * sy));
371
+ doc.set_attr(id, "x2", String(origin.x + (a.x2 - origin.x) * sx));
372
+ doc.set_attr(id, "y2", String(origin.y + (a.y2 - origin.y) * sy));
373
+ return;
374
+ case "polyline":
375
+ case "polygon":
376
+ doc.set_attr(id, "points", scale_points_string(a.points, origin, sx, sy));
377
+ return;
378
+ case "path":
379
+ doc.set_attr(id, "d", scale_path_d(a.d, origin, sx, sy));
380
+ return;
381
+ case "text": {
382
+ if (!(sx !== 1 && sy !== 1)) return;
383
+ const s = Math.min(sx, sy);
384
+ doc.set_attr(id, "x", String(origin.x + (a.x - origin.x) * s));
385
+ doc.set_attr(id, "y", String(origin.y + (a.y - origin.y) * s));
386
+ doc.set_attr(id, "font-size", String(Math.max(1, a.fontSize * s)));
387
+ return;
388
+ }
389
+ case "unsupported": return;
390
+ }
391
+ }
392
+ //#endregion
393
+ //#region src/core/paint.ts
394
+ /**
395
+ * Parse a *computed* paint string into the discriminated union. Returns null
396
+ * for `inherit` / `var()` / empty. Returns an invalid-computed-value record
397
+ * for syntactic errors (rare; we're permissive).
398
+ */
399
+ function parse_paint(declared) {
400
+ if (declared === null || declared === "") return null;
401
+ const trimmed = declared.trim();
402
+ if (trimmed === "") return null;
403
+ if (trimmed === "inherit" || trimmed === "initial" || trimmed === "unset" || trimmed === "revert" || trimmed === "revert-layer") return null;
404
+ if (/^var\s*\(/i.test(trimmed)) return {
405
+ error: "invalid_at_computed_value_time",
406
+ reason: "var() substitution requires a cascade engine (not implemented)"
407
+ };
408
+ if (trimmed === "none") return { kind: "none" };
409
+ if (trimmed === "context-fill" || trimmed === "contextFill") return { kind: "context_fill" };
410
+ if (trimmed === "context-stroke" || trimmed === "contextStroke") return { kind: "context_stroke" };
411
+ const url_match = trimmed.match(/^url\(\s*(["']?)#([^)"']+)\1\s*\)\s*(.*)$/i);
412
+ if (url_match) {
413
+ const id = url_match[2];
414
+ const rest = url_match[3].trim();
415
+ let fallback;
416
+ if (rest !== "") {
417
+ const f = parse_paint(rest);
418
+ if (f && f.kind === "none") fallback = { kind: "none" };
419
+ else if (f && f.kind === "color") fallback = {
420
+ kind: "color",
421
+ value: f.value
422
+ };
423
+ }
424
+ return fallback ? {
425
+ kind: "ref",
426
+ id,
427
+ fallback
428
+ } : {
429
+ kind: "ref",
430
+ id
431
+ };
432
+ }
433
+ if (/^currentcolor$/i.test(trimmed)) return {
434
+ kind: "color",
435
+ value: { kind: "current_color" }
436
+ };
437
+ return {
438
+ kind: "color",
439
+ value: {
440
+ kind: "rgb",
441
+ value: trimmed
442
+ }
443
+ };
444
+ }
445
+ /** Serialize a Paint back to an SVG attribute / inline-style value. */
446
+ function serialize_paint(paint) {
447
+ switch (paint.kind) {
448
+ case "none": return "none";
449
+ case "context_fill": return "context-fill";
450
+ case "context_stroke": return "context-stroke";
451
+ case "color": return paint.value.kind === "current_color" ? "currentColor" : paint.value.value;
452
+ case "ref":
453
+ if (paint.fallback) {
454
+ const f = paint.fallback.kind === "none" ? "none" : paint.fallback.value.kind === "current_color" ? "currentColor" : paint.fallback.value.value;
455
+ return `url(#${paint.id}) ${f}`;
456
+ }
457
+ return `url(#${paint.id})`;
458
+ }
459
+ }
460
+ //#endregion
461
+ export { capture_resize_baseline as a, is_resizable as c, apply_translate as i, serialize_paint as n, capture_translate_baseline as o, apply_resize as r, compute_resize_factors as s, parse_paint as t };
@@ -0,0 +1,49 @@
1
+ import { d as EditorState, f as EditorStyle, k as Providers, o as SvgEditor, t as Commands } from "./editor-JY7AQrR1.mjs";
2
+ import { ReactNode } from "react";
3
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
4
+
5
+ //#region src/react.d.ts
6
+ type SvgEditorProviderProps = {
7
+ svg: string;
8
+ providers?: Providers;
9
+ style?: Partial<EditorStyle>;
10
+ children: ReactNode;
11
+ };
12
+ /**
13
+ * Owns the headless editor and exposes it via context. The editor is created
14
+ * once on first render; subsequent prop changes to `svg` call `editor.load()`.
15
+ */
16
+ declare function SvgEditorProvider({
17
+ svg,
18
+ providers,
19
+ style,
20
+ children
21
+ }: SvgEditorProviderProps): _$react_jsx_runtime0.JSX.Element;
22
+ type SvgEditorCanvasProps = {
23
+ className?: string;
24
+ style?: React.CSSProperties;
25
+ };
26
+ /**
27
+ * Renders the editor's SVG into a `div` and wires it to the DOM surface.
28
+ *
29
+ * Internally calls `attach_dom_surface(editor, { container })` on mount and
30
+ * `handle.detach()` on unmount. This is the only UI component the package
31
+ * ships; everything else (toolbar, property panel, etc.) is consumer-built.
32
+ */
33
+ declare function SvgEditorCanvas({
34
+ className,
35
+ style
36
+ }: SvgEditorCanvasProps): _$react_jsx_runtime0.JSX.Element;
37
+ declare function useSvgEditor(): SvgEditor;
38
+ /**
39
+ * Subscribe to a slice of `editor.state`. Re-renders when the selected slice
40
+ * changes by reference (or by the supplied `equals` function).
41
+ */
42
+ declare function useEditorState<T>(selector: (state: EditorState) => T, equals?: (a: T, b: T) => boolean): T;
43
+ /**
44
+ * Sugar for `useSvgEditor().commands`. The returned object is stable across
45
+ * re-renders (commands themselves don't change identity).
46
+ */
47
+ declare function useCommands(): Commands;
48
+ //#endregion
49
+ export { SvgEditorCanvas, SvgEditorCanvasProps, SvgEditorProvider, SvgEditorProviderProps, useCommands, useEditorState, useSvgEditor };
@@ -0,0 +1,49 @@
1
+ import { d as EditorState, f as EditorStyle, k as Providers, o as SvgEditor, t as Commands } from "./editor-CTtU2gu4.js";
2
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
3
+ import { ReactNode } from "react";
4
+
5
+ //#region src/react.d.ts
6
+ type SvgEditorProviderProps = {
7
+ svg: string;
8
+ providers?: Providers;
9
+ style?: Partial<EditorStyle>;
10
+ children: ReactNode;
11
+ };
12
+ /**
13
+ * Owns the headless editor and exposes it via context. The editor is created
14
+ * once on first render; subsequent prop changes to `svg` call `editor.load()`.
15
+ */
16
+ declare function SvgEditorProvider({
17
+ svg,
18
+ providers,
19
+ style,
20
+ children
21
+ }: SvgEditorProviderProps): _$react_jsx_runtime0.JSX.Element;
22
+ type SvgEditorCanvasProps = {
23
+ className?: string;
24
+ style?: React.CSSProperties;
25
+ };
26
+ /**
27
+ * Renders the editor's SVG into a `div` and wires it to the DOM surface.
28
+ *
29
+ * Internally calls `attach_dom_surface(editor, { container })` on mount and
30
+ * `handle.detach()` on unmount. This is the only UI component the package
31
+ * ships; everything else (toolbar, property panel, etc.) is consumer-built.
32
+ */
33
+ declare function SvgEditorCanvas({
34
+ className,
35
+ style
36
+ }: SvgEditorCanvasProps): _$react_jsx_runtime0.JSX.Element;
37
+ declare function useSvgEditor(): SvgEditor;
38
+ /**
39
+ * Subscribe to a slice of `editor.state`. Re-renders when the selected slice
40
+ * changes by reference (or by the supplied `equals` function).
41
+ */
42
+ declare function useEditorState<T>(selector: (state: EditorState) => T, equals?: (a: T, b: T) => boolean): T;
43
+ /**
44
+ * Sugar for `useSvgEditor().commands`. The returned object is stable across
45
+ * re-renders (commands themselves don't change identity).
46
+ */
47
+ declare function useCommands(): Commands;
48
+ //#endregion
49
+ export { SvgEditorCanvas, SvgEditorCanvasProps, SvgEditorProvider, SvgEditorProviderProps, useCommands, useEditorState, useSvgEditor };
package/dist/react.js ADDED
@@ -0,0 +1,97 @@
1
+ "use client";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const require_dom = require("./dom-CfP_ZURh.js");
4
+ const require_editor = require("./editor-DQWUWrVZ.js");
5
+ let react = require("react");
6
+ let react_jsx_runtime = require("react/jsx-runtime");
7
+ //#region src/react.tsx
8
+ const SvgEditorContext = (0, react.createContext)(null);
9
+ /**
10
+ * Owns the headless editor and exposes it via context. The editor is created
11
+ * once on first render; subsequent prop changes to `svg` call `editor.load()`.
12
+ */
13
+ function SvgEditorProvider({ svg, providers, style, children }) {
14
+ const editor_ref = (0, react.useRef)(null);
15
+ if (editor_ref.current === null) editor_ref.current = require_editor.createSvgEditor({
16
+ svg,
17
+ providers,
18
+ style
19
+ });
20
+ const editor = editor_ref.current;
21
+ const last_svg = (0, react.useRef)(svg);
22
+ (0, react.useEffect)(() => {
23
+ if (last_svg.current !== svg) {
24
+ editor.load(svg);
25
+ last_svg.current = svg;
26
+ }
27
+ }, [svg, editor]);
28
+ (0, react.useEffect)(() => {
29
+ return () => {
30
+ editor.dispose();
31
+ };
32
+ }, [editor]);
33
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SvgEditorContext.Provider, {
34
+ value: editor,
35
+ children
36
+ });
37
+ }
38
+ /**
39
+ * Renders the editor's SVG into a `div` and wires it to the DOM surface.
40
+ *
41
+ * Internally calls `attach_dom_surface(editor, { container })` on mount and
42
+ * `handle.detach()` on unmount. This is the only UI component the package
43
+ * ships; everything else (toolbar, property panel, etc.) is consumer-built.
44
+ */
45
+ function SvgEditorCanvas({ className, style }) {
46
+ const editor = useSvgEditor();
47
+ const ref = (0, react.useRef)(null);
48
+ (0, react.useEffect)(() => {
49
+ const container = ref.current;
50
+ if (!container) return;
51
+ const handle = require_dom.attach_dom_surface(editor, { container });
52
+ return () => handle.detach();
53
+ }, [editor]);
54
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55
+ ref,
56
+ className,
57
+ style
58
+ });
59
+ }
60
+ function useSvgEditor() {
61
+ const editor = (0, react.useContext)(SvgEditorContext);
62
+ if (editor === null) throw new Error("useSvgEditor must be used inside a <SvgEditorProvider>.");
63
+ return editor;
64
+ }
65
+ /**
66
+ * Subscribe to a slice of `editor.state`. Re-renders when the selected slice
67
+ * changes by reference (or by the supplied `equals` function).
68
+ */
69
+ function useEditorState(selector, equals = Object.is) {
70
+ const editor = useSvgEditor();
71
+ const last_ref = (0, react.useRef)({
72
+ has: false,
73
+ value: void 0
74
+ });
75
+ return (0, react.useSyncExternalStore)((cb) => editor.subscribe(cb), () => {
76
+ const next = selector(editor.state);
77
+ if (!last_ref.current.has || !equals(last_ref.current.value, next)) last_ref.current = {
78
+ has: true,
79
+ value: next
80
+ };
81
+ return last_ref.current.value;
82
+ }, () => selector(editor.state));
83
+ }
84
+ /**
85
+ * Sugar for `useSvgEditor().commands`. The returned object is stable across
86
+ * re-renders (commands themselves don't change identity).
87
+ */
88
+ function useCommands() {
89
+ const editor = useSvgEditor();
90
+ return (0, react.useMemo)(() => editor.commands, [editor]);
91
+ }
92
+ //#endregion
93
+ exports.SvgEditorCanvas = SvgEditorCanvas;
94
+ exports.SvgEditorProvider = SvgEditorProvider;
95
+ exports.useCommands = useCommands;
96
+ exports.useEditorState = useEditorState;
97
+ exports.useSvgEditor = useSvgEditor;