@vibecook/ice 0.15.0-desk.4 → 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 (46) hide show
  1. package/dist/{chunk-M7E6E7K5.js → chunk-7XSZRL74.js} +2 -2
  2. package/dist/{chunk-PFJN3XN2.js → chunk-CAJ64OD6.js} +231 -43
  3. package/dist/chunk-CAJ64OD6.js.map +1 -0
  4. package/dist/{chunk-E5ADZNMK.js → chunk-G3LDSWL4.js} +71 -12
  5. package/dist/chunk-G3LDSWL4.js.map +1 -0
  6. package/dist/{chunk-JS6PG6YD.js → chunk-RE4GDHFJ.js} +13 -1
  7. package/dist/chunk-RE4GDHFJ.js.map +1 -0
  8. package/dist/{chunk-I53CLMYG.js → chunk-WWFLFPSG.js} +48 -9
  9. package/dist/chunk-WWFLFPSG.js.map +1 -0
  10. package/dist/desk-kit.js +3 -3
  11. package/dist/desk-objects.js +7 -7
  12. package/dist/desk-objects.js.map +1 -1
  13. package/dist/desk.js +4 -4
  14. package/dist/devtools.js +2 -2
  15. package/dist/dom.js +3 -3
  16. package/dist/index.js +12 -2
  17. package/dist/kernel.js +5 -1
  18. package/dist/react.js +3 -3
  19. package/dist/types/packages/core/src/catalog/desk.d.ts +19 -1
  20. package/dist/types/packages/core/src/catalog/pointer.d.ts +7 -0
  21. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +11 -1
  22. package/dist/types/packages/core/src/engine/frame-control.d.ts +14 -0
  23. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +13 -0
  24. package/dist/types/packages/core/src/index.d.ts +2 -2
  25. package/dist/types/packages/core/src/input/queue.d.ts +5 -1
  26. package/dist/types/packages/core/src/ops/tray.d.ts +20 -1
  27. package/dist/types/packages/core/src/presence/index.d.ts +2 -2
  28. package/dist/types/packages/core/src/presence/presence-kit.d.ts +14 -0
  29. package/dist/types/packages/core/src/presence/publish.d.ts +7 -0
  30. package/dist/types/packages/core/src/presence/remote-cursors.d.ts +17 -2
  31. package/dist/types/packages/core/src/systems/tray.d.ts +13 -1
  32. package/dist/types/packages/desk/src/compose/index.d.ts +2 -1
  33. package/dist/types/packages/desk/src/compose/reflector.d.ts +6 -0
  34. package/dist/types/packages/desk/src/ground.d.ts +11 -3
  35. package/dist/types/packages/desk/src/host/layer.d.ts +49 -3
  36. package/dist/types/packages/desk/src/marks/layout.d.ts +8 -0
  37. package/dist/types/packages/desk/src/still.d.ts +13 -1
  38. package/dist/types/packages/dom/src/desk-host.d.ts +5 -0
  39. package/dist/types/packages/dom/src/pointer-adapter.d.ts +18 -3
  40. package/dist/types/packages/kernel/src/coords.d.ts +32 -1
  41. package/package.json +1 -1
  42. package/dist/chunk-E5ADZNMK.js.map +0 -1
  43. package/dist/chunk-I53CLMYG.js.map +0 -1
  44. package/dist/chunk-JS6PG6YD.js.map +0 -1
  45. package/dist/chunk-PFJN3XN2.js.map +0 -1
  46. /package/dist/{chunk-M7E6E7K5.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-PFJN3XN2.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-M7E6E7K5.js.map
2402
+ //# sourceMappingURL=chunk-7XSZRL74.js.map
@@ -27,7 +27,7 @@ import {
27
27
  visibleRect,
28
28
  wireCubic,
29
29
  zoomAtPoint
30
- } from "./chunk-JS6PG6YD.js";
30
+ } from "./chunk-RE4GDHFJ.js";
31
31
 
32
32
  // ../core/src/boot/hmr.ts
33
33
  function defineSchemaOnce(key, build2) {
@@ -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 }),
@@ -8132,7 +8179,9 @@ function createSelectionChromeSystem(world) {
8132
8179
  },
8133
8180
  {
8134
8181
  name: "selectionChrome",
8135
- access: { write: [Position, Size] },
8182
+ // orderIndependent (petition I34): its writes land on its OWN handle pool alone — rows disjoint from every other derive
8183
+ // writer of Position/Size (the tray's lay writes the tray's specimens alone)
8184
+ access: { write: [Position, Size], orderIndependent: [Position, Size] },
8136
8185
  // Run while there is a selection to mirror OR a pool still to reap.
8137
8186
  runIf: () => world.firstOf(selectedQ2) !== void 0 || handleEntities.length > 0
8138
8187
  }
