@weasel-js/routing 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.ts CHANGED
@@ -1,296 +1,65 @@
1
- import { D as Dispatcher, A as ActionsRegistry, T as Tool, a as AffordanceHit, R as RuleCtx, b as DepSchema, c as Action, d as AnyToolOf, O as OverlayPosition, C as Contribution, S as ScopedBinding } from './index-CKmbNpwl.js';
2
- export { e as DepRegistry, f as DepRegistryProvider, u as useDepRegistry, g as useDepSource, h as useOptionalDepRegistry } from './index-CKmbNpwl.js';
1
+ export { D as DispatcherChannels, U as UseGestureDispatcherOptions, u as useGestureDispatcher } from './internal--mYnGecm.js';
3
2
  import * as react from 'react';
4
- import { RefObject, ReactNode, ReactElement } from 'react';
5
- import { BodyClassification } from '@weasel-js/gestures';
6
- import { PaintedCursorState } from '@weasel-js/cursor';
7
- import '@weasel-js/modes';
3
+ import { ReactNode, ReactElement } from 'react';
4
+ import { D as DepRegistry, a as DepName, b as DepSchema, S as ScopeNode, A as ActionsRegistry, c as Action, d as DepNode, K as KernelOverlay, C as Contribution, e as AnyToolOf, O as OverlayPosition, f as ScopedBinding } from './index-BV53gfyB.js';
5
+ import { ModeRegistry } from '@weasel-js/modes';
6
+ import '@weasel-js/gestures';
7
+ import '@weasel-js/cursor';
8
8
  import '@weasel-js/history';
9
9
 
