@weasel-js/core 0.7.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/dist/{DrawCommand-CJtqqt8H.d.ts → DrawCommand-DCmiNkBj.d.ts} +38 -170
  3. package/dist/{chunk-CQNKCG34.js → chunk-6SHZHXXR.js} +47 -10
  4. package/dist/chunk-6SHZHXXR.js.map +1 -0
  5. package/dist/{chunk-5F3PIERN.js → chunk-D2C6K6FO.js} +1420 -652
  6. package/dist/chunk-D2C6K6FO.js.map +1 -0
  7. package/dist/chunk-DCIU3SRK.js +505 -0
  8. package/dist/chunk-DCIU3SRK.js.map +1 -0
  9. package/dist/chunk-IO5Z75X4.js +167 -0
  10. package/dist/chunk-IO5Z75X4.js.map +1 -0
  11. package/dist/{chunk-VOVKONXA.js → chunk-UB6A6L77.js} +3 -3
  12. package/dist/{chunk-VOVKONXA.js.map → chunk-UB6A6L77.js.map} +1 -1
  13. package/dist/{chunk-BGGZ4CVF.js → chunk-Z3KNFTTS.js} +6 -6
  14. package/dist/chunk-Z3KNFTTS.js.map +1 -0
  15. package/dist/clipboard.d.ts +2 -2
  16. package/dist/clone.d.ts +1 -1
  17. package/dist/{geometry-D9BDMiQi.d.ts → geometry-DVeZ2i-c.d.ts} +1 -1
  18. package/dist/{grid-CaSK9bHV.d.ts → grid-XcFQVS0f.d.ts} +1 -1
  19. package/dist/{index-DOYRTfP0.d.ts → index-ChM3ZJ2v.d.ts} +278 -93
  20. package/dist/index.d.ts +442 -57
  21. package/dist/index.js +6 -6
  22. package/dist/insert.d.ts +2 -2
  23. package/dist/insert.js +5 -5
  24. package/dist/insert.js.map +1 -1
  25. package/dist/move.d.ts +3 -3
  26. package/dist/move.js +2 -2
  27. package/dist/{options-DMWeTELe.d.ts → options-wAvCnOZd.d.ts} +1 -1
  28. package/dist/paint-types-CnLIzqq1.d.ts +261 -0
  29. package/dist/patterns-builtin.d.ts +57 -2
  30. package/dist/patterns-builtin.js +1 -97
  31. package/dist/patterns-builtin.js.map +1 -1
  32. package/dist/{pointSnapToGrid-C3EruUwt.d.ts → pointSnapToGrid-BuDZWBrL.d.ts} +2 -2
  33. package/dist/renderer.d.ts +25 -8
  34. package/dist/renderer.js +6 -6
  35. package/dist/resize.d.ts +3 -3
  36. package/dist/resize.js +2 -2
  37. package/dist/routing.d.ts +7 -7
  38. package/dist/routing.js +1 -1
  39. package/dist/{types-Dcaa0tPq.d.ts → types-BhJJ2OCM.d.ts} +9 -1
  40. package/dist/{types-Cpb4hii1.d.ts → types-DUpI7Apc.d.ts} +3 -95
  41. package/dist/{viewToMat3-D4lrBigW.d.ts → viewToMat3-BvaWCN99.d.ts} +1 -1
  42. package/package.json +6 -6
  43. package/dist/chunk-5F3PIERN.js.map +0 -1
  44. package/dist/chunk-BGGZ4CVF.js.map +0 -1
  45. package/dist/chunk-CQNKCG34.js.map +0 -1
  46. package/dist/chunk-SYM6RAM4.js +0 -232
  47. package/dist/chunk-SYM6RAM4.js.map +0 -1
  48. package/dist/chunk-Y52N27PF.js +0 -39
  49. package/dist/chunk-Y52N27PF.js.map +0 -1
  50. package/dist/registerTexture-BzHTLhD9.d.ts +0 -25
  51. /package/dist/{types-B6MMiodD.d.ts → path-B6MMiodD.d.ts} +0 -0
@@ -1,16 +1,17 @@
1
- import { N as NodeId, S as Scene, P as PoseComposition } from './types-Cpb4hii1.js';
2
- import { M as ModifierState, A as ActionBehavior, c as ResizeAnchor, R as ResizePose, B as BoundsConstraint, P as PointSnapBehavior } from './types-Dcaa0tPq.js';
1
+ import { N as NodeId, R as RectPose, S as Scene } from './types-DUpI7Apc.js';
2
+ import { M as ModifierState, A as ActionBehavior, c as ResizeAnchor, R as ResizePose, B as BoundsConstraint, P as PointSnapBehavior } from './types-BhJJ2OCM.js';
3
3
  import { V as View } from './view-DSQgxBJB.js';
4
4
  import { CapabilityTag } from '@weasel-js/modes';
5
5
  import * as React from 'react';
6
6
  import { MutableRefObject, ReactNode, ReactElement } from 'react';
