@weasel-js/routing 1.9.2 → 1.9.4

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/README.md CHANGED
@@ -21,11 +21,12 @@ scene graph, the renderer and the concrete dependency interfaces all live in
21
21
 
22
22
  ## The dependency schema
23
23
 
24
- `DepSchema` is declared here as an empty interface. Everything that names a
25
- dependency — `Action.requires`, `DepRegistry.get`, `useDepSource` — is keyed on
26
- `keyof DepSchema`, so the set of legal names is whatever has been merged into
27
- it. `@weasel-js/core` merges the kit's own 24 deps; a consumer adds its own the
28
- same way:
24
+ `DepSchema` is declared here with one entry, `activeTool`, the dep routing's own
25
+ `tool.offhand` action requires. Everything that names a dependency —
26
+ `Action.requires`, `DepRegistry.get`, `useDepSource` — is keyed on
27
+ `keyof DepSchema`, so the set of legal names is that entry plus whatever has
28
+ been merged in. `@weasel-js/core` merges the kit's own deps; a consumer adds its
29
+ own the same way:
29
30
 
30
31
  ```ts
31
32
  declare module '@weasel-js/core' {
@@ -1,4 +1,4 @@
1
- import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, routeGestureForSpecKind, getGestureDescriptor, formatRoute, canonicalModifiers, specificity } from '@weasel-js/gestures';
1
+ import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, matchSpec, routeGestureForSpecKind, getGestureDescriptor, formatRoute, canonicalModifiers, VALID_MOD_NAMES, specificity } from '@weasel-js/gestures';
2
2
  export { GESTURE_DESCRIPTORS, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatPhaseAtom, formatRoute, getGestureDescriptor, isKnownGestureName, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, parseRoute, routeGestureForSpecKind, specKindForRouteGesture, specificity } from '@weasel-js/gestures';
3
3
  import { activeModeOf, NORMAL, DEFAULT_MODES } from '@weasel-js/modes';
4
4
  import { resolveCursor } from '@weasel-js/cursor';
@@ -155,6 +155,7 @@ var DepNode = class extends ScopeNode {
155
155
  const stack = this.sources.get(key);
156
156
  if (stack) stack.push(entry);
157
157
  else this.sources.set(key, [entry]);
158
+ this.changed();
158
159
  let released = false;
159
160
  return () => {
160
161
  if (released) return;
@@ -165,6 +166,7 @@ var DepNode = class extends ScopeNode {
165
166
  if (i === -1) return;
166
167
  cur.splice(i, 1);
167
168
  if (cur.length === 0) this.sources.delete(key);
169
+ this.changed();
168
170
  };
169
171
  },
170
172
  get: (name) => {
@@ -173,7 +175,14 @@ var DepNode = class extends ScopeNode {
173
175
  if (source) return source();
174
176
  }
175
177
  return void 0;
176
- }
178
+ },
179
+ subscribe: (listener) => {
180
+ this.tree.listeners.add(listener);
181
+ return () => {
182
+ this.tree.listeners.delete(listener);
183
+ };
184
+ },
185
+ changed: () => this.changed()
177
186
  };
178
187
  constructor(parent, descends) {
179
188
  super(parent, descends);
@@ -248,11 +257,15 @@ function offhandKeyFor(trigger) {
248
257
  }
249
258
  }
250
259
  function buildToolOffhandBindings(specs) {
251
- return specs.map(({ toolId, key }) => ({
252
- spec: { kind: "key-held", key },
253
- opts: { params: { toolId } }
254
- }));
260
+ return specs.map(({ toolId, key }) => ({ spec: heldKeySpec(key), opts: { params: { toolId } } }));
255
261
  }
262
+ function heldKeySpec(key) {
263
+ const isModifier = [key].flat().some((k) => MODIFIER_KEYS.has(k));
264
+ return { kind: "key-held", key, mods: { ...isModifier ? ANY_MODS : SHIFT_OR_ALT } };
265
+ }
266
+ var MODIFIER_KEYS = /* @__PURE__ */ new Set(["Alt", "Control", "Meta", "Shift"]);
267
+ var ANY_MODS = { alt: "optional", ctrl: "optional", meta: "optional", shift: "optional" };
268
+ var SHIFT_OR_ALT = { alt: "optional", shift: "optional" };
256
269
  function makeToolOffhandAction(bindings) {
257
270
  return {
258
271
  id: TOOL_OFFHAND_ID,
@@ -299,6 +312,26 @@ function scopeBindings(entries, state) {
299
312
  }
300
313
  return out;
301
314
  }
315
+ function assembleEntries(entries, inputs) {
316
+ const all = [...entries];
317
+ const ordered = [];
318
+ for (let i = inputs.hotkeyStack.length - 1; i >= 0; i--) {
319
+ const entry = all.find((e) => e.id === inputs.hotkeyStack[i]);
320
+ if (entry && !ordered.includes(entry)) ordered.push(entry);
321
+ }
322
+ for (const entry of all) {
323
+ if (!ordered.includes(entry)) ordered.push(entry);
324
+ }
325
+ const allowed = inputs.allowedCapabilities;
326
+ return {
327
+ ordered,
328
+ state: {
329
+ focusedId: inputs.focusedId,
330
+ engagedIds: new Set(inputs.hotkeyStack),
331
+ ...allowed ? { allows: (tags) => tags.every((tag) => allowed.has(tag)) } : {}
332
+ }
333
+ };
334
+ }
302
335
 
303
336
  // src/devFlag.ts
304
337
  function isDev() {
@@ -313,7 +346,7 @@ function targetConsultsAffordance(specTarget) {
313
346
  if (form === null) return false;
314
347
  switch (form.form) {
315
348
  case "predicate":
316
- return form.kindOf.readsAffordance !== false;
349
+ return form.kindOf.readsAffordance === true;
317
350
  case "affordance":
318
351
  return true;
319
352
  case "body":
@@ -739,9 +772,8 @@ function buildRuleCtx(args) {
739
772
  device: args.device
740
773
  };
741
774
  }
742
- function pointerGestureId(pointerId) {
743
- return `pointer-${pointerId ?? "mouse"}`;
744
- }
775
+
776
+ // src/interactions/dispatcher/dispatchTrace.ts
745
777
  var DEV2 = (() => {
746
778
  try {
747
779
  return Boolean(import.meta.env?.DEV);
@@ -770,6 +802,25 @@ function recordModeSwitch(mode, from, to, detail) {
770
802
  ...detail !== void 0 ? { detail } : {}
771
803
  });
772
804
  }
805
+
806
+ // src/interactions/dispatcher/inputFacts.ts
807
+ function modifiersOf(e) {
808
+ return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
809
+ }
810
+ function worldPointOf(event) {
811
+ switch (event.kind) {
812
+ case "pointerdown":
813
+ case "longpress":
814
+ case "click":
815
+ case "doubleclick":
816
+ case "contextmenu":
817
+ return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
818
+ default:
819
+ return void 0;
820
+ }
821
+ }
822
+
823
+ // src/interactions/dispatcher/ranking.ts
773
824
  function liveInView(binding, view) {
774
825
  const views = binding.opts?.views;
775
826
  return views === void 0 || views.includes(view);
@@ -833,22 +884,76 @@ function sameSpecificity(a, b) {
833
884
  const sb = specificity(b);
834
885
  return sa.every((v, i) => v === sb[i]);
835
886
  }
836
- var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
837
- function modifiersOf(e) {
838
- return { alt: e.altKey, ctrl: e.ctrlKey, meta: e.metaKey, shift: e.shiftKey };
887
+
888
+ // src/interactions/dispatcher/buildDispatchRecord.ts
889
+ function buildDispatchRecord(r, walked, event, ctx, predicted, outcome) {
890
+ const view = ctx.viewId ?? null;
891
+ const candidates = /* @__PURE__ */ new Map();
892
+ const candidateOf = (m) => {
893
+ let c = candidates.get(m);
894
+ if (!c) {
895
+ const eligible = r.actionMap.get(m.binding.actionId)?.eligible;
896
+ c = {
897
+ actionId: m.binding.actionId,
898
+ routes: routesForSpec(m.binding.spec),
899
+ scope: m.scope,
900
+ ownerId: m.ownerId,
901
+ namesView: namesView(m.binding, view),
902
+ specificity: specificity(m.binding.spec),
903
+ ...eligible !== void 0 ? { eligible: describeEligible(eligible) } : {}
904
+ };
905
+ candidates.set(m, c);
906
+ }
907
+ return c;
908
+ };
909
+ const dropped = [
910
+ ...r.barred.map((m) => ({
911
+ candidate: candidateOf(m),
912
+ filter: "claim",
913
+ ...r.claimOwner !== void 0 ? { owner: r.claimOwner } : {}
914
+ }))
915
+ ];
916
+ for (const m of r.ordered) {
917
+ if (r.eligible(m)) continue;
918
+ const c = candidateOf(m);
919
+ dropped.push({ candidate: c, filter: "ineligible", rule: c.eligible ?? "" });
920
+ }
921
+ const gated = (m) => r.actionMap.get(m.binding.actionId)?.eligible !== void 0;
922
+ const ranked = walked.steps.map(({ match, step }, i) => {
923
+ const prev = walked.steps[i - 1]?.match;
924
+ return {
925
+ candidate: candidateOf(match),
926
+ placedBy: prev ? placedBy(prev, match, view, r.ruleCtx !== void 0 && gated(prev) && !gated(match)) : { step: "first" },
927
+ walk: step
928
+ };
929
+ });
930
+ const eventKey = event.kind === "key" || event.kind === "key-held" ? event.key : void 0;
931
+ const world = worldPointOf(event);
932
+ return {
933
+ kind: "dispatch",
934
+ ts: Date.now(),
935
+ input: {
936
+ eventKind: event.kind,
937
+ ...eventKey !== void 0 ? { key: eventKey } : {},
938
+ modifiers: modifiersOf(event),
939
+ viewId: view,
940
+ ...world !== void 0 ? { world } : {},
941
+ ...r.ruleCtx !== void 0 ? { mode: r.ruleCtx.mode } : {}
942
+ },
943
+ matched: [...r.ordered, ...r.barred].map(candidateOf),
944
+ dropped,
945
+ ranked,
946
+ predicted,
947
+ fired: walked.winner?.action.id ?? null,
948
+ outcome
949
+ };
839
950
  }
840
- function worldPointOf(event) {
841
- switch (event.kind) {
842
- case "pointerdown":
843
- case "longpress":
844
- case "click":
845
- case "doubleclick":
846
- case "contextmenu":
847
- return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
848
- default:
849
- return void 0;
850
- }
951
+
952
+ // src/interactions/dispatcher/dispatcher.ts
953
+ function pointerGestureId(pointerId) {
954
+ return `pointer-${pointerId ?? "mouse"}`;
851
955
  }
956
+ var EMPTY_ENGAGED2 = /* @__PURE__ */ new Set();
852
957
  function createDispatcher(opts) {
853
958
  const inFlightHandles = /* @__PURE__ */ new Map();
854
959
  const inFlightOwners = /* @__PURE__ */ new Map();
@@ -949,7 +1054,7 @@ function createDispatcher(opts) {
949
1054
  }
950
1055
  function gestureIdFor(event) {
951
1056
  if (event.kind === "key-held") {
952
- return `key-held-${event.key}`;
1057
+ return `key-held-${event.code ?? event.key}`;
953
1058
  }
954
1059
  if (event.kind === "pointerdown" || event.kind === "pointermove" || event.kind === "pointerup" || event.kind === "pointercancel") {
955
1060
  return pointerGestureId(event.pointerId);
@@ -960,23 +1065,14 @@ function createDispatcher(opts) {
960
1065
  return `ongoing-${event.kind}`;
961
1066
  }
962
1067
  function assembleScopedBindings(ctx) {
963
- const ordered = [];
964
- for (let i = ctx.hotkeyStack.length - 1; i >= 0; i--) {
965
- const id = ctx.hotkeyStack[i];
966
- const entry = ctx.entriesById.get(id);
967
- if (entry && !ordered.includes(entry)) ordered.push(entry);
968
- }
969
- for (const entry of ctx.entriesById.values()) {
970
- if (!ordered.includes(entry)) ordered.push(entry);
971
- }
972
- const capsCtx = ctx.getRuleCtx?.();
973
- const allows = capsCtx ? (tags) => tags.every((tag) => capsCtx.allowedCapabilities.has(tag)) : void 0;
974
- const view = ctx.viewId ?? null;
975
- const result = scopeBindings(ordered, {
1068
+ const allowedCapabilities = ctx.getRuleCtx?.()?.allowedCapabilities;
1069
+ const { ordered, state } = assembleEntries(ctx.entriesById.values(), {
976
1070
  focusedId: ctx.activeToolId,
977
- engagedIds: new Set(ctx.hotkeyStack),
978
- ...allows ? { allows } : {}
979
- }).filter((sb) => liveInView(sb.binding, view));
1071
+ hotkeyStack: ctx.hotkeyStack,
1072
+ ...allowedCapabilities ? { allowedCapabilities } : {}
1073
+ });
1074
+ const view = ctx.viewId ?? null;
1075
+ const result = scopeBindings(ordered, state).filter((sb) => liveInView(sb.binding, view));
980
1076
  for (const action of ctx.actions.list()) {
981
1077
  const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
982
1078
  for (const binding of actionBindings(action)) {
@@ -1001,20 +1097,31 @@ function createDispatcher(opts) {
1001
1097
  }
1002
1098
  return map;
1003
1099
  }
1100
+ function runOnEnd(gestureId, handle, endCtx, reason) {
1101
+ try {
1102
+ handle.onEnd?.(endCtx, reason);
1103
+ } catch (err) {
1104
+ console.error(`weasel dispatcher: gesture "${gestureId}" threw on onEnd`, err);
1105
+ }
1106
+ }
1107
+ function endGesture(gestureId, event, reason) {
1108
+ const handle = inFlightHandles.get(gestureId);
1109
+ if (!handle) return false;
1110
+ const endCtx = buildInvocationCtx(event, {}, gestureId);
1111
+ inFlightHandles.delete(gestureId);
1112
+ inFlightOwners.delete(gestureId);
1113
+ inFlightActions.delete(gestureId);
1114
+ dragOrigins.delete(gestureId);
1115
+ dragPoints.delete(gestureId);
1116
+ pinchStartSpreads.delete(gestureId);
1117
+ runOnEnd(gestureId, handle, endCtx, reason);
1118
+ return true;
1119
+ }
1004
1120
  function handleInput(event, ctx) {
1005
1121
  routedView = ctx.viewId;
1006
1122
  if (event.kind === "key-held" && event.phase === "up") {
1007
1123
  const gestureId = gestureIdFor(event);
1008
- const handle = inFlightHandles.get(gestureId);
1009
- if (handle) {
1010
- const stubCtx = buildInvocationCtx(event, {}, gestureId);
1011
- handle.onEnd?.(stubCtx, "commit");
1012
- inFlightHandles.delete(gestureId);
1013
- inFlightOwners.delete(gestureId);
1014
- inFlightActions.delete(gestureId);
1015
- dragOrigins.delete(gestureId);
1016
- }
1017
- return handle ? "handled" : "unhandled";
1124
+ return endGesture(gestureId, event, "commit") ? "handled" : "unhandled";
1018
1125
  }
1019
1126
  if (event.kind === "key-held" && event.phase === "down") {
1020
1127
  const gestureId = gestureIdFor(event);
@@ -1045,31 +1152,18 @@ function createDispatcher(opts) {
1045
1152
  }
1046
1153
  if (event.kind === "pointerup") {
1047
1154
  const gestureId = gestureIdFor(event);
1048
- const handle = inFlightHandles.get(gestureId);
1049
- if (handle) {
1050
- const endCtx = buildInvocationCtx(event, {}, gestureId);
1051
- handle.onEnd?.(endCtx, "commit");
1052
- inFlightHandles.delete(gestureId);
1053
- inFlightOwners.delete(gestureId);
1054
- inFlightActions.delete(gestureId);
1055
- dragOrigins.delete(gestureId);
1056
- dragPoints.delete(gestureId);
1057
- }
1058
- return handle ? "handled" : "unhandled";
1155
+ return endGesture(gestureId, event, "commit") ? "handled" : "unhandled";
1059
1156
  }
1060
1157
  if (event.kind === "pointercancel") {
1061
1158
  const gestureId = gestureIdFor(event);
1062
- const handle = inFlightHandles.get(gestureId);
1063
- if (handle) {
1064
- const endCtx = buildInvocationCtx(event, {}, gestureId);
1065
- handle.onEnd?.(endCtx, "cancel");
1066
- inFlightHandles.delete(gestureId);
1067
- inFlightOwners.delete(gestureId);
1068
- inFlightActions.delete(gestureId);
1069
- dragOrigins.delete(gestureId);
1070
- dragPoints.delete(gestureId);
1159
+ return endGesture(gestureId, event, "cancel") ? "handled" : "unhandled";
1160
+ }
1161
+ if (event.kind === "multitouch" && event.end !== void 0) {
1162
+ let ended = false;
1163
+ for (const id of [...inFlightHandles.keys()]) {
1164
+ if (id.startsWith("multitouch-")) ended = endGesture(id, event, event.end) || ended;
1071
1165
  }
1072
- return handle ? "handled" : "unhandled";
1166
+ return ended ? "handled" : "unhandled";
1073
1167
  }
1074
1168
  if (event.kind === "multitouch" && event.centroid !== void 0) {
1075
1169
  const gestureId = gestureIdFor(event);
@@ -1079,19 +1173,14 @@ function createDispatcher(opts) {
1079
1173
  handle.onMove(moveCtx);
1080
1174
  return "handled";
1081
1175
  }
1082
- for (const [id, other] of [...inFlightHandles]) {
1083
- if (!id.startsWith("multitouch-") || id === gestureId) continue;
1084
- other.onEnd?.(buildInvocationCtx(event, {}, id), "cancel");
1085
- inFlightHandles.delete(id);
1086
- inFlightOwners.delete(id);
1087
- inFlightActions.delete(id);
1088
- pinchStartSpreads.delete(id);
1176
+ for (const id of [...inFlightHandles.keys()]) {
1177
+ if (id.startsWith("multitouch-") && id !== gestureId) endGesture(id, event, "cancel");
1089
1178
  }
1090
1179
  }
1091
1180
  const r = rank(event, ctx, DEV2);
1092
1181
  const walked = walk(r, event, ctx, (match, action, deps) => invoke(event, match, action, deps), false);
1093
1182
  const outcome = walked.stop === "fired" ? "handled" : "unhandled";
1094
- if (DEV2) recordTrace(buildRecord(r, walked, event, ctx, false, outcome));
1183
+ if (DEV2) recordTrace(buildDispatchRecord(r, walked, event, ctx, false, outcome));
1095
1184
  return outcome;
1096
1185
  }
1097
1186
  function invoke(event, match, action, deps) {
@@ -1296,68 +1385,6 @@ function createDispatcher(opts) {
1296
1385
  }
1297
1386
  return { steps, stop, winner };
1298
1387
  }
1299
- function buildRecord(r, walked, event, ctx, predicted, outcome) {
1300
- const view = ctx.viewId ?? null;
1301
- const candidates = /* @__PURE__ */ new Map();
1302
- const candidateOf = (m) => {
1303
- let c = candidates.get(m);
1304
- if (!c) {
1305
- const eligible = r.actionMap.get(m.binding.actionId)?.eligible;
1306
- c = {
1307
- actionId: m.binding.actionId,
1308
- routes: routesForSpec(m.binding.spec),
1309
- scope: m.scope,
1310
- ownerId: m.ownerId,
1311
- namesView: namesView(m.binding, view),
1312
- specificity: specificity(m.binding.spec),
1313
- ...eligible !== void 0 ? { eligible: describeEligible(eligible) } : {}
1314
- };
1315
- candidates.set(m, c);
1316
- }
1317
- return c;
1318
- };
1319
- const dropped = [
1320
- ...r.barred.map((m) => ({
1321
- candidate: candidateOf(m),
1322
- filter: "claim",
1323
- ...r.claimOwner !== void 0 ? { owner: r.claimOwner } : {}
1324
- }))
1325
- ];
1326
- for (const m of r.ordered) {
1327
- if (r.eligible(m)) continue;
1328
- const c = candidateOf(m);
1329
- dropped.push({ candidate: c, filter: "ineligible", rule: c.eligible ?? "" });
1330
- }
1331
- const gated = (m) => r.actionMap.get(m.binding.actionId)?.eligible !== void 0;
1332
- const ranked = walked.steps.map(({ match, step }, i) => {
1333
- const prev = walked.steps[i - 1]?.match;
1334
- return {
1335
- candidate: candidateOf(match),
1336
- placedBy: prev ? placedBy(prev, match, view, r.ruleCtx !== void 0 && gated(prev) && !gated(match)) : { step: "first" },
1337
- walk: step
1338
- };
1339
- });
1340
- const eventKey = event.kind === "key" || event.kind === "key-held" ? event.key : void 0;
1341
- const world = worldPointOf(event);
1342
- return {
1343
- kind: "dispatch",
1344
- ts: Date.now(),
1345
- input: {
1346
- eventKind: event.kind,
1347
- ...eventKey !== void 0 ? { key: eventKey } : {},
1348
- modifiers: modifiersOf(event),
1349
- viewId: view,
1350
- ...world !== void 0 ? { world } : {},
1351
- ...r.ruleCtx !== void 0 ? { mode: r.ruleCtx.mode } : {}
1352
- },
1353
- matched: [...r.ordered, ...r.barred].map(candidateOf),
1354
- dropped,
1355
- ranked,
1356
- predicted,
1357
- fired: walked.winner?.action.id ?? null,
1358
- outcome
1359
- };
1360
- }
1361
1388
  function resolveAll(event, ctx, resolveOpts) {
1362
1389
  const evaluateShadowed = resolveOpts?.evaluateShadowed ?? false;
1363
1390
  const r = rank(event, ctx, false);
@@ -1416,7 +1443,7 @@ function createDispatcher(opts) {
1416
1443
  function explain(event, ctx, explainOpts) {
1417
1444
  const r = rank(event, ctx, true);
1418
1445
  const walked = walk(r, event, ctx, null, explainOpts?.evaluateShadowed ?? false);
1419
- return buildRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1446
+ return buildDispatchRecord(r, walked, event, ctx, true, walked.winner ? "handled" : "unhandled");
1420
1447
  }
1421
1448
  function cancelAll(reason) {
1422
1449
  const stubCtx = {
@@ -1424,9 +1451,7 @@ function createDispatcher(opts) {
1424
1451
  modifiers: { alt: false, ctrl: false, meta: false, shift: false },
1425
1452
  deps: {}
1426
1453
  };
1427
- for (const handle of inFlightHandles.values()) {
1428
- handle.onEnd?.(stubCtx, reason);
1429
- }
1454
+ const ending = [...inFlightHandles];
1430
1455
  inFlightHandles.clear();
1431
1456
  inFlightOwners.clear();
1432
1457
  inFlightActions.clear();
@@ -1434,6 +1459,7 @@ function createDispatcher(opts) {
1434
1459
  dragOrigins.clear();
1435
1460
  dragPoints.clear();
1436
1461
  pinchStartSpreads.clear();
1462
+ for (const [gestureId, handle] of ending) runOnEnd(gestureId, handle, stubCtx, reason);
1437
1463
  }
1438
1464
  function inFlight() {
1439
1465
  return inFlightHandles;
@@ -1441,8 +1467,8 @@ function createDispatcher(opts) {
1441
1467
  function inFlightCursor() {
1442
1468
  let cursor = null;
1443
1469
  for (const action of inFlightActions.values()) {
1444
- const c = resolveCursor(action.activeCursor ?? action.cursor);
1445
- if (c) cursor = c;
1470
+ const spec = action.activeCursor ?? action.cursor;
1471
+ if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1446
1472
  }
1447
1473
  return cursor;
1448
1474
  }
@@ -1469,13 +1495,20 @@ function createDispatcher(opts) {
1469
1495
  return { kind: lastHandle.kind ?? null, id: lastId };
1470
1496
  }
1471
1497
  const handleInputWithNotify = (event, ctx) => {
1472
- const out = handleInput(event, ctx);
1473
- notify();
1474
- return out;
1498
+ const before = inFlightHandles.size;
1499
+ try {
1500
+ return handleInput(event, ctx);
1501
+ } finally {
1502
+ if (before > 0 || inFlightHandles.size > 0) notify();
1503
+ }
1475
1504
  };
1476
1505
  const cancelAllWithNotify = (reason) => {
1477
- cancelAll(reason);
1478
- notify();
1506
+ const before = inFlightHandles.size;
1507
+ try {
1508
+ cancelAll(reason);
1509
+ } finally {
1510
+ if (before > 0) notify();
1511
+ }
1479
1512
  };
1480
1513
  function beginUiOngoing(actionId, deps, params) {
1481
1514
  const getAction = opts?.getAction;
@@ -1520,6 +1553,9 @@ function createDispatcher(opts) {
1520
1553
  const live = () => !ended && inFlightHandles.get(gestureId) === handle;
1521
1554
  return {
1522
1555
  gestureId,
1556
+ get active() {
1557
+ return live();
1558
+ },
1523
1559
  update(nextParams) {
1524
1560
  if (!live()) return;
1525
1561
  if (!handle.onMove) return;
@@ -1688,33 +1724,45 @@ function findConflictsKeyed(entries, rules, modes = KIT_MODES) {
1688
1724
  const groups = /* @__PURE__ */ new Map();
1689
1725
  for (const entry of rows) {
1690
1726
  for (const arg of argKeysOf(entry)) {
1691
- const key = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}|${canonicalModifiers(entry.modifiers)}`;
1692
- const bucket = groups.get(key);
1693
- if (bucket) {
1694
- if (!bucket.includes(entry)) bucket.push(entry);
1695
- } else groups.set(key, [entry]);
1727
+ const base = `${entry.phase}|${entry.gesture}|${arg}|${targetKey(entry)}`;
1728
+ for (const state of modifierTieKeys(entry)) {
1729
+ const key = `${base}|${state}`;
1730
+ const bucket = groups.get(key);
1731
+ if (bucket) {
1732
+ if (!bucket.members.includes(entry)) bucket.members.push(entry);
1733
+ } else groups.set(key, { base, members: [entry] });
1734
+ }
1696
1735
  }
