@grida/svg-editor 1.0.0-alpha.5 → 1.0.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.
Files changed (41) hide show
  1. package/README.md +365 -384
  2. package/dist/dom-BEjG2bSw.d.mts +320 -0
  3. package/dist/dom-DYD1BHTo.js +6050 -0
  4. package/dist/dom-Y2QR7QSi.d.ts +318 -0
  5. package/dist/dom-tM3Dr1EK.mjs +5971 -0
  6. package/dist/dom.d.mts +3 -2
  7. package/dist/dom.d.ts +3 -2
  8. package/dist/dom.js +13 -1
  9. package/dist/dom.mjs +2 -2
  10. package/dist/editor-CWNtt1vu.mjs +2998 -0
  11. package/dist/editor-CZgwg8BK.d.mts +2537 -0
  12. package/dist/editor-CesShk9o.js +3004 -0
  13. package/dist/editor-kFTYSb_8.d.ts +2537 -0
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +5 -2
  17. package/dist/index.mjs +3 -2
  18. package/dist/model-D0iEDcg3.mjs +5451 -0
  19. package/dist/model-tywvTGZv.js +5640 -0
  20. package/dist/presets.d.mts +17 -2
  21. package/dist/presets.d.ts +17 -2
  22. package/dist/presets.js +16 -8
  23. package/dist/presets.mjs +15 -8
  24. package/dist/react.d.mts +88 -10
  25. package/dist/react.d.ts +88 -10
  26. package/dist/react.js +169 -14
  27. package/dist/react.mjs +159 -16
  28. package/package.json +35 -9
  29. package/dist/dom-CMXNUMjP.d.mts +0 -48
  30. package/dist/dom-DyJy1H6Q.js +0 -1676
  31. package/dist/dom-eIgcZ4JC.d.ts +0 -48
  32. package/dist/dom-l5Y1Wf8C.mjs +0 -1627
  33. package/dist/editor-CRflVqEz.mjs +0 -1846
  34. package/dist/editor-DdgqLDC9.d.ts +0 -840
  35. package/dist/editor-Ds47eN37.js +0 -1858
  36. package/dist/editor-KRAmUodY.d.mts +0 -840
  37. package/dist/index-CHiXYO9-.d.ts +0 -1
  38. package/dist/index-ThDLM4Am.d.mts +0 -1
  39. package/dist/paint-Cfiw4g_J.mjs +0 -477
  40. package/dist/paint-cTjePy5e.js +0 -531
  41. /package/dist/{chunk-CfYAbeIz.mjs → chunk-D7D4PA-g.mjs} +0 -0
package/dist/react.mjs CHANGED
@@ -1,29 +1,25 @@
1
1
  "use client";
2
- import { t as createSvgEditor } from "./editor-CRflVqEz.mjs";
3
- import { t as attach_dom_surface } from "./dom-l5Y1Wf8C.mjs";
4
- import { createContext, useContext, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { t as createSvgEditor } from "./editor-CWNtt1vu.mjs";
3
+ import { t as attach_dom_surface } from "./dom-tM3Dr1EK.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();
@@ -43,7 +39,7 @@ function SvgEditorProvider({ svg, providers, style, children }) {
43
39
  * context for them, because a host may mount multiple canvases in the
44
40
  * same editor session.
45
41
  */
46
- function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAttach }) {
42
+ function SvgEditorCanvas({ className, style, gestures, fit, clipboard, initial_camera, onAttach }) {
47
43
  const editor = useSvgEditor();
48
44
  const ref = useRef(null);
49
45
  const on_attach_ref = useRef(onAttach);
@@ -57,6 +53,7 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
57
53
  container,
58
54
  gestures,
59
55
  fit,
56
+ clipboard,
60
57
  initial_camera: initial_camera_ref.current
61
58
  });
62
59
  on_attach_ref.current?.(handle);
@@ -67,7 +64,8 @@ function SvgEditorCanvas({ className, style, gestures, fit, initial_camera, onAt
67
64
  }, [
68
65
  editor,
69
66
  gestures,
70
- fit
67
+ fit,
68
+ clipboard
71
69
  ]);
