@weasel-js/gestures 1.9.4 → 1.10.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
@@ -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, arg: { name: "motion", values: ["move", "leave"], default: "move" } },
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: "*" } },
@@ -271,47 +272,21 @@ function keyRouteToSpec(r) {
271
272
  return spec;
272
273
  }
273
274
 
274
- // src/ui/match.ts
275
- function resolveSpecValue(value) {
276
- if (value === true) return "required";
277
- if (value === "optional") return "optional";
278
- return "forbidden";
279
- }
280
- function matchModifiers(e, mods2, isMac) {
281
- let alt = "forbidden";
282
- let ctrl = "forbidden";
283
- let meta = "forbidden";
284
- let shift = "forbidden";
285
- if (mods2) {
286
- alt = resolveSpecValue(mods2.alt);
287
- shift = resolveSpecValue(mods2.shift);
288
- if (mods2.mod !== void 0) {
289
- const modReq = resolveSpecValue(mods2.mod);
290
- if (isMac) meta = modReq;
291
- else ctrl = modReq;
292
- } else {
293
- meta = resolveSpecValue(mods2.meta);
294
- ctrl = resolveSpecValue(mods2.ctrl);
295
- }
296
- }
297
- return checkKey(alt, e.altKey) && checkKey(ctrl, e.ctrlKey) && checkKey(meta, e.metaKey) && checkKey(shift, e.shiftKey);
298
- }
299
- function checkKey(req, held) {
300
- if (req === "required") return held;
301
- if (req === "forbidden") return !held;
302
- return true;
303
- }
304
- function matchKey(eventKey, specKey) {
305
- const lower = eventKey.toLowerCase();
306
- if (Array.isArray(specKey)) {
307
- return specKey.some((k) => k.toLowerCase() === lower);
308
- }
309
- return specKey.toLowerCase() === lower;
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;
310
282
  }
311
- function matchKeyOrCode(e, spec) {
312
- if (spec.code !== void 0 && e.code !== void 0 && [spec.code].flat().includes(e.code)) return true;
313
- return matchKey(e.key, spec.key);
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)));
314
287
  }
288
+
289
+ // src/ui/matchTarget.ts
315
290
  function affordanceKindOf(target) {
316
291
  if (typeof target !== "object" || target === null) return void 0;
317
292
  const k = target.kind;
@@ -364,6 +339,48 @@ function matchTarget(target, specTarget, bodyTarget, bodyKind) {
364
339
  }
365
340
  }
366
341
  }