10
+ /** Provides the dep registry for a canvas. `<SceneCanvas>` mounts one; a
11
+ * consumer registering its own dep sources must be inside it. */
12
+ declare function DepRegistryProvider({ children }: {
13
+ children: ReactNode;
14
+ }): react.JSX.Element;
15
+ /** The dep registry in scope. Throws outside a `<DepRegistryProvider>`. */
16
+ declare function useDepRegistry(): DepRegistry;
10
17
  /**
11
- * Everything about handling an input event that depends on *which view* it
12
- * landed in: the dispatcher that runs it, and the three lookups that resolve
13
- * a client point against that view's camera.
14
- *
15
- * @internal One per view. The hook builds the `id: null` one from its flat
16
- * options; `views` supplies the rest, and the hook chooses between them per
17
- * event, so that it — and its listener set — still mounts exactly once.
18
- */
19
- interface DispatcherViewTarget {
20
- /** `null` is the root view — the whole canvas. Matches `ViewTarget.id`. */
21
- id: string | null;
22
- dispatcher: Dispatcher;
23
- affordanceAt: UseGestureDispatcherOptions['affordanceAt'];
24
- classifyTarget: UseGestureDispatcherOptions['classifyTarget'];
25
- clientToWorld: UseGestureDispatcherOptions['clientToWorld'];
26
- /**
27
- * The deps this view answers for itself — its camera, its selection, and
28
- * whatever else is per-view rather than per-canvas. Read per event; every
29
- * name it does not carry still resolves through the canvas registry.
30
- *
31
- * This is why a view does not get a `DepRegistryProvider` of its own. The
32
- * registry is where a consumer registers *sources*, and one per view would
33
- * fragment that: a consumer overriding `insert` would have to know how many
34
- * views exist and override each. An overlay keeps one place to register and
35
- * one authority per dep, with the view claiming only what is genuinely its.
36
- *
37
- * Without this a gesture inside a panel pans the whole canvas and selects
38
- * into the canvas's selection — correct coordinates alone do not make
39
- * routing correct.
40
- */
41
- deps?: () => Partial<DepSchema>;
42
- /**
43
- * This view's chrome-caps rule context, for the eligibility filter. The rule
44
- * table is the surface's, but a rule keyed on selection or the in-flight
45
- * action answers per view — so an action hidden in one panel must not
46
- * decline a gesture in another. `undefined` means "nothing gates here", the
47
- * same answer a surface with no mode registry gives.
48
- */
49
- getRuleCtx?: () => RuleCtx | undefined;
50
- }
51
- /**
52
- * The part of `features/viewports`' `ViewResolver` the dispatcher needs: which
53
- * view a client point belongs to, pinned to the view a gesture began in.
54
- *
55
- * @internal Structural on purpose — a `ViewResolver` satisfies it, and the
56
- * dispatcher stays free of the viewport module.
57
- */
58
- interface ViewIdResolver {
59
- begin(pointerId: number, clientX: number, clientY: number): {
60
- id: string | null;
61
- };
62
- at(pointerId: number | null, clientX: number, clientY: number): {
63
- id: string | null;
64
- };
65
- end(pointerId: number): void;
66
- }
67
- /** Per-channel switches for the listeners `useGestureDispatcher` attaches to
68
- * the canvas element. Omitted or `true` attaches the channel. */
69
- interface DispatcherChannels {
70
- /** `pointerdown`, hover `pointermove`, `pointerleave` — press, drag, click,
71
- * long-press, multi-touch and the hover-cursor pump. */
72
- pointer?: boolean;
73
- /** `wheel`. */
74
- wheel?: boolean;
75
- /** WebKit's `gesturestart` / `gesturechange` / `gestureend` — Safari's
76
- * trackpad pinch, dispatched as `pinch` events. */
77
- pinch?: boolean;
78
- /** `contextmenu`. Off leaves the browser's native menu in place. */
79
- contextMenu?: boolean;
80
- /** OS drop on the element plus `paste` on the window. Off leaves the
81
- * element a non-drop-target and stops `weasel-dropover` being applied. */
82
- ingest?: boolean;
83
- }
84
- /** Options for `useGestureDispatcher`: the element to listen on, the actions
85
- * and tools in play, and the hooks that turn raw DOM events into the world
86
- * coordinates and hit targets bindings match against. */
87
- interface UseGestureDispatcherOptions {
88
- /** Ref to the canvas element. Pointer/wheel/multitouch listeners attach here. */
89
- canvasRef: RefObject<HTMLElement | null>;
90
- /** Action registry (ActionsRegistry from registry.tsx). */
91
- actions: ActionsRegistry;
92
- /** Tool definitions keyed by id. Typically passes an empty Map. */
93
- toolsById: ReadonlyMap<string, Tool>;
94
- /** Default true. Set false to opt out of dispatcher wiring (e.g. demos that disable it). */
95
- enabled?: boolean;
96
- /**
97
- * Observer fired whenever the dispatcher synthesizes a double click, in
98
- * world coordinates. Runs BEFORE the event is dispatched and independently
99
- * of which binding (if any) handles it.
100
- *
101
- * This is deliberately not an Action. `<SceneCanvas onDoubleClick>` is a
102
- * notification — "the user double-clicked, here's what they hit" — and a
103
- * notification must not compete with behavior for the gesture. As a binding
104
- * it would lose to `enterPathEdit` on any body hit and silently never fire.
105
- * Routing it here keeps a single definition of "double click" (the point of
106
- * consolidating the kit's three detectors) without giving it
107
- * first-match-wins semantics it shouldn't have.
108
- */
109
- onDoubleClick?: (world: {
110
- x: number;
111
- y: number;
112
- }) => void;
113
- /**
114
- * Default true. Set false to leave the window `keydown`/`keyup` listeners
115
- * unattached so keyboard-bound actions never dispatch — pointer, wheel, and
116
- * contextmenu channels stay live. `<SceneCanvas>` wires this to
117
- * `enableKeybindings`, so opting out of keybindings disables the modern
118
- * dispatcher key path as well as the legacy `useKeybindings` hook.
119
- */
120
- keyboard?: boolean;
121
- /**
122
- * Optional affordance classifier. Called with the **world** coordinates of
123
- * the pointer — this hook applies the routed view's `clientToWorld` before
124
- * calling it, the same conversion that produces `InputEvent.x`/`y`, so a
125
- * consumer hit-tests in one space. Returns an `AffordanceHit` when the
126
- * pointer lands on a known affordance (resize handle, rotate handle, etc.)
127
- * or `null` when the pointer hit open canvas.
128
- *
129
- * The hit is packed into `InputEvent.pointerdown.affordance` and flows
130
- * through into `InvocationCtx.drag.affordance`. Action invokers that require
131
- * a specific affordance (e.g. `resizeAction` requires `handle:*`) use this
132
- * field as a guard in their `start` body.
133
- *
134
- * When omitted, `affordance` is always `undefined` — meaning only consumers
135
- * that explicitly wire a classifier get affordance-gated behavior.
136
- * `<SceneCanvas>` wires the full chrome→dispatcher bridge.
137
- */
138
- affordanceAt?: (worldPoint: {
139
- x: number;
140
- y: number;
141
- }) => AffordanceHit | null;
142
- /**
143
- * Optional body classifier. Called on every pointerdown with the world-space
144
- * coordinates of the pointer. Its result is packed onto the event as
145
- * `bodyTarget` + `bodyKind`, which `matchTarget` reads to resolve the
146
- * string-form `TargetSpec` values in `Tool.bindings`.
147
- *
148
- * `body` is `'empty'` when nothing is under the pointer, `'selected-body'`
149
- * when the topmost hit belongs to the current selection, or
150
- * `'unselected-body'` when it belongs to a node that isn't selected. `kind`
151
- * is the hit node's semantic kind, when the scene can name it.
152
- *
153
- * When omitted, every body-derived target form (`'empty'`,
154
- * `'selected-body'`, `'unselected-body'`, `kind:<k>`, `kind:<k>:selected`)
155
- * never matches — bindings using those specs are silently skipped.
156
- * `<SceneCanvas>` wires this.
157
- *
158
- * World coordinates, on every path, for the same reason as `affordanceAt`.
159
- */
160
- classifyTarget?: (worldPoint: {
161
- x: number;
162
- y: number;
163
- }) => BodyClassification;
164
- /**
165
- * Optional pre-created `Dispatcher`. When provided, this hook pumps events
166
- * into the supplied instance instead of creating its own. Lets a parent
167
- * scope (e.g. `<SceneCanvas>`) share one dispatcher between the gesture
168
- * mounter and other consumers (the preview-ghost layer).
169
- */
170
- dispatcher?: Dispatcher;
171
- /**
172
- * Converts a client-space pointer position (e.g. `e.clientX`, `e.clientY`)
173
- * to world-space coordinates. When supplied, every pointer/wheel event's
174
- * `x`/`y` is converted before the dispatcher builds `InvocationCtx.world`.
175
- *
176
- * Without this, `ctx.world` is populated with the raw client coords, which
177
- * silently breaks any action whose overlay/output uses absolute world
178
- * positions (marquee, lasso polyline, world-space affordances). Actions
179
- * that only read `drag.delta` are unaffected because client→world deltas
180
- * are equal at scale 1, but as soon as a consumer pans/zooms the view the
181
- * deltas diverge too.
182
- *
183
- * `<SceneCanvas>` always wires this via its canvas rect + current view.
184
- * Tests / harnesses without a view can omit it and continue passing raw
185
- * coordinates as before.
186
- */
187
- clientToWorld?: (clientX: number, clientY: number) => {
188
- x: number;
189
- y: number;
190
- };
191
- /**
192
- * Invoked once per pump event (`pointermove`, `pointerup`, `pointercancel`,
193
- * `key-held` up-phase, `multitouch` move) when a dispatcher-side handle is
194
- * in flight. Lets the host canvas schedule a redraw so dispatcher-only
195
- * overlays (marquee, lasso, preview-ghost) repaint on each frame.
196
- *
197
- * Required because dispatcher actions don't go through the legacy
198
- * `tools.dispatcher.onGestureChange` redraw bump that ambient tool drags
199
- * relied on. Without it, the overlay layer's `draw` never runs between
200
- * pointerdown and pointerup, and the chrome flashes once at gesture start
201
- * and then sits frozen until the gesture ends.
202
- *
203
- * `<SceneCanvas>` wires this to the canvas's `requestRedraw`. Test
204
- * harnesses can omit it; their dispatchers won't paint between events but
205
- * that's already the test contract.
206
- */
207
- requestRedraw?: () => void;
208
- /**
209
- * The surface's painted-cursor channel, read live because the canvas handle
210
- * it lives on arrives a render after this hook runs.
211
- *
212
- * Given one, a cursor the CSS tier cannot express — sized in world units, or
213
- * past the size the browser drops the image at — is published here and drawn
214
- * by the painted-cursor layer instead. Without one, such a cursor resolves to
215
- * `none` and the pointer shows nothing.
216
- */
217
- paintedCursor?: () => PaintedCursorState | undefined;
218
- /**
219
- * Thunk returning the live `RuleCtx` for the current frame. When supplied,
220
- * the dispatcher filters matched candidates by their declared
221
- * `Action.eligible` rule (omitted => always eligible). `<SceneCanvas>`
222
- * wires this; tests / harnesses without chrome-caps state can omit it.
223
- */
224
- getRuleCtx?: () => RuleCtx | undefined;
225
- /**
226
- * Which of the canvas element's input channels to attach. Every one
227
- * defaults to `true`; naming a channel `false` leaves its listeners
228
- * unattached, so the element behaves as if the dispatcher were not there.
229
- *
230
- * A mount that binds two gestures should not also have to take the rest of
231
- * the pipeline's side effects: `contextMenu` suppresses the native menu
232
- * unconditionally, and `ingest` makes the element a valid drop target and
233
- * styles it while an OS drag hovers. `@weasel-js/labkit`'s loupe mounts on
234
- * a lab's own canvas host for a wheel and a held key, and turning the other
235
- * three off is what keeps right-click and file drops working there.
236
- *
237
- * The window keyboard channel is `keyboard` instead of an entry here,
238
- * because `<SceneCanvas enableKeybindings>` drives it.
239
- */
240
- channels?: DispatcherChannels;
241
- /**
242
- * Routing for a canvas hosting more than one view: the non-root dispatch
243
- * records to choose between, read fresh per event, and the resolver that
244
- * chooses. Omit for a single view — then every event runs on the record the
245
- * flat options above describe, which is what they are: view zero.
246
- *
247
- * A resolved id with no live record falls back to the root, so a view that
248
- * unmounts mid-gesture degrades instead of dropping the event.
249
- *
250
- * Keyboard and paste have no coordinates to resolve; they run on the view
251
- * the last coordinate-bearing event resolved to.
252
- *
253
- * @internal
254
- */
255
- views?: {
256
- targets: () => readonly DispatcherViewTarget[];
257
- resolver: ViewIdResolver;
258
- };
259
- }
260
- /**
261
- * Attach the input pipeline to a canvas: DOM pointer, wheel, keyboard and
262
- * multitouch listeners in, normalized input events out, routed through the
263
- * dispatcher to whichever action a binding names.
264
- *
265
- * Everything it needs beyond its options — the active tool, the dep registry —
266
- * comes from context, so it must be mounted inside `<SceneCanvas>`'s
267
- * providers.
18
+ * Like `useDepRegistry`, but returns `null` when no `<DepRegistryProvider>` is
19
+ * in scope instead of throwing. Used by `useStandardActions` to preserve its
20
+ * silent-no-op contract when neither provider is present.
268
21
  */
