@grida/svg-editor 1.0.0-alpha.2 → 1.0.0-alpha.4

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,48 @@
1
+ import { a as SurfaceHandle, d as Gestures, g as Camera, o as SvgEditor } from "./editor-Uu6dZX4y.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 };
package/dist/dom.d.mts CHANGED
@@ -1,16 +1,2 @@
1
- import { a as SurfaceHandle, o as SvgEditor } from "./editor-BryibVvr.mjs";
2
-
3
- //#region src/dom.d.ts
4
- type DomSurfaceOptions = {
5
- /** Mount the SVG inside this container. */container: HTMLElement;
6
- };
7
- /**
8
- * Attach a DOM surface to a headless editor. Returns a `SurfaceHandle` whose
9
- * `detach()` is the inverse — DOM cleared, listeners removed.
10
- *
11
- * Usage is one-shot per container: the surface owns the container's children
12
- * for its lifetime, and `detach()` restores it to empty.
13
- */
14
- declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): SurfaceHandle;
15
- //#endregion
16
- export { DomSurfaceOptions, attach_dom_surface };
1
+ import { n as DomSurfaceOptions, r as attach_dom_surface, t as DomSurfaceHandle } from "./dom-DJnZhtOd.mjs";
2
+ export { DomSurfaceHandle, DomSurfaceOptions, attach_dom_surface };
package/dist/dom.d.ts CHANGED
@@ -1,16 +1,2 @@
1
- import { a as SurfaceHandle, o as SvgEditor } from "./editor-klT8wu-x.js";
2
-
3
- //#region src/dom.d.ts
4
- type DomSurfaceOptions = {
5
- /** Mount the SVG inside this container. */container: HTMLElement;
6
- };
7
- /**
8
- * Attach a DOM surface to a headless editor. Returns a `SurfaceHandle` whose
9
- * `detach()` is the inverse — DOM cleared, listeners removed.
10
- *
11
- * Usage is one-shot per container: the surface owns the container's children
12
- * for its lifetime, and `detach()` restores it to empty.
13
- */
14
- declare function attach_dom_surface(editor: SvgEditor, options: DomSurfaceOptions): SurfaceHandle;
15
- //#endregion
16
- export { DomSurfaceOptions, attach_dom_surface };
1
+ import { n as DomSurfaceOptions, r as attach_dom_surface, t as DomSurfaceHandle } from "./dom-Cn-RtjRL.js";
2
+ export { DomSurfaceHandle, DomSurfaceOptions, attach_dom_surface };
package/dist/dom.js CHANGED
@@ -1,3 +1,3 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dom = require("./dom-CfP_ZURh.js");
2
+ const require_dom = require("./dom-CoVZzFqy.js");
3
3
  exports.attach_dom_surface = require_dom.attach_dom_surface;
package/dist/dom.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as attach_dom_surface } from "./dom-kA8NDuVh.mjs";
1
+ import { t as attach_dom_surface } from "./dom-CmOu0HvI.mjs";
2
2
  export { attach_dom_surface };
@@ -1,4 +1,4 @@
1
- import { i as apply_translate, n as serialize_paint, o as capture_translate_baseline, t as parse_paint } from "./paint-DuCg6Y-K.mjs";
1
+ import { i as apply_translate, l as is_text_input_focused, n as serialize_paint, o as capture_translate_baseline, t as parse_paint } from "./paint-Cfiw4g_J.mjs";
2
2
  import { HistoryImpl } from "@grida/history";
3
3
  import { KeyCode, M, chunkKey, eventToChunk, getKeyboardOS, kb, keybindingsToKeyCodes } from "@grida/keybinding";
4
4
  //#region src/commands/registry.ts
@@ -111,15 +111,6 @@ const TEXT_INPUT_SAFE_MODS = new Set([
111
111
  KeyCode.Ctrl,
112
112
  KeyCode.Alt
113
113
  ]);
