@weasel-js/labkit 0.7.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -3
  3. package/dist/_dts/{DrawCommand-CEvS5AdV.d.ts → DrawCommand-BwRCZYuo.d.ts} +106 -52
  4. package/dist/_dts/{useExperimentState-eSvpwV_P.d.ts → useExperimentState-CJn2hHzd.d.ts} +6 -4
  5. package/dist/{chunk-SSMHPMDV.js → chunk-53XSBIUK.js} +12 -11
  6. package/dist/chunk-53XSBIUK.js.map +1 -0
  7. package/dist/{chunk-BG6NLMOL.js → chunk-FP5LYDVX.js} +1229 -1227
  8. package/dist/chunk-FP5LYDVX.js.map +1 -0
  9. package/dist/{chunk-ERZQAU4N.js → chunk-HNXVLKBG.js} +3 -3
  10. package/dist/{chunk-ERZQAU4N.js.map → chunk-HNXVLKBG.js.map} +1 -1
  11. package/dist/{chunk-5XDFILHZ.js → chunk-XVBXYKFJ.js} +1675 -670
  12. package/dist/chunk-XVBXYKFJ.js.map +1 -0
  13. package/dist/fonts/inter-latin.woff2 +0 -0
  14. package/dist/fonts/oswald-latin-variable.woff2 +0 -0
  15. package/dist/index.d.ts +37 -13
  16. package/dist/index.js +776 -27
  17. package/dist/index.js.map +1 -1
  18. package/dist/passthrough/weasel-canvas.d.ts +1 -1
  19. package/dist/passthrough/weasel-canvas.js +1 -1
  20. package/dist/passthrough/weasel-ui.d.ts +332 -189
  21. package/dist/passthrough/weasel-ui.js +2 -2
  22. package/dist/state/index.d.ts +2 -2
  23. package/dist/state/index.js +2 -2
  24. package/dist/styles.css +229 -288
  25. package/dist/ui/layers/index.js +3 -3
  26. package/package.json +5 -6
  27. package/src/canvas/CanvasStack.less +1 -1
  28. package/src/controls/ControlPanel.less +12 -12
  29. package/src/dragdrop/DragGhost.less +2 -2
  30. package/src/dragdrop/Palette.less +6 -6
  31. package/src/index.ts +2 -0
  32. package/src/lab/Lab.test.tsx +20 -15
  33. package/src/lab/Lab.tsx +33 -32
  34. package/src/lab/LabContext.ts +3 -3
  35. package/src/lab/LabShell.less +14 -14
  36. package/src/lab/LabShell.test.tsx +7 -20
  37. package/src/lab/LabShell.tsx +22 -13
  38. package/src/lab/WorkspaceGrid.less +1 -1
  39. package/src/lab/WorkspaceGrid.stories.tsx +4 -4
  40. package/src/lab/index.ts +1 -1
  41. package/src/lab/useSystemMode.ts +30 -0
  42. package/src/layers/LayerList.less +9 -9
  43. package/src/primitives/FpsMeter.less +5 -5
  44. package/src/primitives/ScaleIndicator.less +7 -7
  45. package/src/primitives/Sidebar.less +12 -12
  46. package/src/primitives/StatusBar.less +10 -10
  47. package/src/primitives/Toolbar.less +16 -16
  48. package/src/state/store.test.ts +17 -9
  49. package/src/state/store.ts +13 -10
  50. package/src/state/types.ts +5 -2
  51. package/src/theme/Interstellar.stories.less +20 -20
  52. package/src/theme/Interstellar.stories.tsx +44 -59
  53. package/src/theme/base.less +29 -28
  54. package/src/theme/interstellar.test.ts +30 -0
  55. package/src/theme/interstellar.tokens.json +57 -0
  56. package/src/theme/interstellar.ts +9 -0
  57. package/src/ui/layers/LayerStack.less +21 -21
  58. package/src/ui/properties/CurveField.less +5 -5
  59. package/src/ui/properties/EffectCard.tsx +2 -2
  60. package/src/ui/properties/PropertyGroup.less +6 -6
  61. package/src/ui/properties/PropertyPanel.less +70 -70
  62. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +1 -1
  63. package/src/ui/properties/storyLayouts.tsx +1 -1
  64. package/src/workspace/Workspace.less +14 -14
  65. package/src/workspace/Workspace.stories.tsx +2 -2
  66. package/src/workspace/Workspace.test.tsx +2 -2
  67. package/dist/chunk-5XDFILHZ.js.map +0 -1
  68. package/dist/chunk-BG6NLMOL.js.map +0 -1
  69. package/dist/chunk-SSMHPMDV.js.map +0 -1
  70. package/dist/theme-interstellar.css +0 -25
  71. package/dist/theme-light.css +0 -13
  72. package/src/fonts/oswald-latin-variable.woff2 +0 -0
  73. package/src/theme/interstellar.less +0 -34
  74. package/src/theme/light.less +0 -14
  75. package/src/theme/tokens.less +0 -85
