@weasel-js/routing 1.11.1 → 1.13.0-rc.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.
@@ -1,6 +1,6 @@
1
1
  import { isDev } from '@weasel-js/react';
2
2
  import { createEmitter } from '@weasel-js/registry';
3
- import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, isApplePlatform, routeGestureForSpecKind, getGestureDescriptor, formatRoute, parseTargetSpec, matchSpec, canonicalModifiers, UNKNOWN_MIME, specificity, VALID_MOD_NAMES, stylusOf } from '@weasel-js/gestures';
3
+ import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, isApplePlatform, bindingSpec, routeGestureForSpecKind, getGestureDescriptor, formatRoute, parseTargetSpec, matchSpec, canonicalModifiers, UNKNOWN_MIME, specificity, formatShortcut, VALID_MOD_NAMES, stylusOf } from '@weasel-js/gestures';
4
4
  export { GESTURE_DESCRIPTORS, LONG_PRESS_MS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, activatesFocusedControl, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, focusedControlOwnsKey, formatPhaseAtom, formatRoute, getGestureDescriptor, isEditableTarget, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute, routeGestureForSpecKind, specKindForRouteGesture, specificity } from '@weasel-js/gestures';
5
5
  import { activeModeOf, NORMAL, DEFAULT_MODES, capabilitiesAllowed } from '@weasel-js/modes';
6
6
  import { resolveCursor } from '@weasel-js/cursor';
@@ -466,6 +466,165 @@ function tuneAction(action, tunings) {
466
466
  for (const t of tunings) out = tuneOnce(out, t);
467
467
  return out;
468
468
  }
469
+ var NO_OVERRIDES = /* @__PURE__ */ new Map();
470
+ function bindingId(b, ownerId, index) {
471
+ return b.opts?.id ?? (ownerId === null ? `${b.actionId}@${index}` : `${ownerId}#${index}`);
472
+ }
473
+ function resolveBindingOverrides(maps) {
474
+ if (maps.length === 0) return NO_OVERRIDES;
475
+ const out = /* @__PURE__ */ new Map();
476
+ for (const map of maps) {
477
+ for (const [id, input] of Object.entries(map)) {
478
+ try {
479
+ out.set(id, bindingSpec(input));
480
+ } catch (err) {
481
+ console.warn(`weasel: binding override "${id}" is not a route: ${err.message}`);
482
+ }
483
+ }
484
+ }
485
+ return out;
486
+ }
487
+ function overridden(b, id, overrides) {
488
+ if (!overrides.has(id)) return b;
489
+ const spec = overrides.get(id);
490
+ return spec === null ? null : { ...b, spec };
491
+ }
492
+ function overriddenBindings(bindings, ownerId, overrides = NO_OVERRIDES) {
493
+ const out = [];
494
+ bindings.forEach((b, i) => {
495
+ const next = overridden(b, bindingId(b, ownerId, i), overrides);
496
+ if (next) out.push(next);
497
+ });
498
+ return out;
499
+ }
500
+ function overrideActionBindings(action, overrides) {
501
+ if (overrides.size === 0 || action.defaultBinding === void 0) return action;
502
+ const declared = actionBindings(action);
503
+ if (!declared.some((b, i) => overrides.has(bindingId(b, null, i)))) return action;
504
+ const kept = [];
505
+ declared.forEach((b, i) => {
506
+ const id = bindingId(b, null, i);
507
+ const next = overridden(b, id, overrides);
508
+ if (next) kept.push({ spec: next.spec, opts: { ...next.opts, id } });
509
+ });
510
+ return { ...action, defaultBinding: kept };
511
+ }
512
+
513
+ // src/interactions/dispatcher/inputFacts.ts
514
+ function modifierStateOf(e) {
515
+ return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
516
+ }
517
+ function worldPointOf(event) {
518
+ switch (event.kind) {
519
+ case "pointerdown":
520
+ case "hover":
521
+ case "longpress":
522
+ case "click":
523
+ case "doubleclick":
524
+ case "contextmenu":
525
+ return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
526
+ case "carry":
527
+ return { x: event.x, y: event.y };
528
+ default:
529
+ return void 0;
530
+ }
531
+ }
532
+ function carriedBy(event) {
533
+ if (event.kind === "carry") return { items: [], carry: { type: event.type, value: event.value } };
534
+ return event.kind === "drop" || event.kind === "paste" ? { items: event.items } : void 0;
535
+ }
536
+ function immediateParams(event, resolved) {
537
+ switch (event.kind) {
538
+ case "wheel":
539
+ return {
540
+ deltaX: event.deltaX,
541
+ deltaY: event.deltaY,
542
+ clientX: event.clientX,
543
+ clientY: event.clientY,
544
+ affordance: event.affordance,
545
+ ...resolved
546
+ };
547
+ case "pinch":
548
+ return {
549
+ scale: event.scale,
550
+ rotation: event.rotation,
551
+ clientX: event.clientX,
552
+ clientY: event.clientY,
553
+ affordance: event.affordance,
554
+ ...resolved
555
+ };
556
+ case "click":
557
+ case "doubleclick":
558
+ return {
559
+ worldX: event.x,
560
+ worldY: event.y,
561
+ affordance: event.affordance,
562
+ // The press point too: an action that places geometry at the click wants it.
563
+ ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
564
+ mods: modifierStateOf(event),
565
+ ...resolved
566
+ };
567
+ // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
568
+ // buffered copy matches `drag` specs, which are ongoing.
569
+ case "contextmenu":
570
+ return {
571
+ worldX: event.x,
572
+ worldY: event.y,
573
+ clientX: event.clientX,
574
+ clientY: event.clientY,
575
+ affordance: event.affordance,
576
+ bodyTarget: event.bodyTarget,
577
+ bodyKind: event.bodyKind,
578
+ mods: modifierStateOf(event),
579
+ ...resolved
580
+ };
581
+ case "pointerdown":
582
+ case "longpress":
583
+ return {
584
+ worldX: event.x,
585
+ worldY: event.y,
586
+ affordance: event.affordance,
587
+ bodyTarget: event.bodyTarget,
588
+ mods: modifierStateOf(event),
589
+ ...resolved
590
+ };
591
+ case "hover":
592
+ return {
593
+ worldX: event.x,
594
+ worldY: event.y,
595
+ clientX: event.clientX,
596
+ clientY: event.clientY,
597
+ affordance: event.affordance,
598
+ bodyTarget: event.bodyTarget,
599
+ mods: modifierStateOf(event),
600
+ held: event.held === true,
601
+ ...resolved
602
+ };
603
+ case "carry":
604
+ return {
605
+ worldX: event.x,
606
+ worldY: event.y,
607
+ clientX: event.clientX,
608
+ clientY: event.clientY,
609
+ carry: { type: event.type, value: event.value },
610
+ affordance: event.affordance,
611
+ bodyTarget: event.bodyTarget,
612
+ bodyKind: event.bodyKind,
613
+ mods: modifierStateOf(event),
614
+ ...resolved
615
+ };
616
+ case "drop":
617
+ case "paste":
618
+ return {
619
+ items: event.items,
620
+ via: event.kind,
621
+ ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
622
+ ...resolved
623
+ };
624
+ default:
625
+ return resolved;
626
+ }
627
+ }
469
628
 