269
- declare function useGestureDispatcher(opts: UseGestureDispatcherOptions): void;
22
+ declare function useOptionalDepRegistry(): DepRegistry | null;
23
+ /** Register a live source for `name` for the lifetime of the calling
24
+ * component. The `source` thunk is called at dispatch time and should
25
+ * return the latest value. */
26
+ declare function useDepSource<K extends DepName>(name: K, source: () => DepSchema[K]): void;
270
27
 
271
28
  /**
272
- * @experimental
273
- * The React half of the actions registry: the context, the provider that owns
274
- * the id → descriptor stack, the mute scope, and the two hooks. The store
275
- * contract it implements is in `registry.tsx`.
29
+ * One node of the actions tree: the actions registered at it, over its
30
+ * parent's. Each id holds a stack of registrants, newest live, so a displaced
31
+ * registrant is restored when the one above it leaves. Every read the registry
32
+ * answers — `list`, `trigger`, `begin` — starts at the node's leaf.
276
33
  */
277
-
34
+ declare class ActionsNode extends ScopeNode<ActionsNode> {
35
+ private readonly contextDeps;
36
+ private readonly actions;
37
+ private readonly muted;
38
+ private readonly dispatchers;
39
+ private cache;
40
+ /** Called on `activate`, so a scope's dep and tool nodes move with it. */
41
+ readonly onActivate: (() => void)[];
42
+ constructor(parent: ActionsNode | null, descends: boolean, contextDeps: () => DepRegistry | null);
43
+ private resolve;
44
+ private snapshot;
45
+ private deps;
46
+ private dispatcher;
47
+ activate(): void;
48
+ readonly registry: ActionsRegistry;
49
+ }
278
50
  /**
279
51
  * @experimental
280
- * A view of the registry in scope that can declare ids not for itself. Wrap
281
- * anything that should be able to opt out of an action — a second
282
- * `<SceneCanvas>` sharing the host's `<ActionsProvider>` mounts one — and its
283
- * `mute` calls stay inside it. Renders nothing of its own.
284
- *
285
- * Not a `BindingScope`: that names the tier a binding matches at (hotkey /
286
- * active / ambient), which this has nothing to do with.
52
+ * Mounts a root `ActionsRegistry` for its lifetime. Children call
53
+ * `useActionsRegistry()` or `useAction()` to participate. Mounts no input
54
+ * listener of its own — the gesture dispatcher owns input.
287
55
  */
