@weasel-js/routing 1.9.4 → 1.10.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.
@@ -1,29 +1,20 @@
1
- import { View } from '@weasel-js/geom';
1
+ import { PoseLinear, View } from '@weasel-js/geom';
2
2
  import * as react from 'react';
3
3
  import { ReactNode } from 'react';
4
+ import { Emitter } from '@weasel-js/registry';
4
5
  import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
5
6
  import { CursorSpec } from '@weasel-js/cursor';
6
- import { ClaimableGesture, GestureSpec, InputEvent, TargetSpec, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName, IngestItem } from '@weasel-js/gestures';
7
- import { Op } from '@weasel-js/history';
7
+ import { ClaimableGesture, GestureSpec, IngestItem, InputEvent, TargetSpec, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
8
8
  import '@weasel-js/select';
9
9
 
10
10
  /**
11
11
  * The primitives the routing surface is typed in.
12
12
  *
13
- * Routing decides which action a gesture runs. To do that it has to name a
14
- * viewport, a rect, a node id and a selection — but it never constructs,
15
- * projects or renders any of them. So they are declared here as structural
16
- * interfaces with no dependencies, and `@weasel-js/core` re-exports each from
17
- * the path its own call sites have always used. Same arrangement as `Op`,
18
- * which is declared in `@weasel-js/history` and re-exported by
19
- * `core/ops/types`. The selection click policy's own vocabulary comes from
20
- * `@weasel-js/select`, where the rules that read it live.
21
- *
22
- * Authority for the *behavior* behind each of these stays in core: `View` is
23
- * what `core/viewport` maintains, `SelectionApi` is what `useSelection`
24
- * returns, `DeviceProfile` is what `resolveDeviceProfile` detects. Changing
25
- * one of these shapes is a change to core's contract that happens to be
26
- * spelled here.
13
+ * Routing has to name a viewport, a rect, a node id, and a selection, but it
14
+ * never constructs, projects, or renders any of them. So they are declared
15
+ * here as structural interfaces with no dependencies, and `@weasel-js/core`
16
+ * re-exports each. The behavior behind them stays in core: changing one of
17
+ * these shapes is a change to core's contract that happens to be spelled here.
27
18
  */
28
19
 
29
20
  /** An opaque scene-node identifier. Branded so a bare string cannot stand in. */
@@ -33,8 +24,9 @@ type NodeId = string & {
33
24
 
34
25
  /**
35
26
  * Axis-aligned rectangle in world space. The optional `rotation` (radians,
36
- * around the AABB center) lets chrome attach an orientation to an otherwise
37
- * axis-aligned rect without a parallel type.
27
+ * around the AABB center) and `transform` (inside it, about the same center)
28
+ * let chrome carry the frame a box is drawn in without a parallel type;
29
+ * `poseFrameOf` composes the two.
38
30
  */
39
31
  interface Bounds {
40
32
  x: number;
@@ -42,6 +34,7 @@ interface Bounds {
42
34
  width: number;
43
35
  height: number;
44
36
  rotation?: number;
37
+ transform?: PoseLinear;
45
38
  }
46
39
  /** The selection as routing reads and writes it. */
47
40
  interface SelectionApi {
@@ -65,6 +58,7 @@ interface SelectionApi {
65
58
  * Apply a click to the selection per the configured mode/extend key.
66
59
  * - `single`: replaces selection with `[id]`, regardless of modifiers.
67
60
  * - `multi`: with the extend key held, toggles `id` in/out of the selection;
61
+ * with the range key held, selects from the last pressed id through `id`;
68
62
  * otherwise replaces with `[id]`.
69
63
  */
70
64
  applyClick(id: NodeId, modifiers: {
@@ -126,8 +120,8 @@ type HitShape = {
126
120
  /** What a recorded viewport change was: a translation or a scale change. */
127
121
  type ViewportGestureKind = 'pan' | 'zoom';
128
122
  /**
129
- * Where a tool reports its internal hit math so the debug overlay can draw it.
130
- * Routing threads a sink through `ToolCtx` and never calls one.
123
+ * Where an action reports its hit math so the debug overlay can draw it. An
124
+ * action reaches the canvas's sink as its `debug` dep; routing never calls one.
131
125
  */
132
126
  interface DebugSink {
133
127
  recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
@@ -188,10 +182,11 @@ type ResizeAnchor = {
188
182
  * registration lives in the node it was made in, and a lookup starts at a leaf
189
183
  * and walks up, so one canvas never answers for another.
190
184
  */
185
+
191
186
  /** What every node of one tree shares: a version and the listeners to it. */
192
187
  interface ScopeTree {
193
188
  version: number;
194
- readonly listeners: Set<() => void>;
189
+ readonly listeners: Emitter;
195
190
  }
196
191
  declare class ScopeNode<N extends ScopeNode<N>> {
197
192
  readonly parent: N | null;
@@ -270,15 +265,12 @@ declare function ActiveToolContextProvider({ children, initialActive, }: ActiveT
270
265
  declare function useActiveToolContext(): ActiveToolContextValue;
271
266
  /**
272
267
  * Like `useActiveToolContext`, but returns `null` when no
273
- * `<ActiveToolContextProvider>` is in scope instead of throwing. Used by
274
- * `useStandardActions` to preserve its silent-no-op contract when no provider
275
- * is present.
268
+ * `<ActiveToolContextProvider>` is in scope instead of throwing.
276
269
  */
277
270
  declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
278
271
  /**
279
- * Conditional `<ActiveToolContextProvider>` wrapper. Mounts a provider only
280
- * when no parent provider is in scope — otherwise renders children unwrapped
281
- * so `<WeaselProvider>` defers to the host's existing scope.
272
+ * Mounts an `<ActiveToolContextProvider>` only when no parent provider is in
273
+ * scope; otherwise renders children unwrapped.
282
274
  */
283
275
  declare function ActiveToolContextProviderIfRoot({ children, }: {
284
276
  children: ReactNode;
@@ -456,25 +448,20 @@ declare const NEVER: Rule;
456
448
  * { when: function hasPath() { … } } → when(hasPath)
457
449
  * ```
458
450
  *
459
- * Never throws and always terminates — unlike `JSON.stringify`, which throws
460
- * on a cyclic value and renders the `when` arm as a useless `{}`.
451
+ * Never throws and always terminates.
461
452
  */
462
453
  declare function describeRule(rule: Rule): string;
463
454
  /** Evaluate a rule against a context. Pure, and cheap enough to run per
464
455
  * frame for every piece of chrome. */
465
456
  declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
466
457
  /**
467
- * Composable eligibility predicate with a fluent surface. Carries its
468
- * underlying `Rule` tree at `.rule` so the dispatcher's eligibility filter and
469
- * the chrome resolver can introspect and share trees.
470
- *
471
- * Callable form `cond(ctx)` evaluates the tree against ctx. The fluent methods
472
- * return new Conditions wrapping new trees.
458
+ * Composable eligibility predicate with a fluent surface. `cond(ctx)`
459
+ * evaluates its `Rule` tree, carried at `.rule`; the fluent methods return new
460
+ * Conditions wrapping new trees.
473
461
  *
474
462
  * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
475
- * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. Mix `.and` and `.or`
476
- * only when you mean left-to-right evaluation. For grouped disjunction, name
477
- * the subexpression or use the top-level `or(...)`.
463
+ * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. For grouped
464
+ * disjunction, name the subexpression or use the top-level `or(...)`.
478
465
  */
479
466
  interface Condition {
480
467
  (ctx: RuleCtx): boolean;
@@ -542,9 +529,6 @@ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
542
529
  * An Action's `invoker` field describes how it's run. `immediate` invokers
543
530
  * fire once (delete, align, undo). `ongoing` invokers open a phase machine
544
531
  * driven by the dispatcher (move, resize, pinch-zoom).
545
- *
546
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`
547
- * § "Types" for the full design.
548
532
  */
549
533
 
550
534
  /** A 2D point in either world or screen coordinates. */
@@ -553,31 +537,20 @@ interface Point2 {
553
537
  y: number;
554
538
  }
555
539
  /**
556
- * Information about which UI affordance was hit at pointerdown.
557
- *
558
- * Populated by the dispatcher when the `affordanceAt` thunk is provided to
559
- * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
560
- * affordance (e.g. a resize handle) use this field as a guard — if the
561
- * affordance is absent or is the wrong kind, they return `{}` and let other
562
- * bindings handle the drag.
540
+ * Which UI affordance was hit at pointerdown. Populated by the dispatcher when
541
+ * the `affordanceAt` thunk is provided to `useGestureDispatcher`.
563
542
  *
564
543
  * `kind` is a discriminator string:
565
544
  * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
566
545
  * `'handle:bottom-right'` — corner resize handles.
567
546
  * - `'rotate-handle'` — the rotation affordance.
568
547
  * - `'anchor:N'` — a path anchor at index N.
569
- *
570
- * `fixedPoint` is the world-space point that should remain stationary during
571
- * the gesture. For resize handles this is the opposite (diagonally fixed)
572
- * corner; for rotate it is the pivot.
573
- *
574
- * `targetIds` are the node ids this affordance belongs to.
575
548
  */
576
549
  interface AffordanceHit {
577
550
  /** Discriminator string, e.g. `'handle:bottom-right'`. */
578
551
  kind: string;
579
552
  /** Id of whatever produced this hit — a kit affordance's `id`, or the
580
- * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
553
+ * registered layer's id. */
581
554
  owner?: string;
582
555
  /** `'exclusive'` means no binding may act on this point unless its target
583
556
  * consults the affordance. `'shared'` (the default) competes on scope and
@@ -592,25 +565,18 @@ interface AffordanceHit {
592
565
  };
593
566
  /** Which nodes this affordance belongs to. */
594
567
  targetIds?: string[];
595
- /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
596
- * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
597
- * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
598
- * leave this undefined. */
568
+ /** Set only when `kind` matches `'handle:*'`: which corner stays fixed
569
+ * during a resize. */
599
570
  anchor?: ResizeAnchor;
600
- /** Cursor to show while the pointer hovers this affordance (no
601
- * gesture in flight). Consumed by the hover-cursor pump in
602
- * `useGestureDispatcher`; unset = the pump falls through to
603
- * action-cursor prediction, then to the active tool's cursor. */
571
+ /** Cursor to show while the pointer hovers this affordance (no gesture in
572
+ * flight). Unset falls through to action-cursor prediction, then to the
573
+ * active tool's cursor. */
604
574
  cursor?: CursorSpec;
605
575
  /**
606
576
  * Free-form payload from whatever produced the hit, carried through to the
607
- * matching action untouched.
608
- *
609
- * Kit affordances describe themselves fully in the fields above and leave
610
- * this unset. It exists for affordances the kit doesn't know the shape of —
577
+ * matching action untouched. Kit affordances leave it unset; it exists for
611
578
  * a registered layer's own chrome, where the hit-test already resolved
612
- * *which* of its pieces was hit and the action would otherwise have to
613
- * redo that work. `@weasel-js/hud` passes the hit widget here.
579
+ * *which* of its pieces was hit.
614
580
  */
615
581
  payload?: unknown;
616
582
  }
@@ -619,9 +585,7 @@ interface AffordanceHit {
619
585
  * stylus state the originating `PointerEvent` carried.
620
586
  *
621
587
  * The stylus fields are absent for mouse/touch on browsers that don't report
622
- * them, and for synthetic events. Consumers that want pressure-driven output
623
- * (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
624
- * their `insert` dep receives — see `apps/site/demos/StrokeAndFillDemo.tsx`.
588
+ * them, and for synthetic events.
625
589
  */
626
590
  interface DragSample extends Point2 {
627
591
  /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
@@ -659,23 +623,19 @@ interface InvocationCtx {
659
623
  * mutates the viewport itself (pan, view-zoom), because world-space
660
624
  * deltas become self-referential as the view shifts mid-drag.
661
625
  *
662
- * Populated when the dispatcher received `clientX`/`clientY` on the
663
- * underlying pointer events. Absent for legacy callers that don't
664
- * provide them.
626
+ * Absent when the underlying pointer events carried no
627
+ * `clientX`/`clientY`.
665
628
  */
666
629
  screenDelta?: Point2;
667
630
  affordance?: AffordanceHit;
668
631
  /**
669
632
  * Full pointermove history for the current drag, in world space, with
670
633
  * per-sample stylus state when the browser reported it.
671
- * Accumulated by the dispatcher on every `pointermove` pump event.
672
634
  * Available only during `onMove` and `onEnd` calls (not on `start`).
673
635
  *
674
636
  * During `onMove` this is the dispatcher's live accumulator, and it grows
675
637
  * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
676
638
  * that is where an action that keeps the trail should take it.
677
- * Used by `lassoSelectAction` to build its polygon vertex list and by
678
- * `insertAction`'s pencil kind to carry the freehand stroke.
679
639
  */
680
640
  points?: DragSample[];
681
641
  };
@@ -697,12 +657,13 @@ interface InvocationCtx {
697
657
  centroid: Point2;
698
658
  /** Distance between the two primary pointers, in CSS pixels. */
699
659
  spread: number;
660
+ /** Degrees the line between the two primary pointers has turned since
661
+ * the gesture began, clockwise positive. It keeps counting past a half
662
+ * turn. Zero where the events carry no `MultiTouchEvent.angle`. */
700
663
  rotation: number;
701
664
  /**
702
- * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
703
- * handle is in flight and a pointermove-pump fires.
665
+ * Pinch-zoom geometry, populated while a multitouch handle is in flight.
704
666
  * `startSpread` is the spread at the moment the gesture began.
705
- * `currentSpread` is the spread at the current frame.
706
667
  */
707
668
  pinch?: {
708
669
  startSpread: number;
@@ -723,33 +684,27 @@ interface InvocationCtx {
723
684
  viewId?: string | null;
724
685
  /**
725
686
  * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
726
- * UI-driven ongoing actions (color picker, opacity slider) so handles can
727
- * read the current value on `start` and updated values on `onMove`. The
728
- * gesture dispatcher does not populate this field; gesture-driven actions
729
- * receive params via `BindingOpts.params` on `start` (the `opts` arg).
687
+ * UI-driven ongoing actions (color picker, opacity slider). The gesture
688
+ * dispatcher does not populate this field; gesture-driven actions receive
689
+ * params via `BindingOpts.params` on `start` (the `opts` arg).
730
690
  */
731
691
  params?: Record<string, unknown>;
732
692
  }
733
693
  /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
734
- * `opts` field and passes to `OngoingInvoker.start`. Today carries
735
- * behaviors; extensible. */
694
+ * `opts` field and passes to `OngoingInvoker.start`. */
736
695
  interface BindingOpts {
737
696
  /** Gesture behaviors the action's own invoker runs. Routing carries the
738
697
  * array to the action and never reads an element, so the element type is
739
- * open here; `@weasel-js/core` narrows it to `ActionBehavior<unknown,
740
- * unknown, unknown>[]`, which is what a behavior is authored against. */
698
+ * open here; `@weasel-js/core` narrows it. */
741
699
  behaviors?: unknown[];
742
- /** Per-binding action parameters. The action's invoker reads
743
- * these via the second arg to `run` (or via InvocationCtx for ongoing
744
- * invokers, when needed). Loose typing (Record<string, unknown>) for
745
- * now; consider per-action typing later via BindingOpts<A>.
700
+ /** Per-binding action parameters, which an immediate invoker reads via the
701
+ * second arg to `run`.
746
702
  *
747
- * params may also be a thunk evaluated each time the
748
- * dispatcher (or invoker) needs the value. Thunks let tools close over
749
- * refs that mutate during a gesture (e.g. polygon `sides` adjusted
750
- * mid-drag via ArrowUp). For ongoing invokers that want the latest
751
- * values at commit, the invoker can re-call the thunk inside `onEnd`
752
- * via `resolveParams(opts?.params)`. */
703
+ * May also be a thunk evaluated each time the value is needed, so a tool
704
+ * can close over refs that mutate during a gesture (e.g. polygon `sides`
705
+ * adjusted mid-drag via ArrowUp). An ongoing invoker that wants the latest
706
+ * values at commit re-calls it inside `onEnd` via
707
+ * `resolveParams(opts?.params)`. */
753
708
  params?: Record<string, unknown> | (() => Record<string, unknown>);
754
709
  /**
755
710
  * The views this binding is live in, by id, where `null` is the surface's
@@ -761,6 +716,13 @@ interface BindingOpts {
761
716
  * drag bound inside a minimap view loses to the active tool's marquee.
762
717
  */
763
718
  views?: readonly (string | null)[];
719
+ /**
720
+ * Says this binding is declared ahead of another on the same input on
721
+ * purpose: its action declines through `enabled()` when the input is not its
722
+ * to take, and the next binding fires. The dispatcher does that for any
723
+ * binding; this tells the route-conflict check the order is the design.
724
+ */
725
+ yields?: boolean;
764
726
  }
765
727
  /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
766
728
  * needed). Returns `undefined` for absent params. */
@@ -792,12 +754,10 @@ type OverlayRole =
792
754
  * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
793
755
  * shape being dragged out, the line a slice would cut along.
794
756
  *
795
- * Every variant is world geometry and nothing else. No `DrawCommand`, no
796
- * color, no coordinate space to choose: an action that reached for any of
797
- * those would be publishing core's own 2D vocabulary, and a surface rendering
798
- * the same gesture some other way could not read it. `resolveOverlays`
799
- * normalizes this into `ResolvedOverlay`, which is what a painter consumes —
800
- * `useDispatcherOverlayLayer` is core's, and it owns every style decision.
757
+ * Every variant is world geometry and nothing else — no `DrawCommand`, no
758
+ * color, no coordinate space to choose — so a surface rendering the same
759
+ * gesture some other way can still read it. The painter owns every style
760
+ * decision.
801
761
  */
802
762
  type OngoingOverlay = {
803
763
  kind: 'marquee';
@@ -823,13 +783,9 @@ type OngoingOverlay = {
823
783
  shiftHeld: boolean;
824
784
  } | {
825
785
  /**
826
- * An open run of world points the gesture is proposing — the slice
827
- * action's cut line, `@weasel-js/diagram`'s routed connector.
828
- *
829
- * Geometry only, like every other variant: `role` is the whole of what
830
- * an action says about appearance, and the painting surface is what
831
- * turns it into a stroke. An action that picked its own color would be
832
- * unpaintable by any renderer but the one it had in mind.
786
+ * An open run of world points the gesture is proposing — a cut line, a
787
+ * routed connector. `role` is the whole of what an action says about
788
+ * appearance.
833
789
  */
834
790
  kind: 'polyline';
835
791
  /** At least two points. Anything shorter is dropped in resolution. */
@@ -841,19 +797,14 @@ type OngoingOverlay = {
841
797
  role?: OverlayRole;
842
798
  } | {
843
799
  /**
844
- * Live insert-drag preview — dispatched by `insertAction` while the
845
- * user is dragging out a new shape. Pre-commit there is no scene node
846
- * to ghost via `previewIds()`/`previewPose()`, so insert paints its
847
- * preview through the dispatcher overlay layer instead.
800
+ * Live insert-drag preview. Pre-commit there is no scene node to ghost
801
+ * via `previewIds()`/`previewPose()`, so insert paints its preview
802
+ * through the overlay instead.
848
803
  *
849
- * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
850
- * the current drag (start/current normalized). `extras` is the
851
- * per-kind extras the action already collected — the overlay
852
- * renderer rebuilds the shape using the same path builders the
853
- * commit factory uses, so the preview matches the eventual node.
854
- *
855
- * `extras` is opaque (`unknown`) at the union level; the overlay
856
- * renderer narrows on `shape` and casts the field shape it expects.
804
+ * `bounds` is the AABB of the current drag (start/current normalized).
805
+ * `extras` is the per-kind extras the action already collected; the
806
+ * overlay renderer narrows on `shape` and casts the field shape it
807
+ * expects.
857
808
  */
858
809
  kind: 'insertPreview';
859
810
  /** Which preview the dispatcher's overlay draws. The kit's own shape
@@ -867,11 +818,9 @@ type OngoingOverlay = {
867
818
  height: number;
868
819
  };
869
820
  extras: unknown;
870
- /** World-space point to paint a small "anchor" dot at. Sells the
871
- * click point as the drag's anchor — particularly useful for
872
- * radial shapes (polygon/star) where no vertex sits on the
873
- * click point, and for any shape in center mode where the dot
874
- * marks the center the shape grows around. */
821
+ /** World-space point to paint a small "anchor" dot at, marking the
822
+ * click point — for radial shapes (polygon/star), where no vertex
823
+ * sits on it, and for any shape in center mode. */
875
824
  anchorPoint?: {
876
825
  x: number;
877
826
  y: number;
@@ -884,42 +833,23 @@ type OngoingOverlay = {
884
833
  interface OngoingHandle {
885
834
  /**
886
835
  * Optional logical action kind — a stable, human-readable tag the
887
- * dispatcher exposes via `getActiveAction()` for chrome-visibility
888
- * rules and any other surface that wants to react to "what action
889
- * is currently in flight" without inspecting handles directly.
890
- *
891
- * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
892
- * `'pan'`, `'pinch'`.
893
- *
894
- * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
895
- * `key-held-Space`, etc.) which keys per-pointer state and is not
896
- * meaningful to consumers.
836
+ * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
837
+ * `'resize'`, `'pan'`.
897
838
  *
898
- * When omitted, the action is "anonymous" — `getActiveAction().kind`
899
- * reports `null` even though a handle is in flight. This is fine for
900
- * actions that don't have visible chrome of their own.
839
+ * When omitted, `getActiveAction().kind` reports `null` even though a
840
+ * handle is in flight.
901
841
  */
902
842
  kind?: string;
903
843
  onMove?(ctx: InvocationCtx): void;
904
844
  onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
905
845
  /**
906
- * Optional preview surface — dispatcher-side ghost overlay.
907
- *
908
- * An ongoing-action implementation may populate `previewIds()` +
909
- * `previewPose(id)` to expose its in-flight preview state for the
910
- * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
911
- * top of the committed scene during the gesture.
912
- *
913
- * Returning `null` (or omitting the method entirely) means "no preview
914
- * this gesture" — the canvas will skip this handle as a source.
846
+ * Optional preview surface: the in-flight state the canvas's preview-ghost
847
+ * layer renders on top of the committed scene during the gesture.
915
848
  *
916
- * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
917
- * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
918
- * returns the interim pose for one of those ids (shape opaque — the
919
- * canvas casts to its `TPose` parameter). The preview-ghost layer
920
- * merges all sources via first-non-null semantics, with tool-side
921
- * previews taking precedence over dispatcher-side (preserves
922
- * backwards-compat during the registry-unification migration).
849
+ * `previewIds()` enumerates the displaced node ids; `previewPose(id)`
850
+ * returns the interim pose for one of them (shape opaque — the canvas
851
+ * casts to its `TPose` parameter). Returning `null`, or omitting the
852
+ * method, means "no preview this gesture".
923
853
  */
924
854
  previewIds?(): Iterable<string> | null;
925
855
  previewPose?(id: string): unknown | null;
@@ -932,40 +862,22 @@ interface OngoingHandle {
932
862
  */
933
863
  previewOpaqueIds?(): Iterable<string> | null;
934
864
  /**
935
- * When `false`, the preview-ghost layer paints the ghost AND the
936
- * source node stays visible at its committed pose. Defaults to
937
- * `true` (move/resize/rotate semantics: ghost replaces the source
938
- * during the gesture). Clone overrides to `false` so the original
939
- * stays put and the ghost appears at the drag target.
865
+ * When `false`, the preview-ghost layer paints the ghost AND the source
866
+ * node stays visible at its committed pose, as a clone wants. Defaults to
867
+ * `true`: the ghost replaces the source during the gesture.
940
868
  */
941
869
  previewHidesSource?: boolean;
942
870
  /**
943
- * Optional per-id preview *data*. Falls back to the committed
944
- * `node.data` when null/absent. Use when the gesture mutates
945
- * `node.data` (e.g. anchor-edit on nodes that store the polygon on
946
- * `data.path`) rather than (or in addition to) the pose. The preview-
947
- * ghost layer assembles a synthetic node from `{ ...node, pose:
948
- * previewPose ?? node.pose, data: previewData ?? node.data }` before
949
- * calling the scene slot's `drawOne`.
950
- *
951
- * Sources compose first-non-null per axis: an action can emit only
952
- * `previewPose` (translation), only `previewData` (data-only edit),
953
- * or both (pose + data both change, e.g. anchor drag on a data.path
954
- * node where the bounds shift).
871
+ * Optional per-id preview *data*, for a gesture that mutates `node.data`
872
+ * (e.g. anchor-edit on nodes that store the polygon on `data.path`) rather
873
+ * than, or in addition to, the pose. Falls back to the committed
874
+ * `node.data` when null/absent.
955
875
  */
956
876
  previewData?(id: string): unknown | null;
957
877
  /**
958
- * Optional chrome surface — dispatcher-side overlay layer.
959
- *
960
- * An ongoing-action implementation may populate `overlay()` to expose a
961
- * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
962
- * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
963
- * Returning `null` (or omitting the method) means "no overlay this
964
- * gesture" — the canvas will skip this handle as a chrome source.
965
- *
966
- * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
967
- * which paints displaced scene-node silhouettes. Marquee and lasso
968
- * gestures don't displace any node, but still need on-screen feedback.
878
+ * Optional chrome surface: a non-ghost visual (marquee rectangle, lasso
879
+ * polyline) painted while the gesture is in flight. Returning `null`, or
880
+ * omitting the method, means "no overlay this gesture".
969
881
  */
970
882
  overlay?(): OngoingOverlay | null;
971
883
  }
@@ -984,9 +896,7 @@ interface OngoingInvoker {
984
896
  timing: 'ongoing';
985
897
  start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
986
898
  }
987
- /** Pluggable invocation strategy for an Action. Future variants
988
- * (`longPress`, `twoStage`, `modal`) extend this union without touching
989
- * the `Action` type. */
899
+ /** Pluggable invocation strategy for an Action. */
990
900
  type Invoker = ImmediateInvoker | OngoingInvoker;
991
901
 
992
902
  /**
@@ -1064,11 +974,9 @@ type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDis
1064
974
  */
1065
975
  interface ActionPresentation {
1066
976
  label: string;
1067
- /** Inline-SVG icon for palette / toolbar surfaces. Mirrors
1068
- * `ToolPresentation.icon` so a generic `<ActionBar>` can render from
1069
- * action metadata the same way `<ToolPalette>` renders from tool
1070
- * metadata. May be a static `ReactNode` or a function (rare; useful
1071
- * for state-aware icons like a "lock" toggle). */
977
+ /** Inline-SVG icon for palette / toolbar surfaces. May be a static
978
+ * `ReactNode` or a function (rare; useful for state-aware icons like a
979
+ * "lock" toggle). */
1072
980
  icon?: ReactNode | (() => ReactNode);
1073
981
  /** Grouping key for palette/menu surfaces. Free-form string; the kit
1074
982
  * ships defaults for `'align'` (six edges/centers), `'distribute'`
@@ -1124,30 +1032,21 @@ interface ActionDispatch extends BindingSource {
1124
1032
  /**
1125
1033
  * @experimental
1126
1034
  * Optional predicate saying whether the action can run right now. Return
1127
- * `true` when it can, or a reason string (e.g. `'Selection required'`)
1128
- * when it cannot. The command palette greys out a disabled row, skips it
1129
- * in keyboard nav, ignores clicks, and shows the reason next to the label.
1130
- * The dispatcher skips a disabled action too, on a key as on a pointer, and
1131
- * falls through to the next binding that matches. `ActionsRegistry.trigger`
1132
- * does not ask, so the action's own `run` should still self-guard.
1035
+ * `true` when it can, or an `ActionDisabledReason` when it cannot. The
1036
+ * command palette greys out a disabled row, skips it in keyboard nav,
1037
+ * ignores clicks, and shows the reason next to the label. The dispatcher
1038
+ * skips a disabled action too, on a key as on a pointer, and falls through
1039
+ * to the next binding that matches. `ActionsRegistry.trigger` does not ask,
1040
+ * so the action's own `run` should still self-guard.
1133
1041
  *
1134
1042
  * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
1135
- * must not throw. If a call throws or exceeds the budget in dev mode,
1136
- * `evaluateEnabled` logs a one-time warning per action id; throws are
1137
- * caught and treated as disabled with reason `'(predicate threw)'`.
1138
- *
1139
- * Snapshot-on-open semantics: the palette evaluates `enabled` once when
1140
- * opened and does NOT re-evaluate on selection changes while open. Live
1141
- * reactive updates are deferred — palette is short-lived.
1043
+ * must not throw. A throw is caught and treated as disabled with reason
1044
+ * `ActionDisabledReason.PredicateThrew`.
1142
1045
  *
1143
- * The reason set is a closed enum — to add a new reason, edit
1144
- * `ActionDisabledReason` and the consumer's display map.
1046
+ * The palette evaluates `enabled` once when opened and does NOT re-evaluate
1047
+ * on selection changes while open.
1145
1048
  *
1146
- * The optional `deps` argument is the same bag passed to
1147
- * `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
1148
- * synthesize it from the surrounding `DepRegistry` so predicates can
1149
- * inspect selection / scene / etc. Predicates that don't need deps just
1150
- * ignore the arg.
1049
+ * `deps` is the same bag passed to `ImmediateInvoker.run`.
1151
1050
  *
1152
1051
  * `at` is the world point of the event being routed, when it has one — a
1153
1052
  * click, a press, a long-press. An action that only applies somewhere
@@ -1155,10 +1054,18 @@ interface ActionDispatch extends BindingSource {
1155
1054
  * falls through to the next binding and hover prediction shows the
1156
1055
  * action's `cursor` only where it would act. Palettes and keystrokes
1157
1056
  * pass no point; treat absent as "anywhere".
1057
+ *
1058
+ * `carried` is what a drop or a paste being routed brings: its items. For
1059
+ * a drag still in flight they hold a MIME and nothing else, since a
1060
+ * browser withholds contents until the drop. An action that takes only
1061
+ * some content refuses the rest here, so a drag it would not take is not
1062
+ * accepted and a paste it would not take stays the page's.
1158
1063
  */
1159
1064
  enabled?: (deps?: ActionDeps, at?: {
1160
1065
  x: number;
1161
1066
  y: number;
1067
+ }, carried?: {
1068
+ items: readonly IngestItem[];
1162
1069
  }) => true | ActionDisabledReason;
1163
1070
  /**
1164
1071
  * @experimental
@@ -1187,12 +1094,11 @@ interface ActionDispatch extends BindingSource {
1187
1094
  /**
1188
1095
  * CSS cursor shown while the pointer hovers a spot where this action
1189
1096
  * would win the drag — or, when the drag's winner declares no cursor,
1190
- * the click. The hover-cursor pump (in `useGestureDispatcher`) runs
1191
- * `Dispatcher.resolveOnly` on each idle pointermove — the same match
1192
- * walk a real press takes — and applies the winning action's `cursor`,
1193
- * so the hint and the actual target stay in sync by construction. Omitted = no override (the active tool's
1194
- * `Tool.cursor` shows). Affordance hits are resolved earlier in the
1195
- * pump via `AffordanceRegion.cursor` and never reach this field.
1097
+ * the click. The hover-cursor pump runs the same match walk a real press
1098
+ * takes, so the hint and the actual target stay in sync. Omitted = no
1099
+ * override (the active tool's `Tool.cursor` shows). Affordance hits are
1100
+ * resolved earlier in the pump via `AffordanceRegion.cursor` and never
1101
+ * reach this field.
1196
1102
  *
1197
1103
  * Static value only. Prediction runs `enabled()` but cannot run the
1198
1104
  * invoker, so an action that matches yet bails at `start()` (empty
@@ -1210,19 +1116,12 @@ interface ActionDispatch extends BindingSource {
1210
1116
  * ("you are panning"). An action can declare either, both, or neither;
1211
1117
  * with only `cursor` set, the hover hint holds for the duration of the
1212
1118
  * gesture.
1213
- *
1214
- * This is where mid-gesture cursors live now. They used to come from the
1215
- * tool side — an `engaged` phase entry's cursor,
1216
- * or a function-form `Tool.cursor` reading the gesture scratch out of the
1217
- * tool-routing dispatcher. Both belonged to a pipeline whose whole job was
1218
- * being taken over by bindings, and neither could describe a cursor for an
1219
- * action a tool doesn't own.
1220
1119
  */
1221
1120
  activeCursor?: CursorSpec;
1222
1121
  }
1223
1122
  /**
1224
1123
  * @experimental
1225
- * Single registered action. v1: one binding per action.
1124
+ * Single registered action.
1226
1125
  */
1227
1126
  interface Action extends ActionDispatch, ActionPresentation {
1228
1127
  }
@@ -1290,20 +1189,9 @@ declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabl
1290
1189
  declare function evaluateActive(action: Action, deps?: ActionDeps): boolean | undefined;
1291
1190
 
1292
1191
  /**
1293
- * Dep registry for registry unification.
1294
- *
1295
- * Holds named "live source" thunks. Actions declare `requires: ['selection']`;
1296
- * the dispatcher calls `registry.get('selection')` at invocation time to
1297
- * build a typed Deps bag.
1298
- *
1299
- * `DepSchema` — the map of dep name → value type — is the single source of
1300
- * truth for what `requires`/`register`/`get` accept. Its concrete fields live
1301
- * in `./depSchema` (co-located with the dep value types). It is declared there
1302
- * as a plain `export interface` rather than via a cross-module
1303
- * `declare module './depRegistry'` augmentation: that augmentation does NOT
1304
- * merge once rollup-plugin-dts flattens both files into one `.d.ts` chunk,
1305
- * which silently emptied `DepSchema` for consumers. Consumers still extend it
1306
- * via `declare module '@weasel-js/core'` augmentation.
1192
+ * The dep registry: named "live source" thunks. Actions declare
1193
+ * `requires: ['selection']`; the dispatcher calls `registry.get('selection')`
1194
+ * at invocation time to build a typed Deps bag.
1307
1195
  */
1308
1196
 
1309
1197
  /** Holds the live sources an action's declared dependencies resolve to.
@@ -1362,12 +1250,12 @@ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift' | (string & {})
1362
1250
  *
1363
1251
  * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
1364
1252
  * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1365
- interface ToolPresentation<TScratch = unknown> {
1253
+ interface ToolPresentation {
1366
1254
  /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1367
1255
  label?: string;
1368
- /** Inline-SVG icon component output. May be a static `ReactNode` or a
1369
- * function of scratch state (rare; useful for shape-aware affordances). */
1370
- icon?: react.ReactNode | ((scratch?: TScratch) => react.ReactNode);
1256
+ /** Inline-SVG icon component output: a static `ReactNode`, or a function
1257
+ * returning one. */
1258
+ icon?: react.ReactNode | (() => react.ReactNode);
1371
1259
  /** Palette grouping key. Tools sharing a group render contiguously
1372
1260
  * with separators between groups. Free-form string; the kit
1373
1261
  * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
@@ -1447,9 +1335,8 @@ interface Contribution<TOverlay = KernelOverlay> extends ContributionRouting, Co
1447
1335
  }
1448
1336
 
1449
1337
  /**
1450
- * Pure matcher primitives live in `@weasel-js/gestures`. This file
1451
- * re-exports them for kit-internal consumers and layers the actions-layer
1452
- * binding-scope / matchBest logic on top.
1338
+ * Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
1339
+ * the actions-layer binding-scope / matchBest logic on top.
1453
1340
  */
1454
1341
 
1455
1342
  /** Where a binding came from, which is also its priority: a held hotkey beats
@@ -1478,25 +1365,13 @@ interface MatchResult {
1478
1365
  * only the body classification. `kindOf` predicates are handed the hit;
1479
1366
  * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
1480
1367
  * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
1481
- * `bodyTarget` / `bodyKind` and never see it — which is why chrome floating
1482
- * over empty canvas used to read as empty canvas.
1368
+ * `bodyTarget` / `bodyKind` and never see it.
1483
1369
  *
1484
1370
  * A predicate declares which it is with `readsAffordance`; one that declares
1485
1371
  * nothing, which only untyped code can write, counts as not reading the hit.
1486
1372
  */
1487
1373
  declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
1488
- /**
1489
- * Find the best-matching binding across all scopes.
1490
- *
1491
- * Precedence: hotkey > active > ambient.
1492
- * Within a scope, more-specific bindings win; same-specificity bindings keep
1493
- * registration order (see `matchSorted`).
1494
- * Returns null when nothing matches.
1495
- *
1496
- * Note: this returns only the single best match. The dispatcher uses
1497
- * `matchSorted` so it can fall through to lower-specificity matches when a
1498
- * higher-specificity match's `enabled()` reports disabled.
1499
- */
1374
+ /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
1500
1375
  declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
1501
1376
  /**
1502
1377
  * Find every binding that matches the event, sorted by precedence
@@ -1505,12 +1380,7 @@ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], is
1505
1380
  * - Scopes by priority: hotkey > active > ambient.
1506
1381
  * - Within a scope, more-specific bindings beat less-specific (see
1507
1382
  * `specificity()`).
1508
- * - Among same-specificity bindings, first-declared wins (stable sort
1509
- * preserves registration order — same tiebreaker as pre-specificity).
1510
- *
1511
- * Used by the dispatcher to implement specificity-ordered fall-through:
1512
- * when the top match's `enabled()` returns a disabled reason (or the
1513
- * action's `start()` returns an empty handle), the next match is tried.
1383
+ * - Among same-specificity bindings, first-declared wins.
1514
1384
  */
1515
1385
  declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
1516
1386
  /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
@@ -1686,11 +1556,9 @@ interface RankedCandidate {
1686
1556
  * `createDispatcher` in `dispatcher.ts` implements it.
1687
1557
  */
1688
1558
 
1689
- /** Everything the dispatcher must consult to route one input event: the
1690
- * registered actions and their deps, which tool is active, which hotkeys are
1691
- * held, and — optionally — the chrome state that action eligibility rules are
1692
- * evaluated against. Rebuilt per event rather than held, so the dispatcher
1693
- * itself stays stateless apart from in-flight gestures. */
1559
+ /** Everything the dispatcher must consult to route one input event. Rebuilt
1560
+ * per event rather than held, so the dispatcher itself stays stateless apart
1561
+ * from in-flight gestures. */
1694
1562
  interface DispatcherContext {
1695
1563
  /** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
1696
1564
  actions: ActionSource;
@@ -1741,9 +1609,7 @@ interface UiOngoingControl {
1741
1609
  }
1742
1610
  /**
1743
1611
  * Successful `Dispatcher.resolveOnly` prediction: the binding + action that
1744
- * would fire if `event` were dispatched for real. `action` is the resolved
1745
- * descriptor so callers (the hover-cursor pump) can read metadata like
1746
- * `Action.cursor` without a second registry lookup.
1612
+ * would fire if `event` were dispatched for real.
1747
1613
  */
1748
1614
  interface ResolveOnlyResult {
1749
1615
  actionId: string;
@@ -1796,14 +1662,12 @@ interface ResolveAllOptions {
1796
1662
  * Evaluate eligibility and `enabled()` for candidates below the winner
1797
1663
  * instead of short-circuiting them to `shadowed`.
1798
1664
  *
1799
- * Off by default, and deliberately so: the default walk's early exit is what
1800
- * keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
1801
- * and `enabled()` predicates are only contractually pure — not free.
1665
+ * Off by default: the default walk's early exit is what keeps
1666
+ * `resolveOnly`'s `enabled()` call count identical to a real dispatch, and
1667
+ * `enabled()` predicates are only contractually pure — not free.
1802
1668
  *
1803
- * Turn it on for diagnostics, where "this one was outranked" is a less
1804
- * useful answer than "this one was outranked AND would have been disabled
1805
- * anyway". With it on, `shadowed` narrows to its precise meaning: this
1806
- * candidate would have fired, but something above it did.
1669
+ * With it on, `shadowed` narrows to its precise meaning: this candidate
1670
+ * would have fired, but something above it did.
1807
1671
  */
1808
1672
  evaluateShadowed?: boolean;
1809
1673
  }
@@ -1825,12 +1689,10 @@ interface Dispatcher {
1825
1689
  */
1826
1690
  handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
1827
1691
  /**
1828
- * Predict which action `event` would route to WITHOUT invoking it. Replays
1829
- * the same walk as `handleInput` — scope assembly, specificity-sorted
1830
- * match, eligibility filter, per-candidate `enabled()` gate — and returns
1831
- * the first candidate that would fire, or `null` when the event would go
1832
- * unhandled. Pure query: no invoker runs, no in-flight state changes, no
1833
- * trace-log entry.
1692
+ * Predict which action `event` would route to WITHOUT invoking it: the
1693
+ * first candidate `handleInput`'s walk would fire, or `null` when the event
1694
+ * would go unhandled. Pure query: no invoker runs, no in-flight state
1695
+ * changes, no trace-log entry.
1834
1696
  *
1835
1697
  * Known divergence from a real dispatch: an ongoing invoker that matches
1836
1698
  * but returns an empty handle at `start()` (runtime bail) makes the real
@@ -1841,25 +1703,15 @@ interface Dispatcher {
1841
1703
  resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
1842
1704
  /**
1843
1705
  * Every binding that matches `event`, in dispatch precedence order, each
1844
- * with a verdict explaining whether it would fire. Same walk as
1845
- * `resolveOnly` — scope assembly, specificity-sorted match, eligibility
1846
- * check, per-candidate `enabled()` gate — without stopping at the winner
1847
- * and without invoking anything. Nothing is dropped: candidates that
1848
- * `resolveOnly`'s walk would filter out are kept here and labelled
1849
- * `ineligible` instead. Pure query: no invoker runs, no in-flight state
1850
- * changes, no trace-log entry.
1706
+ * with a verdict explaining whether it would fire. Nothing is dropped:
1707
+ * candidates that `resolveOnly`'s walk would filter out are kept here and
1708
+ * labeled `ineligible` instead. Pure query, and it shares `resolveOnly`'s
1709
+ * known divergence from a real dispatch.
1851
1710
  *
1852
1711
  * `resolveOnly` is the first `would-fire` entry of this list.
1853
1712
  *
1854
- * Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
1855
- * invoker that matches but returns an empty handle at `start()` makes the
1856
- * real dispatch fall through, and this cannot see that.
1857
- *
1858
- * By default everything below the winner is `shadowed` without being asked,
1859
- * which is what keeps this walk as cheap as the dispatch it replays. Pass
1860
- * `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
1861
- * lower candidate that is ALSO ineligible or disabled says so — see
1862
- * {@link ResolveAllOptions.evaluateShadowed}.
1713
+ * By default everything below the winner is `shadowed` without being asked
1714
+ * — see {@link ResolveAllOptions.evaluateShadowed}.
1863
1715
  */
1864
1716
  resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
1865
1717
  /**
@@ -1872,7 +1724,6 @@ interface Dispatcher {
1872
1724
  explain(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): DispatchRecord;
1873
1725
  /**
1874
1726
  * Synthesize an end-of-gesture for every in-flight ongoing handle.
1875
- * Used by tool-switch cancellation (Q2 decision).
1876
1727
  */
1877
1728
  cancelAll(reason: 'commit' | 'cancel'): void;
1878
1729
  /**
@@ -1888,41 +1739,31 @@ interface Dispatcher {
1888
1739
  */
1889
1740
  inFlightCursor(): CursorSpec | null;
1890
1741
  /**
1891
- * Read-only iterator over currently in-flight `OngoingHandle` instances.
1892
- *
1893
- * Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
1894
- * to walk each handle's `previewIds()` / `previewPose(id)` and render
1895
- * dispatcher-driven gesture previews. Read-only by design:
1896
- * external consumers must not mutate the in-flight map.
1742
+ * Read-only iterator over currently in-flight `OngoingHandle` instances,
1743
+ * for a preview layer to walk each handle's `previewIds()` /
1744
+ * `previewPose(id)`.
1897
1745
  */
1898
1746
  getInFlightHandles(): Iterable<OngoingHandle>;
1899
1747
  /**
1900
- * Subscribe to in-flight state changes. The callback fires after every
1901
- * mutation that affects what the preview-ghost / dispatcher-overlay
1902
- * layers read — handle start, every `onMove` pump, end, cancel,
1903
- * cancel-all — and after any other input handled while a gesture is in
1904
- * flight. Input that arrives with nothing in flight and starts nothing
1905
- * does not fire it. Consumers re-read `getInFlightHandles()` and re-render.
1748
+ * Subscribe to in-flight state changes. The callback fires after handle
1749
+ * start, every `onMove` pump, end, cancel, and cancel-all, and after any
1750
+ * other input handled while a gesture is in flight. Input that arrives
1751
+ * with nothing in flight and starts nothing does not fire it.
1906
1752
  *
1907
1753
  * Returns an unsubscribe function.
1908
1754
  */
1909
1755
  subscribe(fn: () => void): () => void;
1910
1756
  /**
1911
1757
  * Monotonic counter bumped on exactly the events {@link subscribe} fires
1912
- * on. The snapshot half of the `useSyncExternalStore` contract: pair it
1913
- * with `subscribe` to drive a render off in-flight gesture state without
1914
- * a `useReducer` force-rerender.
1758
+ * on: the snapshot half of the `useSyncExternalStore` contract.
1915
1759
  *
1916
- * Starts at 0 and only ever increases. Two reads returning the same number
1917
- * mean no gesture was in flight in between; it does **not** guarantee that
1918
- * a bump changed anything observable (input that matched no binding still
1919
- * counts while a gesture is in flight — see `subscribe`).
1760
+ * A bump does **not** guarantee that anything observable changed (input
1761
+ * that matched no binding still counts while a gesture is in flight — see
1762
+ * `subscribe`).
1920
1763
  */
1921
1764
  getVersion(): number;
1922
1765
  /**
1923
- * Snapshot of the currently active action, for surfaces (chrome-caps
1924
- * visibility rules, debug HUDs) that need to react to "what action
1925
- * is in flight right now."
1766
+ * Snapshot of the currently active action.
1926
1767
  *
1927
1768
  * - `kind` — the `OngoingHandle.kind` reported by the in-flight
1928
1769
  * handle (e.g. `'marquee'`, `'move'`). `null` when no action is
@@ -1933,26 +1774,15 @@ interface Dispatcher {
1933
1774
  *
1934
1775
  * When multiple handles are in flight simultaneously (e.g. a key-held
1935
1776
  * action overlapping a pointer action), the most-recently-started
1936
- * handle wins. This matches user intent: the latest interaction is
1937
- * the one consumers care about.
1938
- *
1939
- * That rule used to be near-vacuous on the pointer side, because every
1940
- * pointer shared one handle slot and two pointer drags could not coexist.
1941
- * With per-pointer keying they can — but only via paths that bypass the
1942
- * multi-pointer policy in `useGestureDispatcher` (which stops a second
1943
- * finger from opening a drag while a pinch is live), such as a mouse and
1944
- * a pen used together. Latest-start remains the right answer there.
1777
+ * handle wins.
1945
1778
  */
1946
1779
  getActiveAction(): {
1947
1780
  kind: string | null;
1948
1781
  id: string | null;
1949
1782
  };
1950
1783
  /**
1951
- * Start an ongoing action driven by UI (not a gesture). Builds an
1952
- * `InvocationCtx` with the given `deps` and `params`, calls
1953
- * `action.invoker.start(ctx, { params })`, and registers the returned
1954
- * handle in the in-flight map so `getInFlightHandles()` reports it —
1955
- * enabling preview rendering via `SceneCanvas`.
1784
+ * Start an ongoing action driven by UI (not a gesture), and register its
1785
+ * handle in the in-flight map so `getInFlightHandles()` reports it.
1956
1786
  *
1957
1787
  * Returns `null` if `actionId` is unknown, the action's invoker is not
1958
1788
  * ongoing, or `start` returned an empty handle.
@@ -1969,10 +1799,8 @@ interface Dispatcher {
1969
1799
  */
1970
1800
 
1971
1801
  /** One entry per "mode change" — a kit state transition that isn't a
1972
- * dispatched input event but is load-bearing for understanding why an
1973
- * input was (or wasn't) handled later. Pushed via `recordModeSwitch()`;
1974
- * consumers include the editAnchors dep (editingId changes), and
1975
- * could expand to active-tool / hotkey-stack / focus changes. */
1802
+ * dispatched input event but explains why an input was (or wasn't) handled
1803
+ * later. Pushed via `recordModeSwitch()`. */
1976
1804
  interface ModeSwitchLogEntry {
1977
1805
  kind: 'mode';
1978
1806
  ts: number;
@@ -1991,12 +1819,31 @@ type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1991
1819
  declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1992
1820
 
1993
1821
  /**
1994
- * Dispatcher orchestrator — pure module, no React, no DOM.
1822
+ * The slot an in-flight ongoing handle is keyed by:
1823
+ * - `key-held`: `key-held-<code>` (`key-held-Space`), so two keys that share a
1824
+ * `key`, such as the two Shifts, hold separately. An event with no code keys
1825
+ * by `key`.
1826
+ * - `pointerdown` and the drag that follows: `pointer-<pointerId>`. An event
1827
+ * with no `pointerId` (a synthesized probe, a programmatic drag, most tests)
1828
+ * keys to `pointer-mouse`.
1829
+ * - `multitouch`: `multitouch-<fingers>`.
1830
+ * - any other kind that starts an ongoing invoker: `ongoing-<kind>`.
1831
+ */
1832
+
1833
+ /**
1834
+ * The in-flight handle slot a pointer's gestures key into. Exported because
1835
+ * the React seam (`useGestureDispatcher`) has to name the same slot when it
1836
+ * asks `dispatcher.inFlight()` whether a given pointer is mid-drag.
1995
1837
  *
1996
- * Assembles `ScopedBinding[]` from the actions registry, active tool, and
1997
- * hotkey stack; matches input events via `matchSorted`; gates each candidate
1998
- * on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
1999
- * in-flight handles.
1838
+ * @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
1839
+ * synthesized event with no originating DOM pointer.
1840
+ */
1841
+ declare function pointerGestureId(pointerId: number | undefined): string;
1842
+ /** The in-flight handle slot a multitouch gesture of `fingers` fingers keys into. */
1843
+ declare function multiTouchGestureId(fingers: number): string;
1844
+
1845
+ /**
1846
+ * Dispatcher orchestrator — pure module, no React, no DOM.
2000
1847
  *
2001
1848
  * ## Precedence and fall-through
2002
1849
  * An exclusive affordance claim first bars every binding that does not
@@ -2012,46 +1859,14 @@ declare function recordModeSwitch(mode: string, from: string | null, to: string
2012
1859
  * Candidates whose `eligible` rule is false are dropped first. The dispatcher
2013
1860
  * walks the rest and fires the first action whose `enabled()` returns
2014
1861
  * `true`. If every candidate's `enabled()` returns a disabled reason, the
2015
- * event is unhandled. This mirrors CSS-style specificity matching with a
2016
- * `:not(:disabled)` filter, and lets a tool declare a high-specificity
2017
- * binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
2018
- * to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
2019
- * when its required deps aren't wired.
2020
- *
2021
- * ## gestureId scheme
2022
- * - `key-held` ongoing actions: `key-held-<code>` (`key-held-Space`), so two
2023
- * keys that share a `key`, such as the two Shifts, hold separately. An event
2024
- * with no code keys by `key`.
2025
- * Chosen because key-held gestures are identified by the held key alone.
2026
- * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
2027
- * the originating DOM `PointerEvent`. Each physical pointer — mouse, each
2028
- * touch, the stylus — gets its own handle slot. Events with no
2029
- * `pointerId` (synthesized probes, programmatic drags, most tests) key to
2030
- * `pointer-mouse`, so a single synthetic pointer behaves as it always has.
2031
- * - `multitouch` ongoing actions: `multitouch-<fingers>`.
2032
- * - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
1862
+ * event is unhandled.
2033
1863
  *
2034
1864
  * ## Action-lookup miss behavior
2035
1865
  * When the walk reaches a binding whose `actionId` has no entry in
2036
1866
  * `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
2037
- * `'unhandled'`. The user's input gesture falls through as if unmatched.
2038
- * This preserves input flow (nothing is swallowed silently) while flagging
2039
- * the misconfiguration at dev time.
1867
+ * `'unhandled'`, so the gesture falls through as if unmatched.
2040
1868
  */
2041
1869
 
2042
- /**
2043
- * The in-flight handle slot a pointer's gestures key into.
2044
- *
2045
- * Exported because the React seam (`useGestureDispatcher`) has to name the
2046
- * same slot when it asks `dispatcher.inFlight()` whether a given pointer is
2047
- * mid-drag. That used to be the string literal `'pointer-mouse'` on both
2048
- * sides, which was correct only because `gestureIdFor` ignored the pointer id
2049
- * entirely — the two lies cancelled out.
2050
- *
2051
- * @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
2052
- * synthesized event with no originating DOM pointer.
2053
- */
2054
- declare function pointerGestureId(pointerId: number | undefined): string;
2055
1870
  /** Build a dispatcher. `getAction` overrides how action ids are resolved;
2056
1871
  * by default the `DispatcherContext`'s registry is used. */
2057
1872
  declare function createDispatcher(opts?: {
@@ -2091,9 +1906,9 @@ interface ActionsRegistry {
2091
1906
  /** Fire an immediate-invoker action by id. The optional `params` arg is
2092
1907
  * forwarded to `ImmediateInvoker.run` as its second argument — use it for
2093
1908
  * parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
2094
- * Returns whether it ran one: `false` for an unknown id, and for an
2095
- * ongoing-invoker action, which `trigger` cannot reach (see `begin`). It
2096
- * does not ask `enabled`. */
1909
+ * Returns whether it ran one: `false` for an unknown id, for an action
1910
+ * whose `enabled` refuses, and for an ongoing-invoker action, which
1911
+ * `trigger` cannot reach (see `begin`). `enabled` is asked with no point. */
2097
1912
  trigger(id: string, params?: Record<string, unknown>): boolean;
2098
1913
  /**
2099
1914
  * Subscribe to registry mutations. The callback fires after any
@@ -2223,12 +2038,6 @@ declare const isAnchorOrControl: TargetPredicate & {
2223
2038
  readsAffordance: true;
2224
2039
  };
2225
2040
 
2226
- /**
2227
- * The long-press the gesture dispatcher is timing, as observable state: where
2228
- * the press landed, when the hold started, how far through it is, and whether
2229
- * any binding would fire on it. Feedback — a filling ring, a haptic — reads
2230
- * this; nothing here decides what the feedback looks like.
2231
- */
2232
2041
  /** A press being held toward a long-press. */
2233
2042
  interface PendingLongPress {
2234
2043
  pointerId: number;
@@ -2281,8 +2090,7 @@ interface LongPressState {
2281
2090
  interface LongPressStore extends LongPressState {
2282
2091
  set(next: PendingLongPress | null): void;
2283
2092
  }
2284
- /** How long a touch or pen press is held before it fires `longPress`, in ms. */
2285
- declare const LONG_PRESS_MS = 500;
2093
+
2286
2094
  /** How `useGestureDispatcher` times, publishes and signals a long-press. */
2287
2095
  interface LongPressOptions {
2288
2096
  /** Hold time in ms. Default {@link LONG_PRESS_MS}. */
@@ -2450,6 +2258,10 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
2450
2258
  * `exit` / `discard` / `commit` / `cancel` while that mode is active, `entry`
2451
2259
  * while it is not. Install it with `<SceneCanvas ambient>`.
2452
2260
  *
2261
+ * An entry key one mode shares with another mode's leaving key is reported as
2262
+ * a route conflict unless `canEnter` is given: that is what keeps the entry
2263
+ * key from firing inside the other mode.
2264
+ *
2453
2265
  * The bindings ride the hotkey tier, so leaving a mode outranks what the same
2454
2266
  * key does otherwise (Escape clearing the selection), and they yield while a
2455
2267
  * gesture is in flight, so Escape mid-drag still cancels the drag first. Keys
@@ -2479,101 +2291,36 @@ interface ToolKeybinding {
2479
2291
  }
2480
2292
  /** Modifier-key snapshot at event dispatch time. */
2481
2293
  type ToolModifiers = ModifierState;
2482
- /** Per-event context passed to every channel handler. `scratch` is typed
2483
- * via the tool's `TScratch` parameter; it survives across a single
2484
- * gesture (pointer-down through end/cancel) and is replaced on next
2485
- * gesture start by `initScratch()`. */
2486
- interface ToolCtx<TScratch = unknown> {
2487
- worldX: number;
2488
- worldY: number;
2489
- modifiers: ToolModifiers;
2490
- selection: SelectionApi;
2491
- /** Adapter/scene access — opaque at this layer; tools that need it
2492
- * cast to a known shape. This layer doesn't constrain it. */
2493
- adapter: unknown;
2494
- applyOps: (ops: Op[], label: string) => void;
2495
- /** Current viewport. Reflects camera-position semantics — see
2496
- * `View` JSDoc. */
2497
- view: View;
2498
- /** Mutate the viewport. In controlled mode this calls the consumer's
2499
- * `onViewChange`; in uncontrolled mode it updates Canvas's internal
2500
- * state. View changes are not undoable. */
2501
- setView: (next: View) => void;
2502
- /** Bounding rect of the canvas element in viewport coords. Used by
2503
- * zoom/pan tools to convert event clientX/clientY to canvas-relative
2504
- * anchors. */
2505
- canvasRect: DOMRect;
2506
- /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
2507
- * threads its sink here so tool-internal hit math (handle hitboxes,
2508
- * rotation handle, etc.) lands in the same overlay as Canvas's own
2509
- * bounds/origin records. Tools should call this conditionally with `?.`. */
2510
- debug?: DebugSink;
2511
- scratch: TScratch;
2512
- }
2513
- /**
2514
- * What `onActivate` / `onDeactivate` receive: the tool's scratch, from
2515
- * `initScratch()`. A lifecycle moment has no pointer, hit or modifier state to
2516
- * report, so nothing else is populated; a callback needing the selection or
2517
- * the scene closes over it, as the tool's actions do.
2518
- */
2519
- interface ToolLifecycleCtx<TScratch = unknown> {
2520
- scratch: TScratch;
2521
- }
2522
- /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
2523
- * `Bounds` type — the optional `rotation` field carries through so a tool
2524
- * can report an oriented preview rect (e.g. mid-rotate). */
2525
- type ToolBounds = Bounds;
2526
2294
  /**
2527
2295
  * The focus-declaring case of a `Contribution`: a mode the user switches
2528
- * into, plus the hooks that only make sense for one (`initScratch`,
2529
- * activate/deactivate, live preview, `cursor`). Everything else — bindings,
2530
- * actions, overlay, presentation — is inherited.
2296
+ * into, plus the hooks that only make sense for one (activate/deactivate,
2297
+ * `cursor`). Everything else — bindings, actions, overlay, presentation — is
2298
+ * inherited.
2531
2299
  */
2532
- interface Tool<TScratch = unknown, TOverlay = KernelOverlay> extends Contribution<TOverlay> {
2300
+ interface Tool<TOverlay = KernelOverlay> extends Contribution<TOverlay> {
2533
2301
  /** The key that activates this tool. `useKeybindings` appends a binding
2534
2302
  * entry for it to the consolidated `tool.activate` action (with
2535
2303
  * `opts.params.toolId` set so the invoker knows which tool to switch to).
2536
2304
  * A tool with none has no activation key. */
2537
2305
  keybinding?: ToolKeybinding;
2538
- initScratch?: () => TScratch;
2539
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
2306
+ cursor?: CursorSpec | (() => CursorSpec);
2540
2307
  /** The tool became live: it took the active slot, or its `hotkey` went
2541
- * down, while it held neither. */
2542
- onActivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2308
+ * down, while it held neither. A lifecycle moment has no pointer, hit or
2309
+ * modifier state to report; a callback needing the selection or the scene
2310
+ * closes over it, as the tool's actions do. */
2311
+ onActivate?: () => void;
2543
2312
  /** The tool stopped being live in both slots, or its canvas unmounted. */
2544
- onDeactivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2545
- /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
2546
- * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
2547
- * reflect live gesture state without reaching into hook internals. The
2548
- * return type is `unknown` here because the Tool interface is pose-agnostic;
2549
- * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
2550
- * the use site. */
2551
- previewPose?: (id: string) => unknown;
2552
- /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
2553
- * on it; otherwise `null`. Optional companion to `previewPose` for tools that
2554
- * can compute bounds without round-tripping through a geometry adapter. */
2555
- previewBounds?: (id: string) => ToolBounds | null;
2556
- /** Returns ids whose committed scene-render should be suppressed while this
2557
- * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
2558
- * preview ghosts replace the committed pose). The standard scene slot
2559
- * consults this alongside `previewPose` to avoid double-rendering. Returns
2560
- * `null` when no gesture is in flight. */
2561
- previewIds?: () => Iterable<string> | null;
2313
+ onDeactivate?: () => void;
2562
2314
  }
2563
2315
  /** Whether `entry` is a tool: a contribution that can hold focus, chosen from
2564
- * a palette or held on a key. Only a tool's focus hooks (`initScratch`,
2565
- * `cursor`, `onActivate`, the previews) ever run. */
2566
- declare function isTool<TOverlay>(entry: Contribution<TOverlay>): entry is Tool<unknown, TOverlay>;
2316
+ * a palette or held on a key. Only a tool's focus hooks (`cursor`,
2317
+ * `onActivate`, `onDeactivate`) ever run. */
2318
+ declare function isTool<TOverlay>(entry: Contribution<TOverlay>): entry is Tool<TOverlay>;
2567
2319
  /** Internal — which slot a tool occupies in the dispatch order. */
2568
2320
  type ToolSlot = 'hotkey' | 'active' | 'ambient';
2569
- /** Internal alias for "a Tool of any scratch type" — used in registries and
2570
- * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
2571
- * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
2572
- * is too strict for containers that accept any concrete `Tool<T>`. */
2573
- type AnyTool = Tool<any, any>;
2574
- /** "A tool of any scratch type that draws `TOverlay`" — the container form for
2575
- * registries that hold heterogeneous scratch shapes but one overlay type. */
2576
- type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
2321
+ /** A tool drawing any overlay type — the element type for registries and
2322
+ * dispatchers that never read the overlay. */
2323
+ type AnyTool = Tool<any>;
2577
2324
 
2578
2325
  /**
2579
2326
  * What a tool declares.
@@ -2584,7 +2331,7 @@ type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
2584
2331
  * definition is mostly `bindings`, plus presentation and any actions the tool
2585
2332
  * itself introduces.
2586
2333
  */
2587
- interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2334
+ interface ToolDef<TOverlay = KernelOverlay> {
2588
2335
  id: string;
2589
2336
  /** Capability tags for modality eligibility. `defineTool` forwards these
2590
2337
  * onto `Tool.eligibility.capabilities`, which is where every gate reads
@@ -2607,7 +2354,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2607
2354
  * Introspection-only — do not make this load-bearing in production.
2608
2355
  * Read off the def via `Tool.def` (the reflection escape hatch). */
2609
2356
  hookName?: string;
2610
- presentation?: ToolPresentation<TScratch>;
2357
+ presentation?: ToolPresentation;
2611
2358
  /** Optional caller-supplied activation key. Most built-in tools have their
2612
2359
  * activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
2613
2360
  * this field is for tools that want their activation key to be
@@ -2625,17 +2372,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2625
2372
  hotkey?: HotkeyTrigger;
2626
2373
  /** Fires as the tool becomes live in the active or hotkey slot. See
2627
2374
  * `Tool.onActivate`. */
2628
- onActivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2375
+ onActivate?: () => void;
2629
2376
  /** Fires as the tool stops being live in either slot. */
2630
- onDeactivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2631
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
2632
- /** Override the default scratch initializer. Default is `() => null`
2633
- * cast to `TScratch`, which works for tools whose scratch is fresh
2634
- * every gesture. Tools that need scratch identity to survive across
2635
- * gesture boundaries (e.g. the pen tool's multi-click subpath state)
2636
- * pass a stable-ref-returning thunk here. The factory forwards this
2637
- * onto the returned `Tool.initScratch`. */
2638
- initScratch?: () => TScratch;
2377
+ onDeactivate?: () => void;
2378
+ cursor?: CursorSpec | (() => CursorSpec);
2639
2379
  /** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
2640
2380
  * gesture dispatcher consults these at active scope while this tool is the
2641
2381
  * active one, and at hotkey scope while it is held. This is the tool's
@@ -2648,7 +2388,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2648
2388
  *
2649
2389
  * The layer's `draw` closure should read dynamic state through refs or
2650
2390
  * closures captured in the enclosing render scope, and gate on it there —
2651
- * `if (!scratch.something) return []` — rather than expecting the kit to
2391
+ * `if (!state.something) return []` — rather than expecting the kit to
2652
2392
  * swap layers as the gesture progresses.
2653
2393
  *
2654
2394
  * Pass an array to contribute several layers; they render in order. */
@@ -2659,20 +2399,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2659
2399
  }
2660
2400
 
2661
2401
  /**
2662
- * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.
2663
- *
2664
- * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
2665
- * tables of hit-keyed route handlers, and `defineTool` compiled them into the
2666
- * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
2667
- * tool-routing dispatcher called. Both ends of that are gone — tools declare
2668
- * `bindings` and the gesture dispatcher routes them — so what remains is
2669
- * identity plumbing plus the id check and the cursor resolver.
2670
- *
2671
- * It stays a function rather than becoming a spread because the id validation
2672
- * and the `initScratch` / `cursor` defaults are worth applying uniformly, and
2673
- * because `Tool.def` gives reflection a handle on the authored form.
2402
+ * Build a `Tool` from a declarative `ToolDef`: validates the id, applies the
2403
+ * `cursor` default, and keeps the authored form on `Tool.def` for reflection.
2674
2404
  */
2675
- declare function defineTool<TScratch = void, TOverlay = KernelOverlay>(def: ToolDef<TScratch, TOverlay>): Tool<TScratch, TOverlay>;
2405
+ declare function defineTool<TOverlay = KernelOverlay>(def: ToolDef<TOverlay>): Tool<TOverlay>;
2676
2406
 
2677
2407
  /**
2678
2408
  * One row in the route registry — a single `GestureBinding` on one tool,
@@ -2720,6 +2450,10 @@ interface RegistryEntry {
2720
2450
  /** The binding's `opts.views`: the views it is live in. Absent when it is
2721
2451
  * live in every view. */
2722
2452
  views?: readonly (string | null)[];
2453
+ /** The binding's `opts.params`, as declared: a thunk is not called. */
2454
+ params?: unknown;
2455
+ /** The binding's `opts.yields`, when it is set. */
2456
+ yields?: true;
2723
2457
  }
2724
2458
  /**
2725
2459
  * Stand-in target for a `{ kindOf }` predicate spec.
@@ -2756,10 +2490,8 @@ declare function buildRouteRegistry(entries: readonly Contribution<unknown>[]):
2756
2490
  */
2757
2491
  declare function routesForSpec(spec: GestureSpec): readonly string[];
2758
2492
 
2759
- /** Two or more tools declare the same exact (phase, gesture, arg, target,
2760
- * modifiers) tuple — the dispatcher's slot precedence picks one
2761
- * arbitrarily (well, deterministically by slot order, but the author
2762
- * probably didn't intend the duplication). */
2493
+ /** Two or more bindings declare the same exact (phase, gesture, arg, target,
2494
+ * modifiers) tuple, so slot order alone picks one. */
2763
2495
  interface Conflict {
2764
2496
  phase: 'initial' | 'engaged' | 'any';
2765
2497
  gesture: GestureName;
@@ -2780,39 +2512,32 @@ interface Conflict {
2780
2512
  * required against Alt required fire on different inputs, and Shift
2781
2513
  * required outranks Shift optional. An optional modifier against a
2782
2514
  * forbidden one is flagged, since both take the bare press.
2783
- * - A binding whose action declines via `enabled()` so a lower-priority
2784
- * one can take the gesture. Detecting that intent would mean evaluating
2785
- * the action; consumers can suppress known-intentional compositions in
2786
- * their UI layer.
2787
- *
2788
- * - Two `{ kindOf }` predicate targets. The route grammar renders every
2789
- * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
2790
- * the rendered target alone reported select's `resize` / `rotate` / `move`
2791
- * drags — three genuinely different predicates — as a three-way conflict.
2792
- * Predicate entries bucket by function identity instead, which means two
2793
- * *separately written but equivalent* predicates go unflagged. That's the
2794
- * right way to be wrong here: this check has to be silent when nothing is
2795
- * wrong or nobody will keep it on.
2796
- *
2515
+ * - Two `{ kindOf }` predicate targets, unless they are the same function.
2516
+ * Predicates bucket by identity, so two separately written but
2517
+ * equivalent ones go unflagged.
2797
2518
  * - Two actions whose `eligible` rules can never hold together — each
2798
2519
  * mode's own Escape exit, say. See {@link rulesExclusive}.
2799
- *
2800
2520
  * - Two bindings whose `opts.views` keep them from being live and equally
2801
- * ranked in any one view. See `viewsTie`.
2521
+ * ranked in any one view.
2522
+ * - Two bindings of one action with equal params that tie on one platform
2523
+ * only: `mod` against `ctrl`, which are one key off a Mac and two on it.
2524
+ * The same pair tying on both platforms is a plain duplicate and is flagged.
2802
2525
  *
2803
- * Note that two bindings sharing a tuple on the SAME tool are now possible
2804
- * (bindings are an array, where phase tables were objects with unique
2805
- * keys) — so a conflict may name one tool twice.
2526
+ * A binding whose action declines via `enabled()` so a lower-priority one
2527
+ * takes the gesture is flagged all the same, since seeing that intent would
2528
+ * mean evaluating the action, unless it says so with `opts.yields`. A pair
2529
+ * whose earlier binding yields is not a conflict; one whose later binding
2530
+ * yields still is, as the earlier one decides alone.
2806
2531
  *
2807
- * This is the raw same-tuple detector. Feeding it a whole tool *registry*
2808
- * over-reports, because registry tools take turns in the active slot and
2809
- * can't collide with each other — see `findScopedConflicts`.
2532
+ * Two bindings on the same tool can share a tuple, so a conflict may name
2533
+ * one tool twice.
2534
+ *
2535
+ * Feeding this a whole tool *registry* over-reports, because registry tools
2536
+ * take turns in the active slot and can't collide with each other — see
2537
+ * `findScopedConflicts`.
2810
2538
  */
2811
2539
  declare function findConflicts(entries: readonly Contribution<unknown>[]): Conflict[];
2812
- /**
2813
- * The tool scopes as the dispatcher sees them — which is what decides whether
2814
- * two same-tuple bindings can actually collide.
2815
- */
2540
+ /** The tool scopes as the dispatcher sees them. */
2816
2541
  interface ToolScopes {
2817
2542
  /** Tools eligible for the active slot, keyed by id or as a flat list. */
2818
2543
  registry: readonly Contribution<unknown>[] | Readonly<Record<string, Contribution<unknown>>>;
@@ -2829,17 +2554,13 @@ interface ToolScopes {
2829
2554
  /**
2830
2555
  * Detect reachable route conflicts in an assembled tool set and report each one.
2831
2556
  *
2832
- * This is the wiring `findConflicts` spent its first life without: the kit
2833
- * could detect the one class of genuine routing ambiguity it has and never
2834
- * looked. Call it once wherever a tool set is assembled (`useTools` does),
2835
- * behind a `process.env.NODE_ENV !== 'production'` guard.
2557
+ * Call it once wherever a tool set is assembled, behind a
2558
+ * `process.env.NODE_ENV !== 'production'` guard.
2836
2559
  *
2837
2560
  * **Warn, never throw.** A conflict between a consumer's tool and a kit tool
2838
- * is a design question — sometimes deliberate, since the loser can still take
2839
- * the gesture by declining through `enabled()` — and exploding in a running
2840
- * app is the wrong way to raise it. A conflict between two *kit* tools is
2841
- * always a bug, but the place to fail on that is the kit's own test suite
2842
- * (`canvas/SceneCanvas.routeConflicts.test.tsx`), not a consumer's console.
2561
+ * is sometimes deliberate, since the loser can still take the gesture when
2562
+ * the winner declines through `enabled()`. A binding ordered that way on
2563
+ * purpose says so with `opts.yields`, and is not reported.
2843
2564
  *
2844
2565
  * @returns The conflicts found, so callers can dedupe repeat reports.
2845
2566
  */
@@ -2853,6 +2574,7 @@ declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: strin
2853
2574
  * Extracted from the deleted singular `useKeybinding` hook so the remaining
2854
2575
  * consumers don't depend on its disappearance.
2855
2576
  */
2577
+
2856
2578
  /**
2857
2579
  * A key plus a modifier policy, as {@link matchesKeyBinding} tests it against a
2858
2580
  * `KeyboardEvent`. Every modifier left unset is forbidden, so `{ key: 'a' }`
@@ -2885,14 +2607,6 @@ interface KeyBinding {
2885
2607
  /** Call `preventDefault` before the handler. Default `true`. */
2886
2608
  preventDefault?: boolean;
2887
2609
  }
2888
- /** Returns true when the target is an input, textarea, or contenteditable element. */
2889
- declare function isEditableTarget(target: EventTarget | null): boolean;
2890
- /**
2891
- * True when `e` is Space or Enter on a focused control those keys activate — a
2892
- * button, a link, a checkbox. The key is that control's, so a page-level
2893
- * listener that claims it cancels the control's own activation.
2894
- */
2895
- declare function activatesFocusedControl(e: KeyboardEvent): boolean;
2896
2610
  /**
2897
2611
  * Pure matcher for `KeyBinding` against a `KeyboardEvent`. Only checks key +
2898
2612
  * modifier policy — does NOT apply the editable-target skip or
@@ -2900,38 +2614,21 @@ declare function activatesFocusedControl(e: KeyboardEvent): boolean;
2900
2614
  * gesture dispatcher so the rules can't drift.
2901
2615
  */
2902
2616
  declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
2903
- type ModifierKeys = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>;
2904
2617
  /** Which history step `e` asks for: Mod+Z undoes, Shift+Mod+Z and Mod+Y redo. `null` for anything else. Takes a
2905
2618
  * React keyboard event as readily as a DOM one. */
2906
- declare function historyKey(e: ModifierKeys): 'undo' | 'redo' | null;
2619
+ declare function historyKey(e: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>): 'undo' | 'redo' | null;
2907
2620
 
2908
2621
  /**
2909
- * Typed keys for the gesture/behavior scratch store.
2910
- *
2911
- * Gestures expose `ctx.scratch: Record<string, unknown>` as a per-gesture
2912
- * mutable bag that behaviors use to stash state across `onDown` / `onMove`
2913
- * / `onEnd` calls. The store is `unknown`-valued because behaviors are
2914
- * tool-agnostic — a single behavior may run under different tools with
2915
- * different scratch shapes. Without a typing convention every read site
2916
- * has to `(ctx.scratch as <some shape>)[KEY]`, and a writer changing the
2917
- * shape can silently break readers with no compile-time signal.
2918
- *
2919
- * `ScratchKey<T>` is a phantom-typed string that carries its payload type
2920
- * via a branded interface. `getScratch` and `setScratch` use the brand to
2921
- * narrow / constrain at the typing layer; at runtime the key is just its
2922
- * underlying string and the store remains a plain `Record`.
2622
+ * Typed keys for the gesture/behavior scratch store, `ctx.scratch`.
2623
+ * `ScratchKey<T>` carries its payload type at the typing layer; at runtime
2624
+ * the key is just its underlying string and the store remains a plain `Record`.
2923
2625
  *
2924
2626
  * @example
2925
2627
  * ```ts
2926
- * // Module-level (or behavior-level) constant, shared by writer + reader:
2628
+ * // Module-level constant, shared by writer + reader:
2927
2629
  * const LASSO_VERTICES = scratchKey<readonly { x: number; y: number }[]>('lasso.vertices');
2928
- *
2929
- * // Writer (a tool's onMove):
2930
2630
  * setScratch(ctx.scratch, LASSO_VERTICES, vertices);
2931
- *
2932
- * // Reader (a behavior's onEnd):
2933
2631
  * const vertices = getScratch(ctx.scratch, LASSO_VERTICES) ?? [];
2934
- * // ^? readonly { x: number; y: number }[] | undefined
2935
2632
  * ```
2936
2633
  */
2937
2634
  /** Phantom-typed key for a scratch slot. The `__scratchKeyPayload` phantom
@@ -2946,13 +2643,9 @@ interface ScratchKey<T> {
2946
2643
  * field already satisfies this; consumers don't need to construct one. */
2947
2644
  type ScratchStore = Record<string, unknown>;
2948
2645
  /**
2949
- * Make a typed scratch key. Use module-level constants so the same key
2950
- * is shared by writer and reader.
2951
- *
2952
- * Namespace the name by behavior or feature to avoid collisions
2953
- * (`'behavior.field'` is a good convention). Two keys with the same
2954
- * `name` refer to the same slot at runtime — the type parameter is a
2955
- * compile-time contract, not a uniqueness guarantee.
2646
+ * Make a typed scratch key. Two keys with the same `name` refer to the same
2647
+ * slot at runtime — the type parameter is a compile-time contract, not a
2648
+ * uniqueness guarantee — so namespace the name (`'behavior.field'`).
2956
2649
  */
2957
2650
  declare function scratchKey<T>(name: string): ScratchKey<T>;
2958
2651
  /** Read a typed slot from the scratch store. Returns `undefined` if
@@ -2993,6 +2686,14 @@ declare function itemsFromFiles(files: File[]): IngestItem[];
2993
2686
  /** Materialize a drop's `DataTransfer`. MUST be called during the `drop`
2994
2687
  * event dispatch (see module doc). */
2995
2688
  declare function itemsFromDataTransfer(dt: DataTransfer): Promise<IngestItem[]>;
2689
+ /**
2690
+ * What a drag still in flight says it carries: one item per flavor, MIME
2691
+ * only. A browser withholds contents until the drop, so nothing may read
2692
+ * `file` or `text` off these; they exist to ask which bindings would match.
2693
+ * A file the browser does not name yet (Firefox reports
2694
+ * `application/x-moz-file`, others only a `Files` type) is `UNKNOWN_MIME`.
2695
+ */
2696
+ declare function itemTypesFromDataTransfer(dt: DataTransfer): readonly IngestItem[];
2996
2697
  /** Materialize a paste's `clipboardData`. Fully synchronous. */
2997
2698
  declare function itemsFromClipboardData(cd: DataTransfer): IngestItem[];
2998
2699
 
@@ -3063,10 +2764,8 @@ declare function dwarn(namespace: string, ...args: unknown[]): void;
3063
2764
  * keyed on.
3064
2765
  *
3065
2766
  * It declares one entry, `activeTool`, because the value and the action that
3066
- * requires it (`tool.offhand`) are both routing's. Routing knows that an
3067
- * action names its dependencies and that the registry resolves them at
3068
- * invocation time; it does not know what a selection, a scene, or a viewport
3069
- * is. Whoever supplies those merges them in:
2767
+ * requires it (`tool.offhand`) are both routing's. Whoever supplies the rest
2768
+ * merges them in:
3070
2769
  *
3071
2770
  * ```ts
3072
2771
  * declare module '@weasel-js/routing' {
@@ -3075,12 +2774,7 @@ declare function dwarn(namespace: string, ...args: unknown[]): void;
3075
2774
  * ```
3076
2775
  *
3077
2776
  * `@weasel-js/core` merges the kit's own deps this way and re-exports
3078
- * `DepSchema`, so a consumer adds its own against `'@weasel-js/core'` and
3079
- * never has to know this module exists. The merge carries through the
3080
- * re-export in both directions — `scripts/smoke-consumer-bundle.mjs` asserts
3081
- * it against the published `.d.ts`, which is where it once broke: a
3082
- * cross-module augmentation stops merging when `rollup-plugin-dts` flattens
3083
- * both files into one chunk.
2777
+ * `DepSchema`, so a consumer adds its own against `'@weasel-js/core'`.
3084
2778
  */
3085
2779
  interface DepSchema {
3086
2780
  /** Currently active tool id + hotkey-hold stack. */
@@ -3109,4 +2803,4 @@ type KernelOverlay = OverlaySchema extends {
3109
2803
  overlay: infer O;
3110
2804
  } ? O : unknown;
3111
2805
 
3112
- export { type DispatchRecord as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type DepRegistry as D, type BindingScope as E, type BindingSource as F, type BodyPredicate as G, type BoundGesture as H, type Bounds as I, type BuildRuleCtxArgs as J, type KernelOverlay as K, type LongPressOptions as L, COARSE_TARGET_SCALE as M, type Condition as N, type OverlayPosition as O, type Conflict as P, type ContributionChrome as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type ContributionDeps as U, type ContributionRouting as V, DEFAULT_ALLOWED_CAPABILITIES as W, DEFAULT_DEVICE_PROFILE as X, type DebugSink as Y, type DetectedDeviceFacts as Z, type DeviceProfile as _, type DepName as a, type TraceLogEntry as a$, type DispatchRecordInput as a0, type DispatcherContext as a1, type DragSample as a2, type DroppedCandidate as a3, type Eligibility as a4, type EligibilityState as a5, type GestureBinding as a6, type HandleKind as a7, type HitShape as a8, type HotkeyTrigger as a9, type PointerSessionCancelReason as aA, type PointerSessionOptions as aB, type RankedCandidate as aC, type RecordCandidate as aD, type RegistryEntry as aE, type ResizeAnchor as aF, type ResolveAllOptions as aG, type ResolveOnlyResult as aH, type ResolvedCandidate as aI, type Rule as aJ, type ScopedBinding as aK, type ScratchKey as aL, type ScratchStore as aM, type SelectionApi as aN, type Selector as aO, type SpecificityPart as aP, TOOL_OFFHAND_ID as aQ, type Tool as aR, type ToolBounds as aS, type ToolCtx as aT, type ToolDef as aU, type ToolKeybinding as aV, type ToolLifecycleCtx as aW, type ToolModifiers as aX, type ToolOffhandBindingSpec as aY, type ToolPresentation as aZ, type ToolSlot as a_, INGEST_STRING_MIMES as aa, type ImmediateInvoker as ab, type InvocationCtx as ac, type Invoker as ad, type KeyBinding as ae, LONG_PRESS_MS as af, type LongPressState as ag, type LongPressStore as ah, type MatchOutcome as ai, type MatchResult as aj, type ModeShortcutHandlers as ak, type ModeSwitchLogEntry as al, type ModifierState as am, NEVER as an, type NodeId as ao, type OngoingHandle as ap, type OngoingInvoker as aq, type OngoingOverlay as ar, type OverlayRole as as, type OverlaySchema as at, PREDICATE_TARGET as au, type PendingLongPress as av, type PlacedBy as aw, type Point2 as ax, type PointerSession as ay, type PointerSessionCallbacks as az, type DepSchema as b, scratchKey as b$, type UiOngoingControl as b0, type ViewportGestureKind as b1, type WalkStep as b2, actionBindings as b3, actionItems as b4, activatesFocusedControl as b5, assembleEntries as b6, buildDepsFromRequires as b7, buildRouteRegistry as b8, buildRuleCtx as b9, isRotateHandle as bA, isSelectedBody as bB, isTool as bC, isUnselectedBody as bD, itemsFromClipboardData as bE, itemsFromDataTransfer as bF, itemsFromFiles as bG, liveScope as bH, makeToolOffhandAction as bI, matchBest as bJ, matchSorted as bK, matchSortedWithBarred as bL, matchesKeyBinding as bM, mergeContributions as bN, modeShortcutSpec as bO, modeShortcuts as bP, offhandKeyFor as bQ, openPointerSession as bR, pointerGestureId as bS, recordModeSwitch as bT, reportRouteConflicts as bU, resolveDeviceProfile as bV, resolveParams as bW, routesForSpec as bX, ruleCanHoldIn as bY, rulesExclusive as bZ, scopeBindings as b_, buildToolOffhandBindings as ba, clientToCanvas as bb, clientToCanvasRect as bc, createDepRegistry as bd, createDispatcher as be, createLongPressStore as bf, defineTool as bg, deleteScratch as bh, describeRule as bi, dlog as bj, dwarn as bk, evaluate as bl, evaluateActive as bm, evaluateEnabled as bn, findConflicts as bo, getScratch as bp, heldKeySpec as bq, historyKey as br, isAnchor as bs, isAnchorOrControl as bt, isBody as bu, isControlHandle as bv, isDebugEnabled as bw, isEditableTarget as bx, isEmpty as by, isResizeHandle as bz, type Action as c, setScratch as c0, targetConsultsAffordance as c1, validateActionDefaultBinding as c2, validateActionId as c3, DepNode as d, type AnyToolOf as e, ActiveToolContextProvider as f, ActiveToolContextProviderIfRoot as g, type ActiveToolContextProviderProps as h, type ActiveToolContextValue as i, useOptionalActiveToolContext as j, type Dispatcher as k, type AffordanceHit as l, ALWAYS as m, type ActionDeps as n, ActionDisabledReason as o, type ActionDispatch as p, type ActionEnabledResult as q, type ActionEntry as r, type ActionItem as s, type ActionPresentation as t, useActiveToolContext as u, type ActionSource as v, type ActionVariant as w, type ActionsProp as x, type AnyTool as y, type AssemblyInputs as z };
2806
+ export { type DeviceProfile as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type DepRegistry as D, type BindingScope as E, type BindingSource as F, type BodyPredicate as G, type BoundGesture as H, type Bounds as I, type BuildRuleCtxArgs as J, type KernelOverlay as K, type LongPressOptions as L, COARSE_TARGET_SCALE as M, type NodeId as N, type OverlayPosition as O, type Condition as P, type Conflict as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type ContributionChrome as U, type ContributionDeps as V, type ContributionRouting as W, DEFAULT_ALLOWED_CAPABILITIES as X, DEFAULT_DEVICE_PROFILE as Y, type DebugSink as Z, type DetectedDeviceFacts as _, type DepName as a, actionBindings as a$, type DispatchRecord as a0, type DispatchRecordInput as a1, type DispatcherContext as a2, type DragSample as a3, type DroppedCandidate as a4, type Eligibility as a5, type EligibilityState as a6, type GestureBinding as a7, type HandleKind as a8, type HitShape as a9, type PointerSessionOptions as aA, type RankedCandidate as aB, type RecordCandidate as aC, type RegistryEntry as aD, type ResizeAnchor as aE, type ResolveAllOptions as aF, type ResolveOnlyResult as aG, type ResolvedCandidate as aH, type Rule as aI, type ScopedBinding as aJ, type ScratchKey as aK, type ScratchStore as aL, type SelectionApi as aM, type Selector as aN, type SpecificityPart as aO, TOOL_OFFHAND_ID as aP, type ToolDef as aQ, type ToolKeybinding as aR, type ToolModifiers as aS, type ToolOffhandBindingSpec as aT, type ToolPresentation as aU, type ToolScopes as aV, type ToolSlot as aW, type TraceLogEntry as aX, type UiOngoingControl as aY, type ViewportGestureKind as aZ, type WalkStep as a_, type HotkeyTrigger as aa, INGEST_STRING_MIMES as ab, type ImmediateInvoker as ac, type InvocationCtx as ad, type Invoker as ae, type KeyBinding as af, type LongPressState as ag, type LongPressStore as ah, type MatchOutcome as ai, type MatchResult as aj, type ModeShortcutHandlers as ak, type ModeSwitchLogEntry as al, type ModifierState as am, NEVER as an, type OngoingHandle as ao, type OngoingInvoker as ap, type OngoingOverlay as aq, type OverlayRole as ar, type OverlaySchema as as, PREDICATE_TARGET as at, type PendingLongPress as au, type PlacedBy as av, type Point2 as aw, type PointerSession as ax, type PointerSessionCallbacks as ay, type PointerSessionCancelReason as az, type DepSchema as b, validateActionId as b$, actionItems as b0, assembleEntries as b1, buildDepsFromRequires as b2, buildRouteRegistry as b3, buildRuleCtx as b4, buildToolOffhandBindings as b5, clientToCanvas as b6, clientToCanvasRect as b7, createDepRegistry as b8, createDispatcher as b9, itemsFromDataTransfer as bA, itemsFromFiles as bB, liveScope as bC, makeToolOffhandAction as bD, matchBest as bE, matchSorted as bF, matchSortedWithBarred as bG, matchesKeyBinding as bH, mergeContributions as bI, modeShortcutSpec as bJ, modeShortcuts as bK, multiTouchGestureId as bL, offhandKeyFor as bM, openPointerSession as bN, pointerGestureId as bO, recordModeSwitch as bP, reportRouteConflicts as bQ, resolveDeviceProfile as bR, resolveParams as bS, routesForSpec as bT, ruleCanHoldIn as bU, rulesExclusive as bV, scopeBindings as bW, scratchKey as bX, setScratch as bY, targetConsultsAffordance as bZ, validateActionDefaultBinding as b_, createLongPressStore as ba, defineTool as bb, deleteScratch as bc, describeRule as bd, dlog as be, dwarn as bf, evaluate as bg, evaluateActive as bh, evaluateEnabled as bi, findConflicts as bj, getScratch as bk, heldKeySpec as bl, historyKey as bm, isAnchor as bn, isAnchorOrControl as bo, isBody as bp, isControlHandle as bq, isDebugEnabled as br, isEmpty as bs, isResizeHandle as bt, isRotateHandle as bu, isSelectedBody as bv, isTool as bw, isUnselectedBody as bx, itemTypesFromDataTransfer as by, itemsFromClipboardData as bz, type Action as c, DepNode as d, type Tool as e, ActiveToolContextProvider as f, ActiveToolContextProviderIfRoot as g, type ActiveToolContextProviderProps as h, type ActiveToolContextValue as i, useOptionalActiveToolContext as j, type Dispatcher as k, type AffordanceHit as l, ALWAYS as m, type ActionDeps as n, ActionDisabledReason as o, type ActionDispatch as p, type ActionEnabledResult as q, type ActionEntry as r, type ActionItem as s, type ActionPresentation as t, useActiveToolContext as u, type ActionSource as v, type ActionVariant as w, type ActionsProp as x, type AnyTool as y, type AssemblyInputs as z };