@weasel-js/core 0.7.0 → 0.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.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/tools/routing/index.ts","../src/tools/routing/defineTool.ts","../src/tools/routing/defineViewportTool.ts","../src/tools/routing/reflection/registry.ts","../src/tools/routing/reflection/conflicts.ts"],"names":[],"mappings":"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","file":"chunk-FSZEXVCR.js","sourcesContent":["export type { ToolDef, ViewportToolDef, ToolKeybinding } from './types';\nexport { parseRoute, formatRoute, formatPhaseAtom, collapseShiftPairs, describeRoute, describeRouteParts, canonicalModifiers, ROUTE_TERMS, ROUTE_FIELD_DEFINITIONS, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES } from './routeGrammar';\nexport type { ParsedRoute, ParsedModifiers, ModifierKey, ModRequirement, PhaseAtom, ChannelRef, DescribeRouteOptions, RouteDescriptionPart, RouteTermLabel, RouteFieldName } from './routeGrammar';\nexport { getGestureDescriptor, isKnownGestureName, GESTURE_DESCRIPTORS } from './gestures';\nexport type { GestureName, GestureDescriptor, GestureArgSpec } from './gestures';\nexport { defineTool } from './defineTool';\nexport { defineViewportTool } from './defineViewportTool';\n\n// Reflection consumers — registry / conflict checker / debug overlay.\nexport * from './reflection';\n","// src/tools/routing/defineTool.ts\nimport type { Tool, ToolCtx } from '../types';\nimport type { ToolDef } from './types';\nimport { RESERVED_ID_NAMES, RESERVED_ID_PREFIXES } from './routeGrammar';\n\n/** Validate that `id` is usable as a tool / channel id in the route\n * grammar. Rejects ids that start with a reserved sigil (would shadow\n * future grammar extensions) and ids that collide with phase keywords\n * (would parse as the bare-phase shorthand). */\nfunction validateId(id: string, kind: 'tool' | 'action'): void {\n if (id.length === 0) {\n throw new Error(`weasel: ${kind} id may not be empty`);\n }\n if (RESERVED_ID_PREFIXES.has(id[0]!)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" starts with reserved sigil \"${id[0]}\" ` +\n `(reserved set: ${[...RESERVED_ID_PREFIXES].join(' ')})`,\n );\n }\n if (RESERVED_ID_NAMES.has(id)) {\n throw new Error(\n `weasel: ${kind} id \"${id}\" collides with a reserved phase keyword ` +\n `(reserved: ${[...RESERVED_ID_NAMES].join(', ')})`,\n );\n }\n}\n\n/**\n * Build a `Tool<TScratch>` from a declarative `ToolDef<TScratch>`.\n *\n * This used to be a translator: `ToolDef` carried `initial` / `engaged` phase\n * tables of hit-keyed route handlers, and `defineTool` compiled them into the\n * imperative `pointer` / `drag` / `keyboard` / `wheel` handlers the\n * tool-routing dispatcher called. Both ends of that are gone — tools declare\n * `bindings` and the gesture dispatcher routes them — so what remains is\n * identity plumbing plus the id check and the cursor resolver.\n *\n * It stays a function rather than becoming a spread because the id validation\n * and the `initScratch` / `cursor` defaults are worth applying uniformly, and\n * because `Tool.def` gives reflection a handle on the authored form.\n */\nexport function defineTool<TScratch = void>(\n def: ToolDef<TScratch>,\n): Tool<TScratch> {\n validateId(def.id, 'tool');\n\n // Normalize `string | ((ctx) => string)` to the function form so callers\n // have one shape to deal with. Returns '' (not undefined) so the signature\n // satisfies `Tool.cursor: (ctx) => string`.\n const resolveCursor = (ctx: ToolCtx<TScratch>): string => {\n if (def.cursor == null) return '';\n return typeof def.cursor === 'function' ? def.cursor(ctx) : def.cursor;\n };\n\n return {\n id: def.id,\n capabilities: def.capabilities,\n actions: def.actions,\n def,\n presentation: def.presentation,\n keybinding: def.keybinding,\n onActivate: def.onActivate,\n onDeactivate: def.onDeactivate,\n initScratch: def.initScratch ?? (() => null as unknown as TScratch),\n cursor: resolveCursor,\n bindings: def.bindings,\n overlay: def.overlay,\n };\n}\n","import type { Tool } from '../types';\nimport type { ViewportToolDef } from './types';\nimport { defineTool } from './defineTool';\n\n/**\n * Define a tool that acts on the viewport rather than the scene.\n *\n * This used to do real work: `ViewportPhaseDef` was a narrowed `PhaseDef`\n * (no click routes, drag restricted to the function form), and the factory\n * lifted it back to the permissive shape before handing it to `defineTool`.\n * With phase tables gone there is no shape left to narrow — a viewport tool\n * declares `bindings` like any other, pointing at `viewport.*` actions.\n *\n * It survives as an authoring signal. `defineViewportTool` at the top of a\n * hook says \"this tool moves the camera, not the drawing\", which is worth\n * more than the type gymnastics it replaced.\n */\nexport function defineViewportTool<TScratch = void>(\n def: ViewportToolDef<TScratch>,\n): Tool<TScratch> {\n return defineTool<TScratch>(def);\n}\n","import type { Tool } from '../../types';\nimport type { GestureSpec, ModSpec, TargetSpec, PhaseSpec } from '@weasel-js/gestures';\nimport type { ParsedModifiers, ModifierKey } from '../routeGrammar';\nimport { getGestureDescriptor, isKnownGestureName, type GestureName } from '../gestures';\n\n/**\n * One row in the route registry — a single `GestureBinding` on one tool,\n * flattened into the route grammar's vocabulary so the inspector, the\n * conflict checker, and `describeRoute` can all read the same shape.\n *\n * Multiple rows can share (gesture, arg, target, modifiers) if different tools\n * declare them; consumers walk the list and group client-side.\n */\nexport interface RegistryEntry {\n toolId: string;\n /** Which phase the binding's `phase` spec restricts it to. `'any'` when it\n * declares none, declares `'*'`, or declares an atom list whose atoms\n * don't agree on one phase — such a binding fires in either phase, and\n * reporting it as `'initial'` made it collide with genuinely-initial\n * bindings in `findConflicts`' bucket key. */\n phase: 'initial' | 'engaged' | 'any';\n /** Structured v3 modifier requirements. Empty object = \"no modifiers\n * held\" (the strict default). */\n modifiers: ParsedModifiers;\n gesture: GestureName;\n /** Resolved arg value for arg-bearing gestures (wheel direction,\n * key name, multiTouchTap fingers). Undefined for gestures whose\n * descriptor has no `arg`. */\n arg: string | undefined;\n /** Target class for hit-testing gestures. `undefined` when the descriptor\n * has `hasTarget: false` or the spec declares no target;\n * {@link PREDICATE_TARGET} when the spec uses a `kindOf` predicate, which\n * the route grammar has no notation for. */\n target: string | undefined;\n /** The action this binding fires. */\n actionId: string;\n /** The `GestureSpec` this row was flattened from, by reference. Reflection\n * consumers that need something the grammar doesn't capture — the\n * specificity tuple, a `kindOf` predicate identity — read it here rather\n * than re-walking `Tool.bindings`. */\n spec: GestureSpec;\n}\n\n/**\n * Stand-in target for a `{ kindOf }` predicate spec.\n *\n * The route grammar can name target *classes* (`empty`, `selected-body`,\n * `kind:rect`) but not arbitrary predicates, so three distinct select-tool\n * bindings all render as this one token. Narrowing it would mean giving the\n * grammar a way to describe a function, which is a real design question and\n * not one this reflection layer should answer by inventing syntax.\n */\nexport const PREDICATE_TARGET = 'predicate';\n\n/** `GestureSpec.kind` → route-grammar gesture name. `multiTouch` has no\n * route-grammar gesture (only its tap synthesis does), and `drop` / `paste`\n * shipped without grammar names, so specs of those kinds are skipped. */\nconst SPEC_KIND_TO_GESTURE: Record<GestureSpec['kind'], GestureName | undefined> = {\n key: 'keyDown',\n 'key-held': 'keyHeld',\n wheel: 'wheel',\n click: 'click',\n doubleClick: 'dblTap',\n contextMenu: 'contextMenu',\n drag: 'drag',\n pointerDown: 'pointerDown',\n multiTouch: undefined,\n multiTouchTap: 'multiTouchTap',\n drop: undefined,\n paste: undefined,\n};\n\n/**\n * Flatten every tool's `bindings` into route-registry rows.\n *\n * This was `buildActionRegistry`, and it walked `ToolDef.initial` /\n * `.engaged` phase tables — the grammar that no longer exists. It was also\n * blind to `Tool.bindings` the entire time the two lived side by side, which\n * is why the inspector under-reported select by more than half. The rename\n * fixes a second thing: it never had anything to do with the Actions\n * Registry, and reading `buildActionRegistry` next to `ActionsRegistry`\n * suggested otherwise.\n */\nexport function buildRouteRegistry(\n tools: readonly Tool<unknown>[],\n): RegistryEntry[] {\n const out: RegistryEntry[] = [];\n for (const tool of tools) {\n for (const binding of tool.bindings ?? []) {\n const entry = entryFor(tool.id, binding.spec, binding.actionId);\n if (entry) out.push(entry);\n }\n }\n return out;\n}\n\nfunction entryFor(\n toolId: string,\n spec: GestureSpec,\n actionId: string,\n): RegistryEntry | null {\n const gesture = SPEC_KIND_TO_GESTURE[spec.kind];\n if (!gesture || !isKnownGestureName(gesture)) return null;\n const descriptor = getGestureDescriptor(gesture);\n return {\n toolId,\n actionId,\n gesture,\n phase: phaseOf('phase' in spec ? spec.phase : undefined),\n modifiers: parseModSpec('mods' in spec ? spec.mods : undefined),\n arg: descriptor.arg ? argOf(spec, descriptor.arg.default) : undefined,\n target: descriptor.hasTarget\n ? targetOf('target' in spec ? spec.target : undefined)\n : undefined,\n spec,\n };\n}\n\n/** Collapse a `PhaseSpec` to the single phase a binding is restricted to, or\n * `'any'` when it isn't restricted to one.\n *\n * `PhaseAtom.channel` says which channel's phase state the binding reads,\n * not which phase it fires in, so it plays no part in this collapse — only\n * the set of distinct `atom.phase` values matters. */\nfunction phaseOf(phase: PhaseSpec | undefined): 'initial' | 'engaged' | 'any' {\n if (phase === undefined || phase === '*') return 'any';\n if (phase === 'initial' || phase === 'engaged') return phase;\n const distinct = new Set(phase.map((atom) => atom.phase));\n if (distinct.size !== 1) return 'any';\n const only = [...distinct][0];\n return only === 'initial' || only === 'engaged' ? only : 'any';\n}\n\n/** `ModSpec` (per-key tri-state) → `ParsedModifiers`. `false` and absent both\n * mean \"must not be held\", which the parsed form spells as an absent key. */\nfunction parseModSpec(mods: ModSpec | undefined): ParsedModifiers {\n if (!mods) return {};\n const out: ParsedModifiers = {};\n for (const [name, req] of Object.entries(mods)) {\n if (req === true) out[name as ModifierKey] = 'required';\n else if (req === 'optional') out[name as ModifierKey] = 'optional';\n }\n return out;\n}\n\nfunction targetOf(target: TargetSpec | undefined): string | undefined {\n if (target === undefined) return undefined;\n return typeof target === 'string' ? target : PREDICATE_TARGET;\n}\n\nfunction argOf(spec: GestureSpec, fallback: string | undefined): string | undefined {\n if ('key' in spec) {\n return Array.isArray(spec.key) ? spec.key.join('|') : spec.key;\n }\n if ('fingers' in spec) return String(spec.fingers);\n if ('direction' in spec) return spec.direction ?? fallback;\n return fallback;\n}\n\n// Re-export for downstream consumers.\nexport { getGestureDescriptor };\nexport type { GestureName };\n","import type { Tool } from '../../types';\nimport type { ParsedModifiers } from '../routeGrammar';\nimport { canonicalModifiers } from '../routeGrammar';\nimport { buildRouteRegistry, type RegistryEntry, type GestureName } from './registry';\n\n/** Two or more tools declare the same exact (phase, gesture, arg, target,\n * modifiers) tuple — the dispatcher's slot precedence picks one\n * arbitrarily (well, deterministically by slot order, but the author\n * probably didn't intend the duplication). */\nexport interface Conflict {\n phase: 'initial' | 'engaged' | 'any';\n gesture: GestureName;\n arg: string | undefined;\n target: string | undefined;\n modifiers: ParsedModifiers;\n /** All tool ids that registered the same tuple. At least 2 by\n * construction. Order matches the input tools[] order. */\n toolIds: string[];\n}\n\n/** Detect exact-tuple overlaps across a tool registration set.\n *\n * Intentionally NOT flagged:\n * - Broad vs. narrow targets (e.g. an untargeted `click` alongside\n * `click` on `empty`) — the dispatcher's specificity ordering resolves\n * those cleanly, and the broad one is usually the intended fallback.\n * - Different modifier requirements on the same target — they fire on\n * different inputs.\n * - A binding whose action declines via `enabled()` so a lower-priority\n * one can take the gesture. Detecting that intent would mean evaluating\n * the action; consumers can suppress known-intentional compositions in\n * their UI layer.\n *\n * Note that two bindings sharing a tuple on the SAME tool are now possible\n * (bindings are an array, where phase tables were objects with unique\n * keys) — so a conflict may name one tool twice.\n */\nexport function findConflicts(\n tools: readonly Tool<unknown>[],\n): Conflict[] {\n const entries = buildRouteRegistry(tools);\n const groups = new Map<string, RegistryEntry[]>();\n for (const entry of entries) {\n const key = `${entry.phase}|${entry.gesture}|${entry.arg ?? ''}|${entry.target ?? ''}|${canonicalModifiers(entry.modifiers)}`;\n const bucket = groups.get(key);\n if (bucket) bucket.push(entry);\n else groups.set(key, [entry]);\n }\n const conflicts: Conflict[] = [];\n for (const bucket of groups.values()) {\n if (bucket.length < 2) continue;\n const first = bucket[0];\n conflicts.push({\n phase: first.phase,\n gesture: first.gesture,\n arg: first.arg,\n target: first.target,\n modifiers: first.modifiers,\n toolIds: bucket.map((e) => e.toolId),\n });\n }\n return conflicts;\n}\n"]}