288
- declare function ActionsScope({ children }: {
289
- children: ReactNode;
290
- }): ReactElement;
291
56
  declare function ActionsProvider({ children }: {
292
57
  children: ReactNode;
293
58
  }): ReactElement;
59
+ /**
60
+ * @experimental
61
+ * Returns the `ActionsRegistry` in scope, or `null` when there is none.
62
+ */
294
63
  declare function useActionsRegistry(): ActionsRegistry | null;
295
64
  /**
296
65
  * @experimental
@@ -301,42 +70,15 @@ declare function useActionsRegistry(): ActionsRegistry | null;
301
70
  */
302
71
  declare function useAction(action: Action): void;
303
72
 
304
- /**
305
- * One UI control's hold on an ongoing action — the handle a color picker, a
306
- * slider or a swatch drives the action through, the way a drag drives one
307
- * through a gesture.
308
- *
309
- * - `input(params)` opens the action on the first call and moves it on every
310
- * later one: the live preview.
311
- * - `commit(params?)` moves it to `params`, if given, and ends it as one
312
- * undoable edit. With nothing open, a commit carrying params is a whole edit
313
- * on its own — a click on a swatch, a switch of paint kind.
314
- * - `cancel()` ends an open edit without keeping it.
315
- *
316
- * All three are no-ops when no action is reachable, so a control renders the
317
- * same with or without a canvas in scope.
318
- */
319
- interface OngoingAction {
320
- input(params: Record<string, unknown>): void;
321
- commit(params?: Record<string, unknown>): void;
322
- cancel(): void;
323
- }
324
- /**
325
- * Drive the ongoing action `actionId` from a UI control: {@link OngoingAction}.
326
- *
327
- * An edit left open when the control unmounts, or when `actionId` changes, is
328
- * committed — it is what the user was looking at when the control went away.
329
- * The returned object is stable for the life of the component.
330
- */
331
- declare function useOngoingAction(actionId: string): OngoingAction;
332
-
333
73
  /** Which tool is active, plus the stack of tools temporarily held active by a
334
74
  * hotkey (space-for-hand and the like). The dispatcher reads this to decide
335
75
  * whose bindings are in scope. */