72
70
  return /* @__PURE__ */ jsx("div", {
73
71
  ref,
@@ -127,5 +125,150 @@ function useCommands() {
127
125
  function useCameraSnapshot(handle, selector, fallback) {
128
126
  return useSyncExternalStore((cb) => handle?.camera.subscribe(cb) ?? (() => {}), () => handle ? selector(handle.camera) : fallback, () => fallback);
129
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
+ }
130
273
  //#endregion
131
- export { SvgEditorCanvas, SvgEditorProvider, useCameraSnapshot, 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.5",
3
+ "version": "1.0.0",
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": "https://github.com/gridaco/grida",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/gridaco/grida"
27
+ },
8
28
  "files": [
9
29
  "dist"
10
30
  ],
@@ -38,17 +58,22 @@
38
58
  "tag": "alpha"
39
59
  },
40
60
  "dependencies": {
41
- "svg-pathdata": "^7.2.0",
42
- "@grida/cmath": "0.1.0",
43
- "@grida/history": "0.1.0",
44
- "@grida/hud": "0.1.0",
45
- "@grida/keybinding": "0.1.0",
46
- "@grida/text-editor": "0.1.0"
61
+ "@grida/cmath": "0.2.3",
62
+ "@grida/color": "0.1.0",
63
+ "@grida/history": "0.1.2",
64
+ "@grida/hud": "0.4.0",
65
+ "@grida/keybinding": "0.2.2",
66
+ "@grida/svg": "0.3.0",
67
+ "@grida/text-editor": "0.1.2",
68
+ "@grida/vn": "0.1.0"
47
69
  },
48
70
  "devDependencies": {
49
71
  "@types/react": "^19",
72
+ "@vitest/browser": "4.0.18",
73
+ "@vitest/browser-playwright": "^4.0.18",
74
+ "playwright": "^1.58.2",
50
75
  "react": "^19",
51
- "tsdown": "^0.21.9",
76
+ "tsdown": "^0.22.1",
52
77
  "typescript": "^6",
53
78
  "vitest": "^4"
54
79
  },
@@ -65,6 +90,7 @@
65
90
  "build": "tsdown",
66
91
  "dev": "tsdown --watch",
67
92
  "test": "vitest run",
93
+ "test:browser": "vitest run --config vitest.browser.config.ts",
68
94
  "test:watch": "vitest",
69
95
  "typecheck": "tsc --noEmit"
70
96
  }
@@ -1,48 +0,0 @@
1
- import { _ as Camera, f as Gestures, o as SurfaceHandle, s as SvgEditor } from "./editor-KRAmUodY.mjs";
2
- import cmath from "@grida/cmath";
3
-
4
- //#region src/dom.d.ts
5
- type DomSurfaceOptions = {
6
- /** Mount the SVG inside this container. */container: HTMLElement;
7
- /**
8
- * Install the default gesture set (wheel-pan/zoom, space-drag, middle-mouse,
9
- * keyboard zoom). Default `true`. Pass `false` to start blank and bind à la
10
- * carte via `handle.gestures.bind(...)`.
11
- */
12
- gestures?: boolean;
13
- /**
14
- * Auto-fit the document into the viewport on initial attach. Default
15
- * `false`. Mirrors Excalidraw's `initialData.scrollToContent`.
16
- * Subsequent `editor.load()` calls do NOT re-fit — call
17
- * `handle.camera.fit("<root>")` yourself if you want that behavior.
18
- */
19
- fit?: boolean;
20
- /**
21
- * Initial camera transform. Default `cmath.transform.identity`. Ignored
22
- * when `fit: true`.
23
- */
24
- initial_camera?: cmath.Transform;
25
- };
26
- /**
27
- * Surface handle for the DOM surface. Extends the editor's core
28
- * `SurfaceHandle` with the viewport-scoped concerns: pan/zoom (`camera`)
29
- * and pointer/wheel/keyboard gesture bindings (`gestures`).
30
- *
31
- * Camera + gestures are **surface-scoped**: detaching the surface drops
32
- * both. They never appear on the headless `SvgEditor`.
33
- */
34
- type DomSurfaceHandle = SurfaceHandle & {
35
- camera: Camera;
36
- gestures: Gestures;
37
- };
38
- /**
39
- * Attach a DOM surface to a headless editor. Returns a `DomSurfaceHandle`
40
- * whose `detach()` is the inverse — DOM cleared, listeners removed,
41
- * gestures uninstalled.
42
- *
43
- * Usage is one-shot per container: the surface owns the container's children
44
- * for its lifetime, and `detach()` restores it to empty.
45
- */
46
- declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): DomSurfaceHandle;
47
- //#endregion
48
- export { DomSurfaceOptions as n, attach_dom_surface as r, DomSurfaceHandle as t };