@@ -8951,6 +9000,8 @@ function createTrayLay(world, opts) {
8951
9000
  let rangeKey = "";
8952
9001
  let laidTray;
8953
9002
  let laidCategory = "";
9003
+ let focusedFor = "";
9004
+ let laidTypes = [];
8954
9005
  const clampToRange = (ctx, tray, frame2, bottom) => {
8955
9006
  if (frame2.face === void 0) return;
8956
9007
  const range = trayScrollMax(bottom, frame2.face - (frame2.foot ?? 0), frame2.pitch);
@@ -8961,18 +9012,48 @@ function createTrayLay(world, opts) {
8961
9012
  rangeKey = key;
8962
9013
  if (t.scroll > range) ctx.edit(tray).set(Tray, { ...t, scroll: range });
8963
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
+ };
8964
9043
  return defineTickSystem9(
8965
9044
  (ctx) => {
8966
9045
  const tray = world.firstOf(trayQ2);
8967
9046
  if (tray === void 0 || !ctx.isAlive(tray)) return;
8968
- const frame2 = opts.pose.current?.frame();
8969
- if (frame2 === void 0 || !(frame2.w > 0) || !(frame2.pitch > 0)) return;
9047
+ const source = opts.pose.current;
9048
+ const frame2 = source?.frame();
9049
+ if (source === null || source === void 0 || frame2 === void 0 || !(frame2.w > 0) || !(frame2.pitch > 0)) return;
8970
9050
  const content = ctx.get(tray, TrayContent);
8971
9051
  const policy = opts.placement;
8972
9052
  const inside = policy === void 0 ? void 0 : currentNavFrame(world);
8973
9053
  const root = world.getResource(BoardRoot)?.root;
8974
9054
  const takes = (type) => policy === void 0 ? true : inside !== void 0 ? policy.canIngress(type, inside) : root === void 0 || policy.canPlace === void 0 || policy.canPlace(type, root);
8975
- const types = hungTypes(world).filter((t2) => takes(t2.type));
9055
+ const draws = (type) => source.draws === void 0 || source.draws(type);
9056
+ const types = hungTypes(world).filter((t2) => takes(t2.type) && draws(t2.type));
8976
9057
  const t = ctx.read(tray, Tray);
8977
9058
  const asked = t.category ?? "";
8978
9059
  const category = asked !== "" && !types.some((q) => categoryOf(q) === asked) ? "" : asked;
@@ -8981,6 +9062,7 @@ function createTrayLay(world, opts) {
8981
9062
  const key = `${tray}|${content?.laid ?? 0}|${frame2.w}|${frame2.pitch}|${types.map((q) => q.type).join(",")}|${category}`;
8982
9063
  if (key === laidKey) {
8983
9064
  clampToRange(ctx, tray, frame2, content?.bottom ?? 0);
9065
+ focusInView(ctx, tray, frame2, content?.bottom ?? 0);
8984
9066
  return;
8985
9067
  }
8986
9068
  const shown = category === "" ? types : types.filter((q) => categoryOf(q) === category);
@@ -9010,15 +9092,20 @@ function createTrayLay(world, opts) {
9010
9092
  ctx.setRelation(spawned, ChildOf, tray, "last");
9011
9093
  }
9012
9094
  for (const e of have.values()) ctx.destroy(e);
9013
- 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)) };
9014
9096
  if (content === void 0) ctx.addComponent(tray, TrayContent, next2);
9015
9097
  else ctx.edit(tray).set(TrayContent, next2);
9016
9098
  laidKey = `${tray}|${next2.laid}|${frame2.w}|${frame2.pitch}|${types.map((q) => q.type).join(",")}|${category}`;
9017
9099
  laidTray = tray;
9018
9100
  laidCategory = category;
9101
+ laidTypes = layout.placed.map((q) => q.type);
9019
9102
  clampToRange(ctx, tray, frame2, layout.bottom);
9103
+ focusInView(ctx, tray, frame2, layout.bottom);
9020
9104
  },
9021
- { name: "trayLay", access: { write: [Position, Size, TrayContent, Tray] } }
9105
+ // Petition I34: in derive, after `trayInput` (react) by the phase — the lay reads back what the input wrote this tick, so the two
9106
+ // `Tray` writers are ORDERED, never attested. `Position`/`Size` it writes on the tray's specimens alone, rows disjoint from the
9107
+ // selection chrome's handle pool (its derive co-writer) — that, and only that, is attested.
9108
+ { name: "trayLay", access: { write: [Position, Size, TrayContent, Tray], orderIndependent: [Position, Size] } }
9022
9109
  );
