@weasel-js/routing 1.10.1 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,53 @@
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, prefValueAtPath } 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
+ }
36
+
37
+ // src/interactions/actions/actionSettings.ts
38
+ var EMPTY = { values: {} };
39
+ function readActionSettings(action, deps) {
40
+ const settings = action.settings;
41
+ if (!settings) return null;
42
+ try {
43
+ return settings.read(deps ?? {});
44
+ } catch {
45
+ return EMPTY;
46
+ }
47
+ }
48
+ function actionSettingsParams(settings, path, value) {
49
+ return settings.params ? settings.params(path, value) : { [path]: value };
50
+ }
15
51
 
16
52
  // src/interactions/actions/binding.ts
17
53
  function actionBindings(action) {
@@ -26,22 +62,60 @@ function actionBindings(action) {
26
62
  });
27
63
  }
28
64
 
29
- // src/devFlag.ts
30
- function isDev() {
31
- return typeof process !== "undefined" ? process.env?.NODE_ENV !== "production" : true;
65
+ // src/interactions/actions/actionShortcuts.ts
66
+ function keySpecShortcut(spec) {
67
+ if (spec.kind !== "key" && spec.kind !== "key-held") return void 0;
68
+ const key = Array.isArray(spec.key) ? spec.key[0] : spec.key;
69
+ if (typeof key !== "string" || key.length === 0) return void 0;
70
+ const mods = "mods" in spec ? spec.mods : void 0;
71
+ return {
72
+ key,
73
+ mod: mods?.mod === true,
74
+ alt: mods?.alt === true,
75
+ shift: mods?.shift === true
76
+ };
77
+ }
78
+ function actionShortcuts(action, params) {
79
+ const out = [];
80
+ const seen = /* @__PURE__ */ new Set();
81
+ for (const { spec, opts } of actionBindings(action)) {
82
+ if (params && !paramsMatch(opts?.params, params)) continue;
83
+ const shortcut = keySpecShortcut(spec);
84
+ if (!shortcut) continue;
85
+ const token = `${shortcut.key}|${shortcut.mod}|${shortcut.alt}|${shortcut.shift}`;
86
+ if (seen.has(token)) continue;
87
+ seen.add(token);
88
+ out.push(shortcut);
89
+ }
90
+ return out;
91
+ }
92
+ function paramsMatch(bound, wanted) {
93
+ if (typeof bound !== "object" || bound === null) return false;
94
+ return Object.entries(wanted).every(([k, v]) => bound[k] === v);
32
95
  }
33
96
 
34
- // src/interactions/actions/actionEnabled.ts
97
+ // src/interactions/actions/action.ts
98
+ var ActionDisabledReason = {
99
+ SelectionRequired: "selection-required",
100
+ SceneEmpty: "scene-empty",
101
+ NotApplicable: "not-applicable",
102
+ /** Sentinel: the predicate threw. Surfaced by `evaluateEnabled`'s catch. */
103
+ PredicateThrew: "predicate-threw"
104
+ };
105
+ function actionCursor(action, params) {
106
+ const c = action?.cursor;
107
+ return typeof c === "function" ? c(params) : c;
108
+ }
35
109
  var enabledSlowWarned = /* @__PURE__ */ new Set();
36
110
  var enabledThrewWarned = /* @__PURE__ */ new Set();
37
111
  var ENABLED_BUDGET_MS = 4;
38
- function evaluateEnabled(action, deps) {
112
+ function evaluateEnabled(action, deps, at) {
39
113
  if (!action.enabled) return { enabled: true };
40
114
  const dev = isDev();
41
115
  const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
42
116
  const start = dev ? now() : 0;
43
117
  try {
44
- const result = action.enabled(deps);
118
+ const result = action.enabled(deps, at);
45
119
  if (dev) {
46
120
  const elapsed = now() - start;
47
121
  if (elapsed > ENABLED_BUDGET_MS && !enabledSlowWarned.has(action.id)) {
@@ -72,15 +146,7 @@ function evaluateActive(action, deps) {
72
146
  return false;
73
147
  }
74
148
  }
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
- })();
149
+ var DEV = isDev();
84
150
  function buildDepsFromRequires(action, depRegistry) {
85
151
  const requires = action.requires ?? [];
86
152
  const entries = requires.map((name) => [name, depRegistry.get(name)]);
@@ -219,13 +285,21 @@ var DepNode = class extends ScopeNode {
219
285
  };
220
286
  },
221
287
  get: (name) => {
288
+ const stacks = [...this.start().chain()].map((n) => n.sources.get(name) ?? []);
289
+ return readFrom(stacks, 0, (stacks[0]?.length ?? 0) - 1);
290
+ },
291
+ subscribe: (listener) => this.tree.listeners.subscribe(listener),
292
+ list: () => {
293
+ const found = /* @__PURE__ */ new Map();
294
+ let depth = 0;
222
295
  for (const n of this.start().chain()) {
223
- const source = n.sources.get(name)?.at(-1);
224
- if (source) return source();
296
+ for (const [name, stack] of n.sources) {
297
+ if (!found.has(name)) found.set(name, { name, depth, stacked: stack.length });
298
+ }
299
+ depth++;
225
300
  }
226
- return void 0;
301
+ return [...found.values()].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
227
302
  },
228
- subscribe: (listener) => this.tree.listeners.subscribe(listener),
229
303
  changed: () => this.changed()
230
304
  };
231
305
  constructor(parent, descends) {
@@ -233,6 +307,11 @@ var DepNode = class extends ScopeNode {
233
307
  NODES.set(this.registry, this);
234
308
  }
235
309
  };
310
+ function readFrom(stacks, scope, index) {
311
+ if (scope >= stacks.length) return void 0;
312
+ if (index < 0) return readFrom(stacks, scope + 1, (stacks[scope + 1]?.length ?? 0) - 1);
313
+ return stacks[scope][index](() => readFrom(stacks, scope, index - 1));
314
+ }
236
315
  var NODES = /* @__PURE__ */ new WeakMap();
237
316
  function depNodeOf(registry) {
238
317
  return registry ? NODES.get(registry) ?? null : null;
@@ -281,18 +360,50 @@ function resolveParams(params) {
281
360
  if (params === void 0) return void 0;
282
361
  return typeof params === "function" ? params() : params;
283
362
  }
363
+ function keysHeld(keys, modifiers, isMac) {
364
+ return keys.some((k) => {
365
+ if (k === "mod") return isMac ? modifiers.meta : modifiers.ctrl;
366
+ return modifiers[k];
367
+ });
368
+ }
369
+ function roleKeysOf(roles, rebinds) {
370
+ if (!roles) return void 0;
371
+ const out = {};
372
+ for (const id of Object.keys(roles)) out[id] = rebinds?.[id] ?? roles[id].keys;
373
+ return out;
374
+ }
375
+ function platformRoleKeys(keys, isMac) {
376
+ const out = {};
377
+ for (const id of Object.keys(keys)) out[id] = keys[id].map((k) => k === "mod" ? isMac ? "meta" : "ctrl" : k);
378
+ return out;
379
+ }
380
+ function resolveRoles(keys, modifiers, isMac) {
381
+ const out = {};
382
+ for (const id of Object.keys(keys)) out[id] = keysHeld(keys[id], modifiers, isMac);
383
+ return out;
384
+ }
385
+ function rebindRoles(roles, rebinds) {
386
+ const out = {};
387
+ for (const id of Object.keys(roles)) {
388
+ out[id] = rebinds[id] ? { ...roles[id], keys: rebinds[id] } : roles[id];
389
+ }
390
+ return out;
391
+ }
392
+ function roleReader(roles) {
393
+ return (ctx, id) => ctx.roles?.[id] ?? keysHeld(roles[id].keys, ctx.modifiers, isApplePlatform());
394
+ }
284
395
 
285
396
  // src/interactions/actions/tuning.ts
286
397
  var WRAPS = /* @__PURE__ */ new WeakMap();
287
398
  function layered(under, over) {
288
- if (typeof under !== "function" && typeof over !== "function") return { ...under, ...over };
289
- return () => ({ ...resolveParams(under), ...resolveParams(over) });
399
+ if (typeof under !== "function" && typeof over !== "function") return tunedParams([{ params: under }], over);
400
+ return () => tunedParams([{ params: under }], resolveParams(over));
290
401
  }
291
402
  function wrapInvoker(invoker, params) {
292
403
  for (let w = invoker; w; w = WRAPS.get(w)?.inner) {
293
404
  if (WRAPS.get(w)?.params === params) return invoker;
294
405
  }
295
- const base = (given) => ({ ...resolveParams(params), ...given });
406
+ const base = (given) => tunedParams([{ params }], given);
296
407
  const wrapped = invoker.timing === "immediate" ? { timing: "immediate", run: (deps, given) => invoker.run(deps, base(given)) } : {
297
408
  timing: "ongoing",
298
409
  start: (ctx, opts) => invoker.start(
@@ -307,16 +418,49 @@ function withBindings(own, added) {
307
418
  const base = own === void 0 ? [] : Array.isArray(own) ? own : [own];
308
419
  return [...base, ...added.filter((b) => !base.includes(b))];
309
420
  }
421
+ var CURSOR_WRAPS = /* @__PURE__ */ new WeakMap();
422
+ function wrapCursor(cursor, params) {
423
+ if (typeof cursor !== "function") return cursor;
424
+ for (let w = cursor; typeof w === "function"; w = CURSOR_WRAPS.get(w)?.inner) {
425
+ if (CURSOR_WRAPS.get(w)?.params === params) return cursor;
426
+ }
427
+ const wrapped = (given) => cursor(tunedParams([{ params }], given));
428
+ CURSOR_WRAPS.set(wrapped, { inner: cursor, params });
429
+ return wrapped;
430
+ }
431
+ var PREVIEW_WRAPS = /* @__PURE__ */ new WeakMap();
432
+ function wrapPreview(preview, params) {
433
+ if (!preview) return preview;
434
+ for (let w = preview; w; w = PREVIEW_WRAPS.get(w)?.inner) {
435
+ if (PREVIEW_WRAPS.get(w)?.params === params) return preview;
436
+ }
437
+ const wrapped = (deps, given) => preview(deps, tunedParams([{ params }], given));
438
+ PREVIEW_WRAPS.set(wrapped, { inner: preview, params });
439
+ return wrapped;
440
+ }
310
441
  function tuneOnce(action, tuning) {
311
442
  const invoker = tuning.params && action.invoker ? wrapInvoker(action.invoker, tuning.params) : action.invoker;
443
+ const cursor = tuning.params ? wrapCursor(action.cursor, tuning.params) : action.cursor;
444
+ const preview = tuning.params ? wrapPreview(action.preview, tuning.params) : action.preview;
312
445
  const bindings = tuning.bindings?.length ? withBindings(action.defaultBinding, tuning.bindings) : null;
313
- if (invoker === action.invoker && bindings === null) return action;
446
+ const modifierRoles = tuning.roles && action.modifierRoles ? rebindRoles(action.modifierRoles, tuning.roles) : null;
447
+ if (invoker === action.invoker && cursor === action.cursor && preview === action.preview && bindings === null && modifierRoles === null) {
448
+ return action;
449
+ }
314
450
  return {
315
451
  ...action,
452
+ ...modifierRoles ? { modifierRoles } : {},
316
453
  ...invoker ? { invoker } : {},
454
+ ...cursor ? { cursor } : {},
455
+ ...preview ? { preview } : {},
317
456
  ...bindings ? { defaultBinding: bindings } : {}
318
457
  };
319
458
  }
459
+ function tunedParams(tunings, given) {
460
+ let out = {};
461
+ for (const t of tunings) out = { ...out, ...resolveParams(t.params) };
462
+ return { ...out, ...given };
463
+ }
320
464
  function tuneAction(action, tunings) {
321
465
  let out = action;
322
466
  for (const t of tunings) out = tuneOnce(out, t);
@@ -404,7 +548,8 @@ function routesForSpec(spec) {
404
548
  const phases = phaseAtomsForSpec("phase" in spec ? spec.phase : void 0);
405
549
  const target = descriptor.hasTarget ? routeTargetForSpec("target" in spec ? spec.target : void 0) ?? "*" : void 0;
406
550
  const args = descriptor.arg ? routeArgsForSpec(spec, descriptor.arg.default) : [void 0];
407
- return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers }));
551
+ const held = spec.kind === "hover" && spec.held !== void 0 ? { held: spec.held === true ? "required" : "optional" } : {};
552
+ return args.map((arg) => formatRoute({ phases, gesture, arg, target, modifiers, ...held }));
408
553
  }
409
554
  function entryFor(ownerId, spec, actionId) {
410
555
  const gesture = routeGestureForSpecKind(spec.kind);
@@ -555,11 +700,11 @@ function matchSortedWithBarred(e, bindings, isMac, engagedChannels, opts = {}) {
555
700
  const scopeBarred = [];
556
701
  for (const sb of bindings) {
557
702
  if (sb.scope !== scope) continue;
558
- const admitted = admits(sb);
559
- if (!admitted && !opts.collectBarred) continue;
703
+ const admitted2 = admits(sb);
704
+ if (!admitted2 && !opts.collectBarred) continue;
560
705
  const phaseCtx = { selfChannel: sb.ownerId, engagedChannels: engaged };
561
706
  if (matchSpec(e, sb.binding.spec, isMac, phaseCtx)) {
562
- (admitted ? scopeMatches : scopeBarred).push({ binding: sb.binding, scope, ownerId: sb.ownerId });
707
+ (admitted2 ? scopeMatches : scopeBarred).push({ binding: sb.binding, scope, ownerId: sb.ownerId });
563
708
  }
564
709
  }
565
710
  scopeMatches.sort((a, b) => compareSpecificity(a.binding.spec, b.binding.spec));
@@ -818,40 +963,13 @@ function buildRuleCtx(args) {
818
963
  };
819
964
  }
820
965
 
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
- })();
829
- var TRACE_LIMIT = 200;
830
- var traceLog = DEV2 && typeof window !== "undefined" ? window.__weaselDispatchLog__ ??= [] : [];
831
- function recordTrace(entry) {
832
- if (!DEV2) return;
833
- traceLog.push(entry);
834
- if (traceLog.length > TRACE_LIMIT) traceLog.shift();
835
- }
836
- function publishLiveDispatch(explain2) {
837
- if (!DEV2 || typeof window === "undefined") return;
838
- window.__weaselDispatchLive__ = explain2;
839
- }
840
- function recordModeSwitch(mode, from, to, detail) {
841
- recordTrace({
842
- kind: "mode",
843
- ts: Date.now(),
844
- mode,
845
- from,
846
- to,
847
- ...detail !== void 0 ? { detail } : {}
848
- });
849
- }
850
-
851
966
  // src/interactions/dispatcher/gestureIds.ts
852
967
  function pointerGestureId(pointerId) {
853
968
  return `pointer-${pointerId ?? "mouse"}`;
854
969
  }
970
+ function carryGestureId(pointerId) {
971
+ return `carry-${pointerId ?? "mouse"}`;
972
+ }
855
973
  function multiTouchGestureId(fingers) {
856
974
  return `multitouch-${fingers}`;
857
975
  }
@@ -868,6 +986,9 @@ function gestureIdFor(event) {
868
986
  if (event.kind === "multitouch") {
869
987
  return multiTouchGestureId(event.fingers);
870
988
  }
989
+ if (event.kind === "carry") {
990
+ return carryGestureId(event.pointerId);
991
+ }
871
992
  return `ongoing-${event.kind}`;
872
993
  }
873
994
 
@@ -911,6 +1032,51 @@ function assembleEntries(entries, inputs) {
911
1032
  }
912
1033
  };
913
1034
  }
1035
+ function createTraceRing(limit = 200, dev = isDev()) {
1036
+ const entries = [];
1037
+ const pushed = createEmitter();
1038
+ let total = 0;
1039
+ let watchers = 0;
1040
+ const recording = () => dev || watchers > 0;
1041
+ return {
1042
+ recording,
1043
+ push(entry) {
1044
+ if (!recording()) return;
1045
+ entries.push(entry);
1046
+ if (entries.length > limit) entries.shift();
1047
+ total++;
1048
+ pushed.emit();
1049
+ },
1050
+ since(cursor) {
1051
+ const first = total - entries.length;
1052
+ return { entries: entries.slice(Math.max(cursor, first) - first), cursor: total };
1053
+ },
1054
+ watch() {
1055
+ watchers++;
1056
+ let released = false;
1057
+ return () => {
1058
+ if (released) return;
1059
+ released = true;
1060
+ watchers--;
1061
+ };
1062
+ },
1063
+ subscribe: pushed.subscribe
1064
+ };
1065
+ }
1066
+ function createDispatchTrace(limit, dev) {
1067
+ const ring = createTraceRing(limit, dev);
1068
+ let explain2 = null;
1069
+ return {
1070
+ ...ring,
1071
+ recordModeSwitch(mode, from, to, detail) {
1072
+ ring.push({ kind: "mode", ts: Date.now(), mode, from, to, ...detail !== void 0 ? { detail } : {} });
1073
+ },
1074
+ setLive(next) {
1075
+ explain2 = next;
1076
+ },
1077
+ live: () => explain2?.() ?? null
1078
+ };
1079
+ }
914
1080
 
915
1081
  // src/interactions/dispatcher/inputFacts.ts
916
1082
  function modifiersOf(e) {
@@ -925,11 +1091,14 @@ function worldPointOf(event) {
925
1091
  case "doubleclick":
926
1092
  case "contextmenu":
927
1093
  return event.x !== void 0 && event.y !== void 0 ? { x: event.x, y: event.y } : void 0;
1094
+ case "carry":
1095
+ return { x: event.x, y: event.y };
928
1096
  default:
929
1097
  return void 0;
930
1098
  }
931
1099
  }
932
1100
  function carriedBy(event) {
1101
+ if (event.kind === "carry") return { items: [], carry: { type: event.type, value: event.value } };
933
1102
  return event.kind === "drop" || event.kind === "paste" ? { items: event.items } : void 0;
934
1103
  }
935
1104
  function immediateParams(event, resolved) {
@@ -965,8 +1134,19 @@ function immediateParams(event, resolved) {
965
1134
  };
966
1135
  // Only a `stage: 'press'` pointerdown reaches an immediate invoker: the
967
1136
  // buffered copy matches `drag` specs, which are ongoing.
968
- case "pointerdown":
969
1137
  case "contextmenu":
1138
+ return {
1139
+ worldX: event.x,
1140
+ worldY: event.y,
1141
+ clientX: event.clientX,
1142
+ clientY: event.clientY,
1143
+ affordance: event.affordance,
1144
+ bodyTarget: event.bodyTarget,
1145
+ bodyKind: event.bodyKind,
1146
+ mods: modifiersOf(event),
1147
+ ...resolved
1148
+ };
1149
+ case "pointerdown":
970
1150
  case "longpress":
971
1151
  return {
972
1152
  worldX: event.x,
@@ -985,6 +1165,20 @@ function immediateParams(event, resolved) {
985
1165
  affordance: event.affordance,
986
1166
  bodyTarget: event.bodyTarget,
987
1167
  mods: modifiersOf(event),
1168
+ held: event.held === true,
1169
+ ...resolved
1170
+ };
1171
+ case "carry":
1172
+ return {
1173
+ worldX: event.x,
1174
+ worldY: event.y,
1175
+ clientX: event.clientX,
1176
+ clientY: event.clientY,
1177
+ carry: { type: event.type, value: event.value },
1178
+ affordance: event.affordance,
1179
+ bodyTarget: event.bodyTarget,
1180
+ bodyKind: event.bodyKind,
1181
+ mods: modifiersOf(event),
988
1182
  ...resolved
989
1183
  };
990
1184
  case "drop":
@@ -1005,16 +1199,20 @@ function liveInView(binding, view) {
1005
1199
  const views = binding.opts?.views;
1006
1200
  return views === void 0 || views.includes(view);
1007
1201
  }
1202
+ function liveForKeys(binding, reach) {
1203
+ if (reach === void 0) return true;
1204
+ return binding.opts?.over ? reach.over : reach.held;
1205
+ }
1008
1206
  function namesView(binding, view) {
1009
1207
  return binding.opts?.views?.includes(view) ?? false;
1010
1208
  }
1011
- function preferViewScoped(matches, view) {
1209
+ function preferViewScoped(matches2, view) {
1012
1210
  const named = [];
1013
1211
  const rest = [];
1014
- for (const m of matches) {
1212
+ for (const m of matches2) {
1015
1213
  (namesView(m.binding, view) ? named : rest).push(m);
1016
1214
  }
1017
- return named.length === 0 ? [...matches] : [...named, ...rest];
1215
+ return named.length === 0 ? [...matches2] : [...named, ...rest];
1018
1216
  }
1019
1217
  function ruleCtxOf(ctx) {
1020
1218
  const r = ctx.getRuleCtx?.();
@@ -1030,7 +1228,7 @@ function isEligible(action, ruleCtx) {
1030
1228
  function describeEligible(eligible) {
1031
1229
  return describeRule(eligibleToRule(eligible));
1032
1230
  }
1033
- function preferContextual(matches, actionLookup, holds, view) {
1231
+ function preferContextual(matches2, actionLookup, holds, view) {
1034
1232
  const out = [];
1035
1233
  let run = [];
1036
1234
  const flush = () => {
@@ -1041,7 +1239,7 @@ function preferContextual(matches, actionLookup, holds, view) {
1041
1239
  out.push(...gated, ...run.filter((m) => !gated.includes(m)));
1042
1240
  run = [];
1043
1241
  };
1044
- for (const m of matches) {
1242
+ for (const m of matches2) {
1045
1243
  const head = run[0];
1046
1244
  if (head && (head.scope !== m.scope || namesView(head.binding, view) !== namesView(m.binding, view) || !sameSpecificity(head.binding.spec, m.binding.spec))) flush();
1047
1245
  run.push(m);
@@ -1133,8 +1331,10 @@ function createGestureFacts() {
1133
1331
  const points = /* @__PURE__ */ new Map();
1134
1332
  const startSpreads = /* @__PURE__ */ new Map();
1135
1333
  const turns = /* @__PURE__ */ new Map();
1334
+ const roles = /* @__PURE__ */ new Map();
1136
1335
  return {
1137
- begin(gestureId, event) {
1336
+ begin(gestureId, event, roleKeys) {
1337
+ if (roleKeys) roles.set(gestureId, roleKeys);
1138
1338
  if (event.kind === "pointerdown") {
1139
1339
  origins.set(gestureId, {
1140
1340
  x: event.x ?? 0,
@@ -1167,7 +1367,9 @@ function createGestureFacts() {
1167
1367
  const trail = points.get(gestureId);
1168
1368
  const startSpread = startSpreads.get(gestureId);
1169
1369
  const turn = turns.get(gestureId);
1370
+ const roleKeys = roles.get(gestureId);
1170
1371
  return {
1372
+ ...roleKeys !== void 0 ? { roleKeys } : {},
1171
1373
  ...origin !== void 0 ? { origin } : {},
1172
1374
  ...trail !== void 0 ? { points: trail } : {},
1173
1375
  ...startSpread !== void 0 ? { startSpread } : {},
@@ -1179,12 +1381,14 @@ function createGestureFacts() {
1179
1381
  points.delete(gestureId);
1180
1382
  startSpreads.delete(gestureId);
1181
1383
  turns.delete(gestureId);
1384
+ roles.delete(gestureId);
1182
1385
  },
1183
1386
  clear() {
1184
1387
  origins.clear();
1185
1388
  points.clear();
1186
1389
  startSpreads.clear();
1187
1390
  turns.clear();
1391
+ roles.clear();
1188
1392
  }
1189
1393
  };
1190
1394
  }
@@ -1229,7 +1433,7 @@ function createInFlightGestures() {
1229
1433
  let cursor = null;
1230
1434
  for (const [id, handle] of handles) {
1231
1435
  const action = actions.get(id);
1232
- const spec = handle.cursor?.() ?? action?.activeCursor ?? action?.cursor;
1436
+ const spec = handle.cursor?.() ?? action?.activeCursor ?? actionCursor(action);
1233
1437
  if (spec !== void 0 && resolveCursor(spec)) cursor = spec;
1234
1438
  }
1235
1439
  return cursor;
@@ -1321,6 +1525,7 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1321
1525
  };
1322
1526
  const screen = clientPoint(event);
1323
1527
  if (screen) base.screen = screen;
1528
+ if (facts.roleKeys) base.roles = resolveRoles(facts.roleKeys, base.modifiers, false);
1324
1529
  if (event.kind === "key") {
1325
1530
  base.key = { key: event.key, repeat: event.repeat ?? false };
1326
1531
  } else if (event.kind === "key-held") {
@@ -1335,6 +1540,9 @@ function buildInvocationCtx(event, deps, viewId, facts = {}) {
1335
1540
  Object.assign(base, pumpDrag(event, facts));
1336
1541
  } else if (event.kind === "multitouch") {
1337
1542
  base.multiTouch = multiTouchOf(event, facts);
1543
+ } else if (event.kind === "carry") {
1544
+ base.world = { x: event.x, y: event.y };
1545
+ base.carry = { type: event.type, value: event.value };
1338
1546
  }
1339
1547
  return base;
1340
1548
  }
@@ -1385,6 +1593,13 @@ function pumpGesture(run, event) {
1385
1593
  handle.onMove(ctxFor(run, event, {}, gestureId));
1386
1594
  return "handled";
1387
1595
  }
1596
+ if (event.kind === "wheel") {
1597
+ for (const [gestureId, handle] of [...inFlight.handles].reverse()) {
1598
+ if (handle.onWheel?.(ctxFor(run, event, {}, gestureId)) === true) return "handled";
1599
+ }
1600
+ return void 0;
1601
+ }
1602
+ if (event.kind === "carry") return pumpCarry(run, event);
1388
1603
  if (event.kind === "pointerup") return ended("commit");
1389
1604
  if (event.kind === "pointercancel") return ended("cancel");
1390
1605
  if (event.kind === "multitouch" && event.end !== void 0) {
@@ -1402,7 +1617,23 @@ function pumpGesture(run, event) {
1402
1617
  }
1403
1618
  return void 0;
1404
1619
  }
1620
+ function pumpCarry(run, event) {
1621
+ if (event.stage === "enter") return void 0;
1622
+ const gestureId = gestureIdFor(event);
1623
+ const handle = run.inFlight.handles.get(gestureId);
1624
+ if (!handle) return event.stage === "leave" ? "unhandled" : void 0;
1625
+ if (event.stage === "move") {
1626
+ handle.onMove?.(ctxFor(run, event, {}, gestureId));
1627
+ return "handled";
1628
+ }
1629
+ return endGesture(run, gestureId, event, event.stage === "drop" ? "commit" : "cancel") ? "handled" : "unhandled";
1630
+ }
1631
+ function carryStageFits(event, timing) {
1632
+ if (event.kind !== "carry") return true;
1633
+ return timing === "immediate" ? event.stage === "drop" : event.stage === "enter" || event.stage === "move";
1634
+ }
1405
1635
  function invokeAction(run, event, match, action, deps) {
1636
+ if (action.invoker && !carryStageFits(event, action.invoker.timing)) return "empty-handle";
1406
1637
  if (action.invoker?.timing === "immediate") {
1407
1638
  try {
1408
1639
  const params = immediateParams(event, resolveParams(match.binding.opts?.params));
@@ -1421,7 +1652,8 @@ function invokeAction(run, event, match, action, deps) {
1421
1652
  return "misbound";
1422
1653
  }
1423
1654
  const gestureId = gestureIdFor(event);
1424
- run.facts.begin(gestureId, event);
1655
+ const roleKeys = roleKeysOf(action.modifierRoles, match.binding.opts?.roles);
1656
+ run.facts.begin(gestureId, event, roleKeys && platformRoleKeys(roleKeys, run.isMac()));
1425
1657
  const handle = action.invoker.start(ctxFor(run, event, deps, gestureId), match.binding.opts);
1426
1658
  if (isEmptyOngoingHandle(handle)) {
1427
1659
  run.facts.drop(gestureId);
@@ -1528,16 +1760,20 @@ function assembleScopedBindings(ctx) {
1528
1760
  ...allowedCapabilities ? { allowedCapabilities } : {}
1529
1761
  });
1530
1762
  const view = ctx.viewId ?? null;
1531
- const result = scopeBindings(ordered, state).filter((sb) => liveInView(sb.binding, view));
1763
+ const live = (b) => liveInView(b, view) && liveForKeys(b, ctx.keyReach);
1764
+ const result = scopeBindings(ordered, state).filter((sb) => live(sb.binding));
1532
1765
  for (const action of ctx.actions.list()) {
1533
1766
  const targetScope = action.scope === "hotkey" ? "hotkey" : "ambient";
1534
1767
  for (const binding of actionBindings(action)) {
1535
- if (!liveInView(binding, view)) continue;
1768
+ if (!live(binding)) continue;
1536
1769
  result.push({ binding, scope: targetScope, ownerId: null });
1537
1770
  }
1538
1771
  }
1539
1772
  return result;
1540
1773
  }
1774
+ function anyBinding(ctx, test) {
1775
+ return assembleScopedBindings(ctx).some((sb) => test(sb.binding.spec));
1776
+ }
1541
1777
  function buildActionMap(registry) {
1542
1778
  const map = /* @__PURE__ */ new Map();
1543
1779
  for (const action of registry.list()) {
@@ -1690,9 +1926,14 @@ function resolveOnly(event, ctx, engaged) {
1690
1926
  actionId: winner.action.id,
1691
1927
  action: winner.action,
1692
1928
  scope: winner.match.scope,
1693
- ownerId: winner.match.ownerId
1929
+ ownerId: winner.match.ownerId,
1930
+ ...paramsOf(winner.match.binding)
1694
1931
  };
1695
1932
  }
1933
+ function paramsOf(binding) {
1934
+ const params = resolveParams(binding.opts?.params);
1935
+ return params ? { params } : {};
1936
+ }
1696
1937
  function explain(event, ctx, engaged, opts) {
1697
1938
  const r = rank(event, ctx, engaged, true);
1698
1939
  const walked = walk(r, event, ctx, null, opts?.evaluateShadowed ?? false);
@@ -1711,17 +1952,20 @@ function createDispatcher(opts) {
1711
1952
  }
1712
1953
  const uiOngoing = createUiOngoing(inFlight, notify, opts?.getAction);
1713
1954
  let routedView;
1714
- const run = { inFlight, facts, viewId: () => routedView };
1955
+ let routedIsMac = false;
1956
+ const run = { inFlight, facts, viewId: () => routedView, isMac: () => routedIsMac };
1715
1957
  const invoking = (event) => (match, action, deps) => invokeAction(run, event, match, action, deps);
1716
1958
  function handleInput(event, ctx) {
1717
1959
  routedView = ctx.viewId;
1960
+ routedIsMac = ctx.isMac;
1718
1961
  const pumped = pumpGesture(run, event);
1719
1962
  if (pumped !== void 0) return pumped;
1720
1963
  if (event.kind === "hover") return hover(event, ctx);
1721
- const r = rank(event, ctx, inFlight.engagedOwners(), DEV2);
1964
+ const trace = ctx.trace?.recording() ? ctx.trace : null;
1965
+ const r = rank(event, ctx, inFlight.engagedOwners(), trace !== null);
1722
1966
  const walked = walk(r, event, ctx, invoking(event), false);
1723
1967
  const outcome = walked.stop === "fired" ? "handled" : "unhandled";
1724
- if (DEV2) recordTrace(buildDispatchRecord(r, walked, event, ctx, false, outcome));
1968
+ trace?.push(buildDispatchRecord(r, walked, event, ctx, false, outcome));
1725
1969
  return outcome;
1726
1970
  }
1727
1971
  function hover(event, ctx) {
@@ -1869,6 +2113,75 @@ function openPointerSession(origin, down, cb, opts = {}) {
1869
2113
  };
1870
2114
  }
1871
2115
 
2116
+ // src/interactions/carry/carry.ts
2117
+ var surfaces = /* @__PURE__ */ new Set();
2118
+ function addCarrySurface(surface) {
2119
+ surfaces.add(surface);
2120
+ return () => {
2121
+ surfaces.delete(surface);
2122
+ };
2123
+ }
2124
+ function contains(r, x, y) {
2125
+ return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
2126
+ }
2127
+ function surfaceAt(doc, x, y) {
2128
+ const hit = typeof doc.elementFromPoint === "function" ? doc.elementFromPoint(x, y) : null;
2129
+ let best = null;
2130
+ for (const s of surfaces) {
2131
+ if (!contains(s.element.getBoundingClientRect(), x, y)) continue;
2132
+ if (hit && !s.element.contains(hit)) continue;
2133
+ if (best && s.element.contains(best.element)) continue;
2134
+ best = s;
2135
+ }
2136
+ return best;
2137
+ }
2138
+ function startCarry(origin, down, payload, cb = {}) {
2139
+ const doc = origin.ownerDocument ?? document;
2140
+ const pointerId = down.pointerId;
2141
+ let over = null;
2142
+ const leave = (at) => {
2143
+ const was = over;
2144
+ over = null;
2145
+ if (was && surfaces.has(was)) was.receive("leave", payload, at, pointerId);
2146
+ };
2147
+ const step = (at) => {
2148
+ const next = surfaceAt(doc, at.clientX, at.clientY);
2149
+ if (next === over) {
2150
+ over?.receive("move", payload, at, pointerId);
2151
+ return;
2152
+ }
2153
+ leave(at);
2154
+ over = next;
2155
+ next?.receive("enter", payload, at, pointerId);
2156
+ };
2157
+ let last = down;
2158
+ const session = openPointerSession(origin, down, {
2159
+ onMove: (e) => {
2160
+ last = e;
2161
+ step(e);
2162
+ cb.onMove?.(e);
2163
+ },
2164
+ onEnd: (e) => {
2165
+ step(e);
2166
+ const on = over;
2167
+ over = null;
2168
+ const taken = on !== null && surfaces.has(on) && on.receive("drop", payload, e, pointerId) === "handled";
2169
+ cb.onEnd?.({ taken, event: e });
2170
+ },
2171
+ onCancel: (reason) => {
2172
+ leave(last);
2173
+ cb.onCancel?.(reason);
2174
+ }
2175
+ });
2176
+ return {
2177
+ pointerId,
2178
+ get active() {
2179
+ return session.active;
2180
+ },
2181
+ cancel: () => session.cancel()
2182
+ };
2183
+ }
2184
+
1872
2185
  // src/tools/types.ts
1873
2186
  function isTool(entry) {
1874
2187
  return !!(entry.eligibility?.focus || entry.eligibility?.offhand);
@@ -2089,6 +2402,20 @@ function normalizeMime(raw) {
2089
2402
  const bare = raw.split(";")[0].trim().toLowerCase();
2090
2403
  return bare || FALLBACK_MIME;
2091
2404
  }
2405
+ var admitted = /* @__PURE__ */ new Map();
2406
+ function admitIngestMime(mime) {
2407
+ const key = normalizeMime(mime);
2408
+ admitted.set(key, (admitted.get(key) ?? 0) + 1);
2409
+ let released = false;
2410
+ return () => {
2411
+ if (released) return;
2412
+ released = true;
2413
+ const n = (admitted.get(key) ?? 1) - 1;
2414
+ if (n > 0) admitted.set(key, n);
2415
+ else admitted.delete(key);
2416
+ };
2417
+ }
2418
+ var keepsString = (mime) => INGEST_STRING_MIMES.has(mime) || admitted.has(mime);
2092
2419
  function itemsFromFiles(files) {
2093
2420
  return files.map((file) => ({ kind: "file", mime: normalizeMime(file.type), file }));
2094
2421
  }
@@ -2102,7 +2429,7 @@ function itemsFromDataTransfer(dt) {
2102
2429
  if (file) files.push({ kind: "file", mime: normalizeMime(file.type), file });
2103
2430
  } else if (item.kind === "string") {
2104
2431
  const mime = normalizeMime(item.type);
2105
- if (INGEST_STRING_MIMES.has(mime)) {
2432
+ if (keepsString(mime)) {
2106
2433
  const slot = strings.length;
2107
2434
  strings.push(null);
2108
2435
  reads.push(
@@ -2128,7 +2455,7 @@ function itemTypesFromDataTransfer(dt) {
2128
2455
  for (const item of Array.from(dt.items ?? [])) {
2129
2456
  const mime = item.type ? normalizeMime(item.type) : "";
2130
2457
  if (item.kind === "file") mimes.push(mime && mime !== "application/x-moz-file" ? mime : UNKNOWN_MIME);
2131
- else if (INGEST_STRING_MIMES.has(mime)) mimes.push(mime);
2458
+ else if (keepsString(mime)) mimes.push(mime);
2132
2459
  }
2133
2460
  if (mimes.length === 0 && Array.from(dt.types ?? []).includes("Files")) mimes.push(UNKNOWN_MIME);
2134
2461
  return mimes.map((mime) => ({ mime }));
@@ -2138,7 +2465,7 @@ function itemsFromClipboardData(cd) {
2138
2465
  for (const file of Array.from(cd.files ?? [])) {
2139
2466
  out.push({ kind: "file", mime: normalizeMime(file.type), file });
2140
2467
  }
2141
- for (const mime of INGEST_STRING_MIMES) {
2468
+ for (const mime of /* @__PURE__ */ new Set([...INGEST_STRING_MIMES, ...admitted.keys()])) {
2142
2469
  const text = cd.getData(mime);
2143
2470
  if (text) out.push({ kind: "string", mime, text });
2144
2471
  }
@@ -2191,6 +2518,332 @@ function dwarn(namespace, ...args) {
2191
2518
  console.warn(`[${namespace}]`, ...args);
2192
2519
  }
2193
2520
 
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
2521
+ // src/menus/pickMenu.ts
2522
+ var oneOf = (want, got) => want === void 0 || (Array.isArray(want) ? want.includes(got) : want === got);
2523
+ function matches(when, hit) {
2524
+ return oneOf(when.bodyTarget, hit.bodyTarget) && oneOf(when.bodyKind, hit.bodyKind);
2525
+ }
2526
+ function mergeMenuDefs(defs, activeTool) {
2527
+ const live = [
2528
+ ...defs.filter((d) => d.owner === null),
2529
+ ...defs.filter((d) => d.owner !== null && d.owner === activeTool)
2530
+ ];
2531
+ const byId = /* @__PURE__ */ new Map();
2532
+ for (const { def } of live) {
2533
+ const prev = byId.get(def.id);
2534
+ if (!prev) {
2535
+ byId.set(def.id, { def: { ...def, menu: { ...def.menu, children: { ...def.menu.children } } }, parts: 1 });
2536
+ continue;
2537
+ }
2538
+ const n = prev.parts++;
2539
+ const children = prev.def.menu.children;
2540
+ children[`~sep${n}`] = { kind: "separator", name: "", description: "", default: void 0 };
2541
+ for (const [k, node] of Object.entries(def.menu.children)) {
2542
+ children[Object.hasOwn(children, k) ? `${k}~${n}` : k] = node;
2543
+ }
2544
+ if (!prev.def.when && def.when) prev.def.when = def.when;
2545
+ }
2546
+ return [...byId.values()].map((m) => m.def);
2547
+ }
2548
+ function pickMenu(defs, hit) {
2549
+ return defs.find((d) => d.when !== void 0 && matches(d.when, hit));
2550
+ }
2551
+ var SUBMENU_AS = /* @__PURE__ */ new Set(["menu", "page", "tab", "panel"]);
2552
+ var noop = () => {
2553
+ };
2554
+ var withAccess = (access) => access ? { access } : {};
2555
+ var sep = (key) => ({ kind: "separator", key });
2556
+ function itemOf(ctx, it, key, label, off, access) {
2557
+ const deps = ctx.src.depsFor(it.action);
2558
+ const point = ctx.at ? { worldX: ctx.at.x, worldY: ctx.at.y } : {};
2559
+ if (it.action.settings) return settingsOf(ctx, it, key, label, off || !evaluateEnabled(it.action, deps, ctx.at).enabled, access);
2560
+ return {
2561
+ kind: "item",
2562
+ key,
2563
+ label,
2564
+ ...withAccess(access),
2565
+ icon: it.icon,
2566
+ shortcut: actionShortcuts(it.action, it.params)[0],
2567
+ disabled: off || !evaluateEnabled(it.action, deps, ctx.at).enabled,
2568
+ checked: evaluateActive(it.action, deps),
2569
+ run: () => {
2570
+ ctx.src.trigger(it.action.id, { ...ctx.params, ...it.params, ...point });
2571
+ }
2572
+ };
2573
+ }
2574
+ function settingsOf(ctx, it, key, label, off, access) {
2575
+ const settings = it.action.settings;
2576
+ const state = off ? null : readActionSettings(it.action, ctx.src.depsFor(it.action));
2577
+ const mixed = new Set(state?.mixed ?? []);
2578
+ const values = (path) => mixed.has(path) ? void 0 : prefValueAtPath(state?.values, path);
2579
+ const onChange = (path, value) => {
2580
+ const params = actionSettingsParams(settings, path, value);
2581
+ if (params) ctx.src.trigger(it.action.id, { ...ctx.params, ...it.params, ...params });
2582
+ };
2583
+ const form = {
2584
+ ...ctx,
2585
+ src: { ...ctx.src, values, onChange, over: settings.schema },
2586
+ root: settings.schema,
2587
+ read: (path, from) => from === "item" ? void 0 : values(path)
2588
+ };
2589
+ const entries = state ? tidy(groupRows(form, settings.schema, "", false)) : [];
2590
+ return {
2591
+ kind: "submenu",
2592
+ key,
2593
+ label,
2594
+ ...it.icon ? { icon: it.icon } : {},
2595
+ ...withAccess(access),
2596
+ ...entries.length === 0 ? { disabled: true } : {},
2597
+ entries
2598
+ };
2599
+ }
2600
+ function itemsNamed(ctx, name) {
2601
+ const action = ctx.byId.get(name);
2602
+ if (action) return actionItems(action);
2603
+ const colon = name.indexOf(":");
2604
+ const owner = colon > 0 ? ctx.byId.get(name.slice(0, colon)) : void 0;
2605
+ return owner ? actionItems(owner).filter((it) => it.key === name) : [];
2606
+ }
2607
+ function actionRows(ctx, leaf, key, off) {
2608
+ const own = leaf.label || leaf.name;
2609
+ if (leaf.action === void 0) {
2610
+ if (!own) return [];
2611
+ if (isDev()) console.warn(`weasel: menu row "${key}" names no registered action, so it does nothing`);
2612
+ return [{ kind: "item", key, label: own, disabled: true, run: noop }];
2613
+ }
2614
+ const items = itemsNamed(ctx, leaf.action);
2615
+ if (items.length === 1) return [itemOf(ctx, items[0], key, own || items[0].label, off, leaf.access)];
2616
+ return items.map((it) => itemOf(ctx, it, `${key}/${it.key}`, it.label, off));
2617
+ }
2618
+ function siblingsOf(ctx, path) {
2619
+ const dot = path.lastIndexOf(".");
2620
+ const parent = dot < 0 ? void 0 : ctx.src.values(path.slice(0, dot));
2621
+ return parent && typeof parent === "object" ? parent : void 0;
2622
+ }
2623
+ function booleanRows(ctx, leaf, key, path, off) {
2624
+ const { values, onChange } = ctx.src;
2625
+ if (!values || !onChange) return [];
2626
+ const stored = values(path);
2627
+ const on = leaf.encoding ? leaf.encoding.read(stored, siblingsOf(ctx, path)) : stored === true;
2628
+ const write = (v) => leaf.encoding ? leaf.encoding.write(v, siblingsOf(ctx, path)) : v;
2629
+ return [{
2630
+ kind: "item",
2631
+ key,
2632
+ label: leaf.name,
2633
+ ...withAccess(leaf.access),
2634
+ checked: on,
2635
+ disabled: off || leaf.readout === true,
2636
+ run: leaf.readout ? noop : () => onChange(path, write(!on))
2637
+ }];
2638
+ }
2639
+ function enumRows(ctx, leaf, key, path, off) {
2640
+ const { values, onChange } = ctx.src;
2641
+ if (!values || !onChange) return [];
2642
+ const stored = values(path);
2643
+ const current = leaf.encoding ? leaf.encoding.read(stored, siblingsOf(ctx, path)) : stored;
2644
+ if (leaf.readout) {
2645
+ const shown = leaf.options.find((o) => o.value === current)?.label ?? "\u2014";
2646
+ return [{ kind: "item", key, label: `${leaf.name}: ${shown}`, disabled: true, run: noop }];
2647
+ }
2648
+ const write = (v) => leaf.encoding ? leaf.encoding.write(v, siblingsOf(ctx, path)) : v;
2649
+ return [
2650
+ sep(`${key}/<`),
2651
+ ...leaf.options.map((o) => ({
2652
+ kind: "item",
2653
+ key: `${key}/${o.value}`,
2654
+ label: o.label,
2655
+ checked: o.value === current,
2656
+ disabled: off || o.disabled === true,
2657
+ run: () => onChange(path, write(o.value))
2658
+ })),
2659
+ sep(`${key}/>`)
2660
+ ];
2661
+ }
2662
+ function numberRows(ctx, given, key, path, off) {
2663
+ const { values, onChange } = ctx.src;
2664
+ if (!values || !onChange) return [];
2665
+ const leaf = prefNumberAt(given, values);
2666
+ const unit = leaf.unit && resolvePrefUnit(leaf.unit);
2667
+ const stored = values(path);
2668
+ const { min, max, step, endless } = prefDisplayBounds(leaf);
2669
+ const disabled = off || leaf.readout === true;
2670
+ return [{
2671
+ kind: "number",
2672
+ key,
2673
+ label: leaf.name,
2674
+ ...withAccess(leaf.access),
2675
+ ...leaf.icon ? { icon: leaf.icon } : {},
2676
+ ...disabled ? { disabled } : {},
2677
+ value: typeof stored === "number" && (Number.isFinite(stored) || endlessAllows(leaf.endless, stored)) ? unit ? unit.toDisplay(stored) : stored : void 0,
2678
+ ...min === void 0 ? {} : { min },
2679
+ ...max === void 0 ? {} : { max },
2680
+ step,
2681
+ ...unit?.suffix === void 0 ? {} : { suffix: unit.suffix },
2682
+ ...leaf.display ? { display: leaf.display } : {},
2683
+ ...endless ? { endless } : {},
2684
+ ...leaf.infinity === void 0 ? {} : { infinity: leaf.infinity },
2685
+ ...unit ? { accepts: prefUnitAccepts(unit) } : {},
2686
+ set: (shown) => onChange(path, unit ? unit.fromDisplay(shown) : shown)
2687
+ }];
2688
+ }
2689
+ var textAt = (ctx, path) => {
2690
+ const v = ctx.src.values?.(path);
2691
+ return typeof v === "string" && v !== "" ? v : void 0;
2692
+ };
2693
+ function colorRows(ctx, leaf, key, path, off) {
2694
+ if (!ctx.src.values) return [];
2695
+ return [{ kind: "color", key, label: leaf.name, ...withAccess(leaf.access), ...off ? { disabled: true } : {}, value: textAt(ctx, path) }];
2696
+ }
2697
+ function stringRows(ctx, leaf, key, path, off) {
2698
+ const { values, onChange } = ctx.src;
2699
+ if (!values) return [];
2700
+ if (leaf.readout || !onChange) {
2701
+ return [{ kind: "item", key, label: `${leaf.name}: ${textAt(ctx, path) ?? "\u2014"}`, ...withAccess(leaf.access), disabled: true, run: noop }];
2702
+ }
2703
+ const stored = values(path);
2704
+ return [{
2705
+ kind: "text",
2706
+ key,
2707
+ label: leaf.name,
2708
+ ...withAccess(leaf.access),
2709
+ ...leaf.icon ? { icon: leaf.icon } : {},
2710
+ ...off ? { disabled: true } : {},
2711
+ value: typeof stored === "string" ? stored : void 0,
2712
+ set: (text) => onChange(path, text)
2713
+ }];
2714
+ }
2715
+ function leafRows(ctx, leaf, key, path, off) {
2716
+ switch (leaf.kind) {
2717
+ case "action":
2718
+ return actionRows(ctx, leaf, key, off);
2719
+ case "actions": {
2720
+ const group = leaf.group;
2721
+ return ctx.src.actions().filter((a) => a.group === group).flatMap((a) => actionItems(a).map((it) => itemOf(ctx, it, `${key}/${it.key}`, it.label, off)));
2722
+ }
2723
+ case "boolean":
2724
+ return booleanRows(ctx, leaf, key, path, off);
2725
+ case "enum":
2726
+ return enumRows(ctx, leaf, key, path, off);
2727
+ case "number":
2728
+ return numberRows(ctx, leaf, key, path, off);
2729
+ case "color":
2730
+ return colorRows(ctx, leaf, key, path, off);
2731
+ case "string":
2732
+ return stringRows(ctx, leaf, key, path, off);
2733
+ case "separator":
2734
+ return [sep(key)];
2735
+ case "label":
2736
+ return [{ kind: "item", key, label: leaf.name, ...withAccess(leaf.access), disabled: true, run: noop }];
2737
+ case "alias": {
2738
+ const alias = leaf;
2739
+ const target = prefAliasTarget(alias, (p) => prefLeafAt(ctx.src.over ?? ctx.root, p));
2740
+ return target ? leafRows(ctx, prefAliasedLeaf(alias, target.leaf), key, target.path, off) : [];
2741
+ }
2742
+ default:
2743
+ return [];
2744
+ }
2745
+ }
2746
+ function groupRows(ctx, group, prefix, off) {
2747
+ const out = [];
2748
+ for (const [k, node] of Object.entries(group.children)) {
2749
+ const path = prefix ? `${prefix}.${k}` : k;
2750
+ if (ctx.read && !prefIsShown(node, ctx.read)) continue;
2751
+ const disabled = off || ctx.read !== void 0 && !prefIsEnabled(node, ctx.read);
2752
+ if (isPrefLeaf(node)) {
2753
+ out.push(...leafRows(ctx, node, path, path, disabled));
2754
+ } else if (node.as !== void 0 && SUBMENU_AS.has(node.as)) {
2755
+ const entries = tidy(groupRows(ctx, node, path, disabled));
2756
+ if (entries.length) {
2757
+ out.push({ kind: "submenu", key: path, label: node.name, ...node.icon ? { icon: node.icon } : {}, ...withAccess(node.access), entries });
2758
+ }
2759
+ } else {
2760
+ out.push(sep(`${path}/<`), ...groupRows(ctx, node, path, disabled), sep(`${path}/>`));
2761
+ }
2762
+ }
2763
+ return out;
2764
+ }
2765
+ function tidy(entries) {
2766
+ const out = [];
2767
+ for (const e of entries) {
2768
+ if (e.kind === "separator" && (!out.length || out[out.length - 1].kind === "separator")) continue;
2769
+ out.push(e);
2770
+ }
2771
+ if (out[out.length - 1]?.kind === "separator") out.pop();
2772
+ return out;
2773
+ }
2774
+ function resolveSchemaMenu(schema, src, at, params) {
2775
+ const values = src.values;
2776
+ const ctx = {
2777
+ src,
2778
+ byId: new Map(src.actions().map((a) => [a.id, a])),
2779
+ root: schema,
2780
+ read: values ? (path, from) => from === "item" ? void 0 : values(path) : void 0,
2781
+ at,
2782
+ params
2783
+ };
2784
+ return tidy(groupRows(ctx, schema, "", false));
2785
+ }
2786
+ function resolveMenu(def, src, at, params) {
2787
+ return resolveSchemaMenu(def.menu, src, at, params);
2788
+ }
2789
+
2790
+ // src/menus/menuHost.ts
2791
+ function createMenuHost(src) {
2792
+ const defs = [];
2793
+ const listeners = /* @__PURE__ */ new Set();
2794
+ let open = null;
2795
+ let seq = 0;
2796
+ const emit = () => {
2797
+ for (const fn of listeners) fn();
2798
+ };
2799
+ const close = () => {
2800
+ if (!open) return;
2801
+ open = null;
2802
+ emit();
2803
+ };
2804
+ const resolve = (req) => {
2805
+ const live = mergeMenuDefs(defs, src.activeTool());
2806
+ const def = req.menu !== void 0 ? live.find((d) => d.id === req.menu) : req.hit ? pickMenu(live, req.hit) : void 0;
2807
+ return def ? resolveMenu(def, src, req.world, req.params) : [];
2808
+ };
2809
+ return {
2810
+ define(def, owner = null) {
2811
+ const entry = { def, owner };
2812
+ defs.push(entry);
2813
+ return () => {
2814
+ const i = defs.indexOf(entry);
2815
+ if (i >= 0) defs.splice(i, 1);
2816
+ };
2817
+ },
2818
+ open(req) {
2819
+ const entries = resolve(req);
2820
+ if (!entries.length) {
2821
+ close();
2822
+ return false;
2823
+ }
2824
+ open = {
2825
+ seq: ++seq,
2826
+ entries,
2827
+ client: req.client,
2828
+ world: req.world,
2829
+ ...req.under ? { under: req.under } : {},
2830
+ source: req.source ?? "pointer"
2831
+ };
2832
+ emit();
2833
+ return true;
2834
+ },
2835
+ has: (req) => resolve(req).length > 0,
2836
+ close,
2837
+ current: () => open,
2838
+ subscribe(fn) {
2839
+ listeners.add(fn);
2840
+ return () => {
2841
+ listeners.delete(fn);
2842
+ };
2843
+ }
2844
+ };
2845
+ }
2846
+
2847
+ export { ALWAYS, ActionDisabledReason, COARSE_TARGET_SCALE, DEFAULT_ALLOWED_CAPABILITIES, DEFAULT_DEVICE_PROFILE, DepNode, INGEST_STRING_MIMES, LONG_PRESS_HAPTIC_MS, NEVER, PREDICATE_TARGET, ScopeNode, TOOL_OFFHAND_ID, actionBindings, actionCursor, actionItemTitle, actionItems, actionSettingsParams, actionShortcuts, addCarrySurface, admitIngestMime, anyBinding, assembleEntries, buildDepsFromRequires, buildRouteRegistry, buildRuleCtx, buildToolOffhandBindings, carryGestureId, clientToCanvas, clientToCanvasRect, createDepRegistry, createDispatchTrace, createDispatcher, createLongPressStore, createMenuHost, createTraceRing, depNodeOf, describeRule, dlog, dwarn, evaluate, evaluateActive, evaluateEnabled, findConflicts, heldKeySpec, historyKey, isDebugEnabled, isTool, itemTypesFromDataTransfer, itemsFromClipboardData, itemsFromDataTransfer, itemsFromFiles, keySpecShortcut, keysHeld, liveScope, makeToolOffhandAction, matchBest, matchSorted, matchSortedWithBarred, matchesKeyBinding, mergeMenuDefs, multiTouchGestureId, offhandKeyFor, openPointerSession, pickMenu, pointerGestureId, pushOwner, readActionSettings, reportRouteConflicts, resolveDeviceProfile, resolveMenu, resolveParams, resolveSchemaMenu, roleReader, routesForSpec, ruleCanHoldIn, rulesExclusive, scopeBindings, startCarry, targetConsultsAffordance, tuneAction, tunedParams, validateActionDefaultBinding, validateActionId };
2848
+ //# sourceMappingURL=chunk-KLBH7VFF.js.map
2849
+ //# sourceMappingURL=chunk-KLBH7VFF.js.map