@grida/svg-editor 1.0.0-alpha.13 → 1.0.0-alpha.14

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.
@@ -1,7 +1,7 @@
1
- import * as _$_grida_history0 from "@grida/history";
2
1
  import { Keybinding, Platform } from "@grida/keybinding";
3
2
  import cmath from "@grida/cmath";
4
3
  import { AnyNode } from "@grida/svg/parser";
4
+ import vn from "@grida/vn";
5
5
  import { SelectMode } from "@grida/hud";
6
6
 
7
7
  //#region src/types.d.ts
@@ -24,12 +24,14 @@ type Rect = {
24
24
  };
25
25
  type Mode = "select" | "edit-content";
26
26
  /**
27
- * SVG element tags supported by the insertion subsystem. Closed set; adding
28
- * a new insertable tag requires a PR.
27
+ * SVG element tags inserted by the **drag-to-size** subsystem. Closed set;
28
+ * adding a new insertable tag requires a PR.
29
29
  *
30
- * `text` is deliberately out of v1: `<text>` has no intrinsic size and any
31
- * usable text-insert UX must immediately mount the inline content-editor on
32
- * the new node rather than dropping a placeholder. Reserved for a future PR.
30
+ * `text` is intentionally NOT here. It is creatable (the `insert-text`
31
+ * tool see {@link Tool}), but via a **click-only** gesture, not
32
+ * drag-to-size: `<text>` has no intrinsic size, so there is nothing for a
33
+ * drag to set. Its creation path mounts the inline content-editor
34
+ * immediately. Design: `docs/wg/feat-svg-editor/text-tool.md`.
33
35
  */
34
36
  type InsertableTag = "rect" | "ellipse" | "line";
35
37
  /**
@@ -48,6 +50,39 @@ type Tool = {
48
50
  } | {
49
51
  type: "insert";
50
52
  tag: InsertableTag;
53
+ }
54
+ /**
55
+ * Text creation tool. A select-mode tool like `insert`, but **click-only**
56
+ * rather than drag-to-size: pointer-down creates a single-line `<text>` at
57
+ * the click point with default appearance and immediately enters
58
+ * content-edit (caret active). `<text>` has no intrinsic size, so the
59
+ * drag-to-size model doesn't apply; a drag box would mean SVG 2 wrapped
60
+ * text, which is a separate (out-of-scope) model. Reverts to `cursor`
61
+ * after the node is placed. Design:
62
+ * `docs/wg/feat-svg-editor/text-tool.md`.
63
+ */
64
+ | {
65
+ type: "insert-text";
66
+ }
67
+ /**
68
+ * Vector content-edit lasso (Q). Empty-space drag draws a freeform
69
+ * polygon that picks vertices + tangents inside it (segments are NOT
70
+ * tested — matches the main editor decision; see `@grida/hud`
71
+ * `VectorSelectionMode`). Valid only while `state.mode === "edit-content"`
72
+ * on a path; tool reverts to cursor on path-content-edit exit.
73
+ */
74
+ | {
75
+ type: "lasso";
76
+ }
77
+ /**
78
+ * Vector content-edit bend. Sticky version of holding Meta — every
79
+ * segment-body drag bends instead of translating, regardless of
80
+ * Meta state. See `@grida/hud` `VectorBendMode`. Valid only while
81
+ * `state.mode === "edit-content"` on a path; tool reverts to cursor
82
+ * on path-content-edit exit.
83
+ */
84
+ | {
85
+ type: "bend";
51
86
  };
52
87
  declare const TOOL_CURSOR: Tool;
