@vibecook/ice 0.15.0-desk.5 → 0.15.0-desk.6

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.
Files changed (38) hide show
  1. package/dist/{chunk-OHEHS2EM.js → chunk-7XSZRL74.js} +2 -2
  2. package/dist/{chunk-HSIFKQNP.js → chunk-CAJ64OD6.js} +210 -35
  3. package/dist/chunk-CAJ64OD6.js.map +1 -0
  4. package/dist/{chunk-5QVYKU64.js → chunk-G3LDSWL4.js} +64 -7
  5. package/dist/chunk-G3LDSWL4.js.map +1 -0
  6. package/dist/{chunk-ZHASMOQV.js → chunk-WWFLFPSG.js} +43 -3
  7. package/dist/chunk-WWFLFPSG.js.map +1 -0
  8. package/dist/desk-kit.js +2 -2
  9. package/dist/desk-objects.js +3 -3
  10. package/dist/desk.js +3 -3
  11. package/dist/devtools.js +1 -1
  12. package/dist/dom.js +2 -2
  13. package/dist/index.js +11 -1
  14. package/dist/react.js +2 -2
  15. package/dist/types/packages/core/src/catalog/desk.d.ts +19 -1
  16. package/dist/types/packages/core/src/catalog/pointer.d.ts +7 -0
  17. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +11 -1
  18. package/dist/types/packages/core/src/engine/frame-control.d.ts +14 -0
  19. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +13 -0
  20. package/dist/types/packages/core/src/index.d.ts +2 -2
  21. package/dist/types/packages/core/src/input/queue.d.ts +5 -1
  22. package/dist/types/packages/core/src/ops/tray.d.ts +20 -1
  23. package/dist/types/packages/core/src/presence/index.d.ts +2 -2
  24. package/dist/types/packages/core/src/presence/presence-kit.d.ts +14 -0
  25. package/dist/types/packages/core/src/presence/publish.d.ts +7 -0
  26. package/dist/types/packages/core/src/presence/remote-cursors.d.ts +17 -2
  27. package/dist/types/packages/core/src/systems/tray.d.ts +3 -0
  28. package/dist/types/packages/desk/src/ground.d.ts +11 -3
  29. package/dist/types/packages/desk/src/host/layer.d.ts +49 -3
  30. package/dist/types/packages/desk/src/marks/layout.d.ts +8 -0
  31. package/dist/types/packages/desk/src/still.d.ts +13 -1
  32. package/dist/types/packages/dom/src/desk-host.d.ts +5 -0
  33. package/dist/types/packages/dom/src/pointer-adapter.d.ts +12 -2
  34. package/package.json +1 -1
  35. package/dist/chunk-5QVYKU64.js.map +0 -1
  36. package/dist/chunk-HSIFKQNP.js.map +0 -1
  37. package/dist/chunk-ZHASMOQV.js.map +0 -1
  38. /package/dist/{chunk-OHEHS2EM.js.map → chunk-7XSZRL74.js.map} +0 -0
@@ -12,7 +12,7 @@ import {
12
12
  defineQuery,
13
13
  field,
14
14
  init
15
- } from "./chunk-HSIFKQNP.js";
15
+ } from "./chunk-CAJ64OD6.js";
16
16
 
17
17
  // ../desk/src/lattice/lod.ts
18
18
  var BASE_SPACING = 20;
@@ -2399,4 +2399,4 @@ export {
2399
2399
  writeChanged,
2400
2400
  lineWeight
2401
2401
  };
