@weasel-js/routing 1.10.1 → 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,18 +332,50 @@ 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
+ }
284
367
 
285
368
  // src/interactions/actions/tuning.ts
286
369
  var WRAPS = /* @__PURE__ */ new WeakMap();
287
370
  function layered(under, over) {
288
- if (typeof under !== "function" && typeof over !== "function") return { ...under, ...over };
289
- return () => ({ ...resolveParams(under), ...resolveParams(over) });
371
+ if (typeof under !== "function" && typeof over !== "function") return tunedParams([{ params: under }], over);
372
+ return () => tunedParams([{ params: under }], resolveParams(over));
290
373
  }
291
374
  function wrapInvoker(invoker, params) {
292
375
  for (let w = invoker; w; w = WRAPS.get(w)?.inner) {
293
376
  if (WRAPS.get(w)?.params === params) return invoker;
294
377
  }
295
- const base = (given) => ({ ...resolveParams(params), ...given });
378
+ const base = (given) => tunedParams([{ params }], given);
296
379
  const wrapped = invoker.timing === "immediate" ? { timing: "immediate", run: (deps, given) => invoker.run(deps, base(given)) } : {
297
380
  timing: "ongoing",
298
381
  start: (ctx, opts) => invoker.start(
@@ -307,16 +390,49 @@ function withBindings(own, added) {
307
390
  const base = own === void 0 ? [] : Array.isArray(own) ? own : [own];
308
391
  return [...base, ...added.filter((b) => !base.includes(b))];
309
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
+ }
310
413
  function tuneOnce(action, tuning) {
311
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;
312
417
  const bindings = tuning.bindings?.length ? withBindings(action.defaultBinding, tuning.bindings) : null;
313
- if (invoker === action.invoker && bindings === null) return action;
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
+ }
314
422
  return {
315
423
  ...action,
424
+ ...modifierRoles ? { modifierRoles } : {},
316
425
  ...invoker ? { invoker } : {},
426
+ ...cursor ? { cursor } : {},
427
+ ...preview ? { preview } : {},
317
428
  ...bindings ? { defaultBinding: bindings } : {}
318
429
  };
319
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
+ }
320
436
  function tuneAction(action, tunings) {
321
437
  let out = action;
322
438
  for (const t of tunings) out = tuneOnce(out, t);
@@ -817,15 +933,7 @@ function buildRuleCtx(args) {
817
933
  device: args.device
818
934
  };
819
935
  }
820
-
821
- // src/interactions/dispatcher/dispatchTrace.ts
822
- var DEV2 = (() => {
823
- try {
824
- return Boolean(import.meta.env?.DEV);
825
- } catch {
826
- return false;
827
- }
828
- })();
936
+ var DEV2 = isDev();
829
937
  var TRACE_LIMIT = 200;
830
938
  var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
831
939
  function recordTrace(entry) {
@@ -868,6 +976,9 @@ function gestureIdFor(event) {
868
976
  if (event.kind === "multitouch") {
869
977
  return multiTouchGestureId(event.fingers);
870
978
  }
979
+ if (event.kind === "carry") {
980
+ return `carry-${event.pointerId ?? "mouse"}`;
981
+ }
871
982
  return `ongoing-${event.kind}`;
872
983
  }
873
984
 
@@ -925,6 +1036,8 @@ function worldPointOf(event) {
925
1036
  case "doubleclick":
926
1037
  case "contextmenu":
927
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 };
928
1041
  default:
929
1042
  return void 0;
930
1043
  }
@@ -965,8 +1078,19 @@ function immediateParams(event, resolved) {
965
1078
  };
966
1079
  // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
967
1080
  // buffered copy matches `drag` specs, which are ongoing.
968
- case "pointerdown":
969
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":
970
1094
  case "longpress":
971
1095
  return {
972
1096
  worldX: event.x,
@@ -987,6 +1111,16 @@ function immediateParams(event, resolved) {
987
1111
  mods: modifiersOf(event),
988
1112
  ...resolved
989
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
+ };
990
1124
  case "drop":
991
1125
  case "paste":
992
1126
  return {
@@ -1008,13 +1142,13 @@ function liveInView(binding, view) {
1008
1142
  function namesView(binding, view) {
1009
1143
  return binding.opts?.views?.includes(view) ?? false;
1010
1144
  }
1011
- function preferViewScoped(matches, view) {
1145
+ function preferViewScoped(matches2, view) {
1012
1146
  const named = [];
1013
1147
  const rest = [];
1014
- for (const m of matches) {
1148
+ for (const m of matches2) {
1015
1149
  (namesView(m.binding, view) ? named : rest).push(m);
1016
1150
  }
1017
- return named.length === 0 ? [...matches] : [...named, ...rest];
1151
+ return named.length === 0 ? [...matches2] : [...named, ...rest];
1018
1152
  }
1019
1153
  function ruleCtxOf(ctx) {
1020
1154
  const r = ctx.getRuleCtx?.();
@@ -1030,7 +1164,7 @@ function isEligible(action, ruleCtx) {
1030
1164
  function describeEligible(eligible) {
1031
1165
  return describeRule(eligibleToRule(eligible));
1032
1166
  }
1033
- function preferContextual(matches, actionLookup, holds, view) {
1167
+ function preferContextual(matches2, actionLookup, holds, view) {
1034
1168
  const out = [];
1035
1169
  let run = [];
1036
1170
  const flush = () => {
@@ -1041,7 +1175,7 @@ function preferContextual(matches, actionLookup, holds, view) {
1041
1175
  out.push(...gated, ...run.filter((m) => !gated.includes(m)));
1042
1176
  run = [];
1043
1177
  };
1044
- for (const m of matches) {
1178
+ for (const m of matches2) {
1045
1179
  const head = run[0];
1046
1180
  if (head && (head.scope !== m.scope || namesView(head.binding, view) !== namesView(m.binding, view) || !sameSpecificity(head.binding.spec, m.binding.spec))) flush();
1047
1181
  run.push(m);
@@ -1133,8 +1267,10 @@ function createGestureFacts() {
1133
1267
  const points = /* @__PURE__ */ new Map();
1134
1268
  const startSpreads = /* @__PURE__ */ new Map();
1135
1269
  const turns = /* @__PURE__ */ new Map();
1270
+ const roles = /* @__PURE__ */ new Map();
1136
1271
  return {
1137
- begin(gestureId, event) {
1272
+ begin(gestureId, event, roleKeys) {
1273
+ if (roleKeys) roles.set(gestureId, roleKeys);
1138
1274
  if (event.kind === "pointerdown") {
1139
1275
  origins.set(gestureId, {
1140
1276
  x: event.x ?? 0,
@@ -1167,7 +1303,9 @@ function createGestureFacts() {
1167
1303
  const trail = points.get(gestureId);
1168
1304
  const startSpread = startSpreads.get(gestureId);
1169
1305
  const turn = turns.get(gestureId);
1306
+ const roleKeys = roles.get(gestureId);
1170
1307
  return {
1308
+ ...roleKeys !== void 0 ? { roleKeys } : {},
1171
1309
  ...origin !== void 0 ? { origin } : {},
1172
1310
  ...trail !== void 0 ? { points: trail } : {},
1173
1311
  ...startSpread !== void 0 ? { startSpread } : {},
@@ -1179,12 +1317,14 @@ function createGestureFacts() {
1179
1317
  points.delete(gestureId);
1180
1318
  startSpreads.delete(gestureId);
1181
1319
  turns.delete(gestureId);
1320
+ roles.delete(gestureId);
1182
1321
  },
1183
1322
  clear() {
1184
1323
  origins.clear();
1185
1324
  points.clear();
1186
1325
  startSpreads.clear();
1187
1326
  turns.clear();
1327
+ roles.clear();
1188
1328
  }
1189
1329
  };
1190
1330
  }
@@ -1229,7 +1369,7 @@ function createInFlightGestures() {
1229
1369
  let cursor = null;
1230
1370
  for (const [id, handle] of handles) {
1231
1371
  const action = actions.get(id);
1232
- const spec = handle.cursor?.() ?? action?.activeCursor ?? action?.cursor;
1372
+ const spec = handle.cursor?.() ?? action?.activeCursor ?? actionCursor(action);
1233
1373
  if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1234
1374
  }
1235
1375
  return cursor;
@@ -1321,6 +1461,7 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1321
1461
  };
1322
1462
  const screen = clientPoint(event);
1323
1463
  if (screen) base.screen = screen;
1464
+ if (facts.roleKeys) base.roles = resolveRoles(facts.roleKeys, base.modifiers, false);
1324
1465
  if (event.kind === "key") {
1325
1466
  base.key = { key: event.key, repeat: event.repeat ?? false };
1326
1467
  } else if (event.kind === "key-held") {
@@ -1335,6 +1476,9 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1335
1476
  Object.assign(base, pumpDrag(event, facts));
1336
1477
  } else if (event.kind === "multitouch") {
1337
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 };
1338
1482
  }
1339
1483
  return base;
1340
1484
  }
@@ -1385,6 +1529,13 @@ function pumpGesture(run, event) {
1385
1529
  handle.onMove(ctxFor(run, event, {}, gestureId));
1386
1530
  return "handled";
1387
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);
1388
1539
  if (event.kind === "pointerup") return ended("commit");
1389
1540
  if (event.kind === "pointercancel") return ended("cancel");
1390
1541
  if (event.kind === "multitouch" && event.end !== void 0) {
@@ -1402,7 +1553,23 @@ function pumpGesture(run, event) {
1402
1553
  }
1403
1554
  return void 0;
1404
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
+ }
1405
1571
  function invokeAction(run, event, match, action, deps) {
1572
+ if (action.invoker && !carryStageFits(event, action.invoker.timing)) return "empty-handle";
1406
1573
  if (action.invoker?.timing === "immediate") {
1407
1574
  try {
1408
1575
  const params = immediateParams(event, resolveParams(match.binding.opts?.params));
@@ -1421,7 +1588,8 @@ function invokeAction(run, event, match, action, deps) {
1421
1588
  return "misbound";
1422
1589
  }
1423
1590
  const gestureId = gestureIdFor(event);
1424
- 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()));
1425
1593
  const handle = action.invoker.start(ctxFor(run, event, deps, gestureId), match.binding.opts);
1426
1594
  if (isEmptyOngoingHandle(handle)) {
1427
1595
  run.facts.drop(gestureId);
@@ -1690,9 +1858,14 @@ function resolveOnly(event, ctx, engaged) {
1690
1858
  actionId: winner.action.id,
1691
1859
  action: winner.action,
1692
1860
  scope: winner.match.scope,
1693
- ownerId: winner.match.ownerId
1861
+ ownerId: winner.match.ownerId,
1862
+ ...paramsOf(winner.match.binding)
1694
1863
  };
1695
1864
  }
1865
+ function paramsOf(binding) {
1866
+ const params = resolveParams(binding.opts?.params);
1867
+ return params ? { params } : {};
1868
+ }
1696
1869
  function explain(event, ctx, engaged, opts) {
1697
1870
  const r = rank(event, ctx, engaged, true);
1698
1871
  const walked = walk(r, event, ctx, null, opts?.evaluateShadowed ?? false);
@@ -1711,10 +1884,12 @@ function createDispatcher(opts) {
1711
1884
  }
1712
1885
  const uiOngoing = createUiOngoing(inFlight, notify, opts?.getAction);
1713
1886
  let routedView;
1714
- const run = { inFlight, facts, viewId: () => routedView };
1887
+ let routedIsMac = false;
1888
+ const run = { inFlight, facts, viewId: () => routedView, isMac: () => routedIsMac };
1715
1889
  const invoking = (event) => (match, action, deps) => invokeAction(run, event, match, action, deps);
1716
1890
  function handleInput(event, ctx) {
1717
1891
  routedView = ctx.viewId;
1892
+ routedIsMac = ctx.isMac;
1718
1893
  const pumped = pumpGesture(run, event);
1719
1894
  if (pumped !== void 0) return pumped;
1720
1895
  if (event.kind === "hover") return hover(event, ctx);
@@ -1869,6 +2044,75 @@ function openPointerSession(origin, down, cb, opts = {}) {
1869
2044
  };
1870
2045
  }
1871
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
+
1872
2116
  // src/tools/types.ts
1873
2117
  function isTool(entry) {
1874
2118
  return !!(entry.eligibility?.focus || entry.eligibility?.offhand);
@@ -2191,6 +2435,291 @@ function dwarn(namespace, ...args) {
2191
2435
  console.warn(`[${namespace}]`, ...args);
2192
2436
  }
2193
2437
 
2194
- 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, tuneAction, validateActionDefaultBinding, validateActionId };
2195
- //# sourceMappingURL=chunk-VCEIQ64H.js.map
2196
- //# sourceMappingURL=chunk-VCEIQ64H.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