1697
1736
  }
1698
1737
  const ruleOf = (entry) => rules?.get(entry.ownerId) ?? ALWAYS;
1699
1738
  const tie = (e, f) => viewsTie(e.views, f.views) && rulesCanHoldTogether(ruleOf(e), ruleOf(f), modes);
1700
- const conflicts = [];
1701
- for (const [key, all] of groups) {
1702
- const bucket = all.filter((e) => all.some((f) => f !== e && tie(e, f)));
1739
+ const conflicts = /* @__PURE__ */ new Map();
1740
+ for (const { base, members } of groups.values()) {
1741
+ const bucket = members.filter((e) => members.some((f) => f !== e && tie(e, f)));
1703
1742
  if (bucket.length < 2) continue;
1704
1743
  const first = bucket[0];
1705
- conflicts.push({
1706
- key,
1707
- conflict: {
1708
- phase: first.phase,
1709
- gesture: first.gesture,
1710
- arg: first.arg,
1711
- target: first.target,
1712
- modifiers: first.modifiers,
1713
- ownerIds: bucket.map((e) => e.ownerId)
1714
- }
1744
+ const key = `${base}|${bucket.map((e) => `${objectId(e.spec)}:${canonicalModifiers(e.modifiers)}`).join(";")}`;
1745
+ if (conflicts.has(key)) continue;
1746
+ conflicts.set(key, {
1747
+ phase: first.phase,
1748
+ gesture: first.gesture,
1749
+ arg: first.arg,
1750
+ target: first.target,
1751
+ modifiers: first.modifiers,
1752
+ ownerIds: bucket.map((e) => e.ownerId)
1715
1753
  });
1716
1754
  }
1717
- return conflicts;
1755
+ return [...conflicts].map(([key, conflict]) => ({ key, conflict }));
1756
+ }
1757
+ function modifierTieKeys(entry) {
1758
+ const optional = VALID_MOD_NAMES.filter((k) => entry.modifiers[k] === "optional");
1759
+ const required = VALID_MOD_NAMES.filter((k) => entry.modifiers[k] === "required");
1760
+ const keys = [];
1761
+ for (let bits = 0; bits < 1 << optional.length; bits++) {
1762
+ const held = [...required, ...optional.filter((_, i) => (bits & 1 << i) !== 0)].sort();
1763
+ keys.push(`${held.join("+")}:${required.length}`);
1764
+ }
1765
+ return keys;
1718
1766
  }
1719
1767
  function viewsTie(a, b) {
1720
1768
  if (a === void 0 || b === void 0) return a === b;
@@ -1730,15 +1778,15 @@ function targetKey(entry) {
1730
1778
  const spec = entry.spec;
1731
1779
  const pred = spec.target;
1732
1780
  if (typeof pred !== "object" || pred === null) return PREDICATE_TARGET;
1733
- return `${PREDICATE_TARGET}#${predicateId(pred)}`;
1781
+ return `${PREDICATE_TARGET}#${objectId(pred)}`;
1734
1782
  }
1735
- var PREDICATE_IDS = /* @__PURE__ */ new WeakMap();
1736
- var nextPredicateId = 0;
1737
- function predicateId(pred) {
1738
- let id = PREDICATE_IDS.get(pred);
1783
+ var OBJECT_IDS = /* @__PURE__ */ new WeakMap();
1784
+ var nextObjectId = 0;
1785
+ function objectId(obj) {
1786
+ let id = OBJECT_IDS.get(obj);
1739
1787
  if (id === void 0) {
1740
- id = nextPredicateId++;
1741
- PREDICATE_IDS.set(pred, id);
1788
+ id = nextObjectId++;
1789
+ OBJECT_IDS.set(obj, id);
1742
1790
  }
1743
1791
  return id;
1744
1792
  }
@@ -1788,7 +1836,7 @@ function findScopedConflicts(scopes) {
1788
1836
  if (ambient.length > 1) add(findConflictsKeyed(ambient, rules, modes));
1789
1837
  if (gatedActions.length > 1) add(findConflictsKeyed(gatedActions, rules, modes));
1790
1838
  const hotkey = [
1791
- ...registry.filter((t) => t.def?.hotkey !== void 0),
1839
+ ...registry.filter((t) => t.eligibility?.offhand !== void 0 || t.def?.hotkey !== void 0),
1792
1840
  ...hotkeyActions
1793
1841
  ];
1794
1842
  if (hotkey.length > 1) add(findConflictsKeyed(hotkey, rules, modes));
@@ -1824,6 +1872,28 @@ function isEditableTarget(target) {
1824
1872
  const ce = target.getAttribute("contenteditable");
1825
1873
  return ce === "true" || ce === "";
1826
1874
  }
1875
+ var ACTIVATED_BY_KEY = [
1876
+ "button",
1877
+ "a[href]",
1878
+ "summary",
1879
+ "select",
1880
+ 'input[type="button"]',
1881
+ 'input[type="submit"]',
1882
+ 'input[type="reset"]',
1883
+ 'input[type="checkbox"]',
1884
+ 'input[type="radio"]',
1885
+ '[role="button"]',
1886
+ '[role="link"]',
1887
+ '[role="checkbox"]',
1888
+ '[role="switch"]',
1889
+ '[role="menuitem"]',
1890
+ '[role="tab"]',
1891
+ '[role="option"]'
1892
+ ].join(", ");
1893
+ function activatesFocusedControl(e) {
1894
+ if (e.key !== " " && e.key !== "Enter") return false;
1895
+ return e.target instanceof Element && e.target.matches(ACTIVATED_BY_KEY);
1896
+ }
1827
1897
  function keyMatches(eventKey, spec) {
1828
1898
  const want = typeof spec === "string" ? [spec] : spec;
1829
1899
  const ek = eventKey.toLowerCase();
@@ -1844,6 +1914,13 @@ function matchesKeyBinding(e, b) {
1844
1914
  }
1845
1915
  return true;
1846
1916
  }
1917
+ function historyKey(e) {
1918
+ const ev = e;
1919
+ if (matchesKeyBinding(ev, { key: "z", mod: true })) return "undo";
1920
+ if (matchesKeyBinding(ev, { key: "z", mod: true, shift: true }) || matchesKeyBinding(ev, { key: "y", mod: true }))
1921
+ return "redo";
1922
+ return null;
1923
+ }
1847
1924
 
1848
1925
  // src/ingestion/ingestItems.ts
1849
1926
  var INGEST_STRING_MIMES = /* @__PURE__ */ new Set([
@@ -1943,7 +2020,11 @@ function dlog(namespace, ...args) {
1943
2020
  if (!isDebugEnabled(namespace)) return;
1944
2021
  console.debug(`[${namespace}]`, ...args);
1945
2022
  }
2023
+ function dwarn(namespace, ...args) {
2024
+ if (!isDebugEnabled(namespace)) return;
2025
+ console.warn(`[${namespace}]`, ...args);
2026
+ }
1946
2027
 
1947
- export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, LONG_PRESS_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, depNodeOf, describeRule, dlog, evaluate, findConflicts, isDev, isEditableTarget, isTool, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
1948
- //# sourceMappingURL=chunk-RTCD26NN.js.map
1949
- //# sourceMappingURL=chunk-RTCD26NN.js.map
2028
+ export { ALWAYS, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, LONG_PRESS_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, activatesFocusedControl, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, depNodeOf, describeRule, dlog, dwarn, evaluate, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isDev, isEditableTarget, isTool, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, offhandKeyFor, openPointerSession, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
2029
+ //# sourceMappingURL=chunk-Y5RURZBH.js.map
2030
+ //# sourceMappingURL=chunk-Y5RURZBH.js.map