@weasel-js/gestures 1.9.3 → 1.10.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.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  var GESTURE_DESCRIPTORS = [
3
3
  { name: "click", hasTarget: true },
4
4
  { name: "pointerDown", hasTarget: true },
5
+ { name: "hover", hasTarget: true },
5
6
  { name: "dblTap", hasTarget: true },
6
7
  { name: "drag", hasTarget: true },
7
8
  { name: "wheel", hasTarget: true, arg: { name: "direction", values: ["up", "down", "*"], default: "*" } },
@@ -11,6 +12,7 @@ var GESTURE_DESCRIPTORS = [
11
12
  { name: "keyHeld", hasTarget: false, arg: { name: "key", values: "free" } },
12
13
  { name: "contextMenu", hasTarget: true },
13
14
  { name: "longPress", hasTarget: true },
15
+ { name: "multiTouch", hasTarget: false, arg: { name: "fingers", values: ["2", "3", "4"] } },
14
16
  { name: "multiTouchTap", hasTarget: false, arg: { name: "fingers", values: ["2", "3", "4"] } },
15
17
  // The arg is the MIME-glob filter. No default: a spec with no `types`
16
18
  // matches any payload, which is what an omitted arg slot means for a
@@ -270,6 +272,20 @@ function keyRouteToSpec(r) {
270
272
  return spec;
271
273
  }
272
274
 
275
+ // src/ui/ingestTypes.ts
276
+ function mimeMatchesGlob(mime, glob) {
277
+ const m = mime.toLowerCase();
278
+ const g = glob.toLowerCase();
279
+ if (g === "*" || g === "*/*") return true;
280
+ if (g.endsWith("/*")) return m.startsWith(g.slice(0, -1));
281
+ return m === g;
282
+ }
283
+ var UNKNOWN_MIME = "*/*";
284
+ function matchIngestTypes(items, types) {
285
+ if (!types || types.length === 0) return true;
286
+ return items.some((it) => it.mime === UNKNOWN_MIME || types.some((g) => mimeMatchesGlob(it.mime, g)));
287
+ }
288
+
273
289
  // src/ui/match.ts
274
290
  function resolveSpecValue(value) {
275
291
  if (value === true) return "required";
@@ -307,6 +323,10 @@ function matchKey(eventKey, specKey) {
307
323
  }
308
324
  return specKey.toLowerCase() === lower;
309
325
  }
326
+ function matchKeyOrCode(e, spec) {
327
+ if (spec.code !== void 0 && e.code !== void 0 && [spec.code].flat().includes(e.code)) return true;
328
+ return matchKey(e.key, spec.key);
329
+ }
310
330
  function affordanceKindOf(target) {
311
331
  if (typeof target !== "object" || target === null) return void 0;
312
332
  const k = target.kind;
@@ -342,8 +362,7 @@ function matchTarget(target, specTarget, bodyTarget, bodyKind) {
342
362
  const form = parseTargetSpec(specTarget);
343
363
  if (form === null) return false;
344
364
  switch (form.form) {
345
- // Receives the raw target (affordance hit for drag, e.target otherwise)
346
- // plus the bodyTarget classification.
365
+ // Receives the affordance hit plus the bodyTarget classification.
347
366
  case "predicate":
348
367
  return form.kindOf(target, bodyTarget);
349
368
  // Resolved from the `classifyTarget` result packed into the event's
@@ -387,17 +406,6 @@ function matchPhaseAtom(a, ctx) {
387
406
  const isEngaged = ctx.engagedChannels.has(id);
388
407
  return a.phase === "engaged" ? isEngaged : !isEngaged;
389
408
  }
390
- function mimeMatchesGlob(mime, glob) {
391
- const m = mime.toLowerCase();
392
- const g = glob.toLowerCase();
393
- if (g === "*" || g === "*/*") return true;
394
- if (g.endsWith("/*")) return m.startsWith(g.slice(0, -1));
395
- return m === g;
396
- }
397
- function matchIngestTypes(items, types) {
398
- if (!types || types.length === 0) return true;
399
- return items.some((it) => types.some((g) => mimeMatchesGlob(it.mime, g)));
400
- }
401
409
  function matchSpec(e, spec, isMac, phaseCtx) {
402
410
  if (spec.phase !== void 0) {
403
411
  const ctx = phaseCtx ?? { selfChannel: null, engagedChannels: EMPTY_ENGAGED };
@@ -406,13 +414,13 @@ function matchSpec(e, spec, isMac, phaseCtx) {
406
414
  switch (spec.kind) {
407
415
  case "key": {
408
416
  if (e.kind !== "key") return false;
409
- if (!matchKey(e.key, spec.key)) return false;
417
+ if (!matchKeyOrCode(e, spec)) return false;
410
418
  return matchModifiers(e, spec.mods, isMac);
411
419
  }
412
420
  case "key-held": {
413
421
  if (e.kind !== "key-held") return false;
414
422
  if (e.phase !== "down") return false;
415
- if (!matchKey(e.key, spec.key)) return false;
423
+ if (!matchKeyOrCode(e, spec)) return false;
416
424
  return matchModifiers(e, spec.mods, isMac);
417
425
  }
418
426
  case "wheel": {
@@ -441,13 +449,13 @@ function matchSpec(e, spec, isMac, phaseCtx) {
441
449
  if (!matchModifiers(e, spec.mods, isMac)) return false;
442
450
  return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
443
451
  }
444
- case "contextMenu": {
445
- if (e.kind !== "contextmenu") return false;
446
- if (!matchModifiers(e, spec.mods, isMac)) return false;
447
- return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
448
- }
449
- case "longPress": {
450
- if (e.kind !== "longpress") return false;
452
+ // Each happens at a point, so its target is what the event classified
453
+ // there: for a long-press, the affordance its press landed on.
454
+ case "contextMenu":
455
+ case "longPress":
456
+ case "hover": {
457
+ if (e.kind !== "contextmenu" && e.kind !== "longpress" && e.kind !== "hover") return false;
458
+ if (e.kind !== spec.kind.toLowerCase()) return false;
451
459
  if (!matchModifiers(e, spec.mods, isMac)) return false;
452
460
  return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
453
461
  }
@@ -501,7 +509,8 @@ var SPEC_KIND_TO_GESTURE = {
501
509
  longPress: "longPress",
502
510
  drag: "drag",
503
511
  pointerDown: "pointerDown",
504
- multiTouch: void 0,
512
+ hover: "hover",
513
+ multiTouch: "multiTouch",
505
514
  multiTouchTap: "multiTouchTap",
506
515
  drop: "drop",
507
516
  paste: "paste"
@@ -550,6 +559,7 @@ function routeToSpec(parsed) {
550
559
  return fail(`${parsed.gesture} has no GestureSpec kind`);
551
560
  case "click":
552
561
  case "pointerDown":
562
+ case "hover":
553
563
  case "doubleClick":
554
564
  case "drag":
555
565
  case "contextMenu":
@@ -567,14 +577,13 @@ function routeToSpec(parsed) {
567
577
  for (const m of optionalMods) optional[m] = "optional";
568
578
  return withMods({ kind, key, ...optionalMods.length > 0 ? { mods: optional } : {} });
569
579
  }
580
+ case "multiTouch":
570
581
  case "multiTouchTap":
571
- if (arg === void 0) return fail("a multiTouchTap spec needs concrete fingers");
582
+ if (arg === void 0) return fail(`a ${kind} spec needs concrete fingers`);
572
583
  return withMods({ kind, fingers: Number(arg) });
573
584
  case "drop":
574
585
  case "paste":
575
586
  return withMods({ kind, ...arg ? { types: arg.split("|") } : {} });
576
- case "multiTouch":
577
- return fail("multiTouch has no route gesture");
578
587
  }
579
588
  }
580
589
 
@@ -593,8 +602,8 @@ var ROUTE_TERMS = {
593
602
  };
594
603
  var ROUTE_FIELD_DEFINITIONS = {
595
604
  phases: "Which lifecycle stage(s) a channel must be in for this route to fire. `initial` = the channel is idle; `engaged` = the channel is mid-gesture. The channel is the binding's own tool (`&`), any tool (`*`), or a named tool id.",
596
- gesture: "The class of input event that triggers this route \u2014 click, drag, double-tap, keyDown/keyUp, wheel, pinch, contextMenu, multiTouchTap, drop, or paste.",
597
- arg: "Sub-class of the gesture. Direction for wheel (up / down / *) and pinch (in / out / *), key name for keyDown / keyUp, finger count for multiTouchTap, MIME glob for drop / paste. Other gestures have no arg slot.",
605
+ gesture: "The class of input event that triggers this route \u2014 click, drag, double-tap, keyDown/keyUp, wheel, pinch, contextMenu, multiTouch, multiTouchTap, drop, or paste.",
606
+ arg: "Sub-class of the gesture. Direction for wheel (up / down / *) and pinch (in / out / *), key name for keyDown / keyUp, finger count for multiTouch / multiTouchTap, MIME glob for drop / paste. Other gestures have no arg slot.",
598
607
  target: "Which hit-test result the gesture must land on. `*` matches any target; `empty` matches the empty canvas; otherwise the value names a specific hit-target kind.",
599
608
  modifiers: "Modifier keys the user must hold (`+key`) or may optionally hold (`?key`) for this route to match. Unlisted modifiers must not be held."
600
609
  };
@@ -666,6 +675,8 @@ function actionClause(parsed, required) {
666
675
  const direction = arg === "in" ? " in" : arg === "out" ? " out" : "";
667
676
  return userDoes(required, `pinches${direction}${target}`);
668
677
  }
678
+ case "multiTouch":
679
+ return userDoes(required, `touches with ${arg ?? "multiple"} fingers`);
669
680
  case "multiTouchTap":
670
681
  return userDoes(required, `taps with ${arg ?? "multiple"} fingers`);
671
682
  case "contextMenu":
@@ -676,6 +687,8 @@ function actionClause(parsed, required) {
676
687
  return userDoes(required, `clicks${target}`);
677
688
  case "pointerDown":
678
689
  return userDoes(required, `presses${target}`);
690
+ case "hover":
691
+ return userDoes(required, `moves the pointer${target}`);
679
692
  case "dblTap":
680
693
  return userDoes(required, `double-taps${target}`);
681
694
  case "drag":
@@ -725,6 +738,54 @@ function canonicalModifiers(mods2) {
725
738
  return Object.entries(mods2).sort(([a], [b]) => a.localeCompare(b)).map(([name, req]) => `${name}=${req}`).join("&");
726
739
  }
727
740
 
741
+ // src/ui/stylus.ts
742
+ function stylusOf(src) {
743
+ return {
744
+ ...typeof src.pressure === "number" ? { pressure: src.pressure } : {},
745
+ ...typeof src.tiltX === "number" ? { tiltX: src.tiltX } : {},
746
+ ...typeof src.tiltY === "number" ? { tiltY: src.tiltY } : {}
747
+ };
748
+ }
749
+
750
+ // src/ui/spec.ts
751
+ function hitPredicate(fn) {
752
+ return Object.assign(
753
+ (hit, bodyTarget) => fn(hit, bodyTarget),
754
+ { readsAffordance: true }
755
+ );
756
+ }
757
+ function bodyPredicate(fn) {
758
+ return Object.assign(
759
+ (_hit, bodyTarget) => fn(bodyTarget),
760
+ { readsAffordance: false }
761
+ );
762
+ }
763
+
764
+ // src/ui/platform.ts
765
+ function isApplePlatform() {
766
+ if (typeof navigator === "undefined" || !navigator) return false;
767
+ const nav = navigator;
768
+ const raw = nav.userAgentData?.platform || nav.platform || nav.userAgent || "";
769
+ return /mac|iphone|ipad|ipod/i.test(raw);
770
+ }
771
+
772
+ // src/ui/fixedSeat.ts
773
+ var HOST_SELECTOR = "[data-wzl-portal-host],[data-wzl-theme],[data-wzl-mode]";
774
+ function nearestPortalHost(from) {
775
+ return from?.closest(HOST_SELECTOR) ?? null;
776
+ }
777
+ function containsFixed(el) {
778
+ const cs = getComputedStyle(el);
779
+ const set = (value) => value !== void 0 && value !== "" && value !== "none";
780
+ return set(cs.transform) || set(cs.filter) || set(cs.backdropFilter) || set(cs.perspective) || /paint|layout|strict|content/.test(cs.contain ?? "");
781
+ }
782
+ function fixedSeat(from) {
783
+ const host = nearestPortalHost(from) ?? document.body;
784
+ if (host === document.body || !containsFixed(host)) return { host, left: 0, top: 0 };
785
+ const rect = host.getBoundingClientRect();
786
+ return { host, left: rect.left, top: rect.top };
787
+ }
788
+
728
789
  // src/ui/specificity.ts
729
790
  function modsCount(mods2) {
730
791
  if (!mods2) return 0;
@@ -783,6 +844,313 @@ function pastDragThreshold(from, to, threshold = DRAG_THRESHOLD_PX) {
783
844
  return Math.hypot(to.clientX - from.clientX, to.clientY - from.clientY) >= threshold;
784
845
  }
785
846
 
847
+ // src/recognizer/thresholds.ts
848
+ var LONG_PRESS_MS = 500;
849
+ var DOUBLE_CLICK_MAX_MS = 600;
850
+ var DOUBLE_CLICK_MAX_PX = 8;
851
+ var TAP_THRESHOLD_PX = 8;
852
+
853
+ // src/recognizer/pressFacts.ts
854
+ function pressedOn(down) {
855
+ return {
856
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
857
+ ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
858
+ ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
859
+ };
860
+ }
861
+ function clickEvents(down, up) {
862
+ const shared = {
863
+ target: up.target,
864
+ altKey: up.altKey,
865
+ ctrlKey: up.ctrlKey,
866
+ metaKey: up.metaKey,
867
+ shiftKey: up.shiftKey,
868
+ x: up.worldX,
869
+ y: up.worldY,
870
+ clientX: up.clientX,
871
+ clientY: up.clientY,
872
+ ...pressedOn(down)
873
+ };
874
+ return {
875
+ click: { kind: "click", ...shared, pressX: down.worldX, pressY: down.worldY },
876
+ doubleclick: { kind: "doubleclick", ...shared }
877
+ };
878
+ }
879
+ function longPressEvents(down) {
880
+ const shared = {
881
+ target: down.target,
882
+ altKey: down.altKey,
883
+ ctrlKey: down.ctrlKey,
884
+ metaKey: down.metaKey,
885
+ shiftKey: down.shiftKey,
886
+ ...pressedOn(down)
887
+ };
888
+ const at2 = { x: down.worldX, y: down.worldY, clientX: down.clientX, clientY: down.clientY };
889
+ return {
890
+ longpress: { kind: "longpress", ...at2, ...shared },
891
+ contextmenu: { kind: "contextmenu", ...at2, ...shared }
892
+ };
893
+ }
894
+
895
+ // src/recognizer/click.ts
896
+ function composeClick(down, up, clicks, now, dispatch, observer) {
897
+ const evs = clickEvents(down, up);
898
+ const world = { x: up.worldX, y: up.worldY };
899
+ dispatch(evs.click);
900
+ observer.click?.(world);
901
+ const t = now();
902
+ const prev = clicks.last;
903
+ 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;
904
+ if (!follows) {
905
+ clicks.last = { t, clientX: up.clientX, clientY: up.clientY };
906
+ return {};
907
+ }
908
+ observer.doubleClick?.(world);
909
+ const doubleClick = dispatch(evs.doubleclick);
910
+ clicks.last = null;
911
+ return { doubleClick };
912
+ }
913
+
914
+ // src/recognizer/longPressHold.ts
915
+ function createLongPressHold(clock, onChange) {
916
+ let pending = null;
917
+ const cancel = (pointerId) => {
918
+ if (!pending || pointerId !== void 0 && pending.pointerId !== pointerId) return;
919
+ clock.clearTimer(pending.timer);
920
+ pending = null;
921
+ onChange(null);
922
+ };
923
+ return {
924
+ arm(press, fire) {
925
+ cancel();
926
+ const timer = clock.setTimer(() => {
927
+ pending = null;
928
+ onChange(null);
929
+ fire();
930
+ }, press.duration);
931
+ pending = { pointerId: press.pointerId, timer };
932
+ onChange(press);
933
+ },
934
+ pendingFor: (pointerId) => pending?.pointerId === pointerId,
935
+ cancel
936
+ };
937
+ }
938
+
939
+ // src/recognizer/multiTouch.ts
940
+ function multiTouchGeometry(positions) {
941
+ const pts = [...positions];
942
+ if (pts.length < 2) return { centroid: { x: 0, y: 0 }, spread: 0, angle: 0 };
943
+ let sumX = 0;
944
+ let sumY = 0;
945
+ for (const p of pts) {
946
+ sumX += p.x;
947
+ sumY += p.y;
948
+ }
949
+ const dx = pts[1].x - pts[0].x;
950
+ const dy = pts[1].y - pts[0].y;
951
+ return {
952
+ centroid: { x: sumX / pts.length, y: sumY / pts.length },
953
+ spread: Math.sqrt(dx * dx + dy * dy),
954
+ angle: Math.atan2(dy, dx) * 180 / Math.PI
955
+ };
956
+ }
957
+ function createMultiTouchEpisode() {
958
+ let episode = null;
959
+ return {
960
+ join(positions, centroid, now) {
961
+ if (!episode) {
962
+ episode = {
963
+ fingers: positions.size,
964
+ centroid: { ...centroid },
965
+ joinedAt: /* @__PURE__ */ new Map(),
966
+ moved: false,
967
+ startedAt: now
968
+ };
969
+ } else {
970
+ episode.fingers = positions.size;
971
+ }
972
+ for (const [id, p] of positions) {
973
+ if (!episode.joinedAt.has(id)) episode.joinedAt.set(id, { ...p });
974
+ }
975
+ },
976
+ moved(pointerId, to) {
977
+ const from = episode?.joinedAt.get(pointerId);
978
+ if (episode && from && Math.hypot(to.x - from.x, to.y - from.y) > TAP_THRESHOLD_PX) episode.moved = true;
979
+ },
980
+ finish(centroid, now, maxHoldMs) {
981
+ const e = episode;
982
+ episode = null;
983
+ if (!e) return null;
984
+ const brief = now - e.startedAt < maxHoldMs;
985
+ const still = Math.hypot(centroid.x - e.centroid.x, centroid.y - e.centroid.y) <= TAP_THRESHOLD_PX;
986
+ return brief && !e.moved && still ? e.fingers : null;
987
+ },
988
+ abandon() {
989
+ episode = null;
990
+ }
991
+ };
992
+ }
993
+
994
+ // src/recognizer/samples.ts
995
+ function modifiersOf(e) {
996
+ return { altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
997
+ }
998
+ function pressFactsOf(s) {
999
+ return {
1000
+ target: s.target,
1001
+ clientX: s.clientX,
1002
+ clientY: s.clientY,
1003
+ worldX: s.x,
1004
+ worldY: s.y,
1005
+ ...modifiersOf(s),
1006
+ ...pressedOn(s)
1007
+ };
1008
+ }
1009
+ var at = (s, world) => ({
1010
+ pointerId: s.pointerId,
1011
+ x: world.x,
1012
+ y: world.y,
1013
+ clientX: s.clientX,
1014
+ clientY: s.clientY,
1015
+ ...modifiersOf(s)
1016
+ });
1017
+ function pointerDownEvent(s) {
1018
+ return { kind: "pointerdown", target: s.target, ...at(s, s), ...stylusOf(s), ...pressedOn(s) };
1019
+ }
1020
+ function pointerMoveEvent(s, world) {
1021
+ return { kind: "pointermove", ...at(s, world), ...stylusOf(s) };
1022
+ }
1023
+ function pointerUpEvent(s, world) {
1024
+ return { kind: "pointerup", ...at(s, world) };
1025
+ }
1026
+
1027
+ // src/recognizer/pointerRecognizer.ts
1028
+ function multiTouchEvent(fingers, mods2, more) {
1029
+ return { kind: "multitouch", fingers, ...modifiersOf(mods2), ...more };
1030
+ }
1031
+ var positionsOf = (held) => new Map([...held].map(([id, p]) => [id, p.pos]));
1032
+ function heldGeometry(held, toSurface, extra) {
1033
+ const g = multiTouchGeometry([...positionsOf(held).values(), ...extra]);
1034
+ return { ...g, centroid: toSurface(g.centroid) };
1035
+ }
1036
+ function fireLongPress(rec, sink, observer) {
1037
+ if (!rec) return;
1038
+ const evs = longPressEvents(rec.down);
1039
+ let result = sink.dispatch(evs.longpress);
1040
+ if (result === "unhandled") result = sink.dispatch(evs.contextmenu);
1041
+ if (result === "unhandled") return;
1042
+ rec.buffered = null;
1043
+ if (rec.holdType) observer.longPress?.(rec.holdType);
1044
+ }
1045
+ function createPointerRecognizer(opts) {
1046
+ const { sink, clock, observer = {}, toSurface = (p) => p } = opts;
1047
+ const world = (s) => opts.toWorld?.(s.clientX, s.clientY) ?? { x: s.clientX, y: s.clientY };
1048
+ const clicks = opts.clicks ?? { last: null };
1049
+ const held = /* @__PURE__ */ new Map();
1050
+ const touches = createMultiTouchEpisode();
1051
+ const hold = createLongPressHold(clock, (pending) => {
1052
+ observer.holdChanged?.(pending);
1053
+ });
1054
+ const longPressMs = () => opts.longPressMs?.() ?? LONG_PRESS_MS;
1055
+ const geometry = (extra = []) => heldGeometry(held, toSurface, extra);
1056
+ const claimForMultiTouch = (mods2) => {
1057
+ hold.cancel();
1058
+ for (const p of held.values()) p.buffered = null;
1059
+ const g = geometry();
1060
+ touches.join(positionsOf(held), g.centroid, clock.now());
1061
+ sink.dispatch(multiTouchEvent(held.size, mods2, g));
1062
+ };
1063
+ return {
1064
+ press(s) {
1065
+ const ev = pointerDownEvent(s);
1066
+ const rec = { pos: { x: s.clientX, y: s.clientY }, buffered: ev, down: pressFactsOf(s) };
1067
+ held.set(s.pointerId, rec);
1068
+ if (held.size < 2) sink.dispatch({ ...ev, stage: "press" });
1069
+ if ((s.pointerType === "touch" || s.pointerType === "pen") && held.size === 1) {
1070
+ rec.holdType = s.pointerType;
1071
+ hold.arm(
1072
+ { pointerId: s.pointerId, pointerType: s.pointerType, press: rec.down, startedAt: clock.now(), duration: longPressMs() },
1073
+ () => {
1074
+ fireLongPress(held.get(s.pointerId), sink, observer);
1075
+ }
1076
+ );
1077
+ }
1078
+ if (held.size >= 2) claimForMultiTouch(s);
1079
+ },
1080
+ move(s) {
1081
+ const rec = held.get(s.pointerId);
1082
+ const to = { x: s.clientX, y: s.clientY };
1083
+ if (rec) rec.pos = to;
1084
+ touches.moved(s.pointerId, to);
1085
+ if (rec?.buffered && pastDragThreshold(rec.down, s)) {
1086
+ const press = rec.buffered;
1087
+ rec.buffered = null;
1088
+ hold.cancel(s.pointerId);
1089
+ sink.dispatch(press);
1090
+ }
1091
+ if (held.size >= 2 && sink.multiTouchInFlight(held.size)) {
1092
+ sink.dispatch(multiTouchEvent(held.size, s, geometry()));
1093
+ }
1094
+ sink.dispatch(pointerMoveEvent(s, world(s)));
1095
+ },
1096
+ release(s) {
1097
+ hold.cancel(s.pointerId);
1098
+ const before = held.size;
1099
+ const rec = held.get(s.pointerId);
1100
+ held.delete(s.pointerId);
1101
+ if (before >= 2) sink.dispatch(multiTouchEvent(before, s, { end: "commit" }));
1102
+ if (before >= 2 && held.size < 2) {
1103
+ const fingers = touches.finish(
1104
+ geometry([{ x: s.clientX, y: s.clientY }]).centroid,
1105
+ clock.now(),
1106
+ longPressMs()
1107
+ );
1108
+ if (fingers !== null) sink.dispatch({ kind: "multitouchtap", fingers, ...modifiersOf(s) });
1109
+ }
1110
+ sink.dispatch(pointerUpEvent(s, world(s)));
1111
+ if (!rec?.buffered) return {};
1112
+ const at2 = world(s);
1113
+ const up = {
1114
+ target: s.target,
1115
+ clientX: s.clientX,
1116
+ clientY: s.clientY,
1117
+ worldX: at2.x,
1118
+ worldY: at2.y,
1119
+ ...modifiersOf(s)
1120
+ };
1121
+ return composeClick(rec.down, up, clicks, () => clock.now(), (ev) => sink.dispatch(ev), observer);
1122
+ },
1123
+ cancel(pointerId) {
1124
+ const rec = held.get(pointerId);
1125
+ if (!rec) return;
1126
+ const before = held.size;
1127
+ held.delete(pointerId);
1128
+ hold.cancel(pointerId);
1129
+ if (before >= 2) sink.dispatch(multiTouchEvent(before, rec.down, { end: "cancel" }));
1130
+ touches.abandon();
1131
+ sink.dispatch({ kind: "pointercancel", pointerId, ...modifiersOf(rec.down) });
1132
+ },
1133
+ takeHold() {
1134
+ let holding = false;
1135
+ for (const [id, rec] of held) {
1136
+ if (!rec.holdType) continue;
1137
+ holding = true;
1138
+ if (!hold.pendingFor(id)) continue;
1139
+ hold.cancel(id);
1140
+ fireLongPress(rec, sink, observer);
1141
+ }
1142
+ return holding;
1143
+ },
1144
+ heldCount: () => held.size,
1145
+ positionOf: (pointerId) => held.get(pointerId)?.pos,
1146
+ reset() {
1147
+ hold.cancel();
1148
+ held.clear();
1149
+ touches.abandon();
1150
+ }
1151
+ };
1152
+ }
1153
+
786
1154
  // src/ui/controlTarget.ts
787
1155
  var CONTROL_SELECTOR = [
788
1156
  "button",
@@ -804,6 +1172,47 @@ function isInControlWithin(target, host) {
804
1172
  return control !== null && control !== host && host.contains(control);
805
1173
  }
806
1174
 
807
- export { ACTIVE_SIGILS, CONTROL_SELECTOR, DRAG_THRESHOLD_PX, GESTURE_DESCRIPTORS, MOD_NAME_SET, RESERVED_ID_NAMES, RESERVED_ID_PREFIXES, RESERVED_SIGILS, ROUTE_FIELD_DEFINITIONS, ROUTE_TERMS, VALID_MOD_NAMES, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, isInControlWithin, isKnownGestureName, keyRouteToSpec, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity };
1175
+ // src/ui/keyOwner.ts
1176
+ function isEditableTarget(target) {
1177
+ if (!target || !(target instanceof Element)) return false;
1178
+ const tag = target.tagName;
1179
+ if (tag === "INPUT" || tag === "TEXTAREA") return true;
1180
+ if (target.isContentEditable) return true;
1181
+ const ce = target.getAttribute("contenteditable");
1182
+ return ce === "true" || ce === "";
1183
+ }
1184
+ var ACTIVATED_BY_KEY = [
1185
+ "button",
1186
+ "a[href]",
1187
+ "summary",
1188
+ "select",
1189
+ 'input[type="button"]',
1190
+ 'input[type="submit"]',
1191
+ 'input[type="reset"]',
1192
+ 'input[type="checkbox"]',
1193
+ 'input[type="radio"]',
1194
+ '[role="button"]',
1195
+ '[role="link"]',
1196
+ '[role="checkbox"]',
1197
+ '[role="switch"]',
1198
+ '[role="menuitem"]',
1199
+ '[role="tab"]',
1200
+ '[role="option"]'
1201
+ ].join(", ");
1202
+ function activatesFocusedControl(e) {
1203
+ if (e.key !== " " && e.key !== "Enter") return false;
1204
+ return e.target instanceof Element && e.target.matches(ACTIVATED_BY_KEY);
1205
+ }
1206
+ var SELECT_NAVIGATION = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
1207
+ function selectOwnsKey(e) {
1208
+ if (!(e.target instanceof Element) || e.target.tagName !== "SELECT") return false;
1209
+ if (e.metaKey || e.ctrlKey || e.altKey) return false;
1210
+ return SELECT_NAVIGATION.has(e.key) || e.key.length === 1;
1211
+ }
1212
+ function focusedControlOwnsKey(e) {
1213
+ return activatesFocusedControl(e) || selectOwnsKey(e);
1214
+ }
1215
+
1216
+ 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, 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 };
808
1217
  //# sourceMappingURL=index.js.map
809
1218
  //# sourceMappingURL=index.js.map