@weasel-js/labkit 1.4.0-pre.1 → 1.4.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.
Files changed (120) hide show
  1. package/dist/_dts/{CanvasStackContext-LnCfqNBA.d.ts → CanvasStackContext-BIRUVVGH.d.ts} +1 -1
  2. package/dist/_dts/PrefsForm.d-CgxUequc.d.ts +20 -0
  3. package/dist/_dts/{frac-C-2c72Ij.d.ts → frac-X7mWgd7y.d.ts} +10 -4
  4. package/dist/_dts/{index-BDVzvRzQ.d.ts → index-FEa1MudK.d.ts} +8 -2
  5. package/dist/_dts/{types-DYMaEvM5.d.ts → types-ChVJJHvk.d.ts} +3 -0
  6. package/dist/_dts/{types-D6s4b7if.d.ts → types-lg4TSCb2.d.ts} +3 -1
  7. package/dist/_dts/{useTrialState-gmMvZqPc.d.ts → useTrialState-Cdm13fvl.d.ts} +2 -1
  8. package/dist/_dts/weasel-canvas-FJTFi3ZZ.d.ts +5041 -0
  9. package/dist/canvas/index.d.ts +16 -16
  10. package/dist/canvas/index.js +2 -2
  11. package/dist/chrome/index.d.ts +6 -6
  12. package/dist/chrome/index.js +5 -5
  13. package/dist/chunk-2YILQ7Y5.js +33 -0
  14. package/dist/chunk-2YILQ7Y5.js.map +1 -0
  15. package/dist/{chunk-TO2FUOKF.js → chunk-53AZ2O2N.js} +16 -13
  16. package/dist/chunk-53AZ2O2N.js.map +1 -0
  17. package/dist/{chunk-QB5SM4BM.js → chunk-7TRFMIWX.js} +55 -41
  18. package/dist/chunk-7TRFMIWX.js.map +1 -0
  19. package/dist/{chunk-6OEYTYML.js → chunk-BMQWLKOL.js} +15 -15
  20. package/dist/chunk-BMQWLKOL.js.map +1 -0
  21. package/dist/{chunk-M75ZU6ZZ.js → chunk-D7QIPGEB.js} +3 -3
  22. package/dist/{chunk-M75ZU6ZZ.js.map → chunk-D7QIPGEB.js.map} +1 -1
  23. package/dist/{chunk-L5QJOOLV.js → chunk-DG2IFMLA.js} +1309 -273
  24. package/dist/chunk-DG2IFMLA.js.map +1 -0
  25. package/dist/{chunk-LN6JDUGB.js → chunk-HXXTULDN.js} +2 -2
  26. package/dist/{chunk-LN6JDUGB.js.map → chunk-HXXTULDN.js.map} +1 -1
  27. package/dist/{chunk-RBGKL7NF.js → chunk-J6SBFZYT.js} +1807 -1814
  28. package/dist/chunk-J6SBFZYT.js.map +1 -0
  29. package/dist/{chunk-XJ6N32QP.js → chunk-LJSIUFYD.js} +14 -30
  30. package/dist/chunk-LJSIUFYD.js.map +1 -0
  31. package/dist/{chunk-AE5CNVRU.js → chunk-MIT3ISW4.js} +37 -31
  32. package/dist/chunk-MIT3ISW4.js.map +1 -0
  33. package/dist/{chunk-HHGVISVZ.js → chunk-MSR7EVS6.js} +7 -5
  34. package/dist/chunk-MSR7EVS6.js.map +1 -0
  35. package/dist/{chunk-ESIQNQ6K.js → chunk-OFCBEZMU.js} +12 -6
  36. package/dist/chunk-OFCBEZMU.js.map +1 -0
  37. package/dist/{chunk-HMFODCOX.js → chunk-PP7ZWAMV.js} +83 -75
  38. package/dist/chunk-PP7ZWAMV.js.map +1 -0
  39. package/dist/{chunk-E2UQVZ44.js → chunk-RLOXQZW3.js} +3 -3
  40. package/dist/{chunk-E2UQVZ44.js.map → chunk-RLOXQZW3.js.map} +1 -1
  41. package/dist/controls/index.d.ts +4 -3
  42. package/dist/controls/index.js +3 -3
  43. package/dist/dragdrop/index.d.ts +5 -5
  44. package/dist/dragdrop/index.js +2 -2
  45. package/dist/index.d.ts +57 -77
  46. package/dist/index.js +308 -161
  47. package/dist/index.js.map +1 -1
  48. package/dist/job/index.js +1 -1
  49. package/dist/layers/index.d.ts +6 -6
  50. package/dist/layers/index.js +3 -3
  51. package/dist/loupe/index.d.ts +7 -13
  52. package/dist/loupe/index.js +2 -2
  53. package/dist/passthrough/weasel-canvas.d.ts +3 -343
  54. package/dist/passthrough/weasel-canvas.js +1 -1
  55. package/dist/passthrough/weasel-ui.d.ts +141 -2866
  56. package/dist/passthrough/weasel-ui.js +2 -2
  57. package/dist/primitives/index.js +4 -4
  58. package/dist/state/index.d.ts +3 -3
  59. package/dist/state/index.js +4 -2
  60. package/dist/state/index.js.map +1 -1
  61. package/dist/styles.css +74 -5
  62. package/dist/surface/index.d.ts +17 -2
  63. package/dist/surface/index.js +3 -2
  64. package/dist/ui/layers/index.js +2 -2
  65. package/dist/undo/index.d.ts +6 -5
  66. package/package.json +8 -8
  67. package/src/annotations/AnnotationOverlay.tsx +10 -6
  68. package/src/annotations/Annotations.less +6 -0
  69. package/src/annotations/Annotations.overlay.test.tsx +113 -0
  70. package/src/annotations/MarkList.tsx +3 -2
  71. package/src/canvas/CanvasStack.tsx +8 -13
  72. package/src/canvas/useOrbit.test.ts +93 -1
  73. package/src/canvas/useOrbit.ts +41 -34
  74. package/src/canvas/usePanZoom.test.ts +102 -1
  75. package/src/canvas/usePanZoom.ts +54 -36
  76. package/src/config/builder.test.ts +5 -0
  77. package/src/config/builder.ts +6 -0
  78. package/src/config/types.ts +1 -0
  79. package/src/controls/ControlPanel.test.tsx +23 -0
  80. package/src/controls/ControlPanel.tsx +3 -0
  81. package/src/job/useJob.ts +1 -1
  82. package/src/lab/Lab.surface.test.tsx +8 -3
  83. package/src/lab/LabHeader.test.tsx +12 -0
  84. package/src/lab/LabHeader.tsx +16 -6
  85. package/src/lab/Workspace.tsx +1 -1
  86. package/src/layers/LayerList.test.tsx +72 -1
  87. package/src/layers/LayerList.tsx +40 -32
  88. package/src/passthrough/weasel-ui.test.ts +9 -0
  89. package/src/passthrough/weasel-ui.ts +4 -0
  90. package/src/primitives/FloatingPanel.test.tsx +82 -0
  91. package/src/primitives/FloatingPanel.tsx +59 -41
  92. package/src/primitives/ZoomControl.tsx +2 -0
  93. package/src/state/store.ts +10 -0
  94. package/src/state/types.ts +3 -0
  95. package/src/surface/AGENTS.md +11 -1
  96. package/src/surface/index.ts +7 -1
  97. package/src/surface/useSurfaceTile.ts +20 -2
  98. package/src/surface/useTiledSurface.ts +4 -1
  99. package/src/theme/Interstellar.stories.tsx +4 -3
  100. package/src/theme/base.less +39 -1
  101. package/src/trial/Trial.less +27 -5
  102. package/src/trial/TrialBody.test.tsx +67 -0
  103. package/src/trial/TrialBody.tsx +155 -0
  104. package/src/trial/TrialChrome.tsx +18 -10
  105. package/src/trial/index.ts +2 -0
  106. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +0 -564
  107. package/dist/_dts/PrefsForm-BkUJZx0A.d.ts +0 -204
  108. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +0 -21
  109. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +0 -87
  110. package/dist/_dts/types-C-gh9Ap-.d.ts +0 -695
  111. package/dist/chunk-6OEYTYML.js.map +0 -1
  112. package/dist/chunk-AE5CNVRU.js.map +0 -1
  113. package/dist/chunk-ESIQNQ6K.js.map +0 -1
  114. package/dist/chunk-HHGVISVZ.js.map +0 -1
  115. package/dist/chunk-HMFODCOX.js.map +0 -1
  116. package/dist/chunk-L5QJOOLV.js.map +0 -1
  117. package/dist/chunk-QB5SM4BM.js.map +0 -1
  118. package/dist/chunk-RBGKL7NF.js.map +0 -1
  119. package/dist/chunk-TO2FUOKF.js.map +0 -1
  120. package/dist/chunk-XJ6N32QP.js.map +0 -1
@@ -1,13 +1,10 @@
1
- import { T as ToolPrefLeaf, a as ToolPrefGroup } from '../_dts/PrefsForm-BkUJZx0A.js';
2
- export { b as BuiltinPref, c as PrefBoolean, d as PrefBooleanControl, e as PrefColor, f as PrefCustom, g as PrefEnum, h as PrefEnumControl, i as PrefEnumEncoding, j as PrefKind, k as PrefNumber, l as PrefNumberControl, m as PrefNumberUnit, n as PrefObject, o as PrefPaint, p as PrefRenderContext, P as PrefRenderer, q as PrefString, r as PrefStringControl } from '../_dts/PrefsForm-BkUJZx0A.js';
3
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { M as ModeDefinition, T as ToolPrefLeaf, a as ToolPrefGroup, e as ToolsApi } from '../_dts/weasel-canvas-FJTFi3ZZ.js';
3
+ export { f as BuiltinPref, g as PrefBoolean, h as PrefBooleanControl, i as PrefColor, j as PrefCustom, k as PrefEnum, l as PrefEnumControl, m as PrefEnumEncoding, n as PrefKind, o as PrefNumber, p as PrefNumberControl, q as PrefNumberUnit, r as PrefObject, s as PrefPaint, t as PrefString, u as PrefStringControl } from '../_dts/weasel-canvas-FJTFi3ZZ.js';
4
4
  import * as react from 'react';
5
- import { MutableRefObject, ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, KeyboardEvent, RefObject, PointerEvent as PointerEvent$1, RefCallback } from 'react';
5
+ import { ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, KeyboardEvent, RefObject, PointerEvent, RefCallback } from 'react';
6
6
  export { LayerStack, LayerStackItem, LayerStackProps } from '../ui/layers/index.js';
7
- import { O as Op, a as NodeId, P as Path, S as Scene, H as History } from '../_dts/types-C-gh9Ap-.js';
8
- import { V as View, D as DrawCommand } from '../_dts/DrawCommand-B3bskUsC.js';
9
- import { B as Bounds } from '../_dts/fitViewToBounds-dZ2UDB6e.js';
10
- import { K as KitInsertShape } from '../_dts/shapeKinds-Cx_rxwsa.js';
7
+ export { a as PrefRenderContext, P as PrefRenderer } from '../_dts/PrefsForm.d-CgxUequc.js';
11
8
  import { TextFieldProps, ValidationResult, CheckboxProps as CheckboxProps$1, SwitchProps as SwitchProps$1, TabProps as TabProps$1, TabListProps as TabListProps$1, TabPanelProps as TabPanelProps$1, TabsProps as TabsProps$1, RadioProps as RadioProps$1, RadioGroupProps as RadioGroupProps$1, NumberFieldProps as NumberFieldProps$1, SelectProps as SelectProps$1, ListBoxItemProps, ComboBoxProps as ComboBoxProps$1, SliderProps as SliderProps$1, ModalOverlayProps, DialogProps as DialogProps$1, PopoverProps } from 'react-aria-components';
12
9
  export { DialogTrigger as CalloutTrigger } from 'react-aria-components';
13
10
 
@@ -44,2855 +41,111 @@ declare function isDebugEnabled(namespace: string): boolean;
44
41
  * Each call is prefixed with `[namespace]` so the source is searchable. */
45
42
  declare function dlog(namespace: string, ...args: unknown[]): void;
46
43
 
