@weasel-js/routing 1.10.0 → 1.11.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,17 +1,38 @@
1
+ import { isDev } from '@weasel-js/react';
1
2
  import { createEmitter } from '@weasel-js/registry';
2
- import { RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, 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, routeGestureForSpecKind, getGestureDescriptor, formatRoute, parseTargetSpec, matchSpec, canonicalModifiers, UNKNOWN_MIME, specificity, VALID_MOD_NAMES, stylusOf } from '@weasel-js/gestures';
3
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';
4
5
  import { activeModeOf, NORMAL, DEFAULT_MODES, capabilitiesAllowed } from '@weasel-js/modes';
5
6
  import { resolveCursor } from '@weasel-js/cursor';
7
+ import { prefIsShown, prefIsEnabled, isPrefLeaf, prefAliasTarget, prefLeafAt, prefAliasedLeaf, prefNumberAt, resolvePrefUnit, prefDisplayBounds, prefUnitAccepts } from '@weasel-js/prefs';
8
+ import { endlessAllows } from '@weasel-js/quantity';
6
9
 
7
- // src/interactions/actions/action.ts
8
- var ActionDisabledReason = {
9
- SelectionRequired: "selection-required",
10
- SceneEmpty: "scene-empty",
11
- NotApplicable: "not-applicable",
12
- /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
13
- PredicateThrew: "predicate-threw"
14
- };
10
+ // src/interactions/actions/actionItems.ts
11
+ var NO_TAGS = Object.freeze([]);
12
+ function actionItems(action) {
13
+ if (action.invoker?.timing !== "immediate") return [];
14
+ if (!action.variants?.length) {
15
+ return [{ key: action.id, action, label: action.label, icon: action.icon, tags: action.tags ?? NO_TAGS }];
16
+ }
17
+ return action.variants.map((v) => ({
18
+ key: `${action.id}:${v.key}`,
19
+ action,
20
+ label: v.label,
21
+ icon: v.icon ?? action.icon,
22
+ tags: v.tags ? [...action.tags ?? NO_TAGS, ...v.tags] : action.tags ?? NO_TAGS,
23
+ params: v.params
24
+ }));
25
+ }
26
+ function actionItemTitle(item) {
27
+ if (item.key === item.action.id) return item.label;
28
+ const words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
29
+ const head = words(item.action.label);
30
+ const label = words(item.label);
31
+ for (let i = 0; i + head.length <= label.length; i++) {
32
+ if (head.every((w, j) => label[i + j] === w)) return item.label;
33
+ }
34
+ return `${item.action.label}: ${item.label}`;
35
+ }
15
36
 
16
37
  // src/interactions/actions/binding.ts
17
38
  function actionBindings(action) {
@@ -26,22 +47,60 @@ function actionBindings(action) {
26
47
  });
27
48
  }
28
49
 
