@weasel-js/routing 1.9.3 → 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,54 +1,32 @@
1
- import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
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';
5
+ import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
4
6
  import { CursorSpec } from '@weasel-js/cursor';
5
- import { ClaimableGesture, GestureSpec, InputEvent, TargetSpec, KeySpec, ParsedModifiers, GestureName, IngestItem } from '@weasel-js/gestures';
6
- import { Op } from '@weasel-js/history';
7
+ import { ClaimableGesture, GestureSpec, IngestItem, InputEvent, TargetSpec, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
7
8
  import '@weasel-js/select';
8
9
 
9
10
  /**
10
11
  * The primitives the routing surface is typed in.
11
12
  *
12
- * Routing decides which action a gesture runs. To do that it has to name a
13
- * viewport, a rect, a node id and a selection — but it never constructs,
14
- * projects or renders any of them. So they are declared here as structural
15
- * interfaces with no dependencies, and `@weasel-js/core` re-exports each from
16
- * the path its own call sites have always used. Same arrangement as `Op`,
17
- * which is declared in `@weasel-js/history` and re-exported by
18
- * `core/ops/types`. The selection click policy's own vocabulary comes from
19
- * `@weasel-js/select`, where the rules that read it live.
20
- *
21
- * Authority for the *behavior* behind each of these stays in core: `View` is
22
- * what `core/viewport` maintains, `SelectionApi` is what `useSelection`
23
- * returns, `DeviceProfile` is what `resolveDeviceProfile` detects. Changing
24
- * one of these shapes is a change to core's contract that happens to be
25
- * 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.
26
18
  */
27
19
 
28
20
  /** An opaque scene-node identifier. Branded so a bare string cannot stand in. */
29
21
  type NodeId = string & {
30
22
  readonly __brand: 'NodeId';
31
23
  };
32
- /**
33
- * Viewport state. `(view.x, view.y)` is the world point currently rendered at
34
- * the canvas top-left; `view.scale.x` / `view.scale.y` is pixels per world
35
- * unit on each axis.
36
- *
37
- * The one outright 2D dead end in the routing surface: a `View` names no
38
- * orientation, so a kernel with a camera cannot express its viewport as one.
39
- */
40
- interface View {
41
- x: number;
42
- y: number;
43
- scale: {
44
- x: number;
45
- y: number;
46
- };
47
- }
24
+
48
25
  /**
49
26
  * Axis-aligned rectangle in world space. The optional `rotation` (radians,
50
- * around the AABB center) lets chrome attach an orientation to an otherwise
51
- * 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.
52
30
  */
53
31
  interface Bounds {
54
32
  x: number;
@@ -56,6 +34,7 @@ interface Bounds {
56
34
  width: number;
57
35
  height: number;
58
36
  rotation?: number;
37
+ transform?: PoseLinear;
59
38
  }
60
39
  /** The selection as routing reads and writes it. */
61
40
  interface SelectionApi {
@@ -79,6 +58,7 @@ interface SelectionApi {
79
58
  * Apply a click to the selection per the configured mode/extend key.
80
59
  * - `single`: replaces selection with `[id]`, regardless of modifiers.
81
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`;
82
62
  * otherwise replaces with `[id]`.
83
63
  */
84
64
  applyClick(id: NodeId, modifiers: {
@@ -140,8 +120,8 @@ type HitShape = {
140
120
  /** What a recorded viewport change was: a translation or a scale change. */
141
121
  type ViewportGestureKind = 'pan' | 'zoom';
142
122
  /**
143
- * Where a tool reports its internal hit math so the debug overlay can draw it.
144
- * 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.
145
125
  */
146
126
  interface DebugSink {
147
127
  recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
@@ -196,6 +176,106 @@ type ResizeAnchor = {
196
176
  y: 'min' | 'max' | 'free';
197
177
  };
198
178
 
179
+ /**
180
+ * The tree both input registries hang on. A provider is a root; every canvas
181
+ * mounts a scope under the registry in scope; a yoke sits between the two. A
182
+ * registration lives in the node it was made in, and a lookup starts at a leaf
183
+ * and walks up, so one canvas never answers for another.
184
+ */
185
+
186
+ /** What every node of one tree shares: a version and the listeners to it. */
187
+ interface ScopeTree {
188
+ version: number;
189
+ readonly listeners: Emitter;
190
+ }
191
+ declare class ScopeNode<N extends ScopeNode<N>> {
192
+ readonly parent: N | null;
193
+ readonly descends: boolean;
194
+ readonly tree: ScopeTree;
195
+ private readonly children;
196
+ private activeChild;
197
+ /** `descends`: whether reads made here answer from the active child down —
198
+ * true for a root or a yoke, which chrome reads through; false for a scope,
199
+ * which answers for itself even with a scope nested inside it. */
200
+ constructor(parent: N | null, descends: boolean);
201
+ /** Bump the tree's version and tell every listener in it. */
202
+ changed(): void;
203
+ /** Join the parent's children, newest last; returns the leave. */
204
+ mount(): () => void;
205
+ /** Make this node the one its ancestors resolve through. */
206
+ activate(): void;
207
+ /** The node a lookup made here starts from: down the active children, or the
208
+ * newest where nothing has been activated yet. A yoke with no members yet
209
+ * is passed over — there is no canvas behind it to answer. */
210
+ leaf(): N;
211
+ private newestMember;
212
+ /** Where a read made through this node starts. */
213
+ start(): N;
214
+ /** Whether this node is on the path a read from the root takes. */
215
+ isActive(): boolean;
216
+ /** This node, then each ancestor up to the root. */
217
+ chain(): Generator<N>;
218
+ }
219
+
220
+ /** Which tool is active, plus the stack of tools temporarily held active by a
221
+ * hotkey (space-for-hand and the like). The dispatcher reads this to decide
222
+ * whose bindings are in scope. */
223
+ interface ActiveToolContextValue {
224
+ /** The focused tool's id, or `null` when no tool holds the active slot —
225
+ * a canvas can run on ambient bindings alone. */
226
+ active: string | null;
227
+ hotkeyStack: string[];
228
+ setActive(id: string | null): void;
229
+ pushHotkey(id: string): void;
230
+ /**
231
+ * Disengage a hotkey-held tool. With `id`, removes that tool's own entry
232
+ * wherever it now sits — holds do not have to come up in the order they went
233
+ * down, and popping the top would disengage a tool whose key is still held.
234
+ * With no id, pops the top.
235
+ */
236
+ popHotkey(id?: string): void;
237
+ }
238
+ /**
239
+ * One node of the tool tree. A node that `owns` a tool holds its state; one
240
+ * that does not — a scope inside a yoke — reads and writes its yoke's.
241
+ */
242
+ declare class ToolNode extends ScopeNode<ToolNode> {
243
+ private state;
244
+ private value;
245
+ constructor(parent: ToolNode | null, descends: boolean, owns: boolean, initialActive: string | null);
246
+ /** The node whose tool a read here resolves to. */
247
+ owner(): ToolNode;
248
+ private update;
249
+ /** A stable value per owner and state, for `useSyncExternalStore`. */
250
+ snapshot(): ActiveToolContextValue;
251
+ subscribe: (listener: () => void) => (() => void);
252
+ }
253
+ /** Props for `<ActiveToolContextProvider>`. */
254
+ interface ActiveToolContextProviderProps {
255
+ children: ReactNode;
256
+ /** Seeds the active slot, and wins over the first `useTools` call's
257
+ * `active`. Omitted, the slot starts empty and that call seeds it. */
258
+ initialActive?: string | null;
259
+ }
260
+ /** Provides root active-tool state. Every canvas below has a tool of its own;
261
+ * reads and writes here reach the one last used. */
262
+ declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react.JSX.Element;
263
+ /** The active-tool state in scope. Throws outside a provider; use
264
+ * `useOptionalActiveToolContext` where one is not guaranteed. */
265
+ declare function useActiveToolContext(): ActiveToolContextValue;
266
+ /**
267
+ * Like `useActiveToolContext`, but returns `null` when no
268
+ * `<ActiveToolContextProvider>` is in scope instead of throwing.
269
+ */
270
+ declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
271
+ /**
272
+ * Mounts an `<ActiveToolContextProvider>` only when no parent provider is in
273
+ * scope; otherwise renders children unwrapped.
274
+ */
275
+ declare function ActiveToolContextProviderIfRoot({ children, }: {
276
+ children: ReactNode;
277
+ }): react.JSX.Element;
278
+
199
279
  /**
200
280
  * Live state read by rule evaluation. Built once per frame on the consuming
201
281
  * surface — chrome-caps, the affordance pipeline, the dispatcher's
@@ -368,25 +448,20 @@ declare const NEVER: Rule;
368
448
  * { when: function hasPath() { … } } → when(hasPath)
369
449
  * ```
370
450
  *
371
- * Never throws and always terminates — unlike `JSON.stringify`, which throws
372
- * on a cyclic value and renders the `when` arm as a useless `{}`.
451
+ * Never throws and always terminates.
373
452
  */
374
453
  declare function describeRule(rule: Rule): string;
375
454
  /** Evaluate a rule against a context. Pure, and cheap enough to run per
376
455
  * frame for every piece of chrome. */
377
456
  declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
378
457
  /**
379
- * Composable eligibility predicate with a fluent surface. Carries its
380
- * underlying `Rule` tree at `.rule` so the dispatcher's eligibility filter and
381
- * the chrome resolver can introspect and share trees.
382
- *
383
- * Callable form `cond(ctx)` evaluates the tree against ctx. The fluent methods
384
- * 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.
385
461
  *
386
462
  * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
387
- * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. Mix `.and` and `.or`
388
- * only when you mean left-to-right evaluation. For grouped disjunction, name
389
- * 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(...)`.
390
465
  */
391
466
  interface Condition {
392
467
  (ctx: RuleCtx): boolean;
@@ -454,9 +529,6 @@ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
454
529
  * An Action's `invoker` field describes how it's run. `immediate` invokers
455
530
  * fire once (delete, align, undo). `ongoing` invokers open a phase machine
456
531
  * driven by the dispatcher (move, resize, pinch-zoom).
457
- *
458
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`
459
- * § "Types" for the full design.
460
532
  */
461
533
 
462
534
  /** A 2D point in either world or screen coordinates. */
@@ -465,31 +537,20 @@ interface Point2 {
465
537
  y: number;
466
538
  }
467
539
  /**
468
- * Information about which UI affordance was hit at pointerdown.
469
- *
470
- * Populated by the dispatcher when the `affordanceAt` thunk is provided to
471
- * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
472
- * affordance (e.g. a resize handle) use this field as a guard — if the
473
- * affordance is absent or is the wrong kind, they return `{}` and let other
474
- * 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`.
475
542
  *
476
543
  * `kind` is a discriminator string:
477
544
  * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
478
545
  * `'handle:bottom-right'` — corner resize handles.
479
546
  * - `'rotate-handle'` — the rotation affordance.
480
547
  * - `'anchor:N'` — a path anchor at index N.
481
- *
482
- * `fixedPoint` is the world-space point that should remain stationary during
483
- * the gesture. For resize handles this is the opposite (diagonally fixed)
484
- * corner; for rotate it is the pivot.
485
- *
486
- * `targetIds` are the node ids this affordance belongs to.
487
548
  */
488
549
  interface AffordanceHit {
489
550
  /** Discriminator string, e.g. `'handle:bottom-right'`. */
490
551
  kind: string;
491
552
  /** Id of whatever produced this hit — a kit affordance's `id`, or the
492
- * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
553
+ * registered layer's id. */
493
554
  owner?: string;
494
555
  /** `'exclusive'` means no binding may act on this point unless its target
495
556
  * consults the affordance. `'shared'` (the default) competes on scope and
@@ -504,25 +565,18 @@ interface AffordanceHit {
504
565
  };
505
566
  /** Which nodes this affordance belongs to. */
506
567
  targetIds?: string[];
507
- /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
508
- * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
509
- * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
510
- * leave this undefined. */
568
+ /** Set only when `kind` matches `'handle:*'`: which corner stays fixed
569
+ * during a resize. */
511
570
  anchor?: ResizeAnchor;
512
- /** Cursor to show while the pointer hovers this affordance (no
513
- * gesture in flight). Consumed by the hover-cursor pump in
514
- * `useGestureDispatcher`; unset = the pump falls through to
515
- * 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. */
516
574
  cursor?: CursorSpec;
517
575
  /**
518
576
  * Free-form payload from whatever produced the hit, carried through to the
519
- * matching action untouched.
520
- *
521
- * Kit affordances describe themselves fully in the fields above and leave
522
- * 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
523
578
  * a registered layer's own chrome, where the hit-test already resolved
524
- * *which* of its pieces was hit and the action would otherwise have to
525
- * redo that work. `@weasel-js/hud` passes the hit widget here.
579
+ * *which* of its pieces was hit.
526
580
  */
527
581
  payload?: unknown;
528
582
  }
@@ -531,9 +585,7 @@ interface AffordanceHit {
531
585
  * stylus state the originating `PointerEvent` carried.
532
586
  *
533
587
  * The stylus fields are absent for mouse/touch on browsers that don't report
534
- * them, and for synthetic events. Consumers that want pressure-driven output
535
- * (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
536
- * their `insert` dep receives — see `apps/site/demos/StrokeAndFillDemo.tsx`.
588
+ * them, and for synthetic events.
537
589
  */
538
590
  interface DragSample extends Point2 {
539
591
  /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
@@ -571,23 +623,19 @@ interface InvocationCtx {
571
623
  * mutates the viewport itself (pan, view-zoom), because world-space
572
624
  * deltas become self-referential as the view shifts mid-drag.
573
625
  *
574
- * Populated when the dispatcher received `clientX`/`clientY` on the
575
- * underlying pointer events. Absent for legacy callers that don't
576
- * provide them.
626
+ * Absent when the underlying pointer events carried no
627
+ * `clientX`/`clientY`.
577
628
  */
578
629
  screenDelta?: Point2;
579
630
  affordance?: AffordanceHit;
580
631
  /**
581
632
  * Full pointermove history for the current drag, in world space, with
582
633
  * per-sample stylus state when the browser reported it.
583
- * Accumulated by the dispatcher on every `pointermove` pump event.
584
634
  * Available only during `onMove` and `onEnd` calls (not on `start`).
585
635
  *
586
636
  * During `onMove` this is the dispatcher's live accumulator, and it grows
587
637
  * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
588
638
  * that is where an action that keeps the trail should take it.
589
- * Used by `lassoSelectAction` to build its polygon vertex list and by
590
- * `insertAction`'s pencil kind to carry the freehand stroke.
591
639
  */
592
640
  points?: DragSample[];
593
641
  };
@@ -609,12 +657,13 @@ interface InvocationCtx {
609
657
  centroid: Point2;
610
658
  /** Distance between the two primary pointers, in CSS pixels. */
611
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`. */
612
663
  rotation: number;
613
664
  /**
614
- * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
615
- * handle is in flight and a pointermove-pump fires.
665
+ * Pinch-zoom geometry, populated while a multitouch handle is in flight.
616
666
  * `startSpread` is the spread at the moment the gesture began.
617
- * `currentSpread` is the spread at the current frame.
618
667
  */
619
668
  pinch?: {
620
669
  startSpread: number;
@@ -635,33 +684,27 @@ interface InvocationCtx {
635
684
  viewId?: string | null;
636
685
  /**
637
686
  * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
638
- * UI-driven ongoing actions (color picker, opacity slider) so handles can
639
- * read the current value on `start` and updated values on `onMove`. The
640
- * gesture dispatcher does not populate this field; gesture-driven actions
641
- * 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).
642
690
  */
643
691
  params?: Record<string, unknown>;
644
692
  }
645
693
  /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
646
- * `opts` field and passes to `OngoingInvoker.start`. Today carries
647
- * behaviors; extensible. */
694
+ * `opts` field and passes to `OngoingInvoker.start`. */
648
695
  interface BindingOpts {
649
696
  /** Gesture behaviors the action's own invoker runs. Routing carries the
650
697
  * array to the action and never reads an element, so the element type is
651
- * open here; `@weasel-js/core` narrows it to `ActionBehavior<unknown,
652
- * unknown, unknown>[]`, which is what a behavior is authored against. */
698
+ * open here; `@weasel-js/core` narrows it. */
653
699
  behaviors?: unknown[];
654
- /** Per-binding action parameters. The action's invoker reads
655
- * these via the second arg to `run` (or via InvocationCtx for ongoing
656
- * invokers, when needed). Loose typing (Record<string, unknown>) for
657
- * 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`.
658
702
  *
659
- * params may also be a thunk evaluated each time the
660
- * dispatcher (or invoker) needs the value. Thunks let tools close over
661
- * refs that mutate during a gesture (e.g. polygon `sides` adjusted
662
- * mid-drag via ArrowUp). For ongoing invokers that want the latest
663
- * values at commit, the invoker can re-call the thunk inside `onEnd`
664
- * 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)`. */
665
708
  params?: Record<string, unknown> | (() => Record<string, unknown>);
666
709
  /**
667
710
  * The views this binding is live in, by id, where `null` is the surface's
@@ -673,6 +716,13 @@ interface BindingOpts {
673
716
  * drag bound inside a minimap view loses to the active tool's marquee.
674
717
  */
675
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;
676
726
  }
677
727
  /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
678
728
  * needed). Returns `undefined` for absent params. */
@@ -704,12 +754,10 @@ type OverlayRole =
704
754
  * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
705
755
  * shape being dragged out, the line a slice would cut along.
706
756
  *
707
- * Every variant is world geometry and nothing else. No `DrawCommand`, no
708
- * color, no coordinate space to choose: an action that reached for any of
709
- * those would be publishing core's own 2D vocabulary, and a surface rendering
710
- * the same gesture some other way could not read it. `resolveOverlays`
711
- * normalizes this into `ResolvedOverlay`, which is what a painter consumes —
712
- * `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.
713
761
  */
714
762
  type OngoingOverlay = {
715
763
  kind: 'marquee';
@@ -735,13 +783,9 @@ type OngoingOverlay = {
735
783
  shiftHeld: boolean;
736
784
  } | {
737
785
  /**
738
- * An open run of world points the gesture is proposing — the slice
739
- * action's cut line, `@weasel-js/diagram`'s routed connector.
740
- *
741
- * Geometry only, like every other variant: `role` is the whole of what
742
- * an action says about appearance, and the painting surface is what
743
- * turns it into a stroke. An action that picked its own color would be
744
- * 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.
745
789
  */
746
790
  kind: 'polyline';
747
791
  /** At least two points. Anything shorter is dropped in resolution. */
@@ -753,19 +797,14 @@ type OngoingOverlay = {
753
797
  role?: OverlayRole;
754
798
  } | {
755
799
  /**
756
- * Live insert-drag preview — dispatched by `insertAction` while the
757
- * user is dragging out a new shape. Pre-commit there is no scene node
758
- * to ghost via `previewIds()`/`previewPose()`, so insert paints its
759
- * 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.
760
803
  *
761
- * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
762
- * the current drag (start/current normalized). `extras` is the
763
- * per-kind extras the action already collected — the overlay
764
- * renderer rebuilds the shape using the same path builders the
765
- * commit factory uses, so the preview matches the eventual node.
766
- *
767
- * `extras` is opaque (`unknown`) at the union level; the overlay
768
- * 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.
769
808
  */
770
809
  kind: 'insertPreview';
771
810
  /** Which preview the dispatcher's overlay draws. The kit's own shape
@@ -779,11 +818,9 @@ type OngoingOverlay = {
779
818
  height: number;
780
819
  };
781
820
  extras: unknown;
782
- /** World-space point to paint a small "anchor" dot at. Sells the
783
- * click point as the drag's anchor — particularly useful for
784
- * radial shapes (polygon/star) where no vertex sits on the
785
- * click point, and for any shape in center mode where the dot
786
- * 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. */
787
824
  anchorPoint?: {
788
825
  x: number;
789
826
  y: number;
@@ -796,42 +833,23 @@ type OngoingOverlay = {
796
833
  interface OngoingHandle {
797
834
  /**
798
835
  * Optional logical action kind — a stable, human-readable tag the
799
- * dispatcher exposes via `getActiveAction()` for chrome-visibility
800
- * rules and any other surface that wants to react to "what action
801
- * is currently in flight" without inspecting handles directly.
802
- *
803
- * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
804
- * `'pan'`, `'pinch'`.
836
+ * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
837
+ * `'resize'`, `'pan'`.
805
838
  *
806
- * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
807
- * `key-held-Space`, etc.) which keys per-pointer state and is not
808
- * meaningful to consumers.
809
- *
810
- * When omitted, the action is "anonymous" — `getActiveAction().kind`
811
- * reports `null` even though a handle is in flight. This is fine for
812
- * 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.
813
841
  */
814
842
  kind?: string;
815
843
  onMove?(ctx: InvocationCtx): void;
816
844
  onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
817
845
  /**
818
- * Optional preview surface — dispatcher-side ghost overlay.
819
- *
820
- * An ongoing-action implementation may populate `previewIds()` +
821
- * `previewPose(id)` to expose its in-flight preview state for the
822
- * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
823
- * top of the committed scene during the gesture.
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.
824
848
  *
825
- * Returning `null` (or omitting the method entirely) means "no preview
826
- * this gesture" — the canvas will skip this handle as a source.
827
- *
828
- * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
829
- * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
830
- * returns the interim pose for one of those ids (shape opaque — the
831
- * canvas casts to its `TPose` parameter). The preview-ghost layer
832
- * merges all sources via first-non-null semantics, with tool-side
833
- * previews taking precedence over dispatcher-side (preserves
834
- * 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".
835
853
  */
836
854
  previewIds?(): Iterable<string> | null;
837
855
  previewPose?(id: string): unknown | null;
@@ -844,40 +862,22 @@ interface OngoingHandle {
844
862
  */
845
863
  previewOpaqueIds?(): Iterable<string> | null;
846
864
  /**
847
- * When `false`, the preview-ghost layer paints the ghost AND the
848
- * source node stays visible at its committed pose. Defaults to
849
- * `true` (move/resize/rotate semantics: ghost replaces the source
850
- * during the gesture). Clone overrides to `false` so the original
851
- * 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.
852
868
  */
853
869
  previewHidesSource?: boolean;
854
870
  /**
855
- * Optional per-id preview *data*. Falls back to the committed
856
- * `node.data` when null/absent. Use when the gesture mutates
857
- * `node.data` (e.g. anchor-edit on nodes that store the polygon on
858
- * `data.path`) rather than (or in addition to) the pose. The preview-
859
- * ghost layer assembles a synthetic node from `{ ...node, pose:
860
- * previewPose ?? node.pose, data: previewData ?? node.data }` before
861
- * calling the scene slot's `drawOne`.
862
- *
863
- * Sources compose first-non-null per axis: an action can emit only
864
- * `previewPose` (translation), only `previewData` (data-only edit),
865
- * or both (pose + data both change, e.g. anchor drag on a data.path
866
- * 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.
867
875
  */
868
876
  previewData?(id: string): unknown | null;
869
877
  /**
870
- * Optional chrome surface — dispatcher-side overlay layer.
871
- *
872
- * An ongoing-action implementation may populate `overlay()` to expose a
873
- * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
874
- * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
875
- * Returning `null` (or omitting the method) means "no overlay this
876
- * gesture" — the canvas will skip this handle as a chrome source.
877
- *
878
- * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
879
- * which paints displaced scene-node silhouettes. Marquee and lasso
880
- * 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".
881
881
  */
882
882
  overlay?(): OngoingOverlay | null;
883
883
  }
@@ -896,9 +896,7 @@ interface OngoingInvoker {
896
896
  timing: 'ongoing';
897
897
  start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
898
898
  }
899
- /** Pluggable invocation strategy for an Action. Future variants
900
- * (`longPress`, `twoStage`, `modal`) extend this union without touching
901
- * the `Action` type. */
899
+ /** Pluggable invocation strategy for an Action. */
902
900
  type Invoker = ImmediateInvoker | OngoingInvoker;
903
901
 
904
902
  /**
@@ -976,11 +974,9 @@ type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDis
976
974
  */
977
975
  interface ActionPresentation {
978
976
  label: string;
979
- /** Inline-SVG icon for palette / toolbar surfaces. Mirrors
980
- * `ToolPresentation.icon` so a generic `<ActionBar>` can render from
981
- * action metadata the same way `<ToolPalette>` renders from tool
982
- * metadata. May be a static `ReactNode` or a function (rare; useful
983
- * 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). */
984
980
  icon?: ReactNode | (() => ReactNode);
985
981
  /** Grouping key for palette/menu surfaces. Free-form string; the kit
986
982
  * ships defaults for `'align'` (six edges/centers), `'distribute'`
@@ -1035,30 +1031,22 @@ interface ActionDispatch extends BindingSource {
1035
1031
  scope?: 'hotkey';
1036
1032
  /**
1037
1033
  * @experimental
1038
- * Optional predicate the command palette consults when rendering. Return
1039
- * `true` when the action is currently triggerable. Return a reason string
1040
- * (e.g. `'Selection required'`) when disabled — the palette greys out
1041
- * the row, skips it in keyboard nav, ignores clicks, and shows the
1042
- * reason next to the label. Keystroke dispatch (the registered binding)
1043
- * is unaffected; the action's own `run` should self-guard.
1034
+ * Optional predicate saying whether the action can run right now. Return
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.
1044
1041
  *
1045
1042
  * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
1046
- * must not throw. If a call throws or exceeds the budget in dev mode,
1047
- * `evaluateEnabled` logs a one-time warning per action id; throws are
1048
- * caught and treated as disabled with reason `'(predicate threw)'`.
1049
- *
1050
- * Snapshot-on-open semantics: the palette evaluates `enabled` once when
1051
- * opened and does NOT re-evaluate on selection changes while open. Live
1052
- * 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`.
1053
1045
  *
1054
- * The reason set is a closed enum — to add a new reason, edit
1055
- * `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.
1056
1048
  *
1057
- * The optional `deps` argument is the same bag passed to
1058
- * `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
1059
- * synthesize it from the surrounding `DepRegistry` so predicates can
1060
- * inspect selection / scene / etc. Predicates that don't need deps just
1061
- * ignore the arg.
1049
+ * `deps` is the same bag passed to `ImmediateInvoker.run`.
1062
1050
  *
1063
1051
  * `at` is the world point of the event being routed, when it has one — a
1064
1052
  * click, a press, a long-press. An action that only applies somewhere
@@ -1066,11 +1054,30 @@ interface ActionDispatch extends BindingSource {
1066
1054
  * falls through to the next binding and hover prediction shows the
1067
1055
  * action's `cursor` only where it would act. Palettes and keystrokes
1068
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.
1069
1063
  */
1070
1064
  enabled?: (deps?: ActionDeps, at?: {
1071
1065
  x: number;
1072
1066
  y: number;
1067
+ }, carried?: {
1068
+ items: readonly IngestItem[];
1073
1069
  }) => true | ActionDisabledReason;
1070
+ /**
1071
+ * @experimental
1072
+ * Whether the action reads as held down right now: bold on the selected
1073
+ * text, snapping switched on. A toolbar button draws it as pressed.
1074
+ * Omitted = the action has no pressed state, which is not the same as
1075
+ * one that is off. Never read during dispatch.
1076
+ *
1077
+ * Under the same contract as `enabled`: pure, fast, and it must not
1078
+ * throw. `deps` is the bag `enabled` is given.
1079
+ */
1080
+ active?: (deps?: ActionDeps) => boolean;
1074
1081
  /**
1075
1082
  * Declarative eligibility rule, evaluated against the current
1076
1083
  * `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
@@ -1087,12 +1094,11 @@ interface ActionDispatch extends BindingSource {
1087
1094
  /**
1088
1095
  * CSS cursor shown while the pointer hovers a spot where this action
1089
1096
  * would win the drag — or, when the drag's winner declares no cursor,
1090
- * the click. The hover-cursor pump (in `useGestureDispatcher`) runs
1091
- * `Dispatcher.resolveOnly` on each idle pointermove — the same match
1092
- * walk a real press takes — and applies the winning action's `cursor`,
1093
- * so the hint and the actual target stay in sync by construction. Omitted = no override (the active tool's
1094
- * `Tool.cursor` shows). Affordance hits are resolved earlier in the
1095
- * 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.
1096
1102
  *
1097
1103
  * Static value only. Prediction runs `enabled()` but cannot run the
1098
1104
  * invoker, so an action that matches yet bails at `start()` (empty
@@ -1110,19 +1116,12 @@ interface ActionDispatch extends BindingSource {
1110
1116
  * ("you are panning"). An action can declare either, both, or neither;
1111
1117
  * with only `cursor` set, the hover hint holds for the duration of the
1112
1118
  * gesture.
1113
- *
1114
- * This is where mid-gesture cursors live now. They used to come from the
1115
- * tool side — an `engaged` phase entry's cursor,
1116
- * or a function-form `Tool.cursor` reading the gesture scratch out of the
1117
- * tool-routing dispatcher. Both belonged to a pipeline whose whole job was
1118
- * being taken over by bindings, and neither could describe a cursor for an
1119
- * action a tool doesn't own.
1120
1119
  */
1121
1120
  activeCursor?: CursorSpec;
1122
1121
  }
1123
1122
  /**
1124
1123
  * @experimental
1125
- * Single registered action. v1: one binding per action.
1124
+ * Single registered action.
1126
1125
  */
1127
1126
  interface Action extends ActionDispatch, ActionPresentation {
1128
1127
  }
@@ -1182,64 +1181,17 @@ interface ActionEnabledResult {
1182
1181
  * predicates should be sub-millisecond).
1183
1182
  */
1184
1183
  declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
1185
-
1186
1184
  /**
1187
- * The tree both input registries hang on. A provider is a root; every canvas
1188
- * mounts a scope under the registry in scope; a yoke sits between the two. A
1189
- * registration lives in the node it was made in, and a lookup starts at a leaf
1190
- * and walks up, so one canvas never answers for another.
1191
- *
1192
- * Design: docs/proposals/2026-09-27-input-scopes.md.
1185
+ * @experimental
1186
+ * Safely evaluate an action's `active` predicate. `undefined` for an action
1187
+ * with no pressed state; a predicate that throws reads as not active.
1193
1188
  */
1194
- /** What every node of one tree shares: a version and the listeners to it. */
1195
- interface ScopeTree {
1196
- version: number;
1197
- readonly listeners: Set<() => void>;
1198
- }
1199
- declare class ScopeNode<N extends ScopeNode<N>> {
1200
- readonly parent: N | null;
1201
- readonly descends: boolean;
1202
- readonly tree: ScopeTree;
1203
- private readonly children;
1204
- private activeChild;
1205
- /** `descends`: whether reads made here answer from the active child down —
1206
- * true for a root or a yoke, which chrome reads through; false for a scope,
1207
- * which answers for itself even with a scope nested inside it. */
1208
- constructor(parent: N | null, descends: boolean);
1209
- /** Bump the tree's version and tell every listener in it. */
1210
- changed(): void;
1211
- /** Join the parent's children, newest last; returns the leave. */
1212
- mount(): () => void;
1213
- /** Make this node the one its ancestors resolve through. */
1214
- activate(): void;
1215
- /** The node a lookup made here starts from: down the active children, or the
1216
- * newest where nothing has been activated yet. A yoke with no members yet
1217
- * is passed over — there is no canvas behind it to answer. */
1218
- leaf(): N;
1219
- private newestMember;
1220
- /** Where a read made through this node starts. */
1221
- start(): N;
1222
- /** Whether this node is on the path a read from the root takes. */
1223
- isActive(): boolean;
1224
- /** This node, then each ancestor up to the root. */
1225
- chain(): Generator<N>;
1226
- }
1189
+ declare function evaluateActive(action: Action, deps?: ActionDeps): boolean | undefined;
1227
1190
 
1228
1191
  /**
1229
- * Dep registry for registry unification.
1230
- *
1231
- * Holds named "live source" thunks. Actions declare `requires: ['selection']`;
1232
- * the dispatcher calls `registry.get('selection')` at invocation time to
1233
- * build a typed Deps bag.
1234
- *
1235
- * `DepSchema` — the map of dep name → value type — is the single source of
1236
- * truth for what `requires`/`register`/`get` accept. Its concrete fields live
1237
- * in `./depSchema` (co-located with the dep value types). It is declared there
1238
- * as a plain `export interface` rather than via a cross-module
1239
- * `declare module './depRegistry'` augmentation: that augmentation does NOT
1240
- * merge once rollup-plugin-dts flattens both files into one `.d.ts` chunk,
1241
- * which silently emptied `DepSchema` for consumers. Consumers still extend it
1242
- * 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.
1243
1195
  */
1244
1196
 
1245
1197
  /** Holds the live sources an action's declared dependencies resolve to.
@@ -1248,6 +1200,15 @@ declare class ScopeNode<N extends ScopeNode<N>> {
1248
1200
  interface DepRegistry {
1249
1201
  register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
1250
1202
  get<K extends DepName>(name: K): DepSchema[K] | undefined;
1203
+ /** Hear that what the registry answers may have changed: a source was
1204
+ * registered or released anywhere in its tree, or an owner called
1205
+ * {@link DepRegistry.changed}. Returns the unsubscribe. A hand-built
1206
+ * registry whose answers never change leaves this and `changed` out. */
1207
+ subscribe?(listener: () => void): () => void;
1208
+ /** Say that a source's value changed. A source is a thunk read on demand,
1209
+ * so its owner is the only one who knows; chrome showing an action's
1210
+ * `enabled` or `active` state re-reads on this. */
1211
+ changed?(): void;
1251
1212
  }
1252
1213
  /** One node of the dep tree: the sources registered at it, over its parent's.
1253
1214
  * Each name holds a stack of sources, newest live, so a displaced source is
@@ -1289,12 +1250,12 @@ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift' | (string & {})
1289
1250
  *
1290
1251
  * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
1291
1252
  * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1292
- interface ToolPresentation<TScratch = unknown> {
1253
+ interface ToolPresentation {
1293
1254
  /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1294
1255
  label?: string;
1295
- /** Inline-SVG icon component output. May be a static `ReactNode` or a
1296
- * function of scratch state (rare; useful for shape-aware affordances). */
1297
- 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);
1298
1259
  /** Palette grouping key. Tools sharing a group render contiguously
1299
1260
  * with separators between groups. Free-form string; the kit
1300
1261
  * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
@@ -1374,9 +1335,8 @@ interface Contribution<TOverlay = KernelOverlay> extends ContributionRouting, Co
1374
1335
  }
1375
1336
 
1376
1337
  /**
1377
- * Pure matcher primitives live in `@weasel-js/gestures`. This file
1378
- * re-exports them for kit-internal consumers and layers the actions-layer
1379
- * 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.
1380
1340
  */
1381
1341
 
1382
1342
  /** Where a binding came from, which is also its priority: a held hotkey beats
@@ -1405,26 +1365,13 @@ interface MatchResult {
1405
1365
  * only the body classification. `kindOf` predicates are handed the hit;
1406
1366
  * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
1407
1367
  * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
1408
- * `bodyTarget` / `bodyKind` and never see it — which is why chrome floating
1409
- * over empty canvas used to read as empty canvas.
1368
+ * `bodyTarget` / `bodyKind` and never see it.
1410
1369
  *
1411
- * A predicate carrying `readsAffordance: false` says the same about itself;
1412
- * the kit's own body predicates do. Shape stays the fallback for predicates
1413
- * that declare nothing.
1370
+ * A predicate declares which it is with `readsAffordance`; one that declares
1371
+ * nothing, which only untyped code can write, counts as not reading the hit.
1414
1372
  */
1415
1373
  declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
1416
- /**
1417
- * Find the best-matching binding across all scopes.
1418
- *
1419
- * Precedence: hotkey > active > ambient.
1420
- * Within a scope, more-specific bindings win; same-specificity bindings keep
1421
- * registration order (see `matchSorted`).
1422
- * Returns null when nothing matches.
1423
- *
1424
- * Note: this returns only the single best match. The dispatcher uses
1425
- * `matchSorted` so it can fall through to lower-specificity matches when a
1426
- * higher-specificity match's `enabled()` reports disabled.
1427
- */
1374
+ /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
1428
1375
  declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
1429
1376
  /**
1430
1377
  * Find every binding that matches the event, sorted by precedence
@@ -1433,12 +1380,7 @@ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], is
1433
1380
  * - Scopes by priority: hotkey > active > ambient.
1434
1381
  * - Within a scope, more-specific bindings beat less-specific (see
1435
1382
  * `specificity()`).
1436
- * - Among same-specificity bindings, first-declared wins (stable sort
1437
- * preserves registration order — same tiebreaker as pre-specificity).
1438
- *
1439
- * Used by the dispatcher to implement specificity-ordered fall-through:
1440
- * when the top match's `enabled()` returns a disabled reason (or the
1441
- * action's `start()` returns an empty handle), the next match is tried.
1383
+ * - Among same-specificity bindings, first-declared wins.
1442
1384
  */
1443
1385
  declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
1444
1386
  /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
@@ -1609,91 +1551,14 @@ interface RankedCandidate {
1609
1551
  }
1610
1552
 
1611
1553
  /**
1612
- * Dispatcher orchestrator — pure module, no React, no DOM.
1613
- *
1614
- * Assembles `ScopedBinding[]` from the actions registry, active tool, and
1615
- * hotkey stack; matches input events via `matchSorted`; gates each candidate
1616
- * on `enabled()`; then invokes `immediate` or `ongoing` invokers and tracks
1617
- * in-flight handles.
1618
- *
1619
- * ## Precedence and fall-through
1620
- * An exclusive affordance claim first bars every binding that does not
1621
- * consult the affordance. The rest are ranked, best first, by:
1622
- * 1. bindings naming the routed view ahead of every other
1623
- * (`preferViewScoped`);
1624
- * 2. scope tier: hotkey > active > ambient;
1625
- * 3. specificity within a tier (`specificity()` in `matcher.ts`);
1626
- * 4. an action gated by an `eligible` rule that holds now ahead of one with
1627
- * no rule (`preferContextual`) — Escape while editing a path exits the
1628
- * edit rather than resetting the tool;
1629
- * 5. registration order.
1630
- * Candidates whose `eligible` rule is false are dropped first. The dispatcher
1631
- * walks the rest and fires the first action whose `enabled()` returns
1632
- * `true`. If every candidate's `enabled()` returns a disabled reason, the
1633
- * event is unhandled. This mirrors CSS-style specificity matching with a
1634
- * `:not(:disabled)` filter, and lets a tool declare a high-specificity
1635
- * binding (e.g. drag-on-empty → areaSelect) that gracefully falls through
1636
- * to a lower-specificity ambient binding (e.g. drag → viewport.dragPan)
1637
- * when its required deps aren't wired.
1638
- *
1639
- * ## gestureId scheme
1640
- * - `key-held` ongoing actions: `key-held-<key>` (e.g. `key-held- ` for Space).
1641
- * Chosen because key-held gestures are identified by the held key alone.
1642
- * - `pointerdown` / drag ongoing actions: `pointer-<pointerId>`, taken from
1643
- * the originating DOM `PointerEvent`. Each physical pointer — mouse, each
1644
- * touch, the stylus — gets its own handle slot. Events with no
1645
- * `pointerId` (synthesized probes, programmatic drags, most tests) key to
1646
- * `pointer-mouse`, so a single synthetic pointer behaves as it always has.
1647
- * - `multitouch` ongoing actions: `multitouch-<fingers>`.
1648
- * - Fallback for any other kind that triggers an ongoing invoker: `ongoing-<kind>`.
1649
- *
1650
- * ## Action-lookup miss behavior
1651
- * When `matchBest` resolves a binding whose `actionId` has no entry in
1652
- * `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
1653
- * `'unhandled'`. The user's input gesture falls through as if unmatched.
1654
- * This preserves input flow (nothing is swallowed silently) while flagging
1655
- * the misconfiguration at dev time.
1554
+ * The dispatcher's contract: what a host hands it per input
1555
+ * (`DispatcherContext`) and what it answers with (`Dispatcher`).
1556
+ * `createDispatcher` in `dispatcher.ts` implements it.
1656
1557
  */
1657
1558
 
1658
- /**
1659
- * The in-flight handle slot a pointer's gestures key into.
1660
- *
1661
- * Exported because the React seam (`useGestureDispatcher`) has to name the
1662
- * same slot when it asks `dispatcher.inFlight()` whether a given pointer is
1663
- * mid-drag. That used to be the string literal `'pointer-mouse'` on both
1664
- * sides, which was correct only because `gestureIdFor` ignored the pointer id
1665
- * entirely — the two lies cancelled out.
1666
- *
1667
- * @param pointerId - `PointerEvent.pointerId`, or `undefined` for a
1668
- * synthesized event with no originating DOM pointer.
1669
- */
1670
- declare function pointerGestureId(pointerId: number | undefined): string;
1671
- /** One entry per "mode change" — a kit state transition that isn't a
1672
- * dispatched input event but is load-bearing for understanding why an
1673
- * input was (or wasn't) handled later. Pushed via `recordModeSwitch()`;
1674
- * consumers include the editAnchors dep (editingId changes), and
1675
- * could expand to active-tool / hotkey-stack / focus changes. */
1676
- interface ModeSwitchLogEntry {
1677
- kind: 'mode';
1678
- ts: number;
1679
- /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1680
- mode: string;
1681
- from: string | null;
1682
- to: string | null;
1683
- /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1684
- * `'selection-dropped target'`, etc. */
1685
- detail?: string;
1686
- }
1687
- /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
1688
- type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1689
- /** Push a mode-switch record into the trace log. Safe to call from
1690
- * anywhere; no-ops outside DEV. */
1691
- declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1692
- /** Everything the dispatcher must consult to route one input event: the
1693
- * registered actions and their deps, which tool is active, which hotkeys are
1694
- * held, and — optionally — the chrome state that action eligibility rules are
1695
- * evaluated against. Rebuilt per event rather than held, so the dispatcher
1696
- * 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. */
1697
1562
  interface DispatcherContext {
1698
1563
  /** All registered actions; the dispatcher walks `.defaultBinding` for ambient bindings. */
1699
1564
  actions: ActionSource;
@@ -1732,17 +1597,19 @@ interface DispatcherContext {
1732
1597
  * calls the handle's `onMove`. Safe to call many times.
1733
1598
  * - `end(reason)` calls `onEnd(ctx, reason)` once and removes the handle
1734
1599
  * from the in-flight map. Idempotent — further calls are no-ops.
1600
+ * - `active` is false once the handle has ended, whether through `end` or
1601
+ * by the dispatcher (`cancelAll`, or a later `beginUiOngoing` for the same
1602
+ * action). `update` and `end` do nothing from then on.
1735
1603
  */
1736
1604
  interface UiOngoingControl {
1737
1605
  readonly gestureId: string;
1606
+ readonly active: boolean;
1738
1607
  update(params?: Record<string, unknown>): void;
1739
1608
  end(reason: 'commit' | 'cancel'): void;
1740
1609
  }
1741
1610
  /**
1742
1611
  * Successful `Dispatcher.resolveOnly` prediction: the binding + action that
1743
- * would fire if `event` were dispatched for real. `action` is the resolved
1744
- * descriptor so callers (the hover-cursor pump) can read metadata like
1745
- * `Action.cursor` without a second registry lookup.
1612
+ * would fire if `event` were dispatched for real.
1746
1613
  */
1747
1614
  interface ResolveOnlyResult {
1748
1615
  actionId: string;
@@ -1795,14 +1662,12 @@ interface ResolveAllOptions {
1795
1662
  * Evaluate eligibility and `enabled()` for candidates below the winner
1796
1663
  * instead of short-circuiting them to `shadowed`.
1797
1664
  *
1798
- * Off by default, and deliberately so: the default walk's early exit is what
1799
- * keeps `resolveOnly`'s `enabled()` call count identical to a real dispatch,
1800
- * 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.
1801
1668
  *
1802
- * Turn it on for diagnostics, where "this one was outranked" is a less
1803
- * useful answer than "this one was outranked AND would have been disabled
1804
- * anyway". With it on, `shadowed` narrows to its precise meaning: this
1805
- * 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.
1806
1671
  */
1807
1672
  evaluateShadowed?: boolean;
1808
1673
  }
@@ -1824,12 +1689,10 @@ interface Dispatcher {
1824
1689
  */
1825
1690
  handleInput(event: InputEvent, ctx: DispatcherContext): 'handled' | 'unhandled';
1826
1691
  /**
1827
- * Predict which action `event` would route to WITHOUT invoking it. Replays
1828
- * the same walk as `handleInput` — scope assembly, specificity-sorted
1829
- * match, eligibility filter, per-candidate `enabled()` gate — and returns
1830
- * the first candidate that would fire, or `null` when the event would go
1831
- * unhandled. Pure query: no invoker runs, no in-flight state changes, no
1832
- * 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.
1833
1696
  *
1834
1697
  * Known divergence from a real dispatch: an ongoing invoker that matches
1835
1698
  * but returns an empty handle at `start()` (runtime bail) makes the real
@@ -1840,25 +1703,15 @@ interface Dispatcher {
1840
1703
  resolveOnly(event: InputEvent, ctx: DispatcherContext): ResolveOnlyResult | null;
1841
1704
  /**
1842
1705
  * Every binding that matches `event`, in dispatch precedence order, each
1843
- * with a verdict explaining whether it would fire. Same walk as
1844
- * `resolveOnly` — scope assembly, specificity-sorted match, eligibility
1845
- * check, per-candidate `enabled()` gate — without stopping at the winner
1846
- * and without invoking anything. Nothing is dropped: candidates that
1847
- * `resolveOnly`'s walk would filter out are kept here and labelled
1848
- * `ineligible` instead. Pure query: no invoker runs, no in-flight state
1849
- * 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.
1850
1710
  *
1851
1711
  * `resolveOnly` is the first `would-fire` entry of this list.
1852
1712
  *
1853
- * Shares `resolveOnly`'s known divergence from a real dispatch: an ongoing
1854
- * invoker that matches but returns an empty handle at `start()` makes the
1855
- * real dispatch fall through, and this cannot see that.
1856
- *
1857
- * By default everything below the winner is `shadowed` without being asked,
1858
- * which is what keeps this walk as cheap as the dispatch it replays. Pass
1859
- * `{ evaluateShadowed: true }` to keep evaluating past the winner, so a
1860
- * lower candidate that is ALSO ineligible or disabled says so — see
1861
- * {@link ResolveAllOptions.evaluateShadowed}.
1713
+ * By default everything below the winner is `shadowed` without being asked
1714
+ * — see {@link ResolveAllOptions.evaluateShadowed}.
1862
1715
  */
1863
1716
  resolveAll(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): ResolvedCandidate[];
1864
1717
  /**
@@ -1871,7 +1724,6 @@ interface Dispatcher {
1871
1724
  explain(event: InputEvent, ctx: DispatcherContext, opts?: ResolveAllOptions): DispatchRecord;
1872
1725
  /**
1873
1726
  * Synthesize an end-of-gesture for every in-flight ongoing handle.
1874
- * Used by tool-switch cancellation (Q2 decision).
1875
1727
  */
1876
1728
  cancelAll(reason: 'commit' | 'cancel'): void;
1877
1729
  /**
@@ -1880,46 +1732,38 @@ interface Dispatcher {
1880
1732
  */
1881
1733
  inFlight(): ReadonlyMap<string, OngoingHandle>;
1882
1734
  /**
1883
- * CSS cursor for the gesture currently in flight, or `null` when nothing
1735
+ * Cursor for the gesture currently in flight, as declared, or `null` when nothing
1884
1736
  * is. Reads `Action.activeCursor` (falling back to `Action.cursor`) off the
1885
1737
  * action whose handle is open — the hover pump applies this instead of its
1886
1738
  * prediction once a gesture starts, which is how `grab` becomes `grabbing`.
1887
1739
  */
1888
- inFlightCursor(): string | null;
1740
+ inFlightCursor(): CursorSpec | null;
1889
1741
  /**
1890
- * Read-only iterator over currently in-flight `OngoingHandle` instances.
1891
- *
1892
- * Surface for the canvas's preview-ghost layer (`usePreviewGhostLayer`)
1893
- * to walk each handle's `previewIds()` / `previewPose(id)` and render
1894
- * dispatcher-driven gesture previews. Read-only by design:
1895
- * 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)`.
1896
1745
  */
1897
1746
  getInFlightHandles(): Iterable<OngoingHandle>;
1898
1747
  /**
1899
- * Subscribe to in-flight state changes. The callback fires after every
1900
- * mutation that affects what the preview-ghost / dispatcher-overlay
1901
- * layers read — handle start, every `onMove` pump, end, cancel,
1902
- * cancel-all. 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.
1903
1752
  *
1904
1753
  * Returns an unsubscribe function.
1905
1754
  */
1906
1755
  subscribe(fn: () => void): () => void;
1907
1756
  /**
1908
1757
  * Monotonic counter bumped on exactly the events {@link subscribe} fires
1909
- * on. The snapshot half of the `useSyncExternalStore` contract: pair it
1910
- * with `subscribe` to drive a render off in-flight gesture state without
1911
- * a `useReducer` force-rerender.
1758
+ * on: the snapshot half of the `useSyncExternalStore` contract.
1912
1759
  *
1913
- * Starts at 0 and only ever increases. Two reads returning the same number
1914
- * mean nothing pumped in between; it does **not** guarantee that a bump
1915
- * changed anything observable (a pump that matched no binding still
1916
- * counts — 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`).
1917
1763
  */
1918
1764
  getVersion(): number;
1919
1765
  /**
1920
- * Snapshot of the currently active action, for surfaces (chrome-caps
1921
- * visibility rules, debug HUDs) that need to react to "what action
1922
- * is in flight right now."
1766
+ * Snapshot of the currently active action.
1923
1767
  *
1924
1768
  * - `kind` — the `OngoingHandle.kind` reported by the in-flight
1925
1769
  * handle (e.g. `'marquee'`, `'move'`). `null` when no action is
@@ -1930,26 +1774,15 @@ interface Dispatcher {
1930
1774
  *
1931
1775
  * When multiple handles are in flight simultaneously (e.g. a key-held
1932
1776
  * action overlapping a pointer action), the most-recently-started
1933
- * handle wins. This matches user intent: the latest interaction is
1934
- * the one consumers care about.
1935
- *
1936
- * That rule used to be near-vacuous on the pointer side, because every
1937
- * pointer shared one handle slot and two pointer drags could not coexist.
1938
- * With per-pointer keying they can — but only via paths that bypass the
1939
- * multi-pointer policy in `useGestureDispatcher` (which stops a second
1940
- * finger from opening a drag while a pinch is live), such as a mouse and
1941
- * a pen used together. Latest-start remains the right answer there.
1777
+ * handle wins.
1942
1778
  */
1943
1779
  getActiveAction(): {
1944
1780
  kind: string | null;
1945
1781
  id: string | null;
1946
1782
  };
1947
1783
  /**
1948
- * Start an ongoing action driven by UI (not a gesture). Builds an
1949
- * `InvocationCtx` with the given `deps` and `params`, calls
1950
- * `action.invoker.start(ctx, { params })`, and registers the returned
1951
- * handle in the in-flight map so `getInFlightHandles()` reports it —
1952
- * 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.
1953
1786
  *
1954
1787
  * Returns `null` if `actionId` is unknown, the action's invoker is not
1955
1788
  * ongoing, or `start` returned an empty handle.
@@ -1959,6 +1792,81 @@ interface Dispatcher {
1959
1792
  */
1960
1793
  beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
1961
1794
  }
1795
+
1796
+ /**
1797
+ * Dev-only trace of what the dispatcher did: one record per resolved input and
1798
+ * one per mode switch, in a rolling buffer the dev tools read.
1799
+ */
1800
+
1801
+ /** One entry per "mode change" — a kit state transition that isn't a
1802
+ * dispatched input event but explains why an input was (or wasn't) handled
1803
+ * later. Pushed via `recordModeSwitch()`. */
1804
+ interface ModeSwitchLogEntry {
1805
+ kind: 'mode';
1806
+ ts: number;
1807
+ /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1808
+ mode: string;
1809
+ from: string | null;
1810
+ to: string | null;
1811
+ /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1812
+ * `'selection-dropped target'`, etc. */
1813
+ detail?: string;
1814
+ }
1815
+ /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
1816
+ type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1817
+ /** Push a mode-switch record into the trace log. Safe to call from
1818
+ * anywhere; no-ops outside DEV. */
1819
+ declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1820
+
1821
+ /**
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.
1837
+ *
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.
1847
+ *
1848
+ * ## Precedence and fall-through
1849
+ * An exclusive affordance claim first bars every binding that does not
1850
+ * consult the affordance. The rest are ranked, best first, by:
1851
+ * 1. bindings naming the routed view ahead of every other
1852
+ * (`preferViewScoped`);
1853
+ * 2. scope tier: hotkey > active > ambient;
1854
+ * 3. specificity within a tier (`specificity()` in `matcher.ts`);
1855
+ * 4. an action gated by an `eligible` rule that holds now ahead of one with
1856
+ * no rule (`preferContextual`) — Escape while editing a path exits the
1857
+ * edit rather than resetting the tool;
1858
+ * 5. registration order.
1859
+ * Candidates whose `eligible` rule is false are dropped first. The dispatcher
1860
+ * walks the rest and fires the first action whose `enabled()` returns
1861
+ * `true`. If every candidate's `enabled()` returns a disabled reason, the
1862
+ * event is unhandled.
1863
+ *
1864
+ * ## Action-lookup miss behavior
1865
+ * When the walk reaches a binding whose `actionId` has no entry in
1866
+ * `ctx.actions.list()`, the dispatcher emits `console.warn` and returns
1867
+ * `'unhandled'`, so the gesture falls through as if unmatched.
1868
+ */
1869
+
1962
1870
  /** Build a dispatcher. `getAction` overrides how action ids are resolved;
1963
1871
  * by default the `DispatcherContext`'s registry is used. */
1964
1872
  declare function createDispatcher(opts?: {
@@ -1998,7 +1906,9 @@ interface ActionsRegistry {
1998
1906
  /** Fire an immediate-invoker action by id. The optional `params` arg is
1999
1907
  * forwarded to `ImmediateInvoker.run` as its second argument — use it for
2000
1908
  * parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
2001
- * Ongoing-invoker actions are not reachable from `trigger`. */
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. */
2002
1912
  trigger(id: string, params?: Record<string, unknown>): boolean;
2003
1913
  /**
2004
1914
  * Subscribe to registry mutations. The callback fires after any
@@ -2023,13 +1933,22 @@ interface ActionsRegistry {
2023
1933
  * Returns a release that takes out only this dispatcher; a `null` wires
2024
1934
  * nothing. */
2025
1935
  setDispatcher(d: Dispatcher | null): () => void;
2026
- /** Make this registry's scope the one chrome above it reaches: `trigger`,
2027
- * `begin` and `list` on any ancestor then answer from here. A dispatcher
2028
- * calls it on the pointerdown and focus that land in its scope. */
1936
+ /** Make this registry's scope the one chrome above it reaches, and the one
1937
+ * keys go to: `trigger`, `begin` and `list` on any ancestor then answer from
1938
+ * here. A dispatcher calls it on the pointerdown and wheel that land in its
1939
+ * scope; moving DOM focus alone does not. */
2029
1940
  activate(): void;
2030
- /** Whether a lookup made from the root of this registry's tree starts here —
2031
- * the question a dispatcher asks before handling a keystroke. */
1941
+ /** Whether a lookup made from the root of this registry's tree starts here.
1942
+ * A dispatcher with no canvas to press in asks this before handling a
1943
+ * keystroke. */
2032
1944
  isActive(): boolean;
1945
+ /** Whether this registry's scope is the one last activated on the page, in
1946
+ * any tree: the question a canvas's dispatcher asks before handling a key
1947
+ * or a paste that reached the page. No scope holds the keys before the
1948
+ * first activation. */
1949
+ holdsKeys(): boolean;
1950
+ /** Let go of the keys if this scope holds them, as its canvas unmounts. */
1951
+ dropKeys(): void;
2033
1952
  }
2034
1953
  /** Validate that `id` is usable as an action id in the route grammar.
2035
1954
  * Rejects ids that start with a reserved sigil (would shadow future
@@ -2060,6 +1979,12 @@ declare function offhandKeyFor(trigger: HotkeyTrigger): string;
2060
1979
  * `opts.params.toolId` so the invoker's `start` can pick out which tool
2061
1980
  * to engage. */
2062
1981
  declare function buildToolOffhandBindings(specs: readonly ToolOffhandBindingSpec[]): BoundGesture[];
1982
+ /** The `key-held` spec for holding `key` (a `KeyboardEvent.key`, or several).
1983
+ * A modifier key gets every modifier optional, since its own keydown reports
1984
+ * its flag set. Any other key engages with Shift or Alt already down and not
1985
+ * with Cmd or Ctrl, so a letter trigger leaves its browser chord alone.
1986
+ * Every hold-a-key binding builds its spec here, so that rule has one home. */
1987
+ declare function heldKeySpec(key: string | string[]): KeyHeldSpec;
2063
1988
  /** Build the consolidated `tool.offhand` action — "hold a key to engage a
2064
1989
  * secondary tool" (e.g. Space-for-hand). `bindings` enumerates one
2065
1990
  * `{ spec: keyHeld, opts: { params: { toolId } } }` per tool; the invoker
@@ -2070,7 +1995,7 @@ declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
2070
1995
 
2071
1996
  /**
2072
1997
  * Named predicates for `defaultBinding.target.kindOf`. Each matches the
2073
- * existing `kindOf` signature `(target: unknown, bodyTarget?: string) => boolean`
1998
+ * `TargetPredicate` shape, declaring whether it reads the hit,
2074
1999
  * so they drop into action descriptors without a wrapper:
2075
2000
  *
2076
2001
  * target: { kindOf: isAnchor }
@@ -2079,10 +2004,9 @@ declare function makeToolOffhandAction(bindings: BoundGesture[]): Action;
2079
2004
  * a new predicate here is what enables every action that needs the same
2080
2005
  * shape to read it. Drift prevention.
2081
2006
  */
2082
- /** A predicate that resolves from `bodyTarget` alone. The exclusive-claim
2083
- * filter reads `readsAffordance`: without it a body predicate looks like it
2084
- * consults the hit, and survives a claim that should have barred it. */
2085
- type BodyPredicate = ((target: unknown, bodyTarget?: string) => boolean) & {
2007
+
2008
+ /** A predicate that resolves from `bodyTarget` alone. */
2009
+ type BodyPredicate = TargetPredicate & {
2086
2010
  readsAffordance: false;
2087
2011
  };
2088
2012
  /** Matches any body hit — selected or unselected node body. */
@@ -2094,22 +2018,26 @@ declare const isUnselectedBody: BodyPredicate;
2094
2018
  /** Empty-canvas hit — no node beneath cursor. */
2095
2019
  declare const isEmpty: BodyPredicate;
2096
2020
  /** Any corner resize handle — top-left/top-right/bottom-left/bottom-right. */
2097
- declare const isResizeHandle: (target: unknown) => boolean;
2021
+ declare const isResizeHandle: TargetPredicate & {
2022
+ readsAffordance: true;
2023
+ };
2098
2024
  /** The rotate-around-center handle above the bounding box. */
2099
- declare const isRotateHandle: (target: unknown) => boolean;
2025
+ declare const isRotateHandle: TargetPredicate & {
2026
+ readsAffordance: true;
2027
+ };
2100
2028
  /** Any path anchor (the on-curve dot). */
2101
- declare const isAnchor: (target: unknown) => boolean;
2029
+ declare const isAnchor: TargetPredicate & {
2030
+ readsAffordance: true;
2031
+ };
2102
2032
  /** Either control handle (controlIn or controlOut). */
2103
- declare const isControlHandle: (target: unknown) => boolean;
2033
+ declare const isControlHandle: TargetPredicate & {
2034
+ readsAffordance: true;
2035
+ };
2104
2036
  /** Any of the three anchor-editing affordances (anchor + both controls). */
2105
- declare const isAnchorOrControl: (target: unknown) => boolean;
2037
+ declare const isAnchorOrControl: TargetPredicate & {
2038
+ readsAffordance: true;
2039
+ };
2106
2040
 
2107
- /**
2108
- * The long-press the gesture dispatcher is timing, as observable state: where
2109
- * the press landed, when the hold started, how far through it is, and whether
2110
- * any binding would fire on it. Feedback — a filling ring, a haptic — reads
2111
- * this; nothing here decides what the feedback looks like.
2112
- */
2113
2041
  /** A press being held toward a long-press. */
2114
2042
  interface PendingLongPress {
2115
2043
  pointerId: number;
@@ -2162,8 +2090,7 @@ interface LongPressState {
2162
2090
  interface LongPressStore extends LongPressState {
2163
2091
  set(next: PendingLongPress | null): void;
2164
2092
  }
2165
- /** How long a touch or pen press is held before it fires `longPress`, in ms. */
2166
- declare const LONG_PRESS_MS = 500;
2093
+
2167
2094
  /** How `useGestureDispatcher` times, publishes and signals a long-press. */
2168
2095
  interface LongPressOptions {
2169
2096
  /** Hold time in ms. Default {@link LONG_PRESS_MS}. */
@@ -2181,6 +2108,7 @@ interface LongPressOptions {
2181
2108
  */
2182
2109
  haptics?: boolean;
2183
2110
  }
2111
+ /** A store holding the press currently being held, or none. Pass it as `LongPressOptions.state`. */
2184
2112
  declare function createLongPressStore(): LongPressStore;
2185
2113
 
2186
2114
  /**
@@ -2265,6 +2193,29 @@ declare function liveScope(id: string, eligibility: Eligibility, state: Eligibil
2265
2193
  * of two same-specificity bindings in one tier wins.
2266
2194
  */
2267
2195
  declare function scopeBindings(entries: Iterable<Contribution<unknown>>, state: EligibilityState): ScopedBinding[];
2196
+ /** What decides which entries are live, and in what order they are walked. */
2197
+ interface AssemblyInputs {
2198
+ focusedId: string | null;
2199
+ /** Ids held by a hotkey, oldest first, as the active-tool context keeps them. */
2200
+ hotkeyStack: readonly string[];
2201
+ /** The capability tags the active mode allows. Omitted: no mode system, so
2202
+ * every entry is allowed. */
2203
+ allowedCapabilities?: ReadonlySet<CapabilityTag>;
2204
+ }
2205
+ /**
2206
+ * The entry set as routing walks it, with the state that tiers it. The
2207
+ * dispatcher's bindings and a canvas's tool overlays both come from here, so a
2208
+ * tool the dispatcher would not route to is never drawn, and two held tools
2209
+ * stack in one order.
2210
+ *
2211
+ * Held entries lead, newest hold first: the top of the stack is the one every
2212
+ * reader treats as engaged, so it wins a tie within the hotkey tier. A tagged
2213
+ * entry is live only while the mode allows every one of its tags.
2214
+ */
2215
+ declare function assembleEntries<T extends Contribution<unknown>>(entries: Iterable<T>, inputs: AssemblyInputs): {
2216
+ ordered: T[];
2217
+ state: EligibilityState;
2218
+ };
2268
2219
 
2269
2220
  /**
2270
2221
  * Concatenate contribution bundles into one registry, preserving order —
@@ -2307,6 +2258,10 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
2307
2258
  * `exit` / `discard` / `commit` / `cancel` while that mode is active, `entry`
2308
2259
  * while it is not. Install it with `<SceneCanvas ambient>`.
2309
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
+ *
2310
2265
  * The bindings ride the hotkey tier, so leaving a mode outranks what the same
2311
2266
  * key does otherwise (Escape clearing the selection), and they yield while a
2312
2267
  * gesture is in flight, so Escape mid-drag still cancels the drag first. Keys
@@ -2315,9 +2270,8 @@ declare function modeShortcutSpec(shortcut: string): KeySpec;
2315
2270
  declare function modeShortcuts(registry: ModeRegistry, handlers: ModeShortcutHandlers): Contribution<never>;
2316
2271
 
2317
2272
  /**
2318
- * Configurable activation-key descriptor for tools that expose their
2319
- * keybinding to the host (currently Lasso and Eyedropper). Captures
2320
- * only the fields meaningful to a caller-supplied tool-select key —
2273
+ * A tool's activation key, as `ToolDef.keybinding` declares it. Captures
2274
+ * only the fields meaningful to a tool-select key —
2321
2275
  * dispatcher-internal fields (`skipInEditable`, `enabled`,
2322
2276
  * `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
2323
2277
  * not part of the configurable surface.
@@ -2337,104 +2291,36 @@ interface ToolKeybinding {
2337
2291
  }
2338
2292
  /** Modifier-key snapshot at event dispatch time. */
2339
2293
  type ToolModifiers = ModifierState;
2340
- /** Per-event context passed to every channel handler. `scratch` is typed
2341
- * via the tool's `TScratch` parameter; it survives across a single
2342
- * gesture (pointer-down through end/cancel) and is replaced on next
2343
- * gesture start by `initScratch()`. */
2344
- interface ToolCtx<TScratch = unknown> {
2345
- worldX: number;
2346
- worldY: number;
2347
- modifiers: ToolModifiers;
2348
- selection: SelectionApi;
2349
- /** Adapter/scene access — opaque at this layer; tools that need it
2350
- * cast to a known shape. This layer doesn't constrain it. */
2351
- adapter: unknown;
2352
- applyOps: (ops: Op[], label: string) => void;
2353
- /** Current viewport. Reflects camera-position semantics — see
2354
- * `View` JSDoc. */
2355
- view: View;
2356
- /** Mutate the viewport. In controlled mode this calls the consumer's
2357
- * `onViewChange`; in uncontrolled mode it updates Canvas's internal
2358
- * state. View changes are not undoable. */
2359
- setView: (next: View) => void;
2360
- /** Bounding rect of the canvas element in viewport coords. Used by
2361
- * zoom/pan tools to convert event clientX/clientY to canvas-relative
2362
- * anchors. */
2363
- canvasRect: DOMRect;
2364
- /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
2365
- * threads its sink here so tool-internal hit math (handle hitboxes,
2366
- * rotation handle, etc.) lands in the same overlay as Canvas's own
2367
- * bounds/origin records. Tools should call this conditionally with `?.`. */
2368
- debug?: DebugSink;
2369
- scratch: TScratch;
2370
- }
2371
- /**
2372
- * What `onActivate` / `onDeactivate` receive: the tool's scratch, from
2373
- * `initScratch()`. A lifecycle moment has no pointer, hit or modifier state to
2374
- * report, so nothing else is populated; a callback needing the selection or
2375
- * the scene closes over it, as the tool's actions do.
2376
- */
2377
- interface ToolLifecycleCtx<TScratch = unknown> {
2378
- scratch: TScratch;
2379
- }
2380
- /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
2381
- * `Bounds` type — the optional `rotation` field carries through so a tool
2382
- * can report an oriented preview rect (e.g. mid-rotate). */
2383
- type ToolBounds = Bounds;
2384
2294
  /**
2385
2295
  * The focus-declaring case of a `Contribution`: a mode the user switches
2386
- * into, plus the hooks that only make sense for one (`initScratch`,
2387
- * activate/deactivate, live preview, `cursor`). Everything else — bindings,
2388
- * actions, overlay, presentation — is inherited.
2389
- */
2390
- interface Tool<TScratch = unknown, TOverlay = KernelOverlay> extends Contribution<TOverlay> {
2391
- /** Optional caller-supplied key. Most built-in tools have their activation
2392
- * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
2393
- * is for tools that want their activation key to be configurable by the
2394
- * host (currently Lasso and Eyedropper). The dynamic loop in
2395
- * `useKeybindings.ts` picks this up and appends a binding entry to the
2396
- * consolidated `tool.activate` action (with `opts.params.toolId` set so
2397
- * the invoker knows which tool to switch to). */
2296
+ * into, plus the hooks that only make sense for one (activate/deactivate,
2297
+ * `cursor`). Everything else — bindings, actions, overlay, presentation — is
2298
+ * inherited.
2299
+ */
2300
+ interface Tool<TOverlay = KernelOverlay> extends Contribution<TOverlay> {
2301
+ /** The key that activates this tool. `useKeybindings` appends a binding
2302
+ * entry for it to the consolidated `tool.activate` action (with
2303
+ * `opts.params.toolId` set so the invoker knows which tool to switch to).
2304
+ * A tool with none has no activation key. */
2398
2305
  keybinding?: ToolKeybinding;
2399
- initScratch?: () => TScratch;
2400
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
2306
+ cursor?: CursorSpec | (() => CursorSpec);
2401
2307
  /** The tool became live: it took the active slot, or its `hotkey` went
2402
- * down, while it held neither. */
2403
- 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;
2404
2312
  /** The tool stopped being live in both slots, or its canvas unmounted. */
2405
- onDeactivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2406
- /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
2407
- * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
2408
- * reflect live gesture state without reaching into hook internals. The
2409
- * return type is `unknown` here because the Tool interface is pose-agnostic;
2410
- * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
2411
- * the use site. */
2412
- previewPose?: (id: string) => unknown;
2413
- /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
2414
- * on it; otherwise `null`. Optional companion to `previewPose` for tools that
2415
- * can compute bounds without round-tripping through a geometry adapter. */
2416
- previewBounds?: (id: string) => ToolBounds | null;
2417
- /** Returns ids whose committed scene-render should be suppressed while this
2418
- * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
2419
- * preview ghosts replace the committed pose). The standard scene slot
2420
- * consults this alongside `previewPose` to avoid double-rendering. Returns
2421
- * `null` when no gesture is in flight. */
2422
- previewIds?: () => Iterable<string> | null;
2313
+ onDeactivate?: () => void;
2423
2314
  }
2424
2315
  /** Whether `entry` is a tool: a contribution that can hold focus, chosen from
2425
- * a palette or held on a key. Only a tool's focus hooks (`initScratch`,
2426
- * `cursor`, `onActivate`, the previews) ever run. */
2427
- 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>;
2428
2319
  /** Internal — which slot a tool occupies in the dispatch order. */
2429
2320
  type ToolSlot = 'hotkey' | 'active' | 'ambient';
2430
- /** Internal alias for "a Tool of any scratch type" — used in registries and
2431
- * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
2432
- * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
2433
- * is too strict for containers that accept any concrete `Tool<T>`. */
2434
- type AnyTool = Tool<any, any>;
2435
- /** "A tool of any scratch type that draws `TOverlay`" — the container form for
2436
- * registries that hold heterogeneous scratch shapes but one overlay type. */
2437
- 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>;
2438
2324
 
2439
2325
  /**
2440
2326
  * What a tool declares.
@@ -2445,7 +2331,7 @@ type AnyToolOf<TOverlay> = Tool<any, TOverlay>;
2445
2331
  * definition is mostly `bindings`, plus presentation and any actions the tool
2446
2332
  * itself introduces.
2447
2333
  */
2448
- interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2334
+ interface ToolDef<TOverlay = KernelOverlay> {
2449
2335
  id: string;
2450
2336
  /** Capability tags for modality eligibility. `defineTool` forwards these
2451
2337
  * onto `Tool.eligibility.capabilities`, which is where every gate reads
@@ -2468,7 +2354,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2468
2354
  * Introspection-only — do not make this load-bearing in production.
2469
2355
  * Read off the def via `Tool.def` (the reflection escape hatch). */
2470
2356
  hookName?: string;
2471
- presentation?: ToolPresentation<TScratch>;
2357
+ presentation?: ToolPresentation;
2472
2358
  /** Optional caller-supplied activation key. Most built-in tools have their
2473
2359
  * activation key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`;
2474
2360
  * this field is for tools that want their activation key to be
@@ -2486,17 +2372,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2486
2372
  hotkey?: HotkeyTrigger;
2487
2373
  /** Fires as the tool becomes live in the active or hotkey slot. See
2488
2374
  * `Tool.onActivate`. */
2489
- onActivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2375
+ onActivate?: () => void;
2490
2376
  /** Fires as the tool stops being live in either slot. */
2491
- onDeactivate?: (ctx: ToolLifecycleCtx<TScratch>) => void;
2492
- cursor?: CursorSpec | ((ctx: ToolCtx<TScratch>) => CursorSpec);
2493
- /** Override the default scratch initializer. Default is `() => null`
2494
- * cast to `TScratch`, which works for tools whose scratch is fresh
2495
- * every gesture. Tools that need scratch identity to survive across
2496
- * gesture boundaries (e.g. the pen tool's multi-click subpath state)
2497
- * pass a stable-ref-returning thunk here. The factory forwards this
2498
- * onto the returned `Tool.initScratch`. */
2499
- initScratch?: () => TScratch;
2377
+ onDeactivate?: () => void;
2378
+ cursor?: CursorSpec | (() => CursorSpec);
2500
2379
  /** Declarative gesture bindings, forwarded onto `Tool.bindings`. The
2501
2380
  * gesture dispatcher consults these at active scope while this tool is the
2502
2381
  * active one, and at hotkey scope while it is held. This is the tool's
@@ -2509,7 +2388,7 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2509
2388
  *
2510
2389
  * The layer's `draw` closure should read dynamic state through refs or
2511
2390
  * closures captured in the enclosing render scope, and gate on it there —
2512
- * `if (!scratch.something) return []` — rather than expecting the kit to
2391
+ * `if (!state.something) return []` — rather than expecting the kit to
2513
2392
  * swap layers as the gesture progresses.
2514
2393
  *
2515
2394
  * Pass an array to contribute several layers; they render in order. */
@@ -2520,20 +2399,10 @@ interface ToolDef<TScratch = void, TOverlay = KernelOverlay> {
2520
2399
  }
2521
2400
 
2522
2401
  /**
2523
- * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.
2524
- *
2525
- * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase
2526
- * tables of hit-keyed route handlers, and `defineTool` compiled them into the
2527
- * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the
2528
- * tool-routing dispatcher called. Both ends of that are gone — tools declare
2529
- * `bindings` and the gesture dispatcher routes them — so what remains is
2530
- * identity plumbing plus the id check and the cursor resolver.
2531
- *
2532
- * It stays a function rather than becoming a spread because the id validation
2533
- * and the `initScratch` / `cursor` defaults are worth applying uniformly, and
2534
- * 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.
2535
2404
  */
2536
- 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>;
2537
2406
 
2538
2407
  /**
2539
2408
  * One row in the route registry — a single `GestureBinding` on one tool,
@@ -2581,6 +2450,10 @@ interface RegistryEntry {
2581
2450
  /** The binding's `opts.views`: the views it is live in. Absent when it is
2582
2451
  * live in every view. */
2583
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;
2584
2457
  }
2585
2458
  /**
2586
2459
  * Stand-in target for a `{ kindOf }` predicate spec.
@@ -2617,10 +2490,8 @@ declare function buildRouteRegistry(entries: readonly Contribution<unknown>[]):
2617
2490
  */
2618
2491
  declare function routesForSpec(spec: GestureSpec): readonly string[];
2619
2492
 
2620
- /** Two or more tools declare the same exact (phase, gesture, arg, target,
2621
- * modifiers) tuple — the dispatcher's slot precedence picks one
2622
- * arbitrarily (well, deterministically by slot order, but the author
2623
- * 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. */
2624
2495
  interface Conflict {
2625
2496
  phase: 'initial' | 'engaged' | 'any';
2626
2497
  gesture: GestureName;
@@ -2637,41 +2508,36 @@ interface Conflict {
2637
2508
  * - Broad vs. narrow targets (e.g. an untargeted `click` alongside
2638
2509
  * `click` on `empty`) — the dispatcher's specificity ordering resolves
2639
2510
  * those cleanly, and the broad one is usually the intended fallback.
2640
- * - Different modifier requirements on the same target — they fire on
2641
- * different inputs.
2642
- * - A binding whose action declines via `enabled()` so a lower-priority
2643
- * one can take the gesture. Detecting that intent would mean evaluating
2644
- * the action; consumers can suppress known-intentional compositions in
2645
- * their UI layer.
2646
- *
2647
- * - Two `{ kindOf }` predicate targets. The route grammar renders every
2648
- * predicate as the single token {@link PREDICATE_TARGET}, so bucketing on
2649
- * the rendered target alone reported select's `resize` / `rotate` / `move`
2650
- * drags — three genuinely different predicates — as a three-way conflict.
2651
- * Predicate entries bucket by function identity instead, which means two
2652
- * *separately written but equivalent* predicates go unflagged. That's the
2653
- * right way to be wrong here: this check has to be silent when nothing is
2654
- * wrong or nobody will keep it on.
2655
- *
2511
+ * - Modifier requirements no one press satisfies at equal rank: Shift
2512
+ * required against Alt required fire on different inputs, and Shift
2513
+ * required outranks Shift optional. An optional modifier against a
2514
+ * forbidden one is flagged, since both take the bare press.
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.
2656
2518
  * - Two actions whose `eligible` rules can never hold together — each
2657
2519
  * mode's own Escape exit, say. See {@link rulesExclusive}.
2658
- *
2659
2520
  * - Two bindings whose `opts.views` keep them from being live and equally
2660
- * ranked in any one view. See {@link 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.
2661
2525
  *
2662
- * Note that two bindings sharing a tuple on the SAME tool are now possible
2663
- * (bindings are an array, where phase tables were objects with unique
2664
- * 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.
2665
2531
  *
2666
- * This is the raw same-tuple detector. Feeding it a whole tool *registry*
2667
- * over-reports, because registry tools take turns in the active slot and
2668
- * can't collide with each other — see {@link 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`.
2669
2538
  */
2670
2539
  declare function findConflicts(entries: readonly Contribution<unknown>[]): Conflict[];
2671
- /**
2672
- * The tool scopes as the dispatcher sees them — which is what decides whether
2673
- * two same-tuple bindings can actually collide.
2674
- */
2540
+ /** The tool scopes as the dispatcher sees them. */
2675
2541
  interface ToolScopes {
2676
2542
  /** Tools eligible for the active slot, keyed by id or as a flat list. */
2677
2543
  registry: readonly Contribution<unknown>[] | Readonly<Record<string, Contribution<unknown>>>;
@@ -2688,17 +2554,13 @@ interface ToolScopes {
2688
2554
  /**
2689
2555
  * Detect reachable route conflicts in an assembled tool set and report each one.
2690
2556
  *
2691
- * This is the wiring `findConflicts` spent its first life without: the kit
2692
- * could detect the one class of genuine routing ambiguity it has and never
2693
- * looked. Call it once wherever a tool set is assembled (`useTools` does),
2694
- * 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.
2695
2559
  *
2696
2560
  * **Warn, never throw.** A conflict between a consumer's tool and a kit tool
2697
- * is a design question — sometimes deliberate, since the loser can still take
2698
- * the gesture by declining through `enabled()` — and exploding in a running
2699
- * app is the wrong way to raise it. A conflict between two *kit* tools is
2700
- * always a bug, but the place to fail on that is the kit's own test suite
2701
- * (`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.
2702
2564
  *
2703
2565
  * @returns The conflicts found, so callers can dedupe repeat reports.
2704
2566
  */
@@ -2712,6 +2574,7 @@ declare function reportRouteConflicts(scopes: ToolScopes, warn?: (message: strin
2712
2574
  * Extracted from the deleted singular `useKeybinding` hook so the remaining
2713
2575
  * consumers don't depend on its disappearance.
2714
2576
  */
2577
+
2715
2578
  /**
2716
2579
  * A key plus a modifier policy, as {@link matchesKeyBinding} tests it against a
2717
2580
  * `KeyboardEvent`. Every modifier left unset is forbidden, so `{ key: 'a' }`
@@ -2744,14 +2607,6 @@ interface KeyBinding {
2744
2607
  /** Call `preventDefault` before the handler. Default `true`. */
2745
2608
  preventDefault?: boolean;
2746
2609
  }
2747
- /** Returns true when the target is an input, textarea, or contenteditable element. */
2748
- declare function isEditableTarget(target: EventTarget | null): boolean;
2749
- /**
2750
- * True when `e` is Space or Enter on a focused control those keys activate — a
2751
- * button, a link, a checkbox. The key is that control's, so a page-level
2752
- * listener that claims it cancels the control's own activation.
2753
- */
2754
- declare function activatesFocusedControl(e: KeyboardEvent): boolean;
2755
2610
  /**
2756
2611
  * Pure matcher for `KeyBinding` against a `KeyboardEvent`. Only checks key +
2757
2612
  * modifier policy — does NOT apply the editable-target skip or
@@ -2759,38 +2614,21 @@ declare function activatesFocusedControl(e: KeyboardEvent): boolean;
2759
2614
  * gesture dispatcher so the rules can't drift.
2760
2615
  */
2761
2616
  declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
2762
- type ModifierKeys = Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>;
2763
2617
  /** Which history step `e` asks for: Mod+Z undoes, Shift+Mod+Z and Mod+Y redo. `null` for anything else. Takes a
2764
2618
  * React keyboard event as readily as a DOM one. */
2765
- declare function historyKey(e: ModifierKeys): 'undo' | 'redo' | null;
2619
+ declare function historyKey(e: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>): 'undo' | 'redo' | null;
2766
2620
 
2767
2621
  /**
2768
- * Typed keys for the gesture/behavior scratch store.
2769
- *
2770
- * Gestures expose `ctx.scratch: Record<string, unknown>` as a per-gesture
2771
- * mutable bag that behaviors use to stash state across `onDown` / `onMove`
2772
- * / `onEnd` calls. The store is `unknown`-valued because behaviors are
2773
- * tool-agnostic — a single behavior may run under different tools with
2774
- * different scratch shapes. Without a typing convention every read site
2775
- * has to `(ctx.scratch as <some shape>)[KEY]`, and a writer changing the
2776
- * shape can silently break readers with no compile-time signal.
2777
- *
2778
- * `ScratchKey<T>` is a phantom-typed string that carries its payload type
2779
- * via a branded interface. `getScratch` and `setScratch` use the brand to
2780
- * narrow / constrain at the typing layer; at runtime the key is just its
2781
- * 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`.
2782
2625
  *
2783
2626
  * @example
2784
2627
  * ```ts
2785
- * // Module-level (or behavior-level) constant, shared by writer + reader:
2628
+ * // Module-level constant, shared by writer + reader:
2786
2629
  * const LASSO_VERTICES = scratchKey<readonly { x: number; y: number }[]>('lasso.vertices');
2787
- *
2788
- * // Writer (a tool's onMove):
2789
2630
  * setScratch(ctx.scratch, LASSO_VERTICES, vertices);
2790
- *
2791
- * // Reader (a behavior's onEnd):
2792
2631
  * const vertices = getScratch(ctx.scratch, LASSO_VERTICES) ?? [];
2793
- * // ^? readonly { x: number; y: number }[] | undefined
2794
2632
  * ```
2795
2633
  */
2796
2634
  /** Phantom-typed key for a scratch slot. The `__scratchKeyPayload` phantom
@@ -2805,13 +2643,9 @@ interface ScratchKey<T> {
2805
2643
  * field already satisfies this; consumers don't need to construct one. */
2806
2644
  type ScratchStore = Record<string, unknown>;
2807
2645
  /**
2808
- * Make a typed scratch key. Use module-level constants so the same key
2809
- * is shared by writer and reader.
2810
- *
2811
- * Namespace the name by behavior or feature to avoid collisions
2812
- * (`'behavior.field'` is a good convention). Two keys with the same
2813
- * `name` refer to the same slot at runtime — the type parameter is a
2814
- * 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'`).
2815
2649
  */
2816
2650
  declare function scratchKey<T>(name: string): ScratchKey<T>;
2817
2651
  /** Read a typed slot from the scratch store. Returns `undefined` if
@@ -2852,6 +2686,14 @@ declare function itemsFromFiles(files: File[]): IngestItem[];
2852
2686
  /** Materialize a drop's `DataTransfer`. MUST be called during the `drop`
2853
2687
  * event dispatch (see module doc). */
2854
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[];
2855
2697
  /** Materialize a paste's `clipboardData`. Fully synchronous. */
2856
2698
  declare function itemsFromClipboardData(cd: DataTransfer): IngestItem[];
2857
2699
 
@@ -2872,9 +2714,41 @@ declare function clientToCanvasRect(rect: {
2872
2714
  top: number;
2873
2715
  }, clientX: number, clientY: number): [number, number];
2874
2716
 
2717
+ /**
2718
+ * Namespaced, opt-in console tracing for the kit.
2719
+ *
2720
+ * Enable in the browser:
2721
+ *
2722
+ * localStorage.setItem('weasel.debug', '*') // all namespaces
2723
+ * localStorage.setItem('weasel.debug', 'tools,dispatch') // specific ones
2724
+ * localStorage.setItem('weasel.debug', '1') // legacy "all"
2725
+ *
2726
+ * Each call site picks a stable namespace string. Off by default; the flag
2727
+ * is read once and cached per page session — reload to pick up a change.
2728
+ *
2729
+ * The one definition: `@weasel-js/core` and `@weasel-js/ui` pass these on, so
2730
+ * every package reads the flag from one cache.
2731
+ *
2732
+ * Known kit namespaces:
2733
+ * - tools — active-tool + hotkey transitions (useTools)
2734
+ * - dispatch — gesture start / end / cancel (dispatcher)
2735
+ * - drag-rect — useDragRect lifecycle
2736
+ * - drag-radial — useDragRadial lifecycle
2737
+ * - scene — restored-op replay gaps (history persistence)
2738
+ * - scene-canvas — SceneCanvas mount / unmount; adapter paste-translation warnings
2739
+ * - history — applyOps push, no-op suppression, serialize/restore
2740
+ * - clipboard — OS clipboard write/read failures, malformed payloads
2741
+ */
2742
+ /** True when `namespace` (or `*`) is enabled. Pass no argument to test
2743
+ * whether *any* namespace is on (legacy global-flag semantics). */
2744
+ declare function isDebugEnabled(namespace?: string): boolean;
2875
2745
  /** Conditional `console.debug` — no-op unless `namespace` (or `*`) is enabled.
2876
2746
  * Each call is prefixed with `[namespace]` so the source is searchable. */
2877
2747
  declare function dlog(namespace: string, ...args: unknown[]): void;
2748
+ /** Conditional `console.warn` — like `dlog` but at warn level. Use for
2749
+ * diagnostics that indicate something worth surfacing during debugging
2750
+ * (e.g. no-op op batches suppressed). */
2751
+ declare function dwarn(namespace: string, ...args: unknown[]): void;
2878
2752
 
2879
2753
  /**
2880
2754
  * @weasel-js/routing — binding-to-action routing.
@@ -2889,10 +2763,9 @@ declare function dlog(namespace: string, ...args: unknown[]): void;
2889
2763
  * `Action.requires`, `DepRegistry.register`/`get` and `useDepSource` are all
2890
2764
  * keyed on.
2891
2765
  *
2892
- * It is declared **empty** here on purpose. Routing knows that an action names
2893
- * its dependencies and that the registry resolves them at invocation time; it
2894
- * does not know what a selection, a scene or a viewport is. Whoever supplies
2895
- * those merges them in:
2766
+ * It declares one entry, `activeTool`, because the value and the action that
2767
+ * requires it (`tool.offhand`) are both routing's. Whoever supplies the rest
2768
+ * merges them in:
2896
2769
  *
2897
2770
  * ```ts
2898
2771
  * declare module '@weasel-js/routing' {
@@ -2900,15 +2773,12 @@ declare function dlog(namespace: string, ...args: unknown[]): void;
2900
2773
  * }
2901
2774
  * ```
2902
2775
  *
2903
- * `@weasel-js/core` merges the kit's own 24 deps this way and re-exports
2904
- * `DepSchema`, so a consumer adds its own against `'@weasel-js/core'` and
2905
- * never has to know this module exists. The merge carries through the
2906
- * re-export in both directions — `scripts/smoke-consumer-bundle.mjs` asserts
2907
- * it against the published `.d.ts`, which is where it once broke: a
2908
- * cross-module augmentation stops merging when `rollup-plugin-dts` flattens
2909
- * both files into one chunk.
2776
+ * `@weasel-js/core` merges the kit's own deps this way and re-exports
2777
+ * `DepSchema`, so a consumer adds its own against `'@weasel-js/core'`.
2910
2778
  */
2911
2779
  interface DepSchema {
2780
+ /** Currently active tool id + hotkey-hold stack. */
2781
+ activeTool: ActiveToolContextValue;
2912
2782
  }
2913
2783
  /** Every dep name declared by anything that has merged into {@link DepSchema}. */
2914
2784
  type DepName = keyof DepSchema;
@@ -2933,4 +2803,4 @@ type KernelOverlay = OverlaySchema extends {
2933
2803
  overlay: infer O;
2934
2804
  } ? O : unknown;
2935
2805
 
2936
- export { type GestureBinding as $, type ActionsRegistry as A, type BindingOpts as B, type Contribution as C, type DepRegistry as D, COARSE_TARGET_SCALE as E, type Condition as F, type Conflict as G, type ContributionChrome as H, type ContributionDeps as I, type ContributionRouting as J, type KernelOverlay as K, type LongPressOptions as L, DEFAULT_ALLOWED_CAPABILITIES as M, DEFAULT_DEVICE_PROFILE as N, type OverlayPosition as O, type DebugSink as P, type DetectedDeviceFacts as Q, type RuleCtx as R, ScopeNode as S, type DeviceProfile as T, type DispatchRecord as U, type DispatchRecordInput as V, type DispatcherContext as W, type DragSample as X, type DroppedCandidate as Y, type Eligibility as Z, type EligibilityState as _, type DepName as a, buildDepsFromRequires as a$, type HandleKind as a0, type HitShape as a1, type HotkeyTrigger as a2, INGEST_STRING_MIMES as a3, type ImmediateInvoker as a4, type InvocationCtx as a5, type Invoker as a6, type KeyBinding as a7, LONG_PRESS_MS as a8, type LongPressState as a9, type ResolveOnlyResult as aA, type ResolvedCandidate as aB, type Rule as aC, type ScratchKey as aD, type ScratchStore as aE, type SelectionApi as aF, type Selector as aG, type SpecificityPart as aH, TOOL_OFFHAND_ID as aI, type Tool as aJ, type ToolBounds as aK, type ToolCtx as aL, type ToolDef as aM, type ToolKeybinding as aN, type ToolLifecycleCtx as aO, type ToolModifiers as aP, type ToolOffhandBindingSpec as aQ, type ToolPresentation as aR, type ToolSlot as aS, type TraceLogEntry as aT, type UiOngoingControl as aU, type View as aV, type ViewportGestureKind as aW, type WalkStep as aX, actionBindings as aY, actionItems as aZ, activatesFocusedControl as a_, type LongPressStore as aa, type MatchOutcome as ab, type MatchResult as ac, type ModeShortcutHandlers as ad, type ModeSwitchLogEntry as ae, type ModifierState as af, NEVER as ag, type NodeId as ah, type OngoingHandle as ai, type OngoingInvoker as aj, type OngoingOverlay as ak, type OverlayRole as al, type OverlaySchema as am, PREDICATE_TARGET as an, type PendingLongPress as ao, type PlacedBy as ap, type Point2 as aq, type PointerSession as ar, type PointerSessionCallbacks as as, type PointerSessionCancelReason as at, type PointerSessionOptions as au, type RankedCandidate as av, type RecordCandidate as aw, type RegistryEntry as ax, type ResizeAnchor as ay, type ResolveAllOptions as az, type DepSchema as b, buildRouteRegistry as b0, buildRuleCtx as b1, buildToolOffhandBindings as b2, clientToCanvas as b3, clientToCanvasRect as b4, createDepRegistry as b5, createDispatcher as b6, createLongPressStore as b7, defineTool as b8, deleteScratch as b9, matchesKeyBinding as bA, mergeContributions as bB, modeShortcutSpec as bC, modeShortcuts as bD, offhandKeyFor as bE, openPointerSession as bF, pointerGestureId as bG, recordModeSwitch as bH, reportRouteConflicts as bI, resolveDeviceProfile as bJ, resolveParams as bK, routesForSpec as bL, ruleCanHoldIn as bM, rulesExclusive as bN, scopeBindings as bO, scratchKey as bP, setScratch as bQ, targetConsultsAffordance as bR, validateActionDefaultBinding as bS, validateActionId as bT, describeRule as ba, dlog as bb, evaluate as bc, evaluateEnabled as bd, findConflicts as be, getScratch as bf, historyKey as bg, isAnchor as bh, isAnchorOrControl as bi, isBody as bj, isControlHandle as bk, isEditableTarget as bl, isEmpty as bm, isResizeHandle as bn, isRotateHandle as bo, isSelectedBody as bp, isTool as bq, isUnselectedBody as br, itemsFromClipboardData as bs, itemsFromDataTransfer as bt, itemsFromFiles as bu, liveScope as bv, makeToolOffhandAction as bw, matchBest as bx, matchSorted as by, matchSortedWithBarred as bz, type Action as c, DepNode as d, type AnyToolOf as e, type ScopedBinding as f, type Dispatcher as g, type AffordanceHit as h, ALWAYS as i, type ActionDeps as j, ActionDisabledReason as k, type ActionDispatch as l, type ActionEnabledResult as m, type ActionEntry as n, type ActionItem as o, type ActionPresentation as p, type ActionSource as q, type ActionVariant as r, type ActionsProp as s, type AnyTool as t, type BindingScope as u, type BindingSource as v, type BodyPredicate as w, type BoundGesture as x, type Bounds as y, type BuildRuleCtxArgs 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 };