342
+
343
+ // src/ui/match.ts
344
+ function resolveSpecValue(value) {
345
+ if (value === true) return "required";
346
+ if (value === "optional") return "optional";
347
+ return "forbidden";
348
+ }
349
+ function matchModifiers(e, mods2, isMac) {
350
+ let alt = "forbidden";
351
+ let ctrl = "forbidden";
352
+ let meta = "forbidden";
353
+ let shift = "forbidden";
354
+ if (mods2) {
355
+ alt = resolveSpecValue(mods2.alt);
356
+ shift = resolveSpecValue(mods2.shift);
357
+ if (mods2.mod !== void 0) {
358
+ const modReq = resolveSpecValue(mods2.mod);
359
+ if (isMac) meta = modReq;
360
+ else ctrl = modReq;
361
+ } else {
362
+ meta = resolveSpecValue(mods2.meta);
363
+ ctrl = resolveSpecValue(mods2.ctrl);
364
+ }
365
+ }
366
+ return checkKey(alt, e.altKey) && checkKey(ctrl, e.ctrlKey) && checkKey(meta, e.metaKey) && checkKey(shift, e.shiftKey);
367
+ }
368
+ function checkKey(req, held) {
369
+ if (req === "required") return held;
370
+ if (req === "forbidden") return !held;
371
+ return true;
372
+ }
373
+ function matchKey(eventKey, specKey) {
374
+ const lower = eventKey.toLowerCase();
375
+ if (Array.isArray(specKey)) {
376
+ return specKey.some((k) => k.toLowerCase() === lower);
377
+ }
378
+ return specKey.toLowerCase() === lower;
379
+ }
380
+ function matchKeyOrCode(e, spec) {
381
+ if (spec.code !== void 0 && e.code !== void 0 && [spec.code].flat().includes(e.code)) return true;
382
+ return matchKey(e.key, spec.key);
383
+ }
367
384
  function normalizePhase(spec) {
368
385
  if (typeof spec === "string") {
369
386
  return [{ channel: "&", phase: spec }];
@@ -391,16 +408,15 @@ function matchPhaseAtom(a, ctx) {
391
408
  const isEngaged = ctx.engagedChannels.has(id);
392
409
  return a.phase === "engaged" ? isEngaged : !isEngaged;
393
410
  }
394
- function mimeMatchesGlob(mime, glob) {
395
- const m = mime.toLowerCase();
396
- const g = glob.toLowerCase();
397
- if (g === "*" || g === "*/*") return true;
398
- if (g.endsWith("/*")) return m.startsWith(g.slice(0, -1));
399
- return m === g;
400
- }
401
- function matchIngestTypes(items, types) {
402
- if (!types || types.length === 0) return true;
403
- return items.some((it) => types.some((g) => mimeMatchesGlob(it.mime, g)));
411
+ function matchPress(e, spec, isMac) {
412
+ if (e.kind !== "pointerdown") return false;
413
+ if (spec.kind === "drag") {
414
+ if (e.stage === "press") return false;
415
+ } else if (e.stage !== "press") {
416
+ return false;
417
+ }
418
+ if (!matchModifiers(e, spec.mods, isMac)) return false;
419
+ return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
404
420
  }
405
421
  function matchSpec(e, spec, isMac, phaseCtx) {
406
422
  if (spec.phase !== void 0) {
@@ -445,27 +461,25 @@ function matchSpec(e, spec, isMac, phaseCtx) {
445
461
  if (!matchModifiers(e, spec.mods, isMac)) return false;
446
462
  return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
447
463
  }
448
- case "contextMenu": {
449
- if (e.kind !== "contextmenu") return false;
450
- if (!matchModifiers(e, spec.mods, isMac)) return false;
451
- return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
452
- }
464
+ // Each happens at a point, so its target is what the event classified
465
+ // there: for a long-press, the affordance its press landed on.
466
+ case "contextMenu":
453
467
  case "longPress": {
454
- if (e.kind !== "longpress") return false;
455
- if (!matchModifiers(e, spec.mods, isMac)) return false;
456
- return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
457
- }
458
- case "drag": {
459
- if (e.kind !== "pointerdown") return false;
460
- if (e.stage === "press") return false;
468
+ if (e.kind !== "contextmenu" && e.kind !== "longpress") return false;
469
+ if (e.kind !== spec.kind.toLowerCase()) return false;
461
470
  if (!matchModifiers(e, spec.mods, isMac)) return false;
462
471
  return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
463
472
  }
464
- case "pointerDown": {
465
- if (e.kind !== "pointerdown" || e.stage !== "press") return false;
473
+ case "hover": {
474
+ if (e.kind !== "hover") return false;
466
475
  if (!matchModifiers(e, spec.mods, isMac)) return false;
467
- return matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
476
+ const on = !e.off && matchTarget(e.affordance, spec.target, e.bodyTarget, e.bodyKind);
477
+ if (spec.motion !== "leave") return on;
478
+ return !on && e.from !== void 0 && matchTarget(e.from.affordance, spec.target, e.from.bodyTarget, e.from.bodyKind);
468
479
  }
480
+ case "drag":
481
+ case "pointerDown":
482
+ return matchPress(e, spec, isMac);
469
483
  case "multiTouch": {
470
484
  if (e.kind !== "multitouch") return false;
471
485
  if (e.fingers !== spec.fingers) return false;
@@ -505,6 +519,7 @@ var SPEC_KIND_TO_GESTURE = {
505
519
  longPress: "longPress",
506
520
  drag: "drag",
507
521
  pointerDown: "pointerDown",
522
+ hover: "hover",
508
523
  multiTouch: "multiTouch",
509
524
  multiTouchTap: "multiTouchTap",
510
525
  drop: "drop",
@@ -552,6 +567,8 @@ function routeToSpec(parsed) {
552
567
  switch (kind) {
553
568
  case void 0:
554
569
  return fail(`${parsed.gesture} has no GestureSpec kind`);
570
+ case "hover":
571
+ return withMods({ kind, ...arg === "leave" ? { motion: "leave" } : {}, ...t });
555
572
  case "click":
556
573
  case "pointerDown":
557
574
  case "doubleClick":
@@ -620,6 +637,10 @@ function partitionModifiers(mods2) {
620
637
  }
621
638
  return { required, optional };
622
639
  }
640
+ function leftClause(target) {
641
+ if (target === void 0 || target === "*") return "off the surface";
642
+ return target === "empty" ? "off empty canvas" : `off a ${target}`;
643
+ }
623
644
  function phaseAtomParts({ channel, phase }) {
624
645
  const subject = channel === "&" ? "the tool" : channel === "*" ? "any tool" : `the ${channel} tool`;
625
646
  if (phase === "initial") return [`${subject} is `, term("idle")];
@@ -681,6 +702,8 @@ function actionClause(parsed, required) {
681
702
  return userDoes(required, `clicks${target}`);
682
703
  case "pointerDown":
683
704
  return userDoes(required, `presses${target}`);
705
+ case "hover":
706
+ return userDoes(required, arg === "leave" ? `moves the pointer ${leftClause(parsed.target)}` : `moves the pointer${target}`);
684
707
  case "dblTap":
685
708
  return userDoes(required, `double-taps${target}`);
686
709
  case "drag":
@@ -730,6 +753,15 @@ function canonicalModifiers(mods2) {
730
753
  return Object.entries(mods2).sort(([a], [b]) => a.localeCompare(b)).map(([name, req]) => `${name}=${req}`).join("&");
731
754
  }
732
755
 
756
+ // src/ui/stylus.ts
757
+ function stylusOf(src) {
758
+ return {
759
+ ...typeof src.pressure === "number" ? { pressure: src.pressure } : {},
760
+ ...typeof src.tiltX === "number" ? { tiltX: src.tiltX } : {},
761
+ ...typeof src.tiltY === "number" ? { tiltY: src.tiltY } : {}
762
+ };
763
+ }
764
+
733
765
  // src/ui/spec.ts
734
766
  function hitPredicate(fn) {
735
767
  return Object.assign(
@@ -752,6 +784,23 @@ function isApplePlatform() {
752
784
  return /mac|iphone|ipad|ipod/i.test(raw);
753
785
  }
754
786
 
787
+ // src/ui/fixedSeat.ts
788
+ var HOST_SELECTOR = "[data-wzl-portal-host],[data-wzl-theme],[data-wzl-mode]";
789
+ function nearestPortalHost(from) {
790
+ return from?.closest(HOST_SELECTOR) ?? null;
791
+ }
792
+ function containsFixed(el) {
793
+ const cs = getComputedStyle(el);
794
+ const set = (value) => value !== void 0 && value !== "" && value !== "none";
795
+ return set(cs.transform) || set(cs.filter) || set(cs.backdropFilter) || set(cs.perspective) || /paint|layout|strict|content/.test(cs.contain ?? "");
796
+ }
797
+ function fixedSeat(from) {
798
+ const host = nearestPortalHost(from) ?? document.body;
799
+ if (host === document.body || !containsFixed(host)) return { host, left: 0, top: 0 };
800
+ const rect = host.getBoundingClientRect();
801
+ return { host, left: rect.left, top: rect.top };
802
+ }
803
+
755
804
  // src/ui/specificity.ts
756
805
  function modsCount(mods2) {
757
806
  if (!mods2) return 0;
@@ -810,6 +859,313 @@ function pastDragThreshold(from, to, threshold = DRAG_THRESHOLD_PX) {
810
859
  return Math.hypot(to.clientX - from.clientX, to.clientY - from.clientY) >= threshold;
811
860
  }
812
861
 
862
+ // src/recognizer/thresholds.ts
863
+ var LONG_PRESS_MS = 500;
864
+ var DOUBLE_CLICK_MAX_MS = 600;
865
+ var DOUBLE_CLICK_MAX_PX = 8;
866
+ var TAP_THRESHOLD_PX = 8;
867
+
868
+ // src/recognizer/pressFacts.ts
869
+ function pressedOn(down) {
870
+ return {
871
+ ...down.affordance !== void 0 ? { affordance: down.affordance } : {},
872
+ ...down.bodyTarget !== void 0 ? { bodyTarget: down.bodyTarget } : {},
873
+ ...down.bodyKind !== void 0 ? { bodyKind: down.bodyKind } : {}
874
+ };
875
+ }
876
+ function clickEvents(down, up) {
877
+ const shared = {
878
+ target: up.target,
879
+ altKey: up.altKey,
880
+ ctrlKey: up.ctrlKey,
881
+ metaKey: up.metaKey,
882
+ shiftKey: up.shiftKey,
883
+ x: up.worldX,
884
+ y: up.worldY,
885
+ clientX: up.clientX,
886
+ clientY: up.clientY,
887
+ ...pressedOn(down)
888
+ };
889
+ return {
890
+ click: { kind: "click", ...shared, pressX: down.worldX, pressY: down.worldY },
891
+ doubleclick: { kind: "doubleclick", ...shared }
892
+ };
893
+ }
894
+ function longPressEvents(down) {
895
+ const shared = {
896
+ target: down.target,
897
+ altKey: down.altKey,
898
+ ctrlKey: down.ctrlKey,
899
+ metaKey: down.metaKey,
900
+ shiftKey: down.shiftKey,
901
+ ...pressedOn(down)
902
+ };
903
+ const at2 = { x: down.worldX, y: down.worldY, clientX: down.clientX, clientY: down.clientY };
904
+ return {
905
+ longpress: { kind: "longpress", ...at2, ...shared },
906
+ contextmenu: { kind: "contextmenu", ...at2, ...shared }
907
+ };
908
+ }
909
+
910
+ // src/recognizer/click.ts
911
+ function composeClick(down, up, clicks, now, dispatch, observer) {
912
+ const evs = clickEvents(down, up);
913
+ const world = { x: up.worldX, y: up.worldY };
914
+ dispatch(evs.click);
915
+ observer.click?.(world);
916
+ const t = now();
917
+ 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;
919
+ if (!follows) {
920
+ clicks.last = { t, clientX: up.clientX, clientY: up.clientY };
921
+ return {};
922
+ }
923
+ observer.doubleClick?.(world);
924
+ const doubleClick = dispatch(evs.doubleclick);
925
+ clicks.last = null;
926
+ return { doubleClick };
927
+ }
928
+
929
+ // src/recognizer/longPressHold.ts
930
+ function createLongPressHold(clock, onChange) {
931
+ let pending = null;
932
+ const cancel = (pointerId) => {
933
+ if (!pending || pointerId !== void 0 && pending.pointerId !== pointerId) return;
934
+ clock.clearTimer(pending.timer);
935
+ pending = null;
936
+ onChange(null);
937
+ };
938
+ return {
939
+ arm(press, fire) {
940
+ cancel();
941
+ const timer = clock.setTimer(() => {
942
+ pending = null;
943
+ onChange(null);
944
+ fire();
945
+ }, press.duration);
946
+ pending = { pointerId: press.pointerId, timer };
947
+ onChange(press);
948
+ },
949
+ pendingFor: (pointerId) => pending?.pointerId === pointerId,
950
+ cancel
951
+ };
952
+ }
953
+
954
+ // src/recognizer/multiTouch.ts
955
+ function multiTouchGeometry(positions) {
956
+ const pts = [...positions];
957
+ if (pts.length < 2) return { centroid: { x: 0, y: 0 }, spread: 0, angle: 0 };
958
+ let sumX = 0;
959
+ let sumY = 0;
960
+ for (const p of pts) {
961
+ sumX += p.x;
962
+ sumY += p.y;
963
+ }
964
+ const dx = pts[1].x - pts[0].x;
965
+ const dy = pts[1].y - pts[0].y;
966
+ return {
967
+ centroid: { x: sumX / pts.length, y: sumY / pts.length },
968
+ spread: Math.sqrt(dx * dx + dy * dy),
969
+ angle: Math.atan2(dy, dx) * 180 / Math.PI
970
+ };
971
+ }
972
+ function createMultiTouchEpisode() {
973
+ let episode = null;
974
+ return {
975
+ join(positions, centroid, now) {
976
+ if (!episode) {
977
+ episode = {
978
+ fingers: positions.size,
979
+ centroid: { ...centroid },
980
+ joinedAt: /* @__PURE__ */ new Map(),
981
+ moved: false,
982
+ startedAt: now
983
+ };
984
+ } else {
985
+ episode.fingers = positions.size;
986
+ }
987
+ for (const [id, p] of positions) {
988
+ if (!episode.joinedAt.has(id)) episode.joinedAt.set(id, { ...p });
989
+ }
990
+ },
991
+ moved(pointerId, to) {
992
+ 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;
994
+ },
995
+ finish(centroid, now, maxHoldMs) {
996
+ const e = episode;
997
+ episode = null;
998
+ if (!e) return null;
999
+ const brief = now - e.startedAt < maxHoldMs;
1000
+ const still = Math.hypot(centroid.x - e.centroid.x, centroid.y - e.centroid.y) <= TAP_THRESHOLD_PX;
1001
+ return brief && !e.moved && still ? e.fingers : null;
1002
+ },
1003
+ abandon() {
1004
+ episode = null;
1005
+ }
1006
+ };
1007
+ }
1008
+
1009
+ // src/recognizer/samples.ts
1010
+ function modifiersOf(e) {
1011
+ return { altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey };
1012
+ }
1013
+ function pressFactsOf(s) {
1014
+ return {
1015
+ target: s.target,
1016
+ clientX: s.clientX,
1017
+ clientY: s.clientY,
1018
+ worldX: s.x,
1019
+ worldY: s.y,
1020
+ ...modifiersOf(s),
1021
+ ...pressedOn(s)
1022
+ };
1023
+ }
1024
+ var at = (s, world) => ({
1025
+ pointerId: s.pointerId,
1026
+ x: world.x,
1027
+ y: world.y,
1028
+ clientX: s.clientX,
1029
+ clientY: s.clientY,
1030
+ ...modifiersOf(s)
1031
+ });
1032
+ function pointerDownEvent(s) {
1033
+ return { kind: "pointerdown", target: s.target, ...at(s, s), ...stylusOf(s), ...pressedOn(s) };
1034
+ }
1035
+ function pointerMoveEvent(s, world) {
1036
+ return { kind: "pointermove", ...at(s, world), ...stylusOf(s) };
1037
+ }
1038
+ function pointerUpEvent(s, world) {
1039
+ return { kind: "pointerup", ...at(s, world) };
1040
+ }
1041
+
1042
+ // src/recognizer/pointerRecognizer.ts
1043
+ function multiTouchEvent(fingers, mods2, more) {
1044
+ return { kind: "multitouch", fingers, ...modifiersOf(mods2), ...more };
1045
+ }
1046
+ var positionsOf = (held) => new Map([...held].map(([id, p]) => [id, p.pos]));
1047
+ function heldGeometry(held, toSurface, extra) {
1048
+ const g = multiTouchGeometry([...positionsOf(held).values(), ...extra]);
1049
+ return { ...g, centroid: toSurface(g.centroid) };
1050
+ }
1051
+ function fireLongPress(rec, sink, observer) {
1052
+ if (!rec) return;
1053
+ const evs = longPressEvents(rec.down);
1054
+ let result = sink.dispatch(evs.longpress);
1055
+ if (result === "unhandled") result = sink.dispatch(evs.contextmenu);
1056
+ if (result === "unhandled") return;
1057
+ rec.buffered = null;
1058
+ if (rec.holdType) observer.longPress?.(rec.holdType);
1059
+ }
1060
+ function createPointerRecognizer(opts) {
1061
+ const { sink, clock, observer = {}, toSurface = (p) => p } = opts;
1062
+ const world = (s) => opts.toWorld?.(s.clientX, s.clientY) ?? { x: s.clientX, y: s.clientY };
1063
+ const clicks = opts.clicks ?? { last: null };
1064
+ const held = /* @__PURE__ */ new Map();
1065
+ const touches = createMultiTouchEpisode();
1066
+ const hold = createLongPressHold(clock, (pending) => {
1067
+ observer.holdChanged?.(pending);
1068
+ });
1069
+ const longPressMs = () => opts.longPressMs?.() ?? LONG_PRESS_MS;
1070
+ const geometry = (extra = []) => heldGeometry(held, toSurface, extra);
1071
+ const claimForMultiTouch = (mods2) => {
1072
+ hold.cancel();
1073
+ for (const p of held.values()) p.buffered = null;
1074
+ const g = geometry();
1075
+ touches.join(positionsOf(held), g.centroid, clock.now());
1076
+ sink.dispatch(multiTouchEvent(held.size, mods2, g));
1077
+ };
1078
+ return {
1079
+ press(s) {
1080
+ const ev = pointerDownEvent(s);
1081
+ const rec = { pos: { x: s.clientX, y: s.clientY }, buffered: ev, down: pressFactsOf(s) };
1082
+ held.set(s.pointerId, rec);
1083
+ if (held.size < 2) sink.dispatch({ ...ev, stage: "press" });
1084
+ if ((s.pointerType === "touch" || s.pointerType === "pen") && held.size === 1) {
1085
+ rec.holdType = s.pointerType;
1086
+ hold.arm(
1087
+ { pointerId: s.pointerId, pointerType: s.pointerType, press: rec.down, startedAt: clock.now(), duration: longPressMs() },
1088
+ () => {
1089
+ fireLongPress(held.get(s.pointerId), sink, observer);
1090
+ }
1091
+ );
1092
+ }
1093
+ if (held.size >= 2) claimForMultiTouch(s);
1094
+ },
1095
+ move(s) {
1096
+ const rec = held.get(s.pointerId);
1097
+ const to = { x: s.clientX, y: s.clientY };
1098
+ if (rec) rec.pos = to;
1099
+ touches.moved(s.pointerId, to);
1100
+ if (rec?.buffered && pastDragThreshold(rec.down, s)) {
1101
+ const press = rec.buffered;
1102
+ rec.buffered = null;
1103
+ hold.cancel(s.pointerId);
1104
+ sink.dispatch(press);
1105
+ }
1106
+ if (held.size >= 2 && sink.multiTouchInFlight(held.size)) {
1107
+ sink.dispatch(multiTouchEvent(held.size, s, geometry()));
1108
+ }
1109
+ sink.dispatch(pointerMoveEvent(s, world(s)));
1110
+ },
1111
+ release(s) {
1112
+ hold.cancel(s.pointerId);
1113
+ const before = held.size;
1114
+ const rec = held.get(s.pointerId);
1115
+ held.delete(s.pointerId);
1116
+ if (before >= 2) sink.dispatch(multiTouchEvent(before, s, { end: "commit" }));
1117
+ if (before >= 2 && held.size < 2) {
1118
+ const fingers = touches.finish(
1119
+ geometry([{ x: s.clientX, y: s.clientY }]).centroid,
1120
+ clock.now(),
1121
+ longPressMs()
1122
+ );
1123
+ if (fingers !== null) sink.dispatch({ kind: "multitouchtap", fingers, ...modifiersOf(s) });
1124
+ }
1125
+ sink.dispatch(pointerUpEvent(s, world(s)));
1126
+ if (!rec?.buffered) return {};
1127
+ const at2 = world(s);
1128
+ const up = {
1129
+ target: s.target,
1130
+ clientX: s.clientX,
1131
+ clientY: s.clientY,
1132
+ worldX: at2.x,
1133
+ worldY: at2.y,
1134
+ ...modifiersOf(s)
1135
+ };
1136
+ return composeClick(rec.down, up, clicks, () => clock.now(), (ev) => sink.dispatch(ev), observer);
1137
+ },
1138
+ cancel(pointerId) {
1139
+ const rec = held.get(pointerId);
1140
+ if (!rec) return;
1141
+ const before = held.size;
1142
+ held.delete(pointerId);
1143
+ hold.cancel(pointerId);
1144
+ if (before >= 2) sink.dispatch(multiTouchEvent(before, rec.down, { end: "cancel" }));
1145
+ touches.abandon();
1146
+ sink.dispatch({ kind: "pointercancel", pointerId, ...modifiersOf(rec.down) });
1147
+ },
1148
+ takeHold() {
1149
+ let holding = false;
1150
+ for (const [id, rec] of held) {
1151
+ if (!rec.holdType) continue;
1152
+ holding = true;
1153
+ if (!hold.pendingFor(id)) continue;
1154
+ hold.cancel(id);
1155
+ fireLongPress(rec, sink, observer);
1156
+ }
1157
+ return holding;
1158
+ },
1159
+ heldCount: () => held.size,
1160
+ positionOf: (pointerId) => held.get(pointerId)?.pos,
1161
+ reset() {
1162
+ hold.cancel();
1163
+ held.clear();
1164
+ touches.abandon();
1165
+ }
1166
+ };
1167
+ }
1168
+
813
1169
  // src/ui/controlTarget.ts
814
1170
  var CONTROL_SELECTOR = [
815
1171
  "button",
@@ -831,6 +1187,106 @@ function isInControlWithin(target, host) {
831
1187
  return control !== null && control !== host && host.contains(control);
832
1188
  }
833
1189
 
834
- 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, bodyPredicate, canonicalModifiers, collapseShiftPairs, describeRoute, describeRouteParts, formatKeyRoute, formatPhaseAtom, formatRoute, getGestureDescriptor, hitPredicate, isApplePlatform, isInControlWithin, isKnownGestureName, keyRouteToSpec, matchIngestTypes, matchKey, matchModifiers, matchPhase, matchSpec, matchTarget, mimeMatchesGlob, modifierComboToParsed, mods, parseKeyRoute, parseRoute, parseTargetSpec, pastDragThreshold, routeGestureForSpecKind, routeToSpec, specKindForRouteGesture, specificity };
1190
+ // src/ui/heldPointers.ts
1191
+ var trackers = /* @__PURE__ */ new WeakMap();
1192
+ function attach(element) {
1193
+ const doc = element.ownerDocument;
1194
+ const win = doc.defaultView;
1195
+ const held = /* @__PURE__ */ new Map();
1196
+ const down = (e) => {
1197
+ const p = e;
1198
+ held.set(p.pointerId, !!p.buttons);
1199
+ };
1200
+ const up = (e) => {
1201
+ held.delete(e.pointerId);
1202
+ };
1203
+ const move = (e) => {
1204
+ const p = e;
1205
+ if (held.get(p.pointerId) && p.buttons === 0) held.delete(p.pointerId);
1206
+ };
1207
+ const blur = () => {
1208
+ held.clear();
1209
+ };
1210
+ element.addEventListener("pointerdown", down, true);
1211
+ doc.addEventListener("pointerup", up, true);
1212
+ doc.addEventListener("pointercancel", up, true);
1213
+ doc.addEventListener("pointermove", move, true);
1214
+ win?.addEventListener("blur", blur);
1215
+ return {
1216
+ readers: 0,
1217
+ held,
1218
+ detach() {
1219
+ element.removeEventListener("pointerdown", down, true);
1220
+ doc.removeEventListener("pointerup", up, true);
1221
+ doc.removeEventListener("pointercancel", up, true);
1222
+ doc.removeEventListener("pointermove", move, true);
1223
+ win?.removeEventListener("blur", blur);
1224
+ }
1225
+ };
1226
+ }
1227
+ function heldPointers(element) {
1228
+ let tracker = trackers.get(element);
1229
+ if (!tracker) {
1230
+ tracker = attach(element);
1231
+ trackers.set(element, tracker);
1232
+ }
1233
+ const shared = tracker;
1234
+ shared.readers += 1;
1235
+ let released = false;
1236
+ return {
1237
+ count: () => shared.held.size,
1238
+ release() {
1239
+ if (released) return;
1240
+ released = true;
1241
+ shared.readers -= 1;
1242
+ if (shared.readers > 0) return;
1243
+ shared.detach();
1244
+ if (trackers.get(element) === shared) trackers.delete(element);
1245
+ }
1246
+ };
1247
+ }
1248
+
1249
+ // src/ui/keyOwner.ts
1250
+ function isEditableTarget(target) {
1251
+ if (!target || !(target instanceof Element)) return false;
1252
+ const tag = target.tagName;
1253
+ if (tag === "INPUT" || tag === "TEXTAREA") return true;
1254
+ if (target.isContentEditable) return true;
1255
+ const ce = target.getAttribute("contenteditable");
1256
+ return ce === "true" || ce === "";
1257
+ }
1258
+ var ACTIVATED_BY_KEY = [
1259
+ "button",
1260
+ "a[href]",
1261
+ "summary",
1262
+ "select",
1263
+ 'input[type="button"]',
1264
+ 'input[type="submit"]',
1265
+ 'input[type="reset"]',
1266
+ 'input[type="checkbox"]',
1267
+ 'input[type="radio"]',
1268
+ '[role="button"]',
1269
+ '[role="link"]',
1270
+ '[role="checkbox"]',
1271
+ '[role="switch"]',
1272
+ '[role="menuitem"]',
1273
+ '[role="tab"]',
1274
+ '[role="option"]'
1275
+ ].join(", ");
1276
+ function activatesFocusedControl(e) {
1277
+ if (e.key !== " " && e.key !== "Enter") return false;
1278
+ return e.target instanceof Element && e.target.matches(ACTIVATED_BY_KEY);
1279
+ }
1280
+ var SELECT_NAVIGATION = /* @__PURE__ */ new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
1281
+ function selectOwnsKey(e) {
1282
+ if (!(e.target instanceof Element) || e.target.tagName !== "SELECT") return false;
1283
+ if (e.metaKey || e.ctrlKey || e.altKey) return false;
1284
+ return SELECT_NAVIGATION.has(e.key) || e.key.length === 1;
1285
+ }
1286
+ function focusedControlOwnsKey(e) {
1287
+ return activatesFocusedControl(e) || selectOwnsKey(e);
1288
+ }
1289
+
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 };
835
1291
  //# sourceMappingURL=index.js.map
836
1292
  //# sourceMappingURL=index.js.map