@weasel-js/gestures 0.5.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.
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/index.d.ts +669 -0
- package/dist/index.js +556 -0
- package/dist/index.js.map +1 -0
- package/package.json +41 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative gesture taxonomy. Single source of truth for:
|
|
3
|
+
* - which gestures hit-test (have a `.target` slot in the route string)
|
|
4
|
+
* - which gestures carry an argument and what values are legal
|
|
5
|
+
* - the default arg value (used when none is specified in a route)
|
|
6
|
+
*
|
|
7
|
+
* Reflection, matcher, and inspector UI all read this table. Adding a new
|
|
8
|
+
* gesture name in one place updates every consumer.
|
|
9
|
+
*/
|
|
10
|
+
type GestureName = 'click' | 'pointerDown' | 'dblTap' | 'drag' | 'wheel' | 'keyDown' | 'keyUp' | 'keyHeld' | 'contextMenu' | 'multiTouchTap';
|
|
11
|
+
interface GestureArgSpec {
|
|
12
|
+
/** Display name for the arg in inspector chips (`direction`, `key`, `fingers`). */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Acceptable values. `'free'` means any string (e.g. key names). */
|
|
15
|
+
values: readonly string[] | 'free';
|
|
16
|
+
/** Default value when a route omits the arg slot. Must be in `values`
|
|
17
|
+
* unless `values === 'free'`. Optional: no default means routes that
|
|
18
|
+
* omit the arg slot match every value (only legal for `'free'` args). */
|
|
19
|
+
default?: string;
|
|
20
|
+
}
|
|
21
|
+
interface GestureDescriptor {
|
|
22
|
+
name: GestureName;
|
|
23
|
+
/** Does the route's `.target` slot apply? Targetless gestures (`wheel`,
|
|
24
|
+
* `keyDown/Up`, `multiTouchTap`) elide it entirely in the v2 grammar. */
|
|
25
|
+
hasTarget: boolean;
|
|
26
|
+
/** Optional argument spec. Encoded as `gesture(value)` in the route string. */
|
|
27
|
+
arg?: GestureArgSpec;
|
|
28
|
+
}
|
|
29
|
+
declare const GESTURE_DESCRIPTORS: readonly GestureDescriptor[];
|
|
30
|
+
declare function getGestureDescriptor(name: GestureName): GestureDescriptor;
|
|
31
|
+
declare function isKnownGestureName(name: string): name is GestureName;
|
|
32
|
+
|
|
33
|
+
/** All valid keys for a modifier sub-table in a route entry. Canonical
|
|
34
|
+
* order: mod → shift → alt (matches formatShortcut). */
|
|
35
|
+
type ModifierCombo = 'default' | 'mod' | 'shift' | 'alt' | 'mod+shift' | 'mod+alt' | 'shift+alt' | 'mod+shift+alt';
|
|
36
|
+
/** Convenience: produce the canonical ModifierCombo from modifiers passed
|
|
37
|
+
* in any order. Useful in computed property syntax:
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* 'rect': {
|
|
41
|
+
* [mods('shift')]: addToSelection,
|
|
42
|
+
* [mods('alt', 'shift')]: cloneAndAdd, // → 'shift+alt'
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare function mods(...keys: ReadonlyArray<'mod' | 'shift' | 'alt'>): ModifierCombo;
|
|
47
|
+
|
|
48
|
+
/** Phase of a gesture lifecycle. `initial` means the tool is idle
|
|
49
|
+
* (scratch null); `engaged` means a gesture is in progress (scratch
|
|
50
|
+
* populated). The route-grammar's `[phase]` slot draws from this set. */
|
|
51
|
+
type RoutePhase = 'initial' | 'engaged';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Route-string grammar v3:
|
|
55
|
+
*
|
|
56
|
+
* route = phaseSlot WS gesture WS argSlot? WS targetSlot? WS modSlot?
|
|
57
|
+
* phaseSlot = '[' phaseList ']'
|
|
58
|
+
* phaseList = phaseAtom (WS ',' WS phaseAtom)*
|
|
59
|
+
* phaseAtom = (channel ':')? phaseValue -- bare phaseValue ≡ '&:phaseValue'
|
|
60
|
+
* channel = '&' | '*' | toolId -- '&' = the binding's own tool
|
|
61
|
+
* phaseValue = 'initial' | 'engaged' | '*'
|
|
62
|
+
* argSlot = '(' argValue ')' -- whitespace inside parens is significant
|
|
63
|
+
* targetSlot = '=>' WS targetValue -- omitted slot defaults to '*' for hasTarget
|
|
64
|
+
* modSlot = modAtom (WS modAtom)*
|
|
65
|
+
* modAtom = sigil modName
|
|
66
|
+
* sigil = '+' | '?' -- ! @ # $ % ^ & * reserved as id-prefix
|
|
67
|
+
* modName = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
|
|
68
|
+
*
|
|
69
|
+
* Shorthand: a bare phaseValue (no `:`) implies channel `&` ("this tool's
|
|
70
|
+
* own phase"). `[engaged]` ≡ `[&:engaged]`; `[*]` ≡ `[&:*]`. The truly-loose
|
|
71
|
+
* form (any channel, any phase) is `[*:*]`.
|
|
72
|
+
*
|
|
73
|
+
* Examples:
|
|
74
|
+
* [initial] click => empty +shift -- self idle
|
|
75
|
+
* [engaged] wheel -- self mid-gesture
|
|
76
|
+
* [rect:engaged] wheel -- when rect tool is mid-gesture
|
|
77
|
+
* [*:engaged] keyDown(Delete) -- when any tool is mid-gesture
|
|
78
|
+
* [initial,engaged] contextMenu => empty -- either self phase
|
|
79
|
+
* [*] click => empty -- self, any phase
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** Channel reference for a phase atom. `'&'` = the binding's own tool;
|
|
83
|
+
* `'*'` = any tool; otherwise a registered tool id. */
|
|
84
|
+
type ChannelRef = '&' | '*' | string;
|
|
85
|
+
/** One element of a phase list: a (channel, phase) pair. The default
|
|
86
|
+
* channel (omitted in the shorthand) is `'&'`. `phase: '*'` means
|
|
87
|
+
* "any phase of the given channel". */
|
|
88
|
+
interface PhaseAtom {
|
|
89
|
+
channel: ChannelRef;
|
|
90
|
+
phase: RoutePhase | '*';
|
|
91
|
+
}
|
|
92
|
+
/** Reserved id-prefix sigils. Tool ids, action ids, channel names, target
|
|
93
|
+
* kinds, and arg values may not start with any of these — keeps the
|
|
94
|
+
* parser unambiguous and reserves single-char prefixes for future grammar
|
|
95
|
+
* extensions. */
|
|
96
|
+
declare const RESERVED_ID_PREFIXES: ReadonlySet<string>;
|
|
97
|
+
/** Reserved bareword channel/id names. These are keywords in the phase
|
|
98
|
+
* slot — a tool may not register with one of these as its id (collides
|
|
99
|
+
* with the bare-phase shorthand). */
|
|
100
|
+
declare const RESERVED_ID_NAMES: ReadonlySet<string>;
|
|
101
|
+
/** v3 modifier requirement on a single modifier key. Absent from a
|
|
102
|
+
* `ParsedModifiers` map means "must not be held" (the strict default). */
|
|
103
|
+
type ModRequirement = 'required' | 'optional';
|
|
104
|
+
/** Canonical modifier names accepted in the modSlot, in canonical order.
|
|
105
|
+
* Single source of truth: the {@link ModifierKey} type and the runtime
|
|
106
|
+
* validator (`MOD_NAME_SET`) are both derived from this tuple. */
|
|
107
|
+
declare const VALID_MOD_NAMES: readonly ["mod", "shift", "alt", "ctrl", "meta"];
|
|
108
|
+
/** A single modifier name accepted in the modSlot. */
|
|
109
|
+
type ModifierKey = (typeof VALID_MOD_NAMES)[number];
|
|
110
|
+
/** Parsed-form modifiers — structured map; absent keys are implicitly
|
|
111
|
+
* forbidden. Empty object = "no modifiers held". */
|
|
112
|
+
type ParsedModifiers = Partial<Record<ModifierKey, ModRequirement>>;
|
|
113
|
+
/** Reserved sigil characters. The parser rejects any of these with a
|
|
114
|
+
* "reserved for future use" error so introducing them later is
|
|
115
|
+
* non-breaking. `*` is NOT in this set — it's the universal wildcard. */
|
|
116
|
+
declare const RESERVED_SIGILS: ReadonlySet<string>;
|
|
117
|
+
/** Active (parseable) sigils today: `+` required, `?` optional. */
|
|
118
|
+
declare const ACTIVE_SIGILS: ReadonlySet<string>;
|
|
119
|
+
declare const MOD_NAME_SET: ReadonlySet<string>;
|
|
120
|
+
|
|
121
|
+
/** v3 parsed-route shape. */
|
|
122
|
+
interface ParsedRoute {
|
|
123
|
+
/** One or more phase atoms (channel + phase). Empty array is invalid.
|
|
124
|
+
* Bare-phase shorthand in the source string desugars to channel `'&'`,
|
|
125
|
+
* so `[engaged]` parses to `[{ channel: '&', phase: 'engaged' }]`. */
|
|
126
|
+
phases: readonly PhaseAtom[];
|
|
127
|
+
gesture: GestureName;
|
|
128
|
+
/** Resolved arg. For arg-bearing gestures, the descriptor's default
|
|
129
|
+
* fills in when the slot is omitted (e.g. `wheel` → `arg: '*'`).
|
|
130
|
+
* Undefined for gestures whose descriptor has no `arg`. */
|
|
131
|
+
arg: string | undefined;
|
|
132
|
+
/** For hasTarget gestures: target string with `'*'` as the wildcard
|
|
133
|
+
* sentinel. Defaults to `'*'` when the slot is omitted. Undefined for
|
|
134
|
+
* hasTarget=false gestures. */
|
|
135
|
+
target: string | undefined;
|
|
136
|
+
/** Structured modifier requirements; empty map = "no modifiers held". */
|
|
137
|
+
modifiers: ParsedModifiers;
|
|
138
|
+
}
|
|
139
|
+
declare function parseRoute(input: string): ParsedRoute;
|
|
140
|
+
/** Render a single phase atom in canonical shorthand form. `&` channel
|
|
141
|
+
* is elided ("`engaged`", "`*`"); other channels use the explicit
|
|
142
|
+
* `channel:phase` form ("`rect:engaged`", "`*:*`"). */
|
|
143
|
+
declare function formatPhaseAtom(a: PhaseAtom): string;
|
|
144
|
+
/** Visual-collapse pass: given a list of route strings, fold pairs that
|
|
145
|
+
* differ ONLY in `shift` presence (one has `+shift`, the other doesn't,
|
|
146
|
+
* everything else identical) into a single route with `?shift`. Useful
|
|
147
|
+
* for inspector / palette displays that want to show "Cmd+[" and
|
|
148
|
+
* "Cmd+Shift+[" as one Cmd+?Shift+[ chip rather than two rows.
|
|
149
|
+
*
|
|
150
|
+
* Preserves input order — the first member of each fold-pair holds the
|
|
151
|
+
* position; the second is dropped. Routes that don't have a `+shift`
|
|
152
|
+
* vs absent twin pass through unchanged. Does NOT fold other modifier
|
|
153
|
+
* pairs (alt/ctrl/meta/mod) — those tend to carry meaning across the
|
|
154
|
+
* same key (e.g., Cmd+Z vs Cmd+Shift+Z are usually two distinct
|
|
155
|
+
* actions, but Cmd+] vs Cmd+Shift+] is more often parametric variation
|
|
156
|
+
* of one action). Open the helper if a use case for the other
|
|
157
|
+
* modifiers shows up.
|
|
158
|
+
*/
|
|
159
|
+
declare function collapseShiftPairs(routes: readonly string[]): string[];
|
|
160
|
+
declare function formatRoute(r: ParsedRoute): string;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* GestureSpec — describes the form of a user input event that can fire an action.
|
|
164
|
+
*
|
|
165
|
+
* Used by `Action.defaultBinding` (the action's preferred gesture) and by
|
|
166
|
+
* `GestureBinding.spec` (a tool's binding table entry). The dispatcher matches
|
|
167
|
+
* incoming input events against registered specs to determine which action to
|
|
168
|
+
* invoke.
|
|
169
|
+
*
|
|
170
|
+
* See `docs/superpowers/specs/2026-05-16-registry-unification-design.md` § "Types".
|
|
171
|
+
*/
|
|
172
|
+
/** Optional modifier-key requirement for a gesture spec.
|
|
173
|
+
*
|
|
174
|
+
* Matching semantics (strict): an omitted modifier field means the
|
|
175
|
+
* modifier MUST NOT be held — i.e., a bare `{ kind: 'key', key: 'Escape' }`
|
|
176
|
+
* matches only unmodified Escape, NOT Cmd+Escape. A `true` means the
|
|
177
|
+
* modifier MUST be held; `false` is the same as omitted (must be absent).
|
|
178
|
+
* This mirrors today's `KeyBinding` matcher and keeps conflict detection
|
|
179
|
+
* coherent.
|
|
180
|
+
*
|
|
181
|
+
* `mod` is a platform-aware shorthand: matches `metaKey` on mac, `ctrlKey`
|
|
182
|
+
* elsewhere (mirrors `KeyBinding.mod`).
|
|
183
|
+
*
|
|
184
|
+
* `shift` additionally accepts `'optional'` meaning "shifted or unshifted
|
|
185
|
+
* both acceptable" — the explicit opt-in for loose matching, used by
|
|
186
|
+
* actions like nudge whose step size depends on shift but whose firing
|
|
187
|
+
* does not. To widen other modifiers similarly, extend their type when
|
|
188
|
+
* a real consumer needs it.
|
|
189
|
+
*/
|
|
190
|
+
type ModSpec = Partial<{
|
|
191
|
+
alt: boolean | 'optional';
|
|
192
|
+
ctrl: boolean | 'optional';
|
|
193
|
+
meta: boolean | 'optional';
|
|
194
|
+
mod: boolean | 'optional';
|
|
195
|
+
shift: boolean | 'optional';
|
|
196
|
+
}>;
|
|
197
|
+
/** Target selector for click and drag gesture specs. String forms are sugar
|
|
198
|
+
* for the kit-owned object-kind registry (TODO.md Tier 1 follow-up); until
|
|
199
|
+
* that ships, consumers can pass `{ kindOf: predicate }` to classify hits
|
|
200
|
+
* themselves. */
|
|
201
|
+
type TargetSpec = 'empty' | 'selected-body' | 'unselected-body' | `kind:${string}` | `kind:${string}:selected` | `affordance:${string}` | {
|
|
202
|
+
/** Predicate. `hit` is the raw target (affordance for drag,
|
|
203
|
+
* `e.target` otherwise); `bodyTarget` is the optional body-class
|
|
204
|
+
* string ('empty' | 'selected-body' | 'unselected-body') when
|
|
205
|
+
* `classifyTarget` is wired. Predicates that only need one of the
|
|
206
|
+
* two can ignore the other. */
|
|
207
|
+
kindOf: (hit: unknown, bodyTarget?: string) => boolean;
|
|
208
|
+
};
|
|
209
|
+
/** Phase qualifier on a gesture spec. Restricts when the spec matches based
|
|
210
|
+
* on per-tool gesture-lifecycle state.
|
|
211
|
+
*
|
|
212
|
+
* Shorthand forms (most common case — gate on the binding's own tool):
|
|
213
|
+
* `'engaged'` → `[{ channel: '&', phase: 'engaged' }]` // self mid-gesture
|
|
214
|
+
* `'initial'` → `[{ channel: '&', phase: 'initial' }]` // self idle
|
|
215
|
+
* `'*'` → `[{ channel: '&', phase: '*' }]` // either self phase
|
|
216
|
+
*
|
|
217
|
+
* Array form for explicit channel:phase atoms — e.g. `[{ channel: 'rect',
|
|
218
|
+
* phase: 'engaged' }]` for "when the rect tool is mid-gesture, regardless of
|
|
219
|
+
* which scope I'm in." See the v3 route grammar in
|
|
220
|
+
* `@weasel-js/gestures/grammar` for the full lattice.
|
|
221
|
+
*
|
|
222
|
+
* When omitted, matches in any phase (preserves pre-phase behavior). */
|
|
223
|
+
type PhaseSpec = 'initial' | 'engaged' | '*' | readonly PhaseAtom[];
|
|
224
|
+
/** Single-keystroke gesture (keydown). */
|
|
225
|
+
interface KeySpec {
|
|
226
|
+
kind: 'key';
|
|
227
|
+
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
228
|
+
key: string | string[];
|
|
229
|
+
mods?: ModSpec;
|
|
230
|
+
phase?: PhaseSpec;
|
|
231
|
+
}
|
|
232
|
+
/** Key-held gesture (keydown opens, keyup closes). Drives "hold space for
|
|
233
|
+
* hand tool"-style interactions. */
|
|
234
|
+
interface KeyHeldSpec {
|
|
235
|
+
kind: 'key-held';
|
|
236
|
+
/** A single key, or an array of acceptable keys (case-insensitive match). */
|
|
237
|
+
key: string | string[];
|
|
238
|
+
mods?: ModSpec;
|
|
239
|
+
phase?: PhaseSpec;
|
|
240
|
+
}
|
|
241
|
+
/** Wheel-event gesture. `direction` filters by deltaY sign; default `'*'`.
|
|
242
|
+
* - `'up'` → matches only deltaY < 0
|
|
243
|
+
* - `'down'` → matches only deltaY > 0
|
|
244
|
+
* - `'*'` → matches either sign (default; universal-wildcard convention) */
|
|
245
|
+
interface WheelSpec {
|
|
246
|
+
kind: 'wheel';
|
|
247
|
+
direction?: 'up' | 'down' | '*';
|
|
248
|
+
mods?: ModSpec;
|
|
249
|
+
phase?: PhaseSpec;
|
|
250
|
+
}
|
|
251
|
+
/** Click gesture (pointerdown + pointerup without movement past the
|
|
252
|
+
* threshold). */
|
|
253
|
+
interface ClickSpec {
|
|
254
|
+
kind: 'click';
|
|
255
|
+
target?: TargetSpec;
|
|
256
|
+
mods?: ModSpec;
|
|
257
|
+
phase?: PhaseSpec;
|
|
258
|
+
}
|
|
259
|
+
/** Double-click: two `click` events within ~500ms and ~5px of each other.
|
|
260
|
+
* Synthesized by `useGestureDispatcher`; emitted AFTER the second
|
|
261
|
+
* `click`. Bindings that want to handle a double-click should declare
|
|
262
|
+
* this kind rather than chasing two `click` events. */
|
|
263
|
+
interface DoubleClickSpec {
|
|
264
|
+
kind: 'doubleClick';
|
|
265
|
+
target?: TargetSpec;
|
|
266
|
+
mods?: ModSpec;
|
|
267
|
+
phase?: PhaseSpec;
|
|
268
|
+
}
|
|
269
|
+
/** Right-click (contextmenu) gesture. The dispatcher calls
|
|
270
|
+
* `preventDefault()` on the underlying DOM event so the native menu
|
|
271
|
+
* doesn't appear — tools/actions fully own the right-click UX. */
|
|
272
|
+
interface ContextMenuSpec {
|
|
273
|
+
kind: 'contextMenu';
|
|
274
|
+
target?: TargetSpec;
|
|
275
|
+
mods?: ModSpec;
|
|
276
|
+
phase?: PhaseSpec;
|
|
277
|
+
}
|
|
278
|
+
/** Drag gesture (pointerdown + pointermove past the threshold). */
|
|
279
|
+
interface DragSpec {
|
|
280
|
+
kind: 'drag';
|
|
281
|
+
target?: TargetSpec;
|
|
282
|
+
mods?: ModSpec;
|
|
283
|
+
phase?: PhaseSpec;
|
|
284
|
+
}
|
|
285
|
+
/** Multi-touch gesture. `fingers` is the required touch count. */
|
|
286
|
+
interface MultiTouchSpec {
|
|
287
|
+
kind: 'multiTouch';
|
|
288
|
+
fingers: number;
|
|
289
|
+
mods?: ModSpec;
|
|
290
|
+
phase?: PhaseSpec;
|
|
291
|
+
}
|
|
292
|
+
/** Multi-touch tap gesture — fires when N fingers touch down then release
|
|
293
|
+
* together without movement past the tap threshold. Synthesized by the
|
|
294
|
+
* dispatcher from the underlying multitouch tracking. */
|
|
295
|
+
interface MultiTouchTapSpec {
|
|
296
|
+
kind: 'multiTouchTap';
|
|
297
|
+
fingers: number;
|
|
298
|
+
mods?: ModSpec;
|
|
299
|
+
phase?: PhaseSpec;
|
|
300
|
+
}
|
|
301
|
+
/** OS drag-and-drop of external content onto the canvas. `types` filters by
|
|
302
|
+
* MIME glob (`'image/*'`, `'text/plain'`); the spec matches when ANY item's
|
|
303
|
+
* MIME matches ANY glob. Omitted or empty = matches any drop. */
|
|
304
|
+
interface DropSpec {
|
|
305
|
+
kind: 'drop';
|
|
306
|
+
types?: string[];
|
|
307
|
+
mods?: ModSpec;
|
|
308
|
+
phase?: PhaseSpec;
|
|
309
|
+
}
|
|
310
|
+
/** System-clipboard paste of external content. Same `types` semantics as
|
|
311
|
+
* {@link DropSpec} — omitted or empty = matches any paste. */
|
|
312
|
+
interface PasteSpec {
|
|
313
|
+
kind: 'paste';
|
|
314
|
+
types?: string[];
|
|
315
|
+
mods?: ModSpec;
|
|
316
|
+
phase?: PhaseSpec;
|
|
317
|
+
}
|
|
318
|
+
/** The full union of supported gesture spec kinds. New invocation forms
|
|
319
|
+
* (long-press, two-stage, modal-dialog) extend this union without touching
|
|
320
|
+
* the `Action` type. */
|
|
321
|
+
type GestureSpec = KeySpec | KeyHeldSpec | WheelSpec | ClickSpec | DoubleClickSpec | ContextMenuSpec | DragSpec | MultiTouchSpec | MultiTouchTapSpec | DropSpec | PasteSpec;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Mini-grammar for the `arg` slot of `keyDown` / `keyUp` routes.
|
|
325
|
+
*
|
|
326
|
+
* keyRoute = key ('?' optionalMod)*
|
|
327
|
+
* optionalMod = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta'
|
|
328
|
+
*
|
|
329
|
+
* `?shift` means "shift may or may not be held; either fires this route."
|
|
330
|
+
* Required modifiers still belong in the route's `:modifiers` slot — this
|
|
331
|
+
* grammar only widens which events match, never narrows.
|
|
332
|
+
*/
|
|
333
|
+
|
|
334
|
+
/** An optional modifier in a key route — the same set as the canonical
|
|
335
|
+
* {@link ModifierKey}. */
|
|
336
|
+
type OptionalMod = ModifierKey;
|
|
337
|
+
interface ParsedKeyRoute {
|
|
338
|
+
key: string;
|
|
339
|
+
optionalMods: readonly OptionalMod[];
|
|
340
|
+
}
|
|
341
|
+
declare function parseKeyRoute(input: string): ParsedKeyRoute;
|
|
342
|
+
declare function formatKeyRoute(r: ParsedKeyRoute): string;
|
|
343
|
+
/** Build a runtime KeySpec from a parsed key route. Each optional modifier
|
|
344
|
+
* becomes `mods.<name>: 'optional'`. The matcher's `matchModifiers`
|
|
345
|
+
* honors `'optional'` uniformly across `mod`, `shift`, `alt`, `ctrl`,
|
|
346
|
+
* and `meta`. */
|
|
347
|
+
declare function keyRouteToSpec(r: ParsedKeyRoute): KeySpec;
|
|
348
|
+
|
|
349
|
+
/** Glossary for route-vocabulary terms surfaced by `describeRouteParts`.
|
|
350
|
+
* Exposed so docs / inspector UIs can render the same definitions. */
|
|
351
|
+
declare const ROUTE_TERMS: {
|
|
352
|
+
readonly idle: "Not in the middle of a drag or other synchronous operation.";
|
|
353
|
+
readonly 'mid-gesture': "In the middle of a drag or other synchronous operation.";
|
|
354
|
+
};
|
|
355
|
+
/** Per-field explanations for the slots of a parsed route. Exposed so
|
|
356
|
+
* inspector UIs / docs can describe what each setting in a route table
|
|
357
|
+
* means without duplicating prose. */
|
|
358
|
+
declare const ROUTE_FIELD_DEFINITIONS: {
|
|
359
|
+
readonly phases: "Which lifecycle stage(s) a channel must be in for this route to fire. `initial` = the channel is idle; `engaged` = the channel is mid-gesture. The channel is the binding's own tool (`&`), any tool (`*`), or a named tool id.";
|
|
360
|
+
readonly gesture: "The class of input event that triggers this route — click, drag, double-tap, keyDown/keyUp, wheel, contextMenu, or multiTouchTap.";
|
|
361
|
+
readonly arg: "Sub-class of the gesture. Direction for wheel (up / down / *), key name for keyDown / keyUp, finger count for multiTouchTap. Other gestures have no arg slot.";
|
|
362
|
+
readonly target: "Which hit-test result the gesture must land on. `*` matches any target; `empty` matches the empty canvas; otherwise the value names a specific hit-target kind.";
|
|
363
|
+
readonly modifiers: "Modifier keys the user must hold (`+key`) or may optionally hold (`?key`) for this route to match. Unlisted modifiers must not be held.";
|
|
364
|
+
};
|
|
365
|
+
type RouteFieldName = keyof typeof ROUTE_FIELD_DEFINITIONS;
|
|
366
|
+
type RouteTermLabel = keyof typeof ROUTE_TERMS;
|
|
367
|
+
/** A part of a structured route description. `string` parts are plain prose;
|
|
368
|
+
* `term` parts carry a glossary lookup so renderers can attach tooltips. */
|
|
369
|
+
type RouteDescriptionPart = string | {
|
|
370
|
+
kind: 'term';
|
|
371
|
+
label: RouteTermLabel;
|
|
372
|
+
definition: string;
|
|
373
|
+
};
|
|
374
|
+
interface DescribeRouteOptions {
|
|
375
|
+
/** Capitalize the first letter of the result. Default true. */
|
|
376
|
+
capitalize?: boolean;
|
|
377
|
+
/** Trailing period. Default true. */
|
|
378
|
+
period?: boolean;
|
|
379
|
+
}
|
|
380
|
+
/** Structured description of when a parsed route fires. Returns an array of
|
|
381
|
+
* prose strings interspersed with `term` parts (e.g. `idle`, `mid-gesture`)
|
|
382
|
+
* so React-based renderers can attach hover-definition tooltips. Use
|
|
383
|
+
* `describeRoute()` for a plain-string version. */
|
|
384
|
+
declare function describeRouteParts(parsed: ParsedRoute, opts?: DescribeRouteOptions): readonly RouteDescriptionPart[];
|
|
385
|
+
/** Plain-spoken English summary of when a parsed route fires. Intended for
|
|
386
|
+
* developer-facing documentation. Term parts collapse to their label only
|
|
387
|
+
* (e.g. `idle`); use `describeRouteParts()` if you want to expose the
|
|
388
|
+
* glossary definitions in your renderer. */
|
|
389
|
+
declare function describeRoute(parsed: ParsedRoute, opts?: DescribeRouteOptions): string;
|
|
390
|
+
|
|
391
|
+
/** Translate the tool-authoring `ModifierCombo` enum (positional string,
|
|
392
|
+
* e.g. `'mod+shift'`) to the v3 `ParsedModifiers` structured map
|
|
393
|
+
* (every listed modifier becomes `'required'`; unlisted means
|
|
394
|
+
* forbidden / absent). `'default'` yields `{}`. */
|
|
395
|
+
declare function modifierComboToParsed(key: ModifierCombo): ParsedModifiers;
|
|
396
|
+
/** Stable canonical serialization of a ParsedModifiers map for use as a
|
|
397
|
+
* hash key (conflicts dedup). Sorts mod names alphabetically and emits
|
|
398
|
+
* `name=req` pairs joined by `&`. */
|
|
399
|
+
declare function canonicalModifiers(mods: ParsedModifiers): string;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Keyboard/pointer modifier flags carried by every {@link InputEvent} arm.
|
|
403
|
+
* Factored out so the four booleans are documented once instead of repeated
|
|
404
|
+
* on all twelve event shapes.
|
|
405
|
+
*/
|
|
406
|
+
interface EventModifiers {
|
|
407
|
+
altKey: boolean;
|
|
408
|
+
ctrlKey: boolean;
|
|
409
|
+
metaKey: boolean;
|
|
410
|
+
shiftKey: boolean;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Body-target classification from the scene hit-test. Populated by
|
|
414
|
+
* `useGestureDispatcher` when a `classifyTarget` thunk is supplied to its
|
|
415
|
+
* options, and read by `matchTarget` to resolve string-form `TargetSpec`
|
|
416
|
+
* values. Absent when `classifyTarget` is not wired.
|
|
417
|
+
*/
|
|
418
|
+
type BodyTarget = 'empty' | 'selected-body' | 'unselected-body';
|
|
419
|
+
/** A keystroke. */
|
|
420
|
+
interface KeyEvent extends EventModifiers {
|
|
421
|
+
kind: 'key';
|
|
422
|
+
key: string;
|
|
423
|
+
repeat?: boolean;
|
|
424
|
+
}
|
|
425
|
+
/** A held key transitioning down or up (e.g. space-to-pan). */
|
|
426
|
+
interface KeyHeldEvent extends EventModifiers {
|
|
427
|
+
kind: 'key-held';
|
|
428
|
+
key: string;
|
|
429
|
+
phase: 'down' | 'up';
|
|
430
|
+
}
|
|
431
|
+
/** A scroll-wheel / trackpad scroll, with raw deltas and client coords. */
|
|
432
|
+
interface WheelEvent extends EventModifiers {
|
|
433
|
+
kind: 'wheel';
|
|
434
|
+
deltaX: number;
|
|
435
|
+
deltaY: number;
|
|
436
|
+
clientX: number;
|
|
437
|
+
clientY: number;
|
|
438
|
+
}
|
|
439
|
+
/** A pointer press — the start of any drag, and the richest event shape. */
|
|
440
|
+
interface PointerDownEvent extends EventModifiers {
|
|
441
|
+
kind: 'pointerdown';
|
|
442
|
+
target?: unknown;
|
|
443
|
+
/** World-space coordinates (post view transform). */
|
|
444
|
+
x?: number;
|
|
445
|
+
y?: number;
|
|
446
|
+
/**
|
|
447
|
+
* Client/screen-space coordinates (raw DOM event). Required for any drag
|
|
448
|
+
* action whose effect mutates the view itself — world coords shift
|
|
449
|
+
* mid-gesture and produce self-referential deltas.
|
|
450
|
+
*/
|
|
451
|
+
clientX?: number;
|
|
452
|
+
clientY?: number;
|
|
453
|
+
/** Generic affordance payload (the kit narrows this to `AffordanceHit`). */
|
|
454
|
+
affordance?: unknown;
|
|
455
|
+
bodyTarget?: BodyTarget;
|
|
456
|
+
}
|
|
457
|
+
/** A pump-only pointer move. Carried in the union so the dispatcher's
|
|
458
|
+
* `handleInput` signature stays uniform; the matcher never matches it. */
|
|
459
|
+
interface PointerMoveEvent extends EventModifiers {
|
|
460
|
+
kind: 'pointermove';
|
|
461
|
+
x: number;
|
|
462
|
+
y: number;
|
|
463
|
+
clientX?: number;
|
|
464
|
+
clientY?: number;
|
|
465
|
+
}
|
|
466
|
+
/** A pump-only pointer release. Not matched by the matcher. */
|
|
467
|
+
interface PointerUpEvent extends EventModifiers {
|
|
468
|
+
kind: 'pointerup';
|
|
469
|
+
x: number;
|
|
470
|
+
y: number;
|
|
471
|
+
clientX?: number;
|
|
472
|
+
clientY?: number;
|
|
473
|
+
}
|
|
474
|
+
/** A pump-only pointer cancel. Not matched by the matcher. */
|
|
475
|
+
interface PointerCancelEvent extends EventModifiers {
|
|
476
|
+
kind: 'pointercancel';
|
|
477
|
+
}
|
|
478
|
+
/** A click (down+up on the same target). */
|
|
479
|
+
interface ClickEvent extends EventModifiers {
|
|
480
|
+
kind: 'click';
|
|
481
|
+
target?: unknown;
|
|
482
|
+
/**
|
|
483
|
+
* World-space coordinates of the click, derived via the `clientToWorld`
|
|
484
|
+
* thunk supplied to the dispatcher. Absent when the thunk isn't wired.
|
|
485
|
+
* Forwarded into action params so click invokers can act on the click's
|
|
486
|
+
* position without their own pointer-listener plumbing.
|
|
487
|
+
*/
|
|
488
|
+
worldX?: number;
|
|
489
|
+
worldY?: number;
|
|
490
|
+
bodyTarget?: BodyTarget;
|
|
491
|
+
}
|
|
492
|
+
/** A double click. `worldX`/`worldY` carry the same meaning as on {@link ClickEvent}. */
|
|
493
|
+
interface DoubleClickEvent extends EventModifiers {
|
|
494
|
+
kind: 'doubleclick';
|
|
495
|
+
target?: unknown;
|
|
496
|
+
worldX?: number;
|
|
497
|
+
worldY?: number;
|
|
498
|
+
bodyTarget?: BodyTarget;
|
|
499
|
+
}
|
|
500
|
+
/** A context-menu (right-click) request. */
|
|
501
|
+
interface ContextMenuEvent extends EventModifiers {
|
|
502
|
+
kind: 'contextmenu';
|
|
503
|
+
target?: unknown;
|
|
504
|
+
bodyTarget?: BodyTarget;
|
|
505
|
+
}
|
|
506
|
+
/** A running multitouch gesture (e.g. pinch/rotate). */
|
|
507
|
+
interface MultitouchEvent extends EventModifiers {
|
|
508
|
+
kind: 'multitouch';
|
|
509
|
+
fingers: number;
|
|
510
|
+
/**
|
|
511
|
+
* Centroid of active pointers in screen space. Populated by
|
|
512
|
+
* `useGestureDispatcher` on the pointermove-pump of a running multitouch
|
|
513
|
+
* handle (updated each frame). Absent on the initial pointerdown-triggered
|
|
514
|
+
* multitouch event.
|
|
515
|
+
*/
|
|
516
|
+
centroid?: {
|
|
517
|
+
x: number;
|
|
518
|
+
y: number;
|
|
519
|
+
};
|
|
520
|
+
/**
|
|
521
|
+
* Distance between the two primary pointers (screen space). Populated on
|
|
522
|
+
* move-pump events alongside `centroid`. Absent on the initial event.
|
|
523
|
+
*/
|
|
524
|
+
spread?: number;
|
|
525
|
+
}
|
|
526
|
+
/** A multi-finger tap (no drag). */
|
|
527
|
+
interface MultitouchTapEvent extends EventModifiers {
|
|
528
|
+
kind: 'multitouchtap';
|
|
529
|
+
fingers: number;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* One piece of external content arriving via OS drop, clipboard paste, or a
|
|
533
|
+
* file picker — fully materialized (string contents already read), so it is
|
|
534
|
+
* safe to hold past the originating DOM event. `File` is a lib.dom type;
|
|
535
|
+
* no runtime DOM dependency.
|
|
536
|
+
*/
|
|
537
|
+
type IngestItem = {
|
|
538
|
+
kind: 'file';
|
|
539
|
+
mime: string;
|
|
540
|
+
file: File;
|
|
541
|
+
} | {
|
|
542
|
+
kind: 'string';
|
|
543
|
+
mime: string;
|
|
544
|
+
text: string;
|
|
545
|
+
};
|
|
546
|
+
/** External content dropped onto the canvas (OS drag-and-drop). */
|
|
547
|
+
interface DropEvent extends EventModifiers {
|
|
548
|
+
kind: 'drop';
|
|
549
|
+
items: readonly IngestItem[];
|
|
550
|
+
/** World-space drop point (post view transform). */
|
|
551
|
+
x?: number;
|
|
552
|
+
y?: number;
|
|
553
|
+
clientX?: number;
|
|
554
|
+
clientY?: number;
|
|
555
|
+
}
|
|
556
|
+
/** External content pasted from the system clipboard. Carries no point. */
|
|
557
|
+
interface PasteEvent extends EventModifiers {
|
|
558
|
+
kind: 'paste';
|
|
559
|
+
items: readonly IngestItem[];
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Normalized input-event shape consumed by the pure matcher. Built by the
|
|
563
|
+
* React seam (`useGestureDispatcher`) from DOM events. Pump-only events
|
|
564
|
+
* ({@link PointerMoveEvent}, {@link PointerUpEvent}, {@link PointerCancelEvent})
|
|
565
|
+
* ride in the same union so the dispatcher's `handleInput` signature stays
|
|
566
|
+
* uniform; the matcher itself never matches them.
|
|
567
|
+
*
|
|
568
|
+
* The `affordance?` field on {@link PointerDownEvent} is widened to `unknown`
|
|
569
|
+
* in this package — the kit narrows it back to `AffordanceHit` at consumption.
|
|
570
|
+
* This keeps `@weasel-js/gestures` free of any kit-affordance type.
|
|
571
|
+
*/
|
|
572
|
+
type InputEvent = KeyEvent | KeyHeldEvent | WheelEvent | PointerDownEvent | PointerMoveEvent | PointerUpEvent | PointerCancelEvent | ClickEvent | DoubleClickEvent | ContextMenuEvent | MultitouchEvent | MultitouchTapEvent | DropEvent | PasteEvent;
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Pure gesture matcher — no React, no state, no DOM.
|
|
576
|
+
*
|
|
577
|
+
* The kit's dispatcher orchestrator wraps these primitives with its
|
|
578
|
+
* actions-layer concerns (BindingScope, ScopedBinding, MatchResult, matchBest).
|
|
579
|
+
*
|
|
580
|
+
* ## key-held phase decision
|
|
581
|
+
* `matchSpec` returns `true` only for `phase: 'down'`, not `phase: 'up'`.
|
|
582
|
+
* The dispatcher tracks the held key independently and handles the `up` phase
|
|
583
|
+
* itself; the matcher never sees an "un-match" it needs to distinguish.
|
|
584
|
+
* This keeps the matcher's output a simple boolean and avoids a three-valued
|
|
585
|
+
* return type.
|
|
586
|
+
*
|
|
587
|
+
* ## TargetSpec string-form decision
|
|
588
|
+
* String-form TargetSpec values ('empty', 'selected-body', `kind:*`, etc.) are
|
|
589
|
+
* sugar for the kit-owned object-kind registry, which hasn't shipped yet
|
|
590
|
+
* (see docs/TODO.md Tier 1). Phase 3 returns `false` for any string-form
|
|
591
|
+
* target spec so that callers get an explicit no-match rather than a silent
|
|
592
|
+
* wildcard or a runtime throw.
|
|
593
|
+
*/
|
|
594
|
+
|
|
595
|
+
type ModifiersEvent = {
|
|
596
|
+
altKey: boolean;
|
|
597
|
+
ctrlKey: boolean;
|
|
598
|
+
metaKey: boolean;
|
|
599
|
+
shiftKey: boolean;
|
|
600
|
+
};
|
|
601
|
+
/**
|
|
602
|
+
* Strict modifier match.
|
|
603
|
+
*
|
|
604
|
+
* - Omitted modifier MUST NOT be held.
|
|
605
|
+
* - `true` MUST be held.
|
|
606
|
+
* - `false` MUST NOT be held (same as omitted; both forms accepted for clarity).
|
|
607
|
+
* - `'optional'` accepts either held or unheld. Supported on every modifier
|
|
608
|
+
* (`alt`, `ctrl`, `meta`, `mod`, `shift`).
|
|
609
|
+
* - `mod` is platform-aware: matches `metaKey` on mac, `ctrlKey` elsewhere.
|
|
610
|
+
* When `mod` is set, the corresponding `meta`/`ctrl` field is implied AND
|
|
611
|
+
* the *other* platform key is forbidden. Callers should not combine `mod`
|
|
612
|
+
* with `meta`/`ctrl`.
|
|
613
|
+
*/
|
|
614
|
+
declare function matchModifiers(e: ModifiersEvent, mods: ModSpec | undefined, isMac: boolean): boolean;
|
|
615
|
+
/** Case-insensitive key match; supports string or string[] (any-of). */
|
|
616
|
+
declare function matchKey(eventKey: string, specKey: string | string[]): boolean;
|
|
617
|
+
/**
|
|
618
|
+
* Match a target value + event against a TargetSpec.
|
|
619
|
+
*
|
|
620
|
+
* - `{ kindOf: predicate }` — calls the predicate with the raw target value
|
|
621
|
+
* (affordance hit on pointerdown; event.target otherwise) AND the
|
|
622
|
+
* `bodyTarget` string. Predicates that only need the target can ignore
|
|
623
|
+
* the second arg.
|
|
624
|
+
* - `'empty'`, `'selected-body'`, `'unselected-body'` — compared against
|
|
625
|
+
* `bodyTarget` on the event (populated by `useGestureDispatcher` when a
|
|
626
|
+
* `classifyTarget` thunk is supplied). Falls back to `false` when absent
|
|
627
|
+
* (kind registry not yet wired).
|
|
628
|
+
* - Other string-forms (`kind:*`, `affordance:*`) — not yet supported; return false.
|
|
629
|
+
* - `undefined` spec.target — any target is accepted.
|
|
630
|
+
*/
|
|
631
|
+
declare function matchTarget(target: unknown, specTarget: unknown, bodyTarget?: string): boolean;
|
|
632
|
+
/** Per-tool gesture-lifecycle state at match time.
|
|
633
|
+
*
|
|
634
|
+
* - `selfChannel` is the tool id `'&'` resolves to — i.e., the tool that
|
|
635
|
+
* owns the binding being evaluated. `null` for bindings without an
|
|
636
|
+
* owning tool (e.g. ambient actions registered without scope ties); in
|
|
637
|
+
* that case any `phase: '&'` atom can't match.
|
|
638
|
+
* - `engagedChannels` is the set of tool ids that currently have an
|
|
639
|
+
* in-flight handle. Derived by the dispatcher from its `inFlight()`
|
|
640
|
+
* map keyed by tool id at match time.
|
|
641
|
+
*/
|
|
642
|
+
interface PhaseContext {
|
|
643
|
+
selfChannel: string | null;
|
|
644
|
+
engagedChannels: ReadonlySet<string>;
|
|
645
|
+
}
|
|
646
|
+
/** True if the binding's `phase` spec is satisfied by the current
|
|
647
|
+
* per-tool engagement state. Omitted spec → always true (no phase
|
|
648
|
+
* constraint). Union semantics: a phase atom list matches when ANY
|
|
649
|
+
* atom matches. */
|
|
650
|
+
declare function matchPhase(spec: PhaseSpec | undefined, ctx: PhaseContext): boolean;
|
|
651
|
+
/** MIME-glob match: `'image/*'` prefix-matches the major type; anything
|
|
652
|
+
* else is an exact (case-insensitive) match; bare `'*'` or `'*\/*'` matches all. */
|
|
653
|
+
declare function mimeMatchesGlob(mime: string, glob: string): boolean;
|
|
654
|
+
/** True if ANY item's MIME matches ANY of the `types` globs.
|
|
655
|
+
* `types: []` (empty) ≡ omitted = match any. */
|
|
656
|
+
declare function matchIngestTypes(items: readonly {
|
|
657
|
+
mime: string;
|
|
658
|
+
}[], types: string[] | undefined): boolean;
|
|
659
|
+
/**
|
|
660
|
+
* Match a single GestureSpec against an InputEvent.
|
|
661
|
+
*
|
|
662
|
+
* For `key-held`, only `phase: 'down'` returns true. The dispatcher tracks the
|
|
663
|
+
* held state independently and handles `phase: 'up'` itself.
|
|
664
|
+
*
|
|
665
|
+
* For TargetSpec string forms, returns false (kind registry not yet available).
|
|
666
|
+
*/
|
|
667
|
+
declare function matchSpec(e: InputEvent, spec: GestureSpec, isMac: boolean, phaseCtx?: PhaseContext): boolean;
|
|
668
|
+
|
|
669
|
+
export { ACTIVE_SIGILS, type BodyTarget, type ChannelRef, type ClickEvent, type ClickSpec, type ContextMenuEvent, type ContextMenuSpec, type DescribeRouteOptions, type DoubleClickEvent, type DoubleClickSpec, type DragSpec, type DropEvent, type DropSpec, type EventModifiers, GESTURE_DESCRIPTORS, type GestureArgSpec, type GestureDescriptor, type GestureName, type GestureSpec, type IngestItem, type InputEvent, type KeyEvent, type KeyHeldEvent, type KeyHeldSpec, type KeySpec, MOD_NAME_SET, type ModRequirement, type ModSpec, type ModifierCombo, type ModifierKey, type ModifiersEvent, type MultiTouchSpec, type MultiTouchTapSpec, type MultitouchEvent, type MultitouchTapEvent, type OptionalMod, type ParsedKeyRoute, type ParsedModifiers, type ParsedRoute, type PasteEvent, type PasteSpec, type PhaseAtom, type PhaseContext, type PhaseSpec, type PointerCancelEvent, type PointerDownEvent, type PointerMoveEvent, type PointerUpEvent, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, type RouteDescriptionPart, type RouteFieldName, type RoutePhase, type RouteTermLabel, type TargetSpec, VALID_MOD_NAMES, type WheelEvent, type WheelSpec, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, keyRouteToSpec, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, parseKeyRoute, parseRoute };
|