@weasel-js/labkit 1.4.0 → 1.4.2

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