7
- import { IngestItem, GestureSpec, InputEvent, ParsedModifiers, GestureName, ChannelRef, DescribeRouteOptions, GESTURE_DESCRIPTORS, GestureArgSpec, GestureDescriptor, ModRequirement, ModifierKey, ParsedRoute, PhaseAtom, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, RouteDescriptionPart, RouteFieldName, RouteTermLabel, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
8
- import { F as FillStyle, a as Stroke, D as DrawCommand } from './DrawCommand-CJtqqt8H.js';
7
+ import { GestureSpec, IngestItem, InputEvent, ParsedModifiers, GestureName, ChannelRef, DescribeRouteOptions, GESTURE_DESCRIPTORS, GestureArgSpec, GestureDescriptor, ModRequirement, ModifierKey, ParsedRoute, PhaseAtom, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, RouteDescriptionPart, RouteFieldName, RouteTermLabel, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, parseRoute } from '@weasel-js/gestures';
8
+ import { B as Bounds, P as PoseProjection } from './geometry-DVeZ2i-c.js';
9
+ import { F as FillStyle, S as Stroke } from './paint-types-CnLIzqq1.js';
10
+ import { D as DrawCommand } from './DrawCommand-DCmiNkBj.js';
9
11
  import * as react_jsx_runtime from 'react/jsx-runtime';
10
- import { P as Path } from './types-B6MMiodD.js';
12
+ import { P as Path } from './path-B6MMiodD.js';
11
13
  import { Op, History } from '@weasel-js/history';
12
14
  import { L as LayoutStrategy, I as InsertAdapter } from './types-B_-khFM0.js';
13
- import { B as Bounds, P as PoseProjection } from './geometry-D9BDMiQi.js';
14
15
  import { Mat3 } from '@weasel-js/geom';
15
16
  import { D as DebugSink } from './types-BJ8_cyT7.js';
16
17
 
@@ -239,6 +240,31 @@ interface CommonAffordanceScratch {
239
240
  y: number;
240
241
  };
241
242
  }
243
+ /**
244
+ * What a **registered layer's** `hitTest` returns. Extends `AffordanceBinding`
245
+ * so existing implementations keep typechecking; the added fields are how a
246
+ * consumer's own chrome says the things kit chrome says through
247
+ * `AffordanceRegion` — which cursor to show, and whether it owns the point
248
+ * outright.
249
+ */
250
+ interface LayerHit<TScratch = unknown> extends AffordanceBinding<TScratch> {
251
+ /** CSS cursor while the pointer is over this hit. Reaches the hover-cursor
252
+ * pump as `AffordanceHit.cursor`, the same path kit chrome uses. */
253
+ cursor?: string;
254
+ /** `'exclusive'` bars every binding whose target doesn't consult the
255
+ * affordance. Omitted means `'shared'` — today's behavior. Same name and
256
+ * meaning as `AffordanceHit.strength`, which it becomes. */
257
+ strength?: 'exclusive' | 'shared';
258
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
259
+ claimedKinds?: readonly ClaimableGesture[];
260
+ }
261
+ /**
262
+ * Gesture kinds an affordance claim can bar, in the spec vocabulary bindings
263
+ * are written in. `'pointer'` is one token because `pointerDown` / `click` /
264
+ * `drag` are a single press protocol — at the event level the first two are
265
+ * the same `kind: 'pointerdown'`, told apart only by `stage`.
266
+ */
267
+ type ClaimableGesture = 'pointer' | 'doubleClick' | 'contextMenu' | 'longPress' | 'wheel';
242
268
 
243
269
  /**
244
270
  * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
@@ -317,7 +343,7 @@ interface RenderLayer<TData> {
317
343
  /** Chrome-caps visibility predicate. When supplied, the layer must
318
344
  * not return a hit from any chrome element whose id reports
319
345
  * `false`. Absent → every element is hittable. */
320
- isVisible?: (id: string) => boolean) => AffordanceBinding | null;
346
+ isVisible?: (id: string) => boolean) => LayerHit | null;
321
347
  /**
322
348
  * Called on every pointermove when no gesture is currently captured.
323
349
  * Lets layers (e.g. HUD widgets) track hover state without participating
@@ -348,6 +374,31 @@ interface RenderLayer<TData> {
348
374
  */
349
375
  declare function drawLayers<TData>(layers: RenderLayer<TData>[], data: TData, visibility: Record<string, boolean>, order: string[] | undefined, view: View | undefined, dims: Dims): DrawCommand[];
350
376
 
377
+ /**
378
+ * Facts about the device the canvas is running on.
379
+ *
380
+ * One object, recomputed when the underlying media queries change, read by
381
+ * two consumers: the chrome-caps rule layer (via `RuleCtx.device`) and the
382
+ * handle-sizing constants (via `targetScale`).
383
+ *
384
+ * Deliberately NOT a form-factor concept. There is no `isPhone` here and
385
+ * there should never be one: chrome layout is the consuming app's decision.
386
+ * The kit's job is to stop assuming a mouse.
387
+ */
388
+ interface DeviceProfile {
389
+ /** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
390
+ readonly coarsePointer: boolean;
391
+ /** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
392
+ readonly canHover: boolean;
393
+ /** Live device pixel ratio. */
394
+ readonly dpr: number;
395
+ /** Multiplier for handle sizes and hit radii. Derived from
396
+ * `coarsePointer` unless explicitly overridden. */
397
+ readonly targetScale: number;
398
+ }
399
+ /** The detected half of a profile — everything except the derived scale. */
400
+ type DetectedDeviceFacts = Omit<DeviceProfile, 'targetScale'>;
401
+
351
402
  /**
352
403
  * Live state read by rule evaluation. Built once per frame on the consuming
353
404
  * surface — chrome-caps, the affordance pipeline, the dispatcher's
@@ -389,6 +440,13 @@ interface RuleCtx {
389
440
  * with nothing being edited should draw no anchors. Absent is treated
390
441
  * as false. */