470
629
  // src/interactions/actions/toolOffhand.ts
471
630
  var TOOL_OFFHAND_ID = "tool.offhand";
@@ -488,7 +647,7 @@ function offhandKeyFor(trigger) {
488
647
  function buildToolOffhandBindings(specs) {
489
648
  return specs.map(({ toolId, key }) => ({
490
649
  spec: { ...heldKeySpec(key), phase: BETWEEN_GESTURES },
491
- opts: { params: { toolId } }
650
+ opts: { id: `${TOOL_OFFHAND_ID}.${toolId}`, params: { toolId } }
492
651
  }));
493
652
  }
494
653
  var BETWEEN_GESTURES = [{ channel: "*", phase: "initial" }];
@@ -996,17 +1155,18 @@ function gestureIdFor(event) {
996
1155
  function liveScope(id, eligibility, state) {
997
1156
  const tags = eligibility.capabilities;
998
1157
  if (tags && state.allows && !state.allows(tags)) return null;
1158
+ if (eligibility.keymap) return state.openKeymap === id ? "hotkey" : null;
999
1159
  if (state.engagedIds?.has(id)) return "hotkey";
1000
1160
  if (eligibility.focus && state.focusedId === id) return "active";
1001
1161
  if (eligibility.always || eligibility.claimed) return "ambient";
1002
1162
  return null;
1003
1163
  }
1004
- function scopeBindings(entries, state) {
1164
+ function scopeBindings(entries, state, overrides = NO_OVERRIDES) {
1005
1165
  const out = [];
1006
1166
  for (const entry of entries) {
1007
1167
  const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
1008
1168
  if (scope === null) continue;
1009
- for (const binding of entry.bindings ?? []) {
1169
+ for (const binding of overriddenBindings(entry.bindings ?? [], entry.id, overrides)) {
1010
1170
  out.push({ binding, scope, ownerId: entry.id });
1011
1171
  }
1012
1172
  }
@@ -1028,6 +1188,7 @@ function assembleEntries(entries, inputs) {
1028
1188
  state: {
1029
1189
  focusedId: inputs.focusedId,
1030
1190
  engagedIds: new Set(inputs.hotkeyStack),
1191
+ ...inputs.openKeymap ? { openKeymap: inputs.openKeymap } : {},
1031
1192
  ...allowed ? { allows: (tags) => capabilitiesAllowed(allowed, tags) } : {}
1032
1193
  }
1033
1194
  };
@@ -1078,122 +1239,6 @@ function createDispatchTrace(limit, dev) {
1078
1239
  };
1079
1240
  }
1080
1241
 
1081
- // src/interactions/dispatcher/inputFacts.ts
1082
- function modifiersOf(e) {
1083
- return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
1084
- }
1085
- function worldPointOf(event) {
1086
- switch (event.kind) {
1087
- case "pointerdown":
1088
- case "hover":
1089
- case "longpress":
1090
- case "click":
1091
- case "doubleclick":
1092
- case "contextmenu":
1093
- return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
1094
- case "carry":
1095
- return { x: event.x, y: event.y };
1096
- default:
1097
- return void 0;
1098
- }
1099
- }
1100
- function carriedBy(event) {
1101
- if (event.kind === "carry") return { items: [], carry: { type: event.type, value: event.value } };
1102
- return event.kind === "drop" || event.kind === "paste" ? { items: event.items } : void 0;
1103
- }
1104
- function immediateParams(event, resolved) {
1105
- switch (event.kind) {
1106
- case "wheel":
1107
- return {
1108
- deltaX: event.deltaX,
1109
- deltaY: event.deltaY,
1110
- clientX: event.clientX,
1111
- clientY: event.clientY,
1112
- affordance: event.affordance,
1113
- ...resolved
1114
- };
1115
- case "pinch":
1116
- return {
1117
- scale: event.scale,
1118
- rotation: event.rotation,
1119
- clientX: event.clientX,
1120
- clientY: event.clientY,
1121
- affordance: event.affordance,
1122
- ...resolved
1123
- };
1124
- case "click":
1125
- case "doubleclick":
1126
- return {
1127
- worldX: event.x,
1128
- worldY: event.y,
1129
- affordance: event.affordance,
1130
- // The press point too: an action that places geometry at the click wants it.
1131
- ...event.kind === "click" ? { pressX: event.pressX, pressY: event.pressY } : {},
1132
- mods: modifiersOf(event),
1133
- ...resolved
1134
- };
1135
- // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
1136
- // buffered copy matches `drag` specs, which are ongoing.
1137
- case "contextmenu":
1138
- return {
1139
- worldX: event.x,
1140
- worldY: event.y,
1141
- clientX: event.clientX,
1142
- clientY: event.clientY,
1143
- affordance: event.affordance,
1144
- bodyTarget: event.bodyTarget,
1145
- bodyKind: event.bodyKind,
1146
- mods: modifiersOf(event),
1147
- ...resolved
1148
- };
1149
- case "pointerdown":
1150
- case "longpress":
1151
- return {
1152
- worldX: event.x,
1153
- worldY: event.y,
1154
- affordance: event.affordance,
1155
- bodyTarget: event.bodyTarget,
1156
- mods: modifiersOf(event),
1157
- ...resolved
1158
- };
1159
- case "hover":
1160
- return {
1161
- worldX: event.x,
1162
- worldY: event.y,
1163
- clientX: event.clientX,
1164
- clientY: event.clientY,
1165
- affordance: event.affordance,
1166
- bodyTarget: event.bodyTarget,
1167
- mods: modifiersOf(event),
1168
- held: event.held === true,
1169
- ...resolved
1170
- };
1171
- case "carry":
1172
- return {
1173
- worldX: event.x,
1174
- worldY: event.y,
1175
- clientX: event.clientX,
1176
- clientY: event.clientY,
1177
- carry: { type: event.type, value: event.value },
1178
- affordance: event.affordance,
1179
- bodyTarget: event.bodyTarget,
1180
- bodyKind: event.bodyKind,
1181
- mods: modifiersOf(event),
1182
- ...resolved
1183
- };
1184
- case "drop":
1185
- case "paste":
1186
- return {
1187
- items: event.items,
1188
- via: event.kind,
1189
- ...event.kind === "drop" && event.x !== void 0 ? { worldX: event.x, worldY: event.y } : {},
1190
- ...resolved
1191
- };
1192
- default:
1193
- return resolved;
1194
- }
1195
- }
1196
-
1197
1242
  // src/interactions/dispatcher/ranking.ts
1198
1243
  function liveInView(binding, view) {
1199
1244
  const views = binding.opts?.views;
@@ -1313,7 +1358,7 @@ function buildDispatchRecord(r, walked, event, ctx, predicted, outcome) {
1313
1358
  input: {
1314
1359
  eventKind: event.kind,
1315
1360
  ...eventKey !== void 0 ? { key: eventKey } : {},
1316
- modifiers: modifiersOf(event),
1361
+ modifiers: modifierStateOf(event),
1317
1362
  viewId: view,
1318
1363
  ...world !== void 0 ? { world } : {},
1319
1364
  ...r.ruleCtx !== void 0 ? { mode: r.ruleCtx.mode } : {}
@@ -1754,19 +1799,22 @@ function createUiOngoing(inFlight, notify, getAction) {
1754
1799
  // src/interactions/dispatcher/walk.ts
1755
1800
  function assembleScopedBindings(ctx) {
1756
1801
  const allowedCapabilities = ctx.getRuleCtx?.()?.allowedCapabilities;
1802
+ const openKeymap = ctx.keymapPass === "none" ? null : ctx.keymaps?.innermost()?.id ?? null;
1757
1803
  const { ordered, state } = assembleEntries(ctx.entriesById.values(), {
1758
1804
  focusedId: ctx.activeToolId,
1759
1805
  hotkeyStack: ctx.hotkeyStack,
1760
- ...allowedCapabilities ? { allowedCapabilities } : {}
1806
+ ...allowedCapabilities ? { allowedCapabilities } : {},
1807
+ ...openKeymap ? { openKeymap } : {}
1761
1808
  });
1762
1809
  const view = ctx.viewId ?? null;
1763
1810
  const live = (b) => liveInView(b, view) && liveForKeys(b, ctx.keyReach);
1764
- const result = scopeBindings(ordered, state).filter((sb) => live(sb.binding));
1811
+ const overrides = ctx.bindingOverrides?.() ?? NO_OVERRIDES;
1812
+ const result = scopeBindings(ordered, state, overrides).filter((sb) => live(sb.binding));
1813
+ if (ctx.keymapPass === "only") return result.filter((sb) => sb.ownerId === openKeymap);
1765
1814
  for (const action of ctx.actions.list()) {
1766
1815
  const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
1767
1816
  for (const binding of actionBindings(action)) {
1768
- if (!live(binding)) continue;
1769
- result.push({ binding, scope: targetScope, ownerId: null });
1817
+ if (live(binding)) result.push({ binding, scope: targetScope, ownerId: null });
1770
1818
  }
1771
1819
  }
1772
1820
  return result;
@@ -1939,6 +1987,68 @@ function explain(event, ctx, engaged, opts) {
1939
1987
  const walked = walk(r, event, ctx, null, opts?.evaluateShadowed ?? false);
1940
1988
  return buildDispatchRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1941
1989
  }
1990
+ var MODIFIER_KEYS2 = /* @__PURE__ */ new Set(["Shift", "Control", "Alt", "Meta", "AltGraph", "CapsLock"]);
1991
+ var STROKES = /* @__PURE__ */ new Set([
1992
+ "click",
1993
+ "doubleclick",
1994
+ "longpress",
1995
+ "contextmenu",
1996
+ "multitouch",
1997
+ "multitouchtap"
1998
+ ]);
1999
+ var isPlainEscape = (e) => e.kind === "key" && e.key === "Escape" && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey;
2000
+ var SETTLES_INTO = ["click", "doubleclick", "longpress", "contextmenu"];
2001
+ function stillLive(id, ctx) {
2002
+ const entry = ctx.entriesById.get(id);
2003
+ if (!entry) return false;
2004
+ const allowed = ctx.getRuleCtx?.()?.allowedCapabilities;
2005
+ const allows = allowed ? (tags) => capabilitiesAllowed(allowed, tags) : void 0;
2006
+ return liveScope(id, entry.eligibility, { focusedId: null, openKeymap: id, ...allows ? { allows } : {} }) !== null;
2007
+ }
2008
+ function routeThroughKeymap(event, ctx, passes) {
2009
+ const stack = ctx.keymaps;
2010
+ const open = stack?.innermost();
2011
+ if (!stack || !open || event.kind === "hover") return void 0;
2012
+ const isKey = event.kind === "key" || event.kind === "key-held";
2013
+ if (isKey && ctx.keyReach && !ctx.keyReach.held) return void 0;
2014
+ if (!stillLive(open.id, ctx)) {
2015
+ stack.closeAll();
2016
+ return void 0;
2017
+ }
2018
+ const verdict = stack.verdict();
2019
+ if (verdict === "taken") return "unhandled";
2020
+ if (verdict === "cascaded") return passes.run("none", event);
2021
+ if (isPlainEscape(event)) {
2022
+ stack.closeInnermost();
2023
+ return "handled";
2024
+ }
2025
+ if (passes.run("only", event) === "handled") {
2026
+ stack.inputSeen();
2027
+ return "handled";
2028
+ }
2029
+ if (isKey) {
2030
+ if (event.kind === "key" && passes.binds({ ...event, kind: "key-held", phase: "down" })) return "unhandled";
2031
+ if (MODIFIER_KEYS2.has(event.key) || event.ctrlKey || event.metaKey || event.key === "Tab") {
2032
+ return passes.run("none", event);
2033
+ }
2034
+ const entry = ctx.entriesById.get(open.id)?.eligibility.keymap;
2035
+ const own = typeof entry === "object" ? entry.unbound : void 0;
2036
+ if ((own ?? ctx.leaderUnbound ?? "modal") === "modal") {
2037
+ stack.swallowed();
2038
+ return "handled";
2039
+ }
2040
+ stack.cascaded();
2041
+ return passes.run("none", event);
2042
+ }
2043
+ if (event.kind === "pointerdown" && SETTLES_INTO.some((kind) => passes.binds({ ...event, kind }))) {
2044
+ return "unhandled";
2045
+ }
2046
+ const outcome = passes.run("none", event);
2047
+ const stroke = STROKES.has(event.kind) || event.kind === "pointerdown" && outcome === "handled";
2048
+ const firstOfDouble = event.kind === "click" && passes.binds({ ...event, kind: "doubleclick" });
2049
+ if (stroke && !firstOfDouble) stack.inputSeen();
2050
+ return outcome;
2051
+ }
1942
2052
 
1943
2053
  // src/interactions/dispatcher/dispatcher.ts
1944
2054
  function createDispatcher(opts) {
@@ -1961,6 +2071,12 @@ function createDispatcher(opts) {
1961
2071
  const pumped = pumpGesture(run, event);
1962
2072
  if (pumped !== void 0) return pumped;
1963
2073
  if (event.kind === "hover") return hover(event, ctx);
2074
+ return routeThroughKeymap(event, ctx, {
2075
+ run: (pass, ev) => route(ev, { ...ctx, keymapPass: pass }),
2076
+ binds: (ev) => rank(ev, { ...ctx, keymapPass: "only" }, inFlight.engagedOwners(), false).ordered.length > 0
2077
+ }) ?? route(event, ctx);
2078
+ }
2079
+ function route(event, ctx) {
1964
2080
  const trace = ctx.trace?.recording() ? ctx.trace : null;
1965
2081
  const r = rank(event, ctx, inFlight.engagedOwners(), trace !== null);
1966
2082
  const walked = walk(r, event, ctx, invoking(event), false);
@@ -2181,6 +2297,229 @@ function startCarry(origin, down, payload, cb = {}) {
2181
2297
  cancel: () => session.cancel()
2182
2298
  };
2183
2299
  }
2300
+ var KEYMAP_HOLD = "hold";
2301
+ function createKeymapStack() {
2302
+ let levels = [];
2303
+ let version = 0;
2304
+ const changed = createEmitter();
2305
+ let depth = 0;
2306
+ let serial = 0;
2307
+ let pending = [];
2308
+ let verdict = null;
2309
+ let snapshot = [];
2310
+ const notify = () => {
2311
+ snapshot = levels.map(({ openedIn: _, ...rest }) => Object.freeze({ ...rest }));
2312
+ version++;
2313
+ changed.emit();
2314
+ };
2315
+ const later = (op) => {
2316
+ if (depth > 0) pending.push(op);
2317
+ else op();
2318
+ };
2319
+ const settle = () => {
2320
+ const before = levels;
2321
+ const opened = (l) => l.openedIn === serial && depth > 0;
2322
+ if (levels.some(opened)) return;
2323
+ for (const l of levels) if (l.heldBy !== void 0) l.usedWhileHeld = true;
2324
+ while (levels.length > 0 && levels.at(-1).heldBy === void 0) levels = levels.slice(0, -1);
2325
+ if (levels !== before) notify();
2326
+ };
2327
+ return {
2328
+ open: () => snapshot,
2329
+ innermost: () => snapshot.at(-1) ?? null,
2330
+ push: (id, label, opts) => {
2331
+ const heldBy = opts?.heldBy;
2332
+ const bindings = opts?.bindings ?? [];
2333
+ const open = levels.findIndex((l) => l.id === id);
2334
+ if (open !== -1) levels = levels.slice(0, open);
2335
+ levels = [...levels, { id, label, bindings, ...heldBy !== void 0 ? { heldBy } : {}, usedWhileHeld: false, openedIn: serial }];
2336
+ if (depth > 0) verdict = "taken";
2337
+ notify();
2338
+ },
2339
+ holdOpened: (heldBy) => {
2340
+ let held = false;
2341
+ levels = levels.map((l) => {
2342
+ if (l.openedIn !== serial || l.heldBy !== void 0) return l;
2343
+ held = true;
2344
+ return { ...l, heldBy };
2345
+ });
2346
+ if (held) notify();
2347
+ return held;
2348
+ },
2349
+ release: (heldBy) => {
2350
+ const i = levels.findIndex((l) => l.heldBy === heldBy);
2351
+ if (i === -1) return;
2352
+ const level = levels[i];
2353
+ if (level.usedWhileHeld) {
2354
+ levels = levels.slice(0, i);
2355
+ while (levels.length > 0 && levels.at(-1).heldBy === void 0) levels = levels.slice(0, -1);
2356
+ } else levels = levels.map((l) => l === level ? { ...l, heldBy: void 0 } : l).map(dropUndefinedHeld);
2357
+ notify();
2358
+ },
2359
+ closeInnermost: () => {
2360
+ if (depth > 0) verdict = "taken";
2361
+ later(() => {
2362
+ if (levels.length === 0) return;
2363
+ levels = levels.slice(0, -1);
2364
+ notify();
2365
+ });
2366
+ },
2367
+ inputSeen: () => {
2368
+ if (depth > 0) verdict ??= "taken";
2369
+ later(settle);
2370
+ },
2371
+ cascaded: () => {
2372
+ if (depth > 0) verdict = "cascaded";
2373
+ later(settle);
2374
+ },
2375
+ swallowed: () => {
2376
+ for (const l of levels) if (l.heldBy !== void 0) l.usedWhileHeld = true;
2377
+ },
2378
+ verdict: () => verdict,
2379
+ closeAll: () => {
2380
+ pending = [];
2381
+ if (levels.length === 0) return;
2382
+ levels = [];
2383
+ notify();
2384
+ },
2385
+ batch: (fn) => {
2386
+ if (depth === 0) {
2387
+ serial++;
2388
+ verdict = null;
2389
+ }
2390
+ depth++;
2391
+ try {
2392
+ return fn();
2393
+ } finally {
2394
+ depth--;
2395
+ if (depth === 0) {
2396
+ const ops = pending;
2397
+ pending = [];
2398
+ depth = 1;
2399
+ try {
2400
+ for (const op of ops) op();
2401
+ } finally {
2402
+ depth = 0;
2403
+ }
2404
+ verdict = null;
2405
+ }
2406
+ }
2407
+ },
2408
+ subscribe: changed.subscribe,
2409
+ getVersion: () => version
2410
+ };
2411
+ }
2412
+ function dropUndefinedHeld(l) {
2413
+ if (l.heldBy !== void 0) return l;
2414
+ const { heldBy: _, ...rest } = l;
2415
+ return rest;
2416
+ }
2417
+
2418
+ // src/keymaps/keymapContribution.ts
2419
+ var keymapLeaderId = (id) => `keymap.${id}`;
2420
+ var keymapLeaderBindingId = (id, index = 0) => index === 0 ? `${keymapLeaderId(id)}.leader` : `${keymapLeaderId(id)}.leader#${index}`;
2421
+ function leaderBindings(def) {
2422
+ return [def.leader].flat().map((spec, i) => ({
2423
+ spec: spec.kind === "key-held" ? { ...spec, kind: "key" } : spec,
2424
+ opts: { id: keymapLeaderBindingId(def.id, i) }
2425
+ }));
2426
+ }
2427
+ var isPlainEscape2 = (spec) => spec.kind === "key" && [spec.key].flat().includes("Escape") && Object.values(spec.mods ?? {}).every((m) => m !== true);
2428
+ function keymapContribution(def) {
2429
+ if (def.bindings.some((b) => isPlainEscape2(b.spec))) {
2430
+ throw new Error(`keymap "${def.id}" binds Escape, which always closes a keymap`);
2431
+ }
2432
+ const open = {
2433
+ id: keymapLeaderId(def.id),
2434
+ label: `Open ${def.label} keys`,
2435
+ scope: "hotkey",
2436
+ defaultBinding: leaderBindings(def),
2437
+ requires: ["keymaps"],
2438
+ enabled: (deps) => deps?.keymaps ? true : ActionDisabledReason.NotApplicable,
2439
+ invoker: {
2440
+ timing: "immediate",
2441
+ run: (deps, params) => {
2442
+ deps.keymaps?.push(def.id, def.label, { bindings: def.bindings, ...params?.hold === true ? { heldBy: KEYMAP_HOLD } : {} });
2443
+ }
2444
+ }
2445
+ };
2446
+ return [
2447
+ {
2448
+ id: def.id,
2449
+ eligibility: { keymap: def.unbound ? { unbound: def.unbound } : true },
2450
+ bindings: [...def.bindings],
2451
+ presentation: { label: def.label },
2452
+ ...def.overlay !== void 0 ? { overlay: def.overlay } : {},
2453
+ ...def.overlayPosition ? { overlayPosition: def.overlayPosition } : {}
2454
+ },
2455
+ {
2456
+ id: `${def.id}.leader`,
2457
+ eligibility: { always: true },
2458
+ actions: [open, ...def.actions ?? []]
2459
+ }
2460
+ ];
2461
+ }
2462
+ var ESCAPE = { shortcut: { key: "Escape", mod: false, alt: false, shift: false }, label: "Close" };
2463
+ var GESTURE_WORDS = {
2464
+ click: "click",
2465
+ doubleClick: "double-click",
2466
+ drag: "drag",
2467
+ longPress: "long-press",
2468
+ contextMenu: "right-click",
2469
+ wheel: "scroll",
2470
+ pinch: "pinch",
2471
+ pointerDown: "press"
2472
+ };
2473
+ var MOD_WORDS = [
2474
+ ["mod", "Mod"],
2475
+ ["ctrl", "Ctrl"],
2476
+ ["meta", "Meta"],
2477
+ ["alt", "Alt"],
2478
+ ["shift", "Shift"]
2479
+ ];
2480
+ function gestureWords(spec) {
2481
+ if (spec.kind === "key-held") {
2482
+ const chip = keySpecShortcut(spec);
2483
+ return chip ? `Hold ${formatShortcut(chip)}` : void 0;
2484
+ }
2485
+ const word = GESTURE_WORDS[spec.kind];
2486
+ if (!word) return void 0;
2487
+ const mods = MOD_WORDS.filter(([m]) => spec.mods?.[m] === true).map(([, w]) => w);
2488
+ const text = [...mods, word].join("-");
2489
+ return text[0].toUpperCase() + text.slice(1);
2490
+ }
2491
+ function describeKeymap(level, actions, overrides = NO_OVERRIDES) {
2492
+ const byId = new Map(actions.map((a) => [a.id, a]));
2493
+ const labelOf = (b) => {
2494
+ const action = byId.get(b.actionId);
2495
+ const params = resolveParams(b.opts?.params);
2496
+ const variant = params && action?.variants?.find((v) => paramsMatch(params, v.params));
2497
+ return variant ? variant.label : action?.label ?? b.actionId;
2498
+ };
2499
+ const rows = [ESCAPE];
2500
+ for (const b of overriddenBindings(level.bindings, level.id, overrides)) {
2501
+ const label = labelOf(b);
2502
+ const shortcut = b.spec.kind === "key" ? keySpecShortcut(b.spec) : void 0;
2503
+ if (shortcut) rows.push({ shortcut, label });
2504
+ else {
2505
+ const gesture = gestureWords(b.spec);
2506
+ if (gesture) rows.push({ gesture, label });
2507
+ }
2508
+ }
2509
+ return { id: level.id, label: level.label, held: level.heldBy !== void 0, rows };
2510
+ }
2511
+ function keymapShortcuts(def, overrides = NO_OVERRIDES) {
2512
+ const leaderChip = overriddenBindings(actionBindings({ id: keymapLeaderId(def.id), defaultBinding: leaderBindings(def) }), null, overrides).map((b) => keySpecShortcut(b.spec)).find((c) => c !== void 0);
2513
+ const out = /* @__PURE__ */ new Map();
2514
+ for (const b of overriddenBindings(def.bindings, def.id, overrides)) {
2515
+ if (b.spec.kind !== "key") continue;
2516
+ const chip = keySpecShortcut(b.spec);
2517
+ if (!chip) continue;
2518
+ const withPath = leaderChip ? { ...chip, leader: [{ label: def.label, shortcut: leaderChip }] } : chip;
2519
+ out.set(b.actionId, [...out.get(b.actionId) ?? [], withPath]);
2520
+ }
2521
+ return out;
2522
+ }
2184
2523
 
2185
2524
  // src/tools/types.ts
2186
2525
  function isTool(entry) {
@@ -2307,7 +2646,10 @@ function eligibleRule(action) {
2307
2646
  return typeof eligible === "function" ? eligible.rule : eligible;
2308
2647
  }
2309
2648
  function findScopedConflicts(scopes) {
2310
- const registry = Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry);
2649
+ const overrides = scopes.overrides;
2650
+ const asBound = (e) => overrides?.size && e.bindings ? { ...e, bindings: overriddenBindings(e.bindings, e.id, overrides) } : e;
2651
+ const registry = (Array.isArray(scopes.registry) ? scopes.registry : Object.values(scopes.registry)).map(asBound);
2652
+ const declaredAmbient = (scopes.ambient ?? []).map(asBound);
2311
2653
  const actions = scopes.actions ?? [];
2312
2654
  const rules = /* @__PURE__ */ new Map();
2313
2655
  for (const a of actions) {
@@ -2322,7 +2664,9 @@ function findScopedConflicts(scopes) {
2322
2664
  (a.eligible !== void 0 ? gatedActions : ungated).push(actionAsEntry(a));
2323
2665
  }
2324
2666
  const modes = scopes.modes ?? KIT_MODES;
2325
- const ambient = [...scopes.ambient ?? [], ...ungated];
2667
+ const isKeymap = (e) => e.eligibility?.keymap !== void 0;
2668
+ const keymaps = declaredAmbient.filter(isKeymap);
2669
+ const ambient = [...declaredAmbient.filter((e) => !isKeymap(e)), ...ungated];
2326
2670
  const out = [];
2327
2671
  const seen = /* @__PURE__ */ new Set();
2328
2672
  const add = (keyed) => {
@@ -2333,7 +2677,7 @@ function findScopedConflicts(scopes) {
2333
2677
  out.push(conflict);
2334
2678
  }
2335
2679
  };
2336
- for (const tool of [...registry, ...ambient, ...gatedActions, ...hotkeyActions]) {
2680
+ for (const tool of [...registry, ...ambient, ...keymaps, ...gatedActions, ...hotkeyActions]) {
2337
2681
  add(findConflictsKeyed([tool], rules, modes));
2338
2682
  }
2339
2683
  if (ambient.length > 1) add(findConflictsKeyed(ambient, rules, modes));
@@ -2827,6 +3171,7 @@ function createMenuHost(src) {
2827
3171
  client: req.client,
2828
3172
  world: req.world,
2829
3173
  ...req.under ? { under: req.under } : {},
3174
+ ...req.side ? { side: req.side } : {},
2830
3175
  source: req.source ?? "pointer"
2831
3176
  };
2832
3177
  emit();
@@ -2844,6 +3189,6 @@ function createMenuHost(src) {
2844
3189
  };
2845
3190
  }
2846
3191
 
2847
- export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, LONG_PRESS_HAPTIC_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, actionCursor, actionItemTitle, actionItems, actionSettingsParams, actionShortcuts, addCarrySurface, admitIngestMime, anyBinding, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, carryGestureId, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatchTrace, createDispatcher, createLongPressStore, createMenuHost, createTraceRing, depNodeOf, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, keySpecShortcut, keysHeld, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, mergeMenuDefs, multiTouchGestureId, offhandKeyFor, openPointerSession, pickMenu, pointerGestureId, pushOwner, readActionSettings, reportRouteConflicts, resolveDeviceProfile, resolveMenu, resolveParams, resolveSchemaMenu, roleReader, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, startCarry, targetConsultsAffordance, tuneAction, tunedParams, validateActionDefaultBinding, validateActionId };
2848
- //# sourceMappingURL=chunk-KLBH7VFF.js.map
2849
- //# sourceMappingURL=chunk-KLBH7VFF.js.map
3192
+ export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, KEYMAP_HOLD, LONG_PRESS_HAPTIC_MS, NEVER, NO_OVERRIDES, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, actionCursor, actionItemTitle, actionItems, actionSettingsParams, actionShortcuts, addCarrySurface, admitIngestMime, anyBinding, assembleEntries, bindingId, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, carryGestureId, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatchTrace, createDispatcher, createKeymapStack, createLongPressStore, createMenuHost, createTraceRing, depNodeOf, describeKeymap, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, keySpecShortcut, keymapContribution, keymapLeaderBindingId, keymapLeaderId, keymapShortcuts, keysHeld, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, mergeMenuDefs, modifierStateOf, multiTouchGestureId, offhandKeyFor, openPointerSession, overriddenBindings, overrideActionBindings, pickMenu, pointerGestureId, pushOwner, readActionSettings, reportRouteConflicts, resolveBindingOverrides, resolveDeviceProfile, resolveMenu, resolveParams, resolveSchemaMenu, roleReader, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, startCarry, targetConsultsAffordance, tuneAction, tunedParams, validateActionDefaultBinding, validateActionId };
3193
+ //# sourceMappingURL=chunk-CRF3ROBB.js.map
3194
+ //# sourceMappingURL=chunk-CRF3ROBB.js.map