@weasel-js/gestures 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.
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ var GESTURE_DESCRIPTORS = [
18
18
  // matches any payload, which is what an omitted arg slot means for a
19
19
  // `'free'` arg.
20
20
  { name: "drop", hasTarget: false, arg: { name: "types", values: "free" } },
21
+ { name: "carry", hasTarget: true, arg: { name: "types", values: "free" } },
21
22
  { name: "paste", hasTarget: false, arg: { name: "types", values: "free" } }
22
23
  ];
23
24
  var BY_NAME = new Map(
@@ -106,6 +107,7 @@ function parseRoute(input) {
106
107
  target = "*";
107
108
  }
108
109
  const modifiers = {};
110
+ let held;
109
111
  while (rest.length > 0) {
110
112
  const ch = rest[0];
111
113
  if (RESERVED_SIGILS.has(ch)) {
@@ -119,6 +121,13 @@ function parseRoute(input) {
119
121
  const nameMatch = /^([a-z]+)/.exec(rest);
120
122
  if (!nameMatch) throw new Error(`invalid route (sigil "${sigil}" without modifier name): ${input}`);
121
123
  const name = nameMatch[1];
124
+ if (name === HELD) {
125
+ if (gestureName !== "hover") throw new Error(`invalid route (${gestureName} takes no "${HELD}"): ${input}`);
126
+ if (held !== void 0) throw new Error(`invalid route (duplicate "${HELD}"): ${input}`);
127
+ held = sigil === "+" ? "required" : "optional";
128
+ rest = rest.slice(nameMatch[0].length).trimStart();
129
+ continue;
130
+ }
122
131
  if (!MOD_NAME_SET.has(name)) {
123
132
  throw new Error(`invalid route (unknown modifier "${name}"): ${input}`);
124
133
  }
@@ -128,8 +137,9 @@ function parseRoute(input) {
128
137
  modifiers[name] = sigil === "+" ? "required" : "optional";
129
138
  rest = rest.slice(nameMatch[0].length).trimStart();
130
139
  }
131
- return { phases, gesture: gestureName, arg, target, modifiers };
140
+ return { phases, gesture: gestureName, arg, target, modifiers, ...held ? { held } : {} };
132
141
  }
142
+ var HELD = "held";
133
143
  function parsePhaseList(raw, input) {
134
144
  if (raw === "") throw new Error(`invalid route (empty phase list): ${input}`);
135
145
  const tokens = raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
@@ -203,6 +213,7 @@ function collapseShiftPairs(routes) {
203
213
  }
204
214
  function isShiftTwin(a, b) {
205
215
  if (a.gesture !== b.gesture) return false;
216
+ if (a.held !== b.held) return false;
206
217
  if (a.arg !== b.arg) return false;
207
218
  if (a.target !== b.target) return false;
208
219
  if (!samePhases(a.phases, b.phases)) return false;
@@ -245,6 +256,8 @@ function formatRoute(r) {
245
256
  if (req === "required") out += ` +${name}`;
246
257
  else if (req === "optional") out += ` ?${name}`;
247
258
  }
259
+ if (r.held === "required") out += ` +${HELD}`;
260
+ else if (r.held === "optional") out += ` ?${HELD}`;
248
261
  return out;
249
262
  }
250
263
 
@@ -473,6 +486,7 @@ function matchSpec(e, spec, isMac, phaseCtx) {
473
486
  case "hover": {
474
487
  if (e.kind !== "hover") return false;
475
488
  if (!matchModifiers(e, spec.mods, isMac)) return false;
489
+ if (!e.off && (e.held === true ? spec.held === void 0 : spec.held === true)) return false;
476
490
  const on = !e.off && matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
477
491
  if (spec.motion !== "leave") return on;
478
492
  return !on && e.from !== void 0 && matchTarget(e.from.affordance, spec.target, e.from.bodyTarget, e.from.bodyKind);
@@ -490,21 +504,27 @@ function matchSpec(e, spec, isMac, phaseCtx) {
490
504
  if (e.fingers !== spec.fingers) return false;
491
505
  return matchModifiers(e, spec.mods, isMac);
492
506
  }
493
- case "drop": {
494
- if (e.kind !== "drop") return false;
495
- if (!matchIngestTypes(e.items, spec.types)) return false;
496
- return matchModifiers(e, spec.mods, isMac);
497
- }
498
- case "paste": {
499
- if (e.kind !== "paste") return false;
500
- if (!matchIngestTypes(e.items, spec.types)) return false;
501
- return matchModifiers(e, spec.mods, isMac);
502
- }
507
+ case "drop":
508
+ case "carry":
509
+ case "paste":
510
+ return matchArrival(e, spec, isMac);
503
511
  default: {
504
512
  return false;
505
513
  }
506
514
  }
507
515
  }
516
+ function matchArrival(e, spec, isMac) {
517
+ if (e.kind !== spec.kind) return false;
518
+ if (e.kind === "carry") {
519
+ const target = "target" in spec ? spec.target : void 0;
520
+ if (e.stage === "leave" || e.stage === "move" && target === void 0) return false;
521
+ if (!matchIngestTypes([{ mime: e.type }], spec.types)) return false;
522
+ if (!matchTarget(e.affordance, target, e.bodyTarget, e.bodyKind)) return false;
523
+ } else if (e.kind === "drop" || e.kind === "paste") {
524
+ if (!matchIngestTypes(e.items, spec.types)) return false;
525
+ }
526
+ return matchModifiers(e, spec.mods, isMac);
527
+ }
508
528
  var EMPTY_ENGAGED = /* @__PURE__ */ new Set();
509
529
 
510
530
  // src/grammar/specKinds.ts
@@ -523,6 +543,7 @@ var SPEC_KIND_TO_GESTURE = {
523
543
  multiTouch: "multiTouch",
524
544
  multiTouchTap: "multiTouchTap",
525
545
  drop: "drop",
546
+ carry: "carry",
526
547
  paste: "paste"
527
548
  };
528
549
  var GESTURE_TO_SPEC_KIND = new Map(
@@ -568,7 +589,12 @@ function routeToSpec(parsed) {
568
589
  case void 0:
569
590
  return fail(`${parsed.gesture} has no GestureSpec kind`);
570
591
  case "hover":
571
- return withMods({ kind, ...arg === "leave" ? { motion: "leave" } : {}, ...t });
592
+ return withMods({
593
+ kind,
594
+ ...arg === "leave" ? { motion: "leave" } : {},
595
+ ...parsed.held ? { held: parsed.held === "required" ? true : "optional" } : {},
596
+ ...t
597
+ });
572
598
  case "click":
573
599
  case "pointerDown":
574
600
  case "doubleClick":
@@ -592,6 +618,8 @@ function routeToSpec(parsed) {
592
618
  case "multiTouchTap":
593
619
  if (arg === void 0) return fail(`a ${kind} spec needs concrete fingers`);
594
620
  return withMods({ kind, fingers: Number(arg) });
621
+ case "carry":
622
+ return withMods({ kind, ...arg ? { types: arg.split("|") } : {}, ...t });
595
623
  case "drop":
596
624
  case "paste":
597
625
  return withMods({ kind, ...arg ? { types: arg.split("|") } : {} });
@@ -662,6 +690,11 @@ function targetClause(target, hasTarget) {
662
690
  if (target === "empty") return " on empty canvas";
663
691
  return ` on a ${target}`;
664
692
  }
693
+ function carryOnto(target) {
694
+ if (target === void 0 || target === "*") return "onto the canvas";
695
+ if (target === "empty") return "onto empty canvas";
696
+ return `onto a ${target}`;
697
+ }
665
698
  function concreteArg(parsed) {
666
699
  return parsed.arg === "*" ? void 0 : parsed.arg;
667
700
  }
@@ -702,14 +735,19 @@ function actionClause(parsed, required) {
702
735
  return userDoes(required, `clicks${target}`);
703
736
  case "pointerDown":
704
737
  return userDoes(required, `presses${target}`);
705
- case "hover":
706
- return userDoes(required, arg === "leave" ? `moves the pointer ${leftClause(parsed.target)}` : `moves the pointer${target}`);
738
+ case "hover": {
739
+ const held = parsed.held === "required" ? " with a button held" : parsed.held === "optional" ? " whether or not a button is held" : "";
740
+ const where = arg === "leave" ? ` ${leftClause(parsed.target)}` : target;
741
+ return userDoes(required, `moves the pointer${where}${held}`);
742
+ }
707
743
  case "dblTap":
708
744
  return userDoes(required, `double-taps${target}`);
709
745
  case "drag":
710
746
  return userDoes(required, `drags${target}`);
711
747
  case "drop":
712
748
  return userDoes(required, `drops ${arg ?? "any"} content onto the canvas`);
749
+ case "carry":
750
+ return userDoes(required, `carries ${arg ?? "anything"} ${carryOnto(parsed.target)}`);
713
751
  case "paste":
714
752
  return userDoes(required, `pastes ${arg ?? "any"} content`);
715
753
  }
@@ -784,6 +822,154 @@ function isApplePlatform() {
784
822
  return /mac|iphone|ipad|ipod/i.test(raw);
785
823
  }
786
824
 
825
+ // src/ui/keySpecsFromMods.ts
826
+ var MOD_BY_PLATFORM = {
827
+ mod: {
828
+ macos: { auto: "\u2318", symbol: "\u2318", text: "Cmd" },
829
+ windows: { auto: "Ctrl", symbol: "\u2388", text: "Ctrl" },
830
+ linux: { auto: "Ctrl", symbol: "\u2388", text: "Ctrl" }
831
+ },
832
+ shift: {
833
+ macos: { auto: "\u21E7", symbol: "\u21E7", text: "Shift" },
834
+ windows: { auto: "\u21E7", symbol: "\u21E7", text: "Shift" },
835
+ linux: { auto: "\u21E7", symbol: "\u21E7", text: "Shift" }
836
+ },
837
+ // Off macOS the symbols are ISO/IEC 9995-7's: ⎈ for Control, ⎇ for Alt.
838
+ alt: {
839
+ macos: { auto: "\u2325", symbol: "\u2325", text: "Option" },
840
+ windows: { auto: "Alt", symbol: "\u2387", text: "Alt" },
841
+ linux: { auto: "Alt", symbol: "\u2387", text: "Alt" }
842
+ },
843
+ ctrl: {
844
+ macos: { auto: "\u2303", symbol: "\u2303", text: "Control" },
845
+ windows: { auto: "Ctrl", symbol: "\u2388", text: "Ctrl" },
846
+ linux: { auto: "Ctrl", symbol: "\u2388", text: "Ctrl" }
847
+ },
848
+ // Windows/Linux Meta has a recognized symbol on the physical key (⊞ Win
849
+ // logo / Super logo), so `auto` shows it. Use `text` for Win / Super.
850
+ meta: {
851
+ macos: { auto: "\u2318", symbol: "\u2318", text: "Cmd" },
852
+ windows: { auto: "\u229E", symbol: "\u229E", text: "Win" },
853
+ linux: { auto: "\u229E", symbol: "\u229E", text: "Super" }
854
+ }
855
+ };
856
+ var MODIFIER_LABELS = new Set(
857
+ Object.values(MOD_BY_PLATFORM).flatMap(
858
+ (byPlatform) => Object.values(byPlatform).flatMap((byLegend) => Object.values(byLegend))
859
+ )
860
+ );
861
+ function detectPlatform() {
862
+ if (typeof navigator === "undefined" || !navigator) return "macos";
863
+ if (isApplePlatform()) return "macos";
864
+ const nav = navigator;
865
+ const raw = nav.userAgentData?.platform || nav.platform || nav.userAgent || "";
866
+ return /\bwin/i.test(raw) ? "windows" : "linux";
867
+ }
868
+ function keySpecsFromMods(mods2, opts = {}) {
869
+ const platform = opts.platform ?? detectPlatform();
870
+ const legend = opts.legend ?? "auto";
871
+ return mods2.map((m) => {
872
+ const label = opts.labels?.[m.name] ?? MOD_BY_PLATFORM[m.name][platform][legend];
873
+ return m.optional ? { label, optional: true } : { label };
874
+ });
875
+ }
876
+ var NAMED_KEY = {
877
+ Escape: { macos: { auto: "\u238B", symbol: "\u238B", text: "Esc" }, windows: { auto: "Esc", symbol: "\u238B", text: "Esc" }, linux: { auto: "Esc", symbol: "\u238B", text: "Esc" } },
878
+ Enter: { macos: { auto: "\u21B5", symbol: "\u21B5", text: "Return" }, windows: { auto: "Enter", symbol: "\u21B5", text: "Enter" }, linux: { auto: "Enter", symbol: "\u21B5", text: "Enter" } },
879
+ Return: { macos: { auto: "\u21B5", symbol: "\u21B5", text: "Return" }, windows: { auto: "Enter", symbol: "\u21B5", text: "Enter" }, linux: { auto: "Enter", symbol: "\u21B5", text: "Enter" } },
880
+ Tab: { macos: { auto: "\u21E5", symbol: "\u21E5", text: "Tab" }, windows: { auto: "Tab", symbol: "\u21E5", text: "Tab" }, linux: { auto: "Tab", symbol: "\u21E5", text: "Tab" } },
881
+ Space: { macos: { auto: "\u2423", symbol: "\u2423", text: "Space" }, windows: { auto: "Space", symbol: "\u2423", text: "Space" }, linux: { auto: "Space", symbol: "\u2423", text: "Space" } },
882
+ " ": { macos: { auto: "\u2423", symbol: "\u2423", text: "Space" }, windows: { auto: "Space", symbol: "\u2423", text: "Space" }, linux: { auto: "Space", symbol: "\u2423", text: "Space" } },
883
+ Backspace: { macos: { auto: "\u232B", symbol: "\u232B", text: "Backspace" }, windows: { auto: "Backspace", symbol: "\u27F5", text: "Backspace" }, linux: { auto: "Backspace", symbol: "\u27F5", text: "Backspace" } },
884
+ Delete: { macos: { auto: "\u2326", symbol: "\u2326", text: "Delete" }, windows: { auto: "Delete", symbol: "\u2326", text: "Delete" }, linux: { auto: "Delete", symbol: "\u2326", text: "Delete" } },
885
+ // Context-menu key (the right-Ctrl-area key on Windows / Linux keyboards
886
+ // with a ▤ icon). Mac has no equivalent; falls back to text 'Menu'.
887
+ ContextMenu: { macos: { auto: "Menu", symbol: "Menu", text: "Menu" }, windows: { auto: "\u25A4", symbol: "\u25A4", text: "Menu" }, linux: { auto: "\u25A4", symbol: "\u25A4", text: "Menu" } },
888
+ // Symbol mode only: Apple's arrows for Home, End, and the page keys, and
889
+ // ISO/IEC 9995-7's symbols for the rest.
890
+ Home: { macos: { auto: "Home", symbol: "\u2196", text: "Home" }, windows: { auto: "Home", symbol: "\u2196", text: "Home" }, linux: { auto: "Home", symbol: "\u2196", text: "Home" } },
891
+ End: { macos: { auto: "End", symbol: "\u2198", text: "End" }, windows: { auto: "End", symbol: "\u2198", text: "End" }, linux: { auto: "End", symbol: "\u2198", text: "End" } },
892
+ PageUp: { macos: { auto: "PgUp", symbol: "\u21DE", text: "PgUp" }, windows: { auto: "PgUp", symbol: "\u21DE", text: "PgUp" }, linux: { auto: "PgUp", symbol: "\u21DE", text: "PgUp" } },
893
+ PageDown: { macos: { auto: "PgDn", symbol: "\u21DF", text: "PgDn" }, windows: { auto: "PgDn", symbol: "\u21DF", text: "PgDn" }, linux: { auto: "PgDn", symbol: "\u21DF", text: "PgDn" } },
894
+ PrintScreen: { macos: { auto: "PrtSc", symbol: "\u2399", text: "PrtSc" }, windows: { auto: "PrtSc", symbol: "\u2399", text: "PrtSc" }, linux: { auto: "PrtSc", symbol: "\u2399", text: "PrtSc" } },
895
+ Insert: { macos: { auto: "Ins", symbol: "\u2380", text: "Insert" }, windows: { auto: "Ins", symbol: "\u2380", text: "Insert" }, linux: { auto: "Ins", symbol: "\u2380", text: "Insert" } },
896
+ ScrollLock: { macos: { auto: "ScrLk", symbol: "\u21F3", text: "ScrLk" }, windows: { auto: "ScrLk", symbol: "\u21F3", text: "ScrLk" }, linux: { auto: "ScrLk", symbol: "\u21F3", text: "ScrLk" } },
897
+ Pause: { macos: { auto: "Pause", symbol: "\u2389", text: "Pause" }, windows: { auto: "Pause", symbol: "\u2389", text: "Pause" }, linux: { auto: "Pause", symbol: "\u2389", text: "Pause" } },
898
+ ArrowUp: { macos: { auto: "\u2191", symbol: "\u2191", text: "\u2191" }, windows: { auto: "\u2191", symbol: "\u2191", text: "\u2191" }, linux: { auto: "\u2191", symbol: "\u2191", text: "\u2191" } },
899
+ ArrowDown: { macos: { auto: "\u2193", symbol: "\u2193", text: "\u2193" }, windows: { auto: "\u2193", symbol: "\u2193", text: "\u2193" }, linux: { auto: "\u2193", symbol: "\u2193", text: "\u2193" } },
900
+ ArrowLeft: { macos: { auto: "\u2190", symbol: "\u2190", text: "\u2190" }, windows: { auto: "\u2190", symbol: "\u2190", text: "\u2190" }, linux: { auto: "\u2190", symbol: "\u2190", text: "\u2190" } },
901
+ ArrowRight: { macos: { auto: "\u2192", symbol: "\u2192", text: "\u2192" }, windows: { auto: "\u2192", symbol: "\u2192", text: "\u2192" }, linux: { auto: "\u2192", symbol: "\u2192", text: "\u2192" } }
902
+ };
903
+ function keySpecFromKey(raw, opts = {}) {
904
+ const platform = opts.platform ?? detectPlatform();
905
+ const legend = opts.legend ?? "auto";
906
+ const entry = NAMED_KEY[raw];
907
+ const override = opts.labels?.[raw] ?? (raw.length === 1 ? opts.labels?.[raw.toUpperCase()] : void 0);
908
+ const label = override ?? (entry ? entry[platform][legend] : raw.length === 1 ? raw.toUpperCase() : raw);
909
+ return opts.optional ? { label, optional: true } : { label };
910
+ }
911
+
912
+ // src/ui/keySpecsFromShortcut.ts
913
+ function keySpecsFromShortcut(shortcut, opts = {}) {
914
+ if (!shortcut) return void 0;
915
+ const mods2 = [];
916
+ if (shortcut.mod) mods2.push({ name: "mod" });
917
+ if (shortcut.shift) mods2.push(shortcut.shift === "optional" ? { name: "shift", optional: true } : { name: "shift" });
918
+ if (shortcut.alt) mods2.push({ name: "alt" });
919
+ const key = typeof shortcut.key === "string" ? shortcut.key : shortcut.key[0];
920
+ const keys = keySpecsFromMods(mods2, opts);
921
+ return key === void 0 ? keys : [...keys, keySpecFromKey(key, opts)];
922
+ }
923
+
924
+ // src/ui/keySpecFromCode.ts
925
+ var MOD_CODES = {
926
+ ShiftLeft: "shift",
927
+ ShiftRight: "shift",
928
+ ControlLeft: "ctrl",
929
+ ControlRight: "ctrl",
930
+ AltLeft: "alt",
931
+ AltRight: "alt",
932
+ MetaLeft: "meta",
933
+ MetaRight: "meta"
934
+ };
935
+ var PRINTED = {
936
+ Backquote: "`",
937
+ Minus: "-",
938
+ Equal: "=",
939
+ BracketLeft: "[",
940
+ BracketRight: "]",
941
+ Backslash: "\\",
942
+ Semicolon: ";",
943
+ Quote: "'",
944
+ Comma: ",",
945
+ Period: ".",
946
+ Slash: "/",
947
+ IntlBackslash: "\xA7",
948
+ CapsLock: "Caps",
949
+ Fn: "fn"
950
+ };
951
+ function keySpecFromCode(code, opts = {}) {
952
+ const own = opts.labels?.[code];
953
+ if (own !== void 0) return { label: own };
954
+ const mod = MOD_CODES[code];
955
+ if (mod) return keySpecsFromMods([{ name: mod }], opts)[0];
956
+ const char = /^Key([A-Z])$/.exec(code) ?? /^Digit(\d)$/.exec(code);
957
+ if (char) return keySpecFromKey(char[1], opts);
958
+ if (code === "NumpadEnter") return keySpecFromKey("Enter", opts);
959
+ const printed = PRINTED[code];
960
+ return printed !== void 0 ? { label: printed } : keySpecFromKey(code, opts);
961
+ }
962
+
963
+ // src/ui/formatShortcut.ts
964
+ var labels = (b, opts) => b ? keySpecsFromShortcut({ ...b, shift: b.shift === true }, opts)?.map((k) => k.label) : void 0;
965
+ function formatShortcutParts(b, opts = {}) {
966
+ return labels(b, opts);
967
+ }
968
+ function formatShortcut(b, opts = {}) {
969
+ const platform = opts.platform ?? detectPlatform();
970
+ return platform === "macos" ? labels(b, { platform })?.join("") : labels(b, { platform, legend: "text" })?.join("+");
971
+ }
972
+
787
973
  // src/ui/fixedSeat.ts
788
974
  var HOST_SELECTOR = "[data-wzl-portal-host],[data-wzl-theme],[data-wzl-mode]";
789
975
  function nearestPortalHost(from) {
@@ -849,7 +1035,7 @@ function specificity(spec) {
849
1035
  const mods2 = "mods" in spec ? spec.mods : void 0;
850
1036
  const m = modsCount(mods2);
851
1037
  const p = phaseRank("phase" in spec ? spec.phase : void 0);
852
- const typed = (spec.kind === "drop" || spec.kind === "paste") && spec.types !== void 0 && spec.types.length > 0 ? 2 : 1;
1038
+ const typed = (spec.kind === "drop" || spec.kind === "carry" || spec.kind === "paste") && spec.types !== void 0 && spec.types.length > 0 ? 2 : 1;
853
1039
  return [t, m, p, typed];
854
1040
  }
855
1041
 
@@ -883,12 +1069,16 @@ function clickEvents(down, up) {
883
1069
  x: up.worldX,
884
1070
  y: up.worldY,
885
1071
  clientX: up.clientX,
886
- clientY: up.clientY,
887
- ...pressedOn(down)
1072
+ clientY: up.clientY
888
1073
  };
1074
+ const named = down.doubleClickBody;
889
1075
  return {
890
- click: { kind: "click", ...shared, pressX: down.worldX, pressY: down.worldY },
891
- doubleclick: { kind: "doubleclick", ...shared }
1076
+ click: { kind: "click", ...shared, ...pressedOn(down), pressX: down.worldX, pressY: down.worldY },
1077
+ doubleclick: {
1078
+ kind: "doubleclick",
1079
+ ...shared,
1080
+ ...pressedOn(named ? { affordance: down.affordance, ...named } : down)
1081
+ }
892
1082
  };
893
1083
  }
894
1084
  function longPressEvents(down) {
@@ -908,14 +1098,14 @@ function longPressEvents(down) {
908
1098
  }
909
1099
 
910
1100
  // src/recognizer/click.ts
911
- function composeClick(down, up, clicks, now, dispatch, observer) {
1101
+ function composeClick(down, up, clicks, now, dispatch, observer, limits = { maxMs: DOUBLE_CLICK_MAX_MS, maxPx: DOUBLE_CLICK_MAX_PX }) {
912
1102
  const evs = clickEvents(down, up);
913
1103
  const world = { x: up.worldX, y: up.worldY };
914
1104
  dispatch(evs.click);
915
1105
  observer.click?.(world);
916
1106
  const t = now();
917
1107
  const prev = clicks.last;
918
- const follows = prev !== null && t - prev.t < DOUBLE_CLICK_MAX_MS && Math.hypot(up.clientX - prev.clientX, up.clientY - prev.clientY) < DOUBLE_CLICK_MAX_PX;
1108
+ const follows = prev !== null && t - prev.t < limits.maxMs && Math.hypot(up.clientX - prev.clientX, up.clientY - prev.clientY) < limits.maxPx;
919
1109
  if (!follows) {
920
1110
  clicks.last = { t, clientX: up.clientX, clientY: up.clientY };
921
1111
  return {};
@@ -969,7 +1159,7 @@ function multiTouchGeometry(positions) {
969
1159
  angle: Math.atan2(dy, dx) * 180 / Math.PI
970
1160
  };
971
1161
  }
972
- function createMultiTouchEpisode() {
1162
+ function createMultiTouchEpisode(tapPx = () => TAP_THRESHOLD_PX) {
973
1163
  let episode = null;
974
1164
  return {
975
1165
  join(positions, centroid, now) {
@@ -990,14 +1180,14 @@ function createMultiTouchEpisode() {
990
1180
  },
991
1181
  moved(pointerId, to) {
992
1182
  const from = episode?.joinedAt.get(pointerId);
993
- if (episode && from && Math.hypot(to.x - from.x, to.y - from.y) > TAP_THRESHOLD_PX) episode.moved = true;
1183
+ if (episode && from && Math.hypot(to.x - from.x, to.y - from.y) > tapPx()) episode.moved = true;
994
1184
  },
995
1185
  finish(centroid, now, maxHoldMs) {
996
1186
  const e = episode;
997
1187
  episode = null;
998
1188
  if (!e) return null;
999
1189
  const brief = now - e.startedAt < maxHoldMs;
1000
- const still = Math.hypot(centroid.x - e.centroid.x, centroid.y - e.centroid.y) <= TAP_THRESHOLD_PX;
1190
+ const still = Math.hypot(centroid.x - e.centroid.x, centroid.y - e.centroid.y) <= tapPx();
1001
1191
  return brief && !e.moved && still ? e.fingers : null;
1002
1192
  },
1003
1193
  abandon() {
@@ -1018,7 +1208,8 @@ function pressFactsOf(s) {
1018
1208
  worldX: s.x,
1019
1209
  worldY: s.y,
1020
1210
  ...modifiersOf(s),
1021
- ...pressedOn(s)
1211
+ ...pressedOn(s),
1212
+ ...s.doubleClickBody !== void 0 ? { doubleClickBody: s.doubleClickBody } : {}
1022
1213
  };
1023
1214
  }
1024
1215
  var at = (s, world) => ({
@@ -1057,23 +1248,42 @@ function fireLongPress(rec, sink, observer) {
1057
1248
  rec.buffered = null;
1058
1249
  if (rec.holdType) observer.longPress?.(rec.holdType);
1059
1250
  }
1251
+ function thresholdsOf(opts) {
1252
+ return {
1253
+ longPressMs: () => opts.longPressMs?.() ?? LONG_PRESS_MS,
1254
+ tapPx: () => opts.tapPx?.() ?? TAP_THRESHOLD_PX,
1255
+ dragPx: () => opts.dragPx?.() ?? DRAG_THRESHOLD_PX,
1256
+ doubleClick: () => ({
1257
+ maxMs: opts.doubleClickMs?.() ?? DOUBLE_CLICK_MAX_MS,
1258
+ maxPx: opts.doubleClickPx?.() ?? DOUBLE_CLICK_MAX_PX
1259
+ })
1260
+ };
1261
+ }
1262
+ function sampleTime(clock) {
1263
+ return (s) => {
1264
+ const now = clock.now();
1265
+ const t = s.timeStamp;
1266
+ return t !== void 0 && t > 0 && t <= now ? t : now;
1267
+ };
1268
+ }
1060
1269
  function createPointerRecognizer(opts) {
1061
1270
  const { sink, clock, observer = {}, toSurface = (p) => p } = opts;
1062
1271
  const world = (s) => opts.toWorld?.(s.clientX, s.clientY) ?? { x: s.clientX, y: s.clientY };
1063
1272
  const clicks = opts.clicks ?? { last: null };
1064
1273
  const held = /* @__PURE__ */ new Map();
1065
- const touches = createMultiTouchEpisode();
1274
+ const { longPressMs, tapPx, dragPx, doubleClick } = thresholdsOf(opts);
1275
+ const touches = createMultiTouchEpisode(tapPx);
1066
1276
  const hold = createLongPressHold(clock, (pending) => {
1067
1277
  observer.holdChanged?.(pending);
1068
1278
  });
1069
- const longPressMs = () => opts.longPressMs?.() ?? LONG_PRESS_MS;
1070
1279
  const geometry = (extra = []) => heldGeometry(held, toSurface, extra);
1071
- const claimForMultiTouch = (mods2) => {
1280
+ const happenedAt = sampleTime(clock);
1281
+ const claimForMultiTouch = (s) => {
1072
1282
  hold.cancel();
1073
1283
  for (const p of held.values()) p.buffered = null;
1074
1284
  const g = geometry();
1075
- touches.join(positionsOf(held), g.centroid, clock.now());
1076
- sink.dispatch(multiTouchEvent(held.size, mods2, g));
1285
+ touches.join(positionsOf(held), g.centroid, happenedAt(s));
1286
+ sink.dispatch(multiTouchEvent(held.size, s, g));
1077
1287
  };
1078
1288
  return {
1079
1289
  press(s) {
@@ -1097,7 +1307,7 @@ function createPointerRecognizer(opts) {
1097
1307
  const to = { x: s.clientX, y: s.clientY };
1098
1308
  if (rec) rec.pos = to;
1099
1309
  touches.moved(s.pointerId, to);
1100
- if (rec?.buffered && pastDragThreshold(rec.down, s)) {
1310
+ if (rec?.buffered && pastDragThreshold(rec.down, s, dragPx())) {
1101
1311
  const press = rec.buffered;
1102
1312
  rec.buffered = null;
1103
1313
  hold.cancel(s.pointerId);
@@ -1117,7 +1327,7 @@ function createPointerRecognizer(opts) {
1117
1327
  if (before >= 2 && held.size < 2) {
1118
1328
  const fingers = touches.finish(
1119
1329
  geometry([{ x: s.clientX, y: s.clientY }]).centroid,
1120
- clock.now(),
1330
+ happenedAt(s),
1121
1331
  longPressMs()
1122
1332
  );
1123
1333
  if (fingers !== null) sink.dispatch({ kind: "multitouchtap", fingers, ...modifiersOf(s) });
@@ -1133,7 +1343,7 @@ function createPointerRecognizer(opts) {
1133
1343
  worldY: at2.y,
1134
1344
  ...modifiersOf(s)
1135
1345
  };
1136
- return composeClick(rec.down, up, clicks, () => clock.now(), (ev) => sink.dispatch(ev), observer);
1346
+ return composeClick(rec.down, up, clicks, () => happenedAt(s), (ev) => sink.dispatch(ev), observer, doubleClick());
1137
1347
  },
1138
1348
  cancel(pointerId) {
1139
1349
  const rec = held.get(pointerId);
@@ -1287,6 +1497,6 @@ function focusedControlOwnsKey(e) {
1287
1497
  return activatesFocusedControl(e) || selectOwnsKey(e);
1288
1498
  }
1289
1499
 
1290
- export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
1500
+ export { ACTIVE_SIGILS, CONTROL_SELECTOR, DOUBLE_CLICK_MAX_MS, DOUBLE_CLICK_MAX_PX, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, LONG_PRESS_MS, MODIFIER_LABELS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, TAP_THRESHOLD_PX, UNKNOWN_MIME, VALID_MOD_NAMES, activatesFocusedControl, bodyPredicate, canonicalModifiers, clickEvents, collapseShiftPairs, createPointerRecognizer, describeRoute, describeRouteParts, detectPlatform, fixedSeat, focusedControlOwnsKey, formatKeyRoute, formatPhaseAtom, formatRoute, formatShortcut, formatShortcutParts, getGestureDescriptor, heldPointers, hitPredicate, isApplePlatform, isEditableTarget, isInControlWithin, isKnownGestureName, keyRouteToSpec, keySpecFromCode, keySpecFromKey, keySpecsFromMods, keySpecsFromShortcut, longPressEvents, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, multiTouchGeometry, nearestPortalHost, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity, stylusOf };
1291
1501
  //# sourceMappingURL=index.js.map
1292
1502
  //# sourceMappingURL=index.js.map