336
76
  interface ActiveToolContextValue {
337
- active: string;
77
+ /** The focused tool's id, or `null` when no tool holds the active slot —
78
+ * a canvas can run on ambient bindings alone. */
79
+ active: string | null;
338
80
  hotkeyStack: string[];
339
- setActive(id: string): void;
81
+ setActive(id: string | null): void;
340
82
  pushHotkey(id: string): void;
341
83
  /**
342
84
  * Disengage a hotkey-held tool. With `id`, removes that tool's own entry
@@ -346,15 +88,33 @@ interface ActiveToolContextValue {
346
88
  */
347
89
  popHotkey(id?: string): void;
348
90
  }
91
+ /**
92
+ * One node of the tool tree. A node that `owns` a tool holds its state; one
93
+ * that does not — a scope inside a yoke — reads and writes its yoke's.
94
+ */
95
+ declare class ToolNode extends ScopeNode<ToolNode> {
96
+ private state;
97
+ private value;
98
+ constructor(parent: ToolNode | null, descends: boolean, owns: boolean, initialActive: string | null);
99
+ /** The node whose tool a read here resolves to. */
100
+ owner(): ToolNode;
101
+ private update;
102
+ /** A stable value per owner and state, for `useSyncExternalStore`. */
103
+ snapshot(): ActiveToolContextValue;
104
+ subscribe: (listener: () => void) => (() => void);
105
+ }
349
106
  /** Props for `<ActiveToolContextProvider>`. */
350
107
  interface ActiveToolContextProviderProps {
351
108
  children: ReactNode;
352
- initialActive?: string;
109
+ /** Seeds the active slot, and wins over the first `useTools` call's
110
+ * `active`. Omitted, the slot starts empty and that call seeds it. */
111
+ initialActive?: string | null;
353
112
  }
354
- /** Provides active-tool state for a canvas. `<SceneCanvas>` mounts one. */
113
+ /** Provides root active-tool state. Every canvas below has a tool of its own;
114
+ * reads and writes here reach the one last used. */
355
115
  declare function ActiveToolContextProvider({ children, initialActive, }: ActiveToolContextProviderProps): react.JSX.Element;