53
88
  type Provenance = {
@@ -221,8 +256,24 @@ type EditorState = {
221
256
  * any change — selection, history, mutation. NOT a good cache key for
222
257
  * tree-shape views because it fires on attribute writes too (e.g. x/y
223
258
  * during a drag).
259
+ *
260
+ * NOT a content fingerprint either: this bumps on UI-state emissions
261
+ * (selection, scope, mode, tool) that leave the serialized SVG
262
+ * unchanged. For "did the document content change?" use
263
+ * {@link content_version}.
224
264
  */
225
265
  readonly version: number;
266
+ /**
267
+ * Bumps on every document mutation — insert, remove, reorder, attribute
268
+ * write, style write, undo, redo, load. Stable across pure UI-state
269
+ * emissions (selection, scope, mode, tool).
270
+ *
271
+ * The honest fingerprint for serialized content: if `content_version`
272
+ * is unchanged, `editor.serialize()` returns the same bytes. Use this
273
+ * — not `version` — as the freshness token when persisting, diffing,
274
+ * or hashing the document.
275
+ */
276
+ readonly content_version: number;
226
277
  /**
227
278
  * Bumps only when the document's tree shape or display-label-affecting
228
279
  * data changes — node added/removed/reordered, text content, or the
@@ -434,6 +485,33 @@ declare class Camera {
434
485
  type AlignDirection = "left" | "right" | "top" | "bottom" | "horizontal_centers" | "vertical_centers";
435
486
  //#endregion
436
487
  //#region src/core/document.d.ts
488
+ /**
489
+ * What `is_vector_edit_target` returns when a node is eligible for
490
+ * vector (vertex) editing — a tag-discriminated snapshot of the authored
491
+ * geometry attributes at enter time.
492
+ *
493
+ * Consumed by `VectorEditSession` (which holds it as `source_attrs`) and by
494
+ * `PathModel` (whose `toNativeAttrs(source_tag)` decides on each commit
495
+ * whether the post-edit form is still expressible in the source tag, or
496
+ * whether the element must promote to `<path d="…">`).
497
+ *
498
+ * Geometry conventions:
499
+ * - All coordinates are in the element's own local space, exactly as
500
+ * authored. No `transform=` resolution, no parent CTM, no viewport
501
+ * remap.
502
+ * - `polyline` / `polygon` points are `[x, y]` tuples so the consumer
503
+ * can hand them straight to `vn.fromPolyline` / `vn.fromPolygon`.
504
+ */
505
+ type VectorEditSource = {
506
+ kind: "path";
507
+ d: string;
508
+ } | {
509
+ kind: "polyline";
510
+ points: ReadonlyArray<readonly [number, number]>;
511
+ } | {
512
+ kind: "polygon";
513
+ points: ReadonlyArray<readonly [number, number]>;
514
+ };
437
515
  interface DocumentEvents {
438
516
  /** Fires after any structural mutation. */
439
517
  on_change(fn: () => void): () => void;
@@ -462,7 +540,7 @@ declare class SvgDocument implements DocumentEvents {
462
540
  private _structure_version;
463
541
  /** Bumps on writes that can shift world-space bounds (`GEOMETRY_ATTRS`,
464
542
  * `set_text`, `insert`, `remove`). Cache key for `GeometryProvider`;
465
- * see docs/wg/feat-svg-editor/geometry.md. */
543
+ * see ../../docs/geometry.md. */
466
544
  private _geometry_version;
467
545
  constructor(svg: string);
468
546
  static parse(svg: string): SvgDocument;
@@ -547,6 +625,44 @@ declare class SvgDocument implements DocumentEvents {
547
625
  * or its parent text.
548
626
  */
549
627
  is_text_edit_target(id: NodeId): boolean;
628
+ /**
629
+ * Returns a tag-discriminated snapshot of the authored geometry attrs
630
+ * if this node is eligible for vector (vertex) editing — else `null`.
631
+ *
632
+ * v1 eligibility:
633
+ * - `<path>` — requires non-empty `d`.
634
+ * - `<polyline>` — requires `points` parseable to ≥ 2 vertices.
635
+ * - `<polygon>` — same as polyline.
636
+ *
637
+ * Deliberately rejects `<line>` in v1: the only useful vertex-edit
638
+ * gestures on a `<line>` are (a) introducing a new vertex (which would
639
+ * have to promote it to `<polyline>`) and (b) bending it with a tangent
640
+ * (which would have to promote it to `<path>`). Both promotions are
641
+ * out of scope for v1, so opening a `<line>` in vector-edit mode would
642
+ * advertise capabilities that don't work.
643
+ *
644
+ * Also rejects `<rect>`, `<circle>`, `<ellipse>`, `<image>`, `<use>` —
645
+ * those would force the same promotion-to-`<path>` machinery (trivia
646
+ * transfer, cross-cutting attr carry, DOM-element swap, history-bracket
647
+ * changes) that v1 keeps out of scope.
648
+ */
649
+ is_vector_edit_target(id: NodeId): VectorEditSource | null;
650
+ /**
651
+ * True iff this `<text>` / `<tspan>` carries a non-empty `rotate=""`
652
+ * per-glyph attribute (which conflicts with element-level rotation).
653
+ */
654
+ has_glyph_rotate(id: NodeId): boolean;
655
+ /**
656
+ * True iff this element's inline `style=""` declares a `transform:`
657
+ * CSS property (which would shadow the editor's `transform=` writes).
658
+ */
659
+ has_inline_css_transform(id: NodeId): boolean;
660
+ /**
661
+ * True iff this element has a direct `<animateTransform>` child
662
+ * (which produces a time-varying transform invisible to attribute writes).
663
+ * Only direct children are checked — nested cases attach to the nearer ancestor.
664
+ */
665
+ has_animate_transform_child(id: NodeId): boolean;
550
666
  text_of(id: NodeId): string;
551
667
  /** Replace all direct text children with a single text node carrying `value`. */
552
668
  set_text(id: NodeId, value: string): void;
@@ -562,6 +678,262 @@ declare class SvgDocument implements DocumentEvents {
562
678
  private emit_attr;
563
679
  }
564
680
  //#endregion
681
+ //#region src/core/vector-edit/model.d.ts
682
+ type Verb = "M" | "L" | "H" | "V" | "C" | "S" | "Q" | "T" | "A" | "Z";
683
+ type VertexId = number;
684
+ type SegmentId = number;
685
+ /** `[vertex_idx, 0]` = ta on segment whose `a === vertex_idx`; `[vertex_idx, 1]` = tb where `b === vertex_idx`. */
686
+ type TangentRef = readonly [VertexId, 0 | 1];
687
+ /**
688
+ * Tangent mirroring policy applied around a vertex when one tangent moves.
689
+ * Mirrors `vn.TangentMirroringMode`.
690
+ *
691
+ * - `auto` — infer from current state (smooth join → mirror angle+length;
692
+ * broken / asymmetric → don't mirror).
693
+ * - `none` — only move the chosen tangent. Other tangents at this vertex
694
+ * stay put.
695
+ * - `angle` — keep opposite tangent collinear (mirror angle), preserve its
696
+ * length. Used when the user wants a sharp-vs-smooth-but-asymmetric
697
+ * handle (Figma's "Mirror angle" mode).
698
+ * - `all` — mirror both angle and length. Standard "smooth" handle pair.
699
+ */
700
+ type TangentMirrorMode = "auto" | "none" | "angle" | "all";
701
+ type SegmentView = {
702
+ a: VertexId;
703
+ b: VertexId;
704
+ ta: readonly [number, number];
705
+ tb: readonly [number, number];
706
+ source_verb?: Verb;
707
+ };
708
+ type PathSnapshot = {
709
+ vertices: ReadonlyArray<readonly [number, number]>;
710
+ segments: ReadonlyArray<SegmentView>;
711
+ };
712
+ type SubSelection = {
713
+ vertices: ReadonlyArray<VertexId>;
714
+ segments: ReadonlyArray<SegmentId>;
715
+ tangents: ReadonlyArray<TangentRef>;
716
+ };
717
+ /**
718
+ * Per-segment metadata maintained alongside vn's segment array.
719
+ * `meta[i]` corresponds to `network.segments[i]`.
720
+ */
721
+ type SegmentMeta = {
722
+ /** The SVG verb that originally produced this segment, if known. */source_verb?: Verb; /** Arc-specific metadata for segments born from an `A` command. */
723
+ arc?: ArcMeta; /** True iff this segment was emitted by a `Z` command (closing the subpath). */
724
+ is_close_segment?: boolean;
725
+ };
726
+ /**
727
+ * When an `A` command is parsed, it decomposes to N cubic segments.
728
+ * All segments in the same arc share the same `group_id` and the same
729
+ * arc parameters, plus each carries a snapshot of its original tangents
730
+ * (used at emit time to detect "still an arc" vs "user has edited").
731
+ */
732
+ type ArcMeta = {
733
+ group_id: number;
734
+ rx: number;
735
+ ry: number;
736
+ x_rot: number;
737
+ large_arc_flag: 0 | 1;
738
+ sweep_flag: 0 | 1; /** Snapshot of this segment's ta at parse time (relative). */
739
+ baseline_ta: cmath.Vector2; /** Snapshot of this segment's tb at parse time (relative). */
740
+ baseline_tb: cmath.Vector2; /** Snapshot of this segment's end-vertex absolute position at parse time. */
741
+ baseline_b_abs: cmath.Vector2; /** Sequence index within the arc group (0..N-1). */
742
+ seq: number; /** Total segments in the arc group. */
743
+ count: number;
744
+ /** Original SVG arc command's `(x, y)` endpoint. Only populated on the
745
+ * LAST segment of the arc group (seq === count - 1). Used by the emitter
746
+ * to write back the exact coordinate the author wrote, avoiding floating-
747
+ * point drift from arc-to-cubic decomposition. */
748
+ original_end?: cmath.Vector2;
749
+ };
750
+ /**
751
+ * Canonical vector-network model for a single SVG path's `d` string.
752
+ *
753
+ * `PathModel` is a self-contained geometry primitive — it parses an SVG
754
+ * path `d` into a vertex/segment graph (with verb hints preserved for
755
+ * round-trip honesty), exposes POJO observers, and serializes back to
756
+ * `d`. It does not hold or reference an `SvgDocument`, an editor
757
+ * instance, the DOM, or any host. It is safe to construct in any
758
+ * environment that can run the package.
759
+ *
760
+ * Public re-exported as a top-level Layer-A primitive from
761
+ * `@grida/svg-editor` for callers that want canonical path geometry
762
+ * without mounting an editor. The full mutation surface (translate /
763
+ * bend / set-tangent / split, etc.) is package-internal and may shift;
764
+ * the publicly-stable contract for external callers is the construction
765
+ * + serialization + observation methods documented at the entry point.
766
+ *
767
+ * @experimental Surface shape is v0; signatures may change before the
768
+ * package reaches semver stability.
769
+ */
770
+ declare class PathModel {
771
+ private readonly _network;
772
+ private readonly _meta;
773
+ private constructor();
774
+ static fromSvgPathD(d: string): PathModel;
775
+ /** Construct from a vn network with no verb info (every segment defaults to undefined verb). */
776
+ static fromVectorNetwork(network: vn.VectorNetwork): PathModel;
777
+ toSvgPathD(): string;
778
+ snapshot(): PathSnapshot;
779
+ bbox(): cmath.Rectangle;
780
+ vertexCount(): number;
781
+ segmentCount(): number;
782
+ /**
783
+ * If the model's current geometry is still expressible in the source
784
+ * SVG tag's native attribute form, return the equivalent
785
+ * `VectorEditSource` (which is also the writeable shape) — else `null`.
786
+ *
787
+ * This is the decider that gates per-gesture native-attrs writeback in
788
+ * `VectorEditSession.apply_d`. `null` means "the user's edit cannot be
789
+ * faithfully written back to the source tag" — in v1 with no
790
+ * promotion, the gesture is refused; in v1.1+ with promotion, the
791
+ * element is rewritten to `<path d="…">`.
792
+ *
793
+ * v1 expressibility (all source kinds require every segment's `ta` and
794
+ * `tb` to be exactly zero — any tangent edit forces promotion):
795
+ *
796
+ * - **path** — always `null` (no native fallback; the canonical form
797
+ * IS `<path d>`, so callers should just write `d` directly).
798
+ * - **polyline** — segments form the canonical open chain
799
+ * `0→1, 1→2, …, (n-2)→(n-1)`. (Topology after `vn.fromPolyline` and
800
+ * any sequence of vertex translates.)
801
+ * - **polygon** — segments form the canonical closed chain
802
+ * `0→1, 1→2, …, (n-1)→0`. (Topology after `vn.fromPolygon` and any
803
+ * sequence of vertex translates.)
804
+ *
805
+ * Anything that changes segment topology (insert-vertex, delete-vertex,
806
+ * close/open shape) leaves the canonical chain and returns `null` here;
807
+ * the higher layer is responsible for routing those to tag-promotion
808
+ * (intra-Vertex or to-path).
809
+ */
810
+ toNativeAttrs(source_tag: VectorEditSource["kind"]): Exclude<VectorEditSource, {
811
+ kind: "path";
812
+ }> | null;
813
+ /** Translate one vertex by `delta`. Connected segments follow because
814
+ * tangents are stored relative to vertices. Verb metadata is preserved
815
+ * as-is; emit-time honesty handles cases where the shape no longer
816
+ * matches the recorded verb (e.g. an H whose endpoint y-coord drifts). */
817
+ translateVertex(v: VertexId, delta: readonly [number, number]): PathModel;
818
+ /** Bulk-translate a set of vertices by the same delta. Atomic — either
819
+ * every move succeeds or none (input is validated up-front). */
820
+ translateVertices(indices: ReadonlyArray<VertexId>, delta: readonly [number, number]): PathModel;
821
+ /** Translate one segment by `delta` — moves both endpoints, dragging
822
+ * their tangents along (tangents are stored relative to vertices, so
823
+ * this is automatic). Other segments connected to the moved endpoints
824
+ * also follow at the shared vertex. */
825
+ translateSegment(seg: SegmentId, delta: readonly [number, number]): PathModel;
826
+ /**
827
+ * Bend a curve segment by dragging a point at parameter `ca` to `cb`
828
+ * (cb is in absolute doc-space). Delegates to vn's `bendSegment` —
829
+ * which solves for the new ta/tb that put `B(ca) === cb`, holding the
830
+ * endpoints fixed.
831
+ *
832
+ * The "frozen" snapshot of the segment at gesture start is the caller's
833
+ * responsibility. Convention: call this from a preview session where
834
+ * each frame replays from the baseline (same pattern as translate).
835
+ */
836
+ bendSegment(seg: SegmentId, ca: number, cb: readonly [number, number], frozen: {
837
+ a: readonly [number, number];
838
+ b: readonly [number, number];
839
+ ta: readonly [number, number];
840
+ tb: readonly [number, number];
841
+ }): PathModel;
842
+ /**
843
+ * Move one tangent control point to a new absolute position. Mirror
844
+ * policy follows vn's `updateTangent`. The other tangent at the same
845
+ * vertex is updated according to the policy.
846
+ *
847
+ * Returns a new PathModel; verb metadata is preserved verbatim.
848
+ * `toSvgPathD` will demote (e.g. L → C) if the new tangents make the
849
+ * recorded verb no longer match the geometry.
850
+ */
851
+ setTangent(t: TangentRef, abs_pos: readonly [number, number], mirror?: TangentMirrorMode): PathModel;
852
+ /**
853
+ * Split segment `seg` at parametric position `t ∈ [0,1]`, inserting a
854
+ * new vertex. Returns the new model and the **canonical (path-order)**
855
+ * index of the inserted vertex.
856
+ *
857
+ * Verb metadata for the split: the original segment's verb propagates
858
+ * to BOTH halves if it was a curve type (`C`/`S`/`Q`/`T`/`A`); for
859
+ * straight verbs (`L`/`H`/`V`), the split halves stay straight (their
860
+ * tangents are zero from vn's `preserveZero` path when both originals
861
+ * were zero). Arc-group identity is dropped from the halves — the
862
+ * arc is broken once split (the emitter will fall back to `C`/`L`).
863
+ *
864
+ * **Index space contract.** `VectorNetworkEditor.splitSegment` APPENDS
865
+ * the new vertex at the end of the network's vertices array — its
866
+ * index is the in-memory insertion order. But `toSvgPathD` / `fromSvgPathD`
867
+ * canonicalize vertices in path order, so the same vertex gets a
868
+ * DIFFERENT index in the d-derived model that consumers re-parse each
869
+ * frame (e.g., the host's `handle_translate_vertices`). Returning the
870
+ * insertion-order index causes the classic split-and-drag bug: the
871
+ * surface holds index N (insertion-order) but the live model has
872
+ * index M (path-order) at that position — drag moves the wrong vertex
873
+ * and the user sees "split happened but the new vertex doesn't move".
874
+ *
875
+ * To prevent that, we round-trip the post-split model through
876
+ * `toSvgPathD` → `fromSvgPathD` and return the canonical (path-order)
877
+ * index of the new vertex. The returned `model` is the canonical
878
+ * one, so any subsequent op on it uses the same index space the d
879
+ * roundtrip exposes. See `__tests__/README.md` §"index identity
880
+ * across the `d` round-trip" for the test pattern that pins this.
881
+ */
882
+ splitSegment(seg: SegmentId, t: number): {
883
+ model: PathModel;
884
+ new_vertex: VertexId;
885
+ };
886
+ /**
887
+ * Doc-space position of a tangent control point. `t` references a
888
+ * segment and which end (`a` or `b`) the tangent belongs to; the
889
+ * result is `vertex + tangent_value + origin`. Returns null if no
890
+ * segment has this tangent (e.g. the vertex is isolated).
891
+ */
892
+ tangentAbsolute(t: TangentRef, origin: readonly [number, number]): [number, number] | null;
893
+ /**
894
+ * Vertices "neighbouring" the current selection — these are the
895
+ * vertices whose tangent handles should render in chrome.
896
+ *
897
+ * Two-phase, mirrors `editor/grida-canvas/reducers/methods/vector.ts`
898
+ * `getUXNeighbouringVertices`:
899
+ *
900
+ * 1. Collect "active" vertices:
901
+ * - every selected vertex
902
+ * - every tangent-owning vertex
903
+ * - both endpoints of every selected segment
904
+ * 2. Expand uniformly to 1-hop neighbours (vertices sharing a segment
905
+ * with any active vertex).
906
+ *
907
+ * Without phase 2 for tangent / segment selections, selecting only a
908
+ * tangent would hide neighbouring-vertex tangents — the user loses
909
+ * spatial context. Phase 2 makes the affordance symmetric: whatever
910
+ * triggered selection, the 1-hop ring of tangent handles is visible.
911
+ *
912
+ * Sorted ascending; deduped.
913
+ */
914
+ neighbouringVertices(sel: SubSelection): VertexId[];
915
+ /**
916
+ * True iff segment `seg`'s curve is entirely contained in the rect.
917
+ * Delegates to `cmath.bezier.containedByRect`.
918
+ */
919
+ segmentContainedByRect(seg: SegmentId, rect: cmath.Rectangle, origin?: readonly [number, number]): boolean;
920
+ /** @internal */
921
+ _rawNetwork(): vn.VectorNetwork;
922
+ /** @internal */
923
+ _rawMeta(): ReadonlyArray<SegmentMeta>;
924
+ /**
925
+ * Map a `TangentRef` to a concrete `(segment_index, control)` pair.
926
+ *
927
+ * `[v, 0]` → first segment whose `a === v` (its `ta`).
928
+ * `[v, 1]` → first segment whose `b === v` (its `tb`).
929
+ *
930
+ * Y-junctions (multi-outgoing or multi-incoming) are uncommon for SVG
931
+ * `<path>` content; v1 picks the first match. If we ever support those
932
+ * cleanly, extend `TangentRef` to carry the segment id explicitly.
933
+ */
934
+ private _locateTangent;
935
+ }
936
+ //#endregion
565
937
  //#region src/core/defs.d.ts
566
938
  interface GradientsApi {
567
939
  list(): ReadonlyArray<GradientEntry>;
@@ -630,6 +1002,21 @@ type KeymapBinding = {
630
1002
  command: CommandId; /** Forwarded as the `args` parameter to the command handler. */
631
1003
  args?: unknown; /** Higher priorities run first in the chain. Default 0. */
632
1004
  priority?: number;
1005
+ /**
1006
+ * Bypass the form-element focus guard. When `true`, this binding fires
1007
+ * even if a text input is focused (`<input>`, `<textarea>`, or any
1008
+ * `contentEditable` element).
1009
+ *
1010
+ * Default `false`. The platform's native text-editing shortcuts —
1011
+ * Cmd+A (select all), Cmd+Z/Y (input undo/redo), Cmd+C/V/X
1012
+ * (clipboard), arrow keys, Backspace, Tab, Enter — must win over
1013
+ * editor shortcuts while the user is typing.
1014
+ *
1015
+ * Opt in sparingly. Reasonable candidates: truly global app shortcuts
1016
+ * like Cmd+S (save). Unreasonable candidates: anything that has a
1017
+ * native text-editing meaning.
1018
+ */
1019
+ allowInFormElement?: boolean;
633
1020
  /**
634
1021
  * Reserved for V2; not honored by the V1 dispatcher. When added, this
635
1022
  * will be evaluated before the handler runs; if false, the binding is
@@ -675,8 +1062,9 @@ declare class Keymap {
675
1062
  * bar even when the binding's handler rejects.
676
1063
  *
677
1064
  * Pure read; runs no handlers, no side effects. Honors the same
678
- * text-input-focused guard `dispatch` uses, so a typing user's
679
- * keystroke isn't "claimed" by an unrelated unmodified key.
1065
+ * form-element focus guard `dispatch` uses, so a typing user's
1066
+ * keystroke isn't "claimed" and the browser's native text-editing
1067
+ * default (Cmd+A select all, Cmd+Z undo, etc.) wins.
680
1068
  */
681
1069
  claims(event: KeyboardEvent): boolean;
682
1070
  /**
@@ -684,6 +1072,12 @@ declare class Keymap {
684
1072
  * order. Returns `true` on the first handler that consumes; returns
685
1073
  * `false` if nothing matched or all matches fell through.
686
1074
  *
1075
+ * **Form-element focus guard.** When a text input is focused
1076
+ * (`<input>`, `<textarea>`, contentEditable), bindings are suppressed
1077
+ * by default so the platform's native shortcuts (Cmd+A, Cmd+Z, Cmd+C,
1078
+ * arrow nav, …) are preserved. A binding can opt out of this guard
1079
+ * with `allowInFormElement: true` — see `KeymapBinding`.
1080
+ *
687
1081
  * `dispatch` is browser-agnostic: it does NOT call `preventDefault()`
688
1082
  * or touch the event in any way. The host decides what to do with the
689
1083
  * platform default — typically `if (keymap.claims(e)) e.preventDefault()`,
@@ -698,7 +1092,6 @@ declare class Keymap {
698
1092
  * V1.
699
1093
  */
700
1094
  private chunkKeysFor;
701
- private has_safe_mod;
702
1095
  }
703
1096
  //#endregion
704
1097
  //#region src/core/geometry.d.ts
@@ -738,7 +1131,7 @@ type GeometrySignals = {
738
1131
  };
739
1132
  /**
740
1133
  * Caches `bounds_of` results keyed on `NodeId`; full-clears on either
741
- * `structure_version` or `geometry_version` bump. See docs/wg/feat-svg-editor/geometry.md for
1134
+ * `structure_version` or `geometry_version` bump. See ../../docs/geometry.md for
742
1135
  * why the cache is load-bearing under the surface's per-tick re-render.
743
1136
  */
744
1137
  declare class MemoizedGeometryProvider implements GeometryProvider {
@@ -777,6 +1170,14 @@ type GestureContext = {
777
1170
  camera: Camera; /** Editor for keymap dispatch / state reads. */
778
1171
  editor: SvgEditor; /** Handle for advanced bindings (e.g. wanting `camera.fit("<selection>")`). */
779
1172
  handle: SurfaceHandle;
1173
+ /**
1174
+ * Predicate returning `true` iff the surface is currently "attended" —
1175
+ * focus inside the container subtree OR pointer over the container.
1176
+ * Gesture bindings whose keydown handlers call `preventDefault()` MUST
1177
+ * consult this before claiming, so the surface doesn't steal page-level
1178
+ * shortcuts when embedded in a larger document. See `util/attention.ts`.
1179
+ */
1180
+ is_attended: () => boolean;
780
1181
  };
781
1182
  type GestureBinding = {
782
1183
  /** Stable id used by `unbind` / `bindings()`. */id: GestureId;
@@ -835,11 +1236,41 @@ type CreateSvgEditorOptions = {
835
1236
  providers?: Providers;
836
1237
  style?: Partial<EditorStyle>;
837
1238
  };
838
- type SvgEditor = ReturnType<typeof createSvgEditor>;
1239
+ /**
1240
+ * Internal-only members the package's own surfaces reach for. NOT part of
1241
+ * the published API — `_internal` is the surface↔editor bridge, `keymap`
1242
+ * is the keymap dispatcher the DOM surface forwards keyboard events to.
1243
+ * Lives on the runtime object for in-package callers; stripped from the
1244
+ * public `SvgEditor` type so the published `.d.ts` doesn't advertise them.
1245
+ */
1246
+ type SvgEditorInternalMembers = "_internal" | "keymap";
1247
+ /** Internal handle. Use only inside `@grida/svg-editor`. */
1248
+ type SvgEditorInternal = ReturnType<typeof _create_svg_editor_internal>;
1249
+ /**
1250
+ * The published editor type. `Omit`s the in-package-only surfaces (see
1251
+ * `SvgEditorInternalMembers`) so consumers don't see them in IntelliSense
1252
+ * or the dist `.d.ts`. They still exist on the runtime object — casting
1253
+ * to `SvgEditorInternal` reaches them, but the contract is clear: stay
1254
+ * inside the package.
1255
+ */
1256
+ type SvgEditor = Omit<SvgEditorInternal, SvgEditorInternalMembers>;
1257
+ /**
1258
+ * Host-provided rendering and input boundary. v0 contract is pure
1259
+ * lifecycle: the editor calls `dispose()` on `editor.detach()` /
1260
+ * `editor.dispose()`; the surface owns its own teardown there (event
1261
+ * listeners, DOM nodes, retained refs).
1262
+ *
1263
+ * **Why so narrow?** The cross-surface vocabulary — paint push, input
1264
+ * routing, hit-testing — isn't pinned yet. The DOM surface re-serializes
1265
+ * the document via `editor.subscribe`, attaches its own listeners, and
1266
+ * owns its own pick. The editor reaches into the DOM surface through
1267
+ * the in-package `_internal` channel (`SurfaceBridge` in
1268
+ * `core/surface-bridge.ts`), not through the public `Surface` type. A
1269
+ * cross-surface paint / input / hit-test contract is deferred until a
1270
+ * second surface implementation arrives and pins each shape (P6 —
1271
+ * public only after dogfooding).
1272
+ */
839
1273
  type Surface$1 = {
840
- paint(snapshot: unknown): void;
841
- hit_test(x: number, y: number): NodeId | null;
842
- on_input(listener: (event: unknown) => void): Unsubscribe;
843
1274
  dispose(): void;
844
1275
  };
845
1276
  type SurfaceHandle = {
@@ -1021,7 +1452,13 @@ type Commands = {
1021
1452
  invoke(id: CommandId, args?: unknown): boolean; /** Whether an id has a registered handler. */
1022
1453
  has(id: CommandId): boolean;
1023
1454
  };
1024
- declare function createSvgEditor(opts: CreateSvgEditorOptions): {
1455
+ /**
1456
+ * Wide internal factory — returns the full object including the
1457
+ * `_internal` / `keymap` surfaces in its inferred type. Stays private.
1458
+ * The public `createSvgEditor` below wraps this and narrows the return
1459
+ * to `SvgEditor` so the published `.d.ts` doesn't advertise internals.
1460
+ */
1461
+ declare function _create_svg_editor_internal(opts: CreateSvgEditorOptions): {
1025
1462
  /**
1026
1463
  * Low-level IR handle. Mutating directly bypasses history; prefer
1027
1464
  * `editor.commands` for app code.
@@ -1118,22 +1555,33 @@ declare function createSvgEditor(opts: CreateSvgEditorOptions): {
1118
1555
  _internal: {
1119
1556
  doc: SvgDocument;
1120
1557
  history: {
1121
- preview: (label: string) => _$_grida_history0.Preview;
1558
+ preview: (label: string) => import("@grida/history").Preview;
1559
+ };
1560
+ insert_text_preview: (initial: Readonly<Record<string, string>>, opts?: {
1561
+ parent?: NodeId;
1562
+ }) => {
1563
+ id: NodeId;
1564
+ commit(): void;
1565
+ discard(): void;
1122
1566
  };
1123
1567
  emit: () => void;
1124
- /** Fires after a drag-commit (via orchestrator), `commands.nudge`, or
1125
- * `commands.translate`. The nudge-dwell watcher subscribes here. */
1126
- subscribe_translate_commit(cb: () => void): () => void; /** Drag-commit publisher; nudge/translate commands publish directly. */
1568
+ subscribe_translate_commit(cb: () => void): () => void;
1127
1569
  notify_translate_commit: () => void;
1128
1570
  set_content_edit_driver(fn: ((target: NodeId) => boolean) | null): void;
1129
- /** Fires the driver immediately with the current override so the
1130
- * surface can sync state on attach. */
1131
1571
  set_surface_hover_override_driver(fn: ((id: NodeId | null) => void) | null): void;
1132
1572
  push_surface_hover(id: NodeId | null): void;
1133
- set_computed_resolver(fn: DomComputedResolver | null): void; /** Surface registers its geometry provider on attach; clears on detach. */
1573
+ set_computed_resolver(fn: DomComputedResolver | null): void;
1134
1574
  set_geometry(p: GeometryProvider | null): void;
1135
1575
  };
1136
1576
  keymap: Keymap;
1137
1577
  };
1578
+ /**
1579
+ * Construct a headless SVG editor. The returned object is the public
1580
+ * editor surface — observation (`state`, `subscribe`), commands
1581
+ * (`commands.*`), lifecycle (`attach` / `dispose`), and the typed-read
1582
+ * caches (`node_paint`, `node_properties`). Surfaces (DOM, headless)
1583
+ * attach later via `editor.attach(surface)`.
1584
+ */
1585
+ declare function createSvgEditor(opts: CreateSvgEditorOptions): SvgEditor;
1138
1586
  //#endregion
1139
- export { GradientEntry as A, PaintPreviewSession as B, Color as C, FileIOProvider as D, EditorStyle as E, LinearGradientDefinition as F, Providers as G, PreviewSession as H, Mode as I, ReorderDirection as J, RadialGradientDefinition as K, NodeId as L, InsertPreviewSession as M, InsertableTag as N, FontResolver as O, InvalidComputedValue as P, Vec2 as Q, Paint as R, ClipboardProvider as S, EditorState as T, PropertyValue as U, PaintValue as V, Provenance as W, Tool as X, TOOL_CURSOR as Y, Unsubscribe as Z, AlignDirection as _, SelectMode as a, CameraConstraints as b, SvgEditor as c, GestureContext as d, GestureId as f, MemoizedGeometryProvider as g, GeometrySignals as h, DomComputedResolver as i, GradientStop as j, GradientDefinition as k, createSvgEditor as l, GeometryProvider as m, CreateSvgEditorOptions as n, Surface$1 as o, Gestures as p, Rect as q, DomComputedPaint as r, SurfaceHandle as s, Commands as t, GestureBinding as u, BoundsResolver as v, DEFAULT_STYLE as w, CameraOptions as x, Camera as y, PaintFallback as z };
1587
+ export { ReorderDirection as $, EditorState as A, LinearGradientDefinition as B, BoundsResolver as C, ClipboardProvider as D, CameraOptions as E, GradientEntry as F, PaintPreviewSession as G, NodeId as H, GradientStop as I, PropertyValue as J, PaintValue as K, InsertPreviewSession as L, FileIOProvider as M, FontResolver as N, Color as O, GradientDefinition as P, Rect as Q, InsertableTag as R, AlignDirection as S, CameraConstraints as T, Paint as U, Mode as V, PaintFallback as W, Providers as X, Provenance as Y, RadialGradientDefinition as Z, PathModel as _, SelectMode as a, Verb as b, SvgEditor as c, GestureContext as d, TOOL_CURSOR as et, GestureId as f, MemoizedGeometryProvider as g, GeometrySignals as h, DomComputedResolver as i, EditorStyle as j, DEFAULT_STYLE as k, createSvgEditor as l, GeometryProvider as m, CreateSvgEditorOptions as n, Unsubscribe as nt, Surface$1 as o, Gestures as p, PreviewSession as q, DomComputedPaint as r, Vec2 as rt, SurfaceHandle as s, Commands as t, Tool as tt, GestureBinding as u, PathSnapshot as v, Camera as w, VertexId as x, SegmentId as y, InvalidComputedValue as z };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as GradientEntry, B as PaintPreviewSession, C as Color, D as FileIOProvider, E as EditorStyle, F as LinearGradientDefinition, G as Providers, H as PreviewSession, I as Mode, J as ReorderDirection, K as RadialGradientDefinition, L as NodeId, M as InsertPreviewSession, N as InsertableTag, O as FontResolver, P as InvalidComputedValue, Q as Vec2, R as Paint, S as ClipboardProvider, T as EditorState, U as PropertyValue, V as PaintValue, W as Provenance, X as Tool, Y as TOOL_CURSOR, Z as Unsubscribe, _ as AlignDirection, a as SelectMode, c as SvgEditor, j as GradientStop, k as GradientDefinition, l as createSvgEditor, n as CreateSvgEditorOptions, o as Surface, q as Rect, s as SurfaceHandle, t as Commands, w as DEFAULT_STYLE, z as PaintFallback } from "./editor-BH03X8cX.mjs";
2
- export { AlignDirection, ClipboardProvider, Color, Commands, CreateSvgEditorOptions, DEFAULT_STYLE, EditorState, EditorStyle, FileIOProvider, FontResolver, GradientDefinition, GradientEntry, GradientStop, InsertPreviewSession, InsertableTag, InvalidComputedValue, LinearGradientDefinition, Mode, NodeId, Paint, PaintFallback, PaintPreviewSession, PaintValue, PreviewSession, PropertyValue, Provenance, Providers, RadialGradientDefinition, Rect, ReorderDirection, SelectMode, Surface, SurfaceHandle, SvgEditor, TOOL_CURSOR, Tool, Unsubscribe, Vec2, createSvgEditor };
1
+ import { $ as ReorderDirection, A as EditorState, B as LinearGradientDefinition, D as ClipboardProvider, F as GradientEntry, G as PaintPreviewSession, H as NodeId, I as GradientStop, J as PropertyValue, K as PaintValue, L as InsertPreviewSession, M as FileIOProvider, N as FontResolver, O as Color, P as GradientDefinition, Q as Rect, R as InsertableTag, S as AlignDirection, U as Paint, V as Mode, W as PaintFallback, X as Providers, Y as Provenance, Z as RadialGradientDefinition, _ as PathModel, a as SelectMode, b as Verb, c as SvgEditor, et as TOOL_CURSOR, j as EditorStyle, k as DEFAULT_STYLE, l as createSvgEditor, n as CreateSvgEditorOptions, nt as Unsubscribe, o as Surface, q as PreviewSession, rt as Vec2, s as SurfaceHandle, t as Commands, tt as Tool, v as PathSnapshot, x as VertexId, y as SegmentId, z as InvalidComputedValue } from "./editor-YQwdWHBb.mjs";
2
+ export { type AlignDirection, type ClipboardProvider, type Color, type Commands, type CreateSvgEditorOptions, DEFAULT_STYLE, type EditorState, type EditorStyle, type FileIOProvider, type FontResolver, type GradientDefinition, type GradientEntry, type GradientStop, type InsertPreviewSession, type InsertableTag, type InvalidComputedValue, type LinearGradientDefinition, type Mode, type NodeId, type Paint, type PaintFallback, type PaintPreviewSession, type PaintValue, PathModel, type PathSnapshot, type PreviewSession, type PropertyValue, type Provenance, type Providers, type RadialGradientDefinition, type Rect, type ReorderDirection, type SegmentId, type SelectMode, type Surface, type SurfaceHandle, type SvgEditor, TOOL_CURSOR, type Tool, type Unsubscribe, type Vec2, type Verb, type VertexId, createSvgEditor };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { A as GradientEntry, B as PaintPreviewSession, C as Color, D as FileIOProvider, E as EditorStyle, F as LinearGradientDefinition, G as Providers, H as PreviewSession, I as Mode, J as ReorderDirection, K as RadialGradientDefinition, L as NodeId, M as InsertPreviewSession, N as InsertableTag, O as FontResolver, P as InvalidComputedValue, Q as Vec2, R as Paint, S as ClipboardProvider, T as EditorState, U as PropertyValue, V as PaintValue, W as Provenance, X as Tool, Y as TOOL_CURSOR, Z as Unsubscribe, _ as AlignDirection, a as SelectMode, c as SvgEditor, j as GradientStop, k as GradientDefinition, l as createSvgEditor, n as CreateSvgEditorOptions, o as Surface, q as Rect, s as SurfaceHandle, t as Commands, w as DEFAULT_STYLE, z as PaintFallback } from "./editor-Bd4-VCEJ.js";
2
- export { AlignDirection, ClipboardProvider, Color, Commands, CreateSvgEditorOptions, DEFAULT_STYLE, EditorState, EditorStyle, FileIOProvider, FontResolver, GradientDefinition, GradientEntry, GradientStop, InsertPreviewSession, InsertableTag, InvalidComputedValue, LinearGradientDefinition, Mode, NodeId, Paint, PaintFallback, PaintPreviewSession, PaintValue, PreviewSession, PropertyValue, Provenance, Providers, RadialGradientDefinition, Rect, ReorderDirection, SelectMode, Surface, SurfaceHandle, SvgEditor, TOOL_CURSOR, Tool, Unsubscribe, Vec2, createSvgEditor };
1
+ import { $ as ReorderDirection, A as EditorState, B as LinearGradientDefinition, D as ClipboardProvider, F as GradientEntry, G as PaintPreviewSession, H as NodeId, I as GradientStop, J as PropertyValue, K as PaintValue, L as InsertPreviewSession, M as FileIOProvider, N as FontResolver, O as Color, P as GradientDefinition, Q as Rect, R as InsertableTag, S as AlignDirection, U as Paint, V as Mode, W as PaintFallback, X as Providers, Y as Provenance, Z as RadialGradientDefinition, _ as PathModel, a as SelectMode, b as Verb, c as SvgEditor, et as TOOL_CURSOR, j as EditorStyle, k as DEFAULT_STYLE, l as createSvgEditor, n as CreateSvgEditorOptions, nt as Unsubscribe, o as Surface, q as PreviewSession, rt as Vec2, s as SurfaceHandle, t as Commands, tt as Tool, v as PathSnapshot, x as VertexId, y as SegmentId, z as InvalidComputedValue } from "./editor-CJ2KuRh5.js";
2
+ export { type AlignDirection, type ClipboardProvider, type Color, type Commands, type CreateSvgEditorOptions, DEFAULT_STYLE, type EditorState, type EditorStyle, type FileIOProvider, type FontResolver, type GradientDefinition, type GradientEntry, type GradientStop, type InsertPreviewSession, type InsertableTag, type InvalidComputedValue, type LinearGradientDefinition, type Mode, type NodeId, type Paint, type PaintFallback, type PaintPreviewSession, type PaintValue, PathModel, type PathSnapshot, type PreviewSession, type PropertyValue, type Provenance, type Providers, type RadialGradientDefinition, type Rect, type ReorderDirection, type SegmentId, type SelectMode, type Surface, type SurfaceHandle, type SvgEditor, TOOL_CURSOR, type Tool, type Unsubscribe, type Vec2, type Verb, type VertexId, createSvgEditor };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_insertions = require("./insertions-BJ-6o6o5.js");
3
- const require_editor = require("./editor-CdyC3uAe.js");
4
- exports.DEFAULT_STYLE = require_insertions.DEFAULT_STYLE;
5
- exports.TOOL_CURSOR = require_insertions.TOOL_CURSOR;
2
+ const require_model = require("./model-DqGqV1H4.js");
3
+ const require_editor = require("./editor-BHHU_Nvz.js");
4
+ exports.DEFAULT_STYLE = require_model.DEFAULT_STYLE;
5
+ exports.PathModel = require_model.PathModel;
6
+ exports.TOOL_CURSOR = require_model.TOOL_CURSOR;
6
7
  exports.createSvgEditor = require_editor.createSvgEditor;