@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
@@ -1,840 +0,0 @@
1
- import * as _$_grida_history0 from "@grida/history";
2
- import { Keybinding, Platform } from "@grida/keybinding";
3
- import cmath from "@grida/cmath";
4
-
5
- //#region src/types.d.ts
6
- /**
7
- * Stable identifier for a node in the editor's document model.
8
- *
9
- * Independent of any backing representation. Generated when the document is
10
- * parsed.
11
- */
12
- type NodeId = string;
13
- type Vec2 = {
14
- x: number;
15
- y: number;
16
- };
17
- type Rect = {
18
- x: number;
19
- y: number;
20
- width: number;
21
- height: number;
22
- };
23
- type Mode = "select" | "edit-content";
24
- type Provenance = {
25
- /** CSS cascade origin (css-cascade-5 §6.2). */origin: "author" | "user_agent"; /** Editor metadata — where in the file the winning declaration lives. */
26
- carrier: "presentation_attribute" | "inline_style" | "stylesheet" | "inherited" | "defaulted";
27
- };
28
- type InvalidComputedValue = {
29
- error: "invalid_at_computed_value_time";
30
- reason: string;
31
- };
32
- type PropertyValue<T = string | number> = {
33
- declared: string | null;
34
- computed: T | InvalidComputedValue | null;
35
- provenance: Provenance;
36
- };
37
- type Color = {
38
- kind: "rgb";
39
- value: string;
40
- } | {
41
- kind: "current_color";
42
- };
43
- type PaintFallback = {
44
- kind: "none";
45
- } | {
46
- kind: "color";
47
- value: Color;
48
- };
49
- type Paint = {
50
- kind: "none";
51
- } | {
52
- kind: "color";
53
- value: Color;
54
- } | {
55
- kind: "ref";
56
- id: string;
57
- fallback?: PaintFallback;
58
- } | {
59
- kind: "context_fill";
60
- } | {
61
- kind: "context_stroke";
62
- };
63
- type PaintValue = {
64
- declared: string | null;
65
- computed: Paint | InvalidComputedValue | null;
66
- provenance: Provenance;
67
- };
68
- type GradientStop = {
69
- offset: number;
70
- color: string;
71
- opacity?: number;
72
- };
73
- type LinearGradientDefinition = {
74
- kind: "linear";
75
- stops: GradientStop[];
76
- x1?: number;
77
- y1?: number;
78
- x2?: number;
79
- y2?: number;
80
- gradient_units?: "user_space_on_use" | "object_bounding_box";
81
- spread_method?: "pad" | "reflect" | "repeat";
82
- };
83
- type RadialGradientDefinition = {
84
- kind: "radial";
85
- stops: GradientStop[];
86
- cx?: number;
87
- cy?: number;
88
- r?: number;
89
- fx?: number;
90
- fy?: number;
91
- gradient_units?: "user_space_on_use" | "object_bounding_box";
92
- spread_method?: "pad" | "reflect" | "repeat";
93
- };
94
- type GradientDefinition = LinearGradientDefinition | RadialGradientDefinition;
95
- type GradientEntry = {
96
- id: string;
97
- definition: GradientDefinition;
98
- ref_count: number;
99
- };
100
- type EditorStyle = {
101
- chrome_color: string;
102
- handle_size: number;
103
- handle_fill: string;
104
- handle_stroke: string;
105
- endpoint_dot_radius: number;
106
- selection_outline_width: number;
107
- /**
108
- * Color for measurement guides (distance lines + numeric pills). Distinct
109
- * from `chrome_color` so the user can tell at a glance whether something
110
- * is selection chrome or a measurement readout.
111
- */
112
- measurement_color: string;
113
- };
114
- declare const DEFAULT_STYLE: EditorStyle;
115
- type ClipboardProvider = {
116
- read(): Promise<string | null>;
117
- write(text: string): Promise<void>;
118
- };
119
- type FontResolver = {
120
- resolve(family: string): Promise<{
121
- available: boolean;
122
- metrics?: {
123
- ascent: number;
124
- descent: number;
125
- unitsPerEm: number;
126
- };
127
- }>;
128
- };
129
- type FileIOProvider = {
130
- openSvg(): Promise<string | null>;
131
- saveSvg(svg: string, suggestedName?: string): Promise<void>;
132
- };
133
- type Providers = {
134
- clipboard?: ClipboardProvider;
135
- fonts?: FontResolver;
136
- file_io?: FileIOProvider;
137
- };
138
- type EditorState = {
139
- readonly selection: ReadonlyArray<NodeId>;
140
- readonly scope: NodeId | null;
141
- readonly mode: Mode;
142
- readonly dirty: boolean;
143
- readonly can_undo: boolean;
144
- readonly can_redo: boolean;
145
- /**
146
- * Bumps on every editor emission. Use this when you need to react to
147
- * any change — selection, history, mutation. NOT a good cache key for
148
- * tree-shape views because it fires on attribute writes too (e.g. x/y
149
- * during a drag).
150
- */
151
- readonly version: number;
152
- /**
153
- * Bumps only when the document's tree shape or display-label-affecting
154
- * data changes — node added/removed/reordered, text content, or the
155
- * `id` attribute. Stable across pure presentation-attribute writes.
156
- *
157
- * The right cache key for hierarchy / layers panels: snapshot once per
158
- * `structure_version` so a drag doesn't invalidate the tree view.
159
- */
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;
172
- };
173
- type Unsubscribe = () => void;
174
- type ReorderDirection = "bring_forward" | "send_backward" | "bring_to_front" | "send_to_back";
175
- type PreviewSession = {
176
- update(value: string): void;
177
- commit(): void;
178
- discard(): void;
179
- };
180
- type PaintPreviewSession = {
181
- update(paint: Paint): void;
182
- commit(): void;
183
- discard(): void;
184
- };
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/core/parser.d.ts
325
- type AttrToken = {
326
- /** Verbatim source name including any prefix, e.g. "xlink:href". */raw_name: string; /** Prefix or null. */
327
- prefix: string | null; /** Local name. */
328
- local: string; /** Resolved namespace URI, or null if unprefixed and no default ns. */
329
- ns: string | null; /** Raw attribute value string (entity-decoded). */
330
- value: string; /** Trivia before the attribute name (whitespace, usually `" "`). */
331
- pre: string; /** Trivia between `=` and the value (usually empty). */
332
- eq_trivia: string; /** Quote character (`"` or `'`). */
333
- quote: '"' | "'";
334
- };
335
- type ElementNode = {
336
- kind: "element";
337
- id: NodeId;
338
- parent: NodeId | null; /** Verbatim tag name with prefix. */
339
- raw_tag: string;
340
- prefix: string | null;
341
- local: string;
342
- ns: string | null;
343
- attrs: AttrToken[];
344
- children: NodeId[]; /** True if the source wrote `<tag/>` (no children). */
345
- self_closing: boolean; /** Trivia inside the tag before `>` or `/>` (e.g. `" "` in `<tag />`). */
346
- open_tag_trailing: string; /** Trivia between `</` and tag name in the close, e.g. usually empty. */
347
- close_tag_leading: string; /** Trivia after the closing tag name before `>`, usually empty. */
348
- close_tag_trailing: string;
349
- };
350
- type TextNode = {
351
- kind: "text";
352
- id: NodeId;
353
- parent: NodeId | null; /** Verbatim text, entity-decoded. */
354
- value: string;
355
- };
356
- type CommentNode = {
357
- kind: "comment";
358
- id: NodeId;
359
- parent: NodeId | null; /** Comment body (between `<!--` and `-->`). */
360
- value: string;
361
- };
362
- type CDataNode = {
363
- kind: "cdata";
364
- id: NodeId;
365
- parent: NodeId | null;
366
- value: string;
367
- };
368
- type PiNode = {
369
- kind: "pi";
370
- id: NodeId;
371
- parent: NodeId | null; /** PI target, e.g. "xml" for `<?xml ... ?>`. */
372
- target: string;
373
- value: string;
374
- };
375
- type DoctypeNode = {
376
- kind: "doctype";
377
- id: NodeId;
378
- parent: NodeId | null; /** Full doctype declaration body, e.g. `svg PUBLIC "..." "..."`. */
379
- value: string;
380
- };
381
- type AnyNode = ElementNode | TextNode | CommentNode | CDataNode | PiNode | DoctypeNode;
382
- //#endregion
383
- //#region src/core/document.d.ts
384
- interface DocumentEvents {
385
- /** Fires after any structural mutation. */
386
- on_change(fn: () => void): () => void;
387
- }
388
- declare class SvgDocument implements DocumentEvents {
389
- private nodes;
390
- private prolog;
391
- private epilog;
392
- /** Snapshot of the input string, used for `reset()`. */
393
- private source;
394
- /** Original parse result, for `reset()`. */
395
- private original;
396
- readonly root: NodeId;
397
- private listeners;
398
- /**
399
- * Counter that bumps ONLY when something the hierarchy view cares about
400
- * changes — tree topology (`insert`/`remove`), text-node content
401
- * (`set_text`), or the `id` attribute (which feeds display labels). Pure
402
- * presentation-attribute writes (x, y, fill, …) do NOT bump it.
403
- *
404
- * Why a separate counter: consumers like the layers panel cache snapshots
405
- * keyed on this. During a drag, x/y writes fire `emit()` repeatedly but
406
- * `structure_version` stays stable, so the panel's snapshot reference
407
- * stays the same and React skips the re-render of the whole tree.
408
- */
409
- private _structure_version;
410
- constructor(svg: string);
411
- static parse(svg: string): SvgDocument;
412
- /** Reload from the original parse, discarding all edits. */
413
- reset_to_original(): void;
414
- /** Replace document with new svg source (clears edits + history-owned state). */
415
- load(svg: string): void;
416
- on_change(fn: () => void): () => void;
417
- /** See `_structure_version` for what this counter signals. */
418
- get structure_version(): number;
419
- private emit;
420
- /** Notify subscribers — for callers that mutate directly via setAttr/etc. */
421
- notify(): void;
422
- get(id: NodeId): AnyNode | null;
423
- is_element(id: NodeId): boolean;
424
- parent_of(id: NodeId): NodeId | null;
425
- children_of(id: NodeId): readonly NodeId[];
426
- /** Element children only — text/comment/cdata filtered out. */
427
- element_children_of(id: NodeId): readonly NodeId[];
428
- next_sibling_of(id: NodeId): NodeId | null;
429
- next_element_sibling_of(id: NodeId): NodeId | null;
430
- tag_of(id: NodeId): string;
431
- contains(ancestor: NodeId, descendant: NodeId): boolean;
432
- all_nodes(): readonly NodeId[];
433
- all_elements(): readonly NodeId[];
434
- find_by_tag(ancestor: NodeId, tag: string): readonly NodeId[];
435
- /** Read attribute by local name, optionally namespace-filtered. */
436
- get_attr(id: NodeId, name: string, ns?: string | null): string | null;
437
- /**
438
- * Set / remove an attribute. If the attribute exists, it is mutated in place
439
- * (preserving source position). If it doesn't, it's appended.
440
- */
441
- set_attr(id: NodeId, name: string, value: string | null, ns?: string | null): void;
442
- attributes_of(id: NodeId): {
443
- name: string;
444
- ns: string | null;
445
- value: string;
446
- }[];
447
- get_style(id: NodeId, property: string): string | null;
448
- set_style(id: NodeId, property: string, value: string | null): void;
449
- get_all_styles(id: NodeId): Array<{
450
- property: string;
451
- value: string;
452
- }>;
453
- /**
454
- * Whether `id` can be opened in the flat-string text editor.
455
- *
456
- * v1 contract: the editor only operates on a *single flat text run*. That
457
- * means the target must be a `<text>` or `<tspan>` whose direct children
458
- * are all text nodes (or it has no children). A `<text>` containing a
459
- * `<tspan>` is *not* honestly editable — `text_of` would drop the tspan
460
- * content from the editor's view, and a flat-text write would leave the
461
- * tspan dangling. Tspan-as-target is fine and well-defined when it's a
462
- * leaf; only the host decides whether to route double-click to a tspan
463
- * or its parent text.
464
- */
465
- is_text_edit_target(id: NodeId): boolean;
466
- text_of(id: NodeId): string;
467
- /** Replace all direct text children with a single text node carrying `value`. */
468
- set_text(id: NodeId, value: string): void;
469
- insert(id: NodeId, parent: NodeId, before: NodeId | null): void;
470
- remove(id: NodeId): void;
471
- /** Create a new element node and register it (not yet inserted). */
472
- create_element(local: string, opts?: {
473
- prefix?: string | null;
474
- ns?: string | null;
475
- }): NodeId;
476
- serialize(): string;
477
- private emit_node;
478
- private emit_attr;
479
- }
480
- //#endregion
481
- //#region src/core/defs.d.ts
482
- interface GradientsApi {
483
- list(): ReadonlyArray<GradientEntry>;
484
- get(id: string): GradientEntry | null;
485
- upsert(definition: GradientDefinition, opts?: {
486
- id?: string;
487
- }): string;
488
- remove(id: string): void;
489
- subscribe(fn: (entries: ReadonlyArray<GradientEntry>) => void): Unsubscribe;
490
- }
491
- type Defs = {
492
- gradients: GradientsApi;
493
- };
494
- //#endregion
495
- //#region src/commands/registry.d.ts
496
- /**
497
- * Command registry.
498
- *
499
- * A passive id-keyed registry of handlers. Built so that:
500
- *
501
- * - keybindings (in `src/keymap`) can address commands by stable id;
502
- * - new commands can be added in ONE place (`src/commands/defaults.ts`)
503
- * without growing the public surface of the editor;
504
- * - "one key, many meanings" can be expressed via chain semantics: a
505
- * handler returns `true` if it consumed, `false`/`void` otherwise,
506
- * and the dispatcher tries the next candidate in the chain.
507
- *
508
- * Handlers are plain closures — they capture whatever editor reference
509
- * they need. The registry itself stays unaware of the editor's type,
510
- * which avoids a circular type dependency between editor and registry.
511
- */
512
- /** Stable, dotted id for a command, e.g. `"history.undo"`. */
513
- type CommandId = string;
514
- /**
515
- * A command handler.
516
- *
517
- * Return `true` if the handler consumed the invocation. Return `false`
518
- * or `undefined` to signal "did not apply" — the dispatcher will try
519
- * the next candidate registered for the same key.
520
- *
521
- * Handlers are closures: they capture their editor reference. No
522
- * editor parameter is passed — keep handlers self-contained.
523
- */
524
- type CommandHandler = (args?: unknown) => boolean | void;
525
- declare class CommandRegistry {
526
- private readonly map;
527
- /**
528
- * Register a command. Returns an unregister function. Re-registering
529
- * the same id replaces the previous handler (last writer wins).
530
- */
531
- register(id: CommandId, handler: CommandHandler): () => void;
532
- /**
533
- * Invoke a command by id. Returns `true` if the handler consumed,
534
- * `false` otherwise (including unknown ids and handlers that returned
535
- * `false`/`undefined`).
536
- */
537
- invoke(id: CommandId, args?: unknown): boolean;
538
- has(id: CommandId): boolean;
539
- /** All registered ids, for debugging / introspection. */
540
- ids(): readonly CommandId[];
541
- }
542
- //#endregion
543
- //#region src/keymap/keymap.d.ts
544
- type KeymapBinding = {
545
- /** Declarative key combination. Build with `kb()` / `c()` / `seq()`. */keybinding: Keybinding; /** Command id to invoke on match. */
546
- command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
547
- args?: unknown; /** Higher priorities run first in the chain. Default 0. */
548
- priority?: number;
549
- /**
550
- * Reserved for V2; not honored by the V1 dispatcher. When added, this
551
- * will be evaluated before the handler runs; if false, the binding is
552
- * skipped without invoking the handler.
553
- */
554
- when?: (ctx: unknown) => boolean;
555
- };
556
- declare class Keymap {
557
- private readonly commands;
558
- private readonly platformGetter;
559
- /**
560
- * Bindings bucketed by canonical chunk-key hash, computed per
561
- * `@grida/keybinding`'s `chunkKey`. Each list is the chain for that
562
- * key, sorted in dispatch order (priority desc, then registration
563
- * order).
564
- */
565
- private readonly buckets;
566
- /** Insert order, so ties on priority are deterministic. */
567
- private seq;
568
- constructor(commands: CommandRegistry, platformGetter?: () => Platform);
569
- /**
570
- * Bind a key combination to a command. Returns an unbind function.
571
- * The same `Keybinding` can be bound to multiple commands — they will
572
- * all be tried in chain order on dispatch.
573
- */
574
- bind(binding: KeymapBinding): () => void;
575
- /**
576
- * Remove bindings matching the spec. If both filters are passed, only
577
- * bindings that match BOTH are removed.
578
- */
579
- unbind(spec: {
580
- keybinding?: Keybinding;
581
- command?: CommandId;
582
- }): void;
583
- /** All registered bindings, for introspection. Order is not guaranteed. */
584
- bindings(): readonly KeymapBinding[];
585
- /**
586
- * Match the event against bound chunks, then run candidates in chain
587
- * order. Returns `true` and calls `preventDefault()` on the first
588
- * handler that consumes; returns `false` if nothing matched or all
589
- * matches fell through.
590
- */
591
- dispatch(event: KeyboardEvent): boolean;
592
- /**
593
- * Compute the set of canonical hashes a `Keybinding` lights up. A
594
- * binding with aliases (multiple sequences) contributes one hash per
595
- * single-chunk alias; multi-chunk sequences (chords) are skipped in
596
- * V1.
597
- */
598
- private chunkKeysFor;
599
- private has_safe_mod;
600
- }
601
- //#endregion
602
- //#region src/core/properties.d.ts
603
- declare function read_property(doc: SvgDocument, id: NodeId, property: string): PropertyValue;
604
- //#endregion
605
- //#region src/gestures/gestures.d.ts
606
- /** Stable identifier for a gesture binding. Used by `unbind({ id })`. */
607
- type GestureId = string;
608
- /**
609
- * Context passed to every installer. Exposes the seams a gesture needs:
610
- * the container element to listen on, the camera to mutate, and the
611
- * editor for keymap dispatch / state reads.
612
- *
613
- * Surface authors construct this once at attach; bindings receive it on
614
- * every `install(...)` call.
615
- */
616
- type GestureContext = {
617
- /** Container element listeners attach to. */container: HTMLElement; /** SVG element being framed by the camera. Useful for hit-testing. */
618
- svg_root: () => SVGSVGElement | null; /** HUD canvas overlay; sits on top of the SVG. */
619
- hud_canvas: HTMLCanvasElement; /** Camera the binding mutates. */
620
- camera: Camera; /** Editor for keymap dispatch / state reads. */
621
- editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
622
- handle: SurfaceHandle;
623
- };
624
- type GestureBinding = {
625
- /** Stable id used by `unbind` / `bindings()`. */id: GestureId;
626
- /**
627
- * Wire DOM listeners (or any side-effect) needed for this gesture.
628
- * Returns the uninstaller — called on `unbind` or surface detach.
629
- */
630
- install(ctx: GestureContext): () => void;
631
- };
632
- /**
633
- * Sibling to `Keymap`. Owns a list of installed gesture bindings; each
634
- * binding's `install(ctx)` is called eagerly when bound and uninstalled
635
- * on `unbind` or surface detach.
636
- */
637
- declare class Gestures {
638
- private readonly ctx;
639
- private entries;
640
- constructor(ctx: GestureContext);
641
- /**
642
- * Install a gesture binding. Returns an unbind function.
643
- * Re-binding the same `id` does NOT replace — both will be active.
644
- * Use `unbind({ id })` first if you want a clean swap.
645
- */
646
- bind(binding: GestureBinding): () => void;
647
- /**
648
- * Remove bindings matching the spec. With `{ id }`, all bindings with
649
- * that id are uninstalled. With no spec, this is a no-op (use
650
- * `dispose()` to nuke everything).
651
- */
652
- unbind(spec: {
653
- id?: GestureId;
654
- }): void;
655
- /** All currently installed bindings. Order is registration order. */
656
- bindings(): readonly GestureBinding[];
657
- /** @internal Uninstall every binding. Surface calls on detach. */
658
- _dispose(): void;
659
- }
660
- //#endregion
661
- //#region src/core/editor.d.ts
662
- /** Resolved paint from the DOM-attached cascade. `resolved_paint` mirrors the
663
- * shape of `PaintValue.computed` so consumers can treat it uniformly with
664
- * the headless cascade. */
665
- type DomComputedPaint = {
666
- computed: string;
667
- resolved_paint: Paint | InvalidComputedValue | null;
668
- };
669
- /** Contract the DOM surface implements to delegate cascade resolution to
670
- * `getComputedStyle()`. Registered via `editor._internal.set_computed_resolver`
671
- * on attach. */
672
- type DomComputedResolver = {
673
- computed_property(id: NodeId, name: string): string | null;
674
- computed_paint(id: NodeId, channel: "fill" | "stroke"): DomComputedPaint | null;
675
- };
676
- type CreateSvgEditorOptions = {
677
- svg: string;
678
- providers?: Providers;
679
- style?: Partial<EditorStyle>;
680
- };
681
- type SvgEditor = ReturnType<typeof createSvgEditor>;
682
- type Surface = {
683
- paint(snapshot: unknown): void;
684
- hit_test(x: number, y: number): NodeId | null;
685
- on_input(listener: (event: unknown) => void): Unsubscribe;
686
- dispose(): void;
687
- };
688
- type SurfaceHandle = {
689
- detach(): void;
690
- };
691
- /**
692
- * Mode for `commands.select`. Matches the HUD's `SelectMode` vocabulary so
693
- * intents can be threaded through without lossy collapsing.
694
- *
695
- * - `"replace"` (default) — set selection to `ids`, discarding the previous set.
696
- * - `"add"` — union: each id in `ids` is added; existing members stay.
697
- * - `"toggle"` — flip each id's membership (present → removed; absent → added).
698
- */
699
- type SelectMode = "replace" | "add" | "toggle";
700
- type Commands = {
701
- select(target: NodeId | ReadonlyArray<NodeId>, opts?: {
702
- mode?: SelectMode;
703
- }): void;
704
- deselect(): void;
705
- enter_scope(group: NodeId): void;
706
- exit_scope(): void;
707
- set_mode(mode: Mode): void;
708
- set_property(name: string, value: string | null): void;
709
- preview_property(name: string): PreviewSession;
710
- set_paint(channel: "fill" | "stroke", paint: Paint): void;
711
- preview_paint(channel: "fill" | "stroke"): PaintPreviewSession;
712
- set_paint_from_gradient(channel: "fill" | "stroke", definition: GradientDefinition, opts?: {
713
- reuse_existing?: boolean;
714
- }): {
715
- gradient_id: string;
716
- };
717
- translate(delta: {
718
- dx: number;
719
- dy: number;
720
- }): void;
721
- reorder(direction: ReorderDirection): void;
722
- remove(): void;
723
- set_text(value: string): void;
724
- load_svg(svg: string): void;
725
- serialize_svg(): string;
726
- undo(): void;
727
- redo(): void;
728
- /**
729
- * Register a command handler under a stable id. Returns an unregister
730
- * function. Re-registering the same id replaces the previous handler.
731
- *
732
- * Handlers return `true` if they consumed the invocation; `false` or
733
- * `undefined` signal "did not apply" — the keymap dispatcher will try
734
- * the next candidate in the chain.
735
- */
736
- register(id: CommandId, handler: CommandHandler): () => void;
737
- /**
738
- * Invoke a registered command by id. Returns `true` if a handler
739
- * consumed the invocation, `false` otherwise (including unknown ids).
740
- */
741
- invoke(id: CommandId, args?: unknown): boolean; /** Whether an id has a registered handler. */
742
- has(id: CommandId): boolean;
743
- };
744
- declare function createSvgEditor(opts: CreateSvgEditorOptions): {
745
- /**
746
- * Low-level IR handle. Mutating directly bypasses history; prefer
747
- * `editor.commands` for app code.
748
- */
749
- document: SvgDocument;
750
- readonly state: EditorState;
751
- subscribe: (fn: (state: EditorState) => void) => Unsubscribe;
752
- subscribe_with_selector: <T>(selector: (s: EditorState) => T, fn: (value: T, prev: T) => void, equals?: (a: T, b: T) => boolean) => Unsubscribe;
753
- node_properties: (id: NodeId, names: ReadonlyArray<string>) => {
754
- readonly [name: string]: ReturnType<typeof read_property>;
755
- };
756
- node_paint: (id: NodeId, channel: "fill" | "stroke") => PaintValue;
757
- dom_computed_property: (id: NodeId, name: string) => string | null;
758
- dom_computed_paint: (id: NodeId, channel: "fill" | "stroke") => DomComputedPaint | null;
759
- /**
760
- * Enter content-edit mode on a `<text>` node. Returns `false` (no-op)
761
- * when no DOM surface is attached.
762
- */
763
- enter_content_edit: (target?: NodeId) => boolean;
764
- defs: Defs;
765
- commands: Commands;
766
- /**
767
- * Human-readable label for hierarchy panels. SVG has no native "name";
768
- * this is the package's single source of truth so panels don't reinvent
769
- * the rule.
770
- *
771
- * Rule:
772
- * - `<text>` → text content, whitespace-collapsed and truncated at
773
- * ~40 chars (falls back to `"text"` for empty content).
774
- * - Otherwise → tag name, suffixed with `#id` when the `id` attribute
775
- * is present (e.g. `"rect #sun"`).
776
- *
777
- * `opts.tagLabel` lets callers substitute a friendlier or localized
778
- * term for the raw tag (e.g. `"rect"` → `"Rectangle"`). Only invoked
779
- * on the non-text branch.
780
- */
781
- display_label(id: NodeId, opts?: {
782
- tagLabel?: (tag: string) => string;
783
- }): string;
784
- tree(): {
785
- root: NodeId;
786
- nodes: ReadonlyMap<NodeId, {
787
- id: NodeId;
788
- tag: string;
789
- name?: string;
790
- parent: NodeId | null;
791
- children: ReadonlyArray<NodeId>;
792
- }>;
793
- };
794
- /**
795
- * The effective hover from the attached HUD surface — what's under the
796
- * pointer, OR whatever `set_surface_hover_override` last pushed. Used
797
- * by out-of-canvas UI (layers panel, breadcrumbs) to mirror the canvas
798
- * highlight. Returns `null` when nothing is hovered.
799
- */
800
- surface_hover(): NodeId | null;
801
- /**
802
- * Push a hover override into the HUD surface — e.g. when the user
803
- * hovers a row in a layers panel. The HUD will render the override's
804
- * outline and (when applicable) drive measurement to that node.
805
- * Pass `null` to clear and let the pointer pick take over again.
806
- */
807
- set_surface_hover_override(id: NodeId | null): void;
808
- /**
809
- * Subscribe to changes in the effective surface hover. Fires when the
810
- * HUD reports a new pointer pick AND when an override is set/cleared.
811
- * Cheap channel — does NOT bump `state.version`.
812
- */
813
- subscribe_surface_hover(cb: () => void): () => void;
814
- modes: readonly Mode[];
815
- readonly style: Readonly<EditorStyle>;
816
- set_style: (partial: Partial<EditorStyle>) => void;
817
- load: (svg: string) => void;
818
- serialize: () => string;
819
- reset: () => void;
820
- attach: (surface: Surface) => SurfaceHandle;
821
- detach: () => void;
822
- dispose: () => void;
823
- providers: Providers;
824
- _internal: {
825
- doc: SvgDocument;
826
- history: {
827
- preview: (label: string) => _$_grida_history0.Preview;
828
- };
829
- emit: () => void;
830
- set_content_edit_driver(fn: ((target: NodeId) => boolean) | null): void;
831
- /** Fires the driver immediately with the current override so the
832
- * surface can sync state on attach. */
833
- set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
834
- push_surface_hover(id: NodeId | null): void;
835
- set_computed_resolver(fn: DomComputedResolver | null): void;
836
- };
837
- keymap: Keymap;
838
- };
839
- //#endregion
840
- export { InvalidComputedValue as A, Provenance as B, EditorState as C, GradientDefinition as D, FontResolver as E, PaintFallback as F, Unsubscribe as G, RadialGradientDefinition as H, PaintPreviewSession as I, Vec2 as K, PaintValue as L, Mode as M, NodeId as N, GradientEntry as O, Paint as P, PreviewSession as R, DEFAULT_STYLE as S, FileIOProvider as T, Rect as U, Providers as V, ReorderDirection as W, Camera as _, SelectMode as a, ClipboardProvider as b, createSvgEditor as c, GestureId as d, Gestures as f, BoundsResolver as g, CommandId as h, DomComputedResolver as i, LinearGradientDefinition as j, GradientStop as k, GestureBinding as l, CommandHandler as m, CreateSvgEditorOptions as n, SurfaceHandle as o, KeymapBinding as p, DomComputedPaint as r, SvgEditor as s, Commands as t, GestureContext as u, CameraConstraints as v, EditorStyle as w, Color as x, CameraOptions as y, PropertyValue as z };