356
116
  /** The active-tool state in scope. Throws outside a provider; use
357
- * `useActiveToolContextOptional` where one is not guaranteed. */
117
+ * `useOptionalActiveToolContext` where one is not guaranteed. */
358
118
  declare function useActiveToolContext(): ActiveToolContextValue;
359
119
  /**
360
120
  * Like `useActiveToolContext`, but returns `null` when no
@@ -366,33 +126,127 @@ declare function useOptionalActiveToolContext(): ActiveToolContextValue | null;
366
126
  /**
367
127
  * Conditional `<ActiveToolContextProvider>` wrapper. Mounts a provider only
368
128
  * when no parent provider is in scope — otherwise renders children unwrapped
369
- * so callers (e.g. `<WeaselProvider>`, `<SceneCanvas>`) defer to the host's
370
- * existing scope. Mirrors `ActionsProviderIfRoot` / `DepRegistryProviderIfRoot`.
129
+ * so `<WeaselProvider>` defers to the host's existing scope.
371
130
  */
372
131
  declare function ActiveToolContextProviderIfRoot({ children, }: {
373
132
  children: ReactNode;
374
133
  }): react.JSX.Element;
375
134
 
135
+ /**
136
+ * @experimental
137
+ * Input scopes and yokes: the tiers between a root registry and a canvas.
138
+ * Design: docs/proposals/2026-09-27-input-scopes.md.
139
+ */
140
+
141
+ /** Props for `<InputScope>`. */
142
+ interface InputScopeProps {
143
+ /** The yoke this scope joins. Omitted, it sits under the registries in
144
+ * scope and keeps a tool of its own. */
145
+ yoke?: Yoke;
146
+ children: ReactNode;
147
+ }
148
+ /**
149
+ * @experimental
150
+ * One canvas's input: actions, deps and a tool of its own, under whichever
151
+ * registries are in scope (or its yoke's). What the canvas registers stays
152
+ * here; what it looks up is answered here first, then by the tiers above.
153
+ * With nothing in scope it is a root.
154
+ *
155
+ * Chrome above calls `trigger` / `begin` / `useActiveToolContext` on its own
156
+ * registries, and those reach the active scope: the one last activated — by a
157
+ * pointerdown or wheel — or the newest where none has been.
158
+ */
159
+ declare function InputScope({ yoke, children }: InputScopeProps): ReactElement;
160
+ /**
161
+ * @experimental
162
+ * Make a yoke for canvases to join, under the registries in scope where it is
163
+ * called. It lives as long as the calling component.
164
+ */
165
+ declare function useYoke(): Yoke;
166
+ /** Props for `<Yoke>`. */
167
+ interface YokeProps {
168
+ value: Yoke;
169
+ children: ReactNode;
170
+ }
171
+ /**
172
+ * @experimental
173
+ * Canvases that share input on purpose: the active tool, the gesture in
174
+ * flight, history and yoke-wide actions. Made once with `useYoke()` where the
175
+ * sharing is decided, and passed to each canvas that joins it (`yoke={yoke}`)
176
+ * and to `<Yoke value={yoke}>` around anything else that should reach it — a
177
+ * toolbar, say. The members can sit anywhere in the tree: the handle, not the
178
+ * tree, says who shares.
179
+ */
180
+ interface Yoke {
181
+ /** @internal */
182
+ readonly actions: ActionsNode;
183
+ /** @internal */
184
+ readonly deps: DepNode;
185
+ /** @internal */
186
+ readonly tools: ToolNode;
187
+ }
188
+ /**
189
+ * @experimental
190
+ * Put `value` in scope for what is not a canvas — a toolbar, a status bar — so
191
+ * its actions, deps and tool reads reach the yoke and, through it, the member
192
+ * canvas last used.
193
+ */
194
+ declare function Yoke({ value, children }: YokeProps): ReactElement;
195
+
196
+ /**
197
+ * One UI control's hold on an ongoing action — the handle a color picker, a
198
+ * slider or a swatch drives the action through, the way a drag drives one
199
+ * through a gesture.
200
+ *
201
+ * - `input(params)` opens the action on the first call and moves it on every
202
+ * later one: the live preview.
203
+ * - `commit(params?)` moves it to `params`, if given, and ends it as one
204
+ * undoable edit. With nothing open, a commit carrying params is a whole edit
205
+ * on its own — a click on a swatch, a switch of paint kind.
206
+ * - `cancel()` ends an open edit without keeping it.
207
+ *
208
+ * All three are no-ops when no action is reachable, so a control renders the
209
+ * same with or without a canvas in scope.
210
+ */
211
+ interface OngoingAction {
212
+ input(params: Record<string, unknown>): void;
213
+ commit(params?: Record<string, unknown>): void;
214
+ cancel(): void;
215
+ }
216
+ /**
217
+ * Drive the ongoing action `actionId` from a UI control: {@link OngoingAction}.
218
+ *
219
+ * An edit left open when the control unmounts, or when `actionId` changes, is
220
+ * committed — it is what the user was looking at when the control went away.
221
+ * The returned object is stable for the life of the component.
222
+ */
223
+ declare function useOngoingAction(actionId: string): OngoingAction;
224
+
376
225
  /** Options for `useTools`: which tools exist, which one starts active, and
377
226
  * which run continuously regardless of the active one. */
