@grida/svg-editor 1.0.0-alpha.1 → 1.0.0-alpha.12

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/dist/react.js CHANGED
@@ -1,30 +1,27 @@
1
1
  "use client";
2
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");
3
+ require("./insertions-BJ-6o6o5.js");
4
+ const require_editor = require("./editor-CdyC3uAe.js");
5
+ const require_dom = require("./dom-Cvm9Towu.js");
5
6
  let react = require("react");
6
7
  let react_jsx_runtime = require("react/jsx-runtime");
7
8
  //#region src/react.tsx
8
9
  const SvgEditorContext = (0, react.createContext)(null);
9
10
  /**
10
11
  * 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
+ * once on first render with `initialSvg`; subsequent changes to that prop are
13
+ * silently ignored. To replace the document at runtime, call
14
+ * `useSvgEditor().load(...)` imperatively, or remount the provider with a
15
+ * different `key`.
12
16
  */
13
- function SvgEditorProvider({ svg, providers, style, children }) {
17
+ function SvgEditorProvider({ initialSvg, providers, style, children }) {
14
18
  const editor_ref = (0, react.useRef)(null);
15
19
  if (editor_ref.current === null) editor_ref.current = require_editor.createSvgEditor({
16
- svg,
20
+ svg: initialSvg,
17
21
  providers,
18
22
  style
19
23
  });
20
24
  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
25
  (0, react.useEffect)(() => {
29
26
  return () => {
30
27
  editor.dispose();
@@ -38,19 +35,38 @@ function SvgEditorProvider({ svg, providers, style, children }) {
38
35
  /**
39
36
  * Renders the editor's SVG into a `div` and wires it to the DOM surface.
40
37
  *
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.
38
+ * Internally calls `attach_dom_surface(editor, { container, ... })` on
39
+ * mount and `handle.detach()` on unmount. Surface-scoped concerns (camera,
40
+ * gestures) are reached via the `onAttach` callback — there is no global
41
+ * context for them, because a host may mount multiple canvases in the
42
+ * same editor session.
44
43
  */
45
- function SvgEditorCanvas({ className, style }) {
44
+ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAttach }) {
46
45
  const editor = useSvgEditor();
47
46
  const ref = (0, react.useRef)(null);
47
+ const on_attach_ref = (0, react.useRef)(onAttach);
48
+ on_attach_ref.current = onAttach;
49
+ const initial_camera_ref = (0, react.useRef)(initial_camera);
50
+ initial_camera_ref.current = initial_camera;
48
51
  (0, react.useEffect)(() => {
49
52
  const container = ref.current;
50
53
  if (!container) return;
51
- const handle = require_dom.attach_dom_surface(editor, { container });
52
- return () => handle.detach();
53
- }, [editor]);
54
+ const handle = require_dom.attach_dom_surface(editor, {
55
+ container,
56
+ gestures,
57
+ fit,
58
+ initial_camera: initial_camera_ref.current
59
+ });
60
+ on_attach_ref.current?.(handle);
61
+ return () => {
62
+ on_attach_ref.current?.(null);
63
+ handle.detach();
64
+ };
65
+ }, [
66
+ editor,
67
+ gestures,
68
+ fit
69
+ ]);
54
70
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
55
71
  ref,
56
72
  className,
@@ -89,9 +105,131 @@ function useCommands() {
89
105
  const editor = useSvgEditor();
90
106
  return (0, react.useMemo)(() => editor.commands, [editor]);
91
107
  }
108
+ /**
109
+ * Subscribe to a slice of `handle.camera` from a `DomSurfaceHandle`. Pass
110
+ * the handle (or null if it isn't attached yet) and a selector that reads
111
+ * what you need from the camera. The returned value updates on every
112
+ * camera mutation — does NOT bump `editor.state.version`.
113
+ *
114
+ * Typical use: zoom badge in a toolbar.
115
+ *
116
+ * ```tsx
117
+ * const zoom = useCameraSnapshot(handle, (c) => c.zoom, 1);
118
+ * return <div>{Math.round(zoom * 100)}%</div>;
119
+ * ```
120
+ *
121
+ * The `fallback` is what's returned when `handle` is `null` (before mount /
122
+ * after detach). It's also the SSR snapshot value — anything that won't
123
+ * mismatch with the first client render.
124
+ */
125
+ function useCameraSnapshot(handle, selector, fallback) {
126
+ return (0, react.useSyncExternalStore)((cb) => handle?.camera.subscribe(cb) ?? (() => {}), () => handle ? selector(handle.camera) : fallback, () => fallback);
127
+ }
128
+ /** Current selection (frozen, identity-stable across no-op emits). */
129
+ function useSelection() {
130
+ return useEditorState((s) => s.selection);
131
+ }
132
+ /** Active tool. Identity-stable when `set_tool` is a no-op. */
133
+ function useTool() {
134
+ return useEditorState((s) => s.tool);
135
+ }
136
+ /** Current mode (`"select"` | `"edit-content"`). */
137
+ function useMode() {
138
+ return useEditorState((s) => s.mode);
139
+ }
140
+ /** Whether the history stack has an undoable entry. */
141
+ function useCanUndo() {
142
+ return useEditorState((s) => s.can_undo);
143
+ }
144
+ /** Whether the history stack has a redoable entry. */
145
+ function useCanRedo() {
146
+ return useEditorState((s) => s.can_redo);
147
+ }
148
+ function use_lifecycle_session(open, deps) {
149
+ const sessionRef = (0, react.useRef)(null);
150
+ const ops = (0, react.useMemo)(() => ({
151
+ ensure() {
152
+ if (!sessionRef.current) sessionRef.current = open();
153
+ return sessionRef.current;
154
+ },
155
+ finalize(action, commit) {
156
+ const s = sessionRef.current;
157
+ if (!s) return;
158
+ sessionRef.current = null;
159
+ if (action === "commit") commit(s);
160
+ else s.discard();
161
+ }
162
+ }), deps);
163
+ (0, react.useEffect)(() => {
164
+ return () => ops.finalize("discard", () => {});
165
+ }, [ops]);
166
+ return ops;
167
+ }
168
+ /** Hook-owned `PaintPreviewSession`. See block comment above. */
169
+ function usePaintPreview(channel) {
170
+ const editor = useSvgEditor();
171
+ const lc = use_lifecycle_session(() => editor.commands.preview_paint(channel), [editor, channel]);
172
+ return (0, react.useMemo)(() => ({
173
+ update: (paint) => lc.ensure().update(paint),
174
+ commit: () => lc.finalize("commit", (s) => s.commit()),
175
+ discard: () => lc.finalize("discard", () => {})
176
+ }), [lc]);
177
+ }
178
+ /** Hook-owned `PreviewSession` for a CSS/SVG property. See block comment above. */
179
+ function usePropertyPreview(name) {
180
+ const editor = useSvgEditor();
181
+ const lc = use_lifecycle_session(() => editor.commands.preview_property(name), [editor, name]);
182
+ return (0, react.useMemo)(() => ({
183
+ update: (value) => lc.ensure().update(value),
184
+ commit: () => lc.finalize("commit", (s) => s.commit()),
185
+ discard: () => lc.finalize("discard", () => {})
186
+ }), [lc]);
187
+ }
188
+ /** Bound `editor.load(svg)`. Stable across renders. */
189
+ function useEditorLoad() {
190
+ const editor = useSvgEditor();
191
+ return (0, react.useCallback)((svg) => editor.load(svg), [editor]);
192
+ }
193
+ /** Bound `editor.serialize()`. Stable across renders. */
194
+ function useEditorSerialize() {
195
+ const editor = useSvgEditor();
196
+ return (0, react.useCallback)(() => editor.serialize(), [editor]);
197
+ }
198
+ /**
199
+ * Push a hover override into the HUD surface — e.g. when the user hovers
200
+ * a row in a layers panel. The HUD will render the override's outline.
201
+ *
202
+ * Pass `null` to clear and let the pointer pick take over again. On
203
+ * unmount, the hook clears any override it set last so the canvas
204
+ * doesn't stay highlighted on a node that no longer has a panel row.
205
+ */
206
+ function useHoverOverride() {
207
+ const editor = useSvgEditor();
208
+ const lastSetRef = (0, react.useRef)(null);
209
+ (0, react.useEffect)(() => {
210
+ return () => {
211
+ if (lastSetRef.current !== null && editor.surface_hover() === lastSetRef.current) editor.set_surface_hover_override(null);
212
+ };
213
+ }, [editor]);
214
+ return (0, react.useCallback)((id) => {
215
+ lastSetRef.current = id;
216
+ editor.set_surface_hover_override(id);
217
+ }, [editor]);
218
+ }
92
219
  //#endregion
93
220
  exports.SvgEditorCanvas = SvgEditorCanvas;
94
221
  exports.SvgEditorProvider = SvgEditorProvider;
222
+ exports.useCameraSnapshot = useCameraSnapshot;
223
+ exports.useCanRedo = useCanRedo;
224
+ exports.useCanUndo = useCanUndo;
95
225
  exports.useCommands = useCommands;
226
+ exports.useEditorLoad = useEditorLoad;
227
+ exports.useEditorSerialize = useEditorSerialize;
96
228
  exports.useEditorState = useEditorState;
229
+ exports.useHoverOverride = useHoverOverride;
230
+ exports.useMode = useMode;
231
+ exports.usePaintPreview = usePaintPreview;
232
+ exports.usePropertyPreview = usePropertyPreview;
233
+ exports.useSelection = useSelection;
97
234
  exports.useSvgEditor = useSvgEditor;
235
+ exports.useTool = useTool;
package/dist/react.mjs CHANGED
@@ -1,29 +1,25 @@
1
1
  "use client";
2
- import { t as createSvgEditor } from "./editor-B5z-gTML.mjs";
3
- import { t as attach_dom_surface } from "./dom-kA8NDuVh.mjs";
4
- import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { t as createSvgEditor } from "./editor-DtuRIs-Q.mjs";
3
+ import { t as attach_dom_surface } from "./dom-BlMk07oX.mjs";
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  //#region src/react.tsx
7
7
  const SvgEditorContext = createContext(null);
8
8
  /**
9
9
  * Owns the headless editor and exposes it via context. The editor is created
10
- * once on first render; subsequent prop changes to `svg` call `editor.load()`.
10
+ * once on first render with `initialSvg`; subsequent changes to that prop are
11
+ * silently ignored. To replace the document at runtime, call
12
+ * `useSvgEditor().load(...)` imperatively, or remount the provider with a
13
+ * different `key`.
11
14
  */
12
- function SvgEditorProvider({ svg, providers, style, children }) {
15
+ function SvgEditorProvider({ initialSvg, providers, style, children }) {
13
16
  const editor_ref = useRef(null);
14
17
  if (editor_ref.current === null) editor_ref.current = createSvgEditor({
15
- svg,
18
+ svg: initialSvg,
16
19
  providers,
17
20
  style
18
21
  });
19
22
  const editor = editor_ref.current;
20
- const last_svg = useRef(svg);
21
- useEffect(() => {
22
- if (last_svg.current !== svg) {
23
- editor.load(svg);
24
- last_svg.current = svg;
25
- }
26
- }, [svg, editor]);
27
23
  useEffect(() => {
28
24
  return () => {
29
25
  editor.dispose();
@@ -37,19 +33,38 @@ function SvgEditorProvider({ svg, providers, style, children }) {
37
33
  /**
38
34
  * Renders the editor's SVG into a `div` and wires it to the DOM surface.
39
35
  *
40
- * Internally calls `attach_dom_surface(editor, { container })` on mount and
41
- * `handle.detach()` on unmount. This is the only UI component the package
42
- * ships; everything else (toolbar, property panel, etc.) is consumer-built.
36
+ * Internally calls `attach_dom_surface(editor, { container, ... })` on
37
+ * mount and `handle.detach()` on unmount. Surface-scoped concerns (camera,
38
+ * gestures) are reached via the `onAttach` callback — there is no global
39
+ * context for them, because a host may mount multiple canvases in the
40
+ * same editor session.
43
41
  */
44
- function SvgEditorCanvas({ className, style }) {
42
+ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAttach }) {
45
43
  const editor = useSvgEditor();
46
44
  const ref = useRef(null);
45
+ const on_attach_ref = useRef(onAttach);
46
+ on_attach_ref.current = onAttach;
47
+ const initial_camera_ref = useRef(initial_camera);
48
+ initial_camera_ref.current = initial_camera;
47
49
  useEffect(() => {
48
50
  const container = ref.current;
49
51
  if (!container) return;
50
- const handle = attach_dom_surface(editor, { container });
51
- return () => handle.detach();
52
- }, [editor]);
52
+ const handle = attach_dom_surface(editor, {
53
+ container,
54
+ gestures,
55
+ fit,
56
+ initial_camera: initial_camera_ref.current
57
+ });
58
+ on_attach_ref.current?.(handle);
59
+ return () => {
60
+ on_attach_ref.current?.(null);
61
+ handle.detach();
62
+ };
63
+ }, [
64
+ editor,
65
+ gestures,
66
+ fit
67
+ ]);
53
68
  return /* @__PURE__ */ jsx("div", {
54
69
  ref,
55
70
  className,
@@ -88,5 +103,116 @@ function useCommands() {
88
103
  const editor = useSvgEditor();
89
104
  return useMemo(() => editor.commands, [editor]);
90
105
  }
106
+ /**
107
+ * Subscribe to a slice of `handle.camera` from a `DomSurfaceHandle`. Pass
108
+ * the handle (or null if it isn't attached yet) and a selector that reads
109
+ * what you need from the camera. The returned value updates on every
110
+ * camera mutation — does NOT bump `editor.state.version`.
111
+ *
112
+ * Typical use: zoom badge in a toolbar.
113
+ *
114
+ * ```tsx
115
+ * const zoom = useCameraSnapshot(handle, (c) => c.zoom, 1);
116
+ * return <div>{Math.round(zoom * 100)}%</div>;
117
+ * ```
118
+ *
119
+ * The `fallback` is what's returned when `handle` is `null` (before mount /
120
+ * after detach). It's also the SSR snapshot value — anything that won't
121
+ * mismatch with the first client render.
122
+ */
123
+ function useCameraSnapshot(handle, selector, fallback) {
124
+ return useSyncExternalStore((cb) => handle?.camera.subscribe(cb) ?? (() => {}), () => handle ? selector(handle.camera) : fallback, () => fallback);
125
+ }
126
+ /** Current selection (frozen, identity-stable across no-op emits). */
127
+ function useSelection() {
128
+ return useEditorState((s) => s.selection);
129
+ }
130
+ /** Active tool. Identity-stable when `set_tool` is a no-op. */
131
+ function useTool() {
132
+ return useEditorState((s) => s.tool);
133
+ }
134
+ /** Current mode (`"select"` | `"edit-content"`). */
135
+ function useMode() {
136
+ return useEditorState((s) => s.mode);
137
+ }
138
+ /** Whether the history stack has an undoable entry. */
139
+ function useCanUndo() {
140
+ return useEditorState((s) => s.can_undo);
141
+ }
142
+ /** Whether the history stack has a redoable entry. */
143
+ function useCanRedo() {
144
+ return useEditorState((s) => s.can_redo);
145
+ }
146
+ function use_lifecycle_session(open, deps) {
147
+ const sessionRef = useRef(null);
148
+ const ops = useMemo(() => ({
149
+ ensure() {
150
+ if (!sessionRef.current) sessionRef.current = open();
151
+ return sessionRef.current;
152
+ },
153
+ finalize(action, commit) {
154
+ const s = sessionRef.current;
155
+ if (!s) return;
156
+ sessionRef.current = null;
157
+ if (action === "commit") commit(s);
158
+ else s.discard();
159
+ }
160
+ }), deps);
161
+ useEffect(() => {
162
+ return () => ops.finalize("discard", () => {});
163
+ }, [ops]);
164
+ return ops;
165
+ }
166
+ /** Hook-owned `PaintPreviewSession`. See block comment above. */
167
+ function usePaintPreview(channel) {
168
+ const editor = useSvgEditor();
169
+ const lc = use_lifecycle_session(() => editor.commands.preview_paint(channel), [editor, channel]);
170
+ return useMemo(() => ({
171
+ update: (paint) => lc.ensure().update(paint),
172
+ commit: () => lc.finalize("commit", (s) => s.commit()),
173
+ discard: () => lc.finalize("discard", () => {})
174
+ }), [lc]);
175
+ }
176
+ /** Hook-owned `PreviewSession` for a CSS/SVG property. See block comment above. */
177
+ function usePropertyPreview(name) {
178
+ const editor = useSvgEditor();
179
+ const lc = use_lifecycle_session(() => editor.commands.preview_property(name), [editor, name]);
180
+ return useMemo(() => ({
181
+ update: (value) => lc.ensure().update(value),
182
+ commit: () => lc.finalize("commit", (s) => s.commit()),
183
+ discard: () => lc.finalize("discard", () => {})
184
+ }), [lc]);
185
+ }
186
+ /** Bound `editor.load(svg)`. Stable across renders. */
187
+ function useEditorLoad() {
188
+ const editor = useSvgEditor();
189
+ return useCallback((svg) => editor.load(svg), [editor]);
190
+ }
191
+ /** Bound `editor.serialize()`. Stable across renders. */
192
+ function useEditorSerialize() {
193
+ const editor = useSvgEditor();
194
+ return useCallback(() => editor.serialize(), [editor]);
195
+ }
196
+ /**
197
+ * Push a hover override into the HUD surface — e.g. when the user hovers
198
+ * a row in a layers panel. The HUD will render the override's outline.
199
+ *
200
+ * Pass `null` to clear and let the pointer pick take over again. On
201
+ * unmount, the hook clears any override it set last so the canvas
202
+ * doesn't stay highlighted on a node that no longer has a panel row.
203
+ */
204
+ function useHoverOverride() {
205
+ const editor = useSvgEditor();
206
+ const lastSetRef = useRef(null);
207
+ useEffect(() => {
208
+ return () => {
209
+ if (lastSetRef.current !== null && editor.surface_hover() === lastSetRef.current) editor.set_surface_hover_override(null);
210
+ };
211
+ }, [editor]);
212
+ return useCallback((id) => {
213
+ lastSetRef.current = id;
214
+ editor.set_surface_hover_override(id);
215
+ }, [editor]);
216
+ }
91
217
  //#endregion
92
- export { SvgEditorCanvas, SvgEditorProvider, useCommands, useEditorState, useSvgEditor };
218
+ export { SvgEditorCanvas, SvgEditorProvider, useCameraSnapshot, useCanRedo, useCanUndo, useCommands, useEditorLoad, useEditorSerialize, useEditorState, useHoverOverride, useMode, usePaintPreview, usePropertyPreview, useSelection, useSvgEditor, useTool };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grida/svg-editor",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.12",
4
4
  "description": "Headless SVG editor (experimental).",
5
5
  "license": "MIT",
6
6
  "author": "Grida",
@@ -26,6 +26,11 @@
26
26
  "types": "./dist/react.d.ts",
27
27
  "import": "./dist/react.mjs",
28
28
  "require": "./dist/react.js"
29
+ },
30
+ "./presets": {
31
+ "types": "./dist/presets.d.ts",
32
+ "import": "./dist/presets.mjs",
33
+ "require": "./dist/presets.js"
29
34
  }
30
35
  },
31
36
  "publishConfig": {
@@ -33,12 +38,12 @@
33
38
  "tag": "alpha"
34
39
  },
35
40
  "dependencies": {
36
- "svg-pathdata": "^7.2.0",
37
- "@grida/hud": "0.1.0",
41
+ "@grida/cmath": "0.2.1",
42
+ "@grida/hud": "0.2.0",
43
+ "@grida/keybinding": "0.2.0",
38
44
  "@grida/history": "0.1.0",
39
- "@grida/cmath": "0.1.0",
40
- "@grida/keybinding": "0.1.0",
41
- "@grida/text-editor": "0.1.0"
45
+ "@grida/svg": "0.1.0",
46
+ "@grida/text-editor": "0.1.1"
42
47
  },
43
48
  "devDependencies": {
44
49
  "@types/react": "^19",