@grida/svg-editor 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +343 -189
- package/dist/chunk-D7D4PA-g.mjs +13 -0
- package/dist/dom-CQkWJNrK.d.ts +237 -0
- package/dist/dom-CuK0LFUY.js +5276 -0
- package/dist/dom-DHaTIObb.mjs +5221 -0
- package/dist/dom-Dw2SPHgc.d.mts +239 -0
- package/dist/dom.d.mts +3 -16
- package/dist/dom.d.ts +3 -16
- package/dist/dom.js +9 -1
- package/dist/dom.mjs +2 -2
- package/dist/editor-BlByfVyF.js +2936 -0
- package/dist/editor-CJ3ROm0G.mjs +2930 -0
- package/dist/editor-CcW4BVth.d.mts +2359 -0
- package/dist/editor-CxqRhhzP.d.ts +2359 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -2
- package/dist/index.mjs +3 -2
- package/dist/model-C6jCFK_p.mjs +5329 -0
- package/dist/model-DVwjrVYp.js +5512 -0
- package/dist/presets.d.mts +61 -0
- package/dist/presets.d.ts +61 -0
- package/dist/presets.js +60 -0
- package/dist/presets.mjs +54 -0
- package/dist/react.d.mts +133 -12
- package/dist/react.d.ts +133 -12
- package/dist/react.js +214 -19
- package/dist/react.mjs +203 -21
- package/package.json +40 -9
- package/dist/dom-CfP_ZURh.js +0 -963
- package/dist/dom-kA8NDuVh.mjs +0 -929
- package/dist/editor-BryibVvr.d.mts +0 -612
- package/dist/editor-DllAMsDu.js +0 -1835
- package/dist/editor-M6j8XGO5.mjs +0 -1823
- package/dist/editor-klT8wu-x.d.ts +0 -612
- package/dist/paint-DHq_3iwU.js +0 -509
- package/dist/paint-DuCg6Y-K.mjs +0 -461
package/dist/react.js
CHANGED
|
@@ -1,30 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const require_editor = require("./editor-BlByfVyF.js");
|
|
4
|
+
const require_dom = require("./dom-CuK0LFUY.js");
|
|
5
5
|
let react = require("react");
|
|
6
6
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
7
7
|
//#region src/react.tsx
|
|
8
8
|
const SvgEditorContext = (0, react.createContext)(null);
|
|
9
9
|
/**
|
|
10
10
|
* Owns the headless editor and exposes it via context. The editor is created
|
|
11
|
-
* once on first render
|
|
11
|
+
* once on first render with `initialSvg`; subsequent changes to that prop are
|
|
12
|
+
* silently ignored. To replace the document at runtime, call
|
|
13
|
+
* `useSvgEditor().load(...)` imperatively, or remount the provider with a
|
|
14
|
+
* different `key`.
|
|
12
15
|
*/
|
|
13
|
-
function SvgEditorProvider({
|
|
16
|
+
function SvgEditorProvider({ initialSvg, providers, style, children }) {
|
|
14
17
|
const editor_ref = (0, react.useRef)(null);
|
|
15
18
|
if (editor_ref.current === null) editor_ref.current = require_editor.createSvgEditor({
|
|
16
|
-
svg,
|
|
19
|
+
svg: initialSvg,
|
|
17
20
|
providers,
|
|
18
21
|
style
|
|
19
22
|
});
|
|
20
23
|
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
24
|
(0, react.useEffect)(() => {
|
|
29
25
|
return () => {
|
|
30
26
|
editor.dispose();
|
|
@@ -38,19 +34,40 @@ function SvgEditorProvider({ svg, providers, style, children }) {
|
|
|
38
34
|
/**
|
|
39
35
|
* Renders the editor's SVG into a `div` and wires it to the DOM surface.
|
|
40
36
|
*
|
|
41
|
-
* Internally calls `attach_dom_surface(editor, { container })` on
|
|
42
|
-
* `handle.detach()` on unmount.
|
|
43
|
-
*
|
|
37
|
+
* Internally calls `attach_dom_surface(editor, { container, ... })` on
|
|
38
|
+
* mount and `handle.detach()` on unmount. Surface-scoped concerns (camera,
|
|
39
|
+
* gestures) are reached via the `onAttach` callback — there is no global
|
|
40
|
+
* context for them, because a host may mount multiple canvases in the
|
|
41
|
+
* same editor session.
|
|
44
42
|
*/
|
|
45
|
-
function SvgEditorCanvas({ className, style }) {
|
|
43
|
+
function SvgEditorCanvas({ className, style, gestures, fit, clipboard, initial_camera, onAttach }) {
|
|
46
44
|
const editor = useSvgEditor();
|
|
47
45
|
const ref = (0, react.useRef)(null);
|
|
46
|
+
const on_attach_ref = (0, react.useRef)(onAttach);
|
|
47
|
+
on_attach_ref.current = onAttach;
|
|
48
|
+
const initial_camera_ref = (0, react.useRef)(initial_camera);
|
|
49
|
+
initial_camera_ref.current = initial_camera;
|
|
48
50
|
(0, react.useEffect)(() => {
|
|
49
51
|
const container = ref.current;
|
|
50
52
|
if (!container) return;
|
|
51
|
-
const handle = require_dom.attach_dom_surface(editor, {
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const handle = require_dom.attach_dom_surface(editor, {
|
|
54
|
+
container,
|
|
55
|
+
gestures,
|
|
56
|
+
fit,
|
|
57
|
+
clipboard,
|
|
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
|
+
clipboard
|
|
70
|
+
]);
|
|
54
71
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
55
72
|
ref,
|
|
56
73
|
className,
|
|
@@ -89,9 +106,187 @@ function useCommands() {
|
|
|
89
106
|
const editor = useSvgEditor();
|
|
90
107
|
return (0, react.useMemo)(() => editor.commands, [editor]);
|
|
91
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Subscribe to a slice of `handle.camera` from a `DomSurfaceHandle`. Pass
|
|
111
|
+
* the handle (or null if it isn't attached yet) and a selector that reads
|
|
112
|
+
* what you need from the camera. The returned value updates on every
|
|
113
|
+
* camera mutation — does NOT bump `editor.state.version`.
|
|
114
|
+
*
|
|
115
|
+
* Typical use: zoom badge in a toolbar.
|
|
116
|
+
*
|
|
117
|
+
* ```tsx
|
|
118
|
+
* const zoom = useCameraSnapshot(handle, (c) => c.zoom, 1);
|
|
119
|
+
* return <div>{Math.round(zoom * 100)}%</div>;
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* The `fallback` is what's returned when `handle` is `null` (before mount /
|
|
123
|
+
* after detach). It's also the SSR snapshot value — anything that won't
|
|
124
|
+
* mismatch with the first client render.
|
|
125
|
+
*/
|
|
126
|
+
function useCameraSnapshot(handle, selector, fallback) {
|
|
127
|
+
return (0, react.useSyncExternalStore)((cb) => handle?.camera.subscribe(cb) ?? (() => {}), () => handle ? selector(handle.camera) : fallback, () => fallback);
|
|
128
|
+
}
|
|
129
|
+
/** Current selection (frozen, identity-stable across no-op emits). */
|
|
130
|
+
function useSelection() {
|
|
131
|
+
return useEditorState((s) => s.selection);
|
|
132
|
+
}
|
|
133
|
+
/** Active tool. Identity-stable when `set_tool` is a no-op. */
|
|
134
|
+
function useTool() {
|
|
135
|
+
return useEditorState((s) => s.tool);
|
|
136
|
+
}
|
|
137
|
+
/** Current mode (`"select"` | `"edit-content"`). */
|
|
138
|
+
function useMode() {
|
|
139
|
+
return useEditorState((s) => s.mode);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* What kind of content-edit is active, or `null` when not in content-edit.
|
|
143
|
+
*
|
|
144
|
+
* Symmetric with `useMode()` but at a finer grain — resolves whether the
|
|
145
|
+
* single selected node is a path or a text node so consumers (e.g. the
|
|
146
|
+
* vector-edit toolbar) can render the right affordances. Mirrors the
|
|
147
|
+
* dispatch logic in the host's `enter_content_edit` router which checks
|
|
148
|
+
* `tag_of(id) === "path"` vs `"text" / "tspan"`.
|
|
149
|
+
*
|
|
150
|
+
* Returns `null` for the (defensive) case of `edit-content` with no
|
|
151
|
+
* selection, and for any tag that's neither path nor text.
|
|
152
|
+
*/
|
|
153
|
+
function useContentEditKind() {
|
|
154
|
+
const editor = useSvgEditor();
|
|
155
|
+
const [mode, id] = useEditorState((s) => `${s.mode}::${s.selection[0] ?? ""}`, (a, b) => a === b).split("::");
|
|
156
|
+
if (mode !== "edit-content" || !id) return null;
|
|
157
|
+
const tag = editor.tree().nodes.get(id)?.tag;
|
|
158
|
+
if (tag === "path") return "path";
|
|
159
|
+
if (tag === "text" || tag === "tspan") return "text";
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
/** Whether the history stack has an undoable entry. */
|
|
163
|
+
function useCanUndo() {
|
|
164
|
+
return useEditorState((s) => s.can_undo);
|
|
165
|
+
}
|
|
166
|
+
/** Whether the history stack has a redoable entry. */
|
|
167
|
+
function useCanRedo() {
|
|
168
|
+
return useEditorState((s) => s.can_redo);
|
|
169
|
+
}
|
|
170
|
+
function use_lifecycle_session(open, deps) {
|
|
171
|
+
const sessionRef = (0, react.useRef)(null);
|
|
172
|
+
const ops = (0, react.useMemo)(() => ({
|
|
173
|
+
ensure() {
|
|
174
|
+
if (sessionRef.current?.live === false) sessionRef.current = null;
|
|
175
|
+
if (!sessionRef.current) sessionRef.current = open();
|
|
176
|
+
return sessionRef.current;
|
|
177
|
+
},
|
|
178
|
+
/** Non-creating read: is a gesture currently open underneath? */
|
|
179
|
+
live() {
|
|
180
|
+
return sessionRef.current?.live === true;
|
|
181
|
+
},
|
|
182
|
+
finalize(action, commit) {
|
|
183
|
+
const s = sessionRef.current;
|
|
184
|
+
if (!s) return;
|
|
185
|
+
sessionRef.current = null;
|
|
186
|
+
if (action === "commit") commit(s);
|
|
187
|
+
else s.discard();
|
|
188
|
+
}
|
|
189
|
+
}), deps);
|
|
190
|
+
(0, react.useEffect)(() => {
|
|
191
|
+
return () => ops.finalize("discard", () => {});
|
|
192
|
+
}, [ops]);
|
|
193
|
+
return ops;
|
|
194
|
+
}
|
|
195
|
+
/** Hook-owned `PaintPreviewSession`. See block comment above. */
|
|
196
|
+
function usePaintPreview(channel) {
|
|
197
|
+
const editor = useSvgEditor();
|
|
198
|
+
const lc = use_lifecycle_session(() => editor.commands.preview_paint(channel), [editor, channel]);
|
|
199
|
+
return (0, react.useMemo)(() => ({
|
|
200
|
+
get live() {
|
|
201
|
+
return lc.live();
|
|
202
|
+
},
|
|
203
|
+
update: (paint) => lc.ensure().update(paint),
|
|
204
|
+
commit: () => lc.finalize("commit", (s) => s.commit()),
|
|
205
|
+
discard: () => lc.finalize("discard", () => {})
|
|
206
|
+
}), [lc]);
|
|
207
|
+
}
|
|
208
|
+
/** Hook-owned `PreviewSession` for a CSS/SVG property. See block comment above. */
|
|
209
|
+
function usePropertyPreview(name) {
|
|
210
|
+
const editor = useSvgEditor();
|
|
211
|
+
const lc = use_lifecycle_session(() => editor.commands.preview_property(name), [editor, name]);
|
|
212
|
+
return (0, react.useMemo)(() => ({
|
|
213
|
+
get live() {
|
|
214
|
+
return lc.live();
|
|
215
|
+
},
|
|
216
|
+
update: (value) => lc.ensure().update(value),
|
|
217
|
+
commit: () => lc.finalize("commit", (s) => s.commit()),
|
|
218
|
+
discard: () => lc.finalize("discard", () => {})
|
|
219
|
+
}), [lc]);
|
|
220
|
+
}
|
|
221
|
+
/** Bound `editor.load(svg)`. Stable across renders. */
|
|
222
|
+
function useEditorLoad() {
|
|
223
|
+
const editor = useSvgEditor();
|
|
224
|
+
return (0, react.useCallback)((svg) => editor.load(svg), [editor]);
|
|
225
|
+
}
|
|
226
|
+
/** Bound `editor.serialize()`. Stable across renders. */
|
|
227
|
+
function useEditorSerialize() {
|
|
228
|
+
const editor = useSvgEditor();
|
|
229
|
+
return (0, react.useCallback)(() => editor.serialize(), [editor]);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Push a hover override into the HUD surface — e.g. when the user hovers
|
|
233
|
+
* a row in a layers panel. The HUD will render the override's outline.
|
|
234
|
+
*
|
|
235
|
+
* Pass `null` to clear and let the pointer pick take over again. On
|
|
236
|
+
* unmount, the hook clears any override it set last so the canvas
|
|
237
|
+
* doesn't stay highlighted on a node that no longer has a panel row.
|
|
238
|
+
*/
|
|
239
|
+
function useHoverOverride() {
|
|
240
|
+
const editor = useSvgEditor();
|
|
241
|
+
const lastSetRef = (0, react.useRef)(null);
|
|
242
|
+
(0, react.useEffect)(() => {
|
|
243
|
+
return () => {
|
|
244
|
+
if (lastSetRef.current !== null && editor.surface_hover() === lastSetRef.current) editor.set_surface_hover_override(null);
|
|
245
|
+
};
|
|
246
|
+
}, [editor]);
|
|
247
|
+
return (0, react.useCallback)((id) => {
|
|
248
|
+
lastSetRef.current = id;
|
|
249
|
+
editor.set_surface_hover_override(id);
|
|
250
|
+
}, [editor]);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Observe pick (tap) outcomes — a discrete click on the canvas, reporting the
|
|
254
|
+
* document-space `point`, the `node_id` under it (`null` for empty canvas),
|
|
255
|
+
* the `button`, and `mods`. The handler fires once per tap, after the editor's
|
|
256
|
+
* own selection handling. Observe-only: it cannot prevent or alter selection.
|
|
257
|
+
*
|
|
258
|
+
* This is the React edge-wire over `editor.subscribe_pick`. The handler is
|
|
259
|
+
* kept in a ref so re-subscription is never triggered by handler identity —
|
|
260
|
+
* pass an inline closure freely. Pick is editor-scoped (it survives surface
|
|
261
|
+
* detach), so this is a hook, not a `SvgEditorCanvas` prop.
|
|
262
|
+
*
|
|
263
|
+
* Typical use: a comment / annotation tool anchors a popover at `e.point` and
|
|
264
|
+
* scopes its action to `e.node_id` (or to the whole document when `null`).
|
|
265
|
+
*
|
|
266
|
+
* @unstable See {@link PickEvent}.
|
|
267
|
+
*/
|
|
268
|
+
function useEditorPick(handler) {
|
|
269
|
+
const editor = useSvgEditor();
|
|
270
|
+
const handler_ref = (0, react.useRef)(handler);
|
|
271
|
+
handler_ref.current = handler;
|
|
272
|
+
(0, react.useEffect)(() => editor.subscribe_pick((e) => handler_ref.current(e)), [editor]);
|
|
273
|
+
}
|
|
92
274
|
//#endregion
|
|
93
275
|
exports.SvgEditorCanvas = SvgEditorCanvas;
|
|
94
276
|
exports.SvgEditorProvider = SvgEditorProvider;
|
|
277
|
+
exports.useCameraSnapshot = useCameraSnapshot;
|
|
278
|
+
exports.useCanRedo = useCanRedo;
|
|
279
|
+
exports.useCanUndo = useCanUndo;
|
|
95
280
|
exports.useCommands = useCommands;
|
|
281
|
+
exports.useContentEditKind = useContentEditKind;
|
|
282
|
+
exports.useEditorLoad = useEditorLoad;
|
|
283
|
+
exports.useEditorPick = useEditorPick;
|
|
284
|
+
exports.useEditorSerialize = useEditorSerialize;
|
|
96
285
|
exports.useEditorState = useEditorState;
|
|
286
|
+
exports.useHoverOverride = useHoverOverride;
|
|
287
|
+
exports.useMode = useMode;
|
|
288
|
+
exports.usePaintPreview = usePaintPreview;
|
|
289
|
+
exports.usePropertyPreview = usePropertyPreview;
|
|
290
|
+
exports.useSelection = useSelection;
|
|
97
291
|
exports.useSvgEditor = useSvgEditor;
|
|
292
|
+
exports.useTool = useTool;
|
package/dist/react.mjs
CHANGED
|
@@ -1,29 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { t as createSvgEditor } from "./editor-
|
|
3
|
-
import { t as attach_dom_surface } from "./dom-
|
|
4
|
-
import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { t as createSvgEditor } from "./editor-CJ3ROm0G.mjs";
|
|
3
|
+
import { t as attach_dom_surface } from "./dom-DHaTIObb.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
|
|
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({
|
|
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,40 @@ 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
|
|
41
|
-
* `handle.detach()` on unmount.
|
|
42
|
-
*
|
|
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, clipboard, 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, {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
const handle = attach_dom_surface(editor, {
|
|
53
|
+
container,
|
|
54
|
+
gestures,
|
|
55
|
+
fit,
|
|
56
|
+
clipboard,
|
|
57
|
+
initial_camera: initial_camera_ref.current
|
|
58
|
+
});
|
|
59
|
+
on_attach_ref.current?.(handle);
|
|
60
|
+
return () => {
|
|
61
|
+
on_attach_ref.current?.(null);
|
|
62
|
+
handle.detach();
|
|
63
|
+
};
|
|
64
|
+
}, [
|
|
65
|
+
editor,
|
|
66
|
+
gestures,
|
|
67
|
+
fit,
|
|
68
|
+
clipboard
|
|
69
|
+
]);
|
|
53
70
|
return /* @__PURE__ */ jsx("div", {
|
|
54
71
|
ref,
|
|
55
72
|
className,
|
|
@@ -88,5 +105,170 @@ function useCommands() {
|
|
|
88
105
|
const editor = useSvgEditor();
|
|
89
106
|
return useMemo(() => editor.commands, [editor]);
|
|
90
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 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
|
+
/**
|
|
141
|
+
* What kind of content-edit is active, or `null` when not in content-edit.
|
|
142
|
+
*
|
|
143
|
+
* Symmetric with `useMode()` but at a finer grain — resolves whether the
|
|
144
|
+
* single selected node is a path or a text node so consumers (e.g. the
|
|
145
|
+
* vector-edit toolbar) can render the right affordances. Mirrors the
|
|
146
|
+
* dispatch logic in the host's `enter_content_edit` router which checks
|
|
147
|
+
* `tag_of(id) === "path"` vs `"text" / "tspan"`.
|
|
148
|
+
*
|
|
149
|
+
* Returns `null` for the (defensive) case of `edit-content` with no
|
|
150
|
+
* selection, and for any tag that's neither path nor text.
|
|
151
|
+
*/
|
|
152
|
+
function useContentEditKind() {
|
|
153
|
+
const editor = useSvgEditor();
|
|
154
|
+
const [mode, id] = useEditorState((s) => `${s.mode}::${s.selection[0] ?? ""}`, (a, b) => a === b).split("::");
|
|
155
|
+
if (mode !== "edit-content" || !id) return null;
|
|
156
|
+
const tag = editor.tree().nodes.get(id)?.tag;
|
|
157
|
+
if (tag === "path") return "path";
|
|
158
|
+
if (tag === "text" || tag === "tspan") return "text";
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
/** Whether the history stack has an undoable entry. */
|
|
162
|
+
function useCanUndo() {
|
|
163
|
+
return useEditorState((s) => s.can_undo);
|
|
164
|
+
}
|
|
165
|
+
/** Whether the history stack has a redoable entry. */
|
|
166
|
+
function useCanRedo() {
|
|
167
|
+
return useEditorState((s) => s.can_redo);
|
|
168
|
+
}
|
|
169
|
+
function use_lifecycle_session(open, deps) {
|
|
170
|
+
const sessionRef = useRef(null);
|
|
171
|
+
const ops = useMemo(() => ({
|
|
172
|
+
ensure() {
|
|
173
|
+
if (sessionRef.current?.live === false) sessionRef.current = null;
|
|
174
|
+
if (!sessionRef.current) sessionRef.current = open();
|
|
175
|
+
return sessionRef.current;
|
|
176
|
+
},
|
|
177
|
+
/** Non-creating read: is a gesture currently open underneath? */
|
|
178
|
+
live() {
|
|
179
|
+
return sessionRef.current?.live === true;
|
|
180
|
+
},
|
|
181
|
+
finalize(action, commit) {
|
|
182
|
+
const s = sessionRef.current;
|
|
183
|
+
if (!s) return;
|
|
184
|
+
sessionRef.current = null;
|
|
185
|
+
if (action === "commit") commit(s);
|
|
186
|
+
else s.discard();
|
|
187
|
+
}
|
|
188
|
+
}), deps);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
return () => ops.finalize("discard", () => {});
|
|
191
|
+
}, [ops]);
|
|
192
|
+
return ops;
|
|
193
|
+
}
|
|
194
|
+
/** Hook-owned `PaintPreviewSession`. See block comment above. */
|
|
195
|
+
function usePaintPreview(channel) {
|
|
196
|
+
const editor = useSvgEditor();
|
|
197
|
+
const lc = use_lifecycle_session(() => editor.commands.preview_paint(channel), [editor, channel]);
|
|
198
|
+
return useMemo(() => ({
|
|
199
|
+
get live() {
|
|
200
|
+
return lc.live();
|
|
201
|
+
},
|
|
202
|
+
update: (paint) => lc.ensure().update(paint),
|
|
203
|
+
commit: () => lc.finalize("commit", (s) => s.commit()),
|
|
204
|
+
discard: () => lc.finalize("discard", () => {})
|
|
205
|
+
}), [lc]);
|
|
206
|
+
}
|
|
207
|
+
/** Hook-owned `PreviewSession` for a CSS/SVG property. See block comment above. */
|
|
208
|
+
function usePropertyPreview(name) {
|
|
209
|
+
const editor = useSvgEditor();
|
|
210
|
+
const lc = use_lifecycle_session(() => editor.commands.preview_property(name), [editor, name]);
|
|
211
|
+
return useMemo(() => ({
|
|
212
|
+
get live() {
|
|
213
|
+
return lc.live();
|
|
214
|
+
},
|
|
215
|
+
update: (value) => lc.ensure().update(value),
|
|
216
|
+
commit: () => lc.finalize("commit", (s) => s.commit()),
|
|
217
|
+
discard: () => lc.finalize("discard", () => {})
|
|
218
|
+
}), [lc]);
|
|
219
|
+
}
|
|
220
|
+
/** Bound `editor.load(svg)`. Stable across renders. */
|
|
221
|
+
function useEditorLoad() {
|
|
222
|
+
const editor = useSvgEditor();
|
|
223
|
+
return useCallback((svg) => editor.load(svg), [editor]);
|
|
224
|
+
}
|
|
225
|
+
/** Bound `editor.serialize()`. Stable across renders. */
|
|
226
|
+
function useEditorSerialize() {
|
|
227
|
+
const editor = useSvgEditor();
|
|
228
|
+
return useCallback(() => editor.serialize(), [editor]);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Push a hover override into the HUD surface — e.g. when the user hovers
|
|
232
|
+
* a row in a layers panel. The HUD will render the override's outline.
|
|
233
|
+
*
|
|
234
|
+
* Pass `null` to clear and let the pointer pick take over again. On
|
|
235
|
+
* unmount, the hook clears any override it set last so the canvas
|
|
236
|
+
* doesn't stay highlighted on a node that no longer has a panel row.
|
|
237
|
+
*/
|
|
238
|
+
function useHoverOverride() {
|
|
239
|
+
const editor = useSvgEditor();
|
|
240
|
+
const lastSetRef = useRef(null);
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
return () => {
|
|
243
|
+
if (lastSetRef.current !== null && editor.surface_hover() === lastSetRef.current) editor.set_surface_hover_override(null);
|
|
244
|
+
};
|
|
245
|
+
}, [editor]);
|
|
246
|
+
return useCallback((id) => {
|
|
247
|
+
lastSetRef.current = id;
|
|
248
|
+
editor.set_surface_hover_override(id);
|
|
249
|
+
}, [editor]);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Observe pick (tap) outcomes — a discrete click on the canvas, reporting the
|
|
253
|
+
* document-space `point`, the `node_id` under it (`null` for empty canvas),
|
|
254
|
+
* the `button`, and `mods`. The handler fires once per tap, after the editor's
|
|
255
|
+
* own selection handling. Observe-only: it cannot prevent or alter selection.
|
|
256
|
+
*
|
|
257
|
+
* This is the React edge-wire over `editor.subscribe_pick`. The handler is
|
|
258
|
+
* kept in a ref so re-subscription is never triggered by handler identity —
|
|
259
|
+
* pass an inline closure freely. Pick is editor-scoped (it survives surface
|
|
260
|
+
* detach), so this is a hook, not a `SvgEditorCanvas` prop.
|
|
261
|
+
*
|
|
262
|
+
* Typical use: a comment / annotation tool anchors a popover at `e.point` and
|
|
263
|
+
* scopes its action to `e.node_id` (or to the whole document when `null`).
|
|
264
|
+
*
|
|
265
|
+
* @unstable See {@link PickEvent}.
|
|
266
|
+
*/
|
|
267
|
+
function useEditorPick(handler) {
|
|
268
|
+
const editor = useSvgEditor();
|
|
269
|
+
const handler_ref = useRef(handler);
|
|
270
|
+
handler_ref.current = handler;
|
|
271
|
+
useEffect(() => editor.subscribe_pick((e) => handler_ref.current(e)), [editor]);
|
|
272
|
+
}
|
|
91
273
|
//#endregion
|
|
92
|
-
export { SvgEditorCanvas, SvgEditorProvider, useCommands, useEditorState, useSvgEditor };
|
|
274
|
+
export { SvgEditorCanvas, SvgEditorProvider, useCameraSnapshot, useCanRedo, useCanUndo, useCommands, useContentEditKind, useEditorLoad, useEditorPick, useEditorSerialize, useEditorState, useHoverOverride, useMode, usePaintPreview, usePropertyPreview, useSelection, useSvgEditor, useTool };
|
package/package.json
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grida/svg-editor",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.21",
|
|
4
4
|
"description": "Headless SVG editor (experimental).",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bezier",
|
|
7
|
+
"design-tool",
|
|
8
|
+
"editor",
|
|
9
|
+
"grida",
|
|
10
|
+
"headless",
|
|
11
|
+
"path-editor",
|
|
12
|
+
"pen-tool",
|
|
13
|
+
"react",
|
|
14
|
+
"svg",
|
|
15
|
+
"svg-editor",
|
|
16
|
+
"typescript",
|
|
17
|
+
"vector",
|
|
18
|
+
"vector-editor",
|
|
19
|
+
"vector-graphics"
|
|
20
|
+
],
|
|
21
|
+
"homepage": "https://grida.co/packages/@grida/svg-editor",
|
|
5
22
|
"license": "MIT",
|
|
6
23
|
"author": "Grida",
|
|
7
|
-
"repository":
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/gridaco/grida"
|
|
27
|
+
},
|
|
8
28
|
"files": [
|
|
9
29
|
"dist"
|
|
10
30
|
],
|
|
@@ -26,6 +46,11 @@
|
|
|
26
46
|
"types": "./dist/react.d.ts",
|
|
27
47
|
"import": "./dist/react.mjs",
|
|
28
48
|
"require": "./dist/react.js"
|
|
49
|
+
},
|
|
50
|
+
"./presets": {
|
|
51
|
+
"types": "./dist/presets.d.ts",
|
|
52
|
+
"import": "./dist/presets.mjs",
|
|
53
|
+
"require": "./dist/presets.js"
|
|
29
54
|
}
|
|
30
55
|
},
|
|
31
56
|
"publishConfig": {
|
|
@@ -33,17 +58,22 @@
|
|
|
33
58
|
"tag": "alpha"
|
|
34
59
|
},
|
|
35
60
|
"dependencies": {
|
|
36
|
-
"
|
|
37
|
-
"@grida/
|
|
38
|
-
"@grida/history": "0.1.
|
|
39
|
-
"@grida/
|
|
40
|
-
"@grida/
|
|
41
|
-
"@grida/
|
|
61
|
+
"@grida/cmath": "0.2.3",
|
|
62
|
+
"@grida/keybinding": "0.2.1",
|
|
63
|
+
"@grida/history": "0.1.2",
|
|
64
|
+
"@grida/hud": "0.2.3",
|
|
65
|
+
"@grida/vn": "0.1.0",
|
|
66
|
+
"@grida/svg": "0.2.0",
|
|
67
|
+
"@grida/text-editor": "0.1.2",
|
|
68
|
+
"@grida/color": "0.1.0"
|
|
42
69
|
},
|
|
43
70
|
"devDependencies": {
|
|
44
71
|
"@types/react": "^19",
|
|
72
|
+
"@vitest/browser": "4.0.18",
|
|
73
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
74
|
+
"playwright": "^1.58.2",
|
|
45
75
|
"react": "^19",
|
|
46
|
-
"tsdown": "^0.
|
|
76
|
+
"tsdown": "^0.22.1",
|
|
47
77
|
"typescript": "^6",
|
|
48
78
|
"vitest": "^4"
|
|
49
79
|
},
|
|
@@ -60,6 +90,7 @@
|
|
|
60
90
|
"build": "tsdown",
|
|
61
91
|
"dev": "tsdown --watch",
|
|
62
92
|
"test": "vitest run",
|
|
93
|
+
"test:browser": "vitest run --config vitest.browser.config.ts",
|
|
63
94
|
"test:watch": "vitest",
|
|
64
95
|
"typecheck": "tsc --noEmit"
|
|
65
96
|
}
|