378
- interface UseToolsOptions<TOverlay = unknown> {
379
- /** Initial active-slot tool id. Must exist in `registry`. */
380
- active: string;
227
+ interface UseToolsOptions<TOverlay = KernelOverlay> {
228
+ /** Initial active-slot tool id, which must exist in `registry`. Omit, or
229
+ * pass `null`, for no active tool: only hotkey and ambient bindings run. */
230
+ active?: string | null;
381
231
  /** Tools eligible for the active slot or hotkey slot. The keys are the
382
232
  * tool ids; the values are the tool records. A tool with `hotkey` set
383
233
  * is wired into the hotkey slot whenever the engagement state matches. */
384
234
  registry: Record<string, AnyToolOf<TOverlay>>;
385
- /** Always-on tools — listen continuously regardless of active slot. */
386
- ambient?: AnyToolOf<TOverlay>[];
235
+ /** Always-on entries — tools or other contributions, live regardless of
236
+ * the active slot. */
237
+ ambient?: readonly Contribution<TOverlay>[];
238
+ /** The modes the canvas can be in, read by the dev-time route-conflict
239
+ * check; without a registry it checks against the kit's `DEFAULT_MODES`. */
240
+ modes?: ModeRegistry;
387
241
  }
388
242
  /** The tool registry's runtime surface: which tool is active, which is
389
243
  * temporarily held by a hotkey, and how to change either. */
390
- interface ToolsApi<TOverlay = unknown> {
391
- /** Current active-slot tool id. */
392
- active: string;
393
- /** Set the active-slot tool. The gesture dispatcher watches the active
394
- * tool and cancels any in-flight handle itself. */
395
- setActive: (id: string) => void;
244
+ interface ToolsApi<TOverlay = KernelOverlay> {
245
+ /** Current active-slot tool id, or `null` when no tool is active. */
246
+ active: string | null;
247
+ /** Set the active-slot tool, or `null` to clear it. The gesture dispatcher
248
+ * watches the active tool and cancels any in-flight handle itself. */
249
+ setActive: (id: string | null) => void;
396
250
  /** Currently hotkey-engaged tool id (or `null`). Derived as the top of
397
251
  * the hotkey stack for backwards compat with the pre-stack API. */
398
252
  hotkeyEngaged: string | null;
@@ -400,8 +254,8 @@ interface ToolsApi<TOverlay = unknown> {
400
254
  engageHotkey: (id: string) => void;
401
255
  /** Disengage the hotkey-slot tool, if any. */
402
256
  disengageHotkey: () => void;
403
- /** All always-on tools, in registration order. */
404
- ambient: readonly AnyToolOf<TOverlay>[];
257
+ /** All always-on entries, in registration order. */
258
+ ambient: readonly Contribution<TOverlay>[];
405
259
  /** Full registry — for userland UI (palette buttons, etc.). */
406
260
  registry: Readonly<Record<string, AnyToolOf<TOverlay>>>;
407
261
  /** Returns true if a tool with the given id is in the registry or ambient list. */
@@ -424,28 +278,35 @@ interface ToolsApi<TOverlay = unknown> {
424
278
  * lives in the context so the gesture dispatcher and any sibling
425
279
  * `useTools` calls all read the same source of truth.
426
280
  *
427
- * **First-mount-wins semantics**: if `opts.active` differs from the context
428
- * default (`'select'`) on first mount, `useTools` pushes `opts.active` to
429
- * the context. Subsequent mounts respect whatever the context currently
430
- * holds (the first caller wins).
281
+ * **First-mount-wins semantics**: if the context's slot is unseeded (no
282
+ * `initialActive`, nothing set yet) on first mount, `useTools` pushes
283
+ * `opts.active` to the context. Subsequent mounts respect whatever the
284
+ * context currently holds (the first caller wins).
431
285
  */
432
- declare function useTools<TOverlay = unknown>(opts: UseToolsOptions<TOverlay>): ToolsApi<TOverlay>;
286
+ declare function useTools<TOverlay = KernelOverlay>(opts: UseToolsOptions<TOverlay>): ToolsApi<TOverlay>;
433
287
 
434
- interface UseContributionsOptions<TOverlay = unknown> {
288
+ /** Options for {@link useContributions}. */
289
+ interface UseContributionsOptions<TOverlay = KernelOverlay> {
435
290
  /** Every registry entry, in declaration order. Order decides which of two
436
291
  * same-specificity bindings in one scope tier wins. */
437
292
  entries: readonly Contribution<TOverlay>[];
438
- /** Desired focused entry id. First-mount-wins against the shared
439
- * `ActiveToolContext` — see `useTools` for the full semantics. */
440
- focused: string;
293
+ /** Desired focused entry id, or `null` for none. First-mount-wins against
294
+ * the shared `ActiveToolContext` — see `useTools` for the full semantics. */
295
+ focused: string | null;
296
+ /** The modes the canvas can be in. The dev-time route-conflict check reads
297
+ * them to decide whether two gated actions can ever compete; without a
298
+ * registry it checks against the kit's `DEFAULT_MODES`. */
299
+ modes?: ModeRegistry;
441
300
  }
442
- interface ContributionsApi<TOverlay = unknown> {
301
+ /** The assembled registry {@link useContributions} returns. */
302
+ interface ContributionsApi<TOverlay = KernelOverlay> {
443
303
  /** Every entry, as passed in. */
444
304
  entries: readonly Contribution<TOverlay>[];
445
- /** Currently focused entry id. */
446
- focused: string;
447
- /** Focus an entry. Writes through to the shared `ActiveToolContext`. */
448
- setFocused: (id: string) => void;
305
+ /** Currently focused entry id, or `null` when nothing is focused. */
306
+ focused: string | null;
307
+ /** Focus an entry, or pass `null` to focus none. Writes through to the
308
+ * shared `ActiveToolContext`. */
309
+ setFocused: (id: string | null) => void;
449
310
  /** Every live binding, tiered by the declaring entry's eligibility. */
450
311
  scopedBindings(): ScopedBinding[];
451
312
  /** Overlays of every live entry declaring `position`, ordered active, then
@@ -462,6 +323,14 @@ interface ContributionsApi<TOverlay = unknown> {
462
323
  * in the context so the gesture dispatcher and every sibling caller read the
463
324
  * same source of truth.
464
325
  */
465
- declare function useContributions<TOverlay = unknown>(opts: UseContributionsOptions<TOverlay>): ContributionsApi<TOverlay>;
326
+ declare function useContributions<TOverlay = KernelOverlay>(opts: UseContributionsOptions<TOverlay>): ContributionsApi<TOverlay>;
327
+ /**
328
+ * Registers the consolidated `tool.offhand` action for every entry declaring
329
+ * an `offhand` trigger, into the nearest `<ActionsProvider>`; its invoker
330
+ * reads `params.toolId` off the matched binding. `useContributions` calls it,
331
+ * and so does any host that assembles its entries above its own provider —
332
+ * `<SceneCanvas>` does — since a null registry here registers nothing.
333
+ */
334
+ declare function useOffhandAction(entries: readonly Contribution<unknown>[]): void;
466
335
 
467
- export { ActionsProvider, ActionsScope, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, type ActiveToolContextProviderProps, type ActiveToolContextValue, type ContributionsApi, type DispatcherChannels, type DispatcherViewTarget, type OngoingAction, type ToolsApi, type UseContributionsOptions, type UseGestureDispatcherOptions, type UseToolsOptions, type ViewIdResolver, useAction, useActionsRegistry, useActiveToolContext, useContributions, useGestureDispatcher, useOngoingAction, useOptionalActiveToolContext, useTools };
336
+ export { ActionsProvider, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, type ActiveToolContextProviderProps, type ActiveToolContextValue, type ContributionsApi, DepRegistry, DepRegistryProvider, InputScope, type InputScopeProps, type OngoingAction, type ToolsApi, type UseContributionsOptions, type UseToolsOptions, Yoke, type YokeProps, useAction, useActionsRegistry, useActiveToolContext, useContributions, useDepRegistry, useDepSource, useOffhandAction, useOngoingAction, useOptionalActiveToolContext, useOptionalDepRegistry, useTools, useYoke };