9023
9110
  }
9024
9111
 
@@ -9044,7 +9131,7 @@ var write = (world, patch) => {
9044
9131
  const e = ensureTray(world);
9045
9132
  const cur = world.read(e, Tray);
9046
9133
  const next2 = { ...cur, ...patch };
9047
- 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);
9048
9135
  };
9049
9136
  function openTray(world) {
9050
9137
  if (heldEntity(world) !== void 0) return false;
@@ -9065,6 +9152,31 @@ function toggleTray(world) {
9065
9152
  function scrollTray(world, scroll) {
9066
9153
  write(world, { scroll, stretch: 0 });
9067
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
+ }
9068
9180
  function trayCategory(world) {
9069
9181
  const e = world.firstOf(trayQ3);
9070
9182
  return e === void 0 ? "" : world.get(e, Tray)?.category ?? "";
@@ -9332,6 +9444,7 @@ function createL0Systems(world, queue) {
9332
9444
  wentCancelled: false,
9333
9445
  handled: false,
9334
9446
  overInteractive: void 0,
9447
+ outside: false,
9335
9448
  mods: { shift: ev.mods.shift, ctrl: ev.mods.ctrl, alt: ev.mods.alt, meta: ev.mods.meta },
9336
9449
  wheelDx: 0,
9337
9450
  wheelDy: 0,
@@ -9361,6 +9474,7 @@ function createL0Systems(world, queue) {
9361
9474
  s.hasWheel = true;
9362
9475
  if (ev.wheelHandled === true) s.wheelHandled = true;
9363
9476
  }
9477
+ s.outside = ev.kind === "leave";
9364
9478
  if (ev.surfaceHandled === true) s.handled = true;
9365
9479
  if (ev.overInteractive !== void 0) s.overInteractive = ev.overInteractive;
9366
9480
  }
@@ -9386,6 +9500,7 @@ function createL0Systems(world, queue) {
9386
9500
  if (s.handled) ctx.addTag(spawned, HandledByWidget);
9387
9501
  if (s.wheelHandled) ctx.addTag(spawned, WheelHandled);
9388
9502
  if (s.overInteractive === true) ctx.addTag(spawned, OverInteractive);
9503
+ if (s.outside) ctx.addTag(spawned, PointerOutside);
9389
9504
  byId.set(id, spawned);
9390
9505
  continue;
9391
9506
  }
@@ -9415,6 +9530,10 @@ function createL0Systems(world, queue) {
9415
9530
  if (s.overInteractive) ctx.addTag(existing, OverInteractive);
9416
9531
  else ctx.removeTag(existing, OverInteractive);
9417
9532
  }
9533
+ if (s.outside !== ctx.hasTag(existing, PointerOutside)) {
9534
+ if (s.outside) ctx.addTag(existing, PointerOutside);
9535
+ else ctx.removeTag(existing, PointerOutside);
9536
+ }
9418
9537
  }
9419
9538
  bumpVersion(world, PointerVersion);
9420
9539
  },
@@ -10990,7 +11109,8 @@ function installInteractionStack(engine, opts = {}) {
10990
11109
  // which now narrow-phases wire entries against wireSync's cached cubics.
10991
11110
  // pressWheel beside it (D3t-a): a press holding a `WheelTurns` widget takes its pointer's wheel from both wheel consumers too.
10992
11111
  // trayInput right after the hand's (design-017 §4, K3): the pegboard drawer's wheel, its board and its inert desk, in the same vocabulary
10993
- engine.addSystems("react", createHeldInput(world, { pose: heldPose }), createTrayInput(world, { pose: trayPose }), createTrayLay(world, { pose: trayPose, placement: opts.placement }), createPressWheel(world), pick.spatialSync, wireSync, pick.picking),
11112
+ // (its lay is derived from what it writes — at the head of derive, below)
11113
+ engine.addSystems("react", createHeldInput(world, { pose: heldPose }), createTrayInput(world, { pose: trayPose }), createPressWheel(world), pick.spatialSync, wireSync, pick.picking),
10994
11114
  engine.addSystems("ctl:spawn", l2.cancelSweep, l2.recognizerSpawn, l2.wheelSpawn, l2.recognizerIntegrity),
10995
11115
  engine.addSystems(
10996
11116
  "ctl:recognize",
@@ -11031,7 +11151,12 @@ function installInteractionStack(engine, opts = {}) {
11031
11151
  // in derive; ports must be indexed before chrome/next-frame picking read it),
11032
11152
  // selectionChrome BEFORE cursor (handles spawn at the derive flush so cursor
11033
11153
  // + next frame's spatialSync see them).
11034
- engine.addSystems("derive", ports, createSelectionChromeSystem(world), cursor),
11154
+ // trayLay at the HEAD of derive (petition I34): it reads back the drawer's facts trayInput wrote in react this tick — its clamp
11155
+ // judges this tick's band, a category move starts the board at its top over this tick's wheel — so it is ORDERED after the input
11156
+ // by the phase, never attested (both wrote `Tray` in react, and strata's dev build warned on every engine). Its specimens are
11157
+ // derived state, spawned at the derive flush for the renderer; the selection chrome, the other Position/Size writer here,
11158
+ // writes rows disjoint from them, and both attest it.
11159
+ engine.addSystems("derive", createTrayLay(world, { pose: trayPose, placement: opts.placement }), ports, createSelectionChromeSystem(world), cursor),
11035
11160
  // insertGhostReap AFTER recognizerReap: it reads terminal phase tags on
11036
11161
  // capturing recognizers pre-flush (deferred destroys land at the group
11037
11162
  // boundary), and its own despawns must not race the marker sweep.
@@ -14114,7 +14239,7 @@ function attachPresence(world, opts) {
14114
14239
  const source = new LoroEphemeralSnapshot(loro);
14115
14240
  const onRemote = opts.onRemote;
14116
14241
  const stopRemote = onRemote === void 0 ? void 0 : loro.subscribe((ev) => {
14117
- if (ev.by !== "local") onRemote();
14242
+ if (ev.by !== "local" && (ev.added.length > 0 || ev.updated.length > 0 || ev.removed.length > 0)) onRemote();
14118
14243
  });
14119
14244
  const outbound = /* @__PURE__ */ new Set();
14120
14245
  const eph = createEphemeralStore(source, {
@@ -14137,6 +14262,8 @@ function attachPresence(world, opts) {
14137
14262
  });
14138
14263
  let localPeer = spawnLocalPeer();
14139
14264
  let detached = false;
14265
+ let awayReason = null;
14266
+ const awayHeard = /* @__PURE__ */ new Set();
14140
14267
  let healQueued = false;
14141
14268
  const stopResetHeal = world.observe({
14142
14269
  onReset: () => {
@@ -14171,6 +14298,19 @@ function attachPresence(world, opts) {
14171
14298
  outbound.delete(fn);
14172
14299
  };
14173
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
+ },
14174
14314
  detach() {
14175
14315
  if (detached) return;
14176
14316
  detached = true;
@@ -14182,6 +14322,7 @@ function attachPresence(world, opts) {
14182
14322
  }
14183
14323
  attachment.detach();
14184
14324
  outbound.clear();
14325
+ awayHeard.clear();
14185
14326
  stopRemote?.();
14186
14327
  loro.destroy();
14187
14328
  }
@@ -14193,6 +14334,11 @@ import { defineQuery as defineQuery47 } from "@vibecook/strata-ecs";
14193
14334
  var SELECTION_KEY_CAP = 32;
14194
14335
  var COUNT_MAX = 65535;
14195
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
+ }
14196
14342
  function createPresencePublish(world, session, opts = {}) {
14197
14343
  const { eph } = session;
14198
14344
  const keyOf = opts.keyOf;
@@ -14214,18 +14360,26 @@ function createPresencePublish(world, session, opts = {}) {
14214
14360
  publishSelection(world);
14215
14361
  };
14216
14362
  function publishCursor(w) {
14217
- let cursor;
14363
+ let mouse;
14218
14364
  w.query(localMouseQ).each((b) => {
14219
14365
  for (const r of b) {
14220
- if (cursor !== void 0) break;
14366
+ if (mouse !== void 0) break;
14221
14367
  const e = b.entity(r);
14222
14368
  if (w.read(e, Pointer).device !== "mouse") continue;
14223
14369
  const pw = w.read(e, PointerWorld);
14224
- cursor = { x: pw.x, y: pw.y, device: "mouse" };
14370
+ mouse = { x: pw.x, y: pw.y, outside: w.hasTag(e, PointerOutside) };
14225
14371
  }
14226
14372
  });
14227
- if (cursor === void 0) return;
14228
- 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)) {
14229
14383
  return;
14230
14384
  }
14231
14385
  if (cursorPresent) eph.edit(localPeer).set(PresenceCursor, cursor);
@@ -14280,6 +14434,9 @@ function createPresencePublish(world, session, opts = {}) {
14280
14434
  lastSummary = summary;
14281
14435
  }
14282
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
+ }
14283
14440
  function summaryEquals(a, b) {
14284
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;
14285
14442
  }
@@ -14288,16 +14445,18 @@ function summaryEquals(a, b) {
14288
14445
  import { Not as Not8, defineQuery as defineQuery48, defineTickSystem as defineTickSystem17 } from "@vibecook/strata-ecs";
14289
14446
  import { Local as Local2 } from "@vibecook/strata-ecs";
14290
14447
  var remotePeerCursorsQ = defineQuery48([PresenceCursor, PresencePeer, Not8(Local2)]);
14291
- function createRemoteCursorsRig(world) {
14448
+ function createRemoteCursorsRig(world, opts = {}) {
14292
14449
  const pool = /* @__PURE__ */ new Map();
14293
14450
  const system = defineTickSystem17(
14294
14451
  (ctx) => {
14295
14452
  const live = /* @__PURE__ */ new Set();
14453
+ const here = presenceCanvasOf(world, opts.peerId ?? "", opts.keyOf);
14296
14454
  ctx.query(remotePeerCursorsQ).each((b) => {
14297
14455
  for (const r of b) {
14298
14456
  const peer = b.entity(r);
14299
- live.add(peer);
14300
14457
  const pc = ctx.read(peer, PresenceCursor);
14458
+ if (pc.away || (pc.canvas ?? "") !== here) continue;
14459
+ live.add(peer);
14301
14460
  const entry = pool.get(peer);
14302
14461
  if (entry === void 0) {
14303
14462
  const cursor = ctx.spawn({
@@ -14345,14 +14504,23 @@ function createRemoteCursorsRig(world) {
14345
14504
  }
14346
14505
  };
14347
14506
  }
14348
- function createRemoteCursorsSystem(world) {
14349
- return createRemoteCursorsRig(world).system;
14507
+ function createRemoteCursorsSystem(world, opts = {}) {
14508
+ return createRemoteCursorsRig(world, opts).system;
14350
14509
  }
14351
14510
  function installPresence(engine, session, opts = {}) {
14352
- const removePublish = engine.onPublish(createPresencePublish(engine.world, session, opts));
14353
- 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 } : {} });
14354
14520
  const removeSystem = engine.addSystems("present", rig.system);
14355
14521
  return () => {
14522
+ installed = false;
14523
+ stopAway();
14356
14524
  removePublish();
14357
14525
  removeSystem();
14358
14526
  rig.reap();
@@ -16247,6 +16415,21 @@ function createCanvasEngine(opts = {}) {
16247
16415
  }
16248
16416
  return spawned;
16249
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
+ },
16250
16433
  insertByDrag(type, o) {
16251
16434
  requireWritable("insertByDrag");
16252
16435
  const targetFrame = requireCurrentFrame("insertByDrag");
@@ -16812,6 +16995,7 @@ export {
16812
16995
  PartTap,
16813
16996
  WheelHandled,
16814
16997
  OverInteractive,
16998
+ PointerOutside,
16815
16999
  LocalPointer,
16816
17000
  PointerWorld,
16817
17001
  Keyboard,
@@ -16974,6 +17158,9 @@ export {
16974
17158
  closeTray,
16975
17159
  toggleTray,
16976
17160
  scrollTray,
17161
+ trayHung,
17162
+ trayFocus,
17163
+ focusTray,
16977
17164
  trayCategory,
16978
17165
  setTrayCategory,
16979
17166
  trayCategories,
@@ -17023,6 +17210,7 @@ export {
17023
17210
  attachBroadcastRelay,
17024
17211
  DEFAULT_PRESENCE_TTL_MS,
17025
17212
  attachPresence,
17213
+ presenceCanvasOf,
17026
17214
  createPresencePublish,
17027
17215
  createRemoteCursorsSystem,
17028
17216
  installPresence,
@@ -17047,4 +17235,4 @@ export {
17047
17235
  Local3 as Local,
17048
17236
  DurableUndoStatus
17049
17237
  };
17050
- //# sourceMappingURL=chunk-PFJN3XN2.js.map
17238
+ //# sourceMappingURL=chunk-CAJ64OD6.js.map