@weasel-js/routing 1.10.1 → 1.11.1

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,10 +1,14 @@
1
- import { PoseLinear, View } from '@weasel-js/geom';
1
+ import { ModifierKey, ClaimableGesture, GestureSpec, InputEvent, TargetSpec, IngestItem, BodyTarget, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
2
+ import { FramedBox, View, Point } from '@weasel-js/geom';
3
+ import { CursorSpec } from '@weasel-js/cursor';
4
+ import { MenuEntry, Rect, MenuShortcut } from '@weasel-js/menu';
5
+ import { PrefGroup } from '@weasel-js/prefs';
6
+ import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
7
+ import * as _weasel_js_icons from '@weasel-js/icons';
8
+ import { IconRef } from '@weasel-js/icons';
2
9
  import * as react from 'react';
3
10
  import { ReactNode } from 'react';
4
- import { Emitter } from '@weasel-js/registry';
5
- import { ClaimableGesture, GestureSpec, IngestItem, InputEvent, TargetSpec, KeyHeldSpec, TargetPredicate, KeySpec, ParsedModifiers, GestureName } from '@weasel-js/gestures';
6
- import { CapabilityTag, ActiveMode, ModeRegistry } from '@weasel-js/modes';
7
- import { CursorSpec } from '@weasel-js/cursor';
11
+ import { Emitter, QueryMatch } from '@weasel-js/registry';
8
12
  import '@weasel-js/select';
9
13
 
10
14
  /**
@@ -22,20 +26,9 @@ type NodeId = string & {
22
26
  readonly __brand: 'NodeId';
23
27
  };
24
28
 
25
- /**
26
- * Axis-aligned rectangle in world space. The optional `rotation` (radians,
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.
30
- */
31
- interface Bounds {
32
- x: number;
33
- y: number;
34
- width: number;
35
- height: number;
36
- rotation?: number;
37
- transform?: PoseLinear;
38
- }
29
+ /** Axis-aligned rectangle in world space, with the frame it is drawn in;
30
+ * geom's `FramedBox`. */
31
+ type Bounds = FramedBox;
39
32
  /** The selection as routing reads and writes it. */
40
33
  interface SelectionApi {
41
34
  /** Current selection. Re-renders trigger when this reference changes. */
@@ -176,126 +169,815 @@ type ResizeAnchor = {
176
169
  y: 'min' | 'max' | 'free';
177
170
  };
178
171
 
172
+ /** The chrome an in-flight gesture paints beside the scene. */
173
+
179
174
  /**
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.
175
+ * What a `polyline` overlay is, in a word — the narrowest thing an action can
176
+ * say about how its run should look without naming a paint.
177
+ *
178
+ * The named roles are the kit's; a painter maps each to a style and falls back
179
+ * to plain chrome for one it does not know, so a package outside core can coin
180
+ * its own and still be drawn.
184
181
  */
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
- }
182
+ type OverlayRole =
183
+ /** Generic in-flight chrome — the marquee/lasso paint. */
184
+ 'chrome'
185
+ /** A cut the gesture would make. */
186
+ | 'cut'
187
+ /** An edge the gesture would author between two points. */
188
+ | 'connector'
189
+ /** The stretch of a path a soft-selection drag reaches. */
190
+ | 'falloff'
191
+ /** What a lock holds a dragged point to — a line, a circle. */
192
+ | 'lock'
193
+ /** Where a dragged item would land: a reorder's insertion bar. */
194
+ | 'insert'
195
+ /** Where a dragged item is over and cannot land. */
196
+ | 'refused' | (string & {});
238
197
  /**
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.
198
+ * What an in-flight gesture proposes to paint *beside* the scene, returned by
199
+ * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
200
+ * shape being dragged out, the line a slice would cut along.
201
+ *
202
+ * Every variant is world geometry and nothing else — no `DrawCommand`, no
203
+ * color, no coordinate space to choose — so a surface rendering the same
204
+ * gesture some other way can still read it. The painter owns every style
205
+ * decision.
241
206
  */
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;
207
+ type OngoingOverlay = {
208
+ kind: 'marquee';
209
+ start: {
210
+ x: number;
211
+ y: number;
212
+ };
213
+ current: {
214
+ x: number;
215
+ y: number;
216
+ };
217
+ shiftHeld: boolean;
218
+ } | {
219
+ kind: 'lasso';
220
+ vertices: ReadonlyArray<{
221
+ x: number;
222
+ y: number;
223
+ }>;
224
+ current: {
225
+ x: number;
226
+ y: number;
227
+ };
228
+ shiftHeld: boolean;
229
+ } | {
230
+ /**
231
+ * An open run of world points the gesture is proposing — a cut line, a
232
+ * routed connector. `role` is the whole of what an action says about
233
+ * appearance.
234
+ */
235
+ kind: 'polyline';
236
+ /** At least two points. Anything shorter is dropped in resolution. */
237
+ points: ReadonlyArray<{
238
+ x: number;
239
+ y: number;
240
+ }>;
241
+ /** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
242
+ role?: OverlayRole;
243
+ } | {
244
+ /**
245
+ * A round brush dragged through world points: the area it has swept so
246
+ * far, and its footprint at the last point. The radius is in screen
247
+ * pixels, so the footprint keeps its size at every zoom.
248
+ */
249
+ kind: 'brush';
250
+ /** The brush's path so far, at least one point; the last is where it is now. */
251
+ points: ReadonlyArray<{
252
+ x: number;
253
+ y: number;
254
+ }>;
255
+ radiusPx: number;
256
+ } | {
257
+ /**
258
+ * Live insert-drag preview. Pre-commit there is no scene node to ghost
259
+ * via `previewIds()`/`previewPose()`, so insert paints its preview
260
+ * through the overlay instead.
261
+ *
262
+ * `bounds` is the AABB of the current drag (start/current normalized).
263
+ * `extras` is the per-kind extras the action already collected; the
264
+ * overlay renderer narrows on `shape` and casts the field shape it
265
+ * expects.
266
+ */
267
+ kind: 'insertPreview';
268
+ /** Which preview the dispatcher's overlay draws. The kit's own shape
269
+ * kinds are the ones it knows how to render; a consumer-defined kind
270
+ * is legal and simply gets no live preview. */
271
+ shape: string;
272
+ bounds: {
273
+ x: number;
274
+ y: number;
275
+ width: number;
276
+ height: number;
277
+ };
278
+ extras: unknown;
279
+ /** World-space point to paint a small "anchor" dot at, marking the
280
+ * click point — for radial shapes (polygon/star), where no vertex
281
+ * sits on it, and for any shape in center mode. */
282
+ anchorPoint?: {
283
+ x: number;
284
+ y: number;
285
+ };
286
+ /** World affines the shape will also be drawn under once it lands, by
287
+ * the container it is drawn into: the preview is painted under each. */
288
+ copies?: readonly (readonly [number, number, number, number, number, number])[];
289
+ };
290
+ /** A handle's overlays as a list: empty when it has none. */
291
+ declare function overlaysOf(handle: OngoingHandle): readonly OngoingOverlay[];
292
+
266
293
  /**
267
- * Like `useActiveToolContext`, but returns `null` when no
268
- * `<ActiveToolContextProvider>` is in scope instead of throwing.
294
+ * Modifier roles: the modifiers an ongoing action reads while it runs, named
295
+ * by what they do rather than which key they are, so a binding or a tuning can
296
+ * move one to another key without forking the action.
269
297
  */
270
- declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
298
+
299
+ /** The keys that hold a role: any one of them held holds it. `'mod'` is Cmd
300
+ * on a Mac and Ctrl elsewhere. An empty list never holds it. */
301
+ type RoleKeys = readonly ModifierKey[];
302
+ /** One modifier an action reads mid-gesture. */
303
+ interface ModifierRole {
304
+ label: string;
305
+ /** The keys that hold it unless a tuning or a binding says otherwise. */
306
+ keys: RoleKeys;
307
+ }
308
+ /** An action's roles by id, e.g. `'handle.holdAngle'`. */
309
+ type ModifierRoles = Readonly<Record<string, ModifierRole>>;
310
+ /** Keys a tuning or a binding gives some of an action's roles. */
311
+ type RoleRebinds = Readonly<Record<string, RoleKeys>>;
312
+ /** Whether any of `keys` is held. */
313
+ declare function keysHeld(keys: RoleKeys, modifiers: ModifierState, isMac: boolean): boolean;
314
+ /**
315
+ * A reader for an action's own roles. It answers from `ctx.roles`, which the
316
+ * dispatcher resolves on every event, and falls back to the role's default
317
+ * keys where nothing resolved them: an invoker called directly, or a context
318
+ * a cancel-all built with no event.
319
+ */
320
+ declare function roleReader<R extends ModifierRoles>(roles: R): (ctx: {
321
+ roles?: Readonly<Record<string, boolean>>;
322
+ modifiers: ModifierState;
323
+ }, id: keyof R & string) => boolean;
324
+
271
325
  /**
272
- * Mounts an `<ActiveToolContextProvider>` only when no parent provider is in
273
- * scope; otherwise renders children unwrapped.
326
+ * Invoker — pluggable invocation strategy for an Action.
327
+ *
328
+ * An Action's `invoker` field describes how it's run. `immediate` invokers
329
+ * fire once (delete, align, undo). `ongoing` invokers open a phase machine
330
+ * driven by the dispatcher (move, resize, pinch-zoom).
274
331
  */
275
- declare function ActiveToolContextProviderIfRoot({ children, }: {
276
- children: ReactNode;
277
- }): react.JSX.Element;
278
332
 
333
+ /** A 2D point in either world or screen coordinates; geom's `Point`. */
334
+ type Point2 = Point;
279
335
  /**
280
- * Live state read by rule evaluation. Built once per frame on the consuming
281
- * surface — chrome-caps, the affordance pipeline, the dispatcher's
282
- * eligibility filter — and discarded.
336
+ * Which UI affordance was hit at pointerdown. Populated by the dispatcher when
337
+ * the `affordanceAt` thunk is provided to `useGestureDispatcher`.
283
338
  *
284
- * Adding a new field is additive: existing rules don't change, new
285
- * selector atoms can read it.
339
+ * `kind` is a discriminator string:
340
+ * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
341
+ * `'handle:bottom-right'` — corner resize handles.
342
+ * - `'handle:top'` / `'handle:right'` / `'handle:bottom'` / `'handle:left'` —
343
+ * edge resize handles.
344
+ * - `'rotate-handle'` — the rotation affordance.
345
+ * - `'anchor:N'` — a path anchor at index N.
286
346
  */
287
- interface RuleCtx {
288
- readonly focused: boolean;
289
- readonly selection: readonly NodeId[];
290
- readonly multiActive: boolean;
291
- readonly modifiers: ModifierState;
292
- readonly action: {
293
- readonly kind: string | null;
294
- readonly id: string | null;
347
+ interface AffordanceHit {
348
+ /** Discriminator string, e.g. `'handle:bottom-right'`. */
349
+ kind: string;
350
+ /** Id of whatever produced this hit — a kit affordance's `id`, or the
351
+ * registered layer's id. */
352
+ owner?: string;
353
+ /** `'exclusive'` means no binding may act on this point unless its target
354
+ * consults the affordance. `'shared'` (the default) competes on scope and
355
+ * specificity as bindings always have. */
356
+ strength?: 'exclusive' | 'shared';
357
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
358
+ claimedKinds?: readonly ClaimableGesture[];
359
+ /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
360
+ fixedPoint?: {
361
+ x: number;
362
+ y: number;
295
363
  };
296
- readonly hover: NodeId | null;
364
+ /** Which nodes this affordance belongs to. */
365
+ targetIds?: string[];
366
+ /** Set only when `kind` matches `'handle:*'`: which corner or edge stays
367
+ * fixed during a resize; `'free'` on the axis an edge handle leaves alone. */
368
+ anchor?: ResizeAnchor;
369
+ /** Cursor to show while the pointer hovers this affordance (no gesture in
370
+ * flight). Unset falls through to action-cursor prediction, then to the
371
+ * active tool's cursor. */
372
+ cursor?: CursorSpec;
297
373
  /**
298
- * How far the viewport is zoomed in, as one number — `1` at native scale.
374
+ * Free-form payload from whatever produced the hit, carried through to the
375
+ * matching action untouched. Kit affordances leave it unset; it exists for
376
+ * a registered layer's own chrome, where the hit-test already resolved
377
+ * *which* of its pieces was hit.
378
+ */
379
+ payload?: unknown;
380
+ }
381
+ /**
382
+ * One accumulated point on a drag trail: world-space position plus whatever
383
+ * stylus state the originating `PointerEvent` carried.
384
+ *
385
+ * The stylus fields are absent for mouse/touch on browsers that don't report
386
+ * them, and for synthetic events.
387
+ */
388
+ interface DragSample extends Point2 {
389
+ /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
390
+ pressure?: number;
391
+ /** Degrees, ±90. Zero for mouse/touch. */
392
+ tiltX?: number;
393
+ /** Degrees, ±90. Zero for mouse/touch. */
394
+ tiltY?: number;
395
+ }
396
+ /** Per-invocation runtime context the dispatcher hands to an Invoker.
397
+ * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
398
+ * populated only for matching gesture kinds. */
399
+ interface InvocationCtx {
400
+ world: Point2;
401
+ /**
402
+ * The pointer in client/screen pixels, for an action that has to place DOM
403
+ * at the pointer. Absent on every invocation whose event carries no client
404
+ * coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
405
+ * read it with a fallback rather than assuming a point.
406
+ *
407
+ * A view-mutating drag wants `drag.screenDelta`, not the difference of two
408
+ * of these: world deltas are self-referential mid-pan, and so is any delta
409
+ * taken across a view change.
410
+ */
411
+ screen?: Point2;
412
+ modifiers: ModifierState;
413
+ deps: ActionDeps;
414
+ drag?: {
415
+ start: Point2;
416
+ current: Point2;
417
+ delta: Point2;
418
+ /**
419
+ * Drag delta in client/screen coordinates (CSS pixels from the drag
420
+ * origin). Use this — never `delta` — for any action whose effect
421
+ * mutates the viewport itself (pan, view-zoom), because world-space
422
+ * deltas become self-referential as the view shifts mid-drag.
423
+ *
424
+ * Absent when the underlying pointer events carried no
425
+ * `clientX`/`clientY`.
426
+ */
427
+ screenDelta?: Point2;
428
+ affordance?: AffordanceHit;
429
+ /**
430
+ * Full pointermove history for the current drag, in world space, with
431
+ * per-sample stylus state when the browser reported it.
432
+ * Available only during `onMove` and `onEnd` calls (not on `start`).
433
+ *
434
+ * During `onMove` this is the dispatcher's live accumulator, and it grows
435
+ * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
436
+ * that is where an action that keeps the trail should take it.
437
+ */
438
+ points?: DragSample[];
439
+ };
440
+ wheel?: {
441
+ deltaX: number;
442
+ deltaY: number;
443
+ deltaZ: number;
444
+ };
445
+ /** One `pinch` sample: `scale` is a multiplier since the previous sample,
446
+ * `rotation` degrees since it. See `PinchEvent`. */
447
+ pinch?: {
448
+ scale: number;
449
+ rotation: number;
450
+ };
451
+ multiTouch?: {
452
+ /** Canvas-local CSS pixels — see `MultiTouchEvent.centroid`. Neither
453
+ * `world` nor `screen`: it is the space `zoomAt` anchors in, so passing
454
+ * it through `clientToWorld` removes the canvas origin twice. */
455
+ centroid: Point2;
456
+ /** Distance between the two primary pointers, in CSS pixels. */
457
+ spread: number;
458
+ /** Degrees the line between the two primary pointers has turned since
459
+ * the gesture began, clockwise positive. It keeps counting past a half
460
+ * turn. Zero where the events carry no `MultiTouchEvent.angle`. */
461
+ rotation: number;
462
+ /**
463
+ * Pinch-zoom geometry, populated while a multitouch handle is in flight.
464
+ * `startSpread` is the spread at the moment the gesture began.
465
+ */
466
+ pinch?: {
467
+ startSpread: number;
468
+ currentSpread: number;
469
+ /** Canvas-local, same as the enclosing `centroid`. */
470
+ centroid: Point2;
471
+ };
472
+ };
473
+ key?: {
474
+ key: string;
475
+ repeat: boolean;
476
+ };
477
+ /** What a carry over the surface brings: its payload, as `startCarry` was
478
+ * handed it. Present for every event of a `carry` binding's gesture. */
479
+ carry?: {
480
+ type: string;
481
+ value: unknown;
482
+ };
483
+ /**
484
+ * The view the input landed in, as the surface's view resolver assigned it:
485
+ * a view id, or `null` for the surface's own camera. Absent where no
486
+ * surface routed the input — a UI-driven trigger, a bare dispatcher.
487
+ */
488
+ viewId?: string | null;
489
+ /**
490
+ * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
491
+ * UI-driven ongoing actions (color picker, opacity slider). The gesture
492
+ * dispatcher does not populate this field; gesture-driven actions receive
493
+ * params via `BindingOpts.params` on `start` (the `opts` arg).
494
+ */
495
+ params?: Record<string, unknown>;
496
+ /**
497
+ * Which of the action's `modifierRoles` are held for this event, by role
498
+ * id. The dispatcher fills it on every event of a gesture whose action
499
+ * declares roles; absent otherwise. Read it through `roleReader`, which
500
+ * falls back to the roles' default keys where it is absent.
501
+ */
502
+ roles?: Readonly<Record<string, boolean>>;
503
+ }
504
+ /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
505
+ * `opts` field and passes to `OngoingInvoker.start`. */
506
+ interface BindingOpts {
507
+ /** Gesture behaviors the action's own invoker runs. Routing carries the
508
+ * array to the action and never reads an element, so the element type is
509
+ * open here; `@weasel-js/core` narrows it. */
510
+ behaviors?: unknown[];
511
+ /** Per-binding action parameters, which an immediate invoker reads via the
512
+ * second arg to `run`.
513
+ *
514
+ * May also be a thunk evaluated each time the value is needed, so a tool
515
+ * can close over refs that mutate during a gesture (e.g. polygon `sides`
516
+ * adjusted mid-drag via ArrowUp). An ongoing invoker that wants the latest
517
+ * values at commit re-calls it inside `onEnd` via
518
+ * `resolveParams(opts?.params)`. */
519
+ params?: Record<string, unknown> | (() => Record<string, unknown>);
520
+ /**
521
+ * The views this binding is live in, by id, where `null` is the surface's
522
+ * own camera. Omitted means every view.
523
+ *
524
+ * A binding that names the view an input landed in outranks every binding
525
+ * that does not, whatever their scope tiers: naming a view is the most
526
+ * specific thing a binding can say about where it applies. Without that, a
527
+ * drag bound inside a minimap view loses to the active tool's marquee.
528
+ */
529
+ views?: readonly (string | null)[];
530
+ /**
531
+ * Says this binding is declared ahead of another on the same input on
532
+ * purpose: its action declines through `enabled()` when the input is not its
533
+ * to take, and the next binding fires. The dispatcher does that for any
534
+ * binding; this tells the route-conflict check the order is the design.
535
+ */
536
+ yields?: boolean;
537
+ /**
538
+ * For a key binding: live while the pointer is over the surface, whichever
539
+ * canvas holds the keys, and only then. For a key that means "here" — a
540
+ * magnifier's peek — on a page with more than one surface that hears it.
541
+ */
542
+ over?: boolean;
543
+ /**
544
+ * Keys for some of the action's `modifierRoles` on this binding, by role
545
+ * id, over the action's defaults and any tuning's.
546
+ */
547
+ roles?: RoleRebinds;
548
+ }
549
+ /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
550
+ * needed). Returns `undefined` for absent params. */
551
+ declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
552
+ /** The deps an action receives, composed per call from what it `requires`.
553
+ * Each name resolves to its {@link DepSchema} entry, and is absent when no
554
+ * source registered it; a name nothing has declared passes through as
555
+ * `unknown`. */
556
+ interface ActionDeps extends Partial<DepSchema> {
557
+ [k: string]: unknown;
558
+ }
559
+
560
+ /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
561
+ * `onMove` on subsequent input events of the same gesture and calls
562
+ * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
563
+ * on pointercancel / blur / escape). */
564
+ interface OngoingHandle {
565
+ /**
566
+ * Optional logical action kind — a stable, human-readable tag the
567
+ * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
568
+ * `'resize'`, `'pan'`.
569
+ *
570
+ * When omitted, `getActiveAction().kind` reports `null` even though a
571
+ * handle is in flight.
572
+ */
573
+ kind?: string;
574
+ /**
575
+ * The cursor this gesture shows now, read after every move. Wins over the
576
+ * action's `activeCursor`, for a cursor that follows the gesture — the
577
+ * rotate glyph turning about its pivot. `undefined` defers to the action.
578
+ */
579
+ cursor?(): CursorSpec | undefined;
580
+ onMove?(ctx: InvocationCtx): void;
581
+ onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
582
+ /**
583
+ * Optional preview surface: the in-flight state the canvas's preview-ghost
584
+ * layer renders on top of the committed scene during the gesture.
585
+ *
586
+ * `previewIds()` enumerates the displaced node ids; `previewPose(id)`
587
+ * returns the interim pose for one of them (shape opaque — the canvas
588
+ * casts to its `TPose` parameter). `previewIds` returning `null`, or
589
+ * omitted, means "no preview this gesture". An id with no `previewPose`
590
+ * is shown at its effective pose: an action that publishes its poses to
591
+ * the scene's pose overrides, as move, resize and rotate do, leaves
592
+ * `previewPose` out rather than answering the same pose twice.
593
+ */
594
+ previewIds?(): Iterable<string> | null;
595
+ previewPose?(id: string): unknown | null;
596
+ /**
597
+ * Subset of `previewIds()` the ghost layer paints at full opacity. The
598
+ * ghost alpha says "this is in flight under the pointer"; a node the
599
+ * gesture merely displaces — a layout sibling reflowing into its
600
+ * destination slot — is not, and reads better settled. Honored at
601
+ * subtree-root granularity.
602
+ */
603
+ previewOpaqueIds?(): Iterable<string> | null;
604
+ /**
605
+ * When `false`, the preview-ghost layer paints the ghost AND the source
606
+ * node stays visible at its committed pose, as a clone wants. Defaults to
607
+ * `true`: the ghost replaces the source during the gesture.
608
+ */
609
+ previewHidesSource?: boolean;
610
+ /**
611
+ * Optional per-id preview *data*, for a gesture that mutates `node.data`
612
+ * (e.g. anchor-edit on nodes that store the polygon on `data.path`) rather
613
+ * than, or in addition to, the pose. Falls back to the committed
614
+ * `node.data` when null/absent.
615
+ */
616
+ previewData?(id: string): unknown | null;
617
+ /**
618
+ * Optional chrome surface: a non-ghost visual (marquee rectangle, lasso
619
+ * polyline) painted while the gesture is in flight — one, or a list for a
620
+ * gesture showing several runs at once. Returning `null`, or omitting the
621
+ * method, means "no overlay this gesture". Read it through
622
+ * {@link overlaysOf}, which answers a list either way.
623
+ */
624
+ overlay?(): OngoingOverlay | readonly OngoingOverlay[] | null;
625
+ /**
626
+ * A wheel turned while this gesture is in flight — scrubbing a value the
627
+ * drag is using, say. Return `true` to claim the wheel, so no wheel binding
628
+ * (the viewport's zoom or pan) sees it; anything else lets it through.
629
+ * Omit it and the wheel goes to the bindings as usual.
630
+ */
631
+ onWheel?(ctx: InvocationCtx): boolean | void;
632
+ }
633
+ /** Fire-once invocation. Runs to completion synchronously (or fires off an
634
+ * async side-effect; the registry doesn't wait). */
635
+ interface ImmediateInvoker {
636
+ timing: 'immediate';
637
+ /** `params` carries the matched binding's opts.params. Invoked from the
638
+ * command palette, or anywhere else with no per-binding context, `params`
639
+ * is undefined; descriptors should default to a sensible variant. */
640
+ run(deps: ActionDeps, params?: Record<string, unknown>): void;
641
+ }
642
+ /** Phase-machine invocation. `start` opens the phase and returns the handle
643
+ * the dispatcher pumps. */
644
+ interface OngoingInvoker {
645
+ timing: 'ongoing';
646
+ start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
647
+ }
648
+ /** Pluggable invocation strategy for an Action. */
649
+ type Invoker = ImmediateInvoker | OngoingInvoker;
650
+
651
+ /**
652
+ * GestureBinding — connects a GestureSpec to an Action id (with per-binding
653
+ * options). Tools own arrays of these on their `bindings` field; ambient
654
+ * gesture-bindings are registered globally.
655
+ */
656
+
657
+ /** An interaction: a gesture spec composed with the id of the action it
658
+ * invokes. Tools declare arrays of these; the dispatcher matches an incoming
659
+ * input event against them and runs the winner's action. */
660
+ interface GestureBinding {
661
+ spec: GestureSpec;
662
+ actionId: string;
663
+ opts?: BindingOpts;
664
+ }
665
+ /**
666
+ * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
667
+ * (no per-binding opts) or an object form that pairs a spec with
668
+ * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
669
+ * Use the object form when two bindings for the same action differ only in
670
+ * a runtime parameter — the dispatcher extracts `opts.params` and passes
671
+ * them to `ImmediateInvoker.run` as its second argument.
672
+ */
673
+ type BoundGesture = GestureSpec | {
674
+ spec: GestureSpec;
675
+ opts: BindingOpts;
676
+ };
677
+ /**
678
+ * The whole of an action `actionBindings` reads. Routing needs this much and
679
+ * none of `Action`'s palette fields, so it is the narrowest type the
680
+ * dispatcher's binding walk can be written against.
681
+ */
682
+ interface BindingSource {
683
+ id: string;
684
+ /** The gesture-spec form of the binding, read by the gesture dispatcher.
685
+ * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
686
+ * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
687
+ * `{ spec, opts }` — use the object form for parametric actions where two
688
+ * bindings for the same action differ only by `opts.params` (e.g. `flip`
689
+ * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
690
+ * passes them to `ImmediateInvoker.run` as its second argument. */
691
+ defaultBinding?: GestureSpec | BoundGesture[];
692
+ }
693
+ /**
694
+ * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
695
+ * `GestureSpec` has `kind` at top level; the object form has `spec`.
696
+ */
697
+ declare function actionBindings(action: BindingSource): GestureBinding[];
698
+
699
+ /**
700
+ * Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
701
+ * the actions-layer binding-scope / matchBest logic on top.
702
+ */
703
+
704
+ /** Where a binding came from, which is also its priority: a held hotkey beats
705
+ * the active tool, which beats bindings that are always in scope. */
706
+ type BindingScope = 'ambient' | 'active' | 'hotkey';
707
+ /** A binding paired with where it came from, ready to be matched against an
708
+ * event. */
709
+ interface ScopedBinding {
710
+ binding: GestureBinding;
711
+ scope: BindingScope;
712
+ /** Id of the tool or contribution that declared this binding —
713
+ * `'&'`-channel phase atoms resolve to it. `null` for a registered
714
+ * action's own binding. */
715
+ ownerId: string | null;
716
+ }
717
+ /** The binding that won a match. */
718
+ interface MatchResult {
719
+ binding: GestureBinding;
720
+ scope: BindingScope;
721
+ /** Id of the entry that declared the binding — propagated from
722
+ * `ScopedBinding` so the dispatcher can record it as the handle owner. */
723
+ ownerId: string | null;
724
+ }
725
+ /**
726
+ * True when a spec's target actually consults the affordance hit rather than
727
+ * only the body classification. `kindOf` predicates are handed the hit;
728
+ * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
729
+ * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
730
+ * `bodyTarget` / `bodyKind` and never see it.
731
+ *
732
+ * A predicate declares which it is with `readsAffordance`; one that declares
733
+ * nothing, which only untyped code can write, counts as not reading the hit.
734
+ */
735
+ declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
736
+ /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
737
+ declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
738
+ /**
739
+ * Find every binding that matches the event, sorted by precedence
740
+ * (best → worst). Ordering rules:
741
+ *
742
+ * - Scopes by priority: hotkey > active > ambient.
743
+ * - Within a scope, more-specific bindings beat less-specific (see
744
+ * `specificity()`).
745
+ * - Among same-specificity bindings, first-declared wins.
746
+ */
747
+ declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
748
+ /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
749
+ interface MatchOutcome {
750
+ matches: MatchResult[];
751
+ /** Bindings whose spec matched the event but which the claim barred, in
752
+ * the same order `matches` uses. Filled only when `collectBarred` is set. */
753
+ barred: MatchResult[];
754
+ /** Owner of the exclusive claim, when one applied. */
755
+ claimOwner?: string;
756
+ }
757
+ /**
758
+ * {@link matchSorted} that can also report the bindings an exclusive claim
759
+ * kept out. Collecting them costs a spec match per barred binding, so it is
760
+ * opt-in — the dispatcher asks only when it is recording.
761
+ */
762
+ declare function matchSortedWithBarred(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, opts?: {
763
+ collectBarred?: boolean;
764
+ warn?: (message: string) => void;
765
+ }): MatchOutcome;
766
+
767
+ /**
768
+ * How one input was resolved: every binding that matched it, what each filter
769
+ * dropped, the order the rest were ranked in and why, and what the walk down
770
+ * that order did. The dispatcher fills one per input in dev builds, and
771
+ * `Dispatcher.explain` fills one on request without invoking anything.
772
+ *
773
+ * Plain data — no functions, no live objects — so it can be kept in a trace
774
+ * buffer, serialized, or built by hand as a fixture.
775
+ */
776
+ interface DispatchRecord {
777
+ kind: 'dispatch';
778
+ ts: number;
779
+ input: DispatchRecordInput;
780
+ /** Every binding whose spec matched the input, in matcher order, including
781
+ * those a filter then dropped. */
782
+ matched: readonly RecordCandidate[];
783
+ dropped: readonly DroppedCandidate[];
784
+ /** The candidates that survived both filters, best first. */
785
+ ranked: readonly RankedCandidate[];
786
+ /** True when nothing was invoked: the record answers "what would happen". */
787
+ predicted: boolean;
788
+ /** Action id of the candidate that fired (or would), else `null`. */
789
+ fired: string | null;
790
+ outcome: 'handled' | 'unhandled';
791
+ }
792
+ /** The input a {@link DispatchRecord} describes, reduced to plain data. */
793
+ interface DispatchRecordInput {
794
+ /** `InputEvent.kind`: `'key'`, `'pointerdown'`, `'wheel'`, … */
795
+ eventKind: string;
796
+ /** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`). */
797
+ key?: string;
798
+ modifiers: {
799
+ alt: boolean;
800
+ ctrl: boolean;
801
+ meta: boolean;
802
+ shift: boolean;
803
+ };
804
+ /** The view the input was routed to; `null` for the surface's own camera. */
805
+ viewId: string | null;
806
+ /** World point, for events that carry one. */
807
+ world?: {
808
+ x: number;
809
+ y: number;
810
+ };
811
+ /** Active mode id, when the host supplies a rule context. */
812
+ mode?: string;
813
+ }
814
+ /** One matched binding. */
815
+ interface RecordCandidate {
816
+ actionId: string;
817
+ /** The binding's spec in route grammar — one per arg alternative. */
818
+ routes: readonly string[];
819
+ scope: BindingScope;
820
+ /** The tool or contribution that declared the binding; `null` for an action's own binding. */
821
+ ownerId: string | null;
822
+ /** Whether the binding's `views` names the routed view. */
823
+ namesView: boolean;
824
+ /** `specificity(spec)`: target, modifiers, phase, exact. */
825
+ specificity: readonly [number, number, number, number];
826
+ /** The action's `eligible` rule as text, when it declares one. */
827
+ eligible?: string;
828
+ }
829
+ /** A matched candidate a filter removed before ranking, and which filter. */
830
+ type DroppedCandidate =
831
+ /** An exclusive claim on the input barred bindings that do not read the
832
+ * claim's affordance. */
833
+ {
834
+ candidate: RecordCandidate;
835
+ filter: 'claim';
836
+ owner?: string;
837
+ }
838
+ /** The action's `eligible` rule does not hold. */
839
+ | {
840
+ candidate: RecordCandidate;
841
+ filter: 'ineligible';
842
+ rule: string;
843
+ };
844
+ /** One field of the `specificity` tuple, in the order it is compared. */
845
+ type SpecificityPart = 'target' | 'mods' | 'phase' | 'exact';
846
+ /** The ranking step that put a candidate below the one before it. */
847
+ type PlacedBy = {
848
+ step: 'first';
849
+ }
850
+ /** The one above names the routed view; this one does not. */
851
+ | {
852
+ step: 'view';
853
+ }
854
+ /** The one above sits in a higher scope tier. */
855
+ | {
856
+ step: 'tier';
857
+ }
858
+ /** Same tier; the one above is more specific in `part`. */
859
+ | {
860
+ step: 'specificity';
861
+ part: SpecificityPart;
862
+ }
863
+ /** Tied; the one above is gated by an `eligible` rule that holds. */
864
+ | {
865
+ step: 'context';
866
+ }
867
+ /** Tied on everything; registration order. */
868
+ | {
869
+ step: 'order';
870
+ };
871
+ /** What the walk did with one ranked candidate. */
872
+ type WalkStep = {
873
+ kind: 'fired';
874
+ }
875
+ /** Predicted records only: this candidate would fire. Nothing was started,
876
+ * so an ongoing action here may still bail at `start()` for real. */
877
+ | {
878
+ kind: 'would-fire';
879
+ }
880
+ /** `enabled()` returned this reason. */
881
+ | {
882
+ kind: 'declined';
883
+ reason: string;
884
+ }
885
+ /** An ongoing action's `start()` returned an empty handle. */
886
+ | {
887
+ kind: 'empty-handle';
888
+ }
889
+ /** An ongoing action bound to a press-time `pointerDown` spec; refused. */
890
+ | {
891
+ kind: 'misbound';
892
+ }
893
+ /** Its action was already tried higher up; each runs at most once. */
894
+ | {
895
+ kind: 'duplicate';
896
+ } | {
897
+ kind: 'no-such-action';
898
+ }
899
+ /** Below the winner; never asked. */
900
+ | {
901
+ kind: 'not-asked';
902
+ }
903
+ /** Below the winner, asked anyway (`evaluateShadowed`), and it passed. */
904
+ | {
905
+ kind: 'outranked';
906
+ };
907
+ /** A candidate that survived the filters: why it sits where it does, and
908
+ * what the walk did with it. */
909
+ interface RankedCandidate {
910
+ candidate: RecordCandidate;
911
+ placedBy: PlacedBy;
912
+ walk: WalkStep;
913
+ }
914
+
915
+ /** One entry per "mode change" — a kit state transition that isn't a
916
+ * dispatched input event but explains why an input was (or wasn't) handled
917
+ * later. Pushed via {@link DispatchTrace.recordModeSwitch}. */
918
+ interface ModeSwitchLogEntry {
919
+ kind: 'mode';
920
+ ts: number;
921
+ /** Logical mode name, e.g. `'editAnchors.editingId'`. */
922
+ mode: string;
923
+ from: string | null;
924
+ to: string | null;
925
+ /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
926
+ * `'selection-dropped target'`, etc. */
927
+ detail?: string;
928
+ }
929
+ /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
930
+ type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
931
+ /** A rolling buffer that records in a dev build, and in any build while something watches it. */
932
+ interface TraceRing<T> {
933
+ /** Whether a push now keeps its entry. A caller building an entry checks first. */
934
+ recording(): boolean;
935
+ push(entry: T): void;
936
+ /** The entries pushed after `cursor`, oldest first, and the cursor to pass
937
+ * next time. Start from 0. Entries the buffer has dropped are skipped. */
938
+ since(cursor: number): {
939
+ entries: T[];
940
+ cursor: number;
941
+ };
942
+ /** Turn recording on until the returned release is called. */
943
+ watch(): () => void;
944
+ /** Hear each kept push. Returns the unsubscribe. */
945
+ subscribe(listener: () => void): () => void;
946
+ }
947
+ /** A {@link TraceRing} keeping the last `limit` entries. `dev` defaults to the build's. */
948
+ declare function createTraceRing<T>(limit?: number, dev?: boolean): TraceRing<T>;
949
+ /** One canvas's dispatch trace. `useGestureDispatcher` makes one, puts it on
950
+ * every `DispatcherContext` it builds, and publishes it as the `dispatchTrace` dep. */
951
+ interface DispatchTrace extends TraceRing<TraceLogEntry> {
952
+ recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
953
+ /** Offer the record for what a press at the pointer would do, built only
954
+ * when {@link DispatchTrace.live} reads it. `null` withdraws it. */
955
+ setLive(explain: (() => DispatchRecord) | null): void;
956
+ live(): DispatchRecord | null;
957
+ }
958
+ /** A {@link DispatchTrace} keeping the last `limit` entries. `dev` defaults to the build's. */
959
+ declare function createDispatchTrace(limit?: number, dev?: boolean): DispatchTrace;
960
+
961
+ /**
962
+ * Live state read by rule evaluation. Built once per frame on the consuming
963
+ * surface — chrome-caps, the affordance pipeline, the dispatcher's
964
+ * eligibility filter — and discarded.
965
+ *
966
+ * Adding a new field is additive: existing rules don't change, new
967
+ * selector atoms can read it.
968
+ */
969
+ interface RuleCtx {
970
+ readonly focused: boolean;
971
+ readonly selection: readonly NodeId[];
972
+ readonly multiActive: boolean;
973
+ readonly modifiers: ModifierState;
974
+ readonly action: {
975
+ readonly kind: string | null;
976
+ readonly id: string | null;
977
+ };
978
+ readonly hover: NodeId | null;
979
+ /**
980
+ * How far the viewport is zoomed in, as one number — `1` at native scale.
299
981
  *
300
982
  * A scalar rather than the `View` this used to carry, because `zoomAtLeast`
301
983
  * is the only selector that ever read one and a number is all it needs. A
@@ -354,612 +1036,219 @@ interface BuildRuleCtxArgs {
354
1036
  zoom?: number;
355
1037
  mode: string;
356
1038
  allowedCapabilities: ReadonlySet<CapabilityTag>;
357
- /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
358
- selectionResizable?: boolean;
359
- /** Optional — omitted means "no path is being anchor-edited". */
360
- editingAnchors?: boolean;
361
- /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
362
- device?: DeviceProfile;
363
- }
364
- /**
365
- * Capability set to assume when no mode registry is wired.
366
- *
367
- * "No modes configured" means *everything the default mode permits*, not
368
- * *nothing permitted*. The empty set is the wrong default: it makes every
369
- * `capability:` rule evaluate false, so a kit default written the
370
- * recommended way (capability rules outlive new modes; mode rules have to
371
- * be edited) would silently hide its chrome for every consumer that hasn't
372
- * opted into modality. `NORMAL.allows` plus the implicit tags is what a
373
- * mode-less canvas actually behaves like.
374
- */
375
- declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
376
- /** Gather the current canvas and mode state into the context that action
377
- * eligibility and chrome-visibility rules are evaluated against. */
378
- declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
379
-
380
- /**
381
- * A selector is a conjunction of key/value tests. Multiple keys at the same
382
- * level AND together. Each key maps to a selector primitive in the evaluator.
383
- */
384
- interface Selector {
385
- selection?: {
386
- is?: number;
387
- atLeast?: number;
388
- empty?: boolean;
389
- };
390
- mode?: string | {
391
- not: string;
392
- } | {
393
- in: readonly string[];
394
- };
395
- capability?: CapabilityTag | readonly CapabilityTag[] | {
396
- in: readonly CapabilityTag[];
397
- } | {
398
- not: CapabilityTag;
399
- };
400
- gesturing?: boolean;
401
- actionIs?: string;
402
- modifierHeld?: keyof ModifierState;
403
- focused?: boolean;
404
- hovering?: boolean;
405
- hoveringSelected?: boolean;
406
- zoomAtLeast?: number;
407
- /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
408
- * mode. Absent flag is treated as `false`. */
409
- editingAnchors?: boolean;
410
- /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
411
- * (resizable), so `{ resizable: true }` passes for legacy ctx builders
412
- * that don't compute it. */
413
- resizable?: boolean;
414
- /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
415
- * (touch, most styluses). Absent device is treated as `false`. */
416
- coarsePointer?: boolean;
417
- /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
418
- * device is treated as `true`. */
419
- canHover?: boolean;
420
- }
421
- /**
422
- * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
423
- * over `Selector` leaves. `when` is the escape hatch — its closure is
424
- * opaque to introspection and should be avoided when a declarative form
425
- * exists. Empty `all` is true; empty `any` is false.
426
- */
427
- type Rule = Selector | {
428
- all: readonly Rule[];
429
- } | {
430
- any: readonly Rule[];
431
- } | {
432
- not: Rule;
433
- } | {
434
- when: (ctx: RuleCtx) => boolean;
435
- };
436
- /** Constant rules. Kept here so they have a single source. */
437
- declare const ALWAYS: Rule;
438
- /** A rule that never passes. `any` of nothing is false. */
439
- declare const NEVER: Rule;
440
- /**
441
- * Render a {@link Rule} as a short human-readable string, for inspectors and
442
- * diagnostics that need to say WHICH rule decided something.
443
- *
444
- * ```
445
- * { capability: 'edits-page' } → capability:edits-page
446
- * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
447
- * { not: { selection: { empty: true } } } → not(selection:empty=true)
448
- * { when: function hasPath() { … } } → when(hasPath)
449
- * ```
450
- *
451
- * Never throws and always terminates.
452
- */
453
- declare function describeRule(rule: Rule): string;
454
- /** Evaluate a rule against a context. Pure, and cheap enough to run per
455
- * frame for every piece of chrome. */
456
- declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
457
- /**
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.
461
- *
462
- * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
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(...)`.
465
- */
466
- interface Condition {
467
- (ctx: RuleCtx): boolean;
468
- readonly rule: Rule;
469
- /** `this && other` */
470
- and(other: Condition | Rule): Condition;
471
- /** `this || other` */
472
- or(other: Condition | Rule): Condition;
473
- /** `this && !other` */
474
- andNot(other: Condition | Rule): Condition;
475
- /** `this || !other` */
476
- orNot(other: Condition | Rule): Condition;
477
- }
478
-
479
- /**
480
- * Whether two rules can never hold at once — no `RuleCtx` passes both.
481
- *
482
- * Conservative: `true` is a proof, `false` means "may overlap". It reasons
483
- * about the selector values themselves (two different `mode`s, an empty
484
- * selection against a non-empty one, `focused: true` against `false`) and
485
- * through `all` / `any` / `not`; a `when` closure is opaque, so it overlaps
486
- * everything. The route-conflict check uses it to stay quiet about bindings
487
- * that share a tuple but are never eligible together.
488
- */
489
- declare function rulesExclusive(a: Rule, b: Rule): boolean;
490
- /**
491
- * Whether `rule` can hold while `mode` is active. Only its `mode` and
492
- * `capability` tests are settled by the mode; every other test could go either
493
- * way, so `false` is a proof and `true` means "may hold".
494
- */
495
- declare function ruleCanHoldIn(rule: Rule, mode: ActiveMode): boolean;
496
-
497
- /** The detected half of a profile — everything except the derived scale. */
498
- type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
499
- /**
500
- * Handle/hit multiplier applied on a coarse pointer.
501
- *
502
- * 8px handle → 14px; 24px rotation distance → 42px. Counting the
503
- * surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
504
- * minimum-touch-target band. One constant so it is tunable in one place.
505
- */
506
- declare const COARSE_TARGET_SCALE = 1.75;
507
- /**
508
- * Fold consumer overrides over detected facts and derive `targetScale`.
509
- *
510
- * `targetScale` is re-derived AFTER the merge, so an override of
511
- * `coarsePointer` alone scales the chrome as expected. An explicit
512
- * `targetScale` override wins over the derivation.
513
- */
514
- declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
515
- /**
516
- * Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
517
- * absent-means value for `RuleCtx.device`.
518
- *
519
- * Derived from `resolveDeviceProfile` rather than hand-written, so it can
520
- * never drift from what the function would compute for the same facts. A
521
- * mouse-like device is the safe default: it is what the kit has always
522
- * assumed, so an absent profile changes nothing for existing consumers.
523
- */
524
- declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
525
-
526
- /**
527
- * Invoker — pluggable invocation strategy for an Action.
528
- *
529
- * An Action's `invoker` field describes how it's run. `immediate` invokers
530
- * fire once (delete, align, undo). `ongoing` invokers open a phase machine
531
- * driven by the dispatcher (move, resize, pinch-zoom).
532
- */
533
-
534
- /** A 2D point in either world or screen coordinates. */
535
- interface Point2 {
536
- x: number;
537
- y: number;
1039
+ /** Optional — omitted means "resizable" (handles show). See {@link RuleCtx}. */
1040
+ selectionResizable?: boolean;
1041
+ /** Optional — omitted means "no path is being anchor-edited". */
1042
+ editingAnchors?: boolean;
1043
+ /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
1044
+ device?: DeviceProfile;
538
1045
  }
539
1046
  /**
540
- * Which UI affordance was hit at pointerdown. Populated by the dispatcher when
541
- * the `affordanceAt` thunk is provided to `useGestureDispatcher`.
1047
+ * Capability set to assume when no mode registry is wired.
542
1048
  *
543
- * `kind` is a discriminator string:
544
- * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
545
- * `'handle:bottom-right'` — corner resize handles.
546
- * - `'rotate-handle'` — the rotation affordance.
547
- * - `'anchor:N'` — a path anchor at index N.
1049
+ * "No modes configured" means *everything the default mode permits*, not
1050
+ * *nothing permitted*. The empty set is the wrong default: it makes every
1051
+ * `capability:` rule evaluate false, so a kit default written the
1052
+ * recommended way (capability rules outlive new modes; mode rules have to
1053
+ * be edited) would silently hide its chrome for every consumer that hasn't
1054
+ * opted into modality. `NORMAL.allows` plus the implicit tags is what a
1055
+ * mode-less canvas actually behaves like.
548
1056
  */
549
- interface AffordanceHit {
550
- /** Discriminator string, e.g. `'handle:bottom-right'`. */
551
- kind: string;
552
- /** Id of whatever produced this hit — a kit affordance's `id`, or the
553
- * registered layer's id. */
554
- owner?: string;
555
- /** `'exclusive'` means no binding may act on this point unless its target
556
- * consults the affordance. `'shared'` (the default) competes on scope and
557
- * specificity as bindings always have. */
558
- strength?: 'exclusive' | 'shared';
559
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
560
- claimedKinds?: readonly ClaimableGesture[];
561
- /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
562
- fixedPoint?: {
563
- x: number;
564
- y: number;
565
- };
566
- /** Which nodes this affordance belongs to. */
567
- targetIds?: string[];
568
- /** Set only when `kind` matches `'handle:*'`: which corner stays fixed
569
- * during a resize. */
570
- anchor?: ResizeAnchor;
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. */
574
- cursor?: CursorSpec;
575
- /**
576
- * Free-form payload from whatever produced the hit, carried through to the
577
- * matching action untouched. Kit affordances leave it unset; it exists for
578
- * a registered layer's own chrome, where the hit-test already resolved
579
- * *which* of its pieces was hit.
580
- */
581
- payload?: unknown;
582
- }
1057
+ declare const DEFAULT_ALLOWED_CAPABILITIES: ReadonlySet<CapabilityTag>;
1058
+ /** Gather the current canvas and mode state into the context that action
1059
+ * eligibility and chrome-visibility rules are evaluated against. */
1060
+ declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
1061
+
583
1062
  /**
584
- * One accumulated point on a drag trail: world-space position plus whatever
585
- * stylus state the originating `PointerEvent` carried.
586
- *
587
- * The stylus fields are absent for mouse/touch on browsers that don't report
588
- * them, and for synthetic events.
1063
+ * A selector is a conjunction of key/value tests. Multiple keys at the same
1064
+ * level AND together. Each key maps to a selector primitive in the evaluator.
589
1065
  */
590
- interface DragSample extends Point2 {
591
- /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
592
- pressure?: number;
593
- /** Degrees, ±90. Zero for mouse/touch. */
594
- tiltX?: number;
595
- /** Degrees, ±90. Zero for mouse/touch. */
596
- tiltY?: number;
597
- }
598
- /** Per-invocation runtime context the dispatcher hands to an Invoker.
599
- * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
600
- * populated only for matching gesture kinds. */
601
- interface InvocationCtx {
602
- world: Point2;
603
- /**
604
- * The pointer in client/screen pixels, for an action that has to place DOM
605
- * at the pointer. Absent on every invocation whose event carries no client
606
- * coordinates — a keystroke, a UI-driven trigger, a synthetic probe — so
607
- * read it with a fallback rather than assuming a point.
608
- *
609
- * A view-mutating drag wants `drag.screenDelta`, not the difference of two
610
- * of these: world deltas are self-referential mid-pan, and so is any delta
611
- * taken across a view change.
612
- */
613
- screen?: Point2;
614
- modifiers: ModifierState;
615
- deps: ActionDeps;
616
- drag?: {
617
- start: Point2;
618
- current: Point2;
619
- delta: Point2;
620
- /**
621
- * Drag delta in client/screen coordinates (CSS pixels from the drag
622
- * origin). Use this — never `delta` — for any action whose effect
623
- * mutates the viewport itself (pan, view-zoom), because world-space
624
- * deltas become self-referential as the view shifts mid-drag.
625
- *
626
- * Absent when the underlying pointer events carried no
627
- * `clientX`/`clientY`.
628
- */
629
- screenDelta?: Point2;
630
- affordance?: AffordanceHit;
631
- /**
632
- * Full pointermove history for the current drag, in world space, with
633
- * per-sample stylus state when the browser reported it.
634
- * Available only during `onMove` and `onEnd` calls (not on `start`).
635
- *
636
- * During `onMove` this is the dispatcher's live accumulator, and it grows
637
- * under you — read it, don't retain it. On `onEnd` it is a snapshot, so
638
- * that is where an action that keeps the trail should take it.
639
- */
640
- points?: DragSample[];
641
- };
642
- wheel?: {
643
- deltaX: number;
644
- deltaY: number;
645
- deltaZ: number;
646
- };
647
- /** One `pinch` sample: `scale` is a multiplier since the previous sample,
648
- * `rotation` degrees since it. See `PinchEvent`. */
649
- pinch?: {
650
- scale: number;
651
- rotation: number;
1066
+ interface Selector {
1067
+ selection?: {
1068
+ is?: number;
1069
+ atLeast?: number;
1070
+ empty?: boolean;
652
1071
  };
653
- multiTouch?: {
654
- /** Canvas-local CSS pixels — see `MultiTouchEvent.centroid`. Neither
655
- * `world` nor `screen`: it is the space `zoomAt` anchors in, so passing
656
- * it through `clientToWorld` removes the canvas origin twice. */
657
- centroid: Point2;
658
- /** Distance between the two primary pointers, in CSS pixels. */
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`. */
663
- rotation: number;
664
- /**
665
- * Pinch-zoom geometry, populated while a multitouch handle is in flight.
666
- * `startSpread` is the spread at the moment the gesture began.
667
- */
668
- pinch?: {
669
- startSpread: number;
670
- currentSpread: number;
671
- /** Canvas-local, same as the enclosing `centroid`. */
672
- centroid: Point2;
673
- };
1072
+ mode?: string | {
1073
+ not: string;
1074
+ } | {
1075
+ in: readonly string[];
674
1076
  };
675
- key?: {
676
- key: string;
677
- repeat: boolean;
1077
+ capability?: CapabilityTag | readonly CapabilityTag[] | {
1078
+ in: readonly CapabilityTag[];
1079
+ } | {
1080
+ not: CapabilityTag;
678
1081
  };
679
- /**
680
- * The view the input landed in, as the surface's view resolver assigned it:
681
- * a view id, or `null` for the surface's own camera. Absent where no
682
- * surface routed the input — a UI-driven trigger, a bare dispatcher.
683
- */
684
- viewId?: string | null;
685
- /**
686
- * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
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).
690
- */
691
- params?: Record<string, unknown>;
692
- }
693
- /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
694
- * `opts` field and passes to `OngoingInvoker.start`. */
695
- interface BindingOpts {
696
- /** Gesture behaviors the action's own invoker runs. Routing carries the
697
- * array to the action and never reads an element, so the element type is
698
- * open here; `@weasel-js/core` narrows it. */
699
- behaviors?: unknown[];
700
- /** Per-binding action parameters, which an immediate invoker reads via the
701
- * second arg to `run`.
702
- *
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)`. */
708
- params?: Record<string, unknown> | (() => Record<string, unknown>);
709
- /**
710
- * The views this binding is live in, by id, where `null` is the surface's
711
- * own camera. Omitted means every view.
712
- *
713
- * A binding that names the view an input landed in outranks every binding
714
- * that does not, whatever their scope tiers: naming a view is the most
715
- * specific thing a binding can say about where it applies. Without that, a
716
- * drag bound inside a minimap view loses to the active tool's marquee.
717
- */
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;
726
- }
727
- /** Resolve `BindingOpts.params` to a concrete record (calling the thunk if
728
- * needed). Returns `undefined` for absent params. */
729
- declare function resolveParams(params: BindingOpts['params']): Record<string, unknown> | undefined;
730
- /** The deps an action receives, composed per call from what it `requires`.
731
- * Each name resolves to its {@link DepSchema} entry, and is absent when no
732
- * source registered it; a name nothing has declared passes through as
733
- * `unknown`. */
734
- interface ActionDeps extends Partial<DepSchema> {
735
- [k: string]: unknown;
1082
+ gesturing?: boolean;
1083
+ actionIs?: string;
1084
+ modifierHeld?: keyof ModifierState;
1085
+ focused?: boolean;
1086
+ hovering?: boolean;
1087
+ hoveringSelected?: boolean;
1088
+ zoomAtLeast?: number;
1089
+ /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
1090
+ * mode. Absent flag is treated as `false`. */
1091
+ editingAnchors?: boolean;
1092
+ /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
1093
+ * (resizable), so `{ resizable: true }` passes for legacy ctx builders
1094
+ * that don't compute it. */
1095
+ resizable?: boolean;
1096
+ /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
1097
+ * (touch, most styluses). Absent device is treated as `false`. */
1098
+ coarsePointer?: boolean;
1099
+ /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
1100
+ * device is treated as `true`. */
1101
+ canHover?: boolean;
736
1102
  }
737
1103
  /**
738
- * What a `polyline` overlay is, in a word — the narrowest thing an action can
739
- * say about how its run should look without naming a paint.
1104
+ * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
1105
+ * over `Selector` leaves. `when` is the escape hatch — its closure is
1106
+ * opaque to introspection and should be avoided when a declarative form
1107
+ * exists. Empty `all` is true; empty `any` is false.
1108
+ */
1109
+ type Rule = Selector | {
1110
+ all: readonly Rule[];
1111
+ } | {
1112
+ any: readonly Rule[];
1113
+ } | {
1114
+ not: Rule;
1115
+ } | {
1116
+ when: (ctx: RuleCtx) => boolean;
1117
+ };
1118
+ /** Constant rules. Kept here so they have a single source. */
1119
+ declare const ALWAYS: Rule;
1120
+ /** A rule that never passes. `any` of nothing is false. */
1121
+ declare const NEVER: Rule;
1122
+ /**
1123
+ * Render a {@link Rule} as a short human-readable string, for inspectors and
1124
+ * diagnostics that need to say WHICH rule decided something.
740
1125
  *
741
- * The named roles are the kit's; a painter maps each to a style and falls back
742
- * to plain chrome for one it does not know, so a package outside core can coin
743
- * its own and still be drawn.
1126
+ * ```
1127
+ * { capability: 'edits-page' } → capability:edits-page
1128
+ * { all: [{ mode: 'normal' }, { focused: true }] } → all(mode:normal, focused:true)
1129
+ * { not: { selection: { empty: true } } } → not(selection:empty=true)
1130
+ * { when: function hasPath() { … } } → when(hasPath)
1131
+ * ```
1132
+ *
1133
+ * Never throws and always terminates.
744
1134
  */
745
- type OverlayRole =
746
- /** Generic in-flight chrome — the marquee/lasso paint. */
747
- 'chrome'
748
- /** A cut the gesture would make. */
749
- | 'cut'
750
- /** An edge the gesture would author between two points. */
751
- | 'connector' | (string & {});
1135
+ declare function describeRule(rule: Rule): string;
1136
+ /** Evaluate a rule against a context. Pure, and cheap enough to run per
1137
+ * frame for every piece of chrome. */
1138
+ declare function evaluate(rule: Rule, ctx: RuleCtx): boolean;
752
1139
  /**
753
- * What an in-flight gesture proposes to paint *beside* the scene, returned by
754
- * `OngoingHandle.overlay()` — a marquee rect, a lasso trail, the outline of a
755
- * shape being dragged out, the line a slice would cut along.
1140
+ * Composable eligibility predicate with a fluent surface. `cond(ctx)`
1141
+ * evaluates its `Rule` tree, carried at `.rule`; the fluent methods return new
1142
+ * Conditions wrapping new trees.
756
1143
  *
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.
1144
+ * **Chain semantics: strict left-to-right, no precedence.** `a.and(b).or(c)` is
1145
+ * `(a && b) || c`; `a.or(b).and(c)` is `(a || b) && c`. For grouped
1146
+ * disjunction, name the subexpression or use the top-level `or(...)`.
761
1147
  */
762
- type OngoingOverlay = {
763
- kind: 'marquee';
764
- start: {
765
- x: number;
766
- y: number;
767
- };
768
- current: {
769
- x: number;
770
- y: number;
771
- };
772
- shiftHeld: boolean;
773
- } | {
774
- kind: 'lasso';
775
- vertices: ReadonlyArray<{
776
- x: number;
777
- y: number;
778
- }>;
779
- current: {
780
- x: number;
781
- y: number;
782
- };
783
- shiftHeld: boolean;
784
- } | {
785
- /**
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.
789
- */
790
- kind: 'polyline';
791
- /** At least two points. Anything shorter is dropped in resolution. */
792
- points: ReadonlyArray<{
793
- x: number;
794
- y: number;
795
- }>;
796
- /** What the run means. Omitted reads as {@link OverlayRole} `'chrome'`. */
797
- role?: OverlayRole;
798
- } | {
799
- /**
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.
803
- *
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.
808
- */
809
- kind: 'insertPreview';
810
- /** Which preview the dispatcher's overlay draws. The kit's own shape
811
- * kinds are the ones it knows how to render; a consumer-defined kind
812
- * is legal and simply gets no live preview. */
813
- shape: string;
814
- bounds: {
815
- x: number;
816
- y: number;
817
- width: number;
818
- height: number;
819
- };
820
- extras: unknown;
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. */
824
- anchorPoint?: {
825
- x: number;
826
- y: number;
827
- };
828
- /** World affines the shape will also be drawn under once it lands, by
829
- * the container it is drawn into: the preview is painted under each. */
830
- copies?: readonly (readonly [number, number, number, number, number, number])[];
831
- };
832
- /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
833
- * `onMove` on subsequent input events of the same gesture and calls
834
- * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
835
- * on pointercancel / blur / escape). */
836
- interface OngoingHandle {
837
- /**
838
- * Optional logical action kind — a stable, human-readable tag the
839
- * dispatcher exposes via `getActiveAction()`, e.g. `'marquee'`, `'move'`,
840
- * `'resize'`, `'pan'`.
841
- *
842
- * When omitted, `getActiveAction().kind` reports `null` even though a
843
- * handle is in flight.
844
- */
845
- kind?: string;
846
- /**
847
- * The cursor this gesture shows now, read after every move. Wins over the
848
- * action's `activeCursor`, for a cursor that follows the gesture — the
849
- * rotate glyph turning about its pivot. `undefined` defers to the action.
850
- */
851
- cursor?(): CursorSpec | undefined;
852
- onMove?(ctx: InvocationCtx): void;
853
- onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
854
- /**
855
- * Optional preview surface: the in-flight state the canvas's preview-ghost
856
- * layer renders on top of the committed scene during the gesture.
857
- *
858
- * `previewIds()` enumerates the displaced node ids; `previewPose(id)`
859
- * returns the interim pose for one of them (shape opaque — the canvas
860
- * casts to its `TPose` parameter). Returning `null`, or omitting the
861
- * method, means "no preview this gesture".
862
- */
863
- previewIds?(): Iterable<string> | null;
864
- previewPose?(id: string): unknown | null;
865
- /**
866
- * Subset of `previewIds()` the ghost layer paints at full opacity. The
867
- * ghost alpha says "this is in flight under the pointer"; a node the
868
- * gesture merely displaces — a layout sibling reflowing into its
869
- * destination slot — is not, and reads better settled. Honored at
870
- * subtree-root granularity.
871
- */
872
- previewOpaqueIds?(): Iterable<string> | null;
873
- /**
874
- * When `false`, the preview-ghost layer paints the ghost AND the source
875
- * node stays visible at its committed pose, as a clone wants. Defaults to
876
- * `true`: the ghost replaces the source during the gesture.
877
- */
878
- previewHidesSource?: boolean;
879
- /**
880
- * Optional per-id preview *data*, for a gesture that mutates `node.data`
881
- * (e.g. anchor-edit on nodes that store the polygon on `data.path`) rather
882
- * than, or in addition to, the pose. Falls back to the committed
883
- * `node.data` when null/absent.
884
- */
885
- previewData?(id: string): unknown | null;
886
- /**
887
- * Optional chrome surface: a non-ghost visual (marquee rectangle, lasso
888
- * polyline) painted while the gesture is in flight. Returning `null`, or
889
- * omitting the method, means "no overlay this gesture".
890
- */
891
- overlay?(): OngoingOverlay | null;
892
- }
893
- /** Fire-once invocation. Runs to completion synchronously (or fires off an
894
- * async side-effect; the registry doesn't wait). */
895
- interface ImmediateInvoker {
896
- timing: 'immediate';
897
- /** `params` carries the matched binding's opts.params. Invoked from the
898
- * command palette, or anywhere else with no per-binding context, `params`
899
- * is undefined; descriptors should default to a sensible variant. */
900
- run(deps: ActionDeps, params?: Record<string, unknown>): void;
901
- }
902
- /** Phase-machine invocation. `start` opens the phase and returns the handle
903
- * the dispatcher pumps. */
904
- interface OngoingInvoker {
905
- timing: 'ongoing';
906
- start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
1148
+ interface Condition {
1149
+ (ctx: RuleCtx): boolean;
1150
+ readonly rule: Rule;
1151
+ /** `this && other` */
1152
+ and(other: Condition | Rule): Condition;
1153
+ /** `this || other` */
1154
+ or(other: Condition | Rule): Condition;
1155
+ /** `this && !other` */
1156
+ andNot(other: Condition | Rule): Condition;
1157
+ /** `this || !other` */
1158
+ orNot(other: Condition | Rule): Condition;
907
1159
  }
908
- /** Pluggable invocation strategy for an Action. */
909
- type Invoker = ImmediateInvoker | OngoingInvoker;
910
1160
 
911
1161
  /**
912
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
913
- * options). Tools own arrays of these on their `bindings` field; ambient
914
- * gesture-bindings are registered globally.
1162
+ * Whether two rules can never hold at once — no `RuleCtx` passes both.
1163
+ *
1164
+ * Conservative: `true` is a proof, `false` means "may overlap". It reasons
1165
+ * about the selector values themselves (two different `mode`s, an empty
1166
+ * selection against a non-empty one, `focused: true` against `false`) and
1167
+ * through `all` / `any` / `not`; a `when` closure is opaque, so it overlaps
1168
+ * everything. The route-conflict check uses it to stay quiet about bindings
1169
+ * that share a tuple but are never eligible together.
1170
+ */
1171
+ declare function rulesExclusive(a: Rule, b: Rule): boolean;
1172
+ /**
1173
+ * Whether `rule` can hold while `mode` is active. Only its `mode` and
1174
+ * `capability` tests are settled by the mode; every other test could go either
1175
+ * way, so `false` is a proof and `true` means "may hold".
915
1176
  */
1177
+ declare function ruleCanHoldIn(rule: Rule, mode: ActiveMode): boolean;
916
1178
 
917
- /** An interaction: a gesture spec composed with the id of the action it
918
- * invokes. Tools declare arrays of these; the dispatcher matches an incoming
919
- * input event against them and runs the winner's action. */
920
- interface GestureBinding {
921
- spec: GestureSpec;
922
- actionId: string;
923
- opts?: BindingOpts;
924
- }
1179
+ /** The detected half of a profile — everything except the derived scale. */
1180
+ type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
925
1181
  /**
926
- * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
927
- * (no per-binding opts) or an object form that pairs a spec with
928
- * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
929
- * Use the object form when two bindings for the same action differ only in
930
- * a runtime parameter — the dispatcher extracts `opts.params` and passes
931
- * them to `ImmediateInvoker.run` as its second argument.
1182
+ * Handle/hit multiplier applied on a coarse pointer.
1183
+ *
1184
+ * 8px handle → 14px; 24px rotation distance → 42px. Counting the
1185
+ * surrounding grab zone, that lands in the Apple HIG 44pt / Material 48dp
1186
+ * minimum-touch-target band. One constant so it is tunable in one place.
932
1187
  */
933
- type BoundGesture = GestureSpec | {
934
- spec: GestureSpec;
935
- opts: BindingOpts;
936
- };
1188
+ declare const COARSE_TARGET_SCALE = 1.75;
937
1189
  /**
938
- * @experimental
939
- * The whole of an action `actionBindings` reads. Routing needs this much and
940
- * none of `Action`'s palette fields, so it is the narrowest type the
941
- * dispatcher's binding walk can be written against.
1190
+ * Fold consumer overrides over detected facts and derive `targetScale`.
1191
+ *
1192
+ * `targetScale` is re-derived AFTER the merge, so an override of
1193
+ * `coarsePointer` alone scales the chrome as expected. An explicit
1194
+ * `targetScale` override wins over the derivation.
1195
+ */
1196
+ declare function resolveDeviceProfile(detected: DetectedDeviceFacts, overrides?: Partial<DeviceProfile>): DeviceProfile;
1197
+ /**
1198
+ * Assumed when `matchMedia` is unavailable (SSR, jsdom) and used as the
1199
+ * absent-means value for `RuleCtx.device`.
1200
+ *
1201
+ * Derived from `resolveDeviceProfile` rather than hand-written, so it can
1202
+ * never drift from what the function would compute for the same facts. A
1203
+ * mouse-like device is the safe default: it is what the kit has always
1204
+ * assumed, so an absent profile changes nothing for existing consumers.
942
1205
  */
943
- interface BindingSource {
944
- id: string;
945
- /** The gesture-spec form of the binding, read by the gesture dispatcher.
946
- * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
947
- * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
948
- * `{ spec, opts }` — use the object form for parametric actions where two
949
- * bindings for the same action differ only by `opts.params` (e.g. `flip`
950
- * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
951
- * passes them to `ImmediateInvoker.run` as its second argument. */
952
- defaultBinding?: GestureSpec | BoundGesture[];
953
- }
1206
+ declare const DEFAULT_DEVICE_PROFILE: DeviceProfile;
1207
+
954
1208
  /**
955
- * @experimental
956
- * Flatten an action's `defaultBinding` into `GestureBinding`s. A bare
957
- * `GestureSpec` has `kind` at top level; the object form has `spec`.
1209
+ * A settings form an action carries: what a surface draws in place of a bare
1210
+ * trigger for an action whose job is to change settings on the selection.
958
1211
  */
959
- declare function actionBindings(action: BindingSource): GestureBinding[];
960
1212
 
1213
+ /** What a settings form shows for the selection right now. */
1214
+ interface ActionSettingsState {
1215
+ /** The values, nested as the schema's groups nest. */
1216
+ values: Record<string, unknown>;
1217
+ /** The leaf paths where the selected items disagree. */
1218
+ mixed?: readonly string[];
1219
+ }
1220
+ /**
1221
+ * The settings form an action carries. A bar or a panel draws the action as a
1222
+ * button opening it, a menu as a submenu of its fields, and a palette as a
1223
+ * dialog; each change triggers the action with the params it makes.
1224
+ */
1225
+ interface ActionSettings {
1226
+ /** The form. Its `name` titles the popover, submenu or dialog. */
1227
+ schema: PrefGroup;
1228
+ /** The selection's current values, from the deps `enabled` is given. Under
1229
+ * `enabled`'s contract: pure, fast, and it must not throw. */
1230
+ read(deps: ActionDeps): ActionSettingsState;
1231
+ /** The params one field's change triggers the action with, or `null` for
1232
+ * none. Default: `{ [path]: value }`. */
1233
+ params?(path: string, value: unknown): Record<string, unknown> | null;
1234
+ }
1235
+ /** `action`'s settings as they stand under `deps`, or `null` for an action
1236
+ * with no settings. A `read` that throws shows nothing rather than breaking
1237
+ * the surface. */
1238
+ declare function readActionSettings(action: Action, deps: ActionDeps | undefined): ActionSettingsState | null;
1239
+ /** The params a change of the field at `path` to `value` triggers the action with. */
1240
+ declare function actionSettingsParams(settings: ActionSettings, path: string, value: unknown): Record<string, unknown> | null;
1241
+
1242
+ /** What content being routed brings to `Action.enabled`: a drop's or a
1243
+ * paste's items, or a carry's payload with no items. */
1244
+ interface Carried {
1245
+ items: readonly IngestItem[];
1246
+ carry?: {
1247
+ type: string;
1248
+ value: unknown;
1249
+ };
1250
+ }
961
1251
  /**
962
- * @experimental
963
1252
  * Closed enum of reasons an action might report itself as disabled. The
964
1253
  * consumer (palette, menu, etc.) maps these symbolic values to display
965
1254
  * strings via its own label map.
@@ -974,20 +1263,23 @@ declare const ActionDisabledReason: {
974
1263
  /** Why an action is unavailable right now. */
975
1264
  type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
976
1265
  /**
977
- * @experimental
978
1266
  * What a palette, menu or toolbar renders for an action. No part of this is
979
1267
  * read during dispatch.
980
1268
  */
981
1269
  interface ActionPresentation {
982
1270
  label: string;
983
- /** Inline-SVG icon for palette / toolbar surfaces. May be a static
984
- * `ReactNode` or a function (rare; useful for state-aware icons like a
985
- * "lock" toggle). */
986
- icon?: ReactNode | (() => ReactNode);
1271
+ /** The action's icon, by qualified name: `icon('group')` in the package that draws it. */
1272
+ icon?: IconRef;
987
1273
  /** Grouping key for palette/menu surfaces. Free-form string; the kit
988
1274
  * ships defaults for `'align'` (six edges/centers), `'distribute'`
989
1275
  * (two axes), and recommends `'pathfinder'` for boolean ops. */
990
1276
  group?: string;
1277
+ /** Words the action is also found by and gathered under. Where `group` is
1278
+ * the one place an action sits, an action carries any number of tags: a
1279
+ * palette search matches them as it matches the label, and
1280
+ * `actionsTagged` / `taggedActionItems` gather every action or entry
1281
+ * carrying one, for a bar or a menu built from a tag. */
1282
+ tags?: readonly string[];
991
1283
  /** Display override for the keyboard shortcut. When omitted, palette
992
1284
  * surfaces derive a label from `defaultBinding` via their own
993
1285
  * formatter. */
@@ -998,9 +1290,13 @@ interface ActionPresentation {
998
1290
  * triggers the action with that variant's `params`. Omitted = the action
999
1291
  * is one entry. See `actionItems`. */
1000
1292
  variants?: readonly ActionVariant[];
1293
+ /** A form of the settings the action changes on the selection. A surface
1294
+ * draws the action as that form instead of a bare trigger, and each change
1295
+ * triggers it with the params `settings.params` makes. Not combined with
1296
+ * `variants`. */
1297
+ settings?: ActionSettings;
1001
1298
  }
1002
1299
  /**
1003
- * @experimental
1004
1300
  * One entry a parametric action contributes to a bar, menu or palette.
1005
1301
  */
1006
1302
  interface ActionVariant {
@@ -1008,13 +1304,14 @@ interface ActionVariant {
1008
1304
  key: string;
1009
1305
  label: string;
1010
1306
  /** Falls back to the action's own `icon`. */
1011
- icon?: ReactNode | (() => ReactNode);
1307
+ icon?: IconRef;
1308
+ /** Tags this variant carries besides its action's, which it carries too. */
1309
+ tags?: readonly string[];
1012
1310
  /** Passed to `trigger`, and matched against each binding's
1013
1311
  * `opts.params` to find the variant's shortcuts. */
1014
1312
  params: Readonly<Record<string, unknown>>;
1015
1313
  }
1016
1314
  /**
1017
- * @experimental
1018
1315
  * What routing consults: the bindings that reach the action, the deps its
1019
1316
  * invoker reads, the gates it passes, and the cursors it claims.
1020
1317
  */
@@ -1036,14 +1333,14 @@ interface ActionDispatch extends BindingSource {
1036
1333
  * shortcuts and global held-key triggers. Default: ambient. */
1037
1334
  scope?: 'hotkey';
1038
1335
  /**
1039
- * @experimental
1040
1336
  * Optional predicate saying whether the action can run right now. Return
1041
1337
  * `true` when it can, or an `ActionDisabledReason` when it cannot. The
1042
1338
  * command palette greys out a disabled row, skips it in keyboard nav,
1043
1339
  * ignores clicks, and shows the reason next to the label. The dispatcher
1044
1340
  * skips a disabled action too, on a key as on a pointer, and falls through
1045
- * to the next binding that matches. `ActionsRegistry.trigger` does not ask,
1046
- * so the action's own `run` should still self-guard.
1341
+ * to the next binding that matches. `ActionsRegistry.trigger` asks too, but
1342
+ * with no point, so a `run` whose answer depends on `at` should still
1343
+ * self-guard.
1047
1344
  *
1048
1345
  * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
1049
1346
  * must not throw. A throw is caught and treated as disabled with reason
@@ -1058,23 +1355,23 @@ interface ActionDispatch extends BindingSource {
1058
1355
  * click, a press, a long-press. An action that only applies somewhere
1059
1356
  * (on a path segment, say) refuses other points here, so the dispatcher
1060
1357
  * falls through to the next binding and hover prediction shows the
1061
- * action's `cursor` only where it would act. Palettes and keystrokes
1062
- * pass no point; treat absent as "anywhere".
1358
+ * action's `cursor` only where it would act. `trigger` passes the
1359
+ * `worldX`/`worldY` its params carry, so a menu row is run under the
1360
+ * question it was shown enabled for. Palettes and keystrokes pass no
1361
+ * point; treat absent as "anywhere".
1063
1362
  *
1064
- * `carried` is what a drop or a paste being routed brings: its items. For
1065
- * a drag still in flight they hold a MIME and nothing else, since a
1066
- * browser withholds contents until the drop. An action that takes only
1067
- * some content refuses the rest here, so a drag it would not take is not
1068
- * accepted and a paste it would not take stays the page's.
1363
+ * `carried` is what a drop, a paste or a carry being routed brings. For
1364
+ * an OS drag still in flight its items hold a MIME and nothing else, since
1365
+ * a browser withholds contents until the drop. A carry has no items and
1366
+ * hands over its payload whole. An action that takes only some content
1367
+ * refuses the rest here, so a drag it would not take is not accepted and a
1368
+ * paste it would not take stays the page's.
1069
1369
  */
1070
1370
  enabled?: (deps?: ActionDeps, at?: {
1071
1371
  x: number;
1072
1372
  y: number;
1073
- }, carried?: {
1074
- items: readonly IngestItem[];
1075
- }) => true | ActionDisabledReason;
1373
+ }, carried?: Carried) => true | ActionDisabledReason;
1076
1374
  /**
1077
- * @experimental
1078
1375
  * Whether the action reads as held down right now: bold on the selected
1079
1376
  * text, snapping switched on. A toolbar button draws it as pressed.
1080
1377
  * Omitted = the action has no pressed state, which is not the same as
@@ -1084,6 +1381,15 @@ interface ActionDispatch extends BindingSource {
1084
1381
  * throw. `deps` is the bag `enabled` is given.
1085
1382
  */
1086
1383
  active?: (deps?: ActionDeps) => boolean;
1384
+ /**
1385
+ * Show what running the action now would do, and change nothing: the
1386
+ * result an operation would make, drawn over the canvas. A toolbar asks
1387
+ * while its button is hovered or focused, through
1388
+ * `ActionsRegistry.preview`, and calls what this returns when that ends.
1389
+ * Asked only while `enabled` says yes, with the deps and params `run`
1390
+ * would get. Omitted = the action has nothing to show.
1391
+ */
1392
+ preview?: (deps: ActionDeps, params?: Record<string, unknown>) => (() => void) | void;
1087
1393
  /**
1088
1394
  * Declarative eligibility rule, evaluated against the current
1089
1395
  * `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
@@ -1106,12 +1412,14 @@ interface ActionDispatch extends BindingSource {
1106
1412
  * resolved earlier in the pump via `AffordanceRegion.cursor` and never
1107
1413
  * reach this field.
1108
1414
  *
1109
- * Static value only. Prediction runs `enabled()` but cannot run the
1110
- * invoker, so an action that matches yet bails at `start()` (empty
1111
- * handle) may still show its cursor — keep `enabled` accurate for
1112
- * actions that declare one.
1415
+ * A function is asked each time with the params the winning binding would
1416
+ * start the action with, tunings included, so a cursor can follow them — a
1417
+ * brush ring as wide as the brush. Read it through {@link actionCursor}.
1418
+ * Prediction runs `enabled()` but cannot run the invoker, so an action that
1419
+ * matches yet bails at `start()` (empty handle) may still show its cursor —
1420
+ * keep `enabled` accurate for actions that declare one.
1113
1421
  */
1114
- cursor?: CursorSpec;
1422
+ cursor?: CursorSpec | ((params: Record<string, unknown> | undefined) => CursorSpec | undefined);
1115
1423
  /**
1116
1424
  * CSS cursor shown while THIS action's ongoing handle is in flight —
1117
1425
  * grabbing while panning, `move` while dragging a selection, `crosshair`
@@ -1124,15 +1432,22 @@ interface ActionDispatch extends BindingSource {
1124
1432
  * gesture.
1125
1433
  */
1126
1434
  activeCursor?: CursorSpec;
1435
+ /**
1436
+ * The modifiers an ongoing action reads while it runs, by role: Shift
1437
+ * holding a handle's angle, Alt breaking its mirror. The dispatcher resolves
1438
+ * each into `InvocationCtx.roles` on the press, every move, and the release,
1439
+ * so a binding's `opts.roles` or a tuning's `roles` can move one to another
1440
+ * key. Read them with {@link roleReader}. A binding's own `mods` are matched
1441
+ * once, at the press; these are read throughout.
1442
+ */
1443
+ modifierRoles?: ModifierRoles;
1127
1444
  }
1128
1445
  /**
1129
- * @experimental
1130
1446
  * Single registered action.
1131
1447
  */
1132
1448
  interface Action extends ActionDispatch, ActionPresentation {
1133
1449
  }
1134
1450
  /**
1135
- * @experimental
1136
1451
  * The whole of an actions registry the dispatcher reads. It walks the list
1137
1452
  * once per event to build its actionId lookup and never mutates it, so the
1138
1453
  * routing side depends on this rather than on `ActionsRegistry`, whose
@@ -1141,450 +1456,500 @@ interface Action extends ActionDispatch, ActionPresentation {
1141
1456
  interface ActionSource {
1142
1457
  list(): readonly Action[];
1143
1458
  }
1459
+ /** `action`'s hover cursor for an invocation given `params`: its `cursor`,
1460
+ * asked when it is a function. */
1461
+ declare function actionCursor(action: Pick<Action, 'cursor'> | undefined, params?: Record<string, unknown>): CursorSpec | undefined;
1144
1462
 
1145
- /**
1146
- * Tuning: what a host changes about an action it did not write — the params
1147
- * every invocation starts from, and bindings added beside the action's own —
1148
- * without restating the descriptor. `ActionsRegistry.tune` holds tunings and
1149
- * applies them to whatever descriptor is live for the id.
1150
- */
1151
-
1152
- /** An action's params, or a thunk read each time the action asks for them. */
1153
- type ActionParams<P = Record<string, unknown>> = Partial<P> | (() => Partial<P>);
1154
- /** A binding added for an action: a gesture alone, or a gesture with the
1155
- * options it passes — the {@link BoundGesture} shape, its params typed. */
1156
- type TunedBinding<P = Record<string, unknown>> = GestureSpec | {
1157
- spec: GestureSpec;
1158
- opts: Omit<BindingOpts, 'params'> & {
1159
- params?: ActionParams<P>;
1160
- };
1161
- };
1162
- /** What a host changes about one action. */
1163
- interface ActionTuning<P = Record<string, unknown>> {
1164
- /** Params every invocation starts from. What a binding or a `trigger` call
1165
- * passes is laid over them, so these fill in what it leaves out. A thunk is
1166
- * read on every call, and an ongoing action resolving its `opts.params`
1167
- * mid-gesture sees the value as it is then. */
1168
- params?: ActionParams<P>;
1169
- /** Bindings added after the action's own `defaultBinding`. */
1170
- bindings?: readonly TunedBinding<P>[];
1463
+ /** Which canvas hit a menu answers. Every field given must match. */
1464
+ interface MenuWhen {
1465
+ bodyTarget?: BodyTarget | readonly BodyTarget[];
1466
+ bodyKind?: string | readonly string[];
1467
+ }
1468
+ /** A menu as data: a prefs schema drawn as a menu (`resolveSchemaMenu`).
1469
+ * A def without `when` is opened only by id, from chrome. */
1470
+ interface MenuDef {
1471
+ id: string;
1472
+ when?: MenuWhen;
1473
+ menu: PrefGroup;
1171
1474
  }
1172
- /** `action` with each tuning applied in turn, a later one's params over an
1173
- * earlier one's. Applying a tuning to a copy it already tuned changes nothing. */
1174
- declare function tuneAction(action: Action, tunings: readonly ActionTuning[]): Action;
1175
-
1176
- /**
1177
- * @experimental
1178
- * The entries an action contributes to a bar, menu or palette — one per
1179
- * variant, or the action itself when it declares none.
1180
- */
1181
-
1182
- /** @experimental One renderable entry: an action, optionally with params. */
1183
- interface ActionItem {
1184
- /** `action.id`, or `action.id:variant.key` — unique across a registry. */
1185
- key: string;
1186
- action: Action;
1187
- label: string;
1188
- icon?: ReactNode | (() => ReactNode);
1189
- /** What to pass to `trigger(action.id, params)`; absent for a plain action. */
1190
- params?: Readonly<Record<string, unknown>>;
1475
+ /** What a right-click landed on. */
1476
+ interface MenuHit {
1477
+ bodyTarget?: BodyTarget;
1478
+ bodyKind?: string;
1479
+ }
1480
+ /** A def with who contributed it: a tool id, or `null` for ambient. */
1481
+ interface OwnedMenuDef {
1482
+ def: MenuDef;
1483
+ owner: string | null;
1191
1484
  }
1192
- /** @experimental Expand `action` into the entries a surface renders. An
1193
- * action without an immediate invoker has none: `trigger` cannot start a
1194
- * drag, so a button for one would do nothing. */
1195
- declare function actionItems(action: Action): ActionItem[];
1196
1485
 
1197
- /**
1198
- * @experimental
1199
- * Palette-side evaluation of `Action.enabled`. The dispatcher calls the
1200
- * predicate raw as a gate; this is the surface that also enforces the
1201
- * contract the predicate's JSDoc states — no throwing, under 4ms in dev.
1202
- */
1486
+ /** What resolving reads: the registered actions, each one's deps, how to run
1487
+ * one, and the values behind the schema's value leaves. */
1488
+ interface MenuSources {
1489
+ actions(): readonly Action[];
1490
+ depsFor(action: Action): ActionDeps | undefined;
1491
+ trigger(id: string, params?: Record<string, unknown>): boolean;
1492
+ /** The value at a leaf's dotted path. Booleans, enums, and numbers are left
1493
+ * out of a menu without both this and `onChange`, colors and strings without
1494
+ * this, and `showIf`/`enableIf` are checked only with it. */
1495
+ values?(path: string): unknown;
1496
+ onChange?(path: string, value: unknown): void;
1497
+ /** The schema an `alias` leaf's `of` is read in. Default: the menu's own. */
1498
+ over?: PrefGroup;
1499
+ }
1500
+ /**
1501
+ * Draw a prefs schema as menu entries. Action leaves become items from the
1502
+ * registry (one per variant for a parametric action), an `actions` leaf one
1503
+ * item per action in its group, a boolean a checked item, an enum radio
1504
+ * rows, and a number and a string a row edited in place (all through `src.values`/`src.onChange`);
1505
+ * a color shows as a swatch, read-only, and so does a string as a disabled row when it is a readout
1506
+ * or nothing writes it. A group drawn `as` a
1507
+ * `menu`, `page`, `tab`, or `panel` is a submenu, and any other group's rows
1508
+ * sit inline between separators. Paint, field, compound, and app-defined leaves are left out.
1509
+ * `at` is the world point the menu was opened at and reaches `enabled` and
1510
+ * every action's params; `params` go to every action, under its own.
1511
+ */
1512
+ declare function resolveSchemaMenu(schema: PrefGroup, src: MenuSources, at?: Point, params?: Record<string, unknown>): MenuEntry[];
1513
+ /** Turn a declared menu into model entries; see {@link resolveSchemaMenu}. */
1514
+ declare function resolveMenu(def: MenuDef, src: MenuSources, at?: Point, params?: Record<string, unknown>): MenuEntry[];
1203
1515
 
1204
- /**
1205
- * @experimental
1206
- * Result of evaluating an Action's `enabled` predicate.
1207
- */
1208
- interface ActionEnabledResult {
1209
- enabled: boolean;
1210
- reason?: ActionDisabledReason;
1516
+ /** Ask to open a menu: by id (chrome) or by what was hit (canvas). */
1517
+ interface MenuOpenRequest {
1518
+ menu?: string;
1519
+ hit?: MenuHit;
1520
+ /** Viewport point the menu is placed at, unless `under` is set. */
1521
+ client: Point;
1522
+ /** Viewport rect of the button the menu hangs from: placed below it, or above when below does not fit. */
1523
+ under?: Rect;
1524
+ /** World point, passed to `enabled` and to every item. */
1525
+ world?: Point;
1526
+ params?: Record<string, unknown>;
1527
+ source?: 'pointer' | 'keyboard';
1528
+ }
1529
+ /** The menu on screen. `seq` changes on every open, so a renderer remounts. */
1530
+ interface OpenMenu {
1531
+ seq: number;
1532
+ entries: readonly MenuEntry[];
1533
+ client: Point;
1534
+ under?: Rect;
1535
+ world?: Point;
1536
+ source: 'pointer' | 'keyboard';
1537
+ }
1538
+ /** Holds the declared menus and the one open menu. */
1539
+ interface MenuHost {
1540
+ /** Declare a menu; `owner` is a tool id, or `null` for always. Returns a release. */
1541
+ define(def: MenuDef, owner?: string | null): () => void;
1542
+ /** Pick, resolve and show. False when nothing matched or nothing resolved. */
1543
+ open(req: MenuOpenRequest): boolean;
1544
+ /** Whether `open(req)` would show anything. Changes no state and notifies no one. */
1545
+ has(req: MenuOpenRequest): boolean;
1546
+ close(): void;
1547
+ current(): OpenMenu | null;
1548
+ subscribe(fn: () => void): () => void;
1211
1549
  }
1212
- /**
1213
- * @experimental
1214
- * Safely evaluate an action's `enabled` predicate. Returns `{enabled: true}`
1215
- * when no predicate is supplied. Catches throws and treats them as disabled
1216
- * with reason `'(predicate threw)'`. In dev mode, warns once per action id
1217
- * when a single call exceeds 4ms (single frame at 240fps — generous; real
1218
- * predicates should be sub-millisecond).
1219
- */
1220
- declare function evaluateEnabled(action: Action, deps?: ActionDeps): ActionEnabledResult;
1221
- /**
1222
- * @experimental
1223
- * Safely evaluate an action's `active` predicate. `undefined` for an action
1224
- * with no pressed state; a predicate that throws reads as not active.
1225
- */
1226
- declare function evaluateActive(action: Action, deps?: ActionDeps): boolean | undefined;
1550
+ /** Creates the host that decides which context menu is open, from the menus registered to it. */
1551
+ declare function createMenuHost(src: MenuSources & {
1552
+ activeTool(): string | null;
1553
+ }): MenuHost;
1227
1554
 
1228
1555
  /**
1229
- * The dep registry: named "live source" thunks. Actions declare
1230
- * `requires: ['selection']`; the dispatcher calls `registry.get('selection')`
1231
- * at invocation time to build a typed Deps bag.
1556
+ * The tree both input registries hang on. A provider is a root; every canvas
1557
+ * mounts a scope under the registry in scope; a yoke sits between the two. A
1558
+ * registration lives in the node it was made in, and a lookup starts at a leaf
1559
+ * and walks up, so one canvas never answers for another.
1232
1560
  */
1233
1561
 
1234
- /** Holds the live sources an action's declared dependencies resolve to.
1235
- * Sources are thunks, read at invocation time, so an action never captures
1236
- * stale state. */
1237
- interface DepRegistry {
1238
- register<K extends DepName>(name: K, source: () => DepSchema[K]): () => void;
1239
- get<K extends DepName>(name: K): DepSchema[K] | undefined;
1240
- /** Hear that what the registry answers may have changed: a source was
1241
- * registered or released anywhere in its tree, or an owner called
1242
- * {@link DepRegistry.changed}. Returns the unsubscribe. A hand-built
1243
- * registry whose answers never change leaves this and `changed` out. */
1244
- subscribe?(listener: () => void): () => void;
1245
- /** Say that a source's value changed. A source is a thunk read on demand,
1246
- * so its owner is the only one who knows; chrome showing an action's
1247
- * `enabled` or `active` state re-reads on this. */
1248
- changed?(): void;
1562
+ /** What every node of one tree shares: a version and the listeners to it. */
1563
+ interface ScopeTree {
1564
+ version: number;
1565
+ readonly listeners: Emitter;
1249
1566
  }
1250
- /** One node of the dep tree: the sources registered at it, over its parent's.
1251
- * Each name holds a stack of sources, newest live, so a displaced source is
1252
- * restored when the one above it leaves. */
1253
- declare class DepNode extends ScopeNode<DepNode> {
1254
- private readonly sources;
1255
- readonly registry: DepRegistry;
1256
- constructor(parent: DepNode | null, descends: boolean);
1567
+ declare class ScopeNode<N extends ScopeNode<N>> {
1568
+ readonly parent: N | null;
1569
+ readonly descends: boolean;
1570
+ readonly tree: ScopeTree;
1571
+ private readonly children;
1572
+ private activeChild;
1573
+ /** `descends`: whether reads made here answer from the active child down —
1574
+ * true for a root or a yoke, which chrome reads through; false for a scope,
1575
+ * which answers for itself even with a scope nested inside it. */
1576
+ constructor(parent: N | null, descends: boolean);
1577
+ /** Bump the tree's version and tell every listener in it. */
1578
+ changed(): void;
1579
+ /** Join the parent's children, newest last; returns the leave. */
1580
+ mount(): () => void;
1581
+ /** Make this node the one its ancestors resolve through. */
1582
+ activate(): void;
1583
+ /** The node a lookup made here starts from: down the active children, or the
1584
+ * newest where nothing has been activated yet. A yoke with no members yet
1585
+ * is passed over — there is no canvas behind it to answer. */
1586
+ leaf(): N;
1587
+ private newestMember;
1588
+ /** Where a read made through this node starts. */
1589
+ start(): N;
1590
+ /** Whether this node is on the path a read from the root takes. */
1591
+ isActive(): boolean;
1592
+ /** This node, then each ancestor up to the root. */
1593
+ chain(): Generator<N>;
1257
1594
  }
1258
- /**
1259
- * A stock dep registry with no parent: what `<DepRegistryProvider>` mounts,
1260
- * for a dispatcher driven without React. Sources registered under one name
1261
- * stack, newest live, and releasing one restores the one beneath it.
1262
- */
1263
- declare function createDepRegistry(): DepRegistry;
1264
-
1265
- /**
1266
- * Shared dep-bag builder — the single implementation behind both the gesture
1267
- * dispatcher's per-invocation `buildDeps` and `ActionsRegistry.trigger`'s
1268
- * `requires` branch. Reads the action's declared `requires`, resolves each
1269
- * name against the `DepRegistry`, and (dev builds only) wraps the bag in a
1270
- * Proxy that warns on undeclared reads.
1271
- */
1272
-
1273
- /** Build the deps bag for an action from its declared `requires`. */
1274
- declare function buildDepsFromRequires(action: Action, depRegistry: DepRegistry): ActionDeps;
1275
1595
 
1276
- /** Hotkey-slot trigger key. The slot is engaged while this key is held —
1277
- * hence "hotkey": active as long as the key is hot. Omitted means the tool
1278
- * is not eligible for the hotkey slot.
1279
- *
1280
- * The named keys map to their DOM names (`'space'` is `' '`, `'ctrl'` is
1281
- * `'Control'`). Anything else is a `KeyboardEvent.key` value, matched
1282
- * case-insensitively as every key spec is — so `'o'` holds on O. */
1283
- type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift' | (string & {});
1284
- /** Presentation metadata for tool palettes / menus. Optional on every
1285
- * tool — consumers that render a palette (`<ToolPalette>`) read these
1286
- * fields to display the tool; consumers that don't can ignore them.
1287
- *
1288
- * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
1289
- * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1290
- interface ToolPresentation {
1291
- /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1292
- label?: string;
1293
- /** Inline-SVG icon component output: a static `ReactNode`, or a function
1294
- * returning one. */
1295
- icon?: react.ReactNode | (() => react.ReactNode);
1296
- /** Palette grouping key. Tools sharing a group render contiguously
1297
- * with separators between groups. Free-form string; the kit
1298
- * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
1299
- group?: string;
1300
- /** Display override for the keyboard shortcut. When omitted the palette
1301
- * derives one from `Tool.keybinding` via its own formatter. */
1302
- shortcut?: string;
1303
- /** Keep the tool off palettes — for a tool that is only ever held, like a
1304
- * scrub engaged by its `hotkey`. It stays registered and selectable by
1305
- * id; only the palette leaves it out. */
1306
- hide?: boolean;
1596
+ /** Which tool is active, plus the stack of tools temporarily held active by a
1597
+ * hotkey (space-for-hand and the like). The dispatcher reads this to decide
1598
+ * whose bindings are in scope. */
1599
+ interface ActiveToolContextValue {
1600
+ /** The focused tool's id, or `null` when no tool holds the active slot —
1601
+ * a canvas can run on ambient bindings alone. */
1602
+ active: string | null;
1603
+ hotkeyStack: string[];
1604
+ setActive(id: string | null): void;
1605
+ pushHotkey(id: string): void;
1606
+ /**
1607
+ * Disengage a hotkey-held tool. With `id`, removes that tool's own entry
1608
+ * wherever it now sits — holds do not have to come up in the order they went
1609
+ * down, and popping the top would disengage a tool whose key is still held.
1610
+ * With no id, pops the top.
1611
+ */
1612
+ popHotkey(id?: string): void;
1307
1613
  }
1308
1614
  /**
1309
- * When an entry's bindings are live. A set, not one value: the hand tool is
1310
- * palette-selectable AND engaged by holding space, and both hold at once.
1615
+ * One node of the tool tree. A node that `owns` a tool holds its state; one
1616
+ * that does not — a scope inside a yoke — reads and writes its yoke's.
1311
1617
  */
1312
- interface Eligibility {
1313
- /** Selectable as the focused entry — exclusive, one at a time. */
1314
- focus?: boolean;
1315
- /** Also live while this key is held. */
1316
- offhand?: HotkeyTrigger;
1317
- /** Live regardless of what is focused. */
1318
- always?: boolean;
1319
- /** Live only for input this entry's own affordances produced. */
1320
- claimed?: boolean;
1321
- /** Modality filter, applied wherever it would otherwise be live. */
1322
- capabilities?: CapabilityTag[];
1618
+ declare class ToolNode extends ScopeNode<ToolNode> {
1619
+ private state;
1620
+ private value;
1621
+ constructor(parent: ToolNode | null, descends: boolean, owns: boolean, initialActive: string | null);
1622
+ /** The node whose tool a read here resolves to. */
1623
+ owner(): ToolNode;
1624
+ private update;
1625
+ /** A stable value per owner and state, for `useSyncExternalStore`. */
1626
+ snapshot(): ActiveToolContextValue;
1627
+ subscribe: (listener: () => void) => (() => void);
1628
+ }
1629
+ /** Props for `<ActiveToolContextProvider>`. */
1630
+ interface ActiveToolContextProviderProps {
1631
+ children: ReactNode;
1632
+ /** Seeds the active slot, and wins over the first `useTools` call's
1633
+ * `active`. Omitted, the slot starts empty and that call seeds it. */
1634
+ initialActive?: string | null;
1323
1635
  }
1636
+ /** Provides root active-tool state. Every canvas below has a tool of its own;
1637
+ * reads and writes here reach the one last used. */
1638
+ declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react.JSX.Element;
1639
+ /** The active-tool state in scope. Throws outside a provider; use
1640
+ * `useOptionalActiveToolContext` where one is not guaranteed. */
1641
+ declare function useActiveToolContext(): ActiveToolContextValue;
1324
1642
  /**
1325
- * Where an entry's overlay sits in the layer stack, relative to the
1326
- * selection chrome. `'top'` is the default and renders above everything;
1327
- * the other two exist for chrome that belongs under the selection handles
1328
- * (a snap-target highlight, say). With no selection overlay in the stack,
1329
- * all three collapse to `'top'`.
1643
+ * Like `useActiveToolContext`, but returns `null` when no
1644
+ * `<ActiveToolContextProvider>` is in scope instead of throwing.
1330
1645
  */
1331
- type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
1332
- /** Dep sources an entry provides while it is installed, by dep name. */
1333
- type ContributionDeps = {
1334
- [K in DepName]?: () => DepSchema[K];
1335
- };
1646
+ declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
1336
1647
  /**
1337
- * What routing reads off an entry: when its bindings are live, which they
1338
- * are, the actions they name, and the deps those actions read.
1648
+ * Mounts an `<ActiveToolContextProvider>` only when no parent provider is in
1649
+ * scope; otherwise renders children unwrapped.
1339
1650
  */
1340
- interface ContributionRouting {
1341
- id: string;
1342
- eligibility: Eligibility;
1343
- bindings?: GestureBinding[];
1344
- actions?: Action[];
1345
- /** Dep sources this entry provides while it is installed. A host installs
1346
- * them for as long as the entry is registered, whatever its eligibility. */
1347
- deps?: ContributionDeps;
1348
- }
1651
+ declare function ActiveToolContextProviderIfRoot({ children, }: {
1652
+ children: ReactNode;
1653
+ }): react.JSX.Element;
1654
+
1349
1655
  /**
1350
- * What an entry draws and how a palette shows it. None of it reaches the
1351
- * dispatcher.
1656
+ * Tuning: what a host changes about an action it did not write — the params
1657
+ * every invocation starts from, and bindings added beside the action's own —
1658
+ * without restating the descriptor. `ActionsRegistry.tune` holds tunings and
1659
+ * applies them to whatever descriptor is live for the id.
1352
1660
  */
1353
- interface ContributionChrome<TOverlay = KernelOverlay> {
1354
- /** One layer, or several composed in the given order.
1355
- *
1356
- * Routing never reads a layer — it collects the live ones and hands them
1357
- * back in scope order. `TOverlay` is what the consuming kernel draws;
1358
- * `@weasel-js/core` binds it to `RenderLayer<unknown>`. */
1359
- overlay?: TOverlay | TOverlay[];
1360
- /** Defaults to `'top'`. Applies to every layer in `overlay`. */
1361
- overlayPosition?: OverlayPosition;
1362
- presentation?: ToolPresentation;
1661
+
1662
+ /** An action's params, or a thunk read each time the action asks for them. */
1663
+ type ActionParams<P = Record<string, unknown>> = Partial<P> | (() => Partial<P>);
1664
+ /** A binding added for an action: a gesture alone, or a gesture with the
1665
+ * options it passes — the {@link BoundGesture} shape, its params typed. */
1666
+ type TunedBinding<P = Record<string, unknown>> = GestureSpec | {
1667
+ spec: GestureSpec;
1668
+ opts: Omit<BindingOpts, 'params'> & {
1669
+ params?: ActionParams<P>;
1670
+ };
1671
+ };
1672
+ /** What a host changes about one action. */
1673
+ interface ActionTuning<P = Record<string, unknown>> {
1674
+ /** Params every invocation starts from. What a binding or a `trigger` call
1675
+ * passes is laid over them, so these fill in what it leaves out. A thunk is
1676
+ * read on every call, and an ongoing action resolving its `opts.params`
1677
+ * mid-gesture sees the value as it is then. */
1678
+ params?: ActionParams<P>;
1679
+ /** Bindings added after the action's own `defaultBinding`. */
1680
+ bindings?: readonly TunedBinding<P>[];
1681
+ /** Keys for some of the action's `modifierRoles`, by role id. A binding's
1682
+ * own `opts.roles` is laid over these. */
1683
+ roles?: RoleRebinds;
1363
1684
  }
1685
+ /** `action` with each tuning applied in turn, a later one's params over an
1686
+ * earlier one's. Applying a tuning to a copy it already tuned changes nothing. */
1687
+ declare function tuneAction(action: Action, tunings: readonly ActionTuning[]): Action;
1688
+
1364
1689
  /**
1365
- * A registry entry: what it contributes, and when it is eligible. Every role
1366
- * is optional and independent — an entry that only routes input declares only
1367
- * `bindings` and `actions`.
1690
+ * The entries an action contributes to a bar, menu or palette — one per
1691
+ * variant, or the action itself when it declares none.
1368
1692
  */
1369
- interface Contribution<TOverlay = KernelOverlay> extends ContributionRouting, ContributionChrome<TOverlay> {
1370
- /** Reflection escape hatch — the authored form, when there was one. */
1371
- def?: unknown;
1693
+
1694
+ /** One renderable entry: an action, optionally with params. */
1695
+ interface ActionItem {
1696
+ /** `action.id`, or `action.id:variant.key` — unique across a registry. */
1697
+ key: string;
1698
+ action: Action;
1699
+ label: string;
1700
+ icon?: IconRef;
1701
+ /** The action's tags, then the variant's own. */
1702
+ tags: readonly string[];
1703
+ /** What to pass to `trigger(action.id, params)`; absent for a plain action. */
1704
+ params?: Readonly<Record<string, unknown>>;
1372
1705
  }
1706
+ /** Expand `action` into the entries a surface renders. An
1707
+ * action without an immediate invoker has none: `trigger` cannot start a
1708
+ * drag, so a button for one would do nothing. */
1709
+ declare function actionItems(action: Action): ActionItem[];
1710
+ /** How a palette titles `item`: a variant reads as its action's
1711
+ * label and its own ("New: A4"), unless its label already holds the action's
1712
+ * as whole words ("Zoom In", "Reset Zoom"). */
1713
+ declare function actionItemTitle(item: ActionItem): string;
1373
1714
 
1374
1715
  /**
1375
- * Re-exports the pure matcher primitives from `@weasel-js/gestures` and layers
1376
- * the actions-layer binding-scope / matchBest logic on top.
1716
+ * Project an action's keyboard bindings into the shape a shortcut chip
1717
+ * renders from (`formatShortcut` / `formatShortcutParts` in `@weasel-js/ui`).
1377
1718
  */
1378
1719
 
1379
- /** Where a binding came from, which is also its priority: a held hotkey beats
1380
- * the active tool, which beats bindings that are always in scope. */
1381
- type BindingScope = 'ambient' | 'active' | 'hotkey';
1382
- /** A binding paired with where it came from, ready to be matched against an
1383
- * event. */
1384
- interface ScopedBinding {
1385
- binding: GestureBinding;
1386
- scope: BindingScope;
1387
- /** Id of the tool or contribution that declared this binding —
1388
- * `'&'`-channel phase atoms resolve to it. `null` for a registered
1389
- * action's own binding. */
1390
- ownerId: string | null;
1391
- }
1392
- /** The binding that won a match. */
1393
- interface MatchResult {
1394
- binding: GestureBinding;
1395
- scope: BindingScope;
1396
- /** Id of the entry that declared the binding — propagated from
1397
- * `ScopedBinding` so the dispatcher can record it as the handle owner. */
1398
- ownerId: string | null;
1399
- }
1720
+ /** One keyboard shortcut, flattened for display: the chip a menu row draws
1721
+ * (`MenuShortcut` in `@weasel-js/menu`, `ShortcutInput` in `@weasel-js/gestures`). */
1722
+ type ActionShortcut = MenuShortcut;
1400
1723
  /**
1401
- * True when a spec's target actually consults the affordance hit rather than
1402
- * only the body classification. `kindOf` predicates are handed the hit;
1403
- * `affordance:<k>` matches on its `kind`. The body-class strings (`'empty'`,
1404
- * `'selected-body'`, `'unselected-body'`) and the `kind:` forms resolve from
1405
- * `bodyTarget` / `bodyKind` and never see it.
1724
+ * The chip form of a single gesture spec, or `undefined` when the spec has
1725
+ * none.
1406
1726
  *
1407
- * A predicate declares which it is with `readsAffordance`; one that declares
1408
- * nothing, which only untyped code can write, counts as not reading the hit.
1409
- */
1410
- declare function targetConsultsAffordance(specTarget: TargetSpec | undefined): boolean;
1411
- /** The first of {@link matchSorted}'s matches, or null when nothing matches. */
1412
- declare function matchBest(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>): MatchResult | null;
1413
- /**
1414
- * Find every binding that matches the event, sorted by precedence
1415
- * (best → worst). Ordering rules:
1727
+ * - A spec's `key` may list spellings of one keycap (`['[', '{']` — the
1728
+ * shifted bracket reports as `'{'`). The first is the shortcut's name.
1729
+ * - A modifier declared `'optional'` matches held or unheld, so it isn't
1730
+ * part of what the user has to press.
1416
1731
  *
1417
- * - Scopes by priority: hotkey > active > ambient.
1418
- * - Within a scope, more-specific bindings beat less-specific (see
1419
- * `specificity()`).
1420
- * - Among same-specificity bindings, first-declared wins.
1421
- */
1422
- declare function matchSorted(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, warn?: (message: string) => void): MatchResult[];
1423
- /** {@link matchSorted}, plus what an exclusive claim on the event barred. */
1424
- interface MatchOutcome {
1425
- matches: MatchResult[];
1426
- /** Bindings whose spec matched the event but which the claim barred, in
1427
- * the same order `matches` uses. Filled only when `collectBarred` is set. */
1428
- barred: MatchResult[];
1429
- /** Owner of the exclusive claim, when one applied. */
1430
- claimOwner?: string;
1431
- }
1432
- /**
1433
- * {@link matchSorted} that can also report the bindings an exclusive claim
1434
- * kept out. Collecting them costs a spec match per barred binding, so it is
1435
- * opt-in — the dispatcher asks only when it is recording.
1732
+ * Non-keyboard specs (drag, wheel, click) have no chip form. Neither does a
1733
+ * *required* `ctrl` or `meta`: the display shape carries only `mod`.
1436
1734
  */
1437
- declare function matchSortedWithBarred(e: InputEvent, bindings: readonly ScopedBinding[], isMac: boolean, engagedChannels?: ReadonlySet<string>, opts?: {
1438
- collectBarred?: boolean;
1439
- warn?: (message: string) => void;
1440
- }): MatchOutcome;
1441
-
1735
+ declare function keySpecShortcut(spec: GestureSpec): ActionShortcut | undefined;
1442
1736
  /**
1443
- * How one input was resolved: every binding that matched it, what each filter
1444
- * dropped, the order the rest were ranked in and why, and what the walk down
1445
- * that order did. The dispatcher fills one per input in dev builds, and
1446
- * `Dispatcher.explain` fills one on request without invoking anything.
1737
+ * Every keyboard shortcut an action answers to, in declaration order.
1738
+ * Specs `keySpecShortcut` has no chip for are skipped, and bindings it leaves
1739
+ * identical are emitted once; an action bound only to those returns empty.
1447
1740
  *
1448
- * Plain data — no functions, no live objects — so it can be kept in a trace
1449
- * buffer, serialized, or built by hand as a fixture.
1741
+ * With `params` — an `ActionItem`'s — only the bindings that pass those same
1742
+ * values count, so each variant of `flip` gets its own key.
1450
1743
  */
1451
- interface DispatchRecord {
1452
- kind: 'dispatch';
1453
- ts: number;
1454
- input: DispatchRecordInput;
1455
- /** Every binding whose spec matched the input, in matcher order, including
1456
- * those a filter then dropped. */
1457
- matched: readonly RecordCandidate[];
1458
- dropped: readonly DroppedCandidate[];
1459
- /** The candidates that survived both filters, best first. */
1460
- ranked: readonly RankedCandidate[];
1461
- /** True when nothing was invoked: the record answers "what would happen". */
1462
- predicted: boolean;
1463
- /** Action id of the candidate that fired (or would), else `null`. */
1464
- fired: string | null;
1465
- outcome: 'handled' | 'unhandled';
1466
- }
1467
- /** The input a {@link DispatchRecord} describes, reduced to plain data. */
1468
- interface DispatchRecordInput {
1469
- /** `InputEvent.kind`: `'key'`, `'pointerdown'`, `'wheel'`, … */
1470
- eventKind: string;
1471
- /** For `key` / `key-held` events, the key id (`'Escape'`, `' '`, `'a'`). */
1472
- key?: string;
1473
- modifiers: {
1474
- alt: boolean;
1475
- ctrl: boolean;
1476
- meta: boolean;
1477
- shift: boolean;
1478
- };
1479
- /** The view the input was routed to; `null` for the surface's own camera. */
1480
- viewId: string | null;
1481
- /** World point, for events that carry one. */
1482
- world?: {
1483
- x: number;
1484
- y: number;
1485
- };
1486
- /** Active mode id, when the host supplies a rule context. */
1487
- mode?: string;
1488
- }
1489
- /** One matched binding. */
1490
- interface RecordCandidate {
1491
- actionId: string;
1492
- /** The binding's spec in route grammar — one per arg alternative. */
1493
- routes: readonly string[];
1494
- scope: BindingScope;
1495
- /** The tool or contribution that declared the binding; `null` for an action's own binding. */
1496
- ownerId: string | null;
1497
- /** Whether the binding's `views` names the routed view. */
1498
- namesView: boolean;
1499
- /** `specificity(spec)`: target, modifiers, phase, exact. */
1500
- specificity: readonly [number, number, number, number];
1501
- /** The action's `eligible` rule as text, when it declares one. */
1502
- eligible?: string;
1503
- }
1504
- /** A matched candidate a filter removed before ranking, and which filter. */
1505
- type DroppedCandidate =
1506
- /** An exclusive claim on the input barred bindings that do not read the
1507
- * claim's affordance. */
1508
- {
1509
- candidate: RecordCandidate;
1510
- filter: 'claim';
1511
- owner?: string;
1512
- }
1513
- /** The action's `eligible` rule does not hold. */
1514
- | {
1515
- candidate: RecordCandidate;
1516
- filter: 'ineligible';
1517
- rule: string;
1518
- };
1519
- /** One field of the `specificity` tuple, in the order it is compared. */
1520
- type SpecificityPart = 'target' | 'mods' | 'phase' | 'exact';
1521
- /** The ranking step that put a candidate below the one before it. */
1522
- type PlacedBy = {
1523
- step: 'first';
1524
- }
1525
- /** The one above names the routed view; this one does not. */
1526
- | {
1527
- step: 'view';
1528
- }
1529
- /** The one above sits in a higher scope tier. */
1530
- | {
1531
- step: 'tier';
1532
- }
1533
- /** Same tier; the one above is more specific in `part`. */
1534
- | {
1535
- step: 'specificity';
1536
- part: SpecificityPart;
1537
- }
1538
- /** Tied; the one above is gated by an `eligible` rule that holds. */
1539
- | {
1540
- step: 'context';
1541
- }
1542
- /** Tied on everything; registration order. */
1543
- | {
1544
- step: 'order';
1545
- };
1546
- /** What the walk did with one ranked candidate. */
1547
- type WalkStep = {
1548
- kind: 'fired';
1744
+ declare function actionShortcuts(action: Action, params?: Readonly<Record<string, unknown>>): readonly ActionShortcut[];
1745
+
1746
+ /**
1747
+ * Finding actions by tag: gathering the ones that carry it, and a person's
1748
+ * search matching it as it matches a label.
1749
+ */
1750
+
1751
+ /** The actions carrying `tag`, themselves or on any variant, in
1752
+ * the order given. Drags included, which have no entries to show. */
1753
+ declare function actionsTagged(actions: readonly Action[], tag: string): Action[];
1754
+ /** The entries carrying `tag`: what a bar or a menu built from
1755
+ * a tag shows. A variant that does not carry it is left out. */
1756
+ declare function taggedActionItems(actions: readonly Action[], tag: string): ActionItem[];
1757
+ /** Whether `item` answers a search for `query`, by its palette title or
1758
+ * by a tag, and which. */
1759
+ declare function matchActionItem(item: ActionItem, query: string): QueryMatch | null;
1760
+
1761
+ /**
1762
+ * Palette-side evaluation of `Action.enabled`. The dispatcher calls the
1763
+ * predicate raw as a gate; this is the surface that also enforces the
1764
+ * contract the predicate's JSDoc states — no throwing, under 4ms in dev.
1765
+ */
1766
+
1767
+ /**
1768
+ * Result of evaluating an Action's `enabled` predicate.
1769
+ */
1770
+ interface ActionEnabledResult {
1771
+ enabled: boolean;
1772
+ reason?: ActionDisabledReason;
1549
1773
  }
1550
- /** Predicted records only: this candidate would fire. Nothing was started,
1551
- * so an ongoing action here may still bail at `start()` for real. */
1552
- | {
1553
- kind: 'would-fire';
1774
+ /**
1775
+ * Safely evaluate an action's `enabled` predicate. Returns `{enabled: true}`
1776
+ * when no predicate is supplied. Catches throws and treats them as disabled
1777
+ * with reason `'(predicate threw)'`. In dev mode, warns once per action id
1778
+ * when a single call exceeds 4ms (single frame at 240fps — generous; real
1779
+ * predicates should be sub-millisecond).
1780
+ */
1781
+ declare function evaluateEnabled(action: Action, deps?: ActionDeps, at?: {
1782
+ x: number;
1783
+ y: number;
1784
+ }): ActionEnabledResult;
1785
+ /**
1786
+ * Safely evaluate an action's `active` predicate. `undefined` for an action
1787
+ * with no pressed state; a predicate that throws reads as not active.
1788
+ */
1789
+ declare function evaluateActive(action: Action, deps?: ActionDeps): boolean | undefined;
1790
+
1791
+ /**
1792
+ * The dep registry: named "live source" thunks. Actions declare
1793
+ * `requires: ['selection']`; the dispatcher calls `registry.get('selection')`
1794
+ * at invocation time to build a typed Deps bag.
1795
+ */
1796
+
1797
+ /** Holds the live sources an action's declared dependencies resolve to.
1798
+ * Sources are thunks, read at invocation time, so an action never captures
1799
+ * stale state. */
1800
+ interface DepRegistry {
1801
+ /** Adds a live source for `name` over any already there. `below` answers
1802
+ * what the registry would without this source, so one can handle part of
1803
+ * a dep and hand the rest to the default it displaced. */
1804
+ register<K extends DepName>(name: K, source: DepSource<K>): () => void;
1805
+ get<K extends DepName>(name: K): DepSchema[K] | undefined;
1806
+ /** Hear that what the registry answers may have changed: a source was
1807
+ * registered or released anywhere in its tree, or an owner called
1808
+ * {@link DepRegistry.changed}. Returns the unsubscribe. A hand-built
1809
+ * registry whose answers never change leaves this and `changed` out. */
1810
+ subscribe?(listener: () => void): () => void;
1811
+ /** Say that a source's value changed. A source is a thunk read on demand,
1812
+ * so its owner is the only one who knows; chrome showing an action's
1813
+ * `enabled` or `active` state re-reads on this. */
1814
+ changed?(): void;
1815
+ /** Every name a `get` here would answer, sorted, with where its live
1816
+ * source sits. For inspection; a hand-built registry may leave it out. */
1817
+ list?(): readonly DepListing[];
1818
+ }
1819
+ /** A live source for dep `K`: a thunk read at invocation time. `below` reads
1820
+ * the source this one displaced, under it in its scope or in a scope above. */
1821
+ type DepSource<K extends DepName> = (below: () => DepSchema[K] | undefined) => DepSchema[K];
1822
+ /** One name a dep registry answers, as {@link DepRegistry.list} reports it. */
1823
+ interface DepListing {
1824
+ name: string;
1825
+ /** How many scopes above the asking one the live source was registered: 0 for its own. */
1826
+ depth: number;
1827
+ /** How many sources that scope holds under the name, the live one included. */
1828
+ stacked: number;
1554
1829
  }
1555
- /** `enabled()` returned this reason. */
1556
- | {
1557
- kind: 'declined';
1558
- reason: string;
1830
+ /** One node of the dep tree: the sources registered at it, over its parent's.
1831
+ * Each name holds a stack of sources, newest live, so a displaced source is
1832
+ * restored when the one above it leaves. */
1833
+ declare class DepNode extends ScopeNode<DepNode> {
1834
+ private readonly sources;
1835
+ readonly registry: DepRegistry;
1836
+ constructor(parent: DepNode | null, descends: boolean);
1559
1837
  }
1560
- /** An ongoing action's `start()` returned an empty handle. */
1561
- | {
1562
- kind: 'empty-handle';
1838
+ /**
1839
+ * A stock dep registry with no parent: what `<DepRegistryProvider>` mounts,
1840
+ * for a dispatcher driven without React. Sources registered under one name
1841
+ * stack, newest live, and releasing one restores the one beneath it.
1842
+ */
1843
+ declare function createDepRegistry(): DepRegistry;
1844
+
1845
+ /**
1846
+ * Shared dep-bag builder — the single implementation behind both the gesture
1847
+ * dispatcher's per-invocation `buildDeps` and `ActionsRegistry.trigger`'s
1848
+ * `requires` branch. Reads the action's declared `requires`, resolves each
1849
+ * name against the `DepRegistry`, and (dev builds only) wraps the bag in a
1850
+ * Proxy that warns on undeclared reads.
1851
+ */
1852
+
1853
+ /** Build the deps bag for an action from its declared `requires`. */
1854
+ declare function buildDepsFromRequires(action: Action, depRegistry: DepRegistry): ActionDeps;
1855
+
1856
+ /** Hotkey-slot trigger key. The slot is engaged while this key is held —
1857
+ * hence "hotkey": active as long as the key is hot. Omitted means the tool
1858
+ * is not eligible for the hotkey slot.
1859
+ *
1860
+ * The named keys map to their DOM names (`'space'` is `' '`, `'ctrl'` is
1861
+ * `'Control'`). Anything else is a `KeyboardEvent.key` value, matched
1862
+ * case-insensitively as every key spec is — so `'o'` holds on O. */
1863
+ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift' | (string & {});
1864
+ /** Presentation metadata for tool palettes / menus. Optional on every
1865
+ * tool — consumers that render a palette (`<ToolPalette>`) read these
1866
+ * fields to display the tool; consumers that don't can ignore them.
1867
+ *
1868
+ * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
1869
+ * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1870
+ interface ToolPresentation {
1871
+ /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1872
+ label?: string;
1873
+ /** The tool's icon, by qualified name: `icon('rect')` in the package that draws it. */
1874
+ icon?: _weasel_js_icons.IconRef;
1875
+ /** Palette grouping key. Tools sharing a group render contiguously
1876
+ * with separators between groups. Free-form string; the kit
1877
+ * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
1878
+ group?: string;
1879
+ /** Display override for the keyboard shortcut. When omitted the palette
1880
+ * derives one from `Tool.keybinding` via its own formatter. */
1881
+ shortcut?: string;
1882
+ /** Keep the tool off palettes — for a tool that is only ever held, like a
1883
+ * scrub engaged by its `hotkey`. It stays registered and selectable by
1884
+ * id; only the palette leaves it out. */
1885
+ hide?: boolean;
1563
1886
  }
1564
- /** An ongoing action bound to a press-time `pointerDown` spec; refused. */
1565
- | {
1566
- kind: 'misbound';
1887
+ /**
1888
+ * When an entry's bindings are live. A set, not one value: the hand tool is
1889
+ * palette-selectable AND engaged by holding space, and both hold at once.
1890
+ */
1891
+ interface Eligibility {
1892
+ /** Selectable as the focused entry — exclusive, one at a time. */
1893
+ focus?: boolean;
1894
+ /** Also live while this key is held. */
1895
+ offhand?: HotkeyTrigger;
1896
+ /** Live regardless of what is focused. */
1897
+ always?: boolean;
1898
+ /** Live only for input this entry's own affordances produced. */
1899
+ claimed?: boolean;
1900
+ /** Modality filter, applied wherever it would otherwise be live. */
1901
+ capabilities?: CapabilityTag[];
1567
1902
  }
1568
- /** Its action was already tried higher up; each runs at most once. */
1569
- | {
1570
- kind: 'duplicate';
1571
- } | {
1572
- kind: 'no-such-action';
1903
+ /**
1904
+ * Where an entry's overlay sits in the layer stack, relative to the
1905
+ * selection chrome. `'top'` is the default and renders above everything;
1906
+ * the other two exist for chrome that belongs under the selection handles
1907
+ * (a snap-target highlight, say). With no selection overlay in the stack,
1908
+ * all three collapse to `'top'`.
1909
+ */
1910
+ type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
1911
+ /** Dep sources an entry provides while it is installed, by dep name. */
1912
+ type ContributionDeps = {
1913
+ [K in DepName]?: () => DepSchema[K];
1914
+ };
1915
+ /**
1916
+ * What routing reads off an entry: when its bindings are live, which they
1917
+ * are, the actions they name, and the deps those actions read.
1918
+ */
1919
+ interface ContributionRouting {
1920
+ id: string;
1921
+ eligibility: Eligibility;
1922
+ bindings?: GestureBinding[];
1923
+ actions?: Action[];
1924
+ /** Menus this entry declares: always, for an ambient entry; while active, for a tool. */
1925
+ menus?: MenuDef[];
1926
+ /** Dep sources this entry provides while it is installed. A host installs
1927
+ * them for as long as the entry is registered, whatever its eligibility. */
1928
+ deps?: ContributionDeps;
1573
1929
  }
1574
- /** Below the winner; never asked. */
1575
- | {
1576
- kind: 'not-asked';
1930
+ /**
1931
+ * What an entry draws and how a palette shows it. None of it reaches the
1932
+ * dispatcher.
1933
+ */
1934
+ interface ContributionChrome<TOverlay = KernelOverlay> {
1935
+ /** One layer, or several composed in the given order.
1936
+ *
1937
+ * Routing never reads a layer — it collects the live ones and hands them
1938
+ * back in scope order. `TOverlay` is what the consuming kernel draws;
1939
+ * `@weasel-js/core` binds it to `RenderLayer<unknown>`. */
1940
+ overlay?: TOverlay | TOverlay[];
1941
+ /** Defaults to `'top'`. Applies to every layer in `overlay`. */
1942
+ overlayPosition?: OverlayPosition;
1943
+ presentation?: ToolPresentation;
1577
1944
  }
1578
- /** Below the winner, asked anyway (`evaluateShadowed`), and it passed. */
1579
- | {
1580
- kind: 'outranked';
1581
- };
1582
- /** A candidate that survived the filters: why it sits where it does, and
1583
- * what the walk did with it. */
1584
- interface RankedCandidate {
1585
- candidate: RecordCandidate;
1586
- placedBy: PlacedBy;
1587
- walk: WalkStep;
1945
+ /**
1946
+ * A registry entry: what it contributes, and when it is eligible. Every role
1947
+ * is optional and independent — an entry that only routes input declares only
1948
+ * `bindings` and `actions`.
1949
+ */
1950
+ interface Contribution<TOverlay = KernelOverlay> extends ContributionRouting, ContributionChrome<TOverlay> {
1951
+ /** Reflection escape hatch — the authored form, when there was one. */
1952
+ def?: unknown;
1588
1953
  }
1589
1954
 
1590
1955
  /**
@@ -1625,6 +1990,19 @@ interface DispatcherContext {
1625
1990
  * reads as the root.
1626
1991
  */
1627
1992
  viewId?: string | null;
1993
+ /**
1994
+ * For a key from the page: whether this surface holds the keys, and whether
1995
+ * the pointer is over it. Bindings with `opts.over` are live only while it
1996
+ * is, the rest only while it holds them. Absent, every binding is live.
1997
+ */
1998
+ keyReach?: KeyReach;
1999
+ /** Where the dispatcher records how it resolved each input. Absent records nothing. */
2000
+ trace?: DispatchTrace;
2001
+ }
2002
+ /** Which of a surface's key bindings a key from the page reaches. */
2003
+ interface KeyReach {
2004
+ held: boolean;
2005
+ over: boolean;
1628
2006
  }
1629
2007
  /**
1630
2008
  * Handle returned by `Dispatcher.beginUiOngoing()` for driving an
@@ -1654,6 +2032,8 @@ interface ResolveOnlyResult {
1654
2032
  scope: BindingScope;
1655
2033
  /** Tool id owning the winning binding; `null` for ambient action bindings. */
1656
2034
  ownerId: string | null;
2035
+ /** The params the winning binding would start the action with, resolved now. */
2036
+ params?: Record<string, unknown>;
1657
2037
  }
1658
2038
  /**
1659
2039
  * One candidate from `Dispatcher.resolveAll` — a binding that matched the
@@ -1830,31 +2210,6 @@ interface Dispatcher {
1830
2210
  beginUiOngoing(actionId: string, deps: ActionDeps, params?: Record<string, unknown>): UiOngoingControl | null;
1831
2211
  }
1832
2212
 
1833
- /**
1834
- * Dev-only trace of what the dispatcher did: one record per resolved input and
1835
- * one per mode switch, in a rolling buffer the dev tools read.
1836
- */
1837
-
1838
- /** One entry per "mode change" — a kit state transition that isn't a
1839
- * dispatched input event but explains why an input was (or wasn't) handled
1840
- * later. Pushed via `recordModeSwitch()`. */
1841
- interface ModeSwitchLogEntry {
1842
- kind: 'mode';
1843
- ts: number;
1844
- /** Logical mode name, e.g. `'editAnchors.editingId'`. */
1845
- mode: string;
1846
- from: string | null;
1847
- to: string | null;
1848
- /** Optional free-form context — `'enterPathEdit'`, `'exitPathEdit'`,
1849
- * `'selection-dropped target'`, etc. */
1850
- detail?: string;
1851
- }
1852
- /** A trace entry: one {@link DispatchRecord} per resolved input, or a mode switch. */
1853
- type TraceLogEntry = DispatchRecord | ModeSwitchLogEntry;
1854
- /** Push a mode-switch record into the trace log. Safe to call from
1855
- * anywhere; no-ops outside DEV. */
1856
- declare function recordModeSwitch(mode: string, from: string | null, to: string | null, detail?: string): void;
1857
-
1858
2213
  /**
1859
2214
  * The slot an in-flight ongoing handle is keyed by:
1860
2215
  * - `key-held`: `key-held-<code>` (`key-held-Space`), so two keys that share a
@@ -1864,6 +2219,7 @@ declare function recordModeSwitch(mode: string, from: string | null, to: string
1864
2219
  * with no `pointerId` (a synthesized probe, a programmatic drag, most tests)
1865
2220
  * keys to `pointer-mouse`.
1866
2221
  * - `multitouch`: `multitouch-<fingers>`.
2222
+ * - `carry`: `carry-<pointerId>`, keyed as a pointer is.
1867
2223
  * - any other kind that starts an ongoing invoker: `ongoing-<kind>`.
1868
2224
  */
1869
2225
 
@@ -1911,18 +2267,15 @@ declare function createDispatcher(opts?: {
1911
2267
  }): Dispatcher;
1912
2268
 
1913
2269
  /**
1914
- * @experimental
1915
2270
  * Partial override or full descriptor passed via `<SceneCanvas actions={...}>`.
1916
2271
  * `null` disables a default at this id.
1917
2272
  */
1918
2273
  type ActionEntry = null | Partial<Action> | Action;
1919
2274
  /**
1920
- * @experimental
1921
2275
  * Shape of the `actions` prop on `<SceneCanvas>`. `null` disables all defaults.
1922
2276
  */
1923
2277
  type ActionsProp = null | Record<string, ActionEntry>;
1924
2278
  /**
1925
- * @experimental
1926
2279
  * Imperative API exposed by `useActionsRegistry()`.
1927
2280
  */
1928
2281
  interface ActionsRegistry {
@@ -1943,14 +2296,25 @@ interface ActionsRegistry {
1943
2296
  * apply to whichever descriptor is live for it, registered before the
1944
2297
  * tuning or after. Returns a release. See {@link ActionTuning}. */
1945
2298
  tune(id: string, tuning: ActionTuning): () => void;
2299
+ /** The params an invocation of `id` from this scope starts with when it
2300
+ * passes `given`: every tuning's params, `given` laid over them. What a
2301
+ * tool's chrome reads to show what the action it binds will do. */
2302
+ paramsOf(id: string, given?: Record<string, unknown>): Record<string, unknown>;
1946
2303
  list(): readonly Action[];
1947
2304
  /** Fire an immediate-invoker action by id. The optional `params` arg is
1948
2305
  * forwarded to `ImmediateInvoker.run` as its second argument — use it for
1949
2306
  * parametric actions (e.g. `trigger('tool.activate', { toolId: 'rect' })`).
1950
2307
  * Returns whether it ran one: `false` for an unknown id, for an action
1951
2308
  * whose `enabled` refuses, and for an ongoing-invoker action, which
1952
- * `trigger` cannot reach (see `begin`). `enabled` is asked with no point. */
2309
+ * `trigger` cannot reach (see `begin`). `enabled` is asked at
2310
+ * `{ x: params.worldX, y: params.worldY }` when both are numbers, and with
2311
+ * no point otherwise. */
1953
2312
  trigger(id: string, params?: Record<string, unknown>): boolean;
2313
+ /** Show what firing `id` with `params` would do, by its `Action.preview`,
2314
+ * and return what takes it down; calling that twice is harmless. Shows
2315
+ * nothing for an unknown id, one with no preview, and one whose `enabled`
2316
+ * refuses, asked as `trigger` asks it. */
2317
+ preview(id: string, params?: Record<string, unknown>): () => void;
1954
2318
  /**
1955
2319
  * Subscribe to registry mutations. The callback fires after any
1956
2320
  * `register`/`unregister` that changes the version. Returns an
@@ -2062,7 +2426,7 @@ declare const isSelectedBody: BodyPredicate;
2062
2426
  declare const isUnselectedBody: BodyPredicate;
2063
2427
  /** Empty-canvas hit — no node beneath cursor. */
2064
2428
  declare const isEmpty: BodyPredicate;
2065
- /** Any corner resize handle — top-left/top-right/bottom-left/bottom-right. */
2429
+ /** Any resize handle: a corner (`handle:top-left`, …) or an edge (`handle:top`, …). */
2066
2430
  declare const isResizeHandle: TargetPredicate & {
2067
2431
  readsAffordance: true;
2068
2432
  };
@@ -2078,6 +2442,10 @@ declare const isAnchor: TargetPredicate & {
2078
2442
  declare const isControlHandle: TargetPredicate & {
2079
2443
  readsAffordance: true;
2080
2444
  };
2445
+ /** A corner-shape widget, set inside a corner anchor of the edited path. */
2446
+ declare const isCornerWidget: TargetPredicate & {
2447
+ readsAffordance: true;
2448
+ };
2081
2449
  /** Any of the three anchor-editing affordances (anchor + both controls). */
2082
2450
  declare const isAnchorOrControl: TargetPredicate & {
2083
2451
  readsAffordance: true;
@@ -2086,7 +2454,13 @@ declare const isAnchorOrControl: TargetPredicate & {
2086
2454
  declare const isSegment: TargetPredicate & {
2087
2455
  readsAffordance: true;
2088
2456
  };
2089
- /** Anything path edit puts on the path: an anchor, a control, or a segment. */
2457
+ /** The grip midway along a curved segment's balance line, `balance:N`, where
2458
+ * N is the flat index of the anchor the segment starts at. */
2459
+ declare const isBalanceGrip: TargetPredicate & {
2460
+ readsAffordance: true;
2461
+ };
2462
+ /** Anything path edit puts on the path: an anchor, a control, a segment, a
2463
+ * segment's balance grip, or a corner widget. */
2090
2464
  declare const isPathEditPart: TargetPredicate & {
2091
2465
  readsAffordance: true;
2092
2466
  };
@@ -2164,6 +2538,29 @@ interface LongPressOptions {
2164
2538
  /** A store holding the press currently being held, or none. Pass it as `LongPressOptions.state`. */
2165
2539
  declare function createLongPressStore(): LongPressStore;
2166
2540
 
2541
+ /**
2542
+ * The `gestures` kit settings area. Declared here rather than in core because
2543
+ * the dispatcher reads it, and routing sits below core.
2544
+ */
2545
+ /** Pointer gesture timings and travel; each falls back to its constant in `@weasel-js/gestures`. */
2546
+ interface GestureKitSettings {
2547
+ /** Longest gap between the clicks of a double click, in ms. */
2548
+ doubleClickMs: number;
2549
+ /** Farthest apart the clicks of a double click may land, in CSS px. */
2550
+ doubleClickPx: number;
2551
+ /** How long a touch or pen press is held to fire a long-press, in ms. */
2552
+ longPressMs: number;
2553
+ /** Farthest a multitouch tap's fingers may travel, in CSS px. */
2554
+ tapPx: number;
2555
+ /** Travel that makes a press a drag, in CSS px. */
2556
+ dragPx: number;
2557
+ }
2558
+ declare module '@weasel-js/react' {
2559
+ interface KitSettings {
2560
+ gestures: GestureKitSettings;
2561
+ }
2562
+ }
2563
+
2167
2564
  /**
2168
2565
  * The DOM half of a drag: one pointer, held down, from press to release.
2169
2566
  *
@@ -2221,6 +2618,60 @@ interface PointerSession {
2221
2618
  */
2222
2619
  declare function openPointerSession(origin: Element, down: PointerEvent | React.PointerEvent, cb: PointerSessionCallbacks, opts?: PointerSessionOptions): PointerSession;
2223
2620
 
2621
+ /**
2622
+ * A press that begins on one element and is carried over another: the in-app
2623
+ * sibling of an OS drag. The source opens the carry; every dispatcher with a
2624
+ * `carry` channel registers its element as a surface, and the one under the
2625
+ * pointer dispatches the carry as `carry` events through its bindings.
2626
+ */
2627
+
2628
+ /** What a carry brings. `type` is what a `carry` spec's `types` globs filter. */
2629
+ interface CarryPayload {
2630
+ type: string;
2631
+ value: unknown;
2632
+ }
2633
+ /** The pointer facts a surface reads off each step of a carry. */
2634
+ interface CarryPointer {
2635
+ clientX: number;
2636
+ clientY: number;
2637
+ altKey: boolean;
2638
+ ctrlKey: boolean;
2639
+ metaKey: boolean;
2640
+ shiftKey: boolean;
2641
+ }
2642
+ /** What a carry did on a surface: came over it, moved on it, was released on
2643
+ * it, or left it — off it, or canceled while over it. */
2644
+ type CarryStage = 'enter' | 'move' | 'drop' | 'leave';
2645
+ /** What {@link startCarry} reports to its source. Exactly one of `onEnd` /
2646
+ * `onCancel` fires. */
2647
+ interface CarryCallbacks {
2648
+ /** Every move of the carrying pointer. */
2649
+ onMove?: (e: PointerEvent) => void;
2650
+ /** The pointer was released. `taken` is whether a binding on the surface
2651
+ * under it took the carry. */
2652
+ onEnd?: (result: {
2653
+ taken: boolean;
2654
+ event: PointerEvent;
2655
+ }) => void;
2656
+ /** The carry ended without a release. Any surface it was over has had its leave. */
2657
+ onCancel?: (reason: PointerSessionCancelReason) => void;
2658
+ }
2659
+ /** A live carry, opened by {@link startCarry}. */
2660
+ interface Carry {
2661
+ readonly pointerId: number;
2662
+ readonly active: boolean;
2663
+ /** End the carry now, as a cancel. Idempotent. */
2664
+ cancel(): void;
2665
+ }
2666
+ /**
2667
+ * Carry `payload` from the press `down` on `origin` until the pointer is
2668
+ * released or the press is canceled. While it moves, the surface under it
2669
+ * hears the carry come over, move, and leave; a release over one is a drop
2670
+ * there. The press is captured to `origin` as any drag is, so nothing the
2671
+ * pointer passes over sees it as a press of its own.
2672
+ */
2673
+ declare function startCarry(origin: Element, down: PointerEvent | React.PointerEvent, payload: CarryPayload, cb?: CarryCallbacks): Carry;
2674
+
2224
2675
  /** What the registry knows at dispatch time. */
2225
2676
  interface EligibilityState {
2226
2677
  focusedId: string | null;
@@ -2401,6 +2852,8 @@ interface ToolDef<TOverlay = KernelOverlay> {
2401
2852
  * binding that points at an action id nothing registered.
2402
2853
  */
2403
2854
  actions?: Action[];
2855
+ /** Menus this tool adds while it is active. */
2856
+ menus?: MenuDef[];
2404
2857
  /** Hook name as exported from the kit barrel (e.g. `'useHandTool'`).
2405
2858
  * Set by built-in hooks for inspector / debugging. Consumer-authored
2406
2859
  * tools may set this to surface their hook name; omitted is fine.
@@ -2457,6 +2910,29 @@ interface ToolDef<TOverlay = KernelOverlay> {
2457
2910
  */
2458
2911
  declare function defineTool<TOverlay = KernelOverlay>(def: ToolDef<TOverlay>): Tool<TOverlay>;
2459
2912
 
2913
+ /**
2914
+ * Tool sets: a named group of tool ids, the unit a consumer opts into whole.
2915
+ * A set names ids, never tools, because a tool is built by a hook that holds
2916
+ * its state; whatever mounts the set supplies the tool for each id.
2917
+ */
2918
+ /** A named group of tool ids. A member is an id or another set, whose ids it takes in order. */
2919
+ interface ToolSet<TId extends string = string> {
2920
+ readonly id: string;
2921
+ readonly members: readonly (TId | ToolSet<TId>)[];
2922
+ }
2923
+ /** One entry of a list of tools to mount: an id, or a set of them. */
2924
+ type ToolEntry<TId extends string = string> = TId | ToolSet<TId>;
2925
+ /** A set named `id` of `members`, in palette order. */
2926
+ declare function toolSet<TId extends string>(id: string, members: readonly ToolEntry<TId>[]): ToolSet<TId>;
2927
+ /** Whether `entry` is a set rather than a single id. */
2928
+ declare function isToolSet<TId extends string>(entry: ToolEntry<TId>): entry is ToolSet<TId>;
2929
+ /** Every id `entries` name, sets opened in place, each id once at its first place. */
2930
+ declare function toolIdsOf<TId extends string>(entries: readonly ToolEntry<TId>[]): TId[];
2931
+ /** `set` less every id `omit` names, sets opened. Keeps `set`'s id with a `-without` suffix. */
2932
+ declare function withoutTools<TId extends string>(set: ToolSet<TId>, ...omit: readonly ToolEntry<TId>[]): ToolSet<TId>;
2933
+ /** Whether `set` names `id`, directly or through a set inside it. */
2934
+ declare function toolSetHas<TId extends string>(set: ToolSet<TId>, id: string): boolean;
2935
+
2460
2936
  /**
2461
2937
  * One row in the route registry — a single `GestureBinding` on one tool,
2462
2938
  * flattened into the route grammar's vocabulary so the inspector, the
@@ -2607,8 +3083,7 @@ interface ToolScopes {
2607
3083
  /**
2608
3084
  * Detect reachable route conflicts in an assembled tool set and report each one.
2609
3085
  *
2610
- * Call it once wherever a tool set is assembled, behind a
2611
- * `process.env.NODE_ENV !== 'production'` guard.
3086
+ * Call it once wherever a tool set is assembled, behind an `isDev()` guard.
2612
3087
  *
2613
3088
  * **Warn, never throw.** A conflict between a consumer's tool and a kit tool
2614
3089
  * is sometimes deliberate, since the loser can still take the gesture when
@@ -2673,6 +3148,30 @@ declare function matchesKeyBinding(e: KeyboardEvent, b: KeyBinding): boolean;
2673
3148
  * React keyboard event as readily as a DOM one. */
2674
3149
  declare function historyKey(e: Pick<KeyboardEvent, 'key' | 'metaKey' | 'ctrlKey' | 'shiftKey' | 'altKey'>): 'undo' | 'redo' | null;
2675
3150
 
3151
+ /**
3152
+ * Key chords a major browser or the desktop under it takes before a page can:
3153
+ * reload, devtools, tab and window commands, menu accelerators. A binding on
3154
+ * one never fires for some users, so kit defaults steer clear of them.
3155
+ *
3156
+ * `mod` is Cmd on macOS and Ctrl elsewhere, as in a `KeySpec`. The list is
3157
+ * the union across Chrome, Safari, Firefox and Edge on macOS, Windows and
3158
+ * Linux, and errs toward listing a chord.
3159
+ */
3160
+
3161
+ /** One reserved chord, and what takes it. */
3162
+ interface ReservedChord {
3163
+ key: string;
3164
+ mod?: boolean;
3165
+ alt?: boolean;
3166
+ shift?: boolean;
3167
+ /** Who takes it, for a message. */
3168
+ use: string;
3169
+ }
3170
+ /** Every chord the module's header describes, one entry per key. */
3171
+ declare const BROWSER_RESERVED_CHORDS: readonly ReservedChord[];
3172
+ /** The first reserved chord `spec` can fire on, or `null` when none. */
3173
+ declare function browserReservedChord(spec: KeySpec): ReservedChord | null;
3174
+
2676
3175
  /**
2677
3176
  * Typed keys for the gesture/behavior scratch store, `ctx.scratch`.
2678
3177
  * `ScratchKey<T>` carries its payload type at the typing layer; at runtime
@@ -2728,12 +3227,21 @@ declare function deleteScratch<T>(store: ScratchStore, key: ScratchKey<T>): bool
2728
3227
  *
2729
3228
  * Output shape: **files first (source order), then strings (item order)**.
2730
3229
  * For paste (`itemsFromClipboardData`) string flavors are emitted in
2731
- * `INGEST_STRING_MIMES` order: text/plain, text/html, text/uri-list.
3230
+ * `INGEST_STRING_MIMES` order: text/plain, text/html, text/uri-list, then
3231
+ * the admitted ones in the order they were first admitted.
2732
3232
  */
2733
3233
 
2734
- /** The string flavors the kit normalizes. Everything else (browser-internal
2735
- * custom flavors, etc.) is dropped at materialization. */
3234
+ /** The string flavors the kit always keeps. Any other is dropped at
3235
+ * materialization unless {@link admitIngestMime} has admitted it. */
2736
3236
  declare const INGEST_STRING_MIMES: ReadonlySet<string>;
3237
+ /**
3238
+ * Keep string flavors of `mime` on drops, drags and pastes as well as the
3239
+ * {@link INGEST_STRING_MIMES}, so content from the app's own UI can travel
3240
+ * under a type of its own. Returns a release; the flavor is kept while any
3241
+ * admission of it is unreleased. Core admits each exact MIME a registered
3242
+ * content handler names.
3243
+ */
3244
+ declare function admitIngestMime(mime: string): () => void;
2737
3245
  /** Wrap raw `File`s (file picker, `CanvasExtensionApi.ingest`) as
2738
3246
  * `IngestItem`s, normalizing each MIME the same way the drop/paste paths
2739
3247
  * do. Synchronous — `File` objects are already fully owned. */
@@ -2805,15 +3313,15 @@ declare function dlog(namespace: string, ...args: unknown[]): void;
2805
3313
  * (e.g. no-op op batches suppressed). */
2806
3314
  declare function dwarn(namespace: string, ...args: unknown[]): void;
2807
3315
 
2808
- /**
2809
- * @weasel-js/routing — binding-to-action routing.
2810
- *
2811
- * The pure half: no React, no DOM. The React seam that pumps browser events
2812
- * into the dispatcher, and the providers that feed it, live behind
2813
- * `@weasel-js/routing/react`.
2814
- *
2815
- * @packageDocumentation
2816
- */
3316
+ /** The menus live now: ambient defs, then the active tool's. Defs that share
3317
+ * an id become one menu: each contribution's children in turn, with a
3318
+ * separator between contributions. Children stay at their own keys, so the
3319
+ * paths their values are read at do not move; a key a previous contribution
3320
+ * took gets `~n` appended. The first def of an id supplies the schema's
3321
+ * name and the def's `when`. */
3322
+ declare function mergeMenuDefs(defs: readonly OwnedMenuDef[], activeTool: string | null): MenuDef[];
3323
+ /** The first def whose `when` matches `hit`. */
3324
+ declare function pickMenu(defs: readonly MenuDef[], hit: MenuHit): MenuDef | undefined;
2817
3325
 
2818
3326
  /**
2819
3327
  * The dependency schema — the map of dep name → value type that
@@ -2836,6 +3344,10 @@ declare function dwarn(namespace: string, ...args: unknown[]): void;
2836
3344
  interface DepSchema {
2837
3345
  /** Currently active tool id + hotkey-hold stack. */
2838
3346
  activeTool: ActiveToolContextValue;
3347
+ /** The menu host of the `ActionsProvider` in scope. */
3348
+ menuHost?: MenuHost;
3349
+ /** The canvas's dispatch trace, for a dep to record a mode switch into. */
3350
+ dispatchTrace?: DispatchTrace;
2839
3351
  }
2840
3352
  /** Every dep name declared by anything that has merged into {@link DepSchema}. */
2841
3353
  type DepName = keyof DepSchema;
@@ -2846,7 +3358,7 @@ type DepName = keyof DepSchema;
2846
3358
  *
2847
3359
  * ```ts
2848
3360
  * declare module '@weasel-js/core' {
2849
- * interface ActionParamSchema { brushDeleteAnchors: { radiusPx: number } }
3361
+ * interface ActionParamSchema { simplifyBrush: { radiusPx: number } }
2850
3362
  * }
2851
3363
  * ```
2852
3364
  *
@@ -2883,4 +3395,4 @@ type KernelOverlay = OverlaySchema extends {
2883
3395
  overlay: infer O;
2884
3396
  } ? O : unknown;
2885
3397
 
2886
- export { DEFAULT_ALLOWED_CAPABILITIES as $, type ActionsRegistry as A, type ActionVariant as B, type Contribution as C, type DepRegistry as D, type ActionsProp as E, type AnyTool as F, type AssemblyInputs as G, type BindingOpts as H, type BindingScope as I, type BindingSource as J, type KernelOverlay as K, type LongPressOptions as L, type BodyPredicate as M, type NodeId as N, type OverlayPosition as O, type BoundGesture as P, type Bounds as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type BuildRuleCtxArgs as U, COARSE_TARGET_SCALE as V, type Condition as W, type Conflict as X, type ContributionChrome as Y, type ContributionDeps as Z, type ContributionRouting as _, type DepName as a, type TraceLogEntry as a$, DEFAULT_DEVICE_PROFILE as a0, type DebugSink as a1, type DetectedDeviceFacts as a2, type DeviceProfile as a3, type DispatchRecord as a4, type DispatchRecordInput as a5, type DispatcherContext as a6, type DragSample as a7, type DroppedCandidate as a8, type Eligibility as a9, type Point2 as aA, type PointerSession as aB, type PointerSessionCallbacks as aC, type PointerSessionCancelReason as aD, type PointerSessionOptions as aE, type RankedCandidate as aF, type RecordCandidate as aG, type RegistryEntry as aH, type ResizeAnchor as aI, type ResolveAllOptions as aJ, type ResolveOnlyResult as aK, type ResolvedCandidate as aL, type Rule as aM, type ScopedBinding as aN, type ScratchKey as aO, type ScratchStore as aP, type SelectionApi as aQ, type Selector as aR, type SpecificityPart as aS, TOOL_OFFHAND_ID as aT, type ToolDef as aU, type ToolKeybinding as aV, type ToolModifiers as aW, type ToolOffhandBindingSpec as aX, type ToolPresentation as aY, type ToolScopes as aZ, type ToolSlot as a_, type EligibilityState as aa, type GestureBinding as ab, type HandleKind as ac, type HitShape as ad, type HotkeyTrigger as ae, INGEST_STRING_MIMES as af, type ImmediateInvoker as ag, type InvocationCtx as ah, type Invoker as ai, type KeyBinding as aj, type LongPressState as ak, type LongPressStore as al, type MatchOutcome as am, type MatchResult as an, type ModeShortcutHandlers as ao, type ModeSwitchLogEntry as ap, type ModifierState as aq, NEVER as ar, type OngoingHandle as as, type OngoingInvoker as at, type OngoingOverlay as au, type OverlayRole as av, type OverlaySchema as aw, PREDICATE_TARGET as ax, type PendingLongPress as ay, type PlacedBy as az, type DepSchema as b, ruleCanHoldIn as b$, type TunedBinding as b0, type UiOngoingControl as b1, type ViewportGestureKind as b2, type WalkStep as b3, actionBindings as b4, actionItems as b5, assembleEntries as b6, buildDepsFromRequires as b7, buildRouteRegistry as b8, buildRuleCtx as b9, isRotateHandle as bA, isSegment as bB, isSelectedBody as bC, isTool as bD, isUnselectedBody as bE, itemTypesFromDataTransfer as bF, itemsFromClipboardData as bG, itemsFromDataTransfer as bH, itemsFromFiles as bI, liveScope as bJ, makeToolOffhandAction as bK, matchBest as bL, matchSorted as bM, matchSortedWithBarred as bN, matchesKeyBinding as bO, mergeContributions as bP, modeShortcutSpec as bQ, modeShortcuts as bR, multiTouchGestureId as bS, offhandKeyFor as bT, openPointerSession as bU, pointerGestureId as bV, recordModeSwitch as bW, reportRouteConflicts as bX, resolveDeviceProfile as bY, resolveParams as bZ, routesForSpec as b_, buildToolOffhandBindings as ba, clientToCanvas as bb, clientToCanvasRect as bc, createDepRegistry as bd, createDispatcher as be, createLongPressStore as bf, defineTool as bg, deleteScratch as bh, describeRule as bi, dlog as bj, dwarn as bk, evaluate as bl, evaluateActive as bm, evaluateEnabled as bn, findConflicts as bo, getScratch as bp, heldKeySpec as bq, historyKey as br, isAnchor as bs, isAnchorOrControl as bt, isBody as bu, isControlHandle as bv, isDebugEnabled as bw, isEmpty as bx, isPathEditPart as by, isResizeHandle as bz, type Action as c, rulesExclusive as c0, scopeBindings as c1, scratchKey as c2, setScratch as c3, targetConsultsAffordance as c4, tuneAction as c5, validateActionDefaultBinding as c6, validateActionId as c7, 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 ActionParamSchema as t, useActiveToolContext as u, type ActionParams as v, type ActionPresentation as w, type ActionSource as x, type ActionTuning as y, type ActionTunings as z };
3398
+ export { type Bounds as $, type Action as A, type BoundGesture as B, type Contribution as C, type DepRegistry as D, type ActionPresentation as E, type ActionSettings as F, type ActionSettingsState as G, type ActionShortcut as H, type ActionSource as I, type ActionTuning as J, type KernelOverlay as K, type LongPressOptions as L, type MenuHost as M, type NodeId as N, type OverlayPosition as O, type ActionTunings as P, type ActionsProp as Q, type RuleCtx as R, ScopeNode as S, ToolNode as T, type AnyTool as U, type AssemblyInputs as V, BROWSER_RESERVED_CHORDS as W, type BindingOpts as X, type BindingScope as Y, type BindingSource as Z, type BodyPredicate as _, type DepName as a, type PointerSessionCallbacks as a$, type BuildRuleCtxArgs as a0, COARSE_TARGET_SCALE as a1, type Carried as a2, type Carry as a3, type CarryCallbacks as a4, type CarryPayload as a5, type CarryPointer as a6, type CarryStage as a7, type Condition as a8, type Conflict as a9, type KeyReach as aA, type LongPressState as aB, type LongPressStore as aC, type MatchOutcome as aD, type MatchResult as aE, type MenuHit as aF, type MenuOpenRequest as aG, type MenuSources as aH, type MenuWhen as aI, type ModeShortcutHandlers as aJ, type ModeSwitchLogEntry as aK, type ModifierRole as aL, type ModifierRoles as aM, type ModifierState as aN, NEVER as aO, type OngoingHandle as aP, type OngoingInvoker as aQ, type OngoingOverlay as aR, type OpenMenu as aS, type OverlayRole as aT, type OverlaySchema as aU, type OwnedMenuDef as aV, PREDICATE_TARGET as aW, type PendingLongPress as aX, type PlacedBy as aY, type Point2 as aZ, type PointerSession as a_, type ContributionChrome as aa, type ContributionDeps as ab, type ContributionRouting as ac, DEFAULT_ALLOWED_CAPABILITIES as ad, DEFAULT_DEVICE_PROFILE as ae, type DebugSink as af, type DepListing as ag, type DetectedDeviceFacts as ah, type DeviceProfile as ai, type DispatchRecord as aj, type DispatchRecordInput as ak, type DispatcherContext as al, type DragSample as am, type DroppedCandidate as an, type Eligibility as ao, type EligibilityState as ap, type GestureBinding as aq, type GestureKitSettings as ar, type HandleKind as as, type HitShape as at, type HotkeyTrigger as au, INGEST_STRING_MIMES as av, type ImmediateInvoker as aw, type InvocationCtx as ax, type Invoker as ay, type KeyBinding as az, type DepSource as b, evaluateActive as b$, type PointerSessionCancelReason as b0, type PointerSessionOptions as b1, type RankedCandidate as b2, type RecordCandidate as b3, type RegistryEntry as b4, type ReservedChord as b5, type ResizeAnchor as b6, type ResolveAllOptions as b7, type ResolveOnlyResult as b8, type ResolvedCandidate as b9, actionCursor as bA, actionItemTitle as bB, actionItems as bC, actionSettingsParams as bD, actionShortcuts as bE, actionsTagged as bF, admitIngestMime as bG, assembleEntries as bH, browserReservedChord as bI, buildDepsFromRequires as bJ, buildRouteRegistry as bK, buildRuleCtx as bL, buildToolOffhandBindings as bM, clientToCanvas as bN, clientToCanvasRect as bO, createDepRegistry as bP, createDispatchTrace as bQ, createDispatcher as bR, createLongPressStore as bS, createMenuHost as bT, createTraceRing as bU, defineTool as bV, deleteScratch as bW, describeRule as bX, dlog as bY, dwarn as bZ, evaluate as b_, type RoleKeys as ba, type RoleRebinds as bb, type Rule as bc, type ScopedBinding as bd, type ScratchKey as be, type ScratchStore as bf, type SelectionApi as bg, type Selector as bh, type SpecificityPart as bi, TOOL_OFFHAND_ID as bj, type ToolDef as bk, type ToolEntry as bl, type ToolKeybinding as bm, type ToolModifiers as bn, type ToolOffhandBindingSpec as bo, type ToolPresentation as bp, type ToolScopes as bq, type ToolSet as br, type ToolSlot as bs, type TraceLogEntry as bt, type TraceRing as bu, type TunedBinding as bv, type UiOngoingControl as bw, type ViewportGestureKind as bx, type WalkStep as by, actionBindings as bz, type ActionDeps as c, tuneAction as c$, evaluateEnabled as c0, findConflicts as c1, getScratch as c2, heldKeySpec as c3, historyKey as c4, isAnchor as c5, isAnchorOrControl as c6, isBalanceGrip as c7, isBody as c8, isControlHandle as c9, modeShortcutSpec as cA, modeShortcuts as cB, multiTouchGestureId as cC, offhandKeyFor as cD, openPointerSession as cE, overlaysOf as cF, pickMenu as cG, pointerGestureId as cH, readActionSettings as cI, reportRouteConflicts as cJ, resolveDeviceProfile as cK, resolveMenu as cL, resolveParams as cM, resolveSchemaMenu as cN, roleReader as cO, routesForSpec as cP, ruleCanHoldIn as cQ, rulesExclusive as cR, scopeBindings as cS, scratchKey as cT, setScratch as cU, startCarry as cV, taggedActionItems as cW, targetConsultsAffordance as cX, toolIdsOf as cY, toolSet as cZ, toolSetHas as c_, isCornerWidget as ca, isDebugEnabled as cb, isEmpty as cc, isPathEditPart as cd, isResizeHandle as ce, isRotateHandle as cf, isSegment as cg, isSelectedBody as ch, isTool as ci, isToolSet as cj, isUnselectedBody as ck, itemTypesFromDataTransfer as cl, itemsFromClipboardData as cm, itemsFromDataTransfer as cn, itemsFromFiles as co, keySpecShortcut as cp, keysHeld as cq, liveScope as cr, makeToolOffhandAction as cs, matchActionItem as ct, matchBest as cu, matchSorted as cv, matchSortedWithBarred as cw, matchesKeyBinding as cx, mergeContributions as cy, mergeMenuDefs as cz, type ActionsRegistry as d, validateActionDefaultBinding as d0, validateActionId as d1, withoutTools as d2, ActionDisabledReason as e, type ActionVariant as f, type MenuDef as g, DepNode as h, type Tool as i, ActiveToolContextProvider as j, ActiveToolContextProviderIfRoot as k, type ActiveToolContextProviderProps as l, type ActiveToolContextValue as m, useOptionalActiveToolContext as n, type Dispatcher as o, type AffordanceHit as p, type DispatchTrace as q, type DepSchema as r, ALWAYS as s, type ActionDispatch as t, useActiveToolContext as u, type ActionEnabledResult as v, type ActionEntry as w, type ActionItem as x, type ActionParamSchema as y, type ActionParams as z };