391
442
  readonly editingAnchors?: boolean;
443
+ /** Device facts — pointer coarseness, hover capability, density.
444
+ *
445
+ * Absent (legacy ctx builders) is treated as
446
+ * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
447
+ * density 1. That is what the kit assumed before this field existed, so
448
+ * an absent profile is behavior-preserving by construction. */
449
+ readonly device?: DeviceProfile;
392
450
  }
393
451
  interface BuildRuleCtxArgs {
394
452
  focused: boolean;
@@ -407,6 +465,8 @@ interface BuildRuleCtxArgs {
407
465
  selectionResizable?: boolean;
408
466
  /** Optional — omitted means "no path is being anchor-edited". */
409
467
  editingAnchors?: boolean;
468
+ /** Optional — omitted means {@link DEFAULT_DEVICE_PROFILE}. */
469
+ device?: DeviceProfile;
410
470
  }
411
471
  declare function buildRuleCtx(args: BuildRuleCtxArgs): RuleCtx;
412
472
 
@@ -444,6 +504,12 @@ interface Selector {
444
504
  * (resizable), so `{ resizable: true }` passes for legacy ctx builders
445
505
  * that don't compute it. */
446
506
  resizable?: boolean;
507
+ /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
508
+ * (touch, most styluses). Absent device is treated as `false`. */
509
+ coarsePointer?: boolean;
510
+ /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
511
+ * device is treated as `true`. */
512
+ canHover?: boolean;
447
513
  }
448
514
  /**
449
515
  * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
@@ -583,6 +649,15 @@ interface Point2 {
583
649
  interface AffordanceHit {
584
650
  /** Discriminator string, e.g. `'handle:bottom-right'`. */
585
651
  kind: string;
652
+ /** Id of whatever produced this hit — a kit affordance's `id`, or the
653
+ * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
654
+ owner?: string;
655
+ /** `'exclusive'` means no binding may act on this point unless its target
656
+ * consults the affordance. `'shared'` (the default) competes on scope and
657
+ * specificity as bindings always have. */
658
+ strength?: 'exclusive' | 'shared';
659
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
660
+ claimedKinds?: readonly ClaimableGesture[];
586
661
  /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
587
662
  fixedPoint?: {
588
663
  x: number;
@@ -910,6 +985,114 @@ interface OngoingInvoker {
910
985
  * the `Action` type. */
911
986
  type Invoker = ImmediateInvoker | OngoingInvoker;
912
987
 
988
+ /**
989
+ * GestureBinding — connects a GestureSpec to an Action id (with per-binding
990
+ * options). Tools own arrays of these on their `bindings` field; ambient
991
+ * gesture-bindings are registered globally.
992
+ *
993
+ * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
994
+ */
995
+
996
+ interface GestureBinding {
997
+ spec: GestureSpec;
998
+ actionId: string;
999
+ opts?: BindingOpts;
1000
+ }
1001
+
1002
+ /**
1003
+ * Pose composition for hierarchical scene graphs.
1004
+ *
1005
+ * As of the nesting change, `getPose(id)` on adapters returns the
1006
+ * **local** pose — relative to the object's direct parent. Anything in the
1007
+ * kit that needs to draw, hit-test, snap, or otherwise reason about world
1008
+ * coordinates routes through `composeWorldPose`, which walks the parent
1009
+ * chain and folds local poses together via a consumer-supplied `compose`.
1010
+ *
1011
+ * Pose shape is generic, so the compose strategy is too. For the common
1012
+ * `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
1013
+ * translation only, child dimensions preserved. Custom pose shapes (paths,
1014
+ * matrix transforms) supply their own.
1015
+ *
1016
+ * The inverse — `rebaseLocalPose` — converts a world-space pose into a
1017
+ * local pose under a target parent. Used when reparenting so the visual
1018
+ * world position of a child is preserved across the parent change.
1019
+ */
1020
+ /** Re-exported; the declaration lives in `core/scene/types.ts`, which names
1021
+ * it and may not import from features. */
1022
+
1023
+ /** Minimal adapter needed by `composeWorldPose` and friends — pose lookup plus parent walk. */
1024
+ interface PoseAdapter<TPose> {
1025
+ getPose(id: string): TPose;
1026
+ getParent(id: string): string | null;
1027
+ }
1028
+ /** Consumer's pose-composition strategy for hierarchical scenes. `compose`
1029
+ * folds a child's pose (in parent's frame) up to the next frame; `decompose`
1030
+ * is its inverse. Default is IDENTITY — an absolute-pose scene where every
1031
+ * node already stores world coords (parent is grouping-only, no transform). */
1032
+ interface PoseComposition<TPose> {
1033
+ compose: (parent: TPose, child: TPose) => TPose;
1034
+ decompose: (parent: TPose, world: TPose) => TPose;
1035
+ }
1036
+ /** Default pose-composition strategy: IDENTITY. Both `compose` and
1037
+ * `decompose` return the child/world pose unchanged, modeling an
1038
+ * absolute-pose scene where every node stores world coords and parents are
1039
+ * grouping-only (no transform). With this strategy `composeWorldPose`
1040
+ * returns a node's own raw pose and `rebaseLocalPose` is a no-op. */
1041
+ declare const IDENTITY_POSE_COMPOSITION: PoseComposition<unknown>;
1042
+ /**
1043
+ * Walk `id`'s parent chain (root first to id last) and fold local poses into
1044
+ * a world pose via `compose`. Returns the world pose for `id`. Cycle-safe:
1045
+ * a visited-set guard breaks if the chain ever loops back to itself.
1046
+ *
1047
+ * `compose(parent, child)` interprets `child` as expressed *in `parent`'s
1048
+ * local frame* and returns the equivalent pose in the next frame up. For a
1049
+ * standard translation-only rect: `world = { x: p.x + c.x, y: p.y + c.y,
1050
+ * width: c.width, height: c.height }`.
1051
+ */
1052
+ declare function composeWorldPose<TPose>(adapter: PoseAdapter<TPose>, id: string, compose: (parent: TPose, child: TPose) => TPose): TPose;
1053
+ /**
1054
+ * Default `compose` for axis-aligned rectangles. Adds translation; preserves
1055
+ * child width/height. Treat as the canonical compose for any
1056
+ * `{x, y, width, height}` pose under a translation-only hierarchy.
1057
+ *
1058
+ * Generic over the concrete pose type so callers with a wider pose
1059
+ * (e.g. `RectPose & { rotation }`) can pass it through; the extra fields
1060
+ * are taken from the child unchanged.
1061
+ */
1062
+ declare function composeRectPose<TPose extends RectPose>(parent: TPose, child: TPose): TPose;
1063
+ /**
1064
+ * Translate a `RectPose`-shaped pose by `(dx, dy)`. Suitable as the default
1065
+ * `translatePose` for `useMove` when poses carry top-level `x`/`y`. Other
1066
+ * fields (width/height, plus any extra props on `TPose`) are preserved.
1067
+ */
1068
+ declare function translateRectPose<TPose extends RectPose>(pose: TPose, dx: number, dy: number): TPose;
1069
+ /**
1070
+ * Convert `worldPose` into a local pose expressed under `newParentId`'s
1071
+ * frame. Used when reparenting so the child's visual world position is
1072
+ * preserved despite the change of frame. Inverse of one `compose` step.
1073
+ *
1074
+ * `decompose(parent, world)` returns the local pose `child` such that
1075
+ * `compose(parent, child) === world`. For axis-aligned rects:
1076
+ * `child = { ...world, x: world.x - parent.x, y: world.y - parent.y }`.
1077
+ *
1078
+ * Pass `newParentId === null` for the root frame; the function returns
1079
+ * `worldPose` unchanged.
1080
+ */
1081
+ declare function rebaseLocalPose<TPose>(adapter: PoseAdapter<TPose>, worldPose: TPose, newParentId: string | null, compose: (parent: TPose, child: TPose) => TPose, decompose: (parent: TPose, world: TPose) => TPose): TPose;
1082
+ /** Inverse of `composeRectPose` — subtracts parent translation. */
1083
+ declare function decomposeRectPose<TPose extends RectPose>(parent: TPose, world: TPose): TPose;
1084
+ /**
1085
+ * Build a `(id) => world pose | null` callback over a `PoseAdapter`.
1086
+ * Convenience for RenderLayers that take a `getPose` callback (selection
1087
+ * overlays, debug layers, etc.) so consumers don't hand-write a
1088
+ * `composeWorldPose` call per layer.
1089
+ *
1090
+ * Returns `null` when `adapter.getPose` or `adapter.getParent` throws — the
1091
+ * common case is an id removed mid-render between selection state and the
1092
+ * next paint. Layers should treat `null` as "skip this id."
1093
+ */
1094
+ declare function worldPoseLookup<TPose>(adapter: PoseAdapter<TPose>, compose: (parent: TPose, child: TPose) => TPose): (id: string) => TPose | null;
1095
+
913
1096
  /** Boolean op identifiers — five Pathfinder primaries plus Crop. */
914
1097
  type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
915
1098
  /**
@@ -1320,6 +1503,35 @@ declare const sliceAction: Action & {
1320
1503
  requires: string[];
1321
1504
  };
1322
1505
 
1506
+ /**
1507
+ * Clipboard dep — the imperative surface `useClipboardOps` returns.
1508
+ *
1509
+ * Consumers publish their live clipboard through `useDepSource('clipboard',
1510
+ * …)` from inside the `<DepRegistryProvider>` (i.e. under `<SceneCanvas>`).
1511
+ * The kit deliberately does not build one for them: `useClipboardOps` needs
1512
+ * an adapter and a selection reader that only the consumer can supply.
1513
+ */
1514
+ interface ClipboardDep {
1515
+ copy(): void;
1516
+ paste(): void;
1517
+ isEmpty(): boolean;
1518
+ }
1519
+ /**
1520
+ * @experimental
1521
+ * Static descriptor for the `clipboard.copy` Action (Cmd/Ctrl+C).
1522
+ */
1523
+ declare const clipboardCopyAction: Action & {
1524
+ requires: string[];
1525
+ };
1526
+ /**
1527
+ * @experimental
1528
+ * Static descriptor for the `clipboard.cut` Action (Cmd/Ctrl+X) — copy, then
1529
+ * the same batched delete `deleteAction` performs, as one undo entry.
1530
+ */
1531
+ declare const clipboardCutAction: Action & {
1532
+ requires: string[];
1533
+ };
1534
+
1323
1535
  /** Optional consumer seam: given a node and the affine `m` that a pose-transform
1324
1536
  * action applied to the node's POSE, return updated `data` with the node's
1325
1537
  * data-held geometry transformed by `m`, or `null` if this node has no
@@ -1832,6 +2044,15 @@ interface DepSchema {
1832
2044
  * Optional: when absent, `sliceAction` is a no-op.
1833
2045
  */
1834
2046
  slice?: SliceDep;
2047
+ /**
2048
+ * Clipboard dep — the imperative surface `useClipboardOps` returns.
2049
+ *
2050
+ * Published by the consumer (`useDepSource('clipboard', …)` from under
2051
+ * `<SceneCanvas>`), because `useClipboardOps` needs an adapter and a
2052
+ * selection reader only the consumer has. Feeds `clipboard.copy` /
2053
+ * `clipboard.cut`; both no-op when the dep is absent.
2054
+ */
2055
+ clipboard?: ClipboardDep;
1835
2056
  /**
1836
2057
  * Optional consumer commit hook. When present, `moveAction` (and other
1837
2058
  * default actions) submit their committed ops through it instead of
@@ -1901,17 +2122,35 @@ declare function useOptionalDepRegistry(): DepRegistry | null;
1901
2122
  declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
1902
2123
 
1903
2124
  /**
1904
- * GestureBinding connects a GestureSpec to an Action id (with per-binding
1905
- * options). Tools own arrays of these on their `bindings` field; ambient
1906
- * gesture-bindings are registered globally.
1907
- *
1908
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
2125
+ * When an entry's bindings are live. A set, not one value: the hand tool is
2126
+ * palette-selectable AND engaged by holding space, and both hold at once.
2127
+ */
2128
+ interface Eligibility {
2129
+ /** Selectable as the focused entry — exclusive, one at a time. */
2130
+ focus?: boolean;
2131
+ /** Also live while this key is held. */
2132
+ offhand?: HotkeyTrigger;
2133
+ /** Live regardless of what is focused. */
2134
+ always?: boolean;
2135
+ /** Live only for input this entry's own affordances produced. */
2136
+ claimed?: boolean;
2137
+ /** Modality filter, applied wherever it would otherwise be live. */
2138
+ capabilities?: CapabilityTag[];
2139
+ }
2140
+ /**
2141
+ * A registry entry: what it contributes, and when it is eligible. Every role
2142
+ * is optional and independent — an entry that only routes input declares only
2143
+ * `bindings` and `actions`.
1909
2144
  */
1910
-
1911
- interface GestureBinding {
1912
- spec: GestureSpec;
1913
- actionId: string;
1914
- opts?: BindingOpts;
2145
+ interface Contribution {
2146
+ id: string;
2147
+ eligibility: Eligibility;
2148
+ bindings?: GestureBinding[];
2149
+ actions?: Action[];
2150
+ overlay?: RenderLayer<unknown>;
2151
+ presentation?: ToolPresentation;
2152
+ /** Reflection escape hatch — the authored form, when there was one. */
2153
+ def?: unknown;
1915
2154
  }
1916
2155
 
1917
2156
  /** Modifier-key snapshot at event dispatch time. `space` is included
@@ -1975,8 +2214,8 @@ type ToolBounds = Bounds;
1975
2214
  * tool — consumers that render a palette (`<ToolPalette>`) read these
1976
2215
  * fields to display the tool; consumers that don't can ignore them.
1977
2216
  *
1978
- * Note: cursor is NOT here. The top-level `Tool.cursor` field below is
1979
- * already plumbed through `<Canvas>` to `style.cursor` on the host. */
2217
+ * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
2218
+ * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
1980
2219
  interface ToolPresentation<TScratch = unknown> {
1981
2220
  /** Human-readable label, distinct from the `id`. Falls back to `id`. */
1982
2221
  label?: string;
@@ -1991,34 +2230,13 @@ interface ToolPresentation<TScratch = unknown> {
1991
2230
  * derives one from `Tool.keybinding` via its own formatter. */
1992
2231
  shortcut?: string;
1993
2232
  }
1994
- /** Full Tool record. */
1995
- interface Tool<TScratch = unknown> {
1996
- id: string;
1997
- /**
1998
- * App-level capability tags for modality. The `weasel-modes` package's
1999
- * `eligibleForMode(mode, capabilities)` predicate consumes these to decide
2000
- * whether the tool is usable in the active mode. Tags are extensible
2001
- * strings — apps can define their own. Untagged tools are treated as
2002
- * ineligible by all modes except those whose `allows` list includes
2003
- * every implicit-or-declared tag (i.e. `normal` in the default preset).
2004
- */
2005
- capabilities?: CapabilityTag[];
2006
- /**
2007
- * Actions this tool owns and needs registered while it is in the tools
2008
- * registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
2009
- * reference by id.
2010
- *
2011
- * Declared here rather than registered by the hook with `useAction`,
2012
- * because tool hooks run wherever the consumer calls them — for
2013
- * `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
2014
- * `useActionsRegistry()` returns null and `useAction` silently no-ops. The
2015
- * result was a binding pointing at an action id nothing had registered, so
2016
- * the gesture fell through to whatever matched next (polygon's
2017
- * wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
2018
- * selection instead). `<ToolActionsMounter>` registers these from inside
2019
- * the provider.
2020
- */
2021
- actions?: Action[];
2233
+ /**
2234
+ * The focus-declaring case of a `Contribution`: a mode the user switches
2235
+ * into, plus the hooks that only make sense for one (`initScratch`,
2236
+ * activate/deactivate, live preview, `cursor`). Everything else — bindings,
2237
+ * actions, overlay, presentation is inherited.
2238
+ */
2239
+ interface Tool<TScratch = unknown> extends Contribution {
2022
2240
  /** Optional caller-supplied key. Most built-in tools have their activation
2023
2241
  * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
2024
2242
  * is for tools that want their activation key to be configurable by the
@@ -2028,11 +2246,9 @@ interface Tool<TScratch = unknown> {
2028
2246
  * the invoker knows which tool to switch to). */
2029
2247
  keybinding?: ToolKeybinding;
2030
2248
  initScratch?: () => TScratch;
2249
+ cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
2031
2250
  onActivate?: (ctx: ToolCtx<TScratch>) => void;
2032
2251
  onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
2033
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
2034
- /** Presentation metadata for tool palettes. See `ToolPresentation`. */
2035
- presentation?: ToolPresentation<TScratch>;
2036
2252
  /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
2037
2253
  * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
2038
2254
  * reflect live gesture state without reaching into hook internals. The
@@ -2050,26 +2266,6 @@ interface Tool<TScratch = unknown> {
2050
2266
  * consults this alongside `previewPose` to avoid double-rendering. Returns
2051
2267
  * `null` when no gesture is in flight. */
2052
2268
  previewIds?: () => Iterable<string> | null;
2053
- /** Optional overlay layer rendered on top of the scene/chrome whenever
2054
- * this tool is in any active slot (active, hotkey, or ambient).
2055
- * The layer's `draw` function reads from this tool's scratch via React
2056
- * closure (re-evaluated each render). Return early from `draw` to render
2057
- * nothing — typically gated on a scratch field like
2058
- * `if (!scratch.overlay) return`. */
2059
- overlay?: RenderLayer<unknown>;
2060
- /** Declarative gesture bindings — the tool's entire input surface. The
2061
- * gesture dispatcher consults these at active scope while this tool is
2062
- * active, and at hotkey scope while it is held. See
2063
- * `docs/superpowers/specs/2026-05-16-registry-unification-design.md`. */
2064
- bindings?: GestureBinding[];
2065
- /** Reflection escape hatch: when this `Tool` was produced by `defineTool`,
2066
- * the source `ToolDef` is attached here so introspection consumers
2067
- * (`buildRouteRegistry`, `findConflicts`, the toolkit-builder UI, the
2068
- * reflection demo) can read the authored form — `hookName` in particular,
2069
- * which the runtime `Tool` doesn't carry. Tools constructed without
2070
- * `defineTool` may leave this undefined. Typed as `unknown` to keep this
2071
- * file from importing the routing types — consumers cast at the use site. */
2072
- def?: unknown;
2073
2269
  }
2074
2270
  /** Internal — which slot a tool occupies in the dispatch order. */
2075
2271
  type ToolSlot = 'hotkey' | 'active' | 'ambient';
@@ -2162,17 +2358,6 @@ interface DispatcherContext {
2162
2358
  activeToolId: string;
2163
2359
  /** Held-hotkey stack, top of stack last. */
2164
2360
  hotkeyStack: readonly string[];
2165
- /**
2166
- * Ids of always-on tools. Their bindings are assembled at AMBIENT scope, so
2167
- * they lose to the active tool on a tie — which is what "always listening,
2168
- * never in the way" needs. Chrome that floats over the scene lives here:
2169
- * `@weasel-js/hud`'s tool is the worked example.
2170
- *
2171
- * Without this an ambient tool's `bindings` were assembled nowhere at all:
2172
- * the walk covered hotkey and active tools plus actions' `defaultBinding`,
2173
- * and an ambient tool is in neither set.
2174
- */
2175
- ambientToolIds?: readonly string[];
2176
2361
  /** Lookup for tool definitions. */
2177
2362
  toolsById: ReadonlyMap<string, Tool>;
2178
2363
  /** Platform flag for `mod` shorthand resolution. */
@@ -2700,13 +2885,10 @@ interface ToolDef<TScratch = void> {
2700
2885
  * to the consolidated `tool.activate` action (with `opts.params.toolId`
2701
2886
  * set so the invoker knows which tool to switch to). */
2702
2887
  keybinding?: ToolKeybinding;
2703
- /** Declarative held-key trigger (reflection / inspector only). When set,
2704
- * signals to the host that this tool can engage via a held key; the host
2705
- * must register the activation via the consolidated `tool.offhand` action
2706
- * (`makeToolOffhandAction` + `buildToolOffhandBindings`). Built-in tools
2707
- * declare held keys in `BUILTIN_OFFHAND_ACTIONS`; configurable-hotkey
2708
- * tools rely on the host to wire the binding. Setting this field does NOT
2709
- * automatically engage the held-key behavior. */
2888
+ /** Held-key trigger: this tool engages while the key is down and
2889
+ * disengages on release. Carried onto `Tool.eligibility.offhand`, which
2890
+ * assembly reads to register the consolidated `tool.offhand` action — the
2891
+ * declaration is the wiring, with nothing for the host to do. */
2710
2892
  hotkey?: HotkeyTrigger;
2711
2893
  onActivate?: (ctx: ToolCtx<TScratch>) => void;
2712
2894
  onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
@@ -2884,6 +3066,9 @@ interface ToolScopes {
2884
3066
  registry: readonly Tool<unknown>[] | Readonly<Record<string, Tool<unknown>>>;
2885
3067
  /** Always-on tools. Every one of these is live at once. */
2886
3068
  ambient?: readonly Tool<unknown>[];
3069
+ /** Registered actions. Their `defaultBinding`s assemble at ambient scope
3070
+ * (hotkey scope when `Action.scope` says so), alongside the tools above. */
3071
+ actions?: readonly Action[];
2887
3072
  }
2888
3073
  /**
2889
3074
  * Detect the same-tuple overlaps that are *reachable* — the ones where the
@@ -2903,10 +3088,10 @@ interface ToolScopes {
2903
3088
  * ordered only by registration;
2904
3089
  * - two **hotkey-capable** tools, which can stack.
2905
3090
  *
2906
- * Not covered: the actions registry's `defaultBinding`s, which the dispatcher
2907
- * also folds into ambient/hotkey scope. They're assembled somewhere else
2908
- * entirely (`ActionsRegistry`, not `useTools`), so catching tool-vs-action
2909
- * collisions means giving this function a second input it doesn't have yet.
3091
+ * Registered actions join the same two buckets: their `defaultBinding`s
3092
+ * assemble at ambient scope (hotkey scope when `Action.scope` says so), so an
3093
+ * ambient tool and an action claiming one tuple really do fall back on
3094
+ * declaration order.
2910
3095
  */
2911
3096
  declare function findScopedConflicts(scopes: ToolScopes): Conflict[];
2912
3097
  /**
@@ -2983,4 +3168,4 @@ declare namespace index {
2983
3168
  export { index_ChannelRef as ChannelRef, type index_Conflict as Conflict, index_DescribeRouteOptions as DescribeRouteOptions, index_GESTURE_DESCRIPTORS as GESTURE_DESCRIPTORS, index_GestureArgSpec as GestureArgSpec, index_GestureDescriptor as GestureDescriptor, index_GestureName as GestureName, index_ModRequirement as ModRequirement, index_ModifierKey as ModifierKey, index_PREDICATE_TARGET as PREDICATE_TARGET, index_ParsedModifiers as ParsedModifiers, index_ParsedRoute as ParsedRoute, index_PhaseAtom as PhaseAtom, index_RESERVED_ID_NAMES as RESERVED_ID_NAMES, index_RESERVED_ID_PREFIXES as RESERVED_ID_PREFIXES, index_ROUTE_FIELD_DEFINITIONS as ROUTE_FIELD_DEFINITIONS, index_ROUTE_TERMS as ROUTE_TERMS, type index_RegistryEntry as RegistryEntry, index_RouteDescriptionPart as RouteDescriptionPart, index_RouteFieldName as RouteFieldName, index_RouteTermLabel as RouteTermLabel, type index_ToolDef as ToolDef, type index_ToolKeybinding as ToolKeybinding, type index_ToolScopes as ToolScopes, type index_ViewportToolDef as ViewportToolDef, index_buildRouteRegistry as buildRouteRegistry, index_canonicalModifiers as canonicalModifiers, index_collapseShiftPairs as collapseShiftPairs, index_defineTool as defineTool, index_defineViewportTool as defineViewportTool, index_describeRoute as describeRoute, index_describeRouteParts as describeRouteParts, index_findConflicts as findConflicts, index_findScopedConflicts as findScopedConflicts, index_formatConflict as formatConflict, index_formatPhaseAtom as formatPhaseAtom, index_formatRoute as formatRoute, index_getGestureDescriptor as getGestureDescriptor, index_isKnownGestureName as isKnownGestureName, index_parseRoute as parseRoute, index_reportRouteConflicts as reportRouteConflicts };
2984
3169
  }
2985
3170
 
2986
- export { DepRegistryProvider as $, type Action as A, type BooleansAdapter as B, type Condition as C, type Dims as D, ActiveToolContextProvider as E, ActiveToolContextProviderIfRoot as F, type GeometryProjection as G, type HotkeyTrigger as H, type InsertExtras as I, type ActiveToolContextProviderProps as J, type ActiveToolContextValue as K, type AreaSelectDep as L, type BindingOpts as M, type BindingScope as N, type BooleanOp as O, type BooleanOpResult as P, type BoundGesture as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type BuildRuleCtxArgs as W, type ClipboardIngestCtx as X, type CustomPaintContext as Y, type DepName as Z, type DepRegistry as _, type Rule as a, type ToolScopes as a$, type DispatcherContext as a0, type DragSample as a1, type EditAnchorsDep as a2, type GestureBinding as a3, type ImmediateInvoker as a4, type IngestCtx as a5, type IngestionDep as a6, type InsertDep as a7, type InvocationCtx as a8, type Invoker as a9, type UiOngoingControl as aA, type ViewApi as aB, applyBooleanOp as aC, buildRuleCtx as aD, createDispatcher as aE, describeRule as aF, drawLayers as aG, enterTextEditAction as aH, evaluate as aI, evaluateEnabled as aJ, registerContentHandler as aK, index as aL, sliceAction as aM, specificity as aN, useAction as aO, useActionsRegistry as aP, useActiveToolContext as aQ, useDepRegistry as aR, useDepSource as aS, useOptionalActiveToolContext as aT, useOptionalDepRegistry as aU, usePointerContext as aV, useSelection as aW, type Conflict as aX, PREDICATE_TARGET as aY, type RegistryEntry as aZ, type ToolDef as a_, type LassoSelectDep as aa, type LayoutDep as ab, type MatchResult as ac, NEVER as ad, type NodeAtPointDep as ae, type OngoingHandle as af, type OngoingInvoker as ag, type OngoingOverlay as ah, type Point2 as ai, PointerContextProvider as aj, type PointerContextValue as ak, type PointerWorldPos as al, type ResizePolicy as am, type ResolveAllOptions as an, type ResolveOnlyResult as ao, type ResolvedCandidate as ap, type ScopedBinding as aq, type SelectionExtendKey as ar, type SelectionMode as as, type Selector as at, type SnapDep as au, type SvgUnpacker as av, type TextEditDep as aw, type ToolModifiers as ax, type ToolPresentation as ay, type ToolSlot as az, type RuleCtx as b, type ViewportToolDef as b0, buildRouteRegistry as b1, defineTool as b2, defineViewportTool as b3, findConflicts as b4, findScopedConflicts as b5, formatConflict as b6, reportRouteConflicts as b7, type ChromeCtx as c, type ChromeId as d, type DepSchema as e, type ActionsRegistry as f, type AffordanceHit as g, type Dispatcher as h, type AnyTool as i, type ToolCtx as j, type ToolKeybinding as k, type AffordanceBinding as l, type ChromeState as m, type SelectionApi as n, type ContentHandlerEntry as o, type SvgIngestOptions as p, type ActionsProp as q, type Affordance as r, type AffordanceRegion as s, type CommonAffordanceScratch as t, ALWAYS as u, type ActionDeps as v, ActionDisabledReason as w, type ActionEnabledResult as x, type ActionEntry as y, ActionsProvider as z };
3171
+ export { type BoundGesture as $, type Action as A, type BooleansAdapter as B, type Condition as C, type Dims as D, type Eligibility as E, type ActionDeps as F, type GeometryProjection as G, type HotkeyTrigger as H, type InsertExtras as I, ActionDisabledReason as J, type ActionEnabledResult as K, type LayerHit as L, type ActionEntry as M, ActionsProvider as N, ActiveToolContextProvider as O, ActiveToolContextProviderIfRoot as P, type ActiveToolContextProviderProps as Q, type RenderLayer as R, type SliceDep as S, type Tool as T, type UseSelectionOptions as U, type VisibilityRules as V, type ActiveToolContextValue as W, type AreaSelectDep as X, type BindingOpts as Y, type BooleanOp as Z, type BooleanOpResult as _, type DeviceProfile as a, index as a$, type BuildRuleCtxArgs as a0, type ClaimableGesture as a1, type ClipboardDep as a2, type ClipboardIngestCtx as a3, type CustomPaintContext as a4, type DepName as a5, type DepRegistry as a6, DepRegistryProvider as a7, type DispatcherContext as a8, type DragSample as a9, type ResolvedCandidate as aA, type SelectionExtendKey as aB, type SelectionMode as aC, type Selector as aD, type SnapDep as aE, type SvgUnpacker as aF, type TextEditDep as aG, type ToolModifiers as aH, type ToolPresentation as aI, type ToolSlot as aJ, type UiOngoingControl as aK, type ViewApi as aL, applyBooleanOp as aM, buildRuleCtx as aN, clipboardCopyAction as aO, clipboardCutAction as aP, composeRectPose as aQ, composeWorldPose as aR, createDispatcher as aS, decomposeRectPose as aT, describeRule as aU, drawLayers as aV, enterTextEditAction as aW, evaluate as aX, evaluateEnabled as aY, rebaseLocalPose as aZ, registerContentHandler as a_, type EditAnchorsDep as aa, type GestureBinding as ab, IDENTITY_POSE_COMPOSITION as ac, type ImmediateInvoker as ad, type IngestCtx as ae, type IngestionDep as af, type InsertDep as ag, type InvocationCtx as ah, type Invoker as ai, type LassoSelectDep as aj, type LayoutDep as ak, type MatchResult as al, NEVER as am, type NodeAtPointDep as an, type OngoingHandle as ao, type OngoingInvoker as ap, type OngoingOverlay as aq, type Point2 as ar, PointerContextProvider as as, type PointerContextValue as at, type PointerWorldPos as au, type PoseAdapter as av, type PoseComposition as aw, type ResizePolicy as ax, type ResolveAllOptions as ay, type ResolveOnlyResult as az, type DetectedDeviceFacts as b, sliceAction as b0, specificity as b1, translateRectPose as b2, useAction as b3, useActionsRegistry as b4, useActiveToolContext as b5, useDepRegistry as b6, useDepSource as b7, useOptionalActiveToolContext as b8, useOptionalDepRegistry as b9, usePointerContext as ba, useSelection as bb, worldPoseLookup as bc, type Conflict as bd, PREDICATE_TARGET as be, type RegistryEntry as bf, type ToolDef as bg, type ToolScopes as bh, type ViewportToolDef as bi, buildRouteRegistry as bj, defineTool as bk, defineViewportTool as bl, findConflicts as bm, findScopedConflicts as bn, formatConflict as bo, reportRouteConflicts as bp, type Rule as c, type RuleCtx as d, type ChromeCtx as e, type ChromeId as f, type DepSchema as g, type ActionsRegistry as h, type AffordanceHit as i, type Dispatcher as j, type AnyTool as k, type ToolCtx as l, type ToolKeybinding as m, type ChromeState as n, type SelectionApi as o, type ContentHandlerEntry as p, type SvgIngestOptions as q, type ActionsProp as r, type Affordance as s, type AffordanceBinding as t, type AffordanceRegion as u, type CommonAffordanceScratch as v, type BindingScope as w, type Contribution as x, type ScopedBinding as y, ALWAYS as z };