2402
- //# sourceMappingURL=chunk-OHEHS2EM.js.map
2402
+ //# sourceMappingURL=chunk-7XSZRL74.js.map
@@ -571,6 +571,9 @@ function createFrameControl(world) {
571
571
  const listeners = /* @__PURE__ */ new Set();
572
572
  let settleLeft = 0;
573
573
  let parked = false;
574
+ const parkListeners = /* @__PURE__ */ new Set();
575
+ let heardParked = false;
576
+ let parkOwed = false;
574
577
  const sources = /* @__PURE__ */ new Map();
575
578
  const wakeListeners = /* @__PURE__ */ new Set();
576
579
  const wakeCounts = {};
@@ -593,6 +596,41 @@ function createFrameControl(world) {
593
596
  for (const r of reporters.values()) if (r.busy()) names.push(r.name);
594
597
  return names;
595
598
  };
599
+ const announcePark = (on) => {
600
+ if (heardParked === on) return;
601
+ heardParked = on;
602
+ for (const fn of [...parkListeners]) {
603
+ if (heardParked !== on) return;
604
+ try {
605
+ fn(on);
606
+ } catch (err) {
607
+ console.error("[ice] frame.onParked \u2014 a listener threw; the gate stands", err);
608
+ }
609
+ }
610
+ };
611
+ const claim = () => {
612
+ if (freezes.size === 0) return true;
613
+ if (!parked && settleLeft <= 0) {
614
+ parked = true;
615
+ if (devGuardsEnabled()) {
616
+ console.warn(
617
+ `ice: frame.freeze \u2014 settle cap (${SETTLE_CAP} frames) reached with reporters still busy: ${busyNames().join(", ")}. Parking on a possibly unsettled frame.`
618
+ );
619
+ }
620
+ }
621
+ if (parked) {
622
+ if (heardParked) return false;
623
+ parkOwed = false;
624
+ announcePark(true);
625
+ return parked ? false : claim();
626
+ }
627
+ settleLeft -= 1;
628
+ if (busyNames().length === 0) {
629
+ parked = true;
630
+ parkOwed = true;
631
+ }
632
+ return true;
633
+ };
596
634
  return {
597
635
  freeze(name) {
598
636
  const token = Symbol(name);
@@ -609,12 +647,15 @@ function createFrameControl(world) {
609
647
  if (released) return;
610
648
  released = true;
611
649
  freezes.delete(token);
612
- if (freezes.size === 0) {
650
+ const open = freezes.size === 0;
651
+ if (open) {
613
652
  settleLeft = 0;
614
653
  parked = false;
654
+ parkOwed = false;
615
655
  }
616
656
  sync();
617
657
  announce();
658
+ if (open) announcePark(false);
618
659
  };
619
660
  },
620
661
  isFrozen: () => freezes.size > 0,
@@ -631,28 +672,19 @@ function createFrameControl(world) {
631
672
  };
632
673
  },
633
674
  settling: () => busyNames(),
634
- claimStep() {
635
- if (freezes.size === 0) return true;
636
- if (parked) return false;
637
- if (settleLeft <= 0) {
638
- parked = true;
639
- if (devGuardsEnabled()) {
640
- console.warn(
641
- `ice: frame.freeze \u2014 settle cap (${SETTLE_CAP} frames) reached with reporters still busy: ${busyNames().join(", ")}. Parking on a possibly unsettled frame.`
642
- );
643
- }
644
- return false;
645
- }
646
- settleLeft -= 1;
647
- if (busyNames().length === 0) parked = true;
648
- return true;
649
- },
675
+ claimStep: claim,
650
676
  onChange(fn) {
651
677
  listeners.add(fn);
652
678
  return () => {
653
679
  listeners.delete(fn);
654
680
  };
655
681
  },
682
+ onParked(fn) {
683
+ parkListeners.add(fn);
684
+ return () => {
685
+ parkListeners.delete(fn);
686
+ };
687
+ },
656
688
  wake(reason) {
657
689
  if (inStep) return;
658
690
  wakeCounts[reason] = (wakeCounts[reason] ?? 0) + 1;
@@ -722,6 +754,10 @@ function createFrameControl(world) {
722
754
  },
723
755
  leaveStep() {
724
756
  inStep = false;
757
+ if (parkOwed) {
758
+ parkOwed = false;
759
+ announcePark(true);
760
+ }
725
761
  },
726
762
  stepping: () => inStep,
727
763
  sleepStats: () => ({ asleep, sleeps, wakes: { ...wakeCounts }, timed, steps })
@@ -1371,7 +1407,14 @@ var Tray = defineComponent("Tray", {
1371
1407
  * `setTrayCategory` (the bar's chips, the desk handle's door) and `trayLay` — which lays only that category's entries, zeroes the
1372
1408
  * scroll and the band when it moves, and falls back to all ("") when the frame hangs none of it.
1373
1409
  */
1374
- category: field3("string", { default: "" })
1410
+ category: field3("string", { default: "" }),
1411
+ /**
1412
+ * Petition I37 — THE KEYBOARD FOCUS on the board: the type of the specimen it is on ("" — none), a fact of the view like `hover`.
1413
+ * Its writer is the op `focusTray` (next/prev/first/last along the lay's order, or a type by its id); `trayLay` scrolls the
1414
+ * specimen it moves to into the board's face, and lets it go when what it was on is laid no more (a category, a frame). The
1415
+ * renderer draws no ring for it: a host does, from the desk handle's `TrayAnchor.focused`.
1416
+ */
1417
+ focus: field3("string", { default: "" })
1375
1418
  });
1376
1419
  var TrayIntent = defineResource(
1377
1420
  "TrayIntent",
@@ -1394,7 +1437,8 @@ var TrayContent = defineComponent("TrayContent", {
1394
1437
  width: field3("f64", { default: 0 }),
1395
1438
  bottom: field3("f64", { default: 0 }),
1396
1439
  laid: field3("u32", { default: 0 }),
1397
- present: field3("string", { default: "[]" })
1440
+ present: field3("string", { default: "[]" }),
1441
+ order: field3("string", { default: "[]" })
1398
1442
  });
1399
1443
  var Specimen = defineTag("Specimen");
1400
1444
  var TrayPress = defineComponent("TrayPress", {
@@ -1646,6 +1690,7 @@ var PointerPart = defineComponent("PointerPart", { part: field7("string", { defa
1646
1690
  var PartTap = defineResource("PartTap", { seq: "u32", target: "eid", part: "string" });
1647
1691
  var WheelHandled = defineTag("WheelHandled");
1648
1692
  var OverInteractive = defineTag("OverInteractive");
1693
+ var PointerOutside = defineTag("PointerOutside");
1649
1694
  var LocalPointer = defineTag("LocalPointer");
1650
1695
  var PointerWorld = defineComponent("PointerWorld", {
1651
1696
  x: field7("f64", { default: 0 }),
@@ -1691,7 +1736,9 @@ var PresenceInfo = defineComponent("PresenceInfo", { name: "string", color: "str
1691
1736
  var PresenceCursor = defineComponent("PresenceCursor", {
1692
1737
  x: field9("f64", { default: 0 }),
1693
1738
  y: field9("f64", { default: 0 }),
1694
- device: enumOf6(["mouse", "touch", "pen"])
1739
+ device: enumOf6(["mouse", "touch", "pen"]),
1740
+ away: field9("bool", { default: false }),
1741
+ canvas: field9("string", { default: "" })
1695
1742
  });
1696
1743
  var SelectionSummary = defineComponent("SelectionSummary", {
1697
1744
  count: field9("u16", { default: 0 }),
@@ -8953,6 +9000,8 @@ function createTrayLay(world, opts) {
8953
9000
  let rangeKey = "";
8954
9001
  let laidTray;
8955
9002
  let laidCategory = "";
9003
+ let focusedFor = "";
9004
+ let laidTypes = [];
8956
9005
  const clampToRange = (ctx, tray, frame2, bottom) => {
8957
9006
  if (frame2.face === void 0) return;
8958
9007
  const range = trayScrollMax(bottom, frame2.face - (frame2.foot ?? 0), frame2.pitch);
@@ -8963,6 +9012,34 @@ function createTrayLay(world, opts) {
8963
9012
  rangeKey = key;
8964
9013
  if (t.scroll > range) ctx.edit(tray).set(Tray, { ...t, scroll: range });
8965
9014
  };
9015
+ const focusInView = (ctx, tray, frame2, bottom) => {
9016
+ const t = ctx.read(tray, Tray);
9017
+ const focus = t.focus ?? "";
9018
+ if (focus !== "" && !laidTypes.includes(focus)) {
9019
+ focusedFor = "";
9020
+ ctx.edit(tray).set(Tray, { ...t, focus: "" });
9021
+ return;
9022
+ }
9023
+ if (focus === focusedFor) return;
9024
+ if (focus === "") {
9025
+ focusedFor = "";
9026
+ return;
9027
+ }
9028
+ const hung = specimensOf(world, tray).find((e) => world.get(e, PrefabId)?.id === focus);
9029
+ if (hung === void 0) return;
9030
+ const at = ctx.get(hung, Position);
9031
+ const size = ctx.get(hung, Size);
9032
+ if (at === void 0 || size === void 0) return;
9033
+ focusedFor = focus;
9034
+ if (frame2.face === void 0) return;
9035
+ const top = frame2.head ?? 0;
9036
+ const showing = frame2.face - (frame2.foot ?? 0) - top;
9037
+ let scroll = t.scroll;
9038
+ if (at.y < scroll + top || size.h > showing) scroll = at.y - top;
9039
+ else if (at.y + size.h > scroll + top + showing) scroll = at.y + size.h - top - showing;
9040
+ scroll = Math.min(Math.max(scroll, 0), trayScrollMax(bottom, frame2.face - (frame2.foot ?? 0), frame2.pitch));
9041
+ if (scroll !== t.scroll || t.stretch !== 0) ctx.edit(tray).set(Tray, { ...t, scroll, stretch: 0 });
9042
+ };
8966
9043
  return defineTickSystem9(
8967
9044
  (ctx) => {
8968
9045
  const tray = world.firstOf(trayQ2);
@@ -8985,6 +9062,7 @@ function createTrayLay(world, opts) {
8985
9062
  const key = `${tray}|${content?.laid ?? 0}|${frame2.w}|${frame2.pitch}|${types.map((q) => q.type).join(",")}|${category}`;
8986
9063
  if (key === laidKey) {
8987
9064
  clampToRange(ctx, tray, frame2, content?.bottom ?? 0);
9065
+ focusInView(ctx, tray, frame2, content?.bottom ?? 0);
8988
9066
  return;
8989
9067
  }
8990
9068
  const shown = category === "" ? types : types.filter((q) => categoryOf(q) === category);
@@ -9014,13 +9092,15 @@ function createTrayLay(world, opts) {
9014
9092
  ctx.setRelation(spawned, ChildOf, tray, "last");
9015
9093
  }
9016
9094
  for (const e of have.values()) ctx.destroy(e);
9017
- const next2 = { width: frame2.w, bottom: layout.bottom, laid: (content?.laid ?? 0) + 1, present: JSON.stringify(presentOf(types)) };
9095
+ const next2 = { width: frame2.w, bottom: layout.bottom, laid: (content?.laid ?? 0) + 1, present: JSON.stringify(presentOf(types)), order: JSON.stringify(layout.placed.map((q) => q.type)) };
9018
9096
  if (content === void 0) ctx.addComponent(tray, TrayContent, next2);
9019
9097
  else ctx.edit(tray).set(TrayContent, next2);
9020
9098
  laidKey = `${tray}|${next2.laid}|${frame2.w}|${frame2.pitch}|${types.map((q) => q.type).join(",")}|${category}`;
9021
9099
  laidTray = tray;
9022
9100
  laidCategory = category;
9101
+ laidTypes = layout.placed.map((q) => q.type);
9023
9102
  clampToRange(ctx, tray, frame2, layout.bottom);
9103
+ focusInView(ctx, tray, frame2, layout.bottom);
9024
9104
  },
9025
9105
  // Petition I34: in derive, after `trayInput` (react) by the phase — the lay reads back what the input wrote this tick, so the two
9026
9106
  // `Tray` writers are ORDERED, never attested. `Position`/`Size` it writes on the tray's specimens alone, rows disjoint from the
@@ -9051,7 +9131,7 @@ var write = (world, patch) => {
9051
9131
  const e = ensureTray(world);
9052
9132
  const cur = world.read(e, Tray);
9053
9133
  const next2 = { ...cur, ...patch };
9054
- if (next2.open !== cur.open || next2.scroll !== cur.scroll || next2.stretch !== cur.stretch || next2.hover !== cur.hover || next2.category !== cur.category) world.edit(e).set(Tray, next2);
9134
+ if (next2.open !== cur.open || next2.scroll !== cur.scroll || next2.stretch !== cur.stretch || next2.hover !== cur.hover || next2.category !== cur.category || next2.focus !== cur.focus) world.edit(e).set(Tray, next2);
9055
9135
  };
9056
9136
  function openTray(world) {
9057
9137
  if (heldEntity(world) !== void 0) return false;
@@ -9072,6 +9152,31 @@ function toggleTray(world) {
9072
9152
  function scrollTray(world, scroll) {
9073
9153
  write(world, { scroll, stretch: 0 });
9074
9154
  }
9155
+ function trayHung(world) {
9156
+ const e = world.firstOf(trayQ3);
9157
+ const raw = e === void 0 ? void 0 : world.get(e, TrayContent)?.order;
9158
+ if (raw === void 0 || raw === null || raw === "") return [];
9159
+ const list = JSON.parse(raw);
9160
+ return Array.isArray(list) ? list.filter((q) => typeof q === "string") : [];
9161
+ }
9162
+ function trayFocus(world) {
9163
+ const e = world.firstOf(trayQ3);
9164
+ return e === void 0 ? "" : world.get(e, Tray)?.focus ?? "";
9165
+ }
9166
+ function focusTray(world, to) {
9167
+ const order = trayHung(world);
9168
+ const cur = trayFocus(world);
9169
+ const i = order.indexOf(cur);
9170
+ const last = order.length - 1;
9171
+ let next2;
9172
+ if (to === "first") next2 = order[0] ?? "";
9173
+ else if (to === "last") next2 = order[last] ?? "";
9174
+ else if (to === "next") next2 = order[i < 0 ? 0 : Math.min(i + 1, last)] ?? "";
9175
+ else if (to === "prev") next2 = order[i < 0 ? last : Math.max(i - 1, 0)] ?? "";
9176
+ else next2 = order.includes(to) ? to : cur;
9177
+ write(world, { focus: next2 });
9178
+ return next2;
9179
+ }
9075
9180
  function trayCategory(world) {
9076
9181
  const e = world.firstOf(trayQ3);
9077
9182
  return e === void 0 ? "" : world.get(e, Tray)?.category ?? "";
@@ -9339,6 +9444,7 @@ function createL0Systems(world, queue) {
9339
9444
  wentCancelled: false,
9340
9445
  handled: false,
9341
9446
  overInteractive: void 0,
9447
+ outside: false,
9342
9448
  mods: { shift: ev.mods.shift, ctrl: ev.mods.ctrl, alt: ev.mods.alt, meta: ev.mods.meta },
9343
9449
  wheelDx: 0,
9344
9450
  wheelDy: 0,
@@ -9368,6 +9474,7 @@ function createL0Systems(world, queue) {
9368
9474
  s.hasWheel = true;
9369
9475
  if (ev.wheelHandled === true) s.wheelHandled = true;
9370
9476
  }
9477
+ s.outside = ev.kind === "leave";
9371
9478
  if (ev.surfaceHandled === true) s.handled = true;
9372
9479
  if (ev.overInteractive !== void 0) s.overInteractive = ev.overInteractive;
9373
9480
  }
@@ -9393,6 +9500,7 @@ function createL0Systems(world, queue) {
9393
9500
  if (s.handled) ctx.addTag(spawned, HandledByWidget);
9394
9501
  if (s.wheelHandled) ctx.addTag(spawned, WheelHandled);
9395
9502
  if (s.overInteractive === true) ctx.addTag(spawned, OverInteractive);
9503
+ if (s.outside) ctx.addTag(spawned, PointerOutside);
9396
9504
  byId.set(id, spawned);
9397
9505
  continue;
9398
9506
  }
@@ -9422,6 +9530,10 @@ function createL0Systems(world, queue) {
9422
9530
  if (s.overInteractive) ctx.addTag(existing, OverInteractive);
9423
9531
  else ctx.removeTag(existing, OverInteractive);
9424
9532
  }
9533
+ if (s.outside !== ctx.hasTag(existing, PointerOutside)) {
9534
+ if (s.outside) ctx.addTag(existing, PointerOutside);
9535
+ else ctx.removeTag(existing, PointerOutside);
9536
+ }
9425
9537
  }
9426
9538
  bumpVersion(world, PointerVersion);
9427
9539
  },
@@ -14127,7 +14239,7 @@ function attachPresence(world, opts) {
14127
14239
  const source = new LoroEphemeralSnapshot(loro);
14128
14240
  const onRemote = opts.onRemote;
14129
14241
  const stopRemote = onRemote === void 0 ? void 0 : loro.subscribe((ev) => {
14130
- if (ev.by !== "local") onRemote();
14242
+ if (ev.by !== "local" && (ev.added.length > 0 || ev.updated.length > 0 || ev.removed.length > 0)) onRemote();
14131
14243
  });
14132
14244
  const outbound = /* @__PURE__ */ new Set();
14133
14245
  const eph = createEphemeralStore(source, {
@@ -14150,6 +14262,8 @@ function attachPresence(world, opts) {
14150
14262
  });
14151
14263
  let localPeer = spawnLocalPeer();
14152
14264
  let detached = false;
14265
+ let awayReason = null;
14266
+ const awayHeard = /* @__PURE__ */ new Set();
14153
14267
  let healQueued = false;
14154
14268
  const stopResetHeal = world.observe({
14155
14269
  onReset: () => {
@@ -14184,6 +14298,19 @@ function attachPresence(world, opts) {
14184
14298
  outbound.delete(fn);
14185
14299
  };
14186
14300
  },
14301
+ setAway(reason) {
14302
+ if (reason !== null && typeof reason !== "string") throw new Error(`ice: presence setAway \u2014 the reason is a string or null (got ${String(reason)})`);
14303
+ if (detached || reason === awayReason) return;
14304
+ awayReason = reason;
14305
+ for (const fn of [...awayHeard]) fn(reason);
14306
+ },
14307
+ away: () => awayReason,
14308
+ onAway(fn) {
14309
+ awayHeard.add(fn);
14310
+ return () => {
14311
+ awayHeard.delete(fn);
14312
+ };
14313
+ },
14187
14314
  detach() {
14188
14315
  if (detached) return;
14189
14316
  detached = true;
@@ -14195,6 +14322,7 @@ function attachPresence(world, opts) {
14195
14322
  }
14196
14323
  attachment.detach();
14197
14324
  outbound.clear();
14325
+ awayHeard.clear();
14198
14326
  stopRemote?.();
14199
14327
  loro.destroy();
14200
14328
  }
@@ -14206,6 +14334,11 @@ import { defineQuery as defineQuery47 } from "@vibecook/strata-ecs";
14206
14334
  var SELECTION_KEY_CAP = 32;
14207
14335
  var COUNT_MAX = 65535;
14208
14336
  var localMouseQ = defineQuery47([Pointer, LocalPointer, PointerWorld]);
14337
+ function presenceCanvasOf(world, peerId, keyOf) {
14338
+ const frame2 = currentNavFrame(world);
14339
+ if (frame2 === void 0 || frame2 === world.getResource(BoardRoot)?.root) return "";
14340
+ return keyOf?.(frame2) ?? `~${peerId}/${frame2}`;
14341
+ }
14209
14342
  function createPresencePublish(world, session, opts = {}) {
14210
14343
  const { eph } = session;
14211
14344
  const keyOf = opts.keyOf;
@@ -14227,18 +14360,26 @@ function createPresencePublish(world, session, opts = {}) {
14227
14360
  publishSelection(world);
14228
14361
  };
14229
14362
  function publishCursor(w) {
14230
- let cursor;
14363
+ let mouse;
14231
14364
  w.query(localMouseQ).each((b) => {
14232
14365
  for (const r of b) {
14233
- if (cursor !== void 0) break;
14366
+ if (mouse !== void 0) break;
14234
14367
  const e = b.entity(r);
14235
14368
  if (w.read(e, Pointer).device !== "mouse") continue;
14236
14369
  const pw = w.read(e, PointerWorld);
14237
- cursor = { x: pw.x, y: pw.y, device: "mouse" };
14370
+ mouse = { x: pw.x, y: pw.y, outside: w.hasTag(e, PointerOutside) };
14238
14371
  }
14239
14372
  });
14240
- if (cursor === void 0) return;
14241
- if (cursorPresent && lastCursor !== void 0 && lastCursor.x === cursor.x && lastCursor.y === cursor.y && lastCursor.device === cursor.device) {
14373
+ const at = mouse ?? (cursorPresent ? lastCursor : void 0);
14374
+ if (at === void 0) return;
14375
+ const cursor = {
14376
+ x: at.x,
14377
+ y: at.y,
14378
+ device: "mouse",
14379
+ away: mouse?.outside === true || session.away() !== null,
14380
+ canvas: presenceCanvasOf(w, session.peerId, keyOf)
14381
+ };
14382
+ if (cursorPresent && lastCursor !== void 0 && cursorEquals(lastCursor, cursor)) {
14242
14383
  return;
14243
14384
  }
14244
14385
  if (cursorPresent) eph.edit(localPeer).set(PresenceCursor, cursor);
@@ -14293,6 +14434,9 @@ function createPresencePublish(world, session, opts = {}) {
14293
14434
  lastSummary = summary;
14294
14435
  }
14295
14436
  }
14437
+ function cursorEquals(a, b) {
14438
+ return a.x === b.x && a.y === b.y && a.device === b.device && a.away === b.away && a.canvas === b.canvas;
14439
+ }
14296
14440
  function summaryEquals(a, b) {
14297
14441
  return a.count === b.count && a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h && a.keys === b.keys;
14298
14442
  }
@@ -14301,16 +14445,18 @@ function summaryEquals(a, b) {
14301
14445
  import { Not as Not8, defineQuery as defineQuery48, defineTickSystem as defineTickSystem17 } from "@vibecook/strata-ecs";
14302
14446
  import { Local as Local2 } from "@vibecook/strata-ecs";
14303
14447
  var remotePeerCursorsQ = defineQuery48([PresenceCursor, PresencePeer, Not8(Local2)]);
14304
- function createRemoteCursorsRig(world) {
14448
+ function createRemoteCursorsRig(world, opts = {}) {
14305
14449
  const pool = /* @__PURE__ */ new Map();
14306
14450
  const system = defineTickSystem17(
14307
14451
  (ctx) => {
14308
14452
  const live = /* @__PURE__ */ new Set();
14453
+ const here = presenceCanvasOf(world, opts.peerId ?? "", opts.keyOf);
14309
14454
  ctx.query(remotePeerCursorsQ).each((b) => {
14310
14455
  for (const r of b) {
14311
14456
  const peer = b.entity(r);
14312
- live.add(peer);
14313
14457
  const pc = ctx.read(peer, PresenceCursor);
14458
+ if (pc.away || (pc.canvas ?? "") !== here) continue;
14459
+ live.add(peer);
14314
14460
  const entry = pool.get(peer);
14315
14461
  if (entry === void 0) {
14316
14462
  const cursor = ctx.spawn({
@@ -14358,14 +14504,23 @@ function createRemoteCursorsRig(world) {
14358
14504
  }
14359
14505
  };
14360
14506
  }
14361
- function createRemoteCursorsSystem(world) {
14362
- return createRemoteCursorsRig(world).system;
14507
+ function createRemoteCursorsSystem(world, opts = {}) {
14508
+ return createRemoteCursorsRig(world, opts).system;
14363
14509
  }
14364
14510
  function installPresence(engine, session, opts = {}) {
14365
- const removePublish = engine.onPublish(createPresencePublish(engine.world, session, opts));
14366
- const rig = createRemoteCursorsRig(engine.world);
14511
+ const publish = createPresencePublish(engine.world, session, opts);
14512
+ const removePublish = engine.onPublish(publish);
14513
+ let installed = true;
14514
+ const stopAway = session.onAway(() => {
14515
+ queueMicrotask(() => {
14516
+ if (installed && !engine.frame.stepping()) publish(engine.world);
14517
+ });
14518
+ });
14519
+ const rig = createRemoteCursorsRig(engine.world, { peerId: session.peerId, ...opts.keyOf !== void 0 ? { keyOf: opts.keyOf } : {} });
14367
14520
  const removeSystem = engine.addSystems("present", rig.system);
14368
14521
  return () => {
14522
+ installed = false;
14523
+ stopAway();
14369
14524
  removePublish();
14370
14525
  removeSystem();
14371
14526
  rig.reap();
@@ -16260,6 +16415,21 @@ function createCanvasEngine(opts = {}) {
16260
16415
  }
16261
16416
  return spawned;
16262
16417
  },
16418
+ layFromTray(type, at) {
16419
+ if (typeof at?.x !== "number" || typeof at.y !== "number" || !Number.isFinite(at.x) || !Number.isFinite(at.y)) {
16420
+ throw new Error(`ice: ops.layFromTray \u2014 the point is ${JSON.stringify(at) ?? String(at)}: { x, y }, a world point of the current frame, two finite numbers.`);
16421
+ }
16422
+ if (heldNow() !== void 0 || !trayHung(world).includes(type)) return void 0;
16423
+ const widget = catalog.widget(type);
16424
+ const entry = widget?.tray;
16425
+ if (widget === void 0 || entry === void 0) return void 0;
16426
+ const { w, h } = widget.defaultSize;
16427
+ const props = trayTakeProps(entry);
16428
+ const laid = ops.spawnWidget(type, { x: at.x - w / 2, y: at.y - h / 2, w, h, ...props !== void 0 ? { props } : {} });
16429
+ ops.setSelection([laid]);
16430
+ closeTray(world);
16431
+ return laid;
16432
+ },
16263
16433
  insertByDrag(type, o) {
16264
16434
  requireWritable("insertByDrag");
16265
16435
  const targetFrame = requireCurrentFrame("insertByDrag");
@@ -16825,6 +16995,7 @@ export {
16825
16995
  PartTap,
16826
16996
  WheelHandled,
16827
16997
  OverInteractive,
16998
+ PointerOutside,
16828
16999
  LocalPointer,
16829
17000
  PointerWorld,
16830
17001
  Keyboard,
@@ -16987,6 +17158,9 @@ export {
16987
17158
  closeTray,
16988
17159
  toggleTray,
16989
17160
  scrollTray,
17161
+ trayHung,
17162
+ trayFocus,
17163
+ focusTray,
16990
17164
  trayCategory,
16991
17165
  setTrayCategory,
16992
17166
  trayCategories,
@@ -17036,6 +17210,7 @@ export {
17036
17210
  attachBroadcastRelay,
17037
17211
  DEFAULT_PRESENCE_TTL_MS,
17038
17212
  attachPresence,
17213
+ presenceCanvasOf,
17039
17214
  createPresencePublish,
17040
17215
  createRemoteCursorsSystem,
17041
17216
  installPresence,
@@ -17060,4 +17235,4 @@ export {
17060
17235
  Local3 as Local,
17061
17236
  DurableUndoStatus
17062
17237
  };
17063
- //# sourceMappingURL=chunk-HSIFKQNP.js.map
17238
+ //# sourceMappingURL=chunk-CAJ64OD6.js.map