47
- /**
48
- * 2D affine transforms in canvas/DOMMatrix order: [a, b, c, d, e, f].
49
- * x' = a·x + c·y + e
50
- * y' = b·x + d·y + f
51
- * Represented as a 6-element number[] (f64). The affine tier of the kernel.
52
- *
53
- * Convention alignment: the renderer already has a `Mat3` in
54
- * `src/renderer/math/mat3.ts`. That one is a 9-element column-major
55
- * `Float32Array` (a full 3×3) shaped for `uniformMatrix3fv` — a deliberately
56
- * different *representation* for the WebGL upload path. Its *logical element
57
- * order* is identical to ours: `create(a, b, c, d, tx, ty)` maps
58
- * `x' = a·x + c·y + tx`, `y' = b·x + d·y + ty` (canvas/DOMMatrix a,b,c,d,e,f).
59
- * We keep the pure 6-tuple f64 form here (the kernel form); the 9-element f32
60
- * form stays a render-layer concern. No second logical convention is created.
61
- */
62
- type Mat3 = number[];
63
-
64
- /**
65
- * Snapshot of modifier-key state at gesture dispatch.
66
- *
67
- * Lives in core rather than beside the gesture types that produce it because
68
- * `core/selection/chromeState.ts` reads it, and core may not import from
69
- * `interactions/`. Re-exported from `interactions/gestures/types.ts`, which
70
- * is still where gesture code names it.
71
- */
72
- interface ModifierState {
73
- alt: boolean;
74
- shift: boolean;
75
- meta: boolean;
76
- ctrl: boolean;
77
- }
78
-
79
- /**
80
- * @experimental
81
- * Result of an affordance hit — what the region computed about itself.
82
- *
83
- * `initialScratch` is the payload: what the region already knows (which
84
- * corner, which target id) so the action that picks up the drag doesn't
85
- * re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
86
- * it into `AffordanceHit`, which flows to the matching action through
87
- * `InvocationCtx.drag.affordance`.
88
- *
89
- * This used to also carry a `drag: DragChannel` naming the handlers the
90
- * tool-routing dispatcher should wire up. Every implementation supplied a
91
- * no-op stub that claimed, because the real routing had already moved to
92
- * bindings; the field went with that dispatcher.
93
- */
94
- interface AffordanceBinding<TScratch = unknown> {
95
- initialScratch?: TScratch;
96
- }
97
- /**
98
- * What a **registered layer's** `hitTest` returns. Extends `AffordanceBinding`
99
- * so existing implementations keep typechecking; the added fields are how a
100
- * consumer's own chrome says the things kit chrome says through
101
- * `AffordanceRegion` — which cursor to show, and whether it owns the point
102
- * outright.
103
- */
104
- interface LayerHit<TScratch = unknown> extends AffordanceBinding<TScratch> {
105
- /** CSS cursor while the pointer is over this hit. Reaches the hover-cursor
106
- * pump as `AffordanceHit.cursor`, the same path kit chrome uses. */
107
- cursor?: string;
108
- /** `'exclusive'` bars every binding whose target doesn't consult the
109
- * affordance. Omitted means `'shared'` — today's behavior. Same name and
110
- * meaning as `AffordanceHit.strength`, which it becomes. */
111
- strength?: 'exclusive' | 'shared';
112
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
113
- claimedKinds?: readonly ClaimableGesture[];
114
- }
115
- /**
116
- * Gesture kinds an affordance claim can bar, in the spec vocabulary bindings
117
- * are written in. `'pointer'` is one token because `pointerDown` / `click` /
118
- * `drag` are a single press protocol — at the event level the first two are
119
- * the same `kind: 'pointerdown'`, told apart only by `stage`.
120
- */
121
- type ClaimableGesture = 'pointer' | 'doubleClick' | 'contextMenu' | 'longPress' | 'wheel';
122
-
123
- /**
124
- * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
125
- * canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
126
- * supplies it explicitly so layers don't have to know about DPR.
127
- */
128
- interface Dims {
129
- width: number;
130
- height: number;
131
- }
132
- /**
133
- * A single named render sub-layer within a canvas renderer.
134
- *
135
- * @template TData - The data object passed to each draw call.
136
- */
137
- interface RenderLayer<TData> {
138
- /** Unique identifier used in visibility maps and ordering arrays. When a
139
- * cache is in use, an id must identify the same logical layer across
140
- * frames — reusing it for a different layer can serve cross-layer commands. */
141
- id: string;
142
- /** Human-readable name for UI toggles. */
143
- label: string;
144
- /**
145
- * Emit a DrawCommand tree for the GL backend to dispatch.
146
- *
147
- * For world-space layers (the default), emit commands in WORLD COORDS —
148
- * `drawLayers` automatically wraps them in `{ kind: 'group', transform:
149
- * viewToMat3(view), ... }` before handing them to the renderer. Do NOT
150
- * apply the view transform yourself.
151
- *
152
- * For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
153
- * coords directly; `drawLayers` passes them through unchanged. If part
154
- * of a screen-space layer's output needs to track the view, wrap that
155
- * subset manually with `viewToMat3(view)`.
156
- */
157
- draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
158
- /**
159
- * Optional cache key. When present and a `LayerCommandCache` is supplied to
160
- * `drawLayers`, the layer's previous `DrawCommand[]` is reused as long as
161
- * every entry is `Object.is`-equal to the previous call's. A layer with no
162
- * `deps` rebuilds on every frame.
163
- *
164
- * **The returned commands must be treated as immutable.** A cached tree is
165
- * handed to the renderer again on later frames, so mutating a tree you
166
- * previously returned corrupts the cache silently rather than erroring.
167
- *
168
- * **Screen-space layers are not protected against a stale `view`/`dims`
169
- * the way world-space layers are** (see `space` below) — include them in
170
- * `deps` if `draw` reads them.
171
- */
172
- deps?: (data: TData, view: View, dims: Dims) => readonly unknown[];
173
- /**
174
- * Whether the layer is shown when no explicit visibility entry exists.
175
- * Defaults to `true` when absent.
176
- */
177
- defaultVisible?: boolean;
178
- /**
179
- * When true, the layer is always drawn regardless of the visibility map.
180
- * Useful for layers that must never be hidden (e.g. base grid).
181
- */
182
- alwaysOn?: boolean;
183
- /**
184
- * Coordinate space the layer draws in.
185
- *
186
- * - `'world'` (default): the layer's `draw` returns world-space commands;
187
- * `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
188
- * automatically.
189
- * - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
190
- * commands; `drawLayers` passes them through unchanged. World-anchored
191
- * chrome inside a screen-space layer must call `worldToScreen` or wrap
192
- * the relevant subset with `viewToMat3(view)` manually.
193
- */
194
- space?: 'world' | 'screen';
195
- /**
196
- * Optional hit-test for **consumer-attached** layers.
197
- *
198
- * Only layers registered through `CanvasExtensionApi.registerLayer` are
199
- * hit-tested: `hitTestExtras` walks them last-registered-first on
200
- * pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
201
- * thunk ahead of the kit's own selection chrome. First non-null result
202
- * wins; null means "I don't claim this hit, try the next layer."
203
- *
204
- * Layers that reach the draw stack some other way — a `Tool.overlay`, an
205
- * entry in the `layers` map — are painted but never hit-tested, so defining
206
- * `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
207
- * goes through `buildAffordanceAt`.)
208
- *
209
- * Coordinates are world-space. The `data` arg is the layer's
210
- * configured data slot (same as `draw`); `view` and `dims` mirror
211
- * `draw`'s arguments.
212
- */
213
- hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
214
- /** Chrome-caps visibility predicate. When supplied, the layer must
215
- * not return a hit from any chrome element whose id reports
216
- * `false`. Absent → every element is hittable. */
217
- isVisible?: (id: string) => boolean) => LayerHit | null;
218
- /**
219
- * Called on every pointermove when no gesture is currently captured.
220
- * Lets layers (e.g. HUD widgets) track hover state without participating
221
- * in the drag pipeline. Coords are world-space; the layer is responsible
222
- * for any further conversion (e.g. world→screen for screen-space layers)
223
- * and for its own throttling.
224
- */
225
- onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
226
- /**
227
- * Called when the cursor leaves the canvas element. Lets layers clear
228
- * any hover state they're holding.
229
- */
230
- onUncapturedLeave?: () => void;
231
- }
232
-
233
- /**
234
- * Facts about the device the canvas is running on.
235
- *
236
- * One object, recomputed when the underlying media queries change, read by
237
- * two consumers: the chrome-caps rule layer (via `RuleCtx.device`) and the
238
- * handle-sizing constants (via `targetScale`).
239
- *
240
- * Deliberately NOT a form-factor concept. There is no `isPhone` here and
241
- * there should never be one: chrome layout is the consuming app's decision.
242
- * The kit's job is to stop assuming a mouse.
243
- */
244
- interface DeviceProfile {
245
- /** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
246
- readonly coarsePointer: boolean;
247
- /** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
248
- readonly canHover: boolean;
249
- /** Live device pixel ratio. */
250
- readonly dpr: number;
251
- /** Multiplier for handle sizes and hit radii. Derived from
252
- * `coarsePointer` unless explicitly overridden. */
253
- readonly targetScale: number;
254
- }
255
-
256
- /** A layout container's extent in world units. */
257
- type ContainerBounds = {
258
- x: number;
259
- y: number;
260
- width: number;
261
- height: number;
262
- };
263
- /** A child a layout strategy is arranging. */
264
- interface LayoutChild<TPose> {
265
- id: string;
266
- pose: TPose;
267
- }
268
- /** One place a dragged child could land. A strategy offers these as the drag
269
- * moves, a `LayoutSnap` picks between them, and the chosen one decides both
270
- * the preview and the committed poses. */
271
- interface DropTarget<TPose> {
272
- /** Where the dragged child lands if this target is picked. */
273
- pose: TPose;
274
- /** Reference point for distance metrics (snap algorithms). */
275
- origin: {
276
- x: number;
277
- y: number;
278
- };
279
- /** Optional axis-aligned region (world units) used by region-aware snaps
280
- * (e.g. `containedThenNearest`). When present, a pointer inside this rect
281
- * is treated as a containment hit on this target. Strategies that emit
282
- * region-shaped targets (gutters, drop-zones) should populate this.
283
- * Strategies whose targets are point-like (free-form, snap-point) can omit
284
- * it and rely on `origin`-distance snaps. */
285
- hitBounds?: {
286
- x: number;
287
- y: number;
288
- width: number;
289
- height: number;
290
- };
291
- /** Strategy-private metadata (e.g. cell coords for tile-grid). */
292
- meta?: unknown;
293
- }
294
- /** Chooses which of a strategy's drop targets the pointer means, or `null`
295
- * to reject the drop. Separate from the strategy so the same arrangement can
296
- * be paired with different snapping rules. */
297
- interface LayoutSnap<TPose> {
298
- pickTarget(targets: DropTarget<TPose>[], pointer: {
299
- x: number;
300
- y: number;
301
- }): DropTarget<TPose> | null;
302
- }
303
- /** The container a layout strategy is arranging children within. */
304
- interface LayoutContainer {
305
- id: string;
306
- bounds: ContainerBounds;
307
- }
308
- /** The child currently being dragged: where it started, where the pointer
309
- * currently proposes it goes, and which container it came from. */
310
- interface LayoutDragged<TPose> {
311
- id: string;
312
- /** The pose the dragged child currently has (pre-drop). */
313
- originPose: TPose;
314
- /** The pose the gesture proposes (pointer-driven, pre-snap). */
315
- pose: TPose;
316
- sourceContainerId: string | null;
317
- }
318
- /**
319
- * How a container arranges its children, and what happens when one is dragged
320
- * into or around it.
321
- *
322
- * The four required methods cover the whole cycle: `childPoses` is the resting
323
- * arrangement, `getDropTargets` enumerates where a drag could land,
324
- * `reflowPoses` is the live preview once a target is picked, and `commitDrop`
325
- * turns the result into ops so the drop is undoable.
326
- */
327
- interface LayoutStrategy<TPose> {
328
- childPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>): Map<string, TPose>;
329
- getDropTargets(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>): DropTarget<TPose>[];
330
- reflowPoses(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Map<string, TPose>;
331
- commitDrop(container: LayoutContainer, children: ReadonlyArray<LayoutChild<TPose>>, dragged: LayoutDragged<TPose>, target: DropTarget<TPose> | null): Op[];
332
- snap: LayoutSnap<TPose>;
333
- /** Optional: predicate for whether a world-space point is inside this
334
- * container. When absent, callers fall back to an axis-aligned bounding-box
335
- * test on the container's pose. Strategies whose containers aren't
336
- * rectangular (circles, irregular zones) implement this to override the
337
- * AABB default. */
338
- contains?(containerPose: TPose, point: {
339
- x: number;
340
- y: number;
341
- }): boolean;
342
- /** Optional: reject a drag before any drop-target work happens. A type-aware
343
- * container (a palette that only takes swatches, say) returns `false` and
344
- * the drag falls through to whatever container is under it next. When
345
- * absent, every drag is considered — rejection is still possible later, by
346
- * `snap.pickTarget` returning null. */
347
- acceptsDrop?(container: LayoutContainer, dragged: LayoutDragged<TPose>): boolean;
348
- }
349
-
350
- /**
351
- * Opaque clipboard payload. `items` is `unknown[]` so each app's clipboard
352
- * adapter stores whatever shape it wants; the kit never inspects entries.
353
- *
354
- * The adapter is responsible for both producing snapshots
355
- * (`snapshotSelection`) and consuming them (`commitPaste`). Type safety lives
356
- * at that boundary, not in the kit.
357
- */
358
- interface ClipboardSnapshot {
359
- items: unknown[];
360
- }
361
- /**
362
- * SnapTarget — where a dragged node would re-parent to if released.
363
- *
364
- * `slotPose` is the pose (in world coordinates) the node should snap to
365
- * within the target. `metadata` is an opaque pass-through for app-specific
366
- * snap details (slot index, visual hint, etc.).
367
- */
368
- interface SnapTarget<TPose = unknown> {
369
- parentId: string;
370
- slotPose: TPose;
371
- metadata?: unknown;
372
- }
373
- /**
374
- * Narrow adapter for `useMove`. Includes optional snap-target
375
- * lookup; apps without container-snapping leave it out.
376
- */
377
- interface MoveAdapter<TNode extends {
378
- id: string;
379
- }, TPose> {
380
- getNode(id: string): TNode | undefined;
381
- /** Enumerate all nodes. `<Canvas>` derives a default rect-pose `pickEvery`
382
- * and the scene-iteration loop from this. */
383
- getNodes(): TNode[];
384
- getPose(id: string): TPose;
385
- /** Optional. Required only by hierarchy-aware paths: layout-pass drop
386
- * targeting (`getLayout` present), nested-hit collapse
387
- * (`pickTopMostHit`), and group-pose composition. Flat scenes may omit. */
388
- getParent?(id: string): string | null;
389
- /** Optional paint depth, read by `pickTopMostHit` to resolve two *siblings*
390
- * whose bodies both cover the pointer. Without it the hit list's own order
391
- * decides, which is right for a back-to-front walk and wrong for anything
392
- * else. See `PickTopMostHitAdapter` for the `compareZ` alternative. */
393
- getZIndex?(id: string): number | null | undefined;
394
- compareZ?(a: string, b: string): number;
395
- setPose(id: string, pose: TPose): void;
396
- /** Optional. Used only by reparent ops (e.g. drag-into-container drops via
397
- * layout strategies). Flat scenes that never reparent may omit. */
398
- setParent?(id: string, parentId: string | null): void;
399
- /** Optional: see SceneAdapter.applyOps. */
400
- applyOps?(ops: Op[], label: string): void;
401
- findSnapTarget?(draggedId: string, worldX: number, worldY: number): SnapTarget<TPose> | null;
402
- /** Optional: ordered children of `parentId`, `null` for the root siblings.
403
- * One contract with {@link OrderedAdapter.getChildren} — the two land on
404
- * the same adapter object, and an implementation that answers only node
405
- * ids returns `[]` for the root, which the ops read as "no siblings".
406
- *
407
- * When present (alongside the `cascadeWorldPose` option on `useMove`),
408
- * dragging a node auto-cascades its descendants in the live overlay so
409
- * structurally-grouped children visually follow the parent during the
410
- * drag. No additional ops are generated — children's local poses don't
411
- * change when the parent's local pose moves. */
412
- getChildren?(parentId: string | null): string[];
413
- /** Optional: layout strategy attached to a container, or null if the
414
- * container uses absolute positioning (default behavior). When present,
415
- * `useMove` uses the strategy to compute drop targets, sibling reflow,
416
- * and the commit op batch when a drag ends over the container. */
417
- getLayout?(containerId: string): LayoutStrategy<TPose> | null;
418
- }
419
- /**
420
- * Narrow adapter for `useInsert` and `useClipboardOps`. The kit knows
421
- * nothing about what tool is active or what shape to construct; it asks the
422
- * adapter to produce node(s) given gesture or paste inputs.
423
- *
424
- * Drag-rectangle path: `commitInsert(bounds)` returns one new node or null.
425
- * Clipboard paste path: `commitPaste(clipboard, offset)` returns the array of
426
- * newly-materialized nodes (in order). Both empty array and array of
427
- * length N are valid; the kit wraps each entry in an `InsertOp`.
428
- *
429
- * `snapshotSelection(ids)` builds the payload that paste later consumes.
430
- * `getPasteOffset` is optional; the kit defaults to a fixed grid-cell offset
431
- * supplied by the consumer (passed to `useClipboardOps` options if needed; see
432
- * the hook for resolution order).
433
- */
434
- interface InsertAdapter<TNode extends {
435
- id: string;
436
- }> {
437
- /** Materialize a new node from drag-rect bounds (drag-to-insert tools).
438
- * Optional — hooks that don't drive insertion (e.g. `useClone`,
439
- * read-only clipboard) won't call it, and `sceneToAdapter` only fills
440
- * it in when `options.commitInsert` is supplied. Hooks that *do* call
441
- * it (insert tools) document the requirement at their own surface. */
442
- commitInsert?(bounds: {
443
- x: number;
444
- y: number;
445
- width: number;
446
- height: number;
447
- }): TNode | null;
448
- /** Materialize one or more new nodes from a clipboard snapshot. Optional
449
- * — required by `useClipboard.paste`, ignored by other consumers. */
450
- commitPaste?(clipboard: ClipboardSnapshot, offset: {
451
- dx: number;
452
- dy: number;
453
- }, ctx?: {
454
- dropPoint?: {
455
- worldX: number;
456
- worldY: number;
457
- };
458
- }): TNode[];
459
- /** Snapshot the current selection into the clipboard payload shape.
460
- * Optional — required by `useClipboard.copy` / `cut` and `useClone`'s
461
- * ghost capture, ignored by other consumers. */
462
- snapshotSelection?(ids: string[]): ClipboardSnapshot;
463
- getPasteOffset?(clipboard: ClipboardSnapshot): {
464
- dx: number;
465
- dy: number;
466
- };
467
- /** Mutator wired by `insertNode`-using ops (kit-side InsertOp). `index`
468
- * carries the z-position a delete captured, so undo restores paint order;
469
- * see `SceneAdapter.insertNode`. */
470
- insertNode(node: TNode, index?: number): void;
471
- /** Mutator wired by `setSelection` ops batched alongside paste. */
472
- setSelection(ids: string[]): void;
473
- /** Optional: see SceneAdapter.applyOps. */
474
- applyOps?(ops: Op[], label: string): void;
475
- /** Returns the current selection. Used by clone behaviors. */
476
- getSelection(): string[];
477
- }
478
-
479
- /** Pointer position in both world and client coords. */
480
- interface PointerState {
481
- worldX: number;
482
- worldY: number;
483
- clientX: number;
484
- clientY: number;
485
- }
486
- /**
487
- * Per-gesture context passed to behaviors. `current` is the running pose
488
- * map; behaviors mutate proposed poses by returning new TPose values from
489
- * onMove. `scratch` is per-gesture key/value storage that resets at the
490
- * next gesture start.
491
- */
492
- interface GestureContext<TPose, TNode extends {
493
- id: string;
494
- } = {
495
- id: string;
496
- }> {
497
- draggedIds: string[];
498
- origin: Map<string, TPose>;
499
- current: Map<string, TPose>;
500
- snap: SnapTarget<TPose> | null;
501
- modifiers: ModifierState;
502
- pointer: PointerState;
503
- adapter: MoveAdapter<TNode, TPose>;
504
- /**
505
- * Per-gesture mutable store. Keys should be namespaced by behavior name to avoid
506
- * collisions: `'behaviorName'` for a single value, `'behaviorName.field'` for
507
- * sub-keys. Two behaviors sharing a key will silently clobber each other.
508
- */
509
- scratch: Record<string, unknown>;
510
- }
511
- /**
512
- * Generalized base behavior. Each hook defines an alias that pins the
513
- * proposed-pose shape (TProposed) and the onMove return shape (TMoveResult).
514
- * onEnd is uniform: first non-undefined return wins (Op[] = commit those,
515
- * null = abort, undefined = defer).
516
- *
517
- * `defaultTransient`: when at least one behavior in a gesture sets this true
518
- * AND the hook's `options.transient` is not explicitly set, the gesture
519
- * commits its ops via `adapter.applyOps(ops)` (no history entry). When
520
- * `options.transient` is set explicitly, that value wins.
521
- */
522
- interface ActionBehavior<TPose, TProposed, TMoveResult> {
523
- defaultTransient?: boolean;
524
- onStart?(ctx: GestureContext<TPose>): void;
525
- onMove?(ctx: GestureContext<TPose>, proposed: TProposed): TMoveResult | void;
526
- onEnd?(ctx: GestureContext<TPose>): Op[] | null | void;
527
- }
528
- /** Which corner/edge of the rect stays fixed during a resize. */
529
- type ResizeAnchor = {
530
- x: 'min' | 'max' | 'free';
531
- y: 'min' | 'max' | 'free';
532
- };
533
- /** Minimum rect-shaped pose required by the resize machinery. */
534
- interface ResizePose {
535
- x: number;
536
- y: number;
537
- width: number;
538
- height: number;
539
- }
540
- /** Per-frame proposed resize: pose plus the anchor pinning the opposite corner. */
541
- interface ResizeProposed<TPose extends ResizePose> {
542
- pose: TPose;
543
- anchor: ResizeAnchor;
544
- }
545
- /** Per-frame result a `BoundsConstraint.onMove` can return to override the proposed pose. */
546
- interface ResizeMoveResult<TPose extends ResizePose> {
547
- pose?: TPose;
548
- }
549
- /** A bounds-frame constraint plugged into `useResize` / `resizeAction`.
550
- * Reads/writes `{x,y,width,height}` and can override the proposed pose
551
- * on each frame (e.g. lock-aspect, clamp-min-size, snap-to-grid). */
552
- type BoundsConstraint<TPose extends ResizePose> = ActionBehavior<TPose, ResizeProposed<TPose>, ResizeMoveResult<TPose>>;
553
- /** Frames a point-snap behavior can return for the hook to back-solve. */
554
- type PointSnapFrame = 'dragged-corner' | 'fixed-corner' | 'center' | 'origin';
555
- /** Per-frame world-space context handed to `PointSnapBehavior.onMove`.
556
- * `draggedCorner` and `fixedCorner` are `null` for edge drags
557
- * (`anchor.x === 'free'` or `anchor.y === 'free'`). `center` and
558
- * `origin` are always present. */
559
- interface PointSnapContext<TPose extends ResizePose> {
560
- draggedCorner: {
561
- worldX: number;
562
- worldY: number;
563
- } | null;
564
- fixedCorner: {
565
- worldX: number;
566
- worldY: number;
567
- } | null;
568
- center: {
569
- worldX: number;
570
- worldY: number;
571
- };
572
- origin: {
573
- worldX: number;
574
- worldY: number;
575
- };
576
- rotation: number;
577
- anchor: ResizeAnchor;
578
- proposed: TPose;
579
- modifiers: ModifierState;
580
- }
581
- /** Per-frame snap result. A behavior returns at most one. */
582
- interface PointSnapResult {
583
- frame: PointSnapFrame;
584
- worldX: number;
585
- worldY: number;
586
- }
587
- /** A point-snap behavior plugged into `useResize`'s `pointSnapBehaviors`. */
588
- interface PointSnapBehavior<TPose extends ResizePose> {
589
- id?: string;
590
- onMove(ctx: PointSnapContext<TPose>): PointSnapResult | null | undefined;
591
- }
592
-
593
- /** The full vocabulary of capability tags shipped in the default preset.
594
- * Apps and other consumers can add their own tags; this list is what
595
- * `weasel-modes` itself uses. */
596
- declare const ALL_TAGS: readonly ["navigation", "creates-selection", "creates-paths", "creates-shapes", "creates-text", "edits-anchors", "edits-text", "transforms-selection", "samples-color", "applies-fill", "edits-page"];
597
- /** One capability a tool or contribution declares, and a mode allows. Any
598
- * string is accepted so apps can add tags of their own; `ALL_TAGS` is the
599
- * set this package ships. */
600
- type CapabilityTag = (typeof ALL_TAGS)[number] | (string & {});
601
-
602
- /** How a mode tints the workspace — the area around the page — so the user
603
- * can see at a glance which mode is active. */
604
- interface WorkspaceVisual {
605
- tint?: string;
606
- gradient?: 'top-down' | 'bottom-up';
607
- intensity?: number;
608
- }
609
- /**
610
- * A mode: an app-level editing context that narrows which tools are usable and
611
- * how the workspace looks. Tools live inside modes; a tool is never "in" one.
612
- *
613
- * `kind` picks the lifecycle. A `soft` mode (path-edit, isolation, text-edit)
614
- * is a scoped context with no commit ceremony — every edit inside it is
615
- * independently undoable and `exit` is non-destructive. A `strict` mode
616
- * (free-transform, crop) is a transaction: the whole session collapses to one
617
- * undoable step and leaving requires an explicit `commit` or `cancel`.
618
- */
619
- interface ModeDefinition {
620
- id: string;
621
- kind: 'soft' | 'strict';
622
- /** Capability tags this mode allows beyond IMPLICIT_TAGS. */
623
- allows: CapabilityTag[];
624
- /** When true, out-of-target objects dim at the renderer layer. */
625
- scoping: boolean;
626
- workspace?: WorkspaceVisual;
627
- entry?: {
628
- shortcut?: string;
629
- trigger?: 'double-click-target';
630
- };
631
- exit?: {
632
- shortcut?: string;
633
- };
634
- commit?: {
635
- shortcut?: string;
636
- };
637
- cancel?: {
638
- shortcut?: string;
639
- };
640
- }
641
-
642
- /** Holds the set of available modes and which one is active, and notifies
643
- * subscribers when that changes. `getVersion` is a monotonic counter for
644
- * render-cache invalidation. Unknown mode ids throw rather than being
645
- * ignored. */
646
- interface ModeRegistry {
647
- current(): ModeDefinition;
648
- setMode(id: string): void;
649
- byId(id: string): ModeDefinition;
650
- getVersion(): number;
651
- subscribe(listener: () => void): () => void;
652
- }
653
-
654
- /**
655
- * Live state read by rule evaluation. Built once per frame on the consuming
656
- * surface — chrome-caps, the affordance pipeline, the dispatcher's
657
- * eligibility filter — and discarded.
658
- *
659
- * Adding a new field is additive: existing rules don't change, new
660
- * selector atoms can read it.
661
- */
662
- interface RuleCtx {
663
- readonly focused: boolean;
664
- readonly selection: readonly NodeId[];
665
- readonly multiActive: boolean;
666
- readonly modifiers: ModifierState;
667
- readonly action: {
668
- readonly kind: string | null;
669
- readonly id: string | null;
670
- };
671
- readonly hover: NodeId | null;
672
- readonly view: View;
673
- /** Active mode id. `'normal'` when no non-default mode is engaged. */
674
- readonly mode: string;
675
- /** Capability tags allowed by the active mode (the union of
676
- * `ModeDefinition.allows` plus implicit tags). The `capability:`
677
- * selector reads this to determine whether a tag is permitted. */
678
- readonly allowedCapabilities: ReadonlySet<CapabilityTag>;
679
- /** Whether the current selection may be resized. `<SceneCanvas>` folds
680
- * `selectTool.resize.resizable` over the selection (true only when every
681
- * selected node is resizable). Read by the `resizable:` selector to gate
682
- * `selection.resize-handles`. Absent (legacy ctx builders) is treated as
683
- * resizable — back-compat: handles show unless a consumer opts a node out. */
684
- readonly selectionResizable?: boolean;
685
- /** Whether a path is currently in anchor-edit mode. Read by the
686
- * `editingAnchors:` selector, which gates the path-edit chrome.
687
- *
688
- * This is deliberately a fact about state, not about permission: the
689
- * anchor overlay and the anchor hit-test must agree, and the thing they
690
- * must agree on is "is there an edited path right now", which no
691
- * capability or mode id answers. A mode that allows `edits-anchors`
692
- * with nothing being edited should draw no anchors. Absent is treated
693
- * as false. */
694
- readonly editingAnchors?: boolean;
695
- /** Device facts — pointer coarseness, hover capability, density.
696
- *
697
- * Absent (legacy ctx builders) is treated as
698
- * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
699
- * density 1. That is what the kit assumed before this field existed, so
700
- * an absent profile is behavior-preserving by construction. */
701
- readonly device?: DeviceProfile;
702
- }
703
-
704
- /**
705
- * A selector is a conjunction of key/value tests. Multiple keys at the same
706
- * level AND together. Each key maps to a selector primitive in the evaluator.
707
- */
708
- interface Selector {
709
- selection?: {
710
- is?: number;
711
- atLeast?: number;
712
- empty?: boolean;
713
- };
714
- mode?: string | {
715
- not: string;
716
- } | {
717
- in: readonly string[];
718
- };
719
- capability?: CapabilityTag | readonly CapabilityTag[] | {
720
- in: readonly CapabilityTag[];
721
- } | {
722
- not: CapabilityTag;
723
- };
724
- gesturing?: boolean;
725
- actionIs?: string;
726
- modifierHeld?: keyof ModifierState;
727
- focused?: boolean;
728
- hovering?: boolean;
729
- hoveringSelected?: boolean;
730
- zoomAtLeast?: number;
731
- /** Matches `ctx.editingAnchors` — true while a path is in anchor-edit
732
- * mode. Absent flag is treated as `false`. */
733
- editingAnchors?: boolean;
734
- /** Matches `ctx.selectionResizable`. Absent flag is treated as `true`
735
- * (resizable), so `{ resizable: true }` passes for legacy ctx builders
736
- * that don't compute it. */
737
- resizable?: boolean;
738
- /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
739
- * (touch, most styluses). Absent device is treated as `false`. */
740
- coarsePointer?: boolean;
741
- /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
742
- * device is treated as `true`. */
743
- canHover?: boolean;
744
- }
745
- /**
746
- * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
747
- * over `Selector` leaves. `when` is the escape hatch — its closure is
748
- * opaque to introspection and should be avoided when a declarative form
749
- * exists. Empty `all` is true; empty `any` is false.
750
- */
751
- type Rule = Selector | {
752
- all: readonly Rule[];
753
- } | {
754
- any: readonly Rule[];
755
- } | {
756
- not: Rule;
757
- } | {
758
- when: (ctx: RuleCtx) => boolean;
759
- };
760
-
761
- /**
762
- * Composable visibility predicate with fluent surface. Carries its underlying
763
- * `Rule` tree at `.rule` so the resolver can introspect / share trees with
764
- * the affordance pipeline and the dispatcher's eligibility filter.
765
- *
766
- * Callable form `cond(ctx)` evaluates the tree against ctx. The fluent
767
- * methods return new Conditions wrapping new trees.
768
- *
769
- * **Chain semantics: strict left-to-right, no precedence.**
770
- * `a.and(b).or(c)` is `(a && b) || c`; `a.or(b).and(c)` is
771
- * `(a || b) && c`. Mix `.and` and `.or` only when you mean
772
- * left-to-right evaluation. For grouped disjunction, name the
773
- * subexpression or use the top-level `or(...)`.
774
- */
775
- interface Condition {
776
- (ctx: RuleCtx): boolean;
777
- readonly rule: Rule;
778
- /** `this && other` */
779
- and(other: Condition | Rule): Condition;
780
- /** `this || other` */
781
- or(other: Condition | Rule): Condition;
782
- /** `this && !other` */
783
- andNot(other: Condition | Rule): Condition;
784
- /** `this || !other` */
785
- orNot(other: Condition | Rule): Condition;
786
- }
787
-
788
- /** Phase of a gesture lifecycle. `initial` means the tool is idle
789
- * (scratch null); `engaged` means a gesture is in progress (scratch
790
- * populated). The route-grammar's `[phase]` slot draws from this set. */
791
- type RoutePhase = 'initial' | 'engaged';
792
-
793
- /**
794
- * Route-string grammar v3:
795
- *
796
- * route = phaseSlot WS gesture WS argSlot? WS targetSlot? WS modSlot?
797
- * phaseSlot = '[' phaseList ']'
798
- * phaseList = phaseAtom (WS ',' WS phaseAtom)*
799
- * phaseAtom = (channel ':')? phaseValue -- bare phaseValue ≡ '&:phaseValue'
800
- * channel = '&' | '*' | toolId -- '&' = the binding's own tool
801
- * phaseValue = 'initial' | 'engaged' | '*'
802
- * argSlot = '(' argValue ')' -- whitespace inside parens is significant
803
- * targetSlot = '=>' WS targetValue -- omitted slot defaults to '*' for hasTarget
804
- * modSlot = modAtom (WS modAtom)*
805
- * modAtom = sigil modName
806
- * sigil = '+' | '?' -- ! @ # $ % ^ & * reserved as id-prefix
807
- * modName = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
808
- *
809
- * Shorthand: a bare phaseValue (no `:`) implies channel `&` ("this tool's
810
- * own phase"). `[engaged]` ≡ `[&:engaged]`; `[*]` ≡ `[&:*]`. The truly-loose
811
- * form (any channel, any phase) is `[*:*]`.
812
- *
813
- * Examples:
814
- * [initial] click => empty +shift -- self idle
815
- * [engaged] wheel -- self mid-gesture
816
- * [rect:engaged] wheel -- when rect tool is mid-gesture
817
- * [*:engaged] keyDown(Delete) -- when any tool is mid-gesture
818
- * [initial,engaged] contextMenu => empty -- either self phase
819
- * [*] click => empty -- self, any phase
820
- */
821
-
822
- /** Channel reference for a phase atom. `'&'` = the binding's own tool;
823
- * `'*'` = any tool; otherwise a registered tool id. */
824
- type ChannelRef = '&' | '*' | string;
825
- /** One element of a phase list: a (channel, phase) pair. The default
826
- * channel (omitted in the shorthand) is `'&'`. `phase: '*'` means
827
- * "any phase of the given channel". */
828
- interface PhaseAtom {
829
- channel: ChannelRef;
830
- phase: RoutePhase | '*';
831
- }
832
-
833
- /**
834
- * GestureSpec — describes the form of a user input event that can fire an action.
835
- *
836
- * Used by `Action.defaultBinding` (the action's preferred gesture) and by
837
- * `GestureBinding.spec` (a tool's binding table entry). The dispatcher matches
838
- * incoming input events against registered specs to determine which action to
839
- * invoke.
840
- *
841
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md` § "Types".
842
- */
843
- /** Optional modifier-key requirement for a gesture spec.
844
- *
845
- * Matching semantics (strict): an omitted modifier field means the
846
- * modifier MUST NOT be held — i.e., a bare `{ kind: 'key', key: 'Escape' }`
847
- * matches only unmodified Escape, NOT Cmd+Escape. A `true` means the
848
- * modifier MUST be held; `false` is the same as omitted (must be absent).
849
- * This mirrors today's `KeyBinding` matcher and keeps conflict detection
850
- * coherent.
851
- *
852
- * `mod` is a platform-aware shorthand: matches `metaKey` on mac, `ctrlKey`
853
- * elsewhere (mirrors `KeyBinding.mod`).
854
- *
855
- * `shift` additionally accepts `'optional'` meaning "shifted or unshifted
856
- * both acceptable" — the explicit opt-in for loose matching, used by
857
- * actions like nudge whose step size depends on shift but whose firing
858
- * does not. To widen other modifiers similarly, extend their type when
859
- * a real consumer needs it.
860
- */
861
- type ModSpec = Partial<{
862
- alt: boolean | 'optional';
863
- ctrl: boolean | 'optional';
864
- meta: boolean | 'optional';
865
- mod: boolean | 'optional';
866
- shift: boolean | 'optional';
867
- }>;
868
- /** The predicate form of {@link TargetSpec}. `hit` is the raw target
869
- * (affordance for drag, `e.target` otherwise); `bodyTarget` is the optional
870
- * body-class string ('empty' | 'selected-body' | 'unselected-body') when
871
- * `classifyTarget` is wired. Predicates that only need one of the two can
872
- * ignore the other. */
873
- interface TargetPredicate {
874
- (hit: unknown, bodyTarget?: string): boolean;
875
- /** `false` declares that the predicate reads `bodyTarget` only. An
876
- * exclusive affordance claim bars bindings whose target doesn't consult
877
- * the hit; a body predicate that declares nothing looks like it does. */
878
- readsAffordance?: boolean;
879
- }
880
- /** Target selector for click and drag gesture specs. String forms are sugar
881
- * for the kit-owned object-kind registry (TODO.md Tier 1 follow-up); until
882
- * that ships, consumers can pass `{ kindOf: predicate }` to classify hits
883
- * themselves.
884
- *
885
- * Adding a form here is a compile error in `parseTargetSpec` until
886
- * `TargetSpecForm` grows a matching variant — which is in turn a compile
887
- * error at every site that switches on one. */
888
- type TargetSpec = 'empty' | 'selected-body' | 'unselected-body' | `kind:${string}` | `kind:${string}:selected` | `affordance:${string}` | {
889
- kindOf: TargetPredicate;
890
- };
891
- /** Phase qualifier on a gesture spec. Restricts when the spec matches based
892
- * on per-tool gesture-lifecycle state.
893
- *
894
- * Shorthand forms (most common case — gate on the binding's own tool):
895
- * `'engaged'` → `[{ channel: '&', phase: 'engaged' }]` // self mid-gesture
896
- * `'initial'` → `[{ channel: '&', phase: 'initial' }]` // self idle
897
- * `'*'` → `[{ channel: '&', phase: '*' }]` // either self phase
898
- *
899
- * Array form for explicit channel:phase atoms — e.g. `[{ channel: 'rect',
900
- * phase: 'engaged' }]` for "when the rect tool is mid-gesture, regardless of
901
- * which scope I'm in." See the v3 route grammar in
902
- * `@weasel-js/gestures/grammar` for the full lattice.
903
- *
904
- * When omitted, matches in any phase (preserves pre-phase behavior). */
905
- type PhaseSpec = 'initial' | 'engaged' | '*' | readonly PhaseAtom[];
906
- /** Single-keystroke gesture (keydown). */
907
- interface KeySpec$1 {
908
- kind: 'key';
909
- /** A single key, or an array of acceptable keys (case-insensitive match). */
910
- key: string | string[];
911
- mods?: ModSpec;
912
- phase?: PhaseSpec;
913
- }
914
- /** Key-held gesture (keydown opens, keyup closes). Drives "hold space for
915
- * hand tool"-style interactions. */
916
- interface KeyHeldSpec {
917
- kind: 'key-held';
918
- /** A single key, or an array of acceptable keys (case-insensitive match). */
919
- key: string | string[];
920
- mods?: ModSpec;
921
- phase?: PhaseSpec;
922
- }
923
- /** Wheel-event gesture. `direction` filters by deltaY sign; default `'*'`.
924
- * - `'up'` → matches only deltaY < 0
925
- * - `'down'` → matches only deltaY > 0
926
- * - `'*'` → matches either sign (default; universal-wildcard convention) */
927
- interface WheelSpec {
928
- kind: 'wheel';
929
- direction?: 'up' | 'down' | '*';
930
- target?: TargetSpec;
931
- mods?: ModSpec;
932
- phase?: PhaseSpec;
933
- }
934
- /** Click gesture (pointerdown + pointerup without movement past the
935
- * threshold). */
936
- interface ClickSpec {
937
- kind: 'click';
938
- target?: TargetSpec;
939
- mods?: ModSpec;
940
- phase?: PhaseSpec;
941
- }
942
- /** Double-click: two `click` events within ~500ms and ~5px of each other.
943
- * Synthesized by `useGestureDispatcher`; emitted AFTER the second
944
- * `click`. Bindings that want to handle a double-click should declare
945
- * this kind rather than chasing two `click` events. */
946
- interface DoubleClickSpec {
947
- kind: 'doubleClick';
948
- target?: TargetSpec;
949
- mods?: ModSpec;
950
- phase?: PhaseSpec;
951
- }
952
- /** Right-click (contextmenu) gesture. The dispatcher calls
953
- * `preventDefault()` on the underlying DOM event so the native menu
954
- * doesn't appear — tools/actions fully own the right-click UX. */
955
- interface ContextMenuSpec {
956
- kind: 'contextMenu';
957
- target?: TargetSpec;
958
- mods?: ModSpec;
959
- phase?: PhaseSpec;
960
- }
961
- /** Drag gesture (pointerdown + pointermove past the threshold). */
962
- interface DragSpec {
963
- kind: 'drag';
964
- target?: TargetSpec;
965
- mods?: ModSpec;
966
- phase?: PhaseSpec;
967
- }
968
- /**
969
- * Bare pointer press, matched at down time — before the dispatcher knows
970
- * whether the gesture will become a click or a drag.
971
- *
972
- * Reach for this only when the effect must be visible while the button is
973
- * still held. Selection is the motivating case: pressing an unselected node
974
- * highlights it immediately, and the drag that may follow then starts from an
975
- * already-correct selection. Anything that can wait for the release belongs on
976
- * a `click` spec, which does not fire on a press that turns into a drag.
977
- *
978
- * A matching binding does NOT own the gesture: the same press goes on to open
979
- * a drag or synthesize a click as usual. Bind an immediate action here, not an
980
- * ongoing one.
981
- */
982
- interface PointerDownSpec {
983
- kind: 'pointerDown';
984
- target?: TargetSpec;
985
- mods?: ModSpec;
986
- phase?: PhaseSpec;
987
- }
988
- /**
989
- * Press held past the long-press threshold without crossing the drag
990
- * threshold. Synthesized by `useGestureDispatcher` from the pointer stream.
991
- *
992
- * Fires for `touch` and `pen` pointers only. A mouse held still for half a
993
- * second is an ordinary slow click, and firing on it would produce a context
994
- * menu nobody asked for.
995
- *
996
- * When a long-press matches no binding, the dispatcher re-dispatches it as a
997
- * `contextmenu` event — so `contextMenu` bindings work under a finger with no
998
- * consumer changes, while `longPress` stays independently bindable.
999
- */
1000
- interface LongPressSpec {
1001
- kind: 'longPress';
1002
- target?: TargetSpec;
1003
- mods?: ModSpec;
1004
- phase?: PhaseSpec;
1005
- }
1006
- /** Multi-touch gesture. `fingers` is the required touch count. */
1007
- interface MultiTouchSpec {
1008
- kind: 'multiTouch';
1009
- fingers: number;
1010
- mods?: ModSpec;
1011
- phase?: PhaseSpec;
1012
- }
1013
- /** Multi-touch tap gesture — fires when N fingers touch down then release
1014
- * together without movement past the tap threshold. Synthesized by the
1015
- * dispatcher from the underlying multitouch tracking. */
1016
- interface MultiTouchTapSpec {
1017
- kind: 'multiTouchTap';
1018
- fingers: number;
1019
- mods?: ModSpec;
1020
- phase?: PhaseSpec;
1021
- }
1022
- /** OS drag-and-drop of external content onto the canvas. `types` filters by
1023
- * MIME glob (`'image/*'`, `'text/plain'`); the spec matches when ANY item's
1024
- * MIME matches ANY glob. Omitted or empty = matches any drop. */
1025
- interface DropSpec {
1026
- kind: 'drop';
1027
- types?: string[];
1028
- mods?: ModSpec;
1029
- phase?: PhaseSpec;
1030
- }
1031
- /** System-clipboard paste of external content. Same `types` semantics as
1032
- * {@link DropSpec} — omitted or empty = matches any paste. */
1033
- interface PasteSpec {
1034
- kind: 'paste';
1035
- types?: string[];
1036
- mods?: ModSpec;
1037
- phase?: PhaseSpec;
1038
- }
1039
- /** The full union of supported gesture spec kinds. New invocation forms
1040
- * (two-stage, modal-dialog) extend this union without touching
1041
- * the `Action` type. */
1042
- type GestureSpec = KeySpec$1 | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | PointerDownSpec | LongPressSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
1043
-
1044
- /** A 2D point in either world or screen coordinates. */
1045
- interface Point2 {
1046
- x: number;
1047
- y: number;
1048
- }
1049
- /**
1050
- * Information about which UI affordance was hit at pointerdown.
1051
- *
1052
- * Populated by the dispatcher when the `affordanceAt` thunk is provided to
1053
- * `useGestureDispatcher`. Tools / action invokers that only fire on a specific
1054
- * affordance (e.g. a resize handle) use this field as a guard — if the
1055
- * affordance is absent or is the wrong kind, they return `{}` and let other
1056
- * bindings handle the drag.
1057
- *
1058
- * `kind` is a discriminator string:
1059
- * - `'handle:top-left'` / `'handle:top-right'` / `'handle:bottom-left'` /
1060
- * `'handle:bottom-right'` — corner resize handles.
1061
- * - `'rotate-handle'` — the rotation affordance.
1062
- * - `'anchor:N'` — a path anchor at index N.
1063
- *
1064
- * `fixedPoint` is the world-space point that should remain stationary during
1065
- * the gesture. For resize handles this is the opposite (diagonally fixed)
1066
- * corner; for rotate it is the pivot.
1067
- *
1068
- * `targetIds` are the node ids this affordance belongs to.
1069
- */
1070
- interface AffordanceHit {
1071
- /** Discriminator string, e.g. `'handle:bottom-right'`. */
1072
- kind: string;
1073
- /** Id of whatever produced this hit — a kit affordance's `id`, or the
1074
- * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
1075
- owner?: string;
1076
- /** `'exclusive'` means no binding may act on this point unless its target
1077
- * consults the affordance. `'shared'` (the default) competes on scope and
1078
- * specificity as bindings always have. */
1079
- strength?: 'exclusive' | 'shared';
1080
- /** Which gestures an exclusive claim bars. Omitted bars all of them. */
1081
- claimedKinds?: readonly ClaimableGesture[];
1082
- /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
1083
- fixedPoint?: {
1084
- x: number;
1085
- y: number;
1086
- };
1087
- /** Which nodes this affordance belongs to. */
1088
- targetIds?: string[];
1089
- /** Set when `kind` matches `'handle:*'`. Identifies which corner stays
1090
- * fixed during a resize so consumers (resizeAction) don't re-parse `kind`.
1091
- * Other affordance kinds (rotate-handle, anchor:N, controlIn:N, controlOut:N)
1092
- * leave this undefined. */
1093
- anchor?: ResizeAnchor;
1094
- /** CSS cursor to show while the pointer hovers this affordance (no
1095
- * gesture in flight). Consumed by the hover-cursor pump in
1096
- * `useGestureDispatcher`; unset = the pump falls through to
1097
- * action-cursor prediction, then to the active tool's cursor. */
1098
- cursor?: string;
1099
- /**
1100
- * Free-form payload from whatever produced the hit, carried through to the
1101
- * matching action untouched.
1102
- *
1103
- * Kit affordances describe themselves fully in the fields above and leave
1104
- * this unset. It exists for affordances the kit doesn't know the shape of —
1105
- * a registered layer's own chrome, where the hit-test already resolved
1106
- * *which* of its pieces was hit and the action would otherwise have to
1107
- * redo that work. `@weasel-js/hud` passes the hit widget here.
1108
- */
1109
- payload?: unknown;
1110
- }
1111
- /**
1112
- * One accumulated point on a drag trail: world-space position plus whatever
1113
- * stylus state the originating `PointerEvent` carried.
1114
- *
1115
- * The stylus fields are absent for mouse/touch on browsers that don't report
1116
- * them, and for synthetic events. Consumers that want pressure-driven output
1117
- * (e.g. `Stroke.vertexWidths` from a pencil stroke) read them off the samples
1118
- * their `insert` dep receives — see `apps/site/demos/VertexWidthsDemo.tsx`.
1119
- */
1120
- interface DragSample extends Point2 {
1121
- /** 0..1. Mouse/touch report 0.5 while a button is held, per the spec. */
1122
- pressure?: number;
1123
- /** Degrees, ±90. Zero for mouse/touch. */
1124
- tiltX?: number;
1125
- /** Degrees, ±90. Zero for mouse/touch. */
1126
- tiltY?: number;
1127
- }
1128
- /** Per-invocation runtime context the dispatcher hands to an Invoker.
1129
- * Gesture-kind-specific fields (`drag`, `wheel`, `multiTouch`, `key`) are
1130
- * populated only for matching gesture kinds. */
1131
- interface InvocationCtx {
1132
- world: Point2;
1133
- screen: Point2;
1134
- modifiers: ModifierState;
1135
- deps: ActionDeps;
1136
- drag?: {
1137
- start: Point2;
1138
- current: Point2;
1139
- delta: Point2;
1140
- /**
1141
- * Drag delta in client/screen coordinates (CSS pixels from the drag
1142
- * origin). Use this — never `delta` — for any action whose effect
1143
- * mutates the viewport itself (pan, view-zoom), because world-space
1144
- * deltas become self-referential as the view shifts mid-drag.
1145
- *
1146
- * Populated when the dispatcher received `clientX`/`clientY` on the
1147
- * underlying pointer events. Absent for legacy callers that don't
1148
- * provide them.
1149
- */
1150
- screenDelta?: Point2;
1151
- affordance?: AffordanceHit;
1152
- /**
1153
- * Full pointermove history for the current drag, in world space, with
1154
- * per-sample stylus state when the browser reported it.
1155
- * Accumulated by the dispatcher on every `pointermove` pump event.
1156
- * Available only during `onMove` and `onEnd` calls (not on `start`).
1157
- * Used by `lassoSelectAction` to build its polygon vertex list and by
1158
- * `insertAction`'s pencil kind to carry the freehand stroke.
1159
- */
1160
- points?: DragSample[];
1161
- };
1162
- wheel?: {
1163
- deltaX: number;
1164
- deltaY: number;
1165
- deltaZ: number;
1166
- };
1167
- multiTouch?: {
1168
- centroid: Point2;
1169
- spread: number;
1170
- rotation: number;
1171
- /**
1172
- * Pinch-zoom geometry. Populated by the dispatcher when a multitouch
1173
- * handle is in flight and a pointermove-pump fires.
1174
- * `startSpread` is the spread at the moment the gesture began.
1175
- * `currentSpread` is the spread at the current frame.
1176
- */
1177
- pinch?: {
1178
- startSpread: number;
1179
- currentSpread: number;
1180
- centroid: Point2;
1181
- };
1182
- };
1183
- key?: {
1184
- key: string;
1185
- repeat: boolean;
1186
- };
1187
- /**
1188
- * Per-invocation parameters. Populated by `ActionsRegistry.begin()` for
1189
- * UI-driven ongoing actions (color picker, opacity slider) so handles can
1190
- * read the current value on `start` and updated values on `onMove`. The
1191
- * gesture dispatcher does not populate this field; gesture-driven actions
1192
- * receive params via `BindingOpts.params` on `start` (the `opts` arg).
1193
- */
1194
- params?: Record<string, unknown>;
1195
- }
1196
- /** Per-invocation options the dispatcher reads from a `GestureBinding`'s
1197
- * `opts` field and passes to `OngoingInvoker.start`. Today carries
1198
- * behaviors; extensible. */
1199
- interface BindingOpts {
1200
- behaviors?: ActionBehavior<unknown, unknown, unknown>[];
1201
- /** Per-binding action parameters. The action's invoker reads
1202
- * these via the second arg to `run` (or via InvocationCtx for ongoing
1203
- * invokers, when needed). Loose typing (Record<string, unknown>) for
1204
- * now; consider per-action typing later via BindingOpts<A>.
1205
- *
1206
- * params may also be a thunk evaluated each time the
1207
- * dispatcher (or invoker) needs the value. Thunks let tools close over
1208
- * refs that mutate during a gesture (e.g. polygon `sides` adjusted
1209
- * mid-drag via ArrowUp). For ongoing invokers that want the latest
1210
- * values at commit, the invoker can re-call the thunk inside `onEnd`
1211
- * via `resolveParams(opts?.params)`. */
1212
- params?: Record<string, unknown> | (() => Record<string, unknown>);
1213
- }
1214
- /** Convention-shaped action dependencies bag. Actions declare which
1215
- * contexts they consume; the dispatcher composes them per call.
1216
- * Consumer-side contexts (e.g. ColorContext) plug in by extending. */
1217
- interface ActionDeps {
1218
- selection?: unknown;
1219
- view?: unknown;
1220
- scene?: unknown;
1221
- pointer?: unknown;
1222
- activeTool?: unknown;
1223
- [k: string]: unknown;
1224
- }
1225
- /**
1226
- * Discriminated overlay shape returned by `OngoingHandle.overlay()`.
1227
- * Dispatcher-side chrome surface for in-flight
1228
- * gestures that paint non-ghost visuals. The canvas's
1229
- * `useDispatcherOverlayLayer` walks every in-flight handle, calls
1230
- * `overlay()`, and dispatches on `kind` to draw the appropriate shape.
1231
- *
1232
- * `marquee` mirrors `AreaSelectOverlay`; `lasso` mirrors `LassoSelectOverlay`.
1233
- * `commands` is the generic escape hatch — actions emit arbitrary
1234
- * `DrawCommand[]` for previews the typed variants can't express (insert
1235
- * shape outlines, paste ghosts of synthetic nodes, custom chrome). World-
1236
- * space is the default; the layer wraps in `viewToMat3` so commands track
1237
- * the camera. Set `space: 'screen'` for projections you've already done
1238
- * yourself (rare).
1239
- */
1240
- type OngoingOverlay = {
1241
- kind: 'marquee';
1242
- start: {
1243
- x: number;
1244
- y: number;
1245
- };
1246
- current: {
1247
- x: number;
1248
- y: number;
1249
- };
1250
- shiftHeld: boolean;
1251
- } | {
1252
- kind: 'lasso';
1253
- vertices: ReadonlyArray<{
1254
- x: number;
1255
- y: number;
1256
- }>;
1257
- current: {
1258
- x: number;
1259
- y: number;
1260
- };
1261
- shiftHeld: boolean;
1262
- } | {
1263
- kind: 'commands';
1264
- commands: readonly DrawCommand[];
1265
- /** Coordinate space the commands are authored in. Default `'world'`
1266
- * — the layer wraps them in `viewToMat3(view)` so they track the
1267
- * camera. `'screen'` emits them as-is (CSS pixels). */
1268
- space?: 'world' | 'screen';
1269
- } | {
1270
- /**
1271
- * Live insert-drag preview — dispatched by `insertAction` while the
1272
- * user is dragging out a new shape. Pre-commit there is no scene node
1273
- * to ghost via `previewIds()`/`previewPose()`, so insert paints its
1274
- * preview through the dispatcher overlay layer instead.
1275
- *
1276
- * `shape` is the kit's built-in insert kind. `bounds` is the AABB of
1277
- * the current drag (start/current normalized). `extras` is the
1278
- * per-kind extras the action already collected — the overlay
1279
- * renderer rebuilds the shape using the same path builders the
1280
- * commit factory uses, so the preview matches the eventual node.
1281
- *
1282
- * `extras` is opaque (`unknown`) at the union level; the overlay
1283
- * renderer narrows on `shape` and casts the field shape it expects.
1284
- */
1285
- kind: 'insertPreview';
1286
- shape: KitInsertShape;
1287
- bounds: {
1288
- x: number;
1289
- y: number;
1290
- width: number;
1291
- height: number;
1292
- };
1293
- extras: unknown;
1294
- /** World-space point to paint a small "anchor" dot at. Sells the
1295
- * click point as the drag's anchor — particularly useful for
1296
- * radial shapes (polygon/star) where no vertex sits on the
1297
- * click point, and for any shape in center mode where the dot
1298
- * marks the center the shape grows around. */
1299
- anchorPoint?: {
1300
- x: number;
1301
- y: number;
1302
- };
1303
- };
1304
- /** Handle returned from an `OngoingInvoker.start`. The dispatcher pumps
1305
- * `onMove` on subsequent input events of the same gesture and calls
1306
- * `onEnd` exactly once (with `'commit'` on natural completion or `'cancel'`
1307
- * on pointercancel / blur / escape). */
1308
- interface OngoingHandle {
1309
- /**
1310
- * Optional logical action kind — a stable, human-readable tag the
1311
- * dispatcher exposes via `getActiveAction()` for chrome-visibility
1312
- * rules and any other surface that wants to react to "what action
1313
- * is currently in flight" without inspecting handles directly.
1314
- *
1315
- * Examples: `'marquee'`, `'lasso'`, `'move'`, `'resize'`, `'rotate'`,
1316
- * `'pan'`, `'pinch'`.
1317
- *
1318
- * Distinct from the dispatcher's internal `gestureId` (`pointer-mouse`,
1319
- * `key-held-Space`, etc.) which keys per-pointer state and is not
1320
- * meaningful to consumers.
1321
- *
1322
- * When omitted, the action is "anonymous" — `getActiveAction().kind`
1323
- * reports `null` even though a handle is in flight. This is fine for
1324
- * actions that don't have visible chrome of their own.
1325
- */
1326
- kind?: string;
1327
- onMove?(ctx: InvocationCtx): void;
1328
- onEnd?(ctx: InvocationCtx, reason: 'commit' | 'cancel'): void;
1329
- /**
1330
- * Optional preview surface — dispatcher-side ghost overlay.
1331
- *
1332
- * An ongoing-action implementation may populate `previewIds()` +
1333
- * `previewPose(id)` to expose its in-flight preview state for the
1334
- * canvas's preview-ghost layer (`usePreviewGhostLayer`) to render on
1335
- * top of the committed scene during the gesture.
1336
- *
1337
- * Returning `null` (or omitting the method entirely) means "no preview
1338
- * this gesture" — the canvas will skip this handle as a source.
1339
- *
1340
- * Semantics mirror the tool-side `Tool.previewIds` / `Tool.previewPose`
1341
- * pair: `previewIds()` enumerates the displaced node ids; `previewPose(id)`
1342
- * returns the interim pose for one of those ids (shape opaque — the
1343
- * canvas casts to its `TPose` parameter). The preview-ghost layer
1344
- * merges all sources via first-non-null semantics, with tool-side
1345
- * previews taking precedence over dispatcher-side (preserves
1346
- * backwards-compat during the registry-unification migration).
1347
- */
1348
- previewIds?(): Iterable<string> | null;
1349
- previewPose?(id: string): unknown | null;
1350
- /**
1351
- * Subset of `previewIds()` the ghost layer paints at full opacity. The
1352
- * ghost alpha says "this is in flight under the pointer"; a node the
1353
- * gesture merely displaces — a layout sibling reflowing into its
1354
- * destination slot — is not, and reads better settled. Honored at
1355
- * subtree-root granularity.
1356
- */
1357
- previewOpaqueIds?(): Iterable<string> | null;
1358
- /**
1359
- * When `false`, the preview-ghost layer paints the ghost AND the
1360
- * source node stays visible at its committed pose. Defaults to
1361
- * `true` (move/resize/rotate semantics: ghost replaces the source
1362
- * during the gesture). Clone overrides to `false` so the original
1363
- * stays put and the ghost appears at the drag target.
1364
- */
1365
- previewHidesSource?: boolean;
1366
- /**
1367
- * Optional per-id preview *data*. Falls back to the committed
1368
- * `node.data` when null/absent. Use when the gesture mutates
1369
- * `node.data` (e.g. anchor-edit on nodes that store the polygon on
1370
- * `data.path`) rather than (or in addition to) the pose. The preview-
1371
- * ghost layer assembles a synthetic node from `{ ...node, pose:
1372
- * previewPose ?? node.pose, data: previewData ?? node.data }` before
1373
- * calling the scene slot's `drawOne`.
1374
- *
1375
- * Sources compose first-non-null per axis: an action can emit only
1376
- * `previewPose` (translation), only `previewData` (data-only edit),
1377
- * or both (pose + data both change, e.g. anchor drag on a data.path
1378
- * node where the bounds shift).
1379
- */
1380
- previewData?(id: string): unknown | null;
1381
- /**
1382
- * Optional chrome surface — dispatcher-side overlay layer.
1383
- *
1384
- * An ongoing-action implementation may populate `overlay()` to expose a
1385
- * non-ghost visual (marquee rectangle, lasso polyline) for the canvas's
1386
- * `useDispatcherOverlayLayer` to paint while the gesture is in flight.
1387
- * Returning `null` (or omitting the method) means "no overlay this
1388
- * gesture" — the canvas will skip this handle as a chrome source.
1389
- *
1390
- * Distinct from the `previewIds()`/`previewPose(id)` ghost surface,
1391
- * which paints displaced scene-node silhouettes. Marquee and lasso
1392
- * gestures don't displace any node, but still need on-screen feedback.
1393
- */
1394
- overlay?(): OngoingOverlay | null;
1395
- }
1396
- /** Fire-once invocation. Runs to completion synchronously (or fires off an
1397
- * async side-effect; the registry doesn't wait). */
1398
- interface ImmediateInvoker {
1399
- timing: 'immediate';
1400
- /** `params` carries the matched binding's opts.params. When
1401
- * invoked via the legacy `Action.run` bridge or from the command palette
1402
- * with no per-binding context, `params` is undefined; descriptors should
1403
- * default to a sensible variant. */
1404
- run(deps: ActionDeps, params?: Record<string, unknown>): void;
1405
- }
1406
- /** Phase-machine invocation. `start` opens the phase and returns the handle
1407
- * the dispatcher pumps. */
1408
- interface OngoingInvoker {
1409
- timing: 'ongoing';
1410
- start(ctx: InvocationCtx, opts?: BindingOpts): OngoingHandle;
1411
- }
1412
- /** Pluggable invocation strategy for an Action. Future variants
1413
- * (`longPress`, `twoStage`, `modal`) extend this union without touching
1414
- * the `Action` type. */
1415
- type Invoker = ImmediateInvoker | OngoingInvoker;
1416
-
1417
- /**
1418
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
1419
- * options). Tools own arrays of these on their `bindings` field; ambient
1420
- * gesture-bindings are registered globally.
1421
- *
1422
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
1423
- */
1424
-
1425
- /** An interaction: a gesture spec composed with the id of the action it
1426
- * invokes. Tools declare arrays of these; the dispatcher matches an incoming
1427
- * input event against them and runs the winner's action. */
1428
- interface GestureBinding {
1429
- spec: GestureSpec;
1430
- actionId: string;
1431
- opts?: BindingOpts;
1432
- }
1433
-
1434
- /**
1435
- * Pose composition for hierarchical scene graphs.
1436
- *
1437
- * As of the nesting change, `getPose(id)` on adapters returns the
1438
- * **local** pose — relative to the object's direct parent. Anything in the
1439
- * kit that needs to draw, hit-test, snap, or otherwise reason about world
1440
- * coordinates routes through `composeWorldPose`, which walks the parent
1441
- * chain and folds local poses together via a consumer-supplied `compose`.
1442
- *
1443
- * Pose shape is generic, so the compose strategy is too. For the common
1444
- * `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
1445
- * translation only, child dimensions preserved. Custom pose shapes (paths,
1446
- * matrix transforms) supply their own.
1447
- *
1448
- * The inverse — `rebaseLocalPose` — converts a world-space pose into a
1449
- * local pose under a target parent. Used when reparenting so the visual
1450
- * world position of a child is preserved across the parent change.
1451
- */
1452
- /** Re-exported; the declaration lives in `core/scene/types.ts`, which names
1453
- * it and may not import from features. */
1454
-
1455
- /** Consumer's pose-composition strategy for hierarchical scenes. `compose`
1456
- * folds a child's pose (in parent's frame) up to the next frame; `decompose`
1457
- * is its inverse. Default is IDENTITY — an absolute-pose scene where every
1458
- * node already stores world coords (parent is grouping-only, no transform). */
1459
- interface PoseComposition<TPose> {
1460
- compose: (parent: TPose, child: TPose) => TPose;
1461
- decompose: (parent: TPose, world: TPose) => TPose;
1462
- }
1463
-
1464
- /** Boolean op identifiers — five Pathfinder primaries plus Crop. */
1465
- type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
1466
- /**
1467
- * z-position descriptor for a path node. `parentId` is the direct parent
1468
- * (or `null` for a top-level node); `index` is the position within that
1469
- * parent's child order. Used by the optional `getZOrder` hook below to
1470
- * reposition the result of a boolean op at the topmost source's slot.
1471
- */
1472
- /** @internal */
1473
- interface BooleanZOrder {
1474
- parentId: string | null;
1475
- index: number;
1476
- }
1477
- /** Adapter the hook and the pure core both consume. */
1478
- interface BooleansAdapter {
1479
- getSelection(): NodeId[];
1480
- getWorldPath(id: NodeId): Path | undefined;
1481
- compareZ(a: NodeId, b: NodeId): number;
1482
- /**
1483
- * Mint a new node from a boolean-op result `Path`. `producedBy` names the
1484
- * op that synthesized it — adapters that store provenance (e.g. for a
1485
- * layer-panel icon) record it; others ignore the arg.
1486
- */
1487
- createPathNode(path: Path, producedBy: BooleanOp): {
1488
- id: string;
1489
- };
1490
- /**
1491
- * Optional: return the full object for an id, used by the delete ops so
1492
- * their `invert` (an insert) can restore the complete object on undo.
1493
- * If omitted, a `{ id }` stub is captured — undo will reinstate the id
1494
- * but consumers reading other fields (path, fill, etc.) will see them as
1495
- * undefined. Mirrors `DeleteAdapter.getNode`; should be provided whenever
1496
- * undo over boolean ops is expected to be lossless.
1497
- */
1498
- getNode?(id: NodeId): {
1499
- id: string;
1500
- } | undefined | null;
1501
- /**
1502
- * Optional: return the parent + child-index of `id` so the result of a
1503
- * boolean op can be placed in the topmost source's z-slot. Adapters that
1504
- * also expose `getChildren`/`setChildOrder` (the `ReorderAdapter`
1505
- * contract) will have the kit emit a `createMoveToIndexOp` after the
1506
- * inserts. Adapters that omit this method get v1 behavior — the result
1507
- * lands wherever the adapter's plain `insertNode` defaults to.
1508
- */
1509
- getZOrder?(id: NodeId): BooleanZOrder | undefined;
1510
- applyOps?(ops: Op[], label?: string): void;
1511
- setSelection?(ids: NodeId[]): void;
1512
- insertNode?(node: {
1513
- id: string;
1514
- }): void;
1515
- removeNode?(id: string): void;
1516
- }
1517
-
1518
- /** API returned by {@link useSelection}. */
1519
- interface SelectionApi {
1520
- /** Current selection. Re-renders trigger when this reference changes. */
1521
- current: readonly NodeId[];
1522
- /** Imperative read for use inside event callbacks (avoids stale closures). */
1523
- get(): NodeId[];
1524
- /** Replace selection. */
1525
- set(ids: NodeId[]): void;
1526
- /** Add id (multi-mode appends; single-mode replaces). */
1527
- add(id: NodeId): void;
1528
- /** Remove id from selection. */
1529
- remove(id: NodeId): void;
1530
- /** Toggle id in/out of selection. */
1531
- toggle(id: NodeId): void;
1532
- /** Clear selection. */
1533
- clear(): void;
1534
- /** True if id is selected. */
1535
- contains(id: NodeId): boolean;
1536
- /**
1537
- * Apply a click to the selection per the configured mode/extend key.
1538
- * - `single`: replaces selection with `[id]`, regardless of modifiers.
1539
- * - `multi`: with the extend key held, toggles `id` in/out of the selection;
1540
- * otherwise replaces with `[id]`.
1541
- */
1542
- applyClick(id: NodeId, modifiers: {
1543
- shift: boolean;
1544
- meta: boolean;
1545
- ctrl: boolean;
1546
- }): void;
1547
- /** Pre-built methods for spreading into an adapter that needs them. */
1548
- adapterMethods: {
1549
- getSelection: () => NodeId[];
1550
- setSelection: (ids: NodeId[]) => void;
1551
- };
1552
- }
1553
-
1554
- /** Context handed to every content handler for one ingest event. */
1555
- interface IngestCtx {
1556
- /** World-space arrival point (drop / pointed imperative ingest); `null`
1557
- * for paste and point-less calls — handlers pick their own policy
1558
- * (the kit image handler centers on the viewport). */
1559
- point: {
1560
- x: number;
1561
- y: number;
1562
- } | null;
1563
- /** Visible canvas area in world coordinates. */
1564
- viewportWorldRect(): {
1565
- x: number;
1566
- y: number;
1567
- width: number;
1568
- height: number;
1569
- };
1570
- /** The kit insert dep — id/layer/undoable-op supplied; the canonical way
1571
- * for a handler to mint a node (`insert.commit(bounds, { kind, ... })`). */
1572
- insert: InsertDep;
1573
- /** Raw op commit for handlers that build their own ops. */
1574
- applyOps(ops: Op[], label?: string): void;
1575
- scene: Scene<unknown, string, unknown>;
1576
- selection: SelectionApi;
1577
- /** Consumer file→src resolver (SceneCanvas `ingestion.resolveSrc`).
1578
- * When absent, the kit image handler embeds as a `data:` URI. */
1579
- resolveSrc?: (file: File) => Promise<string>;
1580
- /** Kit SVG-handler options (SceneCanvas `ingestion.svg`) — e.g.
1581
- * `{ unpack: unpackSvgFiles }` (from `@weasel-js/svg`) to parse SVG files
1582
- * into scene nodes. */
1583
- svg?: SvgIngestOptions;
1584
- /** Clipboard-paste seam — present when the hosting `SceneCanvas` supplied
1585
- * an adapter with `commitPaste`. `reviver` comes from
1586
- * `SceneCanvasProps.ingestion.clipboard`. Absent ⇒ the kit weasel-JSON
1587
- * handler declines inert (dwarn, nothing ingested) — its matched items
1588
- * were already consumed at match time, so they do NOT fall through;
1589
- * only match-level misses flow on to other handlers. */
1590
- clipboard?: ClipboardIngestCtx;
1591
- /** Set to `true` by the kit weasel-JSON handler when it successfully
1592
- * pastes a payload in this event. The `ctx` object is shared across all
1593
- * handlers in one `runIngest` call, and higher-priority handlers' `handle`
1594
- * bodies run (synchronously) before lower ones — so `kit:svg`'s
1595
- * `text/plain` SVG fallback reads this to decline the SVG flavor of a copy
1596
- * whose canonical weasel-JSON flavor already ingested (avoids a
1597
- * double-paste when both flavors ride one clipboard event). */
1598
- consumedWeaselPayload?: boolean;
1599
- /** Full action-deps bag, for consumer handlers that need more. */
1600
- deps: ActionDeps;
1601
- }
1602
-
1603
- /**
1604
- * Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.
1605
- * The hook reads bounds via `getBounds`, runs anchor-relative math on those
1606
- * bounds, then asks `remapBounds` to project the result back into TPose.
1607
- *
1608
- * `remapBounds(pose, src, dst)` is a single operation that subsumes both
1609
- * "set my own AABB to dst" (single-leaf resize) and "scale me as a leaf
1610
- * inside parent's src→dst rect" (group resize) — they're the same affine
1611
- * map. For rect-shaped poses the default geometry interprets the pose as
1612
- * its own bounds; for Path or polygon poses the consumer supplies a
1613
- * projection that knows how to read and rewrite the underlying geometry.
1614
- */
1615
- interface PoseProjection<TPose> {
1616
- getBounds(pose: TPose): ResizePose;
1617
- remapBounds(pose: TPose, src: ResizePose, dst: ResizePose): TPose;
1618
- /** Translate the pose by (dx, dy). Optional — when omitted, callers fall
1619
- * back to a translation derived from `remapBounds` (origin shifted, no
1620
- * scale). Path-shaped poses should provide this for performance. */
1621
- translate?(pose: TPose, dx: number, dy: number): TPose;
1622
- /** True iff any portion of the pose's geometry intersects `rect`. Optional
1623
- * — when omitted, area-select and similar callers test against `getBounds`
1624
- * AABB (looser, but correct for axis-aligned rect poses). */
1625
- intersectsRect?(pose: TPose, rect: ResizePose): boolean;
1626
- /** Interpolate between two poses. Optional — animation helpers fall back to
1627
- * rect-shape lerp when omitted (which fails for non-rect poses). */
1628
- lerp?(a: TPose, b: TPose, t: number): TPose;
1629
- /** Read the pose's rotation in radians. Pivot is the AABB center
1630
- * (`getBounds(pose)` center). Default 0 when omitted — descriptor
1631
- * declares "this pose has no rotation." When supplied and non-zero,
1632
- * `useResize` projects the drag delta into the leaf's local frame,
1633
- * runs anchor math there, and translates the resulting pose so the
1634
- * diagonally opposite world-space corner is pinned. */
1635
- getRotation?(pose: TPose): number;
1636
- /** True iff this pose shape can carry a rotation. Consulted by the
1637
- * rotation affordance to decide whether to render the rotate cursor /
1638
- * drag-band over a selection. When omitted, the kit assumes `true` for
1639
- * back-compat — descriptors whose poses lack `x/y/width/height/rotation`
1640
- * fields (e.g. polygon Paths) should return `false` so the affordance
1641
- * hides instead of exposing a non-functional rotate cursor. */
1642
- supportsRotation?(pose: TPose): boolean;
1643
- }
1644
-
1645
- /** All easings in one bag — useful for demos / pickers. */
1646
- declare const EASINGS: {
1647
- readonly linear: EasingFn;
1648
- readonly easeInQuad: EasingFn;
1649
- readonly easeOutQuad: EasingFn;
1650
- readonly easeInOutQuad: EasingFn;
1651
- readonly easeInCubic: EasingFn;
1652
- readonly easeOutCubic: EasingFn;
1653
- readonly easeInOutCubic: EasingFn;
1654
- readonly easeInQuart: EasingFn;
1655
- readonly easeOutQuart: EasingFn;
1656
- readonly easeInOutQuart: EasingFn;
1657
- readonly easeInQuint: EasingFn;
1658
- readonly easeOutQuint: EasingFn;
1659
- readonly easeInOutQuint: EasingFn;
1660
- readonly easeInSine: EasingFn;
1661
- readonly easeOutSine: EasingFn;
1662
- readonly easeInOutSine: EasingFn;
1663
- readonly easeInExpo: EasingFn;
1664
- readonly easeOutExpo: EasingFn;
1665
- readonly easeInOutExpo: EasingFn;
1666
- readonly easeInCirc: EasingFn;
1667
- readonly easeOutCirc: EasingFn;
1668
- readonly easeInOutCirc: EasingFn;
1669
- readonly easeInBack: EasingFn;
1670
- readonly easeOutBack: EasingFn;
1671
- readonly easeInOutBack: EasingFn;
1672
- readonly easeInElastic: EasingFn;
1673
- readonly easeOutElastic: EasingFn;
1674
- readonly easeInOutElastic: EasingFn;
1675
- readonly easeInBounce: EasingFn;
1676
- readonly easeOutBounce: EasingFn;
1677
- readonly easeInOutBounce: EasingFn;
1678
- };
1679
- /** The name of one of the built-in easing curves. */
1680
- type EasingName = keyof typeof EASINGS;
1681
-
1682
- /** Cubic-bezier control points, CSS `cubic-bezier()` order. The curve's two
1683
- * endpoints are implicit at (0,0) and (1,1). */
1684
- interface BezierEasing {
1685
- /** `readonly` so an `as const` preset is assignable; nothing ever writes it. */
1686
- bezier: readonly [number, number, number, number];
1687
- }
1688
- /** An easing curve as a value: a function, the name of a built-in, or control
1689
- * points. Anything an editor has to name, show or serialize must not be a bare
1690
- * function, which is why the union exists. */
1691
- type EasingSpec = EasingFn | EasingName | BezierEasing;
1692
-
1693
- /** An easing curve: maps normalized progress `t ∈ [0, 1]` to eased progress.
1694
- * Curves may leave the 0–1 range in the middle (back, elastic) but should
1695
- * pass through 0 at 0 and 1 at 1. */
1696
- type EasingFn = (t: number) => number;
1697
-
1698
- /** Factory interpolator: built ONCE at tween start with (from, to), the returned
1699
- * function is called with `t ∈ [0, 1]` each frame. Use for interpolators with
1700
- * expensive setup (color-space conversion, path-string parsing) — d3-interpolate's
1701
- * shape exactly. For cheap interpolations the per-tick `Interpolate<T>` form is
1702
- * fine; this is the escape hatch when setup-per-tick is wasteful. */
1703
- type InterpolatorFactory<T> = (from: T, to: T) => (t: number) => T;
1704
-
1705
- /** How the camera should move. */
1706
- interface ViewAnimationOptions {
1707
- /** Duration in ms. Default 250. */
1708
- ms?: number;
1709
- /** Easing curve. Default `easeOutCubic`. */
1710
- easing?: EasingSpec;
1711
- /** Replace the kit's log-scale / fixed-anchor curve. */
1712
- interpolator?: InterpolatorFactory<View>;
1713
- /** Fires when the target is reached. Not called on cancel. */
1714
- onDone?: () => void;
1715
- }
1716
-
1717
- /**
1718
- * @experimental
1719
- * PointerContext — a tiny ambient context that publishes the world-space
1720
- * position of the canvas pointer, refreshed on every `pointermove` over
1721
- * the canvas. Cleared (set to `null`) on `pointerleave`.
1722
- *
1723
- * Why ref-based and not state-based: cursor moves fire dozens of times per
1724
- * second; routing those through React state would re-render every consumer
1725
- * in the tree. The context exposes a stable `pointerRef` whose `.current`
1726
- * is mutated directly by the publisher, plus a thunk `getDropPoint()` that
1727
- * reads it on demand. Consumers (e.g. `useClipboard`) pull via the thunk
1728
- * inside their callbacks — no subscription, no re-render.
1729
- *
1730
- * `<SceneCanvas>` publishes automatically. `useClipboardOps` consumes when
1731
- * the caller didn't pass an explicit `getDropPoint` option. Other future
1732
- * hit-on-cursor consumers (drop-zone hover, context-menu anchor) can reuse
1733
- * the same context.
1734
- */
1735
-
1736
- /** @experimental World-space pointer position, or `null` when the pointer
1737
- * isn't over the publishing canvas. */
1738
- type PointerWorldPos = {
1739
- worldX: number;
1740
- worldY: number;
1741
- } | null;
1742
- /** @experimental */
1743
- interface PointerContextValue {
1744
- /** Live ref — mutate to publish, read for the latest snapshot. The
1745
- * identity is stable for the lifetime of the provider. */
1746
- readonly pointerRef: MutableRefObject<PointerWorldPos>;
1747
- /** Convenience thunk equivalent to `() => pointerRef.current`. Stable
1748
- * identity for the lifetime of the provider; safe to pass to hooks. */
1749
- readonly getDropPoint: () => PointerWorldPos;
1750
- }
1751
-
1752
- /** Which tool is active, plus the stack of tools temporarily held active by a
1753
- * hotkey (space-for-hand and the like). The dispatcher reads this to decide
1754
- * whose bindings are in scope. */
1755
- interface ActiveToolContextValue {
1756
- active: string;
1757
- hotkeyStack: string[];
1758
- setActive(id: string): void;
1759
- pushHotkey(id: string): void;
1760
- popHotkey(): void;
1761
- }
1762
-
1763
- /**
1764
- * `enterTextEditAction` — immediate Action descriptor for entering in-place
1765
- * text editing on a selected text node.
1766
- *
1767
- * ## Status: REAL
1768
- *
1769
- * Fires via `useTextTool.bindings` when the user clicks on a
1770
- * selected text node. Calls `deps.textEdit.startEdit(id)` to activate the
1771
- * contenteditable overlay managed by `useTextEdit` / `useSceneTextEdit`.
1772
- *
1773
- * ## No defaultBinding / defaultBinding
1774
- *
1775
- * This action has no ambient key or gesture binding — it fires ONLY via
1776
- * `useTextTool`'s `Tool.bindings` entry:
1777
- *
1778
- * ```ts
1779
- * bindings: [
1780
- * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' },
1781
- * ]
1782
- * ```
1783
- *
1784
- * Keeping it binding-free avoids ambient double-fire and scopes the action to
1785
- * the text tool context where `classifyTarget` is already wired.
1786
- *
1787
- * ## Self-guard: only act on text nodes
1788
- *
1789
- * The `'selected-body'` target yields a match for any selected node kind. To
1790
- * avoid entering text-edit mode when the text tool happens to have a non-text
1791
- * node selected, the action self-guards via an optional `isTextNode` predicate
1792
- * on `TextEditDep`:
1793
- *
1794
- * - When `isTextNode` is absent: action fires unconditionally (the binding
1795
- * spec is the real gate — consumers should only bind this action from the
1796
- * text tool).
1797
- * - When `isTextNode(id)` returns `false`: action is a no-op for that node.
1798
- *
1799
- * ### Pre-filtering at dispatch time
1800
- *
1801
- * `classifyTarget` now surfaces node kind, so a binding can pre-filter instead
1802
- * of relying on the self-guard:
1803
- *
1804
- * ```ts
1805
- * { spec: { kind: 'click', target: 'kind:text:selected' }, actionId: 'enterTextEdit' }
1806
- * ```
1807
- *
1808
- * That reads the *routing trait's* kind, so it matches whatever names the
1809
- * consumer registered in `<SceneCanvas routing>` — `'text'` under the kit's
1810
- * inferred default. `isTextNode` stays on `TextEditDep` because it also covers
1811
- * consumers who bind the broader `'selected-body'` target, and because it is
1812
- * the only guard for a consumer who opted out of routing entirely.
1813
- *
1814
- * ## Migration plan for useTextTool
1815
- *
1816
- * When wiring `useTextTool` to `Tool.bindings`:
1817
- *
1818
- * 1. Add to `useTextTool`'s `bindings`:
1819
- * ```ts
1820
- * { spec: { kind: 'click', target: 'selected-body' }, actionId: 'enterTextEdit' }
1821
- * ```
1822
- * 2. Register a `textEdit` dep sourced from the `useTextEdit` / `useSceneTextEdit`
1823
- * return value, plus an `isTextNode` predicate that checks `data.kind === 'text'`
1824
- * (or however the consumer identifies text nodes).
1825
- * 3. The existing `hitExisting` gate in `useTextTool`'s click route becomes
1826
- * redundant — remove it in the same pass.
1827
- */
1828
-
1829
- /**
1830
- * Dep for `enterTextEditAction`.
1831
- *
1832
- * Wrap the return value of `useTextEdit` / `useSceneTextEdit` to source this
1833
- * dep. The `isTextNode` predicate is optional — when absent the action fires
1834
- * unconditionally (the binding spec acts as the gate).
1835
- *
1836
- * @example
1837
- * ```ts
1838
- * const textEdit = useSceneTextEdit({ scene, container });
1839
- * useDepSource('textEdit', () => ({
1840
- * startEdit: textEdit.startEdit,
1841
- * isTextNode: (id) => scene.get(id as NodeId)?.data?.kind === 'text',
1842
- * }));
1843
- * ```
1844
- */
1845
- interface TextEditDep {
1846
- /**
1847
- * Begin editing the node with `id`. Activates the contenteditable overlay
1848
- * managed by `useTextEdit` / `useSceneTextEdit`.
1849
- */
1850
- startEdit(id: string, opts?: {
1851
- caret?: number | 'all';
1852
- }): void;
1853
- /**
1854
- * Optional predicate: returns `true` when the node with `id` is a text node.
1855
- * When absent the action fires on any selected node (binding spec is the gate).
1856
- * When present and returning `false`, the invocation is a no-op.
1857
- */
1858
- isTextNode?(id: string): boolean;
1859
- }
1860
-
1861
- /**
1862
- * Consumer-supplied commit for the Slice action. `commit` receives the finite
1863
- * slice segment (world coords); the consumer scans the scene, splits crossed
1864
- * paths via `splitPathByLine`, and applies the result as one undoable batch.
1865
- */
1866
- interface SliceDep {
1867
- commit(a: Point2, b: Point2): void;
1868
- }
1869
-
1870
- /**
1871
- * Clipboard dep — the imperative surface `useClipboardOps` returns.
1872
- *
1873
- * Consumers publish their live clipboard through `useDepSource('clipboard',
1874
- * …)` from inside the `<DepRegistryProvider>` (i.e. under `<SceneCanvas>`).
1875
- * The kit deliberately does not build one for them: `useClipboardOps` needs
1876
- * an adapter and a selection reader that only the consumer can supply.
1877
- */
1878
- interface ClipboardDep {
1879
- copy(): void;
1880
- paste(): void;
1881
- isEmpty(): boolean;
1882
- }
1883
-
1884
- /** Optional consumer seam: given a node and the affine `m` that a pose-transform
1885
- * action applied to the node's POSE, return updated `data` with the node's
1886
- * data-held geometry transformed by `m`, or `null` if this node has no
1887
- * data-held geometry (the kit leaves `data` alone). */
1888
- interface GeometryProjection {
1889
- transform(node: {
1890
- id?: string;
1891
- data: unknown;
1892
- pose: unknown;
1893
- }, m: Mat3): unknown | null;
1894
- }
1895
-
1896
- /** Minimal view API the action layer consumes. */
1897
- interface ViewApi {
1898
- get(): View;
1899
- set(v: View): void;
1900
- /** Optional recenter callback. When wired, `viewportZoomAction`'s `reset`
1901
- * branch (Cmd-0) calls this instead of resetting to identity — letting
1902
- * consumers re-fit the page (or other reference bounds) into the workspace.
1903
- * Return the target `View` to let the action animate there; return nothing
1904
- * to keep dispatching the view yourself. */
1905
- recenter?(): View | void;
1906
- /** Optional canvas-local host dimensions (CSS px). When wired,
1907
- * `viewportZoomAction`'s keyboard branches (Cmd+= / Cmd+-) anchor at the
1908
- * host center instead of the top-left origin. Null when the host isn't
1909
- * measurable (unmounted). */
1910
- hostSize?(): {
1911
- width: number;
1912
- height: number;
1913
- } | null;
1914
- /** Optional camera animation. `<SceneCanvas>` wires these three; a consumer
1915
- * publishing their own `view` dep need not, and actions fall back to `set`. */
1916
- animate?(to: View, opts?: ViewAnimationOptions): void;
1917
- stopAnimation?(): void;
1918
- /** Where an in-flight camera animation is heading, or null. Compute the next
1919
- * discrete step from this so repeated presses compound. */
1920
- animationTarget?(): View | null;
1921
- }
1922
- /**
1923
- * Adapter dep for `areaSelectAction`.
1924
- *
1925
- * Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>` via AABB
1926
- * overlap over scene nodes. Consumers with custom hit-testing override this
1927
- * dep entry in their own registrar.
1928
- */
1929
- /**
1930
- * Topmost-node-at-world-point dep, consumed by `moveAction` for
1931
- * reparent-on-drop and available to any action that needs a single-best
1932
- * pick. Mirrors the same hit-test plumbing `<SceneCanvas>` feeds to the
1933
- * tool dispatcher; consumers with custom hit-testing override here.
1934
- *
1935
- * `exclude` is iterated once per call and treated as a set membership
1936
- * test — the dep walks hits front-to-back and returns the first id not
1937
- * in the exclude set. Pass moving-node roots + their descendants when
1938
- * the caller wants to ignore the nodes it's manipulating.
1939
- */
1940
- type NodeAtPointDep = (point: {
1941
- x: number;
1942
- y: number;
1943
- }, exclude?: Iterable<NodeId>) => NodeId | null;
1944
- /** What an area-selecting action needs: a way to ask what a region covers,
1945
- * and a way to read and replace the selection. */
1946
- interface AreaSelectDep {
1947
- /** Return ids of all scene nodes whose AABB overlaps `bounds`. */
1948
- hitTestArea(bounds: {
1949
- x: number;
1950
- y: number;
1951
- width: number;
1952
- height: number;
1953
- }): NodeId[];
1954
- /** Return the current selection id list. */
1955
- getSelection(): NodeId[];
1956
- /** Replace the current selection. */
1957
- setSelection(ids: NodeId[]): void;
1958
- }
1959
- /**
1960
- * Adapter dep for `editAnchorsAction`.
1961
- *
1962
- * Provides narrow read/write access to the editable polygon for a single
1963
- * node. Consumers register this dep so anchor-edit actions can read/write
1964
- * the polygon WITHOUT knowing whether it lives directly on the node's
1965
- * pose (`pose.kind === 'polygon'`) or on `node.data.path` (the kit's
1966
- * built-in pen-tool default, also WeaselDraw's shape).
1967
- *
1968
- * Note on live previews: in-flight edit state is surfaced through the
1969
- * dispatcher's standard `OngoingHandle.previewIds/previewPose/previewData`
1970
- * triple (not this dep), so chrome and preview-ghost stay in lock-step
1971
- * via one source of truth.
1972
- */
1973
- interface EditAnchorsDep {
1974
- /** Id of the node currently being edited. Empty string means no node is
1975
- * currently in edit mode — the chrome and gesture both opt out. */
1976
- editingId: string;
1977
- /** Enter/exit edit mode for a specific node. Pass `null` (or an empty
1978
- * string) to exit. `enterPathEditAction` and `exitPathEditAction` call
1979
- * this; consumers can call it directly to drive edit mode programmatically. */
1980
- setEditingId(id: string | null): void;
1981
- /** Returns the COMMITTED editable polygon in world coordinates, or
1982
- * null if this node has no editable polygon. Does NOT consult in-
1983
- * flight previews — callers that need live state read the dispatcher's
1984
- * in-flight handles. */
1985
- getEditablePath(id: string): unknown;
1986
- /** Returns where the polygon is stored — `'pose'` when `node.pose`
1987
- * IS the polygon, `'data'` when it lives on `node.data.path` with a
1988
- * rect pose, or `null` when the node has no editable polygon. The
1989
- * action uses this to know which preview-ghost axis to populate
1990
- * (`previewPose` only / `previewData` + `previewPose` for data.path). */
1991
- getStorageKind(id: string): 'pose' | 'data' | null;
1992
- /** Returns the node's raw `pose` and `data` so storage-aware actions
1993
- * can capture origin state at gesture-start and synthesize a matching
1994
- * `previewPose` / `previewData` during `onMove`. Used by
1995
- * `editAnchorsAction` for the data.path branch (rect pose + data
1996
- * carrying extra fields like fill / stroke that must be preserved
1997
- * through the preview). Returns null when the node is gone. */
1998
- getNodeShape(id: string): {
1999
- pose: unknown;
2000
- data: unknown;
2001
- } | null;
2002
- /** Commit `worldPath` as the new value for `id`. Implementation routes
2003
- * to setPose (when pose IS the polygon) or batched setPose+update
2004
- * (when the polygon lives on data.path). Records one history entry
2005
- * labelled `label`. */
2006
- applyEdit(id: string, worldPath: unknown, label: string): void;
2007
- /**
2008
- * Anchors currently selected within the edited path, as **flat anchor
2009
- * indices** — the same numbering `enumerateAnchors` produces and the
2010
- * `anchor:N` affordance kinds carry.
2011
- *
2012
- * Selection is transient UI state, deliberately not part of the scene:
2013
- * it is cleared whenever `editingId` changes, and any edit that
2014
- * renumbers anchors (insert, delete) is responsible for leaving it
2015
- * coherent. Empty means "no anchor selected" — the keyboard actions
2016
- * (nudge, delete) no-op rather than acting on all anchors, matching
2017
- * Illustrator.
2018
- */
2019
- selectedAnchors: ReadonlySet<number>;
2020
- /** Replace the anchor selection. Pass an empty iterable to clear. */
2021
- setSelectedAnchors(next: Iterable<number>): void;
2022
- /**
2023
- * In-flight anchor-marquee rect in world coords, or null when no
2024
- * marquee drag is active. Written by `marqueeAnchorsAction` and read by
2025
- * the path-editing overlay — the same "ongoing action owns the preview,
2026
- * chrome just draws it" split the move/resize ghosts use.
2027
- */
2028
- marquee: {
2029
- x: number;
2030
- y: number;
2031
- width: number;
2032
- height: number;
2033
- } | null;
2034
- /** Set or clear the in-flight marquee rect. */
2035
- setMarquee(rect: {
2036
- x: number;
2037
- y: number;
2038
- width: number;
2039
- height: number;
2040
- } | null): void;
2041
- }
2042
- /**
2043
- * Adapter dep for `lassoSelectAction`.
2044
- *
2045
- * Provides polygon-lasso hit-testing + selection read/write.
2046
- * Consumers that don't implement `hitTestLasso` can omit it; the action
2047
- * falls back to a bounding-box AABB test via `hitTestArea`.
2048
- */
2049
- interface LassoSelectDep {
2050
- /**
2051
- * Hit-test against a closed polygon (vertex order CW or CCW; last→first
2052
- * closing edge is implicit). Returns matching node ids.
2053
- * Optional — when absent, `lassoSelectAction` falls back to AABB via
2054
- * `hitTestArea`.
2055
- */
2056
- hitTestLasso?(polygon: ReadonlyArray<{
2057
- x: number;
2058
- y: number;
2059
- }>, mode: 'centers' | 'intersect' | 'enclosed'): string[];
2060
- /** Return ids of nodes whose AABB overlaps the given rect (fallback). */
2061
- hitTestArea(bounds: {
2062
- x: number;
2063
- y: number;
2064
- width: number;
2065
- height: number;
2066
- }): string[];
2067
- /** Return the current selection id list. */
2068
- getSelection(): string[];
2069
- /** Replace the current selection. */
2070
- setSelection(ids: string[]): void;
2071
- }
2072
- /**
2073
- * Options for the kit `image/svg+xml` content handler, threaded from
2074
- * SceneCanvas's `ingestion={{ svg }}` prop.
2075
- */
2076
- interface SvgIngestOptions {
2077
- /** Parse dropped/pasted/picked SVG files into native scene nodes (path /
2078
- * text leaves under containers mirroring the source `<g>` structure)
2079
- * instead of the default single embedded-image node.
2080
- *
2081
- * Pass `unpackSvgFiles` from `@weasel-js/svg`:
2082
- *
2083
- * ```ts
2084
- * import { unpackSvgFiles } from '@weasel-js/svg';
2085
- * <SceneCanvas ingestion={{ svg: { unpack: unpackSvgFiles } }} />
2086
- * ```
2087
- *
2088
- * It is injected rather than flagged on with `true` because the SVG parser
2089
- * lives in `@weasel-js/svg`, which depends on this package — core importing
2090
- * it back would make the two mutually dependent and unpublishable
2091
- * separately. Passing the function keeps the parser out of core's bundle
2092
- * for consumers who never unpack. */
2093
- unpack?: SvgUnpacker;
2094
- }
2095
- /** Parses SVG files and inserts the resulting nodes into `ctx.scene`, as one
2096
- * `applyOps` batch per file. Implemented by `unpackSvgFiles` in
2097
- * `@weasel-js/svg`; see {@link SvgIngestOptions.unpack}. */
2098
- type SvgUnpacker = (files: File[], ctx: IngestCtx) => Promise<void>;
2099
- /**
2100
- * Clipboard-paste seam consumed by the kit weasel-JSON content handler
2101
- * (`IngestCtx.clipboard`). Built by `<SceneCanvas>` from its own synthesized
2102
- * adapter + the `ingestion.clipboard` prop; absent when the consumer set
2103
- * `ingestion.clipboard.enabled === false` or the adapter lacks `commitPaste`.
2104
- * Absence makes the handler decline inert (dwarn, nothing ingested) — its
2105
- * matched items were already consumed at match time and do not fall through
2106
- * to other handlers.
2107
- */
2108
- interface ClipboardIngestCtx {
2109
- /** The hosting canvas's adapter — `commitPaste` materializes the pasted
2110
- * nodes (fresh ids, offset applied); insertion still goes through ops. */
2111
- adapter: InsertAdapter<{
2112
- id: string;
2113
- }>;
2114
- /** JSON reviver for the weasel wire payload (typed arrays etc.) — from
2115
- * `SceneCanvasProps.ingestion.clipboard.reviver`. */
2116
- reviver?: (key: string, value: unknown) => unknown;
2117
- }
2118
- /**
2119
- * Dep for the `ingest` action (external-content ingestion).
2120
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
2121
- * `useIngestionDepSource` — canvas rect + current view.
2122
- */
2123
- interface IngestionDep {
2124
- /** Visible canvas area in world coordinates. */
2125
- viewportWorldRect(): {
2126
- x: number;
2127
- y: number;
2128
- width: number;
2129
- height: number;
2130
- };
2131
- /** Consumer file→src resolver (from SceneCanvas's `ingestion` prop).
2132
- * Live accessor — read it at use time. Destructuring (or copying the
2133
- * property early) snapshots the current value and won't track later
2134
- * prop changes across an `await`. */
2135
- resolveSrc?: (file: File) => Promise<string>;
2136
- /** Kit SVG-handler options (from SceneCanvas's `ingestion` prop).
2137
- * Live accessor, same caveat as `resolveSrc`. */
2138
- svg?: SvgIngestOptions;
2139
- /** Clipboard-paste seam for the kit weasel-JSON handler.
2140
- * Live accessor, same caveat as `resolveSrc`. */
2141
- clipboard?: ClipboardIngestCtx;
2142
- }
2143
- /**
2144
- * Per-kind extra geometry passed to `InsertDep.commit`.
2145
- *
2146
- * Built-in tools populate a typed variant so the kit's default factory can
2147
- * render the true tool params (line endpoints, polygon side count, star
2148
- * geometry, pencil sample list). Consumer-defined tools may pass any
2149
- * `{ kind: string; ... }` payload; the kit's factory falls back to AABB
2150
- * inscription for unknown kinds.
2151
- *
2152
- * `bounds` is still passed alongside as a useful AABB pose hint — factories
2153
- * may use it as the node's pose even when richer geometry is available.
2154
- */
2155
- type InsertExtras = {
2156
- kind: 'rect';
2157
- } | {
2158
- kind: 'ellipse';
2159
- } | {
2160
- kind: 'line';
2161
- a: {
2162
- x: number;
2163
- y: number;
2164
- };
2165
- b: {
2166
- x: number;
2167
- y: number;
2168
- };
2169
- } | {
2170
- kind: 'polygon';
2171
- sides: number;
2172
- rotation: number;
2173
- center?: {
2174
- x: number;
2175
- y: number;
2176
- };
2177
- radius?: number;
2178
- } | {
2179
- kind: 'star';
2180
- points: number;
2181
- innerRadiusRatio: number;
2182
- rotation: number;
2183
- center?: {
2184
- x: number;
2185
- y: number;
2186
- };
2187
- outerRadius?: number;
2188
- } | {
2189
- kind: 'pencil';
2190
- samples: ReadonlyArray<DragSample>;
2191
- } | {
2192
- kind: 'text';
2193
- text?: string;
2194
- } | {
2195
- kind: 'image';
2196
- src?: string;
2197
- opacity?: number;
2198
- /** Chrome-only: what the in-flight drag paints. Read by the overlay
2199
- * layer, ignored by the insert dep. */
2200
- preview?: 'bitmap' | 'outline';
2201
- } | {
2202
- kind: string;
2203
- [extra: string]: unknown;
2204
- };
2205
- /**
2206
- * World-space point snapping — grid, guides, or any consumer rule.
2207
- *
2208
- * Sourced by `<SceneCanvas>` from its `toolOptions.snapPoint`. Actions apply
2209
- * it to the coords they ingest so the live preview and the committed
2210
- * geometry agree; `insertAction` snaps the drag's start and current point.
2211
- *
2212
- * Optional: when the dep is absent, actions treat it as identity.
2213
- */
2214
- interface SnapDep {
2215
- /** Snap a world-space point. Return `p` unchanged to opt out. */
2216
- point(p: {
2217
- x: number;
2218
- y: number;
2219
- }): {
2220
- x: number;
2221
- y: number;
2222
- };
2223
- }
2224
- /**
2225
- * Adapter dep for `insertAction`.
2226
- *
2227
- * Provided by `<SceneCanvas>` / `<StandardActionsRegistrar>`. The `extras`
2228
- * carry the active tool's kind + per-kind geometry. Callers
2229
- * that need typed data must supply a richer `insert` dep.
2230
- */
2231
- interface InsertDep {
2232
- /**
2233
- * Materialise a new node from the given drag-rect bounds and typed
2234
- * per-kind extras. Returns the new node's id, or `null` if the consumer
2235
- * rejected the insert (e.g. sub-threshold bounds, unknown kind).
2236
- */
2237
- commit(bounds: {
2238
- x: number;
2239
- y: number;
2240
- width: number;
2241
- height: number;
2242
- }, extras: InsertExtras): NodeId | null;
2243
- }
2244
- /**
2245
- * Adapter dep for `resizeAction`.
2246
- *
2247
- * Carries the four behavior-shaping options the legacy `useResize` hook
2248
- * exposed through `UseResizeOptions`: bounds-frame behaviors (e.g.
2249
- * `lockAspectWithModifier`), world-space anchor-point snap behaviors (e.g.
2250
- * `pointSnapToGrid`), group-expansion (`expandIds`), and pose↔bounds
2251
- * projection (`geometry`).
2252
- *
2253
- * Optional in `DepSchema`: when absent, `resizeAction` falls back to
2254
- * identity defaults (no behaviors, identity expandIds, `RECT_POSE_DESCRIPTOR`
2255
- * geometry). Consumers wire the dep via `useDepSource('resizePolicy', ...)`
2256
- * from any descendant of `<DepRegistryProvider>` / `<SceneCanvas>`.
2257
- *
2258
- * The generic is erased to `unknown` at the schema entry; consumers cast at
2259
- * the call site (mirrors the `scene` entry's convention).
2260
- */
2261
- interface ResizePolicy<TPose> {
2262
- /** Bounds-frame constraints. Constrained to `TPose extends ResizePose` since
2263
- * constraints read/write `{x,y,width,height}`. For non-rect TPose pass `[]`. */
2264
- constraints: TPose extends ResizePose ? BoundsConstraint<TPose>[] : never[];
2265
- /** World-space anchor-point snap behaviors. Same TPose constraint as
2266
- * `constraints`. */
2267
- pointSnap: TPose extends ResizePose ? PointSnapBehavior<TPose>[] : never[];
2268
- /** Group-expansion at gesture start. Identity (`ids => ids`) when group
2269
- * resize isn't wanted. */
2270
- expandIds: (ids: string[]) => string[];
2271
- /** Projection from `TPose` to bounds and back. Use `RECT_POSE_DESCRIPTOR`
2272
- * for plain rect poses. */
2273
- projection: PoseProjection<TPose>;
2274
- }
2275
- /**
2276
- * Layout-strategy lookup by container id, consumed by `moveAction` to run
2277
- * the drag-time reflow pass. Sourced by `<SceneCanvas>` from its `layouts`
2278
- * prop. Optional: `getLayout` returns null for any container when no layout
2279
- * is configured, so the reflow pass is a no-op then.
2280
- */
2281
- interface LayoutDep {
2282
- getLayout(containerId: string): LayoutStrategy<unknown> | null;
2283
- }
2284
- /**
2285
- * The names an action may declare in `requires`, and what each resolves to.
2286
- *
2287
- * This is the whole vocabulary of things an action can reach — selection,
2288
- * scene, view, history, and the rest. Consumers add their own entries by
2289
- * augmenting the interface (`declare module '@weasel-js/core'`), which is what
2290
- * makes a custom dep name type-check in `requires` and in the deps bag.
2291
- */
2292
- interface DepSchema {
2293
- /** Kit selection state — ids of currently selected nodes. */
2294
- selection: SelectionApi;
2295
- /** Current viewport — camera position + scale. */
2296
- view: ViewApi;
2297
- /**
2298
- * Scene tree — structural reads + undoable mutations.
2299
- *
2300
- * The entry uses the fully-erased form `Scene<unknown, string, unknown>`
2301
- * because `DepSchema` must be concrete. Actions that need a typed scene
2302
- * should cast: `deps.scene as Scene<MyData, MyLayer, MyPose>`.
2303
- */
2304
- scene: Scene<unknown, string, unknown>;
2305
- /** Undo/redo history bound to the current scene. */
2306
- history: History;
2307
- /**
2308
- * Canvas pointer position in world space.
2309
- *
2310
- * Exposes `pointerRef` (mutable live ref) and `getDropPoint()` thunk.
2311
- * Marked `@experimental` in the source.
2312
- */
2313
- pointer: PointerContextValue;
2314
- /** Currently active tool id + hotkey-hold stack. */
2315
- activeTool: ActiveToolContextValue;
2316
- /**
2317
- * Area-select dep — AABB hit-test + selection read/write.
2318
- *
2319
- * Sourced from `<SceneCanvas>` via AABB overlap over all scene
2320
- * nodes. Override per-consumer for custom hit-testing (e.g. contain-mode,
2321
- * lock-aware filtering).
2322
- */
2323
- areaSelect: AreaSelectDep;
2324
- /**
2325
- * Topmost node at a world-space point. Sourced by `<SceneCanvas>` from
2326
- * the same picker that feeds the tool dispatcher's `getNodeAtPoint`.
2327
- * Optional: actions that read this (e.g. `moveAction` reparent-on-drop)
2328
- * fall back to a no-op when the dep isn't registered.
2329
- */
2330
- nodeAtPoint?: NodeAtPointDep;
2331
- /**
2332
- * Insert dep — node factory for drag-to-insert.
2333
- *
2334
- * Sourced from `<SceneCanvas>`. The `kind` param comes from
2335
- * the active binding's `opts.params.kind`. Override per-consumer to
2336
- * provide a typed node factory (e.g. with custom data payloads).
2337
- */
2338
- insert: InsertDep;
2339
- /**
2340
- * Snap dep — world-space point snapping (grid / guides).
2341
- *
2342
- * Sourced by `<SceneCanvas>` from `toolOptions.snapPoint`. Optional:
2343
- * absent means no snapping (identity).
2344
- */
2345
- snap?: SnapDep;
2346
- /**
2347
- * Lasso-select dep — polygon hit-test + selection read/write.
2348
- *
2349
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>`.
2350
- * Falls back to AABB hit-test when `hitTestLasso` is absent.
2351
- */
2352
- lassoSelect: LassoSelectDep;
2353
- /**
2354
- * Edit-anchors dep — narrow read/write of one polygon's path pose.
2355
- *
2356
- * Sourced from consumer. Wraps `getPose`/`setPose`/`applyOps`
2357
- * for the currently-being-edited polygon node.
2358
- *
2359
- * The `editAnchorsAction` requires this dep to be registered when anchor
2360
- * editing is active. If absent, `start` returns an empty handle (no-op).
2361
- */
2362
- editAnchors: EditAnchorsDep;
2363
- /**
2364
- * Text-edit dep — activates the in-place text editing overlay.
2365
- *
2366
- * Sourced from consumer via `useTextEdit` / `useSceneTextEdit`.
2367
- * The `enterTextEditAction` requires this dep to be registered by the text
2368
- * tool when text editing is available.
2369
- *
2370
- * The optional `isTextNode` predicate guards against entering edit mode on
2371
- * non-text nodes. A binding can pre-filter instead with a
2372
- * `target: 'kind:text:selected'` spec; the guard remains for consumers who
2373
- * bind the broader `'selected-body'` target or opted out of routing.
2374
- */
2375
- textEdit: TextEditDep;
2376
- /**
2377
- * Resize-policy dep — bounds constraints, point-snap behaviors,
2378
- * group expansion, and pose↔bounds projection for `resizeAction`.
2379
- *
2380
- * Optional: when omitted, `resizeAction` falls back to identity defaults
2381
- * (no constraints, no snap, identity expandIds, `RECT_POSE_DESCRIPTOR`).
2382
- * Consumers wire via `useDepSource('resizePolicy', ...)` or the
2383
- * `useResizePolicy` helper.
2384
- */
2385
- resizePolicy?: ResizePolicy<unknown>;
2386
- /**
2387
- * Booleans adapter — read selection ids, fetch world-space `Path`s,
2388
- * compare z-order, and mint result nodes for Pathfinder ops.
2389
- *
2390
- * Consumers wire via `useBooleansAdapter(adapter)` (a thin wrapper
2391
- * around `useDepSource('booleansAdapter', ...)`). The descriptor's
2392
- * `enabled` predicate reads `deps.selection` for the count check; the
2393
- * invoker reads `deps.booleansAdapter` to execute the op.
2394
- */
2395
- booleansAdapter?: BooleansAdapter;
2396
- /**
2397
- * Gesture dispatcher control surface — exposes `cancelAll(reason)` so
2398
- * actions that need to abort an in-flight handle (Escape cancels a
2399
- * drag, etc.) can do so. Sourced by `<SceneCanvas>` from the
2400
- * dispatcher instance it already owns.
2401
- */
2402
- dispatcher?: {
2403
- cancelAll(reason: 'commit' | 'cancel'): void;
2404
- };
2405
- /**
2406
- * Layout-strategy lookup. Sourced by `<SceneCanvas>` from `layouts`.
2407
- * Optional: absent (or all-null) → `moveAction` skips reflow.
2408
- */
2409
- layout?: LayoutDep;
2410
- /**
2411
- * Slice dep — consumer-supplied commit for the Slice action.
2412
- *
2413
- * Receives the finite slice segment in world coordinates; the consumer
2414
- * scans the scene, splits crossed paths via `splitPathByLine`, and
2415
- * applies the result as one undoable batch.
2416
- *
2417
- * Optional: when absent, `sliceAction` is a no-op.
2418
- */
2419
- slice?: SliceDep;
2420
- /**
2421
- * Clipboard dep — the imperative surface `useClipboardOps` returns.
2422
- *
2423
- * Published by the consumer (`useDepSource('clipboard', …)` from under
2424
- * `<SceneCanvas>`), because `useClipboardOps` needs an adapter and a
2425
- * selection reader only the consumer has. Feeds `clipboard.copy` /
2426
- * `clipboard.cut`; both no-op when the dep is absent.
2427
- */
2428
- clipboard?: ClipboardDep;
2429
- /**
2430
- * Optional consumer commit hook. When present, `moveAction` (and other
2431
- * default actions) submit their committed ops through it instead of
2432
- * `scene.applyBatch`, so apps with their own history integration
2433
- * (checkpoint + push entry) capture the gesture as one undo entry.
2434
- * When absent, commits fall back to `scene.applyBatch`.
2435
- */
2436
- applyOps?: (ops: Op[], label: string) => void;
2437
- /** Optional pose-composition strategy for hierarchical (local-pose) scenes.
2438
- * When absent, defaults to IDENTITY (absolute-pose: nodes store world
2439
- * coords). Local-pose consumers supply { compose: composeRectPose,
2440
- * decompose: decomposeRectPose } (or their pose shape's equivalent). */
2441
- poseComposition?: PoseComposition<unknown>;
2442
- /**
2443
- * Ingestion dep — canvas viewport rect + consumer file→src resolver.
2444
- *
2445
- * Sourced from `<SceneCanvas>` / `<StandardActionsRegistrar>` via
2446
- * `useIngestionDepSource`. Feeds `ingestAction` with the world-space
2447
- * viewport rect for paste-placement and image fit-clamping, and forwards
2448
- * the consumer's optional `resolveSrc` seam.
2449
- *
2450
- * Optional: when absent, the `ingest` action no-ops (there is no
2451
- * placement geometry to work with).
2452
- */
2453
- ingestion?: IngestionDep;
2454
- /**
2455
- * Optional consumer seam for the eager-sync layer: lets pose-transform
2456
- * actions (resize/move/nudge/flip — NOT rotate) ALSO rewrite a node's
2457
- * data-held geometry. Given a node and the affine `m` applied to its pose,
2458
- * `transform(node, m)` returns updated `data` (geometry mapped by `m`) or
2459
- * `null` for nodes with no data-held geometry.
2460
- *
2461
- * Strictly opt-in: when absent (or when `transform` returns null), the kit
2462
- * emits only the pose op and leaves `data` untouched. apps/draw wires this
2463
- * to mirror `data.path` through `transformPath`. Rotate intentionally never
2464
- * consults this seam (rotation lives on the pose, baked at render).
2465
- */
2466
- geometryProjection?: GeometryProjection;
2467
- }
2468
- /**
2469
- * Every dep name the registry knows about — derived from {@link DepSchema} so
2470
- * the two can't drift.
2471
- *
2472
- * Declared here rather than beside the registry so that this `keyof` reference
2473
- * resolves to the exported `DepSchema` declaration; from another module it
2474
- * resolves to that module's import alias, which the API docs can't link.
2475
- */
2476
- type DepName = keyof DepSchema;
2477
-
2478
- /** Which kind of handle a recorded handle marker represents. */
2479
- type HandleKind = 'corner' | 'rotation' | 'anchor';
2480
- /** The geometry a hit region actually tests against, as reported to the debug
2481
- * sink so the overlay can draw the real shape rather than its bounding box. */
2482
- type HitShape = {
2483
- kind: 'rect';
2484
- x: number;
2485
- y: number;
2486
- width: number;
2487
- height: number;
2488
- rotation?: number;
2489
- } | {
2490
- kind: 'circle';
2491
- cx: number;
2492
- cy: number;
2493
- r: number;
2494
- } | {
2495
- kind: 'path';
2496
- d: Path2D;
44
+ declare const ICON_PATHS: {
45
+ readonly clone: "<path d=\"M7 7.4V4.6A1.6 1.6 0 0 1 8.6 3h6.8A1.6 1.6 0 0 1 17 4.6v6.8a1.6 1.6 0 0 1-1.6 1.6H12.6\" stroke-linecap=\"butt\"/><rect x=\"3\" y=\"7\" width=\"10\" height=\"10\" rx=\"1.6\"/><path d=\"M5.7 10.8h4.6M5.7 13.4h3\" stroke-width=\"1\"/>";
46
+ readonly reset: "<path d=\"M7.28 4.88A5.8 5.8 0 1 0 12.72 4.88\"/><path d=\"M13.81 7.57 12.72 4.88 15.56 4.28\"/>";
47
+ readonly close: "<path d=\"M5 5 15 15M15 5 5 15\" stroke-width=\"1.75\"/>";
48
+ readonly export: "<path d=\"M4 12.6v2.9A1.5 1.5 0 0 0 5.5 17h9a1.5 1.5 0 0 0 1.5-1.5v-2.9\"/><path d=\"M10 3.2v8.5\"/><path d=\"M11.7 9.35 10 11.7 8.3 9.35\"/><path d=\"M6.8 14.7h6.4\" stroke-width=\"1\"/>";
49
+ readonly zoomIn: "<circle cx=\"9\" cy=\"9\" r=\"5.4\"/><path d=\"M12.82 12.82 16.9 16.9\"/><path d=\"M9 6.7v4.6M6.7 9h4.6\"/>";
50
+ readonly pan: "<path d=\"M10 8.1V3.2M10 11.9V16.8M8.1 10H3.2M11.9 10H16.8\"/><path d=\"M8.26 5.27 10 3.2 11.74 5.27M11.74 14.73 10 16.8 8.26 14.73M5.27 11.74 3.2 10 5.27 8.26M14.73 8.26 16.8 10 14.73 11.74\"/><circle cx=\"10\" cy=\"10\" r=\"1.9\"/>";
51
+ readonly add: "<path d=\"M10 4.9v10.2M4.9 10h10.2\"/>";
52
+ readonly remove: "<path d=\"M4.9 10h10.2\"/>";
53
+ readonly delete: "<path d=\"M3.6 5.8h12.8M8 5.8V4.5A1.1 1.1 0 0 1 9.1 3.4h1.8A1.1 1.1 0 0 1 12 4.5v1.3\"/><path d=\"M5.2 5.8v9.6A1.6 1.6 0 0 0 6.8 17h6.4a1.6 1.6 0 0 0 1.6-1.6V5.8\"/><path d=\"M8.4 8.8v5.2M11.6 8.8v5.2\" stroke-width=\"1\"/>";
54
+ readonly sort: "<path d=\"M3.6 5.6h7.2M3.6 10h8.8M3.6 14.4h5\"/><path d=\"M15.3 5.4v9.4\"/><path d=\"M16.97 12.81 15.3 14.8 13.63 12.81\"/>";
55
+ readonly undo: "<path d=\"M4.4 8.4h6.8a3.6 3.6 0 0 1 0 7.2H8.6\"/><path d=\"M6.47 10.14 4.4 8.4 6.47 6.66\"/>";
56
+ readonly redo: "<path d=\"M15.6 8.4H8.8a3.6 3.6 0 0 0 0 7.2H11.4\"/><path d=\"M13.53 6.66 15.6 8.4 13.53 10.14\"/>";
57
+ readonly zoomOut: "<circle cx=\"9\" cy=\"9\" r=\"5.4\"/><path d=\"M12.82 12.82 16.9 16.9\"/><path d=\"M6.7 9h4.6\"/>";
58
+ readonly fit: "<path d=\"M3.2 7.2V4.4A1.2 1.2 0 0 1 4.4 3.2h2.8M12.8 3.2h2.8A1.2 1.2 0 0 1 16.8 4.4v2.8M16.8 12.8v2.8a1.2 1.2 0 0 1-1.2 1.2h-2.8M7.2 16.8H4.4a1.2 1.2 0 0 1-1.2-1.2v-2.8\"/><rect x=\"7.4\" y=\"8.2\" width=\"5.2\" height=\"3.6\" rx=\"0.8\" stroke-width=\"1\"/>";
59
+ readonly snapshot: "<path d=\"M3.8 8.3A1.4 1.4 0 0 1 5.2 6.9h1.9L8 5.1h4l.9 1.8h1.9A1.4 1.4 0 0 1 16.2 8.3v6A1.4 1.4 0 0 1 14.8 15.7H5.2A1.4 1.4 0 0 1 3.8 14.3z\"/><circle cx=\"10\" cy=\"11\" r=\"2.75\"/>";
60
+ readonly play: "<path d=\"M7.6 5.2 15.6 10 7.6 14.8Z\"/>";
61
+ readonly pause: "<path d=\"M7.8 5.2v9.6M12.2 5.2v9.6\"/>";
62
+ readonly stop: "<rect x=\"5.6\" y=\"5.6\" width=\"8.8\" height=\"8.8\" rx=\"1.4\"/>";
63
+ readonly step: "<path d=\"M6.4 5.4 13 10 6.4 14.6Z\"/><path d=\"M15.2 5.4v9.2\"/>";
64
+ readonly crosshair: "<circle cx=\"10\" cy=\"10\" r=\"5.6\"/><path d=\"M10 2.8v4.4M10 12.8v4.4M2.8 10h4.4M12.8 10h4.4\"/>";
65
+ readonly fullscreen: "<path d=\"M11.6 3.6h4.8v4.8M8.4 16.4H3.6v-4.8\"/><path d=\"M16.4 3.6 11.2 8.8M3.6 16.4 8.8 11.2\"/>";
66
+ readonly compare: "<rect x=\"3.4\" y=\"4.6\" width=\"13.2\" height=\"10.8\" rx=\"1.6\"/><path d=\"M10 4.6v10.8\" stroke-width=\"1\"/><path d=\"M5.2 7.6h3.4M5.2 10h2.6M5.2 12.4h3.4\" stroke-width=\"1\"/><path d=\"M11.4 7.6h3.4M11.4 10h3.4M11.4 12.4h1.8\" stroke-width=\"1\"/>";
67
+ readonly filter: "<path d=\"M3.4 4.6h13.2L11.8 11v5.4L8.2 14.8V11z\"/>";
68
+ readonly search: "<circle cx=\"9\" cy=\"9\" r=\"5.4\"/><path d=\"M12.82 12.82 16.9 16.9\"/>";
69
+ readonly loupe: "<circle cx=\"9\" cy=\"9\" r=\"5.6\"/><path d=\"M12.96 12.96 17 17\"/><rect x=\"6\" y=\"6\" width=\"6\" height=\"6\" stroke-width=\"1\"/><path d=\"M9 6v6M6 9h6\" stroke-width=\"1\"/>";
70
+ readonly layers: "<path d=\"M10 2.8 17.2 6.6 10 10.4 2.8 6.6z\"/><path d=\"M2.8 10 10 13.8 17.2 10\" stroke-width=\"1\"/><path d=\"M2.8 13.4 10 17.2 17.2 13.4\" stroke-width=\"1\"/>";
71
+ readonly lock: "<rect x=\"4.6\" y=\"9\" width=\"10.8\" height=\"8\" rx=\"1.6\"/><path d=\"M7.2 9V6.6a2.8 2.8 0 0 1 5.6 0V9\"/>";
72
+ readonly unlock: "<rect x=\"4.6\" y=\"9\" width=\"10.8\" height=\"8\" rx=\"1.6\"/><path d=\"M7.2 9V6.6a2.8 2.8 0 0 1 5.6 0\"/>";
73
+ readonly visible: "<path d=\"M2.6 10C4.6 6.6 7.1 5 10 5s5.4 1.6 7.4 5c-2 3.4-4.5 5-7.4 5s-5.4-1.6-7.4-5z\"/><circle cx=\"10\" cy=\"10\" r=\"2.2\"/>";
74
+ readonly hidden: "<path d=\"M2.6 10C4.6 6.6 7.1 5 10 5s5.4 1.6 7.4 5c-2 3.4-4.5 5-7.4 5s-5.4-1.6-7.4-5z\"/><circle cx=\"10\" cy=\"10\" r=\"2.2\"/><path d=\"M4.2 15.8 15.8 4.2\"/>";
75
+ readonly pin: "<path d=\"M8.2 3.4v5.2l-2 2.6h7.6l-2-2.6V3.4z\"/><path d=\"M7 3.4h6\"/><path d=\"M10 11.2v5.4\"/>";
76
+ readonly link: "<path d=\"M8.6 11.4a3.4 3.4 0 0 1 0-4.8l2.2-2.2a3.4 3.4 0 0 1 4.8 4.8l-1.1 1.1\"/><path d=\"M11.4 8.6a3.4 3.4 0 0 1 0 4.8l-2.2 2.2a3.4 3.4 0 0 1-4.8-4.8l1.1-1.1\"/>";
77
+ readonly collapse: "<path d=\"M14.16 4.05 10 7.8 5.84 4.05M5.84 15.95 10 12.2 14.16 15.95\"/>";
78
+ readonly expand: "<path d=\"M5.84 8.15 10 4.4 14.16 8.15M14.16 11.85 10 15.6 5.84 11.85\"/>";
79
+ readonly chevron: "<path d=\"M14.9 7.98 10 12.4 5.1 7.98\"/>";
80
+ readonly tune: "<path d=\"M3.4 6h13.2M3.4 10h13.2M3.4 14h13.2\" stroke-width=\"1.25\"/><circle cx=\"7\" cy=\"6\" r=\"1.8\"/><circle cx=\"12.6\" cy=\"10\" r=\"1.8\"/><circle cx=\"9\" cy=\"14\" r=\"1.8\"/>";
81
+ readonly grid: "<rect x=\"3.4\" y=\"3.4\" width=\"13.2\" height=\"13.2\" rx=\"1.4\"/><path d=\"M7.8 3.4v13.2M12.2 3.4v13.2M3.4 7.8h13.2M3.4 12.2h13.2\" stroke-width=\"1\"/>";
82
+ readonly snap: "<path d=\"M5.4 15.6V9.4a4.6 4.6 0 0 1 9.2 0v6.2h-3.2V9.4a1.4 1.4 0 0 0-2.8 0v6.2z\"/><path d=\"M5.4 12.8h3.2M11.4 12.8h3.2\" stroke-width=\"1\"/>";
83
+ readonly measure: "<rect x=\"2.6\" y=\"7.4\" width=\"14.8\" height=\"5.2\" rx=\"1.2\"/><path d=\"M6 7.4v2.2M9.4 7.4v3M12.8 7.4v2.2\" stroke-width=\"1\"/>";
84
+ readonly randomize: "<rect x=\"3.6\" y=\"3.6\" width=\"12.8\" height=\"12.8\" rx=\"2\"/><circle cx=\"7.2\" cy=\"7.2\" r=\"1\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"10\" cy=\"10\" r=\"1\" fill=\"currentColor\" stroke=\"none\"/><circle cx=\"12.8\" cy=\"12.8\" r=\"1\" fill=\"currentColor\" stroke=\"none\"/>";
85
+ readonly refresh: "<path d=\"M4.27 12.09A6.1 6.1 0 0 1 14.67 6.08\"/><path d=\"M14.67 3.38 14.67 6.08 12.01 5.61\"/><path d=\"M15.73 7.91A6.1 6.1 0 0 1 5.33 13.92\"/><path d=\"M5.33 16.62 5.33 13.92 7.99 14.39\"/>";
86
+ readonly info: "<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M10 9.4v4.4\"/><path d=\"M10 6.5h0\"/>";
87
+ readonly warning: "<path d=\"M10 3.4 17.4 16.2H2.6z\"/><path d=\"M10 8.4v3.4\"/><path d=\"M10 14h0\"/>";
88
+ readonly error: "<circle cx=\"10\" cy=\"10\" r=\"7\"/><path d=\"M7.6 7.6 12.4 12.4M12.4 7.6 7.6 12.4\"/>";
89
+ readonly busy: "<path d=\"M10 3.2A6.8 6.8 0 1 1 3.2 10\"/>";
90
+ readonly modeLight: "<circle cx=\"10\" cy=\"10\" r=\"3.5\"/><path d=\"M15.3 10L17.7 10M13.75 6.25L15.44 4.56M10 4.7L10 2.3M6.25 6.25L4.56 4.56M4.7 10L2.3 10M6.25 13.75L4.56 15.44M10 15.3L10 17.7M13.75 13.75L15.44 15.44\"/>";
91
+ readonly modeDark: "<path d=\"M8.19 3.55A6.7 6.7 0 1 0 16.45 11.81A5.9 5.9 0 0 1 8.19 3.55Z\"/>";
92
+ readonly modeAuto: "<path d=\"M10 2.6Q10 10 17.4 10Q10 10 10 17.4Q10 10 2.6 10Q10 10 10 2.6Z\"/>";
93
+ readonly strokeWidth: "<path d=\"M3.4 5.6h13.2\" stroke-width=\"1\"/><path d=\"M3.4 10h13.2\" stroke-width=\"2\"/><path d=\"M3.4 15.2h13.2\" stroke-width=\"3.4\"/>";
94
+ readonly strokeCap: "<path d=\"M2.5 10H12\" stroke-linecap=\"butt\"/><path d=\"M12 6V14\" stroke-width=\"1\"/>";
95
+ readonly strokeJoin: "<path d=\"M4.4 15.5 10 5.5 15.6 15.5\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\"/>";
96
+ readonly strokeAlign: "<circle cx=\"10\" cy=\"10\" r=\"6.2\"/>";
97
+ readonly capButt: "<path d=\"M5.5 5.25H14.5V14.75H5.5Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"miter\"/>";
98
+ readonly capRound: "<path d=\"M3.13 5.25H12.13V14.75H3.13Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"miter\"/><path d=\"M12.13 4.5A5.5 5.5 0 0 1 12.13 15.5Z\" fill=\"currentColor\" stroke=\"none\"/>";
99
+ readonly capSquare: "<path d=\"M3.13 5.25H12.13V14.75H3.13Z\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linejoin=\"miter\"/><path d=\"M12.13 4.5H17.63V15.5H12.13Z\" fill=\"currentColor\" stroke=\"none\"/>";
100
+ readonly joinMiter: "<path d=\"M10 3.5 15.6 15.5H4.4Z\" fill=\"currentColor\" stroke=\"none\"/>";
101
+ readonly joinRound: "<path d=\"M4.4 15.5V12a5.6 5.6 0 0 1 11.2 0v3.5Z\" fill=\"currentColor\" stroke=\"none\"/>";
102
+ readonly joinBevel: "<path d=\"M6.8 8.2h6.4l2.4 7.3H4.4Z\" fill=\"currentColor\" stroke=\"none\"/>";
103
+ readonly alignInner: "<path d=\"M10 3.8A6.2 6.2 0 1 0 10 16.2A6.2 6.2 0 1 0 10 3.8Z\" fill=\"currentColor\" stroke=\"none\"/>";
104
+ readonly alignCenter: "<path d=\"M10 2.1A7.9 7.9 0 1 0 10 17.9A7.9 7.9 0 1 0 10 2.1ZM10 5.5A4.5 4.5 0 1 0 10 14.5A4.5 4.5 0 1 0 10 5.5Z\" fill=\"currentColor\" stroke=\"none\" fill-rule=\"evenodd\"/>";
105
+ readonly alignOuter: "<path d=\"M0 0H20V20H0ZM10 3.8A6.2 6.2 0 1 0 10 16.2A6.2 6.2 0 1 0 10 3.8Z\" fill=\"currentColor\" stroke=\"none\" fill-rule=\"evenodd\"/>";
106
+ readonly dashSolid: "<path d=\"M2 10h16\" stroke-width=\"3\" stroke-linecap=\"butt\"/>";
107
+ readonly dashDashed: "<path d=\"M2 10h16\" stroke-width=\"3\" stroke-linecap=\"butt\" stroke-dasharray=\"6 4\"/>";
108
+ readonly dashDotted: "<path d=\"M2 10h16\" stroke-width=\"3\" stroke-linecap=\"butt\" stroke-dasharray=\"2.5 2\"/>";
109
+ readonly dashCustom: "<path d=\"M2 10h16\" stroke-width=\"3\" stroke-linecap=\"butt\" stroke-dasharray=\"5 2 2 2\"/>";
110
+ readonly paintSolid: "<path d=\"M3.4 3.4H16.6V16.6H3.4Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"1\"/>";
111
+ readonly paintLinear: "<path d=\"M3.4 3.4H6.7V16.6H3.4Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"1\"/><path d=\"M6.7 3.4H10V16.6H6.7Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.68\"/><path d=\"M10 3.4H13.3V16.6H10Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.42\"/><path d=\"M13.3 3.4H16.6V16.6H13.3Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.2\"/>";
112
+ readonly paintRadial: "<path d=\"M3.4 3.4H16.6V16.6H3.4ZM4.95 4.95H15.05V15.05H4.95Z\" fill=\"currentColor\" fill-rule=\"evenodd\" stroke=\"none\" fill-opacity=\"0.7\"/><path d=\"M4.95 4.95H15.05V15.05H4.95ZM6.5 6.5H13.5V13.5H6.5Z\" fill=\"currentColor\" fill-rule=\"evenodd\" stroke=\"none\" fill-opacity=\"0.53\"/><path d=\"M6.5 6.5H13.5V13.5H6.5ZM8.05 8.05H11.95V11.95H8.05Z\" fill=\"currentColor\" fill-rule=\"evenodd\" stroke=\"none\" fill-opacity=\"0.37\"/><path d=\"M8.05 8.05H11.95V11.95H8.05Z\" fill=\"currentColor\" fill-rule=\"evenodd\" stroke=\"none\" fill-opacity=\"0.2\"/>";
113
+ readonly paintConic: "<path d=\"M10 10L16.6 3.4L16.6 16.6Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"1\"/><path d=\"M10 10L16.6 16.6L3.4 16.6Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.68\"/><path d=\"M10 10L3.4 16.6L3.4 3.4Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.42\"/><path d=\"M10 10L3.4 3.4L16.6 3.4Z\" fill=\"currentColor\" stroke=\"none\" fill-opacity=\"0.2\"/>";
114
+ readonly paintPattern: "<path d=\"M3.4 3.4L4.7 3.4L3.4 4.7Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M8.7 3.4L11.3 3.4L3.4 11.3L3.4 8.7Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M15.3 3.4L16.6 3.4L16.6 4.7L4.7 16.6L3.4 16.6L3.4 15.3Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M16.6 8.7L16.6 11.3L11.3 16.6L8.7 16.6Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M16.6 15.3L16.6 16.6L15.3 16.6Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M15.3 3.4L16.6 3.4L16.6 4.7Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M8.7 3.4L11.3 3.4L16.6 8.7L16.6 11.3Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M3.4 3.4L4.7 3.4L16.6 15.3L16.6 16.6L15.3 16.6L3.4 4.7Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M11.3 16.6L8.7 16.6L3.4 11.3L3.4 8.7Z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M4.7 16.6L3.4 16.6L3.4 15.3Z\" fill=\"currentColor\" stroke=\"none\"/>";
115
+ readonly paintNone: "<path d=\"M3.4 3.4H16.6V16.6H3.4Z\"/><path d=\"M3.4 16.6L16.6 3.4\" stroke=\"var(--wzl-danger, #d94a3f)\" stroke-width=\"1.8\" stroke-linecap=\"butt\"/>";
116
+ readonly layoutRows: "<rect x=\"3.4\" y=\"3.4\" width=\"13.2\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"3.4\" y=\"8.3\" width=\"13.2\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"3.4\" y=\"13.2\" width=\"13.2\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/>";
117
+ readonly layoutColumns: "<rect x=\"3.4\" y=\"3.4\" width=\"3.4\" height=\"13.2\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"8.3\" y=\"3.4\" width=\"3.4\" height=\"13.2\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"13.2\" y=\"3.4\" width=\"3.4\" height=\"13.2\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/>";
118
+ readonly layoutGrid: "<rect x=\"3.4\" y=\"3.4\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"8.3\" y=\"3.4\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"13.2\" y=\"3.4\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"3.4\" y=\"8.3\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"8.3\" y=\"8.3\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"13.2\" y=\"8.3\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"3.4\" y=\"13.2\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"8.3\" y=\"13.2\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/><rect x=\"13.2\" y=\"13.2\" width=\"3.4\" height=\"3.4\" rx=\"1\" fill=\"currentColor\" stroke=\"none\"/>";
2497
119
  };
2498
- /**
2499
- * Where the kit reports what it is doing so the debug overlay can draw it.
2500
- *
2501
- * Recording is push-based and cheap: hit-testers, handle painters and snap
2502
- * strategies call these as they run, whether or not any overlay is watching.
2503
- * Nothing here affects behavior — a sink that discards everything is a valid
2504
- * sink.
2505
- */
2506
- interface DebugSink {
2507
- recordHitbox(id: string, kind: 'body' | 'handle' | 'rotation' | 'anchor', shape: HitShape): void;
2508
- recordHandle(id: string, position: {
2509
- x: number;
2510
- y: number;
2511
- }, kind: HandleKind): void;
2512
- recordBounds(id: string, bounds: {
2513
- x: number;
2514
- y: number;
2515
- width: number;
2516
- height: number;
2517
- }): void;
2518
- recordOrigin(id: string, point: {
2519
- x: number;
2520
- y: number;
2521
- }): void;
2522
- recordSnapCandidate(point: {
2523
- x: number;
2524
- y: number;
2525
- }, accepted: boolean): void;
2526
- recordLayer(id: string, label: string, space: 'world' | 'screen', index: number): void;
2527
- /** Clears every non-snap array. Called at the start of each Canvas render. */
2528
- beginFrame(): void;
2529
- /** Clears the snap array. Called at gesture end. */
2530
- clearSnap(): void;
2531
- }
2532
-
2533
- /**
2534
- * When an entry's bindings are live. A set, not one value: the hand tool is
2535
- * palette-selectable AND engaged by holding space, and both hold at once.
2536
- */
2537
- interface Eligibility {
2538
- /** Selectable as the focused entry — exclusive, one at a time. */
2539
- focus?: boolean;
2540
- /** Also live while this key is held. */
2541
- offhand?: HotkeyTrigger;
2542
- /** Live regardless of what is focused. */
2543
- always?: boolean;
2544
- /** Live only for input this entry's own affordances produced. */
2545
- claimed?: boolean;
2546
- /** Modality filter, applied wherever it would otherwise be live. */
2547
- capabilities?: CapabilityTag[];
2548
- }
2549
- /**
2550
- * Where an entry's overlay sits in the layer stack, relative to the
2551
- * selection chrome. `'top'` is the default and renders above everything;
2552
- * the other two exist for chrome that belongs under the selection handles
2553
- * (a snap-target highlight, say). With no selection overlay in the stack,
2554
- * all three collapse to `'top'`.
2555
- */
2556
- type OverlayPosition = 'top' | 'before-selection' | 'after-selection';
2557
- /**
2558
- * A registry entry: what it contributes, and when it is eligible. Every role
2559
- * is optional and independent — an entry that only routes input declares only
2560
- * `bindings` and `actions`.
2561
- */
2562
- interface Contribution {
2563
- id: string;
2564
- eligibility: Eligibility;
2565
- bindings?: GestureBinding[];
2566
- actions?: Action[];
2567
- /** One layer, or several composed in the given order. */
2568
- overlay?: RenderLayer<unknown> | RenderLayer<unknown>[];
2569
- /** Defaults to `'top'`. Applies to every layer in `overlay`. */
2570
- overlayPosition?: OverlayPosition;
2571
- presentation?: ToolPresentation;
2572
- /** Reflection escape hatch — the authored form, when there was one. */
2573
- def?: unknown;
2574
- }
2575
-
2576
- /**
2577
- * Configurable activation-key descriptor for tools that expose their
2578
- * keybinding to the host (currently Lasso and Eyedropper). Captures
2579
- * only the fields meaningful to a caller-supplied tool-select key —
2580
- * dispatcher-internal fields (`skipInEditable`, `enabled`,
2581
- * `preventDefault`) live on `KeyBinding` in keyHelpers.ts and are
2582
- * not part of the configurable surface.
2583
- */
2584
- interface ToolKeybinding {
2585
- /** Key or list of keys to match (case-insensitive against `event.key`). */
2586
- key: string | readonly string[];
2587
- /** Require Cmd (mac) / Ctrl (others). Default `false`. */
2588
- mod?: boolean;
2589
- /** Require Alt. Default `false`. */
2590
- alt?: boolean;
2591
- /**
2592
- * Shift policy. `undefined`/`false` forbids shift, `true` requires
2593
- * shift, `'optional'` allows either.
2594
- */
2595
- shift?: boolean | 'optional';
2596
- }
120
+ /** Every glyph name in the set. */
121
+ type IconName = keyof typeof ICON_PATHS;
2597
122
 
2598
- /** Modifier-key snapshot at event dispatch time. `space` is included
2599
- * because tools commonly use space as a hotkey-slot trigger and may
2600
- * also want to read it as a flag mid-gesture. */
2601
- interface ToolModifiers {
2602
- alt: boolean;
2603
- shift: boolean;
2604
- meta: boolean;
2605
- ctrl: boolean;
2606
- space: boolean;
2607
- }
2608
- /** Per-event context passed to every channel handler. `scratch` is typed
2609
- * via the tool's `TScratch` parameter; it survives across a single
2610
- * gesture (pointer-down through end/cancel) and is replaced on next
2611
- * gesture start by `initScratch()`. */
2612
- interface ToolCtx<TScratch = unknown> {
2613
- worldX: number;
2614
- worldY: number;
2615
- modifiers: ToolModifiers;
2616
- selection: SelectionApi;
2617
- /** Adapter/scene access — opaque at this layer; tools that need it
2618
- * cast to a known shape. This layer doesn't constrain it. */
2619
- adapter: unknown;
2620
- applyOps: (ops: Op[], label: string) => void;
2621
- /** Current viewport. Reflects camera-position semantics — see
2622
- * `View` JSDoc. */
2623
- view: View;
2624
- /** Mutate the viewport. In controlled mode this calls the consumer's
2625
- * `onViewChange`; in uncontrolled mode it updates Canvas's internal
2626
- * state. View changes are not undoable. */
2627
- setView: (next: View) => void;
2628
- /** Bounding rect of the canvas element in viewport coords. Used by
2629
- * zoom/pan tools to convert event clientX/clientY to canvas-relative
2630
- * anchors. */
2631
- canvasRect: DOMRect;
2632
- /** Screen-space pointer coords relative to `canvasRect`. Useful for
2633
- * viewport tools that pan/zoom in screen space (e.g. hand-pan
2634
- * computes deltas in pixels, not world units). Optional — populated
2635
- * by the dispatcher on pointer events; absent on keyboard events. */
2636
- screenPoint?: {
2637
- x: number;
2638
- y: number;
2639
- };
2640
- /** Optional debug sink. When `<Canvas debug={...}>` is enabled, Canvas
2641
- * threads its sink here so tool-internal hit math (handle hitboxes,
2642
- * rotation handle, etc.) lands in the same overlay as Canvas's own
2643
- * bounds/origin records. Tools should call this conditionally with `?.`. */
2644
- debug?: DebugSink;
2645
- scratch: TScratch;
2646
- }
2647
- /** Hotkey-slot trigger key. The slot is engaged while this key is held —
2648
- * hence "hotkey": active as long as the key is hot. `null` (or omitted)
2649
- * means the tool is not eligible for the hotkey slot. */
2650
- type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
2651
- /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
2652
- * `Bounds` type — the optional `rotation` field carries through so a tool
2653
- * can report an oriented preview rect (e.g. mid-rotate). */
2654
- type ToolBounds = Bounds;
2655
- /** Presentation metadata for tool palettes / menus. Optional on every
2656
- * tool — consumers that render a palette (`<ToolPalette>`) read these
2657
- * fields to display the tool; consumers that don't can ignore them.
2658
- *
2659
- * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
2660
- * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
2661
- interface ToolPresentation<TScratch = unknown> {
2662
- /** Human-readable label, distinct from the `id`. Falls back to `id`. */
123
+ /** Shared prop shape for the icon set. Color comes from the surrounding
124
+ * `color` CSS property — every glyph strokes in `currentColor`. */
125
+ interface IconProps {
126
+ className?: string;
127
+ /** Rendered pixel size, applied to both width and height. Defaults to 20. */
128
+ size?: number;
129
+ /** Accessible name. Omit inside a button that already labels itself; the
130
+ * glyph is then `aria-hidden`. */
2663
131
  label?: string;
2664
- /** Inline-SVG icon component output. May be a static `ReactNode` or a
2665
- * function of scratch state (rare; useful for shape-aware affordances). */
2666
- icon?: react.ReactNode | ((scratch?: TScratch) => react.ReactNode);
2667
- /** Palette grouping key. Tools sharing a group render contiguously
2668
- * with separators between groups. Free-form string; the kit
2669
- * recommends 'select' | 'shape' | 'draw' | 'type' | 'view'. */
2670
- group?: string;
2671
- /** Display override for the keyboard shortcut. When omitted the palette
2672
- * derives one from `Tool.keybinding` via its own formatter. */
2673
- shortcut?: string;
2674
- }
2675
- /**
2676
- * The focus-declaring case of a `Contribution`: a mode the user switches
2677
- * into, plus the hooks that only make sense for one (`initScratch`,
2678
- * activate/deactivate, live preview, `cursor`). Everything else — bindings,
2679
- * actions, overlay, presentation — is inherited.
2680
- */
2681
- interface Tool<TScratch = unknown> extends Contribution {
2682
- /** Optional caller-supplied key. Most built-in tools have their activation
2683
- * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
2684
- * is for tools that want their activation key to be configurable by the
2685
- * host (currently Lasso and Eyedropper). The dynamic loop in
2686
- * `useKeybindings.ts` picks this up and appends a binding entry to the
2687
- * consolidated `tool.activate` action (with `opts.params.toolId` set so
2688
- * the invoker knows which tool to switch to). */
2689
- keybinding?: ToolKeybinding;
2690
- initScratch?: () => TScratch;
2691
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
2692
- onActivate?: (ctx: ToolCtx<TScratch>) => void;
2693
- onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
2694
- /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
2695
- * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
2696
- * reflect live gesture state without reaching into hook internals. The
2697
- * return type is `unknown` here because the Tool interface is pose-agnostic;
2698
- * callers that know the pose shape (e.g. Canvas typed by `TPose`) cast at
2699
- * the use site. */
2700
- previewPose?: (id: string) => unknown;
2701
- /** Returns the in-flight preview bounds for `id` if this tool is mid-gesture
2702
- * on it; otherwise `null`. Optional companion to `previewPose` for tools that
2703
- * can compute bounds without round-tripping through a geometry adapter. */
2704
- previewBounds?: (id: string) => ToolBounds | null;
2705
- /** Returns ids whose committed scene-render should be suppressed while this
2706
- * tool is mid-gesture (e.g. cascade move's dragged + descendant ids whose
2707
- * preview ghosts replace the committed pose). The standard scene slot
2708
- * consults this alongside `previewPose` to avoid double-rendering. Returns
2709
- * `null` when no gesture is in flight. */
2710
- previewIds?: () => Iterable<string> | null;
2711
- }
2712
- /** Internal alias for "a Tool of any scratch type" — used in registries and
2713
- * dispatchers that hold tools of heterogeneous scratch shapes. `any` is
2714
- * intentional: `Tool<TScratch>` is invariant in TScratch, so `Tool<unknown>`
2715
- * is too strict for containers that accept any concrete `Tool<T>`. */
2716
- type AnyTool = Tool<any>;
2717
-
2718
- /**
2719
- * @experimental
2720
- * A single entry in `Action.defaultBinding[]`. Either a bare `GestureSpec`
2721
- * (no per-binding opts) or an object form that pairs a spec with
2722
- * `BindingOpts` for parametric actions (e.g. `{ params: { axis: 'x' } }`).
2723
- * Use the object form when two bindings for the same action differ only in
2724
- * a runtime parameter — the dispatcher extracts `opts.params` and passes
2725
- * them to `ImmediateInvoker.run` as its second argument.
2726
- */
2727
- type BoundGesture = GestureSpec | {
2728
- spec: GestureSpec;
2729
- opts: BindingOpts;
2730
- };
2731
- /**
2732
- * @experimental
2733
- * Single registered action. v1: one binding per action.
2734
- */
2735
- interface Action {
2736
- id: string;
2737
- label: string;
2738
- /** The gesture-spec form of the binding, read by the gesture dispatcher.
2739
- * May be a single `GestureSpec`, a bare `GestureSpec[]` (any-of semantics),
2740
- * or a `BoundGesture[]` where each entry is either a bare `GestureSpec` or
2741
- * `{ spec, opts }` — use the object form for parametric actions where two
2742
- * bindings for the same action differ only by `opts.params` (e.g. `flip`
2743
- * with `axis: 'x'` vs `'y'`). The dispatcher extracts `opts.params` and
2744
- * passes them to `ImmediateInvoker.run` as its second argument. */
2745
- defaultBinding?: GestureSpec | BoundGesture[];
2746
- /** Names of the deps this action's invoker reads (keys of `DepSchema`).
2747
- * The dispatcher (and `trigger`, when `requires` is present) resolves
2748
- * each name against the `DepRegistry` at invocation time and passes the
2749
- * resulting bag to the invoker. Dev builds warn when the invoker reads a
2750
- * dep it didn't declare here — see `buildDepsFromRequires`. */
2751
- requires?: readonly DepName[];
2752
- /** Inline-SVG icon for palette / toolbar surfaces. Mirrors
2753
- * `ToolPresentation.icon` so a generic `<ActionBar>` can render from
2754
- * action metadata the same way `<ToolPalette>` renders from tool
2755
- * metadata. May be a static `ReactNode` or a function (rare; useful
2756
- * for state-aware icons like a "lock" toggle). */
2757
- icon?: ReactNode | (() => ReactNode);
2758
- /** Grouping key for palette/menu surfaces. Free-form string; the kit
2759
- * ships defaults for `'align'` (six edges/centers), `'distribute'`
2760
- * (two axes), and recommends `'pathfinder'` for boolean ops. */
2761
- group?: string;
2762
- /** Display override for the keyboard shortcut. When omitted, palette
2763
- * surfaces derive a label from `defaultBinding` via their own
2764
- * formatter. */
2765
- shortcut?: string;
2766
- /** Pluggable invocation strategy. The gesture dispatcher routes matched
2767
- * bindings through `invoker.start` / `invoker.run` depending on timing.
2768
- * All kit-standard descriptors ship one; consumer-supplied actions
2769
- * without an invoker can still register but won't be triggered. */
2770
- invoker?: Invoker;
2771
- /** When set to `'hotkey'`, this action's `defaultBinding` rides the hotkey
2772
- * `BindingScope` instead of the ambient scope — meaning it beats any
2773
- * active-tool binding on the same input shape. Use for tool-switch
2774
- * shortcuts and global held-key triggers. Default: ambient. */
2775
- scope?: 'hotkey';
2776
- /**
2777
- * @experimental
2778
- * Optional predicate the command palette consults when rendering. Return
2779
- * `true` when the action is currently triggerable. Return a reason string
2780
- * (e.g. `'Selection required'`) when disabled — the palette greys out
2781
- * the row, skips it in keyboard nav, ignores clicks, and shows the
2782
- * reason next to the label. Keystroke dispatch (the registered binding)
2783
- * is unaffected; the action's own `run` should self-guard.
2784
- *
2785
- * **Contract:** must be pure (no side effects), fast (< 4ms in dev), and
2786
- * must not throw. If a call throws or exceeds the budget in dev mode,
2787
- * `evaluateEnabled` logs a one-time warning per action id; throws are
2788
- * caught and treated as disabled with reason `'(predicate threw)'`.
2789
- *
2790
- * Snapshot-on-open semantics: the palette evaluates `enabled` once when
2791
- * opened and does NOT re-evaluate on selection changes while open. Live
2792
- * reactive updates are deferred — palette is short-lived.
2793
- *
2794
- * The reason set is a closed enum — to add a new reason, edit
2795
- * `ActionDisabledReason` and the consumer's display map.
2796
- *
2797
- * The optional `deps` argument is the same bag passed to
2798
- * `ImmediateInvoker.run`; callers (`evaluateEnabled` / the ActionBar) may
2799
- * synthesize it from the surrounding `DepRegistry` so predicates can
2800
- * inspect selection / scene / etc. Predicates that don't need deps just
2801
- * ignore the arg.
2802
- */
2803
- enabled?: (deps?: ActionDeps) => true | ActionDisabledReason;
2804
- /**
2805
- * Declarative eligibility rule, evaluated against the current
2806
- * `RuleCtx` by the dispatcher before invoking `start()`. Omitted =
2807
- * always eligible.
2808
- *
2809
- * Accepts either a fluent `Condition` (callable with `.rule`) or a
2810
- * raw `Rule` tree; the dispatcher normalizes via `.rule` unwrap.
2811
- *
2812
- * Prefer `capability:`-based rules (e.g. `{ capability: 'transforms-selection' }`)
2813
- * over `mode:` rules — capability rules survive new modes being added
2814
- * that allow the same capability.
2815
- */
2816
- eligible?: Rule | Condition;
2817
- /**
2818
- * CSS cursor shown while the pointer hovers a spot where this action
2819
- * would win the drag. The hover-cursor pump (in `useGestureDispatcher`)
2820
- * runs `Dispatcher.resolveOnly` on each idle pointermove — the same
2821
- * match walk a real pointerdown takes — and applies the winning
2822
- * action's `cursor`, so the hint and the actual click target stay in
2823
- * sync by construction. Omitted = no override (the active tool's
2824
- * `Tool.cursor` shows). Affordance hits are resolved earlier in the
2825
- * pump via `AffordanceRegion.cursor` and never reach this field.
2826
- *
2827
- * Static string only. Prediction runs `enabled()` but cannot run the
2828
- * invoker, so an action that matches yet bails at `start()` (empty
2829
- * handle) may still show its cursor — keep `enabled` accurate for
2830
- * actions that declare one.
2831
- */
2832
- cursor?: string;
2833
- /**
2834
- * CSS cursor shown while THIS action's ongoing handle is in flight —
2835
- * grabbing while panning, `move` while dragging a selection, `crosshair`
2836
- * while pulling a marquee.
2837
- *
2838
- * Separate from `cursor` because the two answer different questions:
2839
- * `cursor` is a prediction ("a drag from here would pan"), this is a state
2840
- * ("you are panning"). An action can declare either, both, or neither;
2841
- * with only `cursor` set, the hover hint holds for the duration of the
2842
- * gesture.
2843
- *
2844
- * This is where mid-gesture cursors live now. They used to come from the
2845
- * tool side — `ViewportToolDef.engaged.cursor` for a phase-gated string,
2846
- * or a function-form `Tool.cursor` reading the gesture scratch out of the
2847
- * tool-routing dispatcher. Both belonged to a pipeline whose whole job was
2848
- * being taken over by bindings, and neither could describe a cursor for an
2849
- * action a tool doesn't own.
2850
- */
2851
- activeCursor?: string;
2852
132
  }
2853
- /**
2854
- * @experimental
2855
- * Closed enum of reasons an action might report itself as disabled. The
2856
- * consumer (palette, menu, etc.) maps these symbolic values to display
2857
- * strings via its own label map — see `demo/CommandPalette.tsx` for the
2858
- * canonical mapping.
2859
- */
2860
- declare const ActionDisabledReason: {
2861
- readonly SelectionRequired: "selection-required";
2862
- readonly SceneEmpty: "scene-empty";
2863
- readonly NotApplicable: "not-applicable";
2864
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
2865
- readonly PredicateThrew: "predicate-threw";
2866
- };
2867
- /** Why an action is unavailable right now. */
2868
- type ActionDisabledReason = (typeof ActionDisabledReason)[keyof typeof ActionDisabledReason];
133
+ /** Renders one glyph by name. The named components below are the usual way
134
+ * in; reach for this when the glyph is chosen at runtime. */
135
+ declare function Icon({ name, className, size, label }: IconProps & {
136
+ name: IconName;
137
+ }): react_jsx_runtime.JSX.Element;
2869
138
 
2870
- /** The tool registry's runtime surface: which tool is active, which is
2871
- * temporarily held by a hotkey, and how to change either. */
2872
- interface ToolsApi {
2873
- /** Current active-slot tool id. */
2874
- active: string;
2875
- /** Set the active-slot tool. The gesture dispatcher watches the active
2876
- * tool and cancels any in-flight handle itself. */
2877
- setActive: (id: string) => void;
2878
- /** Currently hotkey-engaged tool id (or `null`). Derived as the top of
2879
- * the hotkey stack for backwards compat with the pre-stack API. */
2880
- hotkeyEngaged: string | null;
2881
- /** Engage a hotkey-slot tool by id. */
2882
- engageHotkey: (id: string) => void;
2883
- /** Disengage the hotkey-slot tool, if any. */
2884
- disengageHotkey: () => void;
2885
- /** All always-on tools, in registration order. */
2886
- ambient: readonly AnyTool[];
2887
- /** Full registry — for userland UI (palette buttons, etc.). */
2888
- registry: Readonly<Record<string, AnyTool>>;
2889
- /** Returns true if a tool with the given id is in the registry or ambient list. */
2890
- has(id: string): boolean;
2891
- /** All overlay layers from currently-engaged tools (active slot, hotkey
2892
- * slot if engaged, all ambient slot tools) that declare `position`.
2893
- * Filters out tools with no `overlay` field. Order: active, then hotkey
2894
- * (if engaged), then ambient (registration order). */
2895
- getActiveOverlays(position?: OverlayPosition): RenderLayer<unknown>[];
139
+ /** Holds the set of available modes and which one is active, and notifies
140
+ * subscribers when that changes. `getVersion` is a monotonic counter for
141
+ * render-cache invalidation. Unknown mode ids throw rather than being
142
+ * ignored. */
143
+ interface ModeRegistry {
144
+ current(): ModeDefinition;
145
+ setMode(id: string): void;
146
+ byId(id: string): ModeDefinition;
147
+ getVersion(): number;
148
+ subscribe(listener: () => void): () => void;
2896
149
  }
2897
150
 
2898
151
  /** Props for {@link ActionBar}. */
@@ -3991,6 +1244,9 @@ type SliderProps<T extends Thumb = Thumb> = {
3991
1244
  allowShiftAll?: boolean;
3992
1245
  renderTrack?: (ctx: TrackCtx) => ReactNode;
3993
1246
  trackHeight?: number;
1247
+ /** `'slim'` drives the track and thumb from the kit's slider tokens, so a
1248
+ * Slider matches the property rows. `trackHeight` still wins if given. */
1249
+ density?: 'default' | 'slim';
3994
1250
  renderReadout?: (thumb: T, index: number) => ReactNode;
3995
1251
  readoutPlacement?: 'none' | 'inline-after' | 'below-thumb';
3996
1252
  ariaLabel?: string;
@@ -4430,7 +1686,7 @@ type InputProps = Omit<TextFieldProps, 'children' | 'className'> & {
4430
1686
  *
4431
1687
  * `ref` forwards to the underlying `<input>`.
4432
1688
  */
4433
- declare const Input: react.ForwardRefExoticComponent<Omit<TextFieldProps, "children" | "className"> & {
1689
+ declare const Input: react.ForwardRefExoticComponent<Omit<TextFieldProps, "className" | "children"> & {
4434
1690
  label?: ReactNode;
4435
1691
  description?: ReactNode;
4436
1692
  errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
@@ -4452,7 +1708,7 @@ type CheckboxProps = Omit<CheckboxProps$1, 'children' | 'className'> & {
4452
1708
  * Single checkbox wrapping React Aria's Checkbox. Supports indeterminate
4453
1709
  * via `isIndeterminate`. The label is supplied as children.
4454
1710
  */
4455
- declare const Checkbox: react.ForwardRefExoticComponent<Omit<CheckboxProps$1, "children" | "className"> & {
1711
+ declare const Checkbox: react.ForwardRefExoticComponent<Omit<CheckboxProps$1, "className" | "children"> & {
4456
1712
  children?: ReactNode;
4457
1713
  className?: string;
4458
1714
  } & react.RefAttributes<HTMLLabelElement>>;
@@ -4472,7 +1728,7 @@ type SwitchProps = Omit<SwitchProps$1, 'children' | 'className'> & {
4472
1728
  *
4473
1729
  * `ref` forwards to the underlying label element.
4474
1730
  */
4475
- declare const Switch: react.ForwardRefExoticComponent<Omit<SwitchProps$1, "children" | "className"> & {
1731
+ declare const Switch: react.ForwardRefExoticComponent<Omit<SwitchProps$1, "className" | "children"> & {
4476
1732
  children?: ReactNode;
4477
1733
  className?: string;
4478
1734
  } & react.RefAttributes<HTMLLabelElement>>;
@@ -4537,6 +1793,9 @@ type NumberFieldProps = Omit<NumberFieldProps$1, 'children' | 'className'> & {
4537
1793
  errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
4538
1794
  /** Hide the up/down stepper buttons. Defaults to false. */
4539
1795
  hideSteppers?: boolean;
1796
+ /** Render with no box until focused — the readout treatment the property
1797
+ * rows use, for a value that sits inside other chrome rather than in a form. */
1798
+ ghost?: boolean;
4540
1799
  /** Native input placeholder — e.g. `'Mixed'` for a multi-selection
4541
1800
  * editor with no shared value. */
4542
1801
  placeholder?: string;
@@ -4550,12 +1809,15 @@ type NumberFieldProps = Omit<NumberFieldProps$1, 'children' | 'className'> & {
4550
1809
  *
4551
1810
  * `ref` forwards to the underlying `<input>`.
4552
1811
  */
4553
- declare const NumberField: react.ForwardRefExoticComponent<Omit<NumberFieldProps$1, "children" | "className"> & {
1812
+ declare const NumberField: react.ForwardRefExoticComponent<Omit<NumberFieldProps$1, "className" | "children"> & {
4554
1813
  label?: ReactNode;
4555
1814
  description?: ReactNode;
4556
1815
  errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);
4557
1816
  /** Hide the up/down stepper buttons. Defaults to false. */
4558
1817
  hideSteppers?: boolean;
1818
+ /** Render with no box until focused — the readout treatment the property
1819
+ * rows use, for a value that sits inside other chrome rather than in a form. */
1820
+ ghost?: boolean;
4559
1821
  /** Native input placeholder — e.g. `'Mixed'` for a multi-selection
4560
1822
  * editor with no shared value. */
4561
1823
  placeholder?: string;
@@ -4814,7 +2076,7 @@ interface Plot2DProps {
4814
2076
  style?: CSSProperties;
4815
2077
  /** Pointer down on the SVG. Receives both plot- and model-space coords
4816
2078
  * pre-computed so consumers don't repeat the rect/transform dance. */
4817
- onPointerDown?: (e: PointerEvent$1<SVGSVGElement>, coords: Plot2DCoords) => void;
2079
+ onPointerDown?: (e: PointerEvent<SVGSVGElement>, coords: Plot2DCoords) => void;
4818
2080
  onKeyDown?: (e: KeyboardEvent<SVGSVGElement>) => void;
4819
2081
  children?: ReactNode;
4820
2082
  }
@@ -5079,9 +2341,20 @@ interface UseReorderDragListOptions {
5079
2341
  items: LayerListItem[];
5080
2342
  selectedIds: string[];
5081
2343
  onReorder(ids: string[], targetIndex: number): void;
2344
+ /** A press that was released without ever engaging a drag — the click a
2345
+ * list row means by it. Fires for locked rows too, which can be selected
2346
+ * but not dragged. Modifiers are read at press, not at release. */
2347
+ onPress?(id: string, mods: PressModifiers): void;
5082
2348
  /** Pointer-move distance (px) before pending drag engages. Default 4. */
5083
2349
  threshold?: number;
5084
2350
  }
2351
+ /** Modifier keys held when a press began. */
2352
+ interface PressModifiers {
2353
+ shiftKey: boolean;
2354
+ ctrlKey: boolean;
2355
+ metaKey: boolean;
2356
+ altKey: boolean;
2357
+ }
5085
2358
  /**
5086
2359
  * Live drag state for rendering feedback: which ids are being dragged and the
5087
2360
  * insertion index the drop would use. Both `null` when no drag is engaged.
@@ -5091,18 +2364,17 @@ interface ReorderDragState {
5091
2364
  targetIndex: number | null;
5092
2365
  }
5093
2366
  /**
5094
- * Props to spread onto the list container and each row, plus the live
5095
- * {@link ReorderDragState}.
2367
+ * A `ref` for the list container, an `onPointerDown` for each row, and the
2368
+ * live {@link ReorderDragState}. The container ref is required, not optional
2369
+ * decoration: it is what the drop index is measured against and what the
2370
+ * pointer session is opened on.
5096
2371
  */
5097
2372
  interface ReorderDragHandlers {
5098
2373
  rowProps(id: string, index: number): {
5099
- onPointerDown(e: PointerEvent$1): void;
2374
+ onPointerDown(e: PointerEvent): void;
5100
2375
  };
5101
2376
  containerProps: {
5102
2377
  ref: RefCallback<HTMLElement>;
5103
- onPointerMove(e: PointerEvent$1): void;
5104
- onPointerUp(e: PointerEvent$1): void;
5105
- onPointerCancel(e: PointerEvent$1): void;
5106
2378
  };
5107
2379
  state: ReorderDragState;
5108
2380
  }
@@ -5113,8 +2385,11 @@ interface ReorderDragHandlers {
5113
2385
  * drop. A drop that would leave a contiguous block where it already is does
5114
2386
  * not call `onReorder`.
5115
2387
  *
5116
- * The pointer is captured on the row, so a drag that leaves the list still
5117
- * tracks and still releases cleanly.
2388
+ * A press opens an `openPointerSession` on the *container*, which owns the
2389
+ * rest of the gesture: a drag that leaves the list still tracks, a release
2390
+ * anywhere still drops, and a release the window never delivered still ends
2391
+ * the drag. The container is the origin rather than the row because rows come
2392
+ * and go as the list re-renders, and a drag must outlive the row it grabbed.
5118
2393
  */
5119
2394
  declare function useReorderDragList(opts: UseReorderDragListOptions): ReorderDragHandlers;
5120
2395
 
@@ -5191,5 +2466,5 @@ declare const MINUS_SIGN = "\u2212";
5191
2466
  */
5192
2467
  declare function formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
5193
2468
 
5194
- export { ActionBar, ActionsBar, Badge, Button, Callout, Checkbox, ComboBox, ComboBoxItem, CurveEditor, DataGrid, Dialog, DragHandleGlyph, EDGE_PROFILES, Field, Input, KeyCap, KeySequence, MINUS_SIGN, NumberField, OptionsBar, Plot2D, PointPlotter, Powerline, ToolPrefGroup as PrefGroup, ToolPrefLeaf as PrefLeaf, Radio, RadioGroup, RangeSlider, Select, SelectItem, Sidebar, SidebarPanel, Slider, Switch, Tab, TabList, TabPanel, Tabs, ToggleBar, ToolButton, ToolGroup, ToolPalette, chromaAt, detectPlatform, dlog, fieldClasses, formatNumber, formatShortcut, formatShortcutParts, inferKeycapKind, isDebugEnabled, isPrefLeaf, keyGlyph, keySpecFromKey, keySpecsFromMods, oklchToHex, paintGradientTrack, prefValueAtPath, useReorderDragList, useRovingTabIndex, visiblePrefSubtree };
5195
- export type { ActionBarProps, ActionsBarItem, ActionsBarProps, ActionsBarSize, ActionsBarVariant, AddPointMode, AnchorRenderProps, AxesSettings, BadgeProps, BadgeShape, BadgeSize, BadgeTone, BadgeVariant, BoundsCtx, BuiltInEdgeName, ButtonProps, ButtonSize, ButtonVariant, CalloutProps, CheckboxProps, ChromaCurve, ChromaCurvePoint, ComboBoxItemProps, ComboBoxOption, ComboBoxProps, ControlPoint, CurveDomain, CurveEditorProps, DataGridColumn, DataGridProps, DialogProps, DragHandleGlyphProps, EdgeCap, EdgeProfile, EndpointMode, FieldOrientation, FieldProps, FillSettings, GradientTrackOpts, GridSettings, InputProps, InterpolationMode, KeyCapProps, KeyCapVariant, KeySequenceProps, KeySpec, KeycapKind, LayerListItem, LogicalMod, LogicalModSpec, NumberFieldProps, OptionsBarItem, OptionsBarProps, OptionsBarSize, OptionsBarVariant, Platform, Plot2DCoords, Plot2DHandle, Plot2DProps, PointPlotterProps, PowerlineProps, PowerlineSegment, RadioGroupProps, RadioProps, RangeSliderProps, ReorderDragHandlers, ReorderDragState, RovingItem, RovingTabIndex, SelectItemProps, SelectOption, SelectProps, SidebarPanelProps, SidebarProps, SliderProps, SwitchProps, TabListProps, TabPanelProps, TabProps, TabsProps, Thumb, ThumbRenderCtx, ThumbShape, ToggleBarItem, ToggleBarProps, ToggleBarSize, ToggleBarVariant, ToolButtonProps, ToolGroupProps, ToolPaletteProps, TrackCtx, UseReorderDragListOptions, UseRovingTabIndexOptions };
2469
+ export { ActionBar, ActionsBar, Badge, Button, Callout, Checkbox, ComboBox, ComboBoxItem, CurveEditor, DataGrid, Dialog, DragHandleGlyph, EDGE_PROFILES, Field, ICON_PATHS, Icon, Input, KeyCap, KeySequence, MINUS_SIGN, NumberField, OptionsBar, Plot2D, PointPlotter, Powerline, ToolPrefGroup as PrefGroup, ToolPrefLeaf as PrefLeaf, Radio, RadioGroup, RangeSlider, Select, SelectItem, Sidebar, SidebarPanel, Slider, Switch, Tab, TabList, TabPanel, Tabs, ToggleBar, ToolButton, ToolGroup, ToolPalette, chromaAt, detectPlatform, dlog, fieldClasses, formatNumber, formatShortcut, formatShortcutParts, inferKeycapKind, isDebugEnabled, isPrefLeaf, keyGlyph, keySpecFromKey, keySpecsFromMods, oklchToHex, paintGradientTrack, prefValueAtPath, useReorderDragList, useRovingTabIndex, visiblePrefSubtree };
2470
+ export type { ActionBarProps, ActionsBarItem, ActionsBarProps, ActionsBarSize, ActionsBarVariant, AddPointMode, AnchorRenderProps, AxesSettings, BadgeProps, BadgeShape, BadgeSize, BadgeTone, BadgeVariant, BoundsCtx, BuiltInEdgeName, ButtonProps, ButtonSize, ButtonVariant, CalloutProps, CheckboxProps, ChromaCurve, ChromaCurvePoint, ComboBoxItemProps, ComboBoxOption, ComboBoxProps, ControlPoint, CurveDomain, CurveEditorProps, DataGridColumn, DataGridProps, DialogProps, DragHandleGlyphProps, EdgeCap, EdgeProfile, EndpointMode, FieldOrientation, FieldProps, FillSettings, GradientTrackOpts, GridSettings, IconName, IconProps, InputProps, InterpolationMode, KeyCapProps, KeyCapVariant, KeySequenceProps, KeySpec, KeycapKind, LayerListItem, LogicalMod, LogicalModSpec, NumberFieldProps, OptionsBarItem, OptionsBarProps, OptionsBarSize, OptionsBarVariant, Platform, Plot2DCoords, Plot2DHandle, Plot2DProps, PointPlotterProps, PowerlineProps, PowerlineSegment, RadioGroupProps, RadioProps, RangeSliderProps, ReorderDragHandlers, ReorderDragState, RovingItem, RovingTabIndex, SelectItemProps, SelectOption, SelectProps, SidebarPanelProps, SidebarProps, SliderProps, SwitchProps, TabListProps, TabPanelProps, TabProps, TabsProps, Thumb, ThumbRenderCtx, ThumbShape, ToggleBarItem, ToggleBarProps, ToggleBarSize, ToggleBarVariant, ToolButtonProps, ToolGroupProps, ToolPaletteProps, TrackCtx, UseReorderDragListOptions, UseRovingTabIndexOptions };