@@ -1,6 +1,6 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { Ref } from 'react';
3
- import { N as Node, V as View, D as DrawCommand, S as Scene, B as Bounds, a as ViewportDims, P as PathDrawCommand } from '../_dts/DrawCommand-CEvS5AdV.js';
3
+ import { N as Node, V as View, D as DrawCommand, S as Scene, B as Bounds, a as ViewportDims, P as PathDrawCommand } from '../_dts/DrawCommand-BwRCZYuo.js';
4
4
 
5
5
  /**
6
6
  * `renderSceneToCanvas` — substrate for detached, read-only scene views
@@ -1,3 +1,3 @@
1
- export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView2 as computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-5XDFILHZ.js';
1
+ export { FALLBACK_FIT_VIEW, MinimapCanvas, SceneViewCanvas, buildSceneViewCommands, computeFitView2 as computeFitView, computeIndicatorCommand, renderSceneToCanvas } from '../chunk-XVBXYKFJ.js';
2
2
  //# sourceMappingURL=weasel-canvas.js.map
3
3
  //# sourceMappingURL=weasel-canvas.js.map
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, CSSProperties, ButtonHTMLAttributes, ReactElement, MutableRefObject, PointerEvent as PointerEvent$1, KeyboardEvent, RefCallback } from 'react';
4
- import { O as Op, V as View, D as DrawCommand, b as NodeId, c as Path, S as Scene, H as History, d as PoseComposition, B as Bounds } from '../_dts/DrawCommand-CEvS5AdV.js';
4
+ import { V as View, D as DrawCommand, O as Op, b as NodeId, c as Path, S as Scene, H as History, B as Bounds } from '../_dts/DrawCommand-BwRCZYuo.js';
5
5
  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 } from 'react-aria-components';
6
6
 
7
7
  /**
@@ -1124,6 +1124,181 @@ interface PowerlineProps {
1124
1124
  }
1125
1125
  declare function Powerline({ segments, startCap, size, variant, depth, gap, className, ...rest }: PowerlineProps): react_jsx_runtime.JSX.Element;
1126
1126
 
1127
+ /**
1128
+ * Snapshot of modifier-key state at gesture dispatch.
1129
+ *
1130
+ * Lives in core rather than beside the gesture types that produce it because
1131
+ * `core/selection/chromeState.ts` reads it, and core may not import from
1132
+ * `interactions/`. Re-exported from `interactions/gestures/types.ts`, which
1133
+ * is still where gesture code names it.
1134
+ */
1135
+ interface ModifierState {
1136
+ alt: boolean;
1137
+ shift: boolean;
1138
+ meta: boolean;
1139
+ ctrl: boolean;
1140
+ }
1141
+
1142
+ /**
1143
+ * @experimental
1144
+ * Result of an affordance hit — what the region computed about itself.
1145
+ *
1146
+ * `initialScratch` is the payload: what the region already knows (which
1147
+ * corner, which target id) so the action that picks up the drag doesn't
1148
+ * re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
1149
+ * it into `AffordanceHit`, which flows to the matching action through
1150
+ * `InvocationCtx.drag.affordance`.
1151
+ *
1152
+ * This used to also carry a `drag: DragChannel` naming the handlers the
1153
+ * tool-routing dispatcher should wire up. Every implementation supplied a
1154
+ * no-op stub that claimed, because the real routing had already moved to
1155
+ * bindings; the field went with that dispatcher.
1156
+ */
1157
+ interface AffordanceBinding<TScratch = unknown> {
1158
+ initialScratch?: TScratch;
1159
+ }
1160
+ /**
1161
+ * What a **registered layer's** `hitTest` returns. Extends `AffordanceBinding`
1162
+ * so existing implementations keep typechecking; the added fields are how a
1163
+ * consumer's own chrome says the things kit chrome says through
1164
+ * `AffordanceRegion` — which cursor to show, and whether it owns the point
1165
+ * outright.
1166
+ */
1167
+ interface LayerHit<TScratch = unknown> extends AffordanceBinding<TScratch> {
1168
+ /** CSS cursor while the pointer is over this hit. Reaches the hover-cursor
1169
+ * pump as `AffordanceHit.cursor`, the same path kit chrome uses. */
1170
+ cursor?: string;
1171
+ /** `'exclusive'` bars every binding whose target doesn't consult the
1172
+ * affordance. Omitted means `'shared'` — today's behavior. Same name and
1173
+ * meaning as `AffordanceHit.strength`, which it becomes. */
1174
+ strength?: 'exclusive' | 'shared';
1175
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
1176
+ claimedKinds?: readonly ClaimableGesture[];
1177
+ }
1178
+ /**
1179
+ * Gesture kinds an affordance claim can bar, in the spec vocabulary bindings
1180
+ * are written in. `'pointer'` is one token because `pointerDown` / `click` /
1181
+ * `drag` are a single press protocol — at the event level the first two are
1182
+ * the same `kind: 'pointerdown'`, told apart only by `stage`.
1183
+ */
1184
+ type ClaimableGesture = 'pointer' | 'doubleClick' | 'contextMenu' | 'longPress' | 'wheel';
1185
+
1186
+ /**
1187
+ * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
1188
+ * canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
1189
+ * supplies it explicitly so layers don't have to know about DPR.
1190
+ */
1191
+ interface Dims {
1192
+ width: number;
1193
+ height: number;
1194
+ }
1195
+ /**
1196
+ * A single named render sub-layer within a canvas renderer.
1197
+ *
1198
+ * @template TData - The data object passed to each draw call.
1199
+ */
1200
+ interface RenderLayer<TData> {
1201
+ /** Unique identifier used in visibility maps and ordering arrays. */
1202
+ id: string;
1203
+ /** Human-readable name for UI toggles. */
1204
+ label: string;
1205
+ /**
1206
+ * Emit a DrawCommand tree for the GL backend to dispatch.
1207
+ *
1208
+ * For world-space layers (the default), emit commands in WORLD COORDS —
1209
+ * `drawLayers` automatically wraps them in `{ kind: 'group', transform:
1210
+ * viewToMat3(view), ... }` before handing them to the renderer. Do NOT
1211
+ * apply the view transform yourself.
1212
+ *
1213
+ * For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
1214
+ * coords directly; `drawLayers` passes them through unchanged. If part
1215
+ * of a screen-space layer's output needs to track the view, wrap that
1216
+ * subset manually with `viewToMat3(view)`.
1217
+ */
1218
+ draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
1219
+ /**
1220
+ * Whether the layer is shown when no explicit visibility entry exists.
1221
+ * Defaults to `true` when absent.
1222
+ */
1223
+ defaultVisible?: boolean;
1224
+ /**
1225
+ * When true, the layer is always drawn regardless of the visibility map.
1226
+ * Useful for layers that must never be hidden (e.g. base grid).
1227
+ */
1228
+ alwaysOn?: boolean;
1229
+ /**
1230
+ * Coordinate space the layer draws in.
1231
+ *
1232
+ * - `'world'` (default): the layer's `draw` returns world-space commands;
1233
+ * `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
1234
+ * automatically.
1235
+ * - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
1236
+ * commands; `drawLayers` passes them through unchanged. World-anchored
1237
+ * chrome inside a screen-space layer must call `worldToScreen` or wrap
1238
+ * the relevant subset with `viewToMat3(view)` manually.
1239
+ */
1240
+ space?: 'world' | 'screen';
1241
+ /**
1242
+ * Optional hit-test for **consumer-attached** layers.
1243
+ *
1244
+ * Only layers registered through `CanvasExtensionApi.registerLayer` are
1245
+ * hit-tested: `hitTestExtras` walks them last-registered-first on
1246
+ * pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
1247
+ * thunk ahead of the kit's own selection chrome. First non-null result
1248
+ * wins; null means "I don't claim this hit, try the next layer."
1249
+ *
1250
+ * Layers that reach the draw stack some other way — a `Tool.overlay`, an
1251
+ * entry in the `layers` map — are painted but never hit-tested, so defining
1252
+ * `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
1253
+ * goes through `buildAffordanceAt`.)
1254
+ *
1255
+ * Coordinates are world-space. The `data` arg is the layer's
1256
+ * configured data slot (same as `draw`); `view` and `dims` mirror
1257
+ * `draw`'s arguments.
1258
+ */
1259
+ hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
1260
+ /** Chrome-caps visibility predicate. When supplied, the layer must
1261
+ * not return a hit from any chrome element whose id reports
1262
+ * `false`. Absent → every element is hittable. */
1263
+ isVisible?: (id: string) => boolean) => LayerHit | null;
1264
+ /**
1265
+ * Called on every pointermove when no gesture is currently captured.
1266
+ * Lets layers (e.g. HUD widgets) track hover state without participating
1267
+ * in the drag pipeline. Coords are world-space; the layer is responsible
1268
+ * for any further conversion (e.g. world→screen for screen-space layers)
1269
+ * and for its own throttling.
1270
+ */
1271
+ onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
1272
+ /**
1273
+ * Called when the cursor leaves the canvas element. Lets layers clear
1274
+ * any hover state they're holding.
1275
+ */
1276
+ onUncapturedLeave?: () => void;
1277
+ }
1278
+
1279
+ /**
1280
+ * Facts about the device the canvas is running on.
1281
+ *
1282
+ * One object, recomputed when the underlying media queries change, read by
1283
+ * two consumers: the chrome-caps rule layer (via `RuleCtx.device`) and the
1284
+ * handle-sizing constants (via `targetScale`).
1285
+ *
1286
+ * Deliberately NOT a form-factor concept. There is no `isPhone` here and
1287
+ * there should never be one: chrome layout is the consuming app's decision.
1288
+ * The kit's job is to stop assuming a mouse.
1289
+ */
1290
+ interface DeviceProfile {
1291
+ /** `matchMedia('(pointer: coarse)')` — the primary pointer is imprecise. */
1292
+ readonly coarsePointer: boolean;
1293
+ /** `matchMedia('(hover: hover)')` — the primary pointer can hover. */
1294
+ readonly canHover: boolean;
1295
+ /** Live device pixel ratio. */
1296
+ readonly dpr: number;
1297
+ /** Multiplier for handle sizes and hit radii. Derived from
1298
+ * `coarsePointer` unless explicitly overridden. */
1299
+ readonly targetScale: number;
1300
+ }
1301
+
1127
1302
  type ContainerBounds = {
1128
1303
  x: number;
1129
1304
  y: number;
@@ -1309,13 +1484,6 @@ interface InsertAdapter<TNode extends {
1309
1484
  getSelection(): string[];
1310
1485
  }
1311
1486
 
1312
- /** Snapshot of modifier-key state at gesture dispatch. */
1313
- interface ModifierState {
1314
- alt: boolean;
1315
- shift: boolean;
1316
- meta: boolean;
1317
- ctrl: boolean;
1318
- }
1319
1487
  /** Pointer position in both world and client coords. */
1320
1488
  interface PointerState {
1321
1489
  worldX: number;
@@ -1430,118 +1598,6 @@ interface PointSnapBehavior<TPose extends ResizePose> {
1430
1598
  onMove(ctx: PointSnapContext<TPose>): PointSnapResult | null | undefined;
1431
1599
  }
1432
1600
 
1433
- /**
1434
- * @experimental
1435
- * Result of an affordance hit — what the region computed about itself.
1436
- *
1437
- * `initialScratch` is the payload: what the region already knows (which
1438
- * corner, which target id) so the action that picks up the drag doesn't
1439
- * re-derive it. `<SceneCanvas>` reads it out of the layer hit-test and packs
1440
- * it into `AffordanceHit`, which flows to the matching action through
1441
- * `InvocationCtx.drag.affordance`.
1442
- *
1443
- * This used to also carry a `drag: DragChannel` naming the handlers the
1444
- * tool-routing dispatcher should wire up. Every implementation supplied a
1445
- * no-op stub that claimed, because the real routing had already moved to
1446
- * bindings; the field went with that dispatcher.
1447
- */
1448
- interface AffordanceBinding<TScratch = unknown> {
1449
- initialScratch?: TScratch;
1450
- }
1451
-
1452
- /**
1453
- * Canvas size in CSS pixels — passed to `draw` for layers that anchor to
1454
- * canvas edges (e.g. the debug overlay's layer-list panel). The GL backend
1455
- * supplies it explicitly so layers don't have to know about DPR.
1456
- */
1457
- interface Dims {
1458
- width: number;
1459
- height: number;
1460
- }
1461
- /**
1462
- * A single named render sub-layer within a canvas renderer.
1463
- *
1464
- * @template TData - The data object passed to each draw call.
1465
- */
1466
- interface RenderLayer<TData> {
1467
- /** Unique identifier used in visibility maps and ordering arrays. */
1468
- id: string;
1469
- /** Human-readable name for UI toggles. */
1470
- label: string;
1471
- /**
1472
- * Emit a DrawCommand tree for the GL backend to dispatch.
1473
- *
1474
- * For world-space layers (the default), emit commands in WORLD COORDS —
1475
- * `drawLayers` automatically wraps them in `{ kind: 'group', transform:
1476
- * viewToMat3(view), ... }` before handing them to the renderer. Do NOT
1477
- * apply the view transform yourself.
1478
- *
1479
- * For screen-space layers (`space: 'screen'`), emit commands in CSS-pixel
1480
- * coords directly; `drawLayers` passes them through unchanged. If part
1481
- * of a screen-space layer's output needs to track the view, wrap that
1482
- * subset manually with `viewToMat3(view)`.
1483
- */
1484
- draw: (data: TData, view: View, dims: Dims) => DrawCommand[];
1485
- /**
1486
- * Whether the layer is shown when no explicit visibility entry exists.
1487
- * Defaults to `true` when absent.
1488
- */
1489
- defaultVisible?: boolean;
1490
- /**
1491
- * When true, the layer is always drawn regardless of the visibility map.
1492
- * Useful for layers that must never be hidden (e.g. base grid).
1493
- */
1494
- alwaysOn?: boolean;
1495
- /**
1496
- * Coordinate space the layer draws in.
1497
- *
1498
- * - `'world'` (default): the layer's `draw` returns world-space commands;
1499
- * `drawLayers` wraps them in a `kind: 'group'` with `viewToMat3(view)`
1500
- * automatically.
1501
- * - `'screen'`: the layer's `draw` returns screen-space (CSS-pixel)
1502
- * commands; `drawLayers` passes them through unchanged. World-anchored
1503
- * chrome inside a screen-space layer must call `worldToScreen` or wrap
1504
- * the relevant subset with `viewToMat3(view)` manually.
1505
- */
1506
- space?: 'world' | 'screen';
1507
- /**
1508
- * Optional hit-test for **consumer-attached** layers.
1509
- *
1510
- * Only layers registered through `CanvasExtensionApi.registerLayer` are
1511
- * hit-tested: `hitTestExtras` walks them last-registered-first on
1512
- * pointerdown, and `<SceneCanvas>` folds the result into its `affordanceAt`
1513
- * thunk ahead of the kit's own selection chrome. First non-null result
1514
- * wins; null means "I don't claim this hit, try the next layer."
1515
- *
1516
- * Layers that reach the draw stack some other way — a `Tool.overlay`, an
1517
- * entry in the `layers` map — are painted but never hit-tested, so defining
1518
- * `hitTest` on one has no effect. (The kit's own chrome doesn't need it: it
1519
- * goes through `buildAffordanceAt`.)
1520
- *
1521
- * Coordinates are world-space. The `data` arg is the layer's
1522
- * configured data slot (same as `draw`); `view` and `dims` mirror
1523
- * `draw`'s arguments.
1524
- */
1525
- hitTest?: (worldX: number, worldY: number, data: TData, view: View, dims: Dims,
1526
- /** Chrome-caps visibility predicate. When supplied, the layer must
1527
- * not return a hit from any chrome element whose id reports
1528
- * `false`. Absent → every element is hittable. */
1529
- isVisible?: (id: string) => boolean) => AffordanceBinding | null;
1530
- /**
1531
- * Called on every pointermove when no gesture is currently captured.
1532
- * Lets layers (e.g. HUD widgets) track hover state without participating
1533
- * in the drag pipeline. Coords are world-space; the layer is responsible
1534
- * for any further conversion (e.g. world→screen for screen-space layers)
1535
- * and for its own throttling.
1536
- */
1537
- onUncapturedMove?: (worldX: number, worldY: number, evt: PointerEvent, view: View, dims: Dims) => void;
1538
- /**
1539
- * Called when the cursor leaves the canvas element. Lets layers clear
1540
- * any hover state they're holding.
1541
- */
1542
- onUncapturedLeave?: () => void;
1543
- }
1544
-
1545
1601
  /** The full vocabulary of capability tags shipped in the default preset.
1546
1602
  * Apps and other consumers can add their own tags; this list is what
1547
1603
  * `weasel-modes` itself uses. */
@@ -1625,6 +1681,13 @@ interface RuleCtx {
1625
1681
  * with nothing being edited should draw no anchors. Absent is treated
1626
1682
  * as false. */
1627
1683
  readonly editingAnchors?: boolean;
1684
+ /** Device facts — pointer coarseness, hover capability, density.
1685
+ *
1686
+ * Absent (legacy ctx builders) is treated as
1687
+ * {@link DEFAULT_DEVICE_PROFILE}: a fine pointer that can hover, at
1688
+ * density 1. That is what the kit assumed before this field existed, so
1689
+ * an absent profile is behavior-preserving by construction. */
1690
+ readonly device?: DeviceProfile;
1628
1691
  }
1629
1692
 
1630
1693
  /**
@@ -1661,6 +1724,12 @@ interface Selector {
1661
1724
  * (resizable), so `{ resizable: true }` passes for legacy ctx builders
1662
1725
  * that don't compute it. */
1663
1726
  resizable?: boolean;
1727
+ /** Matches `ctx.device.coarsePointer` — the primary pointer is imprecise
1728
+ * (touch, most styluses). Absent device is treated as `false`. */
1729
+ coarsePointer?: boolean;
1730
+ /** Matches `ctx.device.canHover` — the primary pointer can hover. Absent
1731
+ * device is treated as `true`. */
1732
+ canHover?: boolean;
1664
1733
  }
1665
1734
  /**
1666
1735
  * Composable visibility/eligibility rule. Trees of `all`/`any`/`not` nodes
@@ -1836,6 +1905,7 @@ interface KeyHeldSpec {
1836
1905
  interface WheelSpec {
1837
1906
  kind: 'wheel';
1838
1907
  direction?: 'up' | 'down' | '*';
1908
+ target?: TargetSpec;
1839
1909
  mods?: ModSpec;
1840
1910
  phase?: PhaseSpec;
1841
1911
  }
@@ -1893,6 +1963,24 @@ interface PointerDownSpec {
1893
1963
  mods?: ModSpec;
1894
1964
  phase?: PhaseSpec;
1895
1965
  }
1966
+ /**
1967
+ * Press held past the long-press threshold without crossing the drag
1968
+ * threshold. Synthesized by `useGestureDispatcher` from the pointer stream.
1969
+ *
1970
+ * Fires for `touch` and `pen` pointers only. A mouse held still for half a
1971
+ * second is an ordinary slow click, and firing on it would produce a context
1972
+ * menu nobody asked for.
1973
+ *
1974
+ * When a long-press matches no binding, the dispatcher re-dispatches it as a
1975
+ * `contextmenu` event — so `contextMenu` bindings work under a finger with no
1976
+ * consumer changes, while `longPress` stays independently bindable.
1977
+ */
1978
+ interface LongPressSpec {
1979
+ kind: 'longPress';
1980
+ target?: TargetSpec;
1981
+ mods?: ModSpec;
1982
+ phase?: PhaseSpec;
1983
+ }
1896
1984
  /** Multi-touch gesture. `fingers` is the required touch count. */
1897
1985
  interface MultiTouchSpec {
1898
1986
  kind: 'multiTouch';
@@ -1927,9 +2015,9 @@ interface PasteSpec {
1927
2015
  phase?: PhaseSpec;
1928
2016
  }
1929
2017
  /** The full union of supported gesture spec kinds. New invocation forms
1930
- * (long-press, two-stage, modal-dialog) extend this union without touching
2018
+ * (two-stage, modal-dialog) extend this union without touching
1931
2019
  * the `Action` type. */
1932
- type GestureSpec = KeySpec | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | PointerDownSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
2020
+ type GestureSpec = KeySpec | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | PointerDownSpec | LongPressSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
1933
2021
 
1934
2022
  /** A 2D point in either world or screen coordinates. */
1935
2023
  interface Point2 {
@@ -1960,6 +2048,15 @@ interface Point2 {
1960
2048
  interface AffordanceHit {
1961
2049
  /** Discriminator string, e.g. `'handle:bottom-right'`. */
1962
2050
  kind: string;
2051
+ /** Id of whatever produced this hit — a kit affordance's `id`, or the
2052
+ * registered layer's id. Read only by the dispatcher's dead-claim warning today. */
2053
+ owner?: string;
2054
+ /** `'exclusive'` means no binding may act on this point unless its target
2055
+ * consults the affordance. `'shared'` (the default) competes on scope and
2056
+ * specificity as bindings always have. */
2057
+ strength?: 'exclusive' | 'shared';
2058
+ /** Which gestures an exclusive claim bars. Omitted bars all of them. */
2059
+ claimedKinds?: readonly ClaimableGesture[];
1963
2060
  /** World-space fixed/pivot point. For resize: opposite corner. For rotate: pivot. */
1964
2061
  fixedPoint?: {
1965
2062
  x: number;
@@ -2287,6 +2384,50 @@ interface OngoingInvoker {
2287
2384
  * the `Action` type. */
2288
2385
  type Invoker = ImmediateInvoker | OngoingInvoker;
2289
2386
 
2387
+ /**
2388
+ * GestureBinding — connects a GestureSpec to an Action id (with per-binding
2389
+ * options). Tools own arrays of these on their `bindings` field; ambient
2390
+ * gesture-bindings are registered globally.
2391
+ *
2392
+ * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
2393
+ */
2394
+
2395
+ interface GestureBinding {
2396
+ spec: GestureSpec;
2397
+ actionId: string;
2398
+ opts?: BindingOpts;
2399
+ }
2400
+
2401
+ /**
2402
+ * Pose composition for hierarchical scene graphs.
2403
+ *
2404
+ * As of the nesting change, `getPose(id)` on adapters returns the
2405
+ * **local** pose — relative to the object's direct parent. Anything in the
2406
+ * kit that needs to draw, hit-test, snap, or otherwise reason about world
2407
+ * coordinates routes through `composeWorldPose`, which walks the parent
2408
+ * chain and folds local poses together via a consumer-supplied `compose`.
2409
+ *
2410
+ * Pose shape is generic, so the compose strategy is too. For the common
2411
+ * `{x, y, width, height}` axis-aligned rect, use `composeRectPose` —
2412
+ * translation only, child dimensions preserved. Custom pose shapes (paths,
2413
+ * matrix transforms) supply their own.
2414
+ *
2415
+ * The inverse — `rebaseLocalPose` — converts a world-space pose into a
2416
+ * local pose under a target parent. Used when reparenting so the visual
2417
+ * world position of a child is preserved across the parent change.
2418
+ */
2419
+ /** Re-exported; the declaration lives in `core/scene/types.ts`, which names
2420
+ * it and may not import from features. */
2421
+
2422
+ /** Consumer's pose-composition strategy for hierarchical scenes. `compose`
2423
+ * folds a child's pose (in parent's frame) up to the next frame; `decompose`
2424
+ * is its inverse. Default is IDENTITY — an absolute-pose scene where every
2425
+ * node already stores world coords (parent is grouping-only, no transform). */
2426
+ interface PoseComposition<TPose> {
2427
+ compose: (parent: TPose, child: TPose) => TPose;
2428
+ decompose: (parent: TPose, world: TPose) => TPose;
2429
+ }
2430
+
2290
2431
  /** Boolean op identifiers — five Pathfinder primaries plus Crop. */
2291
2432
  type BooleanOp = 'union' | 'intersect' | 'subtract' | 'exclude' | 'divide' | 'crop';
2292
2433
  /**
@@ -2576,6 +2717,20 @@ interface SliceDep {
2576
2717
  commit(a: Point2, b: Point2): void;
2577
2718
  }
2578
2719
 
2720
+ /**
2721
+ * Clipboard dep — the imperative surface `useClipboardOps` returns.
2722
+ *
2723
+ * Consumers publish their live clipboard through `useDepSource('clipboard',
2724
+ * …)` from inside the `<DepRegistryProvider>` (i.e. under `<SceneCanvas>`).
2725
+ * The kit deliberately does not build one for them: `useClipboardOps` needs
2726
+ * an adapter and a selection reader that only the consumer can supply.
2727
+ */
2728
+ interface ClipboardDep {
2729
+ copy(): void;
2730
+ paste(): void;
2731
+ isEmpty(): boolean;
2732
+ }
2733
+
2579
2734
  /**
2580
2735
  * Bridges arbitrary `TPose` shapes into the resize hook's bounds-driven math.
2581
2736
  * The hook reads bounds via `getBounds`, runs anchor-relative math on those
@@ -3147,6 +3302,15 @@ interface DepSchema {
3147
3302
  * Optional: when absent, `sliceAction` is a no-op.
3148
3303
  */
3149
3304
  slice?: SliceDep;
3305
+ /**
3306
+ * Clipboard dep — the imperative surface `useClipboardOps` returns.
3307
+ *
3308
+ * Published by the consumer (`useDepSource('clipboard', …)` from under
3309
+ * `<SceneCanvas>`), because `useClipboardOps` needs an adapter and a
3310
+ * selection reader only the consumer has. Feeds `clipboard.copy` /
3311
+ * `clipboard.cut`; both no-op when the dep is absent.
3312
+ */
3313
+ clipboard?: ClipboardDep;
3150
3314
  /**
3151
3315
  * Optional consumer commit hook. When present, `moveAction` (and other
3152
3316
  * default actions) submit their committed ops through it instead of
@@ -3196,20 +3360,6 @@ interface DepSchema {
3196
3360
  */
3197
3361
  type DepName = keyof DepSchema;
3198
3362
 
3199
- /**
3200
- * GestureBinding — connects a GestureSpec to an Action id (with per-binding
3201
- * options). Tools own arrays of these on their `bindings` field; ambient
3202
- * gesture-bindings are registered globally.
3203
- *
3204
- * See `docs/superpowers/specs/2026-05-16-registry-unification-design.md`.
3205
- */
3206
-
3207
- interface GestureBinding {
3208
- spec: GestureSpec;
3209
- actionId: string;
3210
- opts?: BindingOpts;
3211
- }
3212
-
3213
3363
  type HandleKind = 'corner' | 'rotation' | 'anchor';
3214
3364
  type HitShape = {
3215
3365
  kind: 'rect';
@@ -3254,6 +3404,38 @@ interface DebugSink {
3254
3404
  clearSnap(): void;
3255
3405
  }
3256
3406
 
3407
+ /**
3408
+ * When an entry's bindings are live. A set, not one value: the hand tool is
3409
+ * palette-selectable AND engaged by holding space, and both hold at once.
3410
+ */
3411
+ interface Eligibility {
3412
+ /** Selectable as the focused entry — exclusive, one at a time. */
3413
+ focus?: boolean;
3414
+ /** Also live while this key is held. */
3415
+ offhand?: HotkeyTrigger;
3416
+ /** Live regardless of what is focused. */
3417
+ always?: boolean;
3418
+ /** Live only for input this entry's own affordances produced. */
3419
+ claimed?: boolean;
3420
+ /** Modality filter, applied wherever it would otherwise be live. */
3421
+ capabilities?: CapabilityTag[];
3422
+ }
3423
+ /**
3424
+ * A registry entry: what it contributes, and when it is eligible. Every role
3425
+ * is optional and independent — an entry that only routes input declares only
3426
+ * `bindings` and `actions`.
3427
+ */
3428
+ interface Contribution {
3429
+ id: string;
3430
+ eligibility: Eligibility;
3431
+ bindings?: GestureBinding[];
3432
+ actions?: Action[];
3433
+ overlay?: RenderLayer<unknown>;
3434
+ presentation?: ToolPresentation;
3435
+ /** Reflection escape hatch — the authored form, when there was one. */
3436
+ def?: unknown;
3437
+ }
3438
+
3257
3439
  /** Modifier-key snapshot at event dispatch time. `space` is included
3258
3440
  * because tools commonly use space as a hotkey-slot trigger and may
3259
3441
  * also want to read it as a flag mid-gesture. */
@@ -3303,6 +3485,10 @@ interface ToolCtx<TScratch = unknown> {
3303
3485
  debug?: DebugSink;
3304
3486
  scratch: TScratch;
3305
3487
  }
3488
+ /** Hotkey-slot trigger key. The slot is engaged while this key is held —
3489
+ * hence "hotkey": active as long as the key is hot. `null` (or omitted)
3490
+ * means the tool is not eligible for the hotkey slot. */
3491
+ type HotkeyTrigger = 'space' | 'alt' | 'ctrl' | 'meta' | 'shift';
3306
3492
  /** World-space AABB shape used by `previewBounds`. Alias of the kit-wide
3307
3493
  * `Bounds` type — the optional `rotation` field carries through so a tool
3308
3494
  * can report an oriented preview rect (e.g. mid-rotate). */
@@ -3311,8 +3497,8 @@ type ToolBounds = Bounds;
3311
3497
  * tool — consumers that render a palette (`<ToolPalette>`) read these
3312
3498
  * fields to display the tool; consumers that don't can ignore them.
3313
3499
  *
3314
- * Note: cursor is NOT here. The top-level `Tool.cursor` field below is
3315
- * already plumbed through `<Canvas>` to `style.cursor` on the host. */
3500
+ * Note: cursor is NOT here. `Tool.cursor` (inherited from `Contribution`)
3501
+ * is already plumbed through `<Canvas>` to `style.cursor` on the host. */
3316
3502
  interface ToolPresentation<TScratch = unknown> {
3317
3503
  /** Human-readable label, distinct from the `id`. Falls back to `id`. */
3318
3504
  label?: string;
@@ -3327,34 +3513,13 @@ interface ToolPresentation<TScratch = unknown> {
3327
3513
  * derives one from `Tool.keybinding` via its own formatter. */
3328
3514
  shortcut?: string;
3329
3515
  }
3330
- /** Full Tool record. */
3331
- interface Tool<TScratch = unknown> {
3332
- id: string;
3333
- /**
3334
- * App-level capability tags for modality. The `weasel-modes` package's
3335
- * `eligibleForMode(mode, capabilities)` predicate consumes these to decide
3336
- * whether the tool is usable in the active mode. Tags are extensible
3337
- * strings — apps can define their own. Untagged tools are treated as
3338
- * ineligible by all modes except those whose `allows` list includes
3339
- * every implicit-or-declared tag (i.e. `normal` in the default preset).
3340
- */
3341
- capabilities?: CapabilityTag[];
3342
- /**
3343
- * Actions this tool owns and needs registered while it is in the tools
3344
- * registry — e.g. polygon's `polygon.adjustSides`, which its own bindings
3345
- * reference by id.
3346
- *
3347
- * Declared here rather than registered by the hook with `useAction`,
3348
- * because tool hooks run wherever the consumer calls them — for
3349
- * `<SceneCanvas>` that is ABOVE `<ActionsProviderIfRoot>`, where
3350
- * `useActionsRegistry()` returns null and `useAction` silently no-ops. The
3351
- * result was a binding pointing at an action id nothing had registered, so
3352
- * the gesture fell through to whatever matched next (polygon's
3353
- * wheel/arrow-key side adjustment did nothing and `nudge.*` moved the
3354
- * selection instead). `<ToolActionsMounter>` registers these from inside
3355
- * the provider.
3356
- */
3357
- actions?: Action[];
3516
+ /**
3517
+ * The focus-declaring case of a `Contribution`: a mode the user switches
3518
+ * into, plus the hooks that only make sense for one (`initScratch`,
3519
+ * activate/deactivate, live preview, `cursor`). Everything else — bindings,
3520
+ * actions, overlay, presentation is inherited.
3521
+ */
3522
+ interface Tool<TScratch = unknown> extends Contribution {
3358
3523
  /** Optional caller-supplied key. Most built-in tools have their activation
3359
3524
  * key declared in `BUILTIN_SELECT_KEYS` in `useKeybindings.ts`; this field
3360
3525
  * is for tools that want their activation key to be configurable by the
@@ -3364,11 +3529,9 @@ interface Tool<TScratch = unknown> {
3364
3529
  * the invoker knows which tool to switch to). */
3365
3530
  keybinding?: ToolKeybinding;
3366
3531
  initScratch?: () => TScratch;
3532
+ cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
3367
3533
  onActivate?: (ctx: ToolCtx<TScratch>) => void;
3368
3534
  onDeactivate?: (ctx: ToolCtx<TScratch>) => void;
3369
- cursor?: string | ((ctx: ToolCtx<TScratch>) => string);
3370
- /** Presentation metadata for tool palettes. See `ToolPresentation`. */
3371
- presentation?: ToolPresentation<TScratch>;
3372
3535
  /** Returns the in-flight preview pose for `id` if this tool is mid-gesture
3373
3536
  * on it; otherwise `null`. Lets `Canvas.helpersRef.getEffectivePose`
3374
3537
  * reflect live gesture state without reaching into hook internals. The
@@ -3386,26 +3549,6 @@ interface Tool<TScratch = unknown> {
3386
3549
  * consults this alongside `previewPose` to avoid double-rendering. Returns
3387
3550
  * `null` when no gesture is in flight. */
3388
3551
  previewIds?: () => Iterable<string> | null;
3389
- /** Optional overlay layer rendered on top of the scene/chrome whenever
3390
- * this tool is in any active slot (active, hotkey, or ambient).
3391
- * The layer's `draw` function reads from this tool's scratch via React
3392
- * closure (re-evaluated each render). Return early from `draw` to render
3393
- * nothing — typically gated on a scratch field like
3394
- * `if (!scratch.overlay) return`. */
3395
- overlay?: RenderLayer<unknown>;
3396
- /** Declarative gesture bindings — the tool's entire input surface. The
3397
- * gesture dispatcher consults these at active scope while this tool is
3398
- * active, and at hotkey scope while it is held. See
3399
- * `docs/superpowers/specs/2026-05-16-registry-unification-design.md`. */
3400
- bindings?: GestureBinding[];
3401
- /** Reflection escape hatch: when this `Tool` was produced by `defineTool`,
3402
- * the source `ToolDef` is attached here so introspection consumers
3403
- * (`buildRouteRegistry`, `findConflicts`, the toolkit-builder UI, the
3404
- * reflection demo) can read the authored form — `hookName` in particular,
3405
- * which the runtime `Tool` doesn't carry. Tools constructed without
3406
- * `defineTool` may leave this undefined. Typed as `unknown` to keep this
3407
- * file from importing the routing types — consumers cast at the use site. */
3408
- def?: unknown;
3409
3552
  }
3410
3553
  /** Internal alias for "a Tool of any scratch type" — used in registries and
3411
3554
  * dispatchers that hold tools of heterogeneous scratch shapes. `any` is