114
- function is_text_input_focused() {
115
- if (typeof document === "undefined") return false;
116
- const el = document.activeElement;
117
- if (!el) return false;
118
- const tag = el.tagName;
119
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
120
- if (el.isContentEditable) return true;
121
- return false;
122
- }
123
114
  var Keymap = class {
124
115
  constructor(commands, platformGetter = getKeyboardOS) {
125
116
  this.commands = commands;
@@ -1327,6 +1318,13 @@ function createSvgEditor(opts) {
1327
1318
  let doc_version = 0;
1328
1319
  /** doc_version at the last load()/serialize(); compared to derive `dirty`. */
1329
1320
  let baseline_doc_version = 0;
1321
+ /**
1322
+ * Bumps once per `editor.load(svg)` call. The constructor's initial parse
1323
+ * does NOT count — it's the "factory" state. Hosts subscribe via
1324
+ * `subscribe_with_selector(s => s.load_version, ...)` to react to fresh
1325
+ * document loads without firing on every edit.
1326
+ */
1327
+ let load_version = 0;
1330
1328
  let style = {
1331
1329
  ...DEFAULT_STYLE,
1332
1330
  ...opts.style ?? {}
@@ -1344,7 +1342,8 @@ function createSvgEditor(opts) {
1344
1342
  can_undo: history.stack.canUndo,
1345
1343
  can_redo: history.stack.canRedo,
1346
1344
  version,
1347
- structure_version: doc.structure_version
1345
+ structure_version: doc.structure_version,
1346
+ load_version
1348
1347
  });
1349
1348
  }
1350
1349
  function emit() {
@@ -1652,6 +1651,7 @@ function createSvgEditor(opts) {
1652
1651
  mode = "select";
1653
1652
  history.clear();
1654
1653
  baseline_doc_version = doc_version;
1654
+ load_version++;
1655
1655
  emit();
1656
1656
  }
1657
1657
  function serialize_svg() {
@@ -1,113 +1,7 @@
1
+ import cmath from "@grida/cmath";
1
2
  import * as _$_grida_history0 from "@grida/history";
2
3
  import { Keybinding, Platform } from "@grida/keybinding";
3
4
 
4
- //#region src/commands/registry.d.ts
5
- /**
6
- * Command registry.
7
- *
8
- * A passive id-keyed registry of handlers. Built so that:
9
- *
10
- * - keybindings (in `src/keymap`) can address commands by stable id;
11
- * - new commands can be added in ONE place (`src/commands/defaults.ts`)
12
- * without growing the public surface of the editor;
13
- * - "one key, many meanings" can be expressed via chain semantics: a
14
- * handler returns `true` if it consumed, `false`/`void` otherwise,
15
- * and the dispatcher tries the next candidate in the chain.
16
- *
17
- * Handlers are plain closures — they capture whatever editor reference
18
- * they need. The registry itself stays unaware of the editor's type,
19
- * which avoids a circular type dependency between editor and registry.
20
- */
21
- /** Stable, dotted id for a command, e.g. `"history.undo"`. */
22
- type CommandId = string;
23
- /**
24
- * A command handler.
25
- *
26
- * Return `true` if the handler consumed the invocation. Return `false`
27
- * or `undefined` to signal "did not apply" — the dispatcher will try
28
- * the next candidate registered for the same key.
29
- *
30
- * Handlers are closures: they capture their editor reference. No
31
- * editor parameter is passed — keep handlers self-contained.
32
- */
33
- type CommandHandler = (args?: unknown) => boolean | void;
34
- declare class CommandRegistry {
35
- private readonly map;
36
- /**
37
- * Register a command. Returns an unregister function. Re-registering
38
- * the same id replaces the previous handler (last writer wins).
39
- */
40
- register(id: CommandId, handler: CommandHandler): () => void;
41
- /**
42
- * Invoke a command by id. Returns `true` if the handler consumed,
43
- * `false` otherwise (including unknown ids and handlers that returned
44
- * `false`/`undefined`).
45
- */
46
- invoke(id: CommandId, args?: unknown): boolean;
47
- has(id: CommandId): boolean;
48
- /** All registered ids, for debugging / introspection. */
49
- ids(): readonly CommandId[];
50
- }
51
- //#endregion
52
- //#region src/keymap/keymap.d.ts
53
- type KeymapBinding = {
54
- /** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
55
- command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
56
- args?: unknown; /** Higher priorities run first in the chain. Default 0. */
57
- priority?: number;
58
- /**
59
- * Reserved for V2; not honored by the V1 dispatcher. When added, this
60
- * will be evaluated before the handler runs; if false, the binding is
61
- * skipped without invoking the handler.
62
- */
63
- when?: (ctx: unknown) => boolean;
64
- };
65
- declare class Keymap {
66
- private readonly commands;
67
- private readonly platformGetter;
68
- /**
69
- * Bindings bucketed by canonical chunk-key hash, computed per
70
- * `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
71
- * key, sorted in dispatch order (priority desc, then registration
72
- * order).
73
- */
74
- private readonly buckets;
75
- /** Insert order, so ties on priority are deterministic. */
76
- private seq;
77
- constructor(commands: CommandRegistry, platformGetter?: () => Platform);
78
- /**
79
- * Bind a key combination to a command. Returns an unbind function.
80
- * The same `Keybinding` can be bound to multiple commands — they will
81
- * all be tried in chain order on dispatch.
82
- */
83
- bind(binding: KeymapBinding): () => void;
84
- /**
85
- * Remove bindings matching the spec. If both filters are passed, only
86
- * bindings that match BOTH are removed.
87
- */
88
- unbind(spec: {
89
- keybinding?: Keybinding;
90
- command?: CommandId;
91
- }): void;
92
- /** All registered bindings, for introspection. Order is not guaranteed. */
93
- bindings(): readonly KeymapBinding[];
94
- /**
95
- * Match the event against bound chunks, then run candidates in chain
96
- * order. Returns `true` and calls `preventDefault()` on the first
97
- * handler that consumes; returns `false` if nothing matched or all
98
- * matches fell through.
99
- */
100
- dispatch(event: KeyboardEvent): boolean;
101
- /**
102
- * Compute the set of canonical hashes a `Keybinding` lights up. A
103
- * binding with aliases (multiple sequences) contributes one hash per
104
- * single-chunk alias; multi-chunk sequences (chords) are skipped in
105
- * V1.
106
- */
107
- private chunkKeysFor;
108
- private has_safe_mod;
109
- }
110
- //#endregion
111
5
  //#region src/types.d.ts
112
6
  /**
113
7
  * Stable identifier for a node in the editor's document model.
@@ -264,6 +158,17 @@ type EditorState = {
264
158
  * `structure_version` so a drag doesn't invalidate the tree view.
265
159
  */
266
160
  readonly structure_version: number;
161
+ /**
162
+ * Bumps once per `editor.load(svg)` call. Distinct from
163
+ * `structure_version` (which bumps on edits too). Starts at 0; the
164
+ * constructor's initial SVG does NOT count as a load. Use this when
165
+ * you want to react to "a new document was loaded" — e.g. refit
166
+ * camera to the new root, reset host-side UI state, clear per-file
167
+ * scratch — without firing on text edits, reorders, or deletes.
168
+ *
169
+ * Monotonic, never resets.
170
+ */
171
+ readonly load_version: number;
267
172
  };
268
173
  type Unsubscribe = () => void;
269
174
  type ReorderDirection = "bring_forward" | "send_backward" | "bring_to_front" | "send_to_back";
@@ -278,6 +183,251 @@ type PaintPreviewSession = {
278
183
  discard(): void;
279
184
  };
280
185
  //#endregion
186
+ //#region src/core/camera.d.ts
187
+ /**
188
+ * Returns world-space bounds for the given target, or `null` when
189
+ * unresolvable (e.g. empty selection, unknown node id). Implemented by the
190
+ * surface — the camera itself has no view into the document.
191
+ *
192
+ * Only string targets are passed to the resolver — `Rect` targets are
193
+ * handled by the camera as identity (the rect IS its own bounds).
194
+ */
195
+ type BoundsResolver = (target: "<root>" | "<selection>" | NodeId) => Rect | null;
196
+ type CameraOptions = {
197
+ resolve_bounds: BoundsResolver;
198
+ initial?: cmath.Transform;
199
+ };
200
+ /**
201
+ * Camera viewport constraint. Discriminated union with `type` so future
202
+ * variants (`'contain'`, `'pan-region'`) can be added without breaking
203
+ * existing call sites — each future variant has its own payload shape.
204
+ *
205
+ * v1.1 ships only `'cover'`. CSS analogy: `object-fit: cover` — the
206
+ * bounds rect covers the viewport edge-to-edge. Zoom is lower-bounded
207
+ * at fit-with-padding; pan is clamped so the bounds always covers the
208
+ * viewport. Use for slide / page / kiosk UX where the user should
209
+ * never see past the artwork.
210
+ */
211
+ type CameraConstraints = {
212
+ /** Bounds cover viewport (viewport ⊆ bounds). Keynote / slide UX. */type: "cover"; /** World-space rect, or `"<root>"` to resolve via BoundsResolver. */
213
+ bounds: Rect | "<root>"; /** Screen-pixel breathing room between bounds and viewport edge. */
214
+ padding?: number;
215
+ };
216
+ /**
217
+ * Surface-scoped pan/zoom state.
218
+ *
219
+ * The public shape leads with the peer convention (`center` / `zoom` /
220
+ * `bounds`) and keeps the matrix as an advanced read. Methods mirror
221
+ * Figma/Penpot where they overlap.
222
+ */
223
+ declare class Camera {
224
+ private _transform;
225
+ private viewport_w;
226
+ private viewport_h;
227
+ private listeners;
228
+ private resolve_bounds;
229
+ private _constraints;
230
+ constructor(opts: CameraOptions);
231
+ /**
232
+ * Current viewport constraint, or `null` for free pan/zoom. Set with
233
+ * `camera.constraints = { type: 'cover', bounds: '<root>', padding: 80 }`
234
+ * to clamp zoom + pan; assign `null` to clear.
235
+ *
236
+ * Constraints are applied synchronously inside `set_transform` (and
237
+ * `_set_viewport_size`), so every public mutation respects them
238
+ * automatically — the host never needs to subscribe-and-clamp itself.
239
+ */
240
+ get constraints(): CameraConstraints | null;
241
+ set constraints(c: CameraConstraints | null);
242
+ /** Underlying 2D affine. World→screen. */
243
+ get transform(): cmath.Transform;
244
+ /** Uniform scale factor. 1 = 100 %. */
245
+ get zoom(): number;
246
+ /** World-space point currently at viewport center. */
247
+ get center(): Vec2;
248
+ /** World-space rectangle visible in the viewport. */
249
+ get bounds(): Rect;
250
+ /** Translate the camera by a screen-space delta. */
251
+ pan(delta_screen: Vec2): void;
252
+ /**
253
+ * Multiply zoom by `factor` keeping `origin_screen` fixed in world space.
254
+ * Used by wheel-zoom-at-cursor and pinch-zoom.
255
+ */
256
+ zoom_at(factor: number, origin_screen: Vec2): void;
257
+ /** Pan so `c` lands at the viewport center. Zoom unchanged. */
258
+ set_center(c: Vec2): void;
259
+ /** Set zoom directly; pivot defaults to viewport center. */
260
+ set_zoom(z: number, pivot_screen?: Vec2): void;
261
+ /**
262
+ * Replace the entire transform.
263
+ *
264
+ * Idempotent: when the new transform is element-wise equal to the current
265
+ * one, this is a no-op (no notification fires). This is the seam that
266
+ * makes external constraint loops (e.g. "subscribe → compute clamped →
267
+ * set_transform") terminate: the clamp re-emits the same transform on
268
+ * the second pass, set_transform short-circuits, no recursion.
269
+ *
270
+ * When `camera.constraints` is non-null, the input transform is clamped
271
+ * synchronously before being stored — every public mutation respects the
272
+ * constraint automatically.
273
+ */
274
+ set_transform(t: cmath.Transform): void;
275
+ /** Viewport size in screen pixels. Read by host code computing constraints. */
276
+ get viewport_size(): {
277
+ width: number;
278
+ height: number;
279
+ };
280
+ /**
281
+ * Fit a target into the viewport.
282
+ *
283
+ * - `"<root>"` — the document root's content bounds (host-resolved).
284
+ * - `"<selection>"` — current editor.state.selection's union bounds.
285
+ * - `NodeId` — that node's content bounds.
286
+ * - `Rect` — an explicit world-space rectangle.
287
+ *
288
+ * No-ops if the target resolves to `null` (e.g. empty selection) or if
289
+ * the viewport size is 0 (no container).
290
+ */
291
+ fit(target: "<root>" | "<selection>" | NodeId | Rect, opts?: {
292
+ margin?: number;
293
+ }): void;
294
+ /** Snap back to identity. */
295
+ reset(): void;
296
+ /**
297
+ * Subscribe to camera changes. Fires on every mutation. Cheap channel —
298
+ * does NOT bump `editor.state.version`. Same pattern as
299
+ * `editor.subscribe_surface_hover`.
300
+ */
301
+ subscribe(cb: () => void): Unsubscribe;
302
+ /** @internal Surface drives this on container resize. */
303
+ _set_viewport_size(w: number, h: number): void;
304
+ /** Convert a screen-space point to world-space. */
305
+ screen_to_world(p: Vec2): Vec2;
306
+ /** Convert a world-space point to screen-space. */
307
+ world_to_screen(p: Vec2): Vec2;
308
+ /**
309
+ * Apply the current constraint (if any) to a candidate transform.
310
+ * Pure: returns the clamped result, never mutates state. Returns the
311
+ * input unchanged when constraints are null / bounds are unresolvable /
312
+ * viewport is 0.
313
+ */
314
+ private apply_constraints;
315
+ /**
316
+ * Re-clamp the stored transform against the current constraint. Called
317
+ * from the `constraints` setter; `_set_viewport_size` has its own
318
+ * notify-inclusive path.
319
+ */
320
+ private reenforce;
321
+ private notify;
322
+ }
323
+ //#endregion
324
+ //#region src/commands/registry.d.ts
325
+ /**
326
+ * Command registry.
327
+ *
328
+ * A passive id-keyed registry of handlers. Built so that:
329
+ *
330
+ * - keybindings (in `src/keymap`) can address commands by stable id;
331
+ * - new commands can be added in ONE place (`src/commands/defaults.ts`)
332
+ * without growing the public surface of the editor;
333
+ * - "one key, many meanings" can be expressed via chain semantics: a
334
+ * handler returns `true` if it consumed, `false`/`void` otherwise,
335
+ * and the dispatcher tries the next candidate in the chain.
336
+ *
337
+ * Handlers are plain closures — they capture whatever editor reference
338
+ * they need. The registry itself stays unaware of the editor's type,
339
+ * which avoids a circular type dependency between editor and registry.
340
+ */
341
+ /** Stable, dotted id for a command, e.g. `"history.undo"`. */
342
+ type CommandId = string;
343
+ /**
344
+ * A command handler.
345
+ *
346
+ * Return `true` if the handler consumed the invocation. Return `false`
347
+ * or `undefined` to signal "did not apply" — the dispatcher will try
348
+ * the next candidate registered for the same key.
349
+ *
350
+ * Handlers are closures: they capture their editor reference. No
351
+ * editor parameter is passed — keep handlers self-contained.
352
+ */
353
+ type CommandHandler = (args?: unknown) => boolean | void;
354
+ declare class CommandRegistry {
355
+ private readonly map;
356
+ /**
357
+ * Register a command. Returns an unregister function. Re-registering
358
+ * the same id replaces the previous handler (last writer wins).
359
+ */
360
+ register(id: CommandId, handler: CommandHandler): () => void;
361
+ /**
362
+ * Invoke a command by id. Returns `true` if the handler consumed,
363
+ * `false` otherwise (including unknown ids and handlers that returned
364
+ * `false`/`undefined`).
365
+ */
366
+ invoke(id: CommandId, args?: unknown): boolean;
367
+ has(id: CommandId): boolean;
368
+ /** All registered ids, for debugging / introspection. */
369
+ ids(): readonly CommandId[];
370
+ }
371
+ //#endregion
372
+ //#region src/keymap/keymap.d.ts
373
+ type KeymapBinding = {
374
+ /** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
375
+ command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
376
+ args?: unknown; /** Higher priorities run first in the chain. Default 0. */
377
+ priority?: number;
378
+ /**
379
+ * Reserved for V2; not honored by the V1 dispatcher. When added, this
380
+ * will be evaluated before the handler runs; if false, the binding is
381
+ * skipped without invoking the handler.
382
+ */
383
+ when?: (ctx: unknown) => boolean;
384
+ };
385
+ declare class Keymap {
386
+ private readonly commands;
387
+ private readonly platformGetter;
388
+ /**
389
+ * Bindings bucketed by canonical chunk-key hash, computed per
390
+ * `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
391
+ * key, sorted in dispatch order (priority desc, then registration
392
+ * order).
393
+ */
394
+ private readonly buckets;
395
+ /** Insert order, so ties on priority are deterministic. */
396
+ private seq;
397
+ constructor(commands: CommandRegistry, platformGetter?: () => Platform);
398
+ /**
399
+ * Bind a key combination to a command. Returns an unbind function.
400
+ * The same `Keybinding` can be bound to multiple commands — they will
401
+ * all be tried in chain order on dispatch.
402
+ */
403
+ bind(binding: KeymapBinding): () => void;
404
+ /**
405
+ * Remove bindings matching the spec. If both filters are passed, only
406
+ * bindings that match BOTH are removed.
407
+ */
408
+ unbind(spec: {
409
+ keybinding?: Keybinding;
410
+ command?: CommandId;
411
+ }): void;
412
+ /** All registered bindings, for introspection. Order is not guaranteed. */
413
+ bindings(): readonly KeymapBinding[];
414
+ /**
415
+ * Match the event against bound chunks, then run candidates in chain
416
+ * order. Returns `true` and calls `preventDefault()` on the first
417
+ * handler that consumes; returns `false` if nothing matched or all
418
+ * matches fell through.
419
+ */
420
+ dispatch(event: KeyboardEvent): boolean;
421
+ /**
422
+ * Compute the set of canonical hashes a `Keybinding` lights up. A
423
+ * binding with aliases (multiple sequences) contributes one hash per
424
+ * single-chunk alias; multi-chunk sequences (chords) are skipped in
425
+ * V1.
426
+ */
427
+ private chunkKeysFor;
428
+ private has_safe_mod;
429
+ }
430
+ //#endregion
281
431
  //#region src/core/parser.d.ts
282
432
  type AttrToken = {
283
433
  /** Verbatim source name including any prefix, e.g. "xlink:href". */raw_name: string; /** Prefix or null. */
@@ -439,6 +589,62 @@ type Defs = {
439
589
  //#region src/core/properties.d.ts
440
590
  declare function read_property(doc: SvgDocument, id: NodeId, property: string): PropertyValue;
441
591
  //#endregion
592
+ //#region src/gestures/gestures.d.ts
593
+ /** Stable identifier for a gesture binding. Used by `unbind({ id })`. */
594
+ type GestureId = string;
595
+ /**
596
+ * Context passed to every installer. Exposes the seams a gesture needs:
597
+ * the container element to listen on, the camera to mutate, and the
598
+ * editor for keymap dispatch / state reads.
599
+ *
600
+ * Surface authors construct this once at attach; bindings receive it on
601
+ * every `install(...)` call.
602
+ */
603
+ type GestureContext = {
604
+ /** Container element listeners attach to. */container: HTMLElement; /** SVG element being framed by the camera. Useful for hit-testing. */
605
+ svg_root: () => SVGSVGElement | null; /** HUD canvas overlay; sits on top of the SVG. */
606
+ hud_canvas: HTMLCanvasElement; /** Camera the binding mutates. */
607
+ camera: Camera; /** Editor for keymap dispatch / state reads. */
608
+ editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
609
+ handle: SurfaceHandle;
610
+ };
611
+ type GestureBinding = {
612
+ /** Stable id used by `unbind` / `bindings()`. */id: GestureId;
613
+ /**
614
+ * Wire DOM listeners (or any side-effect) needed for this gesture.
615
+ * Returns the uninstaller — called on `unbind` or surface detach.
616
+ */
617
+ install(ctx: GestureContext): () => void;
618
+ };
619
+ /**
620
+ * Sibling to `Keymap`. Owns a list of installed gesture bindings; each
621
+ * binding's `install(ctx)` is called eagerly when bound and uninstalled
622
+ * on `unbind` or surface detach.
623
+ */
624
+ declare class Gestures {
625
+ private readonly ctx;
626
+ private entries;
627
+ constructor(ctx: GestureContext);
628
+ /**
629
+ * Install a gesture binding. Returns an unbind function.
630
+ * Re-binding the same `id` does NOT replace — both will be active.
631
+ * Use `unbind({ id })` first if you want a clean swap.
632
+ */
633
+ bind(binding: GestureBinding): () => void;
634
+ /**
635
+ * Remove bindings matching the spec. With `{ id }`, all bindings with
636
+ * that id are uninstalled. With no spec, this is a no-op (use
637
+ * `dispose()` to nuke everything).
638
+ */
639
+ unbind(spec: {
640
+ id?: GestureId;
641
+ }): void;
642
+ /** All currently installed bindings. Order is registration order. */
643
+ bindings(): readonly GestureBinding[];
644
+ /** @internal Uninstall every binding. Surface calls on detach. */
645
+ _dispose(): void;
646
+ }
647
+ //#endregion
442
648
  //#region src/core/editor.d.ts
443
649
  /** Resolved paint from the DOM-attached cascade. `resolved_paint` mirrors the
444
650
  * shape of `PaintValue.computed` so consumers can treat it uniformly with
@@ -609,4 +815,4 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
609
815
  keymap: Keymap;
610
816
  };
611
817
  //#endregion
612
- export { RadialGradientDefinition as A, PaintFallback as C, PropertyValue as D, PreviewSession as E, KeymapBinding as F, CommandHandler as I, CommandId as L, ReorderDirection as M, Unsubscribe as N, Provenance as O, Vec2 as P, Paint as S, PaintValue as T, GradientStop as _, SurfaceHandle as a, Mode as b, ClipboardProvider as c, EditorState as d, EditorStyle as f, GradientEntry as g, GradientDefinition as h, DomComputedResolver as i, Rect as j, Providers as k, Color as l, FontResolver as m, CreateSvgEditorOptions as n, SvgEditor as o, FileIOProvider as p, DomComputedPaint as r, createSvgEditor as s, Commands as t, DEFAULT_STYLE as u, InvalidComputedValue as v, PaintPreviewSession as w, NodeId as x, LinearGradientDefinition as y };
818
+ export { LinearGradientDefinition as A, Providers as B, EditorStyle as C, GradientEntry as D, GradientDefinition as E, PaintPreviewSession as F, Vec2 as G, Rect as H, PaintValue as I, PreviewSession as L, NodeId as M, Paint as N, GradientStop as O, PaintFallback as P, PropertyValue as R, EditorState as S, FontResolver as T, ReorderDirection as U, RadialGradientDefinition as V, Unsubscribe as W, CameraConstraints as _, SurfaceHandle as a, Color as b, GestureBinding as c, Gestures as d, KeymapBinding as f, Camera as g, BoundsResolver as h, DomComputedResolver as i, Mode as j, InvalidComputedValue as k, GestureContext as l, CommandId as m, CreateSvgEditorOptions as n, SvgEditor as o, CommandHandler as p, DomComputedPaint as r, createSvgEditor as s, Commands as t, GestureId as u, CameraOptions as v, FileIOProvider as w, DEFAULT_STYLE as x, ClipboardProvider as y, Provenance as z };
@@ -1,5 +1,5 @@
1
- require("./dom-CfP_ZURh.js");
2
- const require_paint = require("./paint-DHq_3iwU.js");
1
+ require("./dom-CoVZzFqy.js");
2
+ const require_paint = require("./paint-dDV-Trt9.js");
3
3
  let _grida_history = require("@grida/history");
4
4
  let _grida_keybinding = require("@grida/keybinding");
5
5
  //#region src/commands/registry.ts
@@ -112,15 +112,6 @@ const TEXT_INPUT_SAFE_MODS = new Set([
112
112
  _grida_keybinding.KeyCode.Ctrl,
113
113
  _grida_keybinding.KeyCode.Alt
114
114
  ]);
115
- function is_text_input_focused() {
116
- if (typeof document === "undefined") return false;
117
- const el = document.activeElement;
118
- if (!el) return false;
119
- const tag = el.tagName;
120
- if (tag === "INPUT" || tag === "TEXTAREA") return true;
121
- if (el.isContentEditable) return true;
122
- return false;
123
- }
124
115
  var Keymap = class {
125
116
  constructor(commands, platformGetter = _grida_keybinding.getKeyboardOS) {
126
117
  this.commands = commands;
@@ -189,7 +180,7 @@ var Keymap = class {
189
180
  const hash = (0, _grida_keybinding.chunkKey)(chunk);
190
181
  const list = this.buckets.get(hash);
191
182
  if (!list || list.length === 0) return false;
192
- const text_focused = is_text_input_focused();
183
+ const text_focused = require_paint.is_text_input_focused();
193
184
  for (const { binding } of list) {
194
185
  if (text_focused && !this.has_safe_mod(chunk.mods)) continue;
195
186
  if (this.commands.invoke(binding.command, binding.args)) {
@@ -1328,6 +1319,13 @@ function createSvgEditor(opts) {
1328
1319
  let doc_version = 0;
1329
1320
  /** doc_version at the last load()/serialize(); compared to derive `dirty`. */
1330
1321
  let baseline_doc_version = 0;
1322
+ /**
1323
+ * Bumps once per `editor.load(svg)` call. The constructor's initial parse
1324
+ * does NOT count — it's the "factory" state. Hosts subscribe via
1325
+ * `subscribe_with_selector(s => s.load_version, ...)` to react to fresh
1326
+ * document loads without firing on every edit.
1327
+ */
1328
+ let load_version = 0;
1331
1329
  let style = {
1332
1330
  ...DEFAULT_STYLE,
1333
1331
  ...opts.style ?? {}
@@ -1345,7 +1343,8 @@ function createSvgEditor(opts) {
1345
1343
  can_undo: history.stack.canUndo,
1346
1344
  can_redo: history.stack.canRedo,
1347
1345
  version,
1348
- structure_version: doc.structure_version
1346
+ structure_version: doc.structure_version,
1347
+ load_version
1349
1348
  });
1350
1349
  }
1351
1350
  function emit() {
@@ -1653,6 +1652,7 @@ function createSvgEditor(opts) {
1653
1652
  mode = "select";
1654
1653
  history.clear();
1655
1654
  baseline_doc_version = doc_version;
1655
+ load_version++;
1656
1656
  emit();
1657
1657
  }
1658
1658
  function serialize_svg() {