29
- // src/devFlag.ts
30
- function isDev() {
31
- return typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
50
+ // src/interactions/actions/actionShortcuts.ts
51
+ function keySpecShortcut(spec) {
52
+ if (spec.kind !== "key" && spec.kind !== "key-held") return void 0;
53
+ const key = Array.isArray(spec.key) ? spec.key[0] : spec.key;
54
+ if (typeof key !== "string" || key.length === 0) return void 0;
55
+ const mods = "mods" in spec ? spec.mods : void 0;
56
+ return {
57
+ key,
58
+ mod: mods?.mod === true,
59
+ alt: mods?.alt === true,
60
+ shift: mods?.shift === true
61
+ };
62
+ }
63
+ function actionShortcuts(action, params) {
64
+ const out = [];
65
+ const seen = /* @__PURE__ */ new Set();
66
+ for (const { spec, opts } of actionBindings(action)) {
67
+ if (params && !paramsMatch(opts?.params, params)) continue;
68
+ const shortcut = keySpecShortcut(spec);
69
+ if (!shortcut) continue;
70
+ const token = `${shortcut.key}|${shortcut.mod}|${shortcut.alt}|${shortcut.shift}`;
71
+ if (seen.has(token)) continue;
72
+ seen.add(token);
73
+ out.push(shortcut);
74
+ }
75
+ return out;
76
+ }
77
+ function paramsMatch(bound, wanted) {
78
+ if (typeof bound !== "object" || bound === null) return false;
79
+ return Object.entries(wanted).every(([k, v]) => bound[k] === v);
32
80
  }
33
81
 
34
- // src/interactions/actions/actionEnabled.ts
82
+ // src/interactions/actions/action.ts
83
+ var ActionDisabledReason = {
84
+ SelectionRequired: "selection-required",
85
+ SceneEmpty: "scene-empty",
86
+ NotApplicable: "not-applicable",
87
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
88
+ PredicateThrew: "predicate-threw"
89
+ };
90
+ function actionCursor(action, params) {
91
+ const c = action?.cursor;
92
+ return typeof c === "function" ? c(params) : c;
93
+ }
35
94
  var enabledSlowWarned = /* @__PURE__ */ new Set();
36
95
  var enabledThrewWarned = /* @__PURE__ */ new Set();
37
96
  var ENABLED_BUDGET_MS = 4;
38
- function evaluateEnabled(action, deps) {
97
+ function evaluateEnabled(action, deps, at) {
39
98
  if (!action.enabled) return { enabled: true };
40
99
  const dev = isDev();
41
100
  const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
42
101
  const start = dev ? now() : 0;
43
102
  try {
44
- const result = action.enabled(deps);
103
+ const result = action.enabled(deps, at);
45
104
  if (dev) {
46
105
  const elapsed = now() - start;
47
106
  if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
@@ -72,15 +131,7 @@ function evaluateActive(action, deps) {
72
131
  return false;
73
132
  }
74
133
  }
75
-
76
- // src/interactions/actions/buildDeps.ts
77
- var DEV = (() => {
78
- try {
79
- return Boolean(import.meta.env?.DEV);
80
- } catch {
81
- return false;
82
- }
83
- })();
134
+ var DEV = isDev();
84
135
  function buildDepsFromRequires(action, depRegistry) {
85
136
  const requires = action.requires ?? [];
86
137
  const entries = requires.map((name) => [name, depRegistry.get(name)]);
@@ -281,6 +332,112 @@ function resolveParams(params) {
281
332
  if (params === void 0) return void 0;
282
333
  return typeof params === "function" ? params() : params;
283
334
  }
335
+ function keysHeld(keys, modifiers, isMac) {
336
+ return keys.some((k) => {
337
+ if (k === "mod") return isMac ? modifiers.meta : modifiers.ctrl;
338
+ return modifiers[k];
339
+ });
340
+ }
341
+ function roleKeysOf(roles, rebinds) {
342
+ if (!roles) return void 0;
343
+ const out = {};
344
+ for (const id of Object.keys(roles)) out[id] = rebinds?.[id] ?? roles[id].keys;
345
+ return out;
346
+ }
347
+ function platformRoleKeys(keys, isMac) {
348
+ const out = {};
349
+ for (const id of Object.keys(keys)) out[id] = keys[id].map((k) => k === "mod" ? isMac ? "meta" : "ctrl" : k);
350
+ return out;
351
+ }
352
+ function resolveRoles(keys, modifiers, isMac) {
353
+ const out = {};
354
+ for (const id of Object.keys(keys)) out[id] = keysHeld(keys[id], modifiers, isMac);
355
+ return out;
356
+ }
357
+ function rebindRoles(roles, rebinds) {
358
+ const out = {};
359
+ for (const id of Object.keys(roles)) {
360
+ out[id] = rebinds[id] ? { ...roles[id], keys: rebinds[id] } : roles[id];
361
+ }
362
+ return out;
363
+ }
364
+ function roleReader(roles) {
365
+ return (ctx, id) => ctx.roles?.[id] ?? keysHeld(roles[id].keys, ctx.modifiers, isApplePlatform());
366
+ }
367
+
368
+ // src/interactions/actions/tuning.ts
369
+ var WRAPS = /* @__PURE__ */ new WeakMap();
370
+ function layered(under, over) {
371
+ if (typeof under !== "function" && typeof over !== "function") return tunedParams([{ params: under }], over);
372
+ return () => tunedParams([{ params: under }], resolveParams(over));
373
+ }
374
+ function wrapInvoker(invoker, params) {
375
+ for (let w = invoker; w; w = WRAPS.get(w)?.inner) {
376
+ if (WRAPS.get(w)?.params === params) return invoker;
377
+ }
378
+ const base = (given) => tunedParams([{ params }], given);
379
+ const wrapped = invoker.timing === "immediate" ? { timing: "immediate", run: (deps, given) => invoker.run(deps, base(given)) } : {
380
+ timing: "ongoing",
381
+ start: (ctx, opts) => invoker.start(
382
+ ctx.params ? { ...ctx, params: base(ctx.params) } : ctx,
383
+ { ...opts, params: layered(params, opts?.params) }
384
+ )
385
+ };
386
+ WRAPS.set(wrapped, { inner: invoker, params });
387
+ return wrapped;
388
+ }
389
+ function withBindings(own, added) {
390
+ const base = own === void 0 ? [] : Array.isArray(own) ? own : [own];
391
+ return [...base, ...added.filter((b) => !base.includes(b))];
392
+ }
393
+ var CURSOR_WRAPS = /* @__PURE__ */ new WeakMap();
394
+ function wrapCursor(cursor, params) {
395
+ if (typeof cursor !== "function") return cursor;
396
+ for (let w = cursor; typeof w === "function"; w = CURSOR_WRAPS.get(w)?.inner) {
397
+ if (CURSOR_WRAPS.get(w)?.params === params) return cursor;
398
+ }
399
+ const wrapped = (given) => cursor(tunedParams([{ params }], given));
400
+ CURSOR_WRAPS.set(wrapped, { inner: cursor, params });
401
+ return wrapped;
402
+ }
403
+ var PREVIEW_WRAPS = /* @__PURE__ */ new WeakMap();
404
+ function wrapPreview(preview, params) {
405
+ if (!preview) return preview;
406
+ for (let w = preview; w; w = PREVIEW_WRAPS.get(w)?.inner) {
407
+ if (PREVIEW_WRAPS.get(w)?.params === params) return preview;
408
+ }
409
+ const wrapped = (deps, given) => preview(deps, tunedParams([{ params }], given));
410
+ PREVIEW_WRAPS.set(wrapped, { inner: preview, params });
411
+ return wrapped;
412
+ }
413
+ function tuneOnce(action, tuning) {
414
+ const invoker = tuning.params && action.invoker ? wrapInvoker(action.invoker, tuning.params) : action.invoker;
415
+ const cursor = tuning.params ? wrapCursor(action.cursor, tuning.params) : action.cursor;
416
+ const preview = tuning.params ? wrapPreview(action.preview, tuning.params) : action.preview;
417
+ const bindings = tuning.bindings?.length ? withBindings(action.defaultBinding, tuning.bindings) : null;
418
+ const modifierRoles = tuning.roles && action.modifierRoles ? rebindRoles(action.modifierRoles, tuning.roles) : null;
419
+ if (invoker === action.invoker && cursor === action.cursor && preview === action.preview && bindings === null && modifierRoles === null) {
420
+ return action;
421
+ }
422
+ return {
423
+ ...action,
424
+ ...modifierRoles ? { modifierRoles } : {},
425
+ ...invoker ? { invoker } : {},
426
+ ...cursor ? { cursor } : {},
427
+ ...preview ? { preview } : {},
428
+ ...bindings ? { defaultBinding: bindings } : {}
429
+ };
430
+ }
431
+ function tunedParams(tunings, given) {
432
+ let out = {};
433
+ for (const t of tunings) out = { ...out, ...resolveParams(t.params) };
434
+ return { ...out, ...given };
435
+ }
436
+ function tuneAction(action, tunings) {
437
+ let out = action;
438
+ for (const t of tunings) out = tuneOnce(out, t);
439
+ return out;
440
+ }
284
441
 
285
442
  // src/interactions/actions/toolOffhand.ts
286
443
  var TOOL_OFFHAND_ID = "tool.offhand";
@@ -301,8 +458,12 @@ function offhandKeyFor(trigger) {
301
458
  }
302
459
  }
303
460
  function buildToolOffhandBindings(specs) {
304
- return specs.map(({ toolId, key }) => ({ spec: heldKeySpec(key), opts: { params: { toolId } } }));
461
+ return specs.map(({ toolId, key }) => ({
462
+ spec: { ...heldKeySpec(key), phase: BETWEEN_GESTURES },
463
+ opts: { params: { toolId } }
464
+ }));
305
465
  }
466
+ var BETWEEN_GESTURES = [{ channel: "*", phase: "initial" }];
306
467
  function heldKeySpec(key) {
307
468
  const isModifier = [key].flat().some((k) => MODIFIER_KEYS.has(k));
308
469
  return { kind: "key-held", key, mods: { ...isModifier ? ANY_MODS : SHIFT_OR_ALT } };
@@ -409,6 +570,7 @@ function routeArgsForSpec(spec, fallback) {
409
570
  if ("key" in spec) return Array.isArray(spec.key) ? spec.key : [spec.key];
410
571
  if ("fingers" in spec) return [String(spec.fingers)];
411
572
  if ("direction" in spec) return [spec.direction ?? fallback];
573
+ if (spec.kind === "hover") return [spec.motion ?? fallback];
412
574
  if ("types" in spec) return [spec.types?.length ? spec.types.join("|") : fallback];
413
575
  return [fallback];
414
576
  }
@@ -771,15 +933,7 @@ function buildRuleCtx(args) {
771
933
  device: args.device
772
934
  };
773
935
  }
774
-
775
- // src/interactions/dispatcher/dispatchTrace.ts
776
- var DEV2 = (() => {
777
- try {
778
- return Boolean(import.meta.env?.DEV);
779
- } catch {
780
- return false;
781
- }
782
- })();
936
+ var DEV2 = isDev();
783
937
  var TRACE_LIMIT = 200;
784
938
  var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
785
939
  function recordTrace(entry) {
@@ -822,6 +976,9 @@ function gestureIdFor(event) {
822
976
  if (event.kind === "multitouch") {
823
977
  return multiTouchGestureId(event.fingers);
824
978
  }
979
+ if (event.kind === "carry") {
980
+ return `carry-${event.pointerId ?? "mouse"}`;
981
+ }
825
982
  return `ongoing-${event.kind}`;
826
983
  }
827
984
 
@@ -879,6 +1036,8 @@ function worldPointOf(event) {
879
1036
  case "doubleclick":
880
1037
  case "contextmenu":
881
1038
  return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
1039
+ case "carry":
1040
+ return { x: event.x, y: event.y };
882
1041
  default:
883
1042
  return void 0;
884
1043
  }
@@ -919,8 +1078,19 @@ function immediateParams(event, resolved) {
919
1078
  };
920
1079
  // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
921
1080
  // buffered copy matches `drag` specs, which are ongoing.
922
- case "pointerdown":
923
1081
  case "contextmenu":
1082
+ return {
1083
+ worldX: event.x,
1084
+ worldY: event.y,
1085
+ clientX: event.clientX,
1086
+ clientY: event.clientY,
1087
+ affordance: event.affordance,
1088
+ bodyTarget: event.bodyTarget,
1089
+ bodyKind: event.bodyKind,
1090
+ mods: modifiersOf(event),
1091
+ ...resolved
1092
+ };
1093
+ case "pointerdown":
924
1094
  case "longpress":
925
1095
  return {
926
1096
  worldX: event.x,
@@ -941,6 +1111,16 @@ function immediateParams(event, resolved) {
941
1111
  mods: modifiersOf(event),
942
1112
  ...resolved
943
1113
  };
1114
+ case "carry":
1115
+ return {
1116
+ worldX: event.x,
1117
+ worldY: event.y,
1118
+ clientX: event.clientX,
1119
+ clientY: event.clientY,
1120
+ carry: { type: event.type, value: event.value },
1121
+ mods: modifiersOf(event),
1122
+ ...resolved
1123
+ };
944
1124
  case "drop":
945
1125
  case "paste":
946
1126
  return {
@@ -962,13 +1142,13 @@ function liveInView(binding, view) {
962
1142
  function namesView(binding, view) {
963
1143
  return binding.opts?.views?.includes(view) ?? false;
964
1144
  }
965
- function preferViewScoped(matches, view) {
1145
+ function preferViewScoped(matches2, view) {
966
1146
  const named = [];
967
1147
  const rest = [];
968
- for (const m of matches) {
1148
+ for (const m of matches2) {
969
1149
  (namesView(m.binding, view) ? named : rest).push(m);
970
1150
  }
971
- return named.length === 0 ? [...matches] : [...named, ...rest];
1151
+ return named.length === 0 ? [...matches2] : [...named, ...rest];
972
1152
  }
973
1153
  function ruleCtxOf(ctx) {
974
1154
  const r = ctx.getRuleCtx?.();
@@ -984,7 +1164,7 @@ function isEligible(action, ruleCtx) {
984
1164
  function describeEligible(eligible) {
985
1165
  return describeRule(eligibleToRule(eligible));
986
1166
  }
987
- function preferContextual(matches, actionLookup, holds, view) {
1167
+ function preferContextual(matches2, actionLookup, holds, view) {
988
1168
  const out = [];
989
1169
  let run = [];
990
1170
  const flush = () => {
@@ -995,7 +1175,7 @@ function preferContextual(matches, actionLookup, holds, view) {
995
1175
  out.push(...gated, ...run.filter((m) => !gated.includes(m)));
996
1176
  run = [];
997
1177
  };
998
- for (const m of matches) {
1178
+ for (const m of matches2) {
999
1179
  const head = run[0];
1000
1180
  if (head && (head.scope !== m.scope || namesView(head.binding, view) !== namesView(m.binding, view) || !sameSpecificity(head.binding.spec, m.binding.spec))) flush();
1001
1181
  run.push(m);
@@ -1087,8 +1267,10 @@ function createGestureFacts() {
1087
1267
  const points = /* @__PURE__ */ new Map();
1088
1268
  const startSpreads = /* @__PURE__ */ new Map();
1089
1269
  const turns = /* @__PURE__ */ new Map();
1270
+ const roles = /* @__PURE__ */ new Map();
1090
1271
  return {
1091
- begin(gestureId, event) {
1272
+ begin(gestureId, event, roleKeys) {
1273
+ if (roleKeys) roles.set(gestureId, roleKeys);
1092
1274
  if (event.kind === "pointerdown") {
1093
1275
  origins.set(gestureId, {
1094
1276
  x: event.x ?? 0,
@@ -1121,7 +1303,9 @@ function createGestureFacts() {
1121
1303
  const trail = points.get(gestureId);
1122
1304
  const startSpread = startSpreads.get(gestureId);
1123
1305
  const turn = turns.get(gestureId);
1306
+ const roleKeys = roles.get(gestureId);
1124
1307
  return {
1308
+ ...roleKeys !== void 0 ? { roleKeys } : {},
1125
1309
  ...origin !== void 0 ? { origin } : {},
1126
1310
  ...trail !== void 0 ? { points: trail } : {},
1127
1311
  ...startSpread !== void 0 ? { startSpread } : {},
@@ -1133,12 +1317,14 @@ function createGestureFacts() {
1133
1317
  points.delete(gestureId);
1134
1318
  startSpreads.delete(gestureId);
1135
1319
  turns.delete(gestureId);
1320
+ roles.delete(gestureId);
1136
1321
  },
1137
1322
  clear() {
1138
1323
  origins.clear();
1139
1324
  points.clear();
1140
1325
  startSpreads.clear();
1141
1326
  turns.clear();
1327
+ roles.clear();
1142
1328
  }
1143
1329
  };
1144
1330
  }
@@ -1181,8 +1367,9 @@ function createInFlightGestures() {
1181
1367
  },
1182
1368
  cursor() {
1183
1369
  let cursor = null;
1184
- for (const action of actions.values()) {
1185
- const spec = action.activeCursor ?? action.cursor;
1370
+ for (const [id, handle] of handles) {
1371
+ const action = actions.get(id);
1372
+ const spec = handle.cursor?.() ?? action?.activeCursor ?? actionCursor(action);
1186
1373
  if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1187
1374
  }
1188
1375
  return cursor;
@@ -1274,6 +1461,7 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1274
1461
  };
1275
1462
  const screen = clientPoint(event);
1276
1463
  if (screen) base.screen = screen;
1464
+ if (facts.roleKeys) base.roles = resolveRoles(facts.roleKeys, base.modifiers, false);
1277
1465
  if (event.kind === "key") {
1278
1466
  base.key = { key: event.key, repeat: event.repeat ?? false };
1279
1467
  } else if (event.kind === "key-held") {
@@ -1288,6 +1476,9 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1288
1476
  Object.assign(base, pumpDrag(event, facts));
1289
1477
  } else if (event.kind === "multitouch") {
1290
1478
  base.multiTouch = multiTouchOf(event, facts);
1479
+ } else if (event.kind === "carry") {
1480
+ base.world = { x: event.x, y: event.y };
1481
+ base.carry = { type: event.type, value: event.value };
1291
1482
  }
1292
1483
  return base;
1293
1484
  }
@@ -1338,6 +1529,13 @@ function pumpGesture(run, event) {
1338
1529
  handle.onMove(ctxFor(run, event, {}, gestureId));
1339
1530
  return "handled";
1340
1531
  }
1532
+ if (event.kind === "wheel") {
1533
+ for (const [gestureId, handle] of [...inFlight.handles].reverse()) {
1534
+ if (handle.onWheel?.(ctxFor(run, event, {}, gestureId)) === true) return "handled";
1535
+ }
1536
+ return void 0;
1537
+ }
1538
+ if (event.kind === "carry") return pumpCarry(run, event);
1341
1539
  if (event.kind === "pointerup") return ended("commit");
1342
1540
  if (event.kind === "pointercancel") return ended("cancel");
1343
1541
  if (event.kind === "multitouch" && event.end !== void 0) {
@@ -1355,7 +1553,23 @@ function pumpGesture(run, event) {
1355
1553
  }
1356
1554
  return void 0;
1357
1555
  }
1556
+ function pumpCarry(run, event) {
1557
+ if (event.stage === "enter") return void 0;
1558
+ const gestureId = gestureIdFor(event);
1559
+ const handle = run.inFlight.handles.get(gestureId);
1560
+ if (!handle) return event.stage === "drop" ? void 0 : "unhandled";
1561
+ if (event.stage === "move") {
1562
+ handle.onMove?.(ctxFor(run, event, {}, gestureId));
1563
+ return "handled";
1564
+ }
1565
+ return endGesture(run, gestureId, event, event.stage === "drop" ? "commit" : "cancel") ? "handled" : "unhandled";
1566
+ }
1567
+ function carryStageFits(event, timing) {
1568
+ if (event.kind !== "carry") return true;
1569
+ return event.stage === (timing === "immediate" ? "drop" : "enter");
1570
+ }
1358
1571
  function invokeAction(run, event, match, action, deps) {
1572
+ if (action.invoker && !carryStageFits(event, action.invoker.timing)) return "empty-handle";
1359
1573
  if (action.invoker?.timing === "immediate") {
1360
1574
  try {
1361
1575
  const params = immediateParams(event, resolveParams(match.binding.opts?.params));
@@ -1374,7 +1588,8 @@ function invokeAction(run, event, match, action, deps) {
1374
1588
  return "misbound";
1375
1589
  }
1376
1590
  const gestureId = gestureIdFor(event);
1377
- run.facts.begin(gestureId, event);
1591
+ const roleKeys = roleKeysOf(action.modifierRoles, match.binding.opts?.roles);
1592
+ run.facts.begin(gestureId, event, roleKeys && platformRoleKeys(roleKeys, run.isMac()));
1378
1593
  const handle = action.invoker.start(ctxFor(run, event, deps, gestureId), match.binding.opts);
1379
1594
  if (isEmptyOngoingHandle(handle)) {
1380
1595
  run.facts.drop(gestureId);
@@ -1643,9 +1858,14 @@ function resolveOnly(event, ctx, engaged) {
1643
1858
  actionId: winner.action.id,
1644
1859
  action: winner.action,
1645
1860
  scope: winner.match.scope,
1646
- ownerId: winner.match.ownerId
1861
+ ownerId: winner.match.ownerId,
1862
+ ...paramsOf(winner.match.binding)
1647
1863
  };
1648
1864
  }
1865
+ function paramsOf(binding) {
1866
+ const params = resolveParams(binding.opts?.params);
1867
+ return params ? { params } : {};
1868
+ }
1649
1869
  function explain(event, ctx, engaged, opts) {
1650
1870
  const r = rank(event, ctx, engaged, true);
1651
1871
  const walked = walk(r, event, ctx, null, opts?.evaluateShadowed ?? false);
@@ -1664,10 +1884,12 @@ function createDispatcher(opts) {
1664
1884
  }
1665
1885
  const uiOngoing = createUiOngoing(inFlight, notify, opts?.getAction);
1666
1886
  let routedView;
1667
- const run = { inFlight, facts, viewId: () => routedView };
1887
+ let routedIsMac = false;
1888
+ const run = { inFlight, facts, viewId: () => routedView, isMac: () => routedIsMac };
1668
1889
  const invoking = (event) => (match, action, deps) => invokeAction(run, event, match, action, deps);
1669
1890
  function handleInput(event, ctx) {
1670
1891
  routedView = ctx.viewId;
1892
+ routedIsMac = ctx.isMac;
1671
1893
  const pumped = pumpGesture(run, event);
1672
1894
  if (pumped !== void 0) return pumped;
1673
1895
  if (event.kind === "hover") return hover(event, ctx);
@@ -1822,6 +2044,75 @@ function openPointerSession(origin, down, cb, opts = {}) {
1822
2044
  };
1823
2045
  }
1824
2046
 
2047
+ // src/interactions/carry/carry.ts
2048
+ var surfaces = /* @__PURE__ */ new Set();
2049
+ function addCarrySurface(surface) {
2050
+ surfaces.add(surface);
2051
+ return () => {
2052
+ surfaces.delete(surface);
2053
+ };
2054
+ }
2055
+ function contains(r, x, y) {
2056
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
2057
+ }
2058
+ function surfaceAt(doc, x, y) {
2059
+ const hit = typeof doc.elementFromPoint === "function" ? doc.elementFromPoint(x, y) : null;
2060
+ let best = null;
2061
+ for (const s of surfaces) {
2062
+ if (!contains(s.element.getBoundingClientRect(), x, y)) continue;
2063
+ if (hit && !s.element.contains(hit)) continue;
2064
+ if (best && s.element.contains(best.element)) continue;
2065
+ best = s;
2066
+ }
2067
+ return best;
2068
+ }
2069
+ function startCarry(origin, down, payload, cb = {}) {
2070
+ const doc = origin.ownerDocument ?? document;
2071
+ const pointerId = down.pointerId;
2072
+ let over = null;
2073
+ const leave = (at) => {
2074
+ const was = over;
2075
+ over = null;
2076
+ if (was && surfaces.has(was)) was.receive("leave", payload, at, pointerId);
2077
+ };
2078
+ const step = (at) => {
2079
+ const next = surfaceAt(doc, at.clientX, at.clientY);
2080
+ if (next === over) {
2081
+ over?.receive("move", payload, at, pointerId);
2082
+ return;
2083
+ }
2084
+ leave(at);
2085
+ over = next;
2086
+ next?.receive("enter", payload, at, pointerId);
2087
+ };
2088
+ let last = down;
2089
+ const session = openPointerSession(origin, down, {
2090
+ onMove: (e) => {
2091
+ last = e;
2092
+ step(e);
2093
+ cb.onMove?.(e);
2094
+ },
2095
+ onEnd: (e) => {
2096
+ step(e);
2097
+ const on = over;
2098
+ over = null;
2099
+ const taken = on !== null && surfaces.has(on) && on.receive("drop", payload, e, pointerId) === "handled";
2100
+ cb.onEnd?.({ taken, event: e });
2101
+ },
2102
+ onCancel: (reason) => {
2103
+ leave(last);
2104
+ cb.onCancel?.(reason);
2105
+ }
2106
+ });
2107
+ return {
2108
+ pointerId,
2109
+ get active() {
2110
+ return session.active;
2111
+ },
2112
+ cancel: () => session.cancel()
2113
+ };
2114
+ }
2115
+
1825
2116
  // src/tools/types.ts
1826
2117
  function isTool(entry) {
1827
2118
  return !!(entry.eligibility?.focus || entry.eligibility?.offhand);
@@ -2144,6 +2435,291 @@ function dwarn(namespace, ...args) {
2144
2435
  console.warn(`[${namespace}]`, ...args);
2145
2436
  }
2146
2437
 
2147
- export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DEV2 as DEV, DepNode, INGEST_STRING_MIMES, LONG_PRESS_HAPTIC_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, depNodeOf, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isDev, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, multiTouchGestureId, offhandKeyFor, openPointerSession, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveParams, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, targetConsultsAffordance, validateActionDefaultBinding, validateActionId };
2148
- //# sourceMappingURL=chunk-4DR6AJMN.js.map
2149
- //# sourceMappingURL=chunk-4DR6AJMN.js.map
2438
+ // src/menus/pickMenu.ts
2439
+ var oneOf = (want, got) => want === void 0 || (Array.isArray(want) ? want.includes(got) : want === got);
2440
+ function matches(when, hit) {
2441
+ return oneOf(when.bodyTarget, hit.bodyTarget) && oneOf(when.bodyKind, hit.bodyKind);
2442
+ }
2443
+ function mergeMenuDefs(defs, activeTool) {
2444
+ const live = [
2445
+ ...defs.filter((d) => d.owner === null),
2446
+ ...defs.filter((d) => d.owner !== null && d.owner === activeTool)
2447
+ ];
2448
+ const byId = /* @__PURE__ */ new Map();
2449
+ for (const { def } of live) {
2450
+ const prev = byId.get(def.id);
2451
+ if (!prev) {
2452
+ byId.set(def.id, { def: { ...def, menu: { ...def.menu, children: { ...def.menu.children } } }, parts: 1 });
2453
+ continue;
2454
+ }
2455
+ const n = prev.parts++;
2456
+ const children = prev.def.menu.children;
2457
+ children[`~sep${n}`] = { kind: "separator", name: "", description: "", default: void 0 };
2458
+ for (const [k, node] of Object.entries(def.menu.children)) {
2459
+ children[Object.hasOwn(children, k) ? `${k}~${n}` : k] = node;
2460
+ }
2461
+ if (!prev.def.when && def.when) prev.def.when = def.when;
2462
+ }
2463
+ return [...byId.values()].map((m) => m.def);
2464
+ }
2465
+ function pickMenu(defs, hit) {
2466
+ return defs.find((d) => d.when !== void 0 && matches(d.when, hit));
2467
+ }
2468
+ var SUBMENU_AS = /* @__PURE__ */ new Set(["menu", "page", "tab", "panel"]);
2469
+ var noop = () => {
2470
+ };
2471
+ var withAccess = (access) => access ? { access } : {};
2472
+ var sep = (key) => ({ kind: "separator", key });
2473
+ function itemOf(ctx, it, key, label, off, access) {
2474
+ const deps = ctx.src.depsFor(it.action);
2475
+ const point = ctx.at ? { worldX: ctx.at.x, worldY: ctx.at.y } : {};
2476
+ return {
2477
+ kind: "item",
2478
+ key,
2479
+ label,
2480
+ ...withAccess(access),
2481
+ icon: it.icon,
2482
+ shortcut: actionShortcuts(it.action, it.params)[0],
2483
+ disabled: off || !evaluateEnabled(it.action, deps, ctx.at).enabled,
2484
+ checked: evaluateActive(it.action, deps),
2485
+ run: () => {
2486
+ ctx.src.trigger(it.action.id, { ...ctx.params, ...it.params, ...point });
2487
+ }
2488
+ };
2489
+ }
2490
+ function itemsNamed(ctx, name) {
2491
+ const action = ctx.byId.get(name);
2492
+ if (action) return actionItems(action);
2493
+ const colon = name.indexOf(":");
2494
+ const owner = colon > 0 ? ctx.byId.get(name.slice(0, colon)) : void 0;
2495
+ return owner ? actionItems(owner).filter((it) => it.key === name) : [];
2496
+ }
2497
+ function actionRows(ctx, leaf, key, off) {
2498
+ const own = leaf.label || leaf.name;
2499
+ if (leaf.action === void 0) {
2500
+ if (!own) return [];
2501
+ if (isDev()) console.warn(`weasel: menu row "${key}" names no registered action, so it does nothing`);
2502
+ return [{ kind: "item", key, label: own, disabled: true, run: noop }];
2503
+ }
2504
+ const items = itemsNamed(ctx, leaf.action);
2505
+ if (items.length === 1) return [itemOf(ctx, items[0], key, own || items[0].label, off, leaf.access)];
2506
+ return items.map((it) => itemOf(ctx, it, `${key}/${it.key}`, it.label, off));
2507
+ }
2508
+ function siblingsOf(ctx, path) {
2509
+ const dot = path.lastIndexOf(".");
2510
+ const parent = dot < 0 ? void 0 : ctx.src.values(path.slice(0, dot));
2511
+ return parent && typeof parent === "object" ? parent : void 0;
2512
+ }
2513
+ function booleanRows(ctx, leaf, key, path, off) {
2514
+ const { values, onChange } = ctx.src;
2515
+ if (!values || !onChange) return [];
2516
+ const stored = values(path);
2517
+ const on = leaf.encoding ? leaf.encoding.read(stored, siblingsOf(ctx, path)) : stored === true;
2518
+ const write = (v) => leaf.encoding ? leaf.encoding.write(v, siblingsOf(ctx, path)) : v;
2519
+ return [{
2520
+ kind: "item",
2521
+ key,
2522
+ label: leaf.name,
2523
+ ...withAccess(leaf.access),
2524
+ checked: on,
2525
+ disabled: off || leaf.readout === true,
2526
+ run: leaf.readout ? noop : () => onChange(path, write(!on))
2527
+ }];
2528
+ }
2529
+ function enumRows(ctx, leaf, key, path, off) {
2530
+ const { values, onChange } = ctx.src;
2531
+ if (!values || !onChange) return [];
2532
+ const stored = values(path);
2533
+ const current = leaf.encoding ? leaf.encoding.read(stored, siblingsOf(ctx, path)) : stored;
2534
+ if (leaf.readout) {
2535
+ const shown = leaf.options.find((o) => o.value === current)?.label ?? "\u2014";
2536
+ return [{ kind: "item", key, label: `${leaf.name}: ${shown}`, disabled: true, run: noop }];
2537
+ }
2538
+ const write = (v) => leaf.encoding ? leaf.encoding.write(v, siblingsOf(ctx, path)) : v;
2539
+ return [
2540
+ sep(`${key}/<`),
2541
+ ...leaf.options.map((o) => ({
2542
+ kind: "item",
2543
+ key: `${key}/${o.value}`,
2544
+ label: o.label,
2545
+ checked: o.value === current,
2546
+ disabled: off || o.disabled === true,
2547
+ run: () => onChange(path, write(o.value))
2548
+ })),
2549
+ sep(`${key}/>`)
2550
+ ];
2551
+ }
2552
+ function numberRows(ctx, given, key, path, off) {
2553
+ const { values, onChange } = ctx.src;
2554
+ if (!values || !onChange) return [];
2555
+ const leaf = prefNumberAt(given, values);
2556
+ const unit = leaf.unit && resolvePrefUnit(leaf.unit);
2557
+ const stored = values(path);
2558
+ const { min, max, step, endless } = prefDisplayBounds(leaf);
2559
+ const disabled = off || leaf.readout === true;
2560
+ return [{
2561
+ kind: "number",
2562
+ key,
2563
+ label: leaf.name,
2564
+ ...withAccess(leaf.access),
2565
+ ...leaf.icon ? { icon: leaf.icon } : {},
2566
+ ...disabled ? { disabled } : {},
2567
+ value: typeof stored === "number" && (Number.isFinite(stored) || endlessAllows(leaf.endless, stored)) ? unit ? unit.toDisplay(stored) : stored : void 0,
2568
+ ...min === void 0 ? {} : { min },
2569
+ ...max === void 0 ? {} : { max },
2570
+ step,
2571
+ ...unit?.suffix === void 0 ? {} : { suffix: unit.suffix },
2572
+ ...leaf.display ? { display: leaf.display } : {},
2573
+ ...endless ? { endless } : {},
2574
+ ...leaf.infinity === void 0 ? {} : { infinity: leaf.infinity },
2575
+ ...unit ? { accepts: prefUnitAccepts(unit) } : {},
2576
+ set: (shown) => onChange(path, unit ? unit.fromDisplay(shown) : shown)
2577
+ }];
2578
+ }
2579
+ var textAt = (ctx, path) => {
2580
+ const v = ctx.src.values?.(path);
2581
+ return typeof v === "string" && v !== "" ? v : void 0;
2582
+ };
2583
+ function colorRows(ctx, leaf, key, path, off) {
2584
+ if (!ctx.src.values) return [];
2585
+ return [{ kind: "color", key, label: leaf.name, ...withAccess(leaf.access), ...off ? { disabled: true } : {}, value: textAt(ctx, path) }];
2586
+ }
2587
+ function stringRows(ctx, leaf, key, path) {
2588
+ if (!ctx.src.values) return [];
2589
+ return [{ kind: "item", key, label: `${leaf.name}: ${textAt(ctx, path) ?? "\u2014"}`, ...withAccess(leaf.access), disabled: true, run: noop }];
2590
+ }
2591
+ function leafRows(ctx, leaf, key, path, off) {
2592
+ switch (leaf.kind) {
2593
+ case "action":
2594
+ return actionRows(ctx, leaf, key, off);
2595
+ case "actions": {
2596
+ const group = leaf.group;
2597
+ return ctx.src.actions().filter((a) => a.group === group).flatMap((a) => actionItems(a).map((it) => itemOf(ctx, it, `${key}/${it.key}`, it.label, off)));
2598
+ }
2599
+ case "boolean":
2600
+ return booleanRows(ctx, leaf, key, path, off);
2601
+ case "enum":
2602
+ return enumRows(ctx, leaf, key, path, off);
2603
+ case "number":
2604
+ return numberRows(ctx, leaf, key, path, off);
2605
+ case "color":
2606
+ return colorRows(ctx, leaf, key, path, off);
2607
+ case "string":
2608
+ return stringRows(ctx, leaf, key, path);
2609
+ case "separator":
2610
+ return [sep(key)];
2611
+ case "label":
2612
+ return [{ kind: "item", key, label: leaf.name, ...withAccess(leaf.access), disabled: true, run: noop }];
2613
+ case "alias": {
2614
+ const alias = leaf;
2615
+ const target = prefAliasTarget(alias, (p) => prefLeafAt(ctx.src.over ?? ctx.root, p));
2616
+ return target ? leafRows(ctx, prefAliasedLeaf(alias, target.leaf), key, target.path, off) : [];
2617
+ }
2618
+ default:
2619
+ return [];
2620
+ }
2621
+ }
2622
+ function groupRows(ctx, group, prefix, off) {
2623
+ const out = [];
2624
+ for (const [k, node] of Object.entries(group.children)) {
2625
+ const path = prefix ? `${prefix}.${k}` : k;
2626
+ if (ctx.read && !prefIsShown(node, ctx.read)) continue;
2627
+ const disabled = off || ctx.read !== void 0 && !prefIsEnabled(node, ctx.read);
2628
+ if (isPrefLeaf(node)) {
2629
+ out.push(...leafRows(ctx, node, path, path, disabled));
2630
+ } else if (node.as !== void 0 && SUBMENU_AS.has(node.as)) {
2631
+ const entries = tidy(groupRows(ctx, node, path, disabled));
2632
+ if (entries.length) {
2633
+ out.push({ kind: "submenu", key: path, label: node.name, ...node.icon ? { icon: node.icon } : {}, ...withAccess(node.access), entries });
2634
+ }
2635
+ } else {
2636
+ out.push(sep(`${path}/<`), ...groupRows(ctx, node, path, disabled), sep(`${path}/>`));
2637
+ }
2638
+ }
2639
+ return out;
2640
+ }
2641
+ function tidy(entries) {
2642
+ const out = [];
2643
+ for (const e of entries) {
2644
+ if (e.kind === "separator" && (!out.length || out[out.length - 1].kind === "separator")) continue;
2645
+ out.push(e);
2646
+ }
2647
+ if (out[out.length - 1]?.kind === "separator") out.pop();
2648
+ return out;
2649
+ }
2650
+ function resolveSchemaMenu(schema, src, at, params) {
2651
+ const values = src.values;
2652
+ const ctx = {
2653
+ src,
2654
+ byId: new Map(src.actions().map((a) => [a.id, a])),
2655
+ root: schema,
2656
+ read: values ? (path, from) => from === "item" ? void 0 : values(path) : void 0,
2657
+ at,
2658
+ params
2659
+ };
2660
+ return tidy(groupRows(ctx, schema, "", false));
2661
+ }
2662
+ function resolveMenu(def, src, at, params) {
2663
+ return resolveSchemaMenu(def.menu, src, at, params);
2664
+ }
2665
+
2666
+ // src/menus/menuHost.ts
2667
+ function createMenuHost(src) {
2668
+ const defs = [];
2669
+ const listeners = /* @__PURE__ */ new Set();
2670
+ let open = null;
2671
+ let seq = 0;
2672
+ const emit = () => {
2673
+ for (const fn of listeners) fn();
2674
+ };
2675
+ const close = () => {
2676
+ if (!open) return;
2677
+ open = null;
2678
+ emit();
2679
+ };
2680
+ const resolve = (req) => {
2681
+ const live = mergeMenuDefs(defs, src.activeTool());
2682
+ const def = req.menu !== void 0 ? live.find((d) => d.id === req.menu) : req.hit ? pickMenu(live, req.hit) : void 0;
2683
+ return def ? resolveMenu(def, src, req.world, req.params) : [];
2684
+ };
2685
+ return {
2686
+ define(def, owner = null) {
2687
+ const entry = { def, owner };
2688
+ defs.push(entry);
2689
+ return () => {
2690
+ const i = defs.indexOf(entry);
2691
+ if (i >= 0) defs.splice(i, 1);
2692
+ };
2693
+ },
2694
+ open(req) {
2695
+ const entries = resolve(req);
2696
+ if (!entries.length) {
2697
+ close();
2698
+ return false;
2699
+ }
2700
+ open = {
2701
+ seq: ++seq,
2702
+ entries,
2703
+ client: req.client,
2704
+ world: req.world,
2705
+ ...req.under ? { under: req.under } : {},
2706
+ source: req.source ?? "pointer"
2707
+ };
2708
+ emit();
2709
+ return true;
2710
+ },
2711
+ has: (req) => resolve(req).length > 0,
2712
+ close,
2713
+ current: () => open,
2714
+ subscribe(fn) {
2715
+ listeners.add(fn);
2716
+ return () => {
2717
+ listeners.delete(fn);
2718
+ };
2719
+ }
2720
+ };
2721
+ }
2722
+
2723
+ export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DEV2 as DEV, DepNode, INGEST_STRING_MIMES, LONG_PRESS_HAPTIC_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, actionCursor, actionItemTitle, actionItems, actionShortcuts, addCarrySurface, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatcher, createLongPressStore, createMenuHost, depNodeOf, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, gestureIdFor, heldKeySpec, historyKey, isDebugEnabled, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, keySpecShortcut, keysHeld, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, mergeMenuDefs, multiTouchGestureId, offhandKeyFor, openPointerSession, pickMenu, pointerGestureId, publishLiveDispatch, pushOwner, recordModeSwitch, reportRouteConflicts, resolveDeviceProfile, resolveMenu, resolveParams, resolveSchemaMenu, roleReader, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, startCarry, targetConsultsAffordance, tuneAction, tunedParams, validateActionDefaultBinding, validateActionId };
2724
+ //# sourceMappingURL=chunk-QBQQIFPI.js.map
2725
+ //# sourceMappingURL=chunk-QBQQIFPI.js.map