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

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 (39) hide show
  1. package/dist/{chunk-ZHASMOQV.js → chunk-644DO6PN.js} +75 -13
  2. package/dist/chunk-644DO6PN.js.map +1 -0
  3. package/dist/{chunk-5QVYKU64.js → chunk-A6PUTIL7.js} +64 -7
  4. package/dist/chunk-A6PUTIL7.js.map +1 -0
  5. package/dist/{chunk-HSIFKQNP.js → chunk-SPG45AVV.js} +212 -36
  6. package/dist/chunk-SPG45AVV.js.map +1 -0
  7. package/dist/{chunk-OHEHS2EM.js → chunk-US7WJNR6.js} +2 -2
  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/l1-pick.d.ts +7 -4
  28. package/dist/types/packages/core/src/systems/tray.d.ts +3 -0
  29. package/dist/types/packages/desk/src/ground.d.ts +11 -3
  30. package/dist/types/packages/desk/src/host/layer.d.ts +54 -3
  31. package/dist/types/packages/desk/src/marks/layout.d.ts +8 -0
  32. package/dist/types/packages/desk/src/still.d.ts +16 -1
  33. package/dist/types/packages/dom/src/desk-host.d.ts +5 -0
  34. package/dist/types/packages/dom/src/pointer-adapter.d.ts +21 -2
  35. package/package.json +1 -1
  36. package/dist/chunk-5QVYKU64.js.map +0 -1
  37. package/dist/chunk-HSIFKQNP.js.map +0 -1
  38. package/dist/chunk-ZHASMOQV.js.map +0 -1
  39. /package/dist/{chunk-OHEHS2EM.js.map → chunk-US7WJNR6.js.map} +0 -0
@@ -8,7 +8,7 @@ import {
8
8
  Viewport,
9
9
  defineQuery,
10
10
  writeRuntimeResource
11
- } from "./chunk-HSIFKQNP.js";
11
+ } from "./chunk-SPG45AVV.js";
12
12
  import {
13
13
  clientToScreen,
14
14
  screenSizeOf,
@@ -172,7 +172,8 @@ function deviceOf(e) {
172
172
  }
173
173
  function attachPointerAdapter(host, queue) {
174
174
  const { container } = host;
175
- const view = container.ownerDocument.defaultView;
175
+ const doc = container.ownerDocument;
176
+ const view = doc.defaultView;
176
177
  let spaceHeld = false;
177
178
  const live = /* @__PURE__ */ new Map();
178
179
  const CAPTURE_SLOP_PX = 4;
@@ -186,6 +187,18 @@ function attachPointerAdapter(host, queue) {
186
187
  let lastMods = { shift: false, ctrl: false, alt: false, meta: false, space: false };
187
188
  const atRest = /* @__PURE__ */ new Map();
188
189
  const sameMods = (a, b) => a.shift === b.shift && a.ctrl === b.ctrl && a.alt === b.alt && a.meta === b.meta && a.space === b.space;
190
+ let leaving;
191
+ let left = false;
192
+ let lastMouse;
193
+ const mouseBack = () => {
194
+ if (leaving !== void 0) clearTimeout(leaving);
195
+ leaving = void 0;
196
+ left = false;
197
+ };
198
+ const enqueue = (ev) => {
199
+ if (ev.pointerId === "mouse") lastMouse = { x: ev.screenX, y: ev.screenY };
200
+ queue.enqueue(ev);
201
+ };
189
202
  const relative = (clientX, clientY) => clientToScreen(clientX, clientY, container.getBoundingClientRect(), container);
190
203
  const pointerMods = (e) => ({
191
204
  shift: e.shiftKey,
@@ -201,8 +214,9 @@ function attachPointerAdapter(host, queue) {
201
214
  const native = crossesInteractive(e.target, container);
202
215
  live.set(id, { device, x, y, downX: x, downY: y, native, captured: false });
203
216
  atRest.delete(id);
217
+ if (id === "mouse") mouseBack();
204
218
  const surfaceHandled = native;
205
- queue.enqueue({
219
+ enqueue({
206
220
  kind: "down",
207
221
  pointerId: id,
208
222
  device,
@@ -219,6 +233,7 @@ function attachPointerAdapter(host, queue) {
219
233
  };
220
234
  const onPointerMove = (e) => {
221
235
  const id = pointerIdOf(e);
236
+ if (id === "mouse") mouseBack();
222
237
  const device = deviceOf(e);
223
238
  const { x, y } = relative(e.clientX, e.clientY);
224
239
  const over = crossesInteractive(e.target, container);
@@ -241,7 +256,7 @@ function attachPointerAdapter(host, queue) {
241
256
  if (last !== void 0 && last.x === x && last.y === y && last.target === e.target && last.over === over && sameMods(last.mods, mods)) return;
242
257
  atRest.set(id, { x, y, target: e.target, over, mods });
243
258
  } else atRest.delete(id);
244
- queue.enqueue({
259
+ enqueue({
245
260
  kind: "move",
246
261
  pointerId: id,
247
262
  device,
@@ -266,7 +281,7 @@ function attachPointerAdapter(host, queue) {
266
281
  } catch {
267
282
  }
268
283
  }
269
- queue.enqueue({
284
+ enqueue({
270
285
  kind,
271
286
  pointerId: id,
272
287
  device,
@@ -282,20 +297,21 @@ function attachPointerAdapter(host, queue) {
282
297
  const onPointerCancel = endPointer("cancel");
283
298
  const onWheel = (e) => {
284
299
  atRest.delete("mouse");
300
+ mouseBack();
285
301
  const { x, y } = relative(e.clientX, e.clientY);
286
302
  const scale = e.deltaMode === 1 ? WHEEL_LINE_PX : 1;
287
303
  if (e.ctrlKey) {
288
304
  e.preventDefault();
289
- queue.enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel: { dx: 0, dy: 0, pinch: e.deltaY } });
305
+ enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel: { dx: 0, dy: 0, pinch: e.deltaY } });
290
306
  return;
291
307
  }
292
308
  const wheel = { dx: e.deltaX * scale, dy: e.deltaY * scale, pinch: 0 };
293
309
  if (wheelCede(e.target, container, e.deltaX, e.deltaY)) {
294
- queue.enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel, wheelHandled: true });
310
+ enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel, wheelHandled: true });
295
311
  return;
296
312
  }
297
313
  e.preventDefault();
298
- queue.enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel });
314
+ enqueue({ kind: "wheel", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel });
299
315
  };
300
316
  const emitKeyIfChanged = (e) => {
301
317
  const mods = { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey, space: spaceHeld };
@@ -303,7 +319,7 @@ function attachPointerAdapter(host, queue) {
303
319
  return;
304
320
  }
305
321
  lastMods = { ...mods };
306
- queue.enqueue({ kind: "key", pointerId: "", device: "mouse", screenX: 0, screenY: 0, buttons: 0, mods });
322
+ enqueue({ kind: "key", pointerId: "", device: "mouse", screenX: 0, screenY: 0, buttons: 0, mods });
307
323
  };
308
324
  const isSpace = (e) => e.key === " " || e.code === "Space";
309
325
  const onKeyDown = (e) => {
@@ -329,13 +345,13 @@ function attachPointerAdapter(host, queue) {
329
345
  };
330
346
  const cancelAllInput = () => {
331
347
  for (const [id, s] of live) {
332
- queue.enqueue({ kind: "cancel", pointerId: id, device: s.device, screenX: s.x, screenY: s.y, buttons: 0, mods: { ...NO_MODS, space: spaceHeld } });
348
+ enqueue({ kind: "cancel", pointerId: id, device: s.device, screenX: s.x, screenY: s.y, buttons: 0, mods: { ...NO_MODS, space: spaceHeld } });
333
349
  }
334
350
  live.clear();
335
351
  spaceHeld = false;
336
352
  if (lastMods.space || lastMods.shift || lastMods.ctrl || lastMods.alt || lastMods.meta) {
337
353
  lastMods = { ...NO_MODS };
338
- queue.enqueue({
354
+ enqueue({
339
355
  kind: "key",
340
356
  pointerId: "",
341
357
  device: "mouse",
@@ -349,22 +365,67 @@ function attachPointerAdapter(host, queue) {
349
365
  const onBlur = () => {
350
366
  cancelAllInput();
351
367
  };
368
+ const leaveAt = (x, y, mods) => {
369
+ if (leaving !== void 0 || left) return;
370
+ leaving = setTimeout(() => {
371
+ leaving = void 0;
372
+ left = true;
373
+ atRest.delete("mouse");
374
+ enqueue({ kind: "leave", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: 0, mods });
375
+ }, 0);
376
+ };
377
+ const onPointerLeave = (e) => {
378
+ if (e.target !== container || e.relatedTarget != null || pointerIdOf(e) !== "mouse" || live.has("mouse")) return;
379
+ const { x, y } = relative(e.clientX, e.clientY);
380
+ leaveAt(x, y, pointerMods(e));
381
+ };
382
+ const onWindowOut = (e) => {
383
+ if (e.relatedTarget != null || pointerIdOf(e) !== "mouse" || live.has("mouse")) return;
384
+ const at = e.target instanceof Node && container.contains(e.target) ? relative(e.clientX, e.clientY) : lastMouse;
385
+ if (at !== void 0) leaveAt(at.x, at.y, pointerMods(e));
386
+ };
387
+ const onWindowOver = (e) => {
388
+ if (e.relatedTarget != null || pointerIdOf(e) !== "mouse" || leaving === void 0) return;
389
+ clearTimeout(leaving);
390
+ leaving = void 0;
391
+ };
392
+ const onPointerEnter = (e) => {
393
+ if (e.target !== container || pointerIdOf(e) !== "mouse") return;
394
+ if (leaving !== void 0) {
395
+ mouseBack();
396
+ return;
397
+ }
398
+ if (!left) return;
399
+ mouseBack();
400
+ const { x, y } = relative(e.clientX, e.clientY);
401
+ enqueue({ kind: "move", pointerId: "mouse", device: "mouse", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), tMs: e.timeStamp });
402
+ };
352
403
  container.addEventListener("pointerdown", onPointerDown);
353
404
  container.addEventListener("pointermove", onPointerMove);
354
405
  container.addEventListener("pointerup", onPointerUp);
355
406
  container.addEventListener("pointercancel", onPointerCancel);
356
407
  container.addEventListener("wheel", onWheel, { passive: false });
408
+ container.addEventListener("pointerleave", onPointerLeave);
409
+ container.addEventListener("pointerenter", onPointerEnter);
410
+ doc.addEventListener("pointerout", onWindowOut, true);
411
+ doc.addEventListener("pointerover", onWindowOver, true);
357
412
  view?.addEventListener("keydown", onKeyDown);
358
413
  view?.addEventListener("keyup", onKeyUp);
359
414
  view?.addEventListener("blur", onBlur);
360
415
  return () => {
361
416
  cancelAllInput();
362
417
  atRest.clear();
418
+ if (leaving !== void 0) clearTimeout(leaving);
419
+ leaving = void 0;
363
420
  container.removeEventListener("pointerdown", onPointerDown);
364
421
  container.removeEventListener("pointermove", onPointerMove);
365
422
  container.removeEventListener("pointerup", onPointerUp);
366
423
  container.removeEventListener("pointercancel", onPointerCancel);
367
424
  container.removeEventListener("wheel", onWheel);
425
+ container.removeEventListener("pointerleave", onPointerLeave);
426
+ container.removeEventListener("pointerenter", onPointerEnter);
427
+ doc.removeEventListener("pointerout", onWindowOut, true);
428
+ doc.removeEventListener("pointerover", onWindowOver, true);
368
429
  view?.removeEventListener("keydown", onKeyDown);
369
430
  view?.removeEventListener("keyup", onKeyUp);
370
431
  view?.removeEventListener("blur", onBlur);
@@ -558,7 +619,8 @@ function createDeskHost(opts) {
558
619
  readMarquee: () => stack.marqueeBuffer,
559
620
  spatial: stack.index,
560
621
  ...engine.compositorDevice !== void 0 ? { gpu: engine.compositorDevice } : {},
561
- frame: core.frame
622
+ frame: core.frame,
623
+ ops: engine.ops
562
624
  });
563
625
  } catch (err) {
564
626
  host.dispose();
@@ -644,4 +706,4 @@ export {
644
706
  createDeskHost,
645
707
  DOM_VERSION
646
708
  };
647
- //# sourceMappingURL=chunk-ZHASMOQV.js.map
709
+ //# sourceMappingURL=chunk-644DO6PN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../dom/src/host.ts","../../dom/src/loop.ts","../../dom/src/input-ownership.ts","../../dom/src/pointer-adapter.ts","../../dom/src/reflectors/cursor.ts","../../dom/src/reflectors/remote-cursors.ts","../../dom/src/widget-focus.ts","../../dom/src/desk-host.ts","../../dom/src/index.ts"],"sourcesContent":["/**\n * The canvas host (design-004 §1, host pipeline; SCREEN SPACE ONLY since design-015 D5b).\n *\n * The container is styled to be a stable, gesture-clean viewport: `relative` so\n * absolutely-positioned children (the desk's canvas, the one focused editor, the\n * remote-cursor plane, the app's screen-space chrome) anchor to it,\n * `overflow:hidden` to clip them, `touch-action:none` + `user-select:none` so\n * browser scroll/zoom and text selection never fight the interaction stack.\n *\n * THE CONTENT PLANE IS GONE (design-015 §2 law 2). Until D5b the host also made\n * one camera-transformed `<div>` — M3's \"content plane\", the P1 every DOM\n * widget mounted in, carrying the camera as ONE CSS transform written by the\n * plane-transform reflector (kernel `planeCssTransform`, gone since D7). No DOM element\n * carries a camera transform any more: everything under the camera is the\n * desk renderer's, and the DOM that remains is screen-space.\n */\n\nexport interface CanvasHost {\n /** The app-provided viewport element (styled, not created, by the host). */\n readonly container: HTMLElement;\n /** Clear the inline styles the host wrote. */\n dispose(): void;\n}\n\n/** The container styles the host owns unconditionally (cleared on dispose). */\nconst CONTAINER_STYLE: Readonly<Record<string, string>> = {\n overflow: \"hidden\",\n touchAction: \"none\",\n userSelect: \"none\",\n};\n\nexport function createCanvasHost(container: HTMLElement): CanvasHost {\n // The children need the container to be a POSITIONED containing block — but\n // `absolute`/`fixed` already qualify, so only promote a `static` container\n // to `relative`. Stomping an app's `position: absolute` with inline\n // `relative` collapses the common `#app { position: absolute; inset: 0 }`\n // sizing pattern to zero height (inset offsets a relative box, it does not\n // size it), and the host's own overflow:hidden then clips everything.\n // Inline style first (authoritative and environment-independent), computed\n // second (catches stylesheet rules like `#app { position: absolute }`).\n const view = container.ownerDocument.defaultView;\n const position =\n container.style.position !== \"\"\n ? container.style.position\n : (view?.getComputedStyle(container).position ?? \"\");\n const promoteToRelative = position === \"\" || position === \"static\";\n if (promoteToRelative) container.style.position = \"relative\";\n // Record each host-owned property's prior inline value BEFORE overwriting it,\n // so dispose restores what the caller set (e.g. an inline overflow:auto) rather\n // than blanket-clearing it. Empty string = the caller had no inline value here.\n const priorContainerStyle = Object.keys(CONTAINER_STYLE).map((prop) => {\n // camelCase → kebab-case for the CSSOM property API (touchAction → touch-action).\n const cssName = prop.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);\n return [cssName, container.style.getPropertyValue(cssName)] as const;\n });\n Object.assign(container.style, CONTAINER_STYLE);\n\n return {\n container,\n dispose() {\n if (promoteToRelative) container.style.removeProperty(\"position\");\n for (const [cssName, prior] of priorContainerStyle) {\n // Restore the caller's prior inline value, or clear the host's if the\n // caller had none.\n if (prior !== \"\") container.style.setProperty(cssName, prior);\n else container.style.removeProperty(cssName);\n }\n },\n };\n}\n","/**\n * The rAF frame loop (design-002 §1: core is scheduler-free — the DOM package\n * owns the loop; core never touches the platform).\n *\n * Drives `engine.step(now)` once per animation frame, threading the rAF\n * timestamp straight into the frame clock (so FrameInfo.dt is real wall time,\n * clamped in core). The returned stop fn cancels the pending frame AND latches\n * so a frame already in flight cannot re-schedule — calling it is idempotent.\n *\n * THE FREEZE GATE (2026-08-04). Before each frame the loop claims a step from\n * `engine.frame`. While a freeze is held the gate walks its settle and then\n * refuses, at which point the loop PARKS: it stops scheduling rAF at all\n * rather than spinning a callback that does nothing — \"the engine is stopped\"\n * should mean the browser has no work queued for it either. Thaw arrives\n * through the gate's change subscription, which schedules the next frame.\n *\n * The dt clamp is what makes resume safe: a freeze that lasted an hour hands\n * `step` an enormous `now`, and core clamps the delta (frame-info.ts), so\n * tweens advance by one ordinary frame instead of teleporting to their end.\n *\n * THE LOOP OUTLIVES A THROWING STEP (2026-08-15, petition I14). Until this fix\n * the reschedule sat on the line AFTER `engine.step(now)`, so ONE throw from a\n * publish hook (which propagate loudly by design — engine.ts) escaped the rAF\n * callback, left `handle` un-renewed, and killed the canvas PERMANENTLY: with\n * `parked` still false, `wake` early-returns forever, so even freeze/thaw could\n * not restart it. Recovery needed a fresh `startRafLoop`. The next frame is now\n * scheduled in a `finally`, so a faulting frame is loud but survivable — the\n * throw still reaches the browser's error handler; only the death is gone.\n *\n * BEFORE EACH STEP (2026-09-27, ICE M21 K1): an optional `beforeStep` — where a host reads what the\n * platform changes without an event it can hear (the device's ratio: `createDeskHost`). It runs\n * only for a step that runs (never while parked), and inside the same `try`, so a throwing hook is\n * as survivable as a throwing step.\n *\n * THE SLEEP (2026-09-27, ICE M21 K7a — `@ice/core`'s frame-control.ts): with `sleep`, the loop asks\n * the frame gate after every step when the next one is due, and SLEEPS when it is not the next\n * frame — no rAF queued, and a timer only when a registered wake names a time (a flight, a caret's\n * blink, a layer let go). A wake (an input, a write from outside a step, a document's or a room's\n * arrival, a desk's flux) restarts it in the next frame; so does a freeze, whose settle walk needs\n * steps. A desk at rest then costs the main thread nothing at all (design-015 §11.4). Without\n * `sleep` the loop steps every frame, as it always did.\n */\nimport type { Engine } from \"@ice/core\";\n\nexport interface RafLoopOptions {\n /** Sleep when the engine is quiet (K7a): no frame scheduled until a wake or a registered time. Default false. */\n readonly sleep?: boolean;\n}\n\nexport function startRafLoop(engine: Engine, beforeStep?: (now: number) => void, opts: RafLoopOptions = {}): () => void {\n const sleepy = opts.sleep === true;\n const frame = engine.frame;\n let handle = 0;\n let stopped = false;\n let parked = false;\n let asleep = false;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const tick = (now: number): void => {\n if (stopped) return;\n if (!frame.claimStep()) {\n // Park: no step, and deliberately no reschedule. `wake` owns the restart.\n parked = true;\n handle = 0;\n return;\n }\n try {\n beforeStep?.(now);\n engine.step(now);\n } finally {\n // Re-checked AFTER the body: a `stop()` called from inside the step\n // (a publish hook, a reflector) must still win, or the loop would\n // resurrect itself past its own stopper.\n if (!stopped) schedule();\n }\n };\n\n /** After a step: the next frame, or sleep — until a wake, or the soonest registered time. */\n const schedule = (): void => {\n if (!sleepy) {\n handle = requestAnimationFrame(tick);\n return;\n }\n const t = performance.now();\n const due = frame.nextStep(t);\n if (due <= t) {\n handle = requestAnimationFrame(tick);\n return;\n }\n handle = 0;\n asleep = true;\n frame.sleeping(true);\n if (due !== Number.POSITIVE_INFINITY) timer = setTimeout(() => rise(true), Math.max(0, due - t));\n };\n\n /** Out of the sleep: the next frame steps (`timed`: for a registered time alone). */\n const rise = (timed: boolean): void => {\n if (stopped || !asleep) return;\n asleep = false;\n if (timer !== undefined) clearTimeout(timer);\n timer = undefined;\n frame.sleeping(false, timed);\n handle = requestAnimationFrame(tick);\n };\n\n // (a freeze taken while the loop sleeps rises it through the world's doors — the freeze writes its `FrameMode` mirror — so its\n // settle walk has the steps it needs; the sleep test pins it)\n const wake = (): void => {\n if (stopped || !parked || frame.isFrozen()) return;\n parked = false;\n handle = requestAnimationFrame(tick);\n };\n const unsubscribe = frame.onChange(wake);\n const unsubscribeWake = sleepy ? frame.onWake(() => rise(false)) : undefined;\n\n handle = requestAnimationFrame(tick);\n\n return () => {\n stopped = true;\n unsubscribe();\n unsubscribeWake?.();\n if (timer !== undefined) clearTimeout(timer);\n if (asleep) frame.sleeping(false);\n cancelAnimationFrame(handle);\n };\n}\n","/**\n * Input-ownership predicates (design-007 §4 — one predicate FAMILY, not one\n * predicate; petitions I1/I4). \"Does the widget own input here\" is answered\n * per-surface at the right scope, sharing the DOM-walk mechanics:\n *\n * - `isEditableTarget` — the 4-class editable gate (keymap default + the\n * adapter's Space-types fix). Kept narrow so UNDECLARED widgets behave\n * exactly as before (design-007 F5).\n * - `keyboardClaimOf` — the `data-canvas-keyboard` claim marker written by the\n * dom-widgets reflector for `interaction.keyboard: \"exclusive\"` widgets.\n * Read UNBOUNDED (page-global) on purpose: `document.activeElement` is one\n * per page, so engine B's keymap must stand down while you type into engine\n * A's claiming widget (design-007 F2 — only the DOM is page-global).\n * - `wheelCede` — the NARROW, DYNAMIC scroll opt-out (design-007 §3.4/§4):\n * plain wheel cedes to native scroll only when a scroller WITH ROOM in the\n * wheel's dominant axis sits inside a claim-marked / `data-canvas-\n * interactive` / editable subtree. At the scroll bounds the predicate turns\n * false and the wheel falls through to canvas pan/zoom — the familiar\n * nested-scroll feel — which also keeps scroll-chaining from ever reaching\n * the page. Deliberately NOT the full `crossesInteractive` set: hovering a\n * bare `<button>` must not kill canvas zoom (design-007 F4).\n *\n * The keymap (`@ice/react`) imports these — the two historically disagreeing\n * guards now share one implementation (the I1 unification).\n */\n\n/** The claim marker the dom-widgets reflector writes on an exclusive widget's host. */\nexport const KEYBOARD_CLAIM_ATTR = \"data-canvas-keyboard\";\n\n/** Marker value for `keyboardEscape: \"widget\"` (the widget receives even Escape). */\nexport const CLAIM_OWNS_ESCAPE = \"escape\";\n\nexport interface KeyboardClaim {\n /** The event-target chain crosses a claim marker (⇒ engine keyboard stands down). */\n readonly claimed: boolean;\n /** The claiming widget owns Escape too (`keyboardEscape: \"widget\"`). */\n readonly ownsEscape: boolean;\n}\n\nconst NO_CLAIM: KeyboardClaim = { claimed: false, ownsEscape: false };\n\n/** The keymap's editable gate — exactly INPUT/TEXTAREA/SELECT/contentEditable. */\nexport function isEditableTarget(target: EventTarget | null): boolean {\n if (target === null || !(target instanceof HTMLElement)) return false;\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || tag === \"SELECT\" || target.isContentEditable;\n}\n\n/**\n * Resolve the keyboard claim for an event target (keydown targets ARE\n * `document.activeElement`, so this reads browser focus — the single source of\n * truth, design-007 §2.1). Unbounded `closest` by design (see header).\n */\nexport function keyboardClaimOf(target: EventTarget | null): KeyboardClaim {\n if (!(target instanceof Element)) return NO_CLAIM;\n const marked = target.closest(`[${KEYBOARD_CLAIM_ATTR}]`);\n if (marked === null) return NO_CLAIM;\n return { claimed: true, ownsEscape: marked.getAttribute(KEYBOARD_CLAIM_ATTR) === CLAIM_OWNS_ESCAPE };\n}\n\n/**\n * The scroll-opt-out subtree roots (design-007 §4 `crossesScrollable` — the\n * narrow set): the claim marker, the explicit interactive opt-in, and real\n * editable scrollers. NOT bare button/a/media.\n */\nconst SCROLL_OPT_OUT_SELECTOR =\n `[${KEYBOARD_CLAIM_ATTR}], [data-canvas-interactive], input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"]`;\n\n/** Sub-pixel tolerance for fractional scroll positions. */\nconst SCROLL_EPS = 1;\n\n/** The element can consume this wheel's dominant-axis delta RIGHT NOW (overflow + room). */\nfunction hasScrollRoom(el: HTMLElement, dx: number, dy: number): boolean {\n const style = el.ownerDocument.defaultView?.getComputedStyle(el);\n if (style === undefined || style === null) return false;\n if (Math.abs(dy) >= Math.abs(dx)) {\n if (dy === 0) return false;\n const oy = style.overflowY;\n if (oy !== \"auto\" && oy !== \"scroll\") return false;\n if (el.scrollHeight <= el.clientHeight + SCROLL_EPS) return false;\n return dy > 0 ? el.scrollTop + el.clientHeight < el.scrollHeight - SCROLL_EPS : el.scrollTop > SCROLL_EPS;\n }\n if (dx === 0) return false;\n const ox = style.overflowX;\n if (ox !== \"auto\" && ox !== \"scroll\") return false;\n if (el.scrollWidth <= el.clientWidth + SCROLL_EPS) return false;\n return dx > 0 ? el.scrollLeft + el.clientWidth < el.scrollWidth - SCROLL_EPS : el.scrollLeft > SCROLL_EPS;\n}\n\n/**\n * Should this plain (non-pinch) wheel be ceded to native widget scroll?\n * Walks target → container: remembers any scroller-with-room, cedes on the\n * first opt-out root at or above it. Walking up meets scrollers BEFORE their\n * widget's marker (the marker sits on the host), so \"a scroller inside an\n * opt-out subtree\" is exactly \"saw a scroller by the time the marker matches\".\n * ctrl/pinch wheels never reach this — they are ALWAYS the canvas's zoom.\n */\nexport function wheelCede(\n target: EventTarget | null,\n container: HTMLElement,\n dx: number,\n dy: number,\n): boolean {\n if (!(target instanceof Element)) return false;\n if (dx === 0 && dy === 0) return false;\n let sawScrollableRoom = false;\n for (let el: Element | null = target; el !== null && el !== container; el = el.parentElement) {\n if (el instanceof HTMLElement && hasScrollRoom(el, dx, dy)) sawScrollableRoom = true;\n if (sawScrollableRoom && el.matches(SCROLL_OPT_OUT_SELECTOR)) return true;\n }\n return false;\n}\n","/**\n * The real L0 pointer adapter (design-003 §2–§3; replaces the M3 demo-input scaffold).\n *\n * Translates DOM pointer / wheel / keyboard events into normalized `InputEvent`s\n * and ONLY enqueues them (Law 2: adapters enqueue, they never touch the world —\n * this file imports no ECS state and writes nothing). `pointerIngest` drains the\n * queue once per tick and turns the facts into world state.\n *\n * Details that matter:\n * - stable pointer ids: `\"mouse\"` for the one mouse, `\"touch:<pointerId>\"` per\n * contact, `\"pen\"` — ingest keys pointer entities off these (design-003 §2);\n * - container-relative CSS px (the kernel screen space): the client point mapped\n * into the container's LAYOUT space by kernel `clientToScreen` (petition I39) —\n * its offset from the bounding rect's top-left divided by the scale a CSS\n * transform draws the container at (`rect.width / clientWidth`), so a press\n * lands on what the desk draws under it at any transform, in the space the\n * viewport is measured in (the layout size, desk-host.ts);\n * - the button: every pointer fact carries PointerEvent `buttons`, and a down's\n * mask IS the button that pressed (a second button joins a held press as a\n * pointermove, never a down). Which press is a gesture is the stack's call,\n * not this file's — only the primary's (petition I28, core press-button.ts):\n * a secondary press is enqueued like any other, a point. `contextmenu` is\n * never listened to here — it is the host's, untouched;\n * - `setPointerCapture` on down so a drag keeps delivering outside the element;\n * - Space is the pan modifier (design-003 §4.4): tracked here and threaded onto\n * every event's `mods.space`; keydown/keyup emit a `key` fact only when the\n * modifier tuple actually changes. Space's preventDefault/latch is OWNERSHIP-\n * AWARE (design-007 §3.4, petition I1): into an editable it neither latches\n * nor preventDefaults (Space must TYPE — the §1.6 live-bug fix); into a\n * `data-canvas-keyboard` claim it cedes the pan SEMANTICS but keeps killing\n * the page-scroll default (the key still reaches the widget's own listener —\n * preventDefault is not stopPropagation); elsewhere it pans, as ever;\n * - wheel: ctrl-wheel / trackpad pinch is ALWAYS `preventDefault` + the canvas\n * zoom signal (`wheel.pinch`) — a browser page-zoom over a widget is\n * catastrophic, claimed or not. A PLAIN wheel over a scroller-with-room\n * inside a claim/`data-canvas-interactive`/editable subtree (`wheelCede`,\n * design-007 §3.4/§3.5) is ceded: NO preventDefault (native scroll\n * proceeds), the fact still lands flagged `wheelHandled` so ingest stamps\n * the one-tick `WheelHandled` and the wheel recognizers skip the tick. At\n * the scroll bounds the predicate flips and the wheel falls through to\n * canvas pan/zoom. Everything else: `preventDefault`, dx/dy (deltaMode\n * lines → px ×16) — unchanged;\n * - window blur enqueues a synthetic `cancel` for every pointer still down\n * (design-003 §8) — the browser will not send the pointerups;\n * - a NO-OP move is dropped (petition I33): a `pointermove` with no button down\n * and no press of ours live, whose position, target, hover verdict and\n * modifiers are those of the last move this adapter enqueued for that pointer,\n * says nothing the world does not hold — so it enqueues nothing: no wake, no\n * step, no `PointerVersion` bump, no touch of the desk's wind. Chromium\n * re-sends a resting pointer's move on a focus or visibility change, and other\n * windows' input raises trusted moves with no motion (VibeField DK-10b); each\n * held a still desk out of rest. Every press is untouched — a down, an up, a\n * cancel, and every move while a button is down or a press of ours is live\n * (a held drag's same-position moves included) are enqueued as before — and a\n * transition or a wheel starts the comparison afresh, so the first move after\n * one always lands. A `pointerleave`/`pointerenter` pair at one position is a\n * no-op (the leave below is answered by its enter and says nothing), and the\n * move that follows it is the same move;\n * - the pointer OUT OF THE HOST (petitions I42, I44): the mouse leaving the\n * WINDOW, no press of ours live, wherever it is in the host's document — off\n * the container itself (its `pointerleave`, no related target), or off a\n * host's chrome BESIDE it (a `pointerout` with no related target, heard on the\n * document in the capture phase: a page's handler cannot stop it) — is said\n * once, as a `leave` fact at the pointer's LAST POINT over the host: off the\n * container, the point it left; off the chrome, the point of the last fact\n * this adapter enqueued for the mouse — the desk never saw it over the chrome\n * (no such fact: nothing is said). Ingest tags the pointer `PointerOutside`;\n * presence publishes the peer `away`. An exit off the container is heard\n * twice (its pointerout reaches the document, then its pointerleave): one\n * leave, never two. It is deferred one task, so a leave answered at once —\n * by the container's enter, or the document's `pointerover` with no related\n * target (Chromium's pair at a resting point, above, at either) — enqueues\n * nothing. Once said, only the pointer back over the HOST clears it: an\n * enter after a said leave is a move at its point, and any fact of its own\n * clears it too, so a pointer back at rest is never left away; back in the\n * window over the chrome the adapter hears no fact, and it stays away.\n *\n * Widget opt-out (the pinned widget-event contract, design-002 §8 / design-004 §4):\n * on `pointerdown` ONLY, if the event's target chain (up to the container) crosses\n * a native interactive (input/textarea/select/button/a[href]/contenteditable/\n * media[controls]) or an explicit `[data-canvas-interactive]`, the down fact is\n * flagged `surfaceHandled: true` — the fact still lands (the one input path is\n * absolute), but recognizer-spawn/arbitration skip it (`HandledByWidget` flows\n * through L0). `stopPropagation()` inside widget content is the OTHER boundary:\n * a stopped event never reaches the container listener, so it never becomes a\n * canvas fact at all — that is the design, not a gap.\n *\n * Hover-time amendment (design-002 §8, 2026-07-18): down/move facts ALSO carry\n * `overInteractive` — the same `crossesInteractive` check, run at hover time —\n * so the world can telegraph the opt-out BEFORE a down (cursor affordances).\n * (Until design-015 D5b the GL router's hover verdict was OR'd in; the GL\n * route left with the islands it picked.) It never gates gestures:\n * `surfaceHandled` stays down-only and keeps its exact meaning. During a\n * container-captured drag every event retargets to the container, so\n * `overInteractive` reads false mid-gesture — correct: the gesture owns the\n * pointer, not the widget under it.\n */\nimport { NO_MODS, type InputEvent, type InputMods, type InputQueue } from \"@ice/core\";\nimport { clientToScreen } from \"@ice/kernel\";\nimport type { CanvasHost } from \"./host\";\nimport { isEditableTarget, keyboardClaimOf, wheelCede } from \"./input-ownership\";\n\n/** DOM_DELTA_LINE → px (one wheel \"line\" ≈ 16px; matches typical browser mapping). */\nconst WHEEL_LINE_PX = 16;\n\n/**\n * Elements whose `pointerdown` is the widget's, not the canvas's (design-002 §8).\n * `contenteditable=\"\"` (bare attribute) and `=\"true\"` are editable; `=\"false\"` is\n * not. Media only opts out WITH visible controls.\n */\nconst INTERACTIVE_SELECTOR =\n 'input, textarea, select, button, a[href], [contenteditable=\"\"], [contenteditable=\"true\"], audio[controls], video[controls], [data-canvas-interactive]';\n\n/** True if `target`'s ancestor chain, up to but excluding `container`, holds an interactive surface. */\nfunction crossesInteractive(target: EventTarget | null, container: HTMLElement): boolean {\n if (!(target instanceof Element)) return false;\n const match = target.closest(INTERACTIVE_SELECTOR);\n // `closest` walks to the document root; bound it to inside the canvas so an\n // interactive ANCESTOR of the container (unusual, but legal) does not count.\n return match !== null && container.contains(match);\n}\n\nfunction pointerIdOf(e: PointerEvent): string {\n if (e.pointerType === \"touch\") return `touch:${e.pointerId}`;\n if (e.pointerType === \"pen\") return \"pen\";\n return \"mouse\";\n}\n\nfunction deviceOf(e: PointerEvent): InputEvent[\"device\"] {\n if (e.pointerType === \"touch\") return \"touch\";\n if (e.pointerType === \"pen\") return \"pen\";\n return \"mouse\";\n}\n\nexport function attachPointerAdapter(host: CanvasHost, queue: InputQueue): () => void {\n const { container } = host;\n const doc = container.ownerDocument;\n const view = doc.defaultView;\n\n let spaceHeld = false;\n // Pointers currently down — blur-cancel sweep + DEFERRED-CAPTURE state.\n // Capture timing (field report 2026-07-12, v1's click-vs-drag discrimination):\n // capturing on DOWN retargets the derived `click`/`dblclick` to the container,\n // killing every widget-body handler (todo rows, folder double-click). So:\n // - GL-claimed downs capture IMMEDIATELY (the router synthesizes its own\n // clicks; island drags must keep delivering outside the element);\n // - native-interactive downs NEVER capture (the engine ignores them);\n // - everything else captures on the FIRST held-button move past a 4px slop\n // — below every recognizer dead zone, so a drag is always captured\n // before it can activate, while a clean tap never captures at all.\n const live = new Map<\n string,\n {\n device: InputEvent[\"device\"];\n x: number;\n y: number;\n downX: number;\n downY: number;\n native: boolean;\n captured: boolean;\n }\n >();\n const CAPTURE_SLOP_PX = 4;\n\n const capture = (pointerId: number): void => {\n if (typeof container.setPointerCapture !== \"function\") return;\n try {\n container.setPointerCapture(pointerId);\n } catch {\n // capture is best-effort — a detached or unsupported target must not throw here.\n }\n };\n // Last emitted keyboard tuple — emit a `key` fact only when it actually changes.\n let lastMods = { shift: false, ctrl: false, alt: false, meta: false, space: false };\n // THE LAST MOVE AT REST (petition I33): per pointer, the last move enqueued with no button down and no press of ours live —\n // what a no-op move is measured against. A press or a wheel deletes it (the first move after one always lands); the window's\n // blur keeps it (a resting pointer's re-dispatch after the focus returns is exactly the no-op to drop); detach clears it.\n const atRest = new Map<string, { x: number; y: number; target: EventTarget | null; over: boolean; mods: InputMods }>();\n const sameMods = (a: InputMods, b: InputMods): boolean =>\n a.shift === b.shift && a.ctrl === b.ctrl && a.alt === b.alt && a.meta === b.meta && a.space === b.space;\n\n // THE POINTER OUT OF THE HOST (petitions I42, I44): a leave waiting out its task (an answer cancels it), whether one was said, and\n // the mouse's point in the last fact enqueued for it — where a leave off the host's chrome is said (the desk never saw it there)\n let leaving: ReturnType<typeof setTimeout> | undefined;\n let left = false;\n let lastMouse: { x: number; y: number } | undefined;\n /** The mouse is over the host (any fact of its own): a leave waiting is no leave, and one said is answered by this fact. */\n const mouseBack = (): void => {\n if (leaving !== undefined) clearTimeout(leaving);\n leaving = undefined;\n left = false;\n };\n /** Every fact this adapter says is enqueued here; the mouse's keeps its point (petition I44 — a leave off the chrome is said at it). */\n const enqueue = (ev: InputEvent): void => {\n if (ev.pointerId === \"mouse\") lastMouse = { x: ev.screenX, y: ev.screenY };\n queue.enqueue(ev);\n };\n\n // the client point in the container's LAYOUT space (petition I39): a transform scales its bounding rect, never the space the desk draws in\n const relative = (clientX: number, clientY: number): { x: number; y: number } => clientToScreen(clientX, clientY, container.getBoundingClientRect(), container);\n\n const pointerMods = (e: PointerEvent | WheelEvent): InputMods => ({\n shift: e.shiftKey,\n ctrl: e.ctrlKey,\n alt: e.altKey,\n meta: e.metaKey,\n space: spaceHeld,\n });\n\n const onPointerDown = (e: PointerEvent): void => {\n const id = pointerIdOf(e);\n const device = deviceOf(e);\n const { x, y } = relative(e.clientX, e.clientY);\n // Widget opt-out: down on a native interactive / [data-canvas-interactive]\n // flags the fact so recognizers skip it (design-002 §8).\n const native = crossesInteractive(e.target, container);\n live.set(id, { device, x, y, downX: x, downY: y, native, captured: false });\n atRest.delete(id);\n if (id === \"mouse\") mouseBack();\n const surfaceHandled = native;\n enqueue({\n kind: \"down\",\n pointerId: id,\n device,\n screenX: x,\n screenY: y,\n buttons: e.buttons,\n mods: pointerMods(e),\n tMs: e.timeStamp,\n ...(surfaceHandled ? { surfaceHandled: true } : {}),\n // The down IS a hover-bearing fact: a touch's first event must seed the\n // pointer's OverInteractive truth (no prior moves exist to).\n overInteractive: surfaceHandled,\n });\n };\n\n const onPointerMove = (e: PointerEvent): void => {\n const id = pointerIdOf(e);\n if (id === \"mouse\") mouseBack();\n const device = deviceOf(e);\n const { x, y } = relative(e.clientX, e.clientY);\n const over = crossesInteractive(e.target, container);\n const mods = pointerMods(e);\n let seen = live.get(id);\n // ADOPTED-gesture hardening (ops.insertByDrag, 2026-07-19): a held-button\n // move for a pointer whose down we never saw means widget content started\n // the gesture and handed it to the engine (the tray tile stopPropagation'd\n // its down — the sanctioned boundary). Track it from HERE so the deferred\n // capture below still takes the pointer before it can leave the window\n // mid-drag, and the blur sweep knows to cancel it. Gated off interactive\n // targets: a move over opted-out content (text selection in a widget's\n // input, the tray panel itself) must never trigger a container capture.\n if (seen === undefined && e.buttons !== 0 && !over) {\n seen = { device, x, y, downX: x, downY: y, native: false, captured: false };\n live.set(id, seen);\n }\n if (seen !== undefined) {\n seen.x = x;\n seen.y = y;\n // Deferred capture: a real drag is forming — take it before any\n // recognizer dead zone (>=8px) can activate.\n if (\n !seen.captured &&\n !seen.native &&\n e.buttons !== 0 &&\n Math.hypot(x - seen.downX, y - seen.downY) > CAPTURE_SLOP_PX\n ) {\n capture(e.pointerId);\n seen.captured = true;\n }\n }\n // A NO-OP move (petition I33): at rest — no button down, no press of ours live — and the last move enqueued for this pointer\n // in every fact it carries but the time. Dropped: the world already holds all of it, and an enqueue would wake the loop.\n if (seen === undefined && e.buttons === 0) {\n const last = atRest.get(id);\n if (last !== undefined && last.x === x && last.y === y && last.target === e.target && last.over === over && sameMods(last.mods, mods)) return;\n atRest.set(id, { x, y, target: e.target, over, mods });\n } else atRest.delete(id);\n enqueue({\n kind: \"move\",\n pointerId: id,\n device,\n screenX: x,\n screenY: y,\n buttons: e.buttons,\n mods,\n tMs: e.timeStamp,\n // Hover fact, every move: the DOM chain check.\n overInteractive: over,\n });\n };\n\n const endPointer =\n (kind: \"up\" | \"cancel\") =>\n (e: PointerEvent): void => {\n const id = pointerIdOf(e);\n const device = deviceOf(e);\n const { x, y } = relative(e.clientX, e.clientY);\n live.delete(id);\n atRest.delete(id);\n if (typeof container.releasePointerCapture === \"function\") {\n try {\n container.releasePointerCapture(e.pointerId);\n } catch {\n // release is best-effort (capture may already be gone).\n }\n }\n enqueue({\n kind,\n pointerId: id,\n device,\n screenX: x,\n screenY: y,\n buttons: e.buttons,\n mods: pointerMods(e),\n // No overInteractive stamp: a capture-retargeted up reads the container,\n // not the content under the pointer — the next real move re-truths it.\n });\n };\n const onPointerUp = endPointer(\"up\");\n const onPointerCancel = endPointer(\"cancel\");\n\n const onWheel = (e: WheelEvent): void => {\n // a wheel moves the camera under the pointer: the next move is measured afresh (petition I33), never dropped as a repeat\n atRest.delete(\"mouse\");\n mouseBack();\n const { x, y } = relative(e.clientX, e.clientY);\n const scale = e.deltaMode === 1 ? WHEEL_LINE_PX : 1; // DOM_DELTA_LINE → px\n if (e.ctrlKey) {\n // Pinch/ctrl-wheel: ALWAYS the canvas's zoom (design-007 F4) — the\n // browser's page pinch-zoom must never fire, claimed content or not.\n e.preventDefault();\n enqueue({ kind: \"wheel\", pointerId: \"mouse\", device: \"mouse\", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel: { dx: 0, dy: 0, pinch: e.deltaY } });\n return;\n }\n const wheel = { dx: e.deltaX * scale, dy: e.deltaY * scale, pinch: 0 };\n if (wheelCede(e.target, container, e.deltaX, e.deltaY)) {\n // Ceded to widget scroll (I4): no preventDefault — native scroll consumes\n // the deltas. The fact still lands (the one input path is absolute),\n // flagged so recognizers treat the tick as wheel silence. The room-check\n // in wheelCede means an at-bounds scroller falls through to the branch\n // below — and scroll-chaining can never escape to the page.\n enqueue({ kind: \"wheel\", pointerId: \"mouse\", device: \"mouse\", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel, wheelHandled: true });\n return;\n }\n e.preventDefault(); // own zoom/pan — never let the page scroll\n enqueue({ kind: \"wheel\", pointerId: \"mouse\", device: \"mouse\", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), wheel });\n };\n\n /** Emit a `key` fact iff the modifier tuple changed (design-003 §2 minimal keyboard). */\n const emitKeyIfChanged = (e: KeyboardEvent): void => {\n const mods: InputMods = { shift: e.shiftKey, ctrl: e.ctrlKey, alt: e.altKey, meta: e.metaKey, space: spaceHeld };\n if (\n mods.shift === lastMods.shift &&\n mods.ctrl === lastMods.ctrl &&\n mods.alt === lastMods.alt &&\n mods.meta === lastMods.meta &&\n mods.space === lastMods.space\n ) {\n return;\n }\n lastMods = { ...mods };\n enqueue({ kind: \"key\", pointerId: \"\", device: \"mouse\", screenX: 0, screenY: 0, buttons: 0, mods });\n };\n\n const isSpace = (e: KeyboardEvent): boolean => e.key === \" \" || e.code === \"Space\";\n\n const onKeyDown = (e: KeyboardEvent): void => {\n if (isSpace(e) && !e.defaultPrevented) {\n // Ownership-aware Space (design-007 §3.4; the keydown target IS the\n // focused node, so this reads browser focus — the one truth).\n if (isEditableTarget(e.target)) {\n // Space must TYPE into widget fields (§1.6 live-bug fix): no\n // preventDefault, no pan latch.\n } else if (keyboardClaimOf(e.target).claimed) {\n // Cede the pan SEMANTICS to the claiming widget; keep suppressing the\n // page-scroll default (§3.3 residual — preventDefault still lets the\n // key reach the widget's own keydown listener).\n e.preventDefault();\n } else {\n e.preventDefault(); // Space would otherwise page-scroll\n spaceHeld = true;\n }\n }\n emitKeyIfChanged(e);\n };\n const onKeyUp = (e: KeyboardEvent): void => {\n if (isSpace(e)) {\n if (!e.defaultPrevented && !isEditableTarget(e.target) && !keyboardClaimOf(e.target).claimed) {\n e.preventDefault();\n }\n // Clear UNCONDITIONALLY: the down may have latched over canvas and the\n // up can land after focus moved into a widget/editable — a skipped clear\n // here would strand the pan modifier held.\n spaceHeld = false;\n }\n emitKeyIfChanged(e);\n };\n\n /**\n * Drain every latched input to a clean slate (Law 2: enqueue only). Called on\n * window blur AND on detach: the browser delivers no more pointerups/keyups\n * once we blur, and detaching mid-gesture (unmounting the canvas while the\n * engine outlives it) must not strand an Active drag / held buttons / latched\n * modifiers in the world for a remount of the same engine to resurrect. Also\n * clears the deferred-capture `live` map so no per-pointer tracking survives.\n */\n const cancelAllInput = (): void => {\n // The browser will not deliver pointerups once we lose focus — cancel every\n // still-down pointer so no gesture is stranded Active (design-003 §8).\n for (const [id, s] of live) {\n enqueue({ kind: \"cancel\", pointerId: id, device: s.device, screenX: s.x, screenY: s.y, buttons: 0, mods: { ...NO_MODS, space: spaceHeld } });\n }\n live.clear();\n // Modifier keyups are also lost on blur — ENQUEUE the clearing fact, or\n // the world's Keyboard resource stays latched (space-stuck: every canvas\n // drag pans forever). Clearing only local state was the bug: lastMods\n // already saying \"space up\" suppressed the next emitKeyIfChanged too.\n spaceHeld = false;\n if (lastMods.space || lastMods.shift || lastMods.ctrl || lastMods.alt || lastMods.meta) {\n lastMods = { ...NO_MODS };\n enqueue({\n kind: \"key\",\n pointerId: \"\",\n device: \"mouse\",\n screenX: 0,\n screenY: 0,\n buttons: 0,\n mods: { ...NO_MODS },\n });\n }\n };\n\n const onBlur = (): void => {\n cancelAllInput();\n };\n\n // THE POINTER OUT OF THE HOST (petitions I42, I44; module doc): said a task later at (x, y), unless an answer comes first — and\n // once: a leave waiting or said is not said again (an exit off the container is heard twice, its pointerout and its pointerleave)\n const leaveAt = (x: number, y: number, mods: InputMods): void => {\n if (leaving !== undefined || left) return;\n leaving = setTimeout(() => {\n leaving = undefined;\n left = true;\n atRest.delete(\"mouse\"); // the move that brings it back always lands (petition I33's comparison starts afresh)\n enqueue({ kind: \"leave\", pointerId: \"mouse\", device: \"mouse\", screenX: x, screenY: y, buttons: 0, mods });\n }, 0);\n };\n const onPointerLeave = (e: PointerEvent): void => {\n if (e.target !== container || e.relatedTarget != null || pointerIdOf(e) !== \"mouse\" || live.has(\"mouse\")) return;\n const { x, y } = relative(e.clientX, e.clientY);\n leaveAt(x, y, pointerMods(e));\n };\n // …out of the WINDOW from anywhere in the document (I44): off the container at the point it left, off the chrome beside it at the\n // last point said for the mouse (none said: nothing to say)\n const onWindowOut = (e: PointerEvent): void => {\n if (e.relatedTarget != null || pointerIdOf(e) !== \"mouse\" || live.has(\"mouse\")) return;\n const at = e.target instanceof Node && container.contains(e.target) ? relative(e.clientX, e.clientY) : lastMouse;\n if (at !== undefined) leaveAt(at.x, at.y, pointerMods(e));\n };\n // …back in the window: an answer to a leave WAITING (the resting pair, at the document), never to one said — over the chrome the\n // desk hears nothing, so the pointer stays away until it is back over the host\n const onWindowOver = (e: PointerEvent): void => {\n if (e.relatedTarget != null || pointerIdOf(e) !== \"mouse\" || leaving === undefined) return;\n clearTimeout(leaving);\n leaving = undefined;\n };\n const onPointerEnter = (e: PointerEvent): void => {\n if (e.target !== container || pointerIdOf(e) !== \"mouse\") return;\n if (leaving !== undefined) {\n mouseBack(); // the leave answered at once: none was said\n return;\n }\n if (!left) return;\n mouseBack();\n const { x, y } = relative(e.clientX, e.clientY);\n // back over the host — at rest or not, the pointer is here (no hover verdict: the enter's target is the container; the next move's is the truth)\n enqueue({ kind: \"move\", pointerId: \"mouse\", device: \"mouse\", screenX: x, screenY: y, buttons: e.buttons, mods: pointerMods(e), tMs: e.timeStamp });\n };\n\n container.addEventListener(\"pointerdown\", onPointerDown);\n container.addEventListener(\"pointermove\", onPointerMove);\n container.addEventListener(\"pointerup\", onPointerUp);\n container.addEventListener(\"pointercancel\", onPointerCancel);\n container.addEventListener(\"wheel\", onWheel, { passive: false });\n container.addEventListener(\"pointerleave\", onPointerLeave);\n container.addEventListener(\"pointerenter\", onPointerEnter);\n // Capture phase (I44): the window's exit and return are the document's facts — no page handler's stopPropagation can hide them.\n doc.addEventListener(\"pointerout\", onWindowOut, true);\n doc.addEventListener(\"pointerover\", onWindowOver, true);\n view?.addEventListener(\"keydown\", onKeyDown);\n view?.addEventListener(\"keyup\", onKeyUp);\n view?.addEventListener(\"blur\", onBlur);\n\n return () => {\n // Sweep first (a detach can land mid-gesture) — same clean-slate cancel as\n // blur; facts drain on the engine's next step. Then unwire the listeners.\n cancelAllInput();\n atRest.clear();\n if (leaving !== undefined) clearTimeout(leaving);\n leaving = undefined;\n container.removeEventListener(\"pointerdown\", onPointerDown);\n container.removeEventListener(\"pointermove\", onPointerMove);\n container.removeEventListener(\"pointerup\", onPointerUp);\n container.removeEventListener(\"pointercancel\", onPointerCancel);\n container.removeEventListener(\"wheel\", onWheel);\n container.removeEventListener(\"pointerleave\", onPointerLeave);\n container.removeEventListener(\"pointerenter\", onPointerEnter);\n doc.removeEventListener(\"pointerout\", onWindowOut, true);\n doc.removeEventListener(\"pointerover\", onWindowOver, true);\n view?.removeEventListener(\"keydown\", onKeyDown);\n view?.removeEventListener(\"keyup\", onKeyUp);\n view?.removeEventListener(\"blur\", onBlur);\n };\n}\n","/**\n * The cursor reflector (design-003 §7 L4 cursor projection; v1 approach:\n * local cursor = OS cursor via one `style.cursor` write).\n *\n * `createCursorSync` resolves the cursor string into closure state every\n * `derive` tick with no observable ECS stamp (a plain string, not a\n * component/resource write), so there is nothing for `observe` to watch —\n * hence `always: true` with an internal last-value cache, mirroring the\n * \"cheap-overlay style\" carve-out `ReflectorDef.always` exists for. The cache\n * is what keeps the write ONE-per-change: `flush` runs every frame, but\n * `host.container.style.cursor` is only touched when `readCursor()` differs\n * from the last-applied value.\n *\n * Law 10: reflectors run post-notify, write output only, never read layout\n * or write ECS — this flush touches only `host.container.style`.\n */\nimport type { ReflectorDef, World } from \"@ice/core\";\nimport type { CanvasHost } from \"../host\";\n\nexport function createCursorReflector(\n host: CanvasHost,\n readCursor: () => string,\n): ReflectorDef & { writes(): number } {\n let writes = 0;\n let last: string | undefined;\n return {\n name: \"cursor\",\n always: true,\n flush(_world: World) {\n const next = readCursor();\n if (next === last) return;\n last = next;\n host.container.style.cursor = next;\n writes++;\n },\n writes: () => writes,\n };\n}\n","/**\n * P5 — remote cursors (design-004 §1: screen-space DOM, \"remote cursors are\n * always custom nodes\"; design-003 §7).\n *\n * Renders one pooled node per REMOTE cursor entity (`CursorVisual` kind\n * \"remote\" + world `Position`, derived by core's remote-cursors system from\n * presence peers; `Follows` names the peer whose `PresenceInfo` supplies\n * color + label). Screen-space by design: an acknowledged small-N\n * camera→position consumer (design-002 §5 rescope), recomputed on camera or\n * cursor dirt only. The plane mounts LAST in the container, above chrome;\n * `pointer-events: none` throughout — cursors are never hit-testable.\n */\nimport {\n Camera,\n CursorVisual,\n Follows,\n Position,\n PresenceInfo,\n defineQuery,\n type Entity,\n type ReflectorDef,\n type World,\n} from \"@ice/core\";\nimport { worldToScreen } from \"@ice/kernel\";\nimport type { CanvasHost } from \"../host\";\n\nconst remoteCursorQ = defineQuery([Position, CursorVisual]);\n\nexport interface RemoteCursorsReflector {\n readonly reflector: ReflectorDef;\n /** Remove the plane + pool (doc close / HMR). */\n destroy(): void;\n}\n\nexport function createRemoteCursorsReflector(host: CanvasHost, world: World): RemoteCursorsReflector {\n const doc = host.container.ownerDocument;\n const plane = doc.createElement(\"div\");\n Object.assign(plane.style, {\n position: \"absolute\",\n inset: \"0\",\n overflow: \"hidden\",\n pointerEvents: \"none\",\n } as Partial<CSSStyleDeclaration>);\n host.container.appendChild(plane); // last child ⇒ topmost (P5 over P4)\n\n interface Node {\n root: HTMLDivElement;\n label: HTMLDivElement;\n color: string;\n name: string;\n x: number;\n y: number;\n }\n const pool = new Map<Entity, Node>();\n\n const ensure = (e: Entity): Node => {\n let n = pool.get(e);\n if (n !== undefined) return n;\n const root = doc.createElement(\"div\");\n Object.assign(root.style, {\n position: \"absolute\",\n left: \"0\",\n top: \"0\",\n willChange: \"transform\",\n pointerEvents: \"none\",\n } as Partial<CSSStyleDeclaration>);\n // Arrow glyph + name chip (color applied on first style pass).\n const arrow = doc.createElement(\"div\");\n Object.assign(arrow.style, {\n width: \"0\",\n height: \"0\",\n borderLeft: \"6px solid transparent\",\n borderRight: \"6px solid transparent\",\n borderBottom: \"14px solid currentColor\",\n transform: \"rotate(-32deg)\",\n } as Partial<CSSStyleDeclaration>);\n const label = doc.createElement(\"div\");\n Object.assign(label.style, {\n marginTop: \"2px\",\n marginLeft: \"8px\",\n padding: \"1px 6px\",\n borderRadius: \"8px\",\n font: \"11px system-ui, sans-serif\",\n color: \"#fff\",\n whiteSpace: \"nowrap\",\n } as Partial<CSSStyleDeclaration>);\n root.appendChild(arrow);\n root.appendChild(label);\n plane.appendChild(root);\n n = { root, label, color: \"\", name: \"\", x: Number.NaN, y: Number.NaN };\n pool.set(e, n);\n return n;\n };\n\n const reflector: ReflectorDef = {\n name: \"remoteCursors\",\n observe: {\n resources: [Camera],\n queries: [{ q: remoteCursorQ, cols: [Position, CursorVisual] }],\n },\n flush: (w) => {\n const cam = w.getResource(Camera) ?? { x: 0, y: 0, zoom: 1 };\n const seen = new Set<Entity>();\n w.query(remoteCursorQ).each((b) => {\n for (const r of b) {\n const e = b.entity(r);\n if (w.get(e, CursorVisual)?.kind !== \"remote\") continue;\n seen.add(e);\n const p = w.read(e, Position);\n const s = worldToScreen(p.x, p.y, cam);\n const n = ensure(e);\n if (n.x !== s.x || n.y !== s.y) {\n n.root.style.transform = `translate(${s.x}px, ${s.y}px)`;\n n.x = s.x;\n n.y = s.y;\n }\n const peer = w.getRelation(e, Follows);\n const info = peer !== undefined ? w.get(peer, PresenceInfo) : undefined;\n const color = info?.color ?? \"#7a8aa0\";\n const name = info?.name ?? \"\";\n if (n.color !== color) {\n n.root.style.color = color;\n n.label.style.background = color;\n n.color = color;\n }\n if (n.name !== name) {\n n.label.textContent = name;\n n.label.style.display = name === \"\" ? \"none\" : \"block\";\n n.name = name;\n }\n }\n });\n for (const [e, n] of pool) {\n if (!seen.has(e)) {\n n.root.remove();\n pool.delete(e);\n }\n }\n },\n };\n\n return {\n reflector,\n destroy() {\n pool.clear();\n plane.remove();\n },\n };\n}\n","/**\n * The focus driver — acquisition + programmatic focus for keyboard-claiming\n * widgets (design-007 §2.3–§2.5, petition I1).\n *\n * The single source of truth is BROWSER focus: `document.activeElement` inside\n * a `data-canvas-keyboard` subtree is what makes every engine keyboard surface\n * stand down (input-ownership.ts). This driver's whole job is to make that\n * state reachable — it drives `.focus()`, it never mirrors focus into the\n * world (there is no Focus resource; design-007 §2.2 demoted it, and despawn\n * integrity is free: removing the node resets `activeElement`).\n *\n * Acquisition is DOM-at-event-time, not a world system: the browser's own event\n * targeting IS the pick (the down target sits inside the claiming host — since\n * design-015 D5b that host is screen-space chrome, never a widget under the\n * camera; the desk's one focused editor acquires focus on its own, the same\n * principle). A capture-phase listener keeps ordering deterministic across\n * browsers (Safari's click-focus quirks):\n *\n * - down on a natively-focusable node inside the claim (textarea, canvas\n * with tabindex, the declared proxy itself) → the browser's own mousedown\n * focus is correct; the driver does nothing.\n * - down anywhere else inside the claim → the driver focuses the claim's\n * declared `[data-canvas-focus]` proxy when one exists, else the host\n * (which carries `tabindex=\"-1\"`), with `preventScroll` — and\n * `preventDefault()`s the pointerdown so the native focus fixup cannot\n * fight the choice. Canceling a pointerdown suppresses only the compat\n * mouse events (focus fixup + text selection — the container is already\n * `user-select:none`); `click` still derives and the adapter's container\n * listener has already enqueued the down fact (one input path intact).\n * - down outside any claim → untouched; the browser blurs to body/other\n * content on its own (click-away release, design-007 §2.4).\n *\n * Release stays where the design put it: Escape in the keymap (blur), lifecycle\n * via node removal, window blur deliberately does NOT force-blur (the browser\n * restores focus on refocus — adapter's cancelAllInput keeps its hands off).\n */\nimport type { Entity } from \"@ice/core\";\nimport type { CanvasHost } from \"./host\";\nimport { KEYBOARD_CLAIM_ATTR } from \"./input-ownership\";\n\n/** The widget-declared focus target (a proxy descendant): `<div data-canvas-focus tabIndex={-1}>`. */\nexport const FOCUS_PROXY_ATTR = \"data-canvas-focus\";\n\n/**\n * Natively click-focusable content — where the browser's own mousedown focus\n * already lands inside the widget (the driver must not override it).\n */\nconst FOCUSABLE_SELECTOR =\n 'input, textarea, select, button, a[href], [contenteditable=\"\"], [contenteditable=\"true\"], [tabindex], audio[controls], video[controls], iframe';\n\n/**\n * Resolves an entity to a claiming host's content element — what `focusWidget` walks from. The\n * dom-widgets reflector was the one lookup until design-015 D5b; a host that keeps per-entity\n * DOM (none in the engine today) may still hand one in.\n */\nexport interface FocusHostLookup {\n hostFor(entity: Entity): HTMLElement | undefined;\n}\n\nexport interface WidgetFocusHandle {\n /**\n * Programmatically focus a claiming widget (its proxy, else its host).\n * Returns false when the entity has no host in the lookup (always, without\n * one) or no keyboard claim — only `keyboard: \"exclusive\"` widgets are\n * focusable (declaration drives focusability; design-007 §3.1).\n */\n focusWidget(entity: Entity): boolean;\n /** Blur whatever claim currently holds focus. False when none does. */\n blurFocus(): boolean;\n /** Remove the acquisition listener. */\n detach(): void;\n}\n\n/** The claiming host's focus node: the declared proxy, else the (tabindexed) host. */\nfunction focusNodeOf(claimHost: HTMLElement): HTMLElement {\n const proxy = claimHost.querySelector<HTMLElement>(`[${FOCUS_PROXY_ATTR}]`);\n return proxy ?? claimHost;\n}\n\nexport function attachWidgetFocus(host: CanvasHost, lookup?: FocusHostLookup): WidgetFocusHandle {\n const { container } = host;\n const doc = container.ownerDocument;\n\n const onPointerDown = (e: PointerEvent): void => {\n if (!(e.target instanceof Element)) return;\n const marked = e.target.closest(`[${KEYBOARD_CLAIM_ATTR}]`);\n if (!(marked instanceof HTMLElement) || !container.contains(marked)) return;\n // A natively-focusable node under the pointer (deeper than the host)\n // takes browser focus on its own — the common editable/terminal case.\n const focusable = e.target.closest(FOCUSABLE_SELECTOR);\n if (focusable !== null && focusable !== marked && marked.contains(focusable)) return;\n const node = focusNodeOf(marked);\n if (doc.activeElement === node) return; // already held — never disturb mid-gesture\n e.preventDefault(); // suppress the native focus fixup; the driver owns the choice\n node.focus({ preventScroll: true });\n };\n\n // Capture phase: runs before widget content's own handlers and before the\n // adapter's bubble listener — deterministic acquisition, no races.\n container.addEventListener(\"pointerdown\", onPointerDown, true);\n\n return {\n focusWidget(entity) {\n const content = lookup?.hostFor(entity);\n const hostEl = content?.parentElement;\n if (!(hostEl instanceof HTMLElement) || !hostEl.hasAttribute(KEYBOARD_CLAIM_ATTR)) return false;\n const node = focusNodeOf(hostEl);\n node.focus({ preventScroll: true });\n return doc.activeElement === node;\n },\n blurFocus() {\n const active = doc.activeElement;\n // SVG focus targets included — same rule as the keymap's Escape release.\n if (!(active instanceof HTMLElement || active instanceof SVGElement)) return false;\n if (active.closest(`[${KEYBOARD_CLAIM_ATTR}]`) === null) return false;\n active.blur();\n return true;\n },\n detach() {\n container.removeEventListener(\"pointerdown\", onPointerDown, true);\n },\n };\n}\n","/**\n * `createDeskHost` — the vanilla mount (design-015 §3, D-D15; D5b). What `<InfiniteCanvas>` did\n * for the hybrid, with the hybrid gone: ONE container, the desk's layer in it, and this package's\n * screen-space half around it — the pointer adapter (L0's one producer), the rAF loop, the OS\n * cursor, the room's other people (the remote cursors), the focus driver, the viewport sync.\n * `@ice/react`'s `<Desk>` wraps it; an app without React mounts it directly.\n *\n * THE DESK ARRIVES AS AN OPAQUE LAYER FACTORY. This package never imports `@ice/desk` (the wall\n * `nobody-imports-desk`): the factory's context and its handle are typed STRUCTURALLY here —\n * exactly as `<InfiniteCanvas ground={groundCompose(…)}>` received the ground — and\n * `@ice/desk`'s `deskLayer(opts)` (its src/host/) returns a function assignable to {@link LayerFactory}.\n * The context carries the interaction stack's seams the renderer fills (the frame pick, the nav\n * geometry, the held pose — design-014 B3b, design-015 §8–§9), the marquee buffer it draws, and the\n * stack's exact pick, which the layer's handle answers a host's screen-point pick with (petition I27).\n *\n * Boot order (registration order = reflector flush order): host → layer → reflectors [ layer ·\n * cursor · remoteCursors — unless the layer's handle says `cursors: false` (petition I26: its host\n * draws the room's people itself) ] → pointer adapter → focus → viewport (one layout read, then a\n * ResizeObserver, and the device's ratio read before every step) → rAF loop. `dispose` undoes it in reverse. The engine is NOT owned here — it\n * outlives the mount (the app disposes it). A layer factory that throws leaves nothing claimed on\n * the engine: it runs before any registration, and only the local host is undone.\n */\nimport {\n Viewport,\n writeRuntimeResource,\n type CanvasEngine,\n type EngineGpu,\n type InteractionStack,\n type ReflectorDef,\n type World,\n} from \"@ice/core\";\nimport { screenSizeOf } from \"@ice/kernel\";\nimport { createCanvasHost, type CanvasHost } from \"./host\";\nimport { startRafLoop } from \"./loop\";\nimport { attachPointerAdapter } from \"./pointer-adapter\";\nimport { createCursorReflector } from \"./reflectors/cursor\";\nimport { createRemoteCursorsReflector } from \"./reflectors/remote-cursors\";\nimport { attachWidgetFocus, type WidgetFocusHandle } from \"./widget-focus\";\n\n/**\n * What the host needs of a mounted layer: its drawing reflector and its teardown. The desk's own\n * handle (`@ice/desk`'s `DeskLayerHandle`) carries far more; an app keeps that through its\n * own factory wrapper, or reads it off {@link DeskHost.layer} typed as the factory returned it.\n */\nexport interface LayerHandle {\n readonly reflector: ReflectorDef & { available(): boolean };\n dispose(): void;\n /**\n * The room's other people (petition I26): `false` — the layer's host draws its peers itself (VibeField draws each by face, from\n * `usePresencePeers`), so this host mounts no remote cursors: no plane, no reflector. Absent or `true`, it mounts them, as ever.\n * Read once, when the factory returns; the presence session, the roster and the OS cursor are the same either way.\n */\n readonly cursors?: boolean;\n}\n\n/** The mount context a layer factory receives: the host, the world and the seams the renderer fills. */\nexport interface LayerContext {\n readonly host: CanvasHost;\n readonly world: World;\n /** The frame pick slot (design-014, B3b): the renderer sets its hit test here at mount, clears it at dispose. */\n readonly framePick: InteractionStack[\"framePick\"];\n /**\n * The stack's exact pick out of the tick (petition I27; `InteractionStack.pickAt`): what a press at a screen point (CSS px of the\n * view) would touch now, through the frame pick above — the layer answers a host's screen-point pick (`handle.pick`) with it.\n */\n readonly pickAt: InteractionStack[\"pickAt\"];\n /** The nav geometry slot (design-015 §9, D2b): the renderer's word on its containers' drawn faces. */\n readonly navGeometry: InteractionStack[\"navGeometry\"];\n /** The held pose slot (design-015 §8, D4b): where the object in hand is on screen, as drawn. */\n readonly heldPose: InteractionStack[\"heldPose\"];\n /** The tray pose slot (design-017 §4, K3): where the pegboard drawer is on screen, as drawn. */\n readonly trayPose: InteractionStack[\"trayPose\"];\n /** The presentation transition coordinator: the layer registers the `ground` plane so a nav flight can prepare. */\n readonly transitions: CanvasEngine[\"transitions\"];\n /** The engine's catalog: the object types whose kinds the layer registers. */\n readonly catalog: CanvasEngine[\"catalog\"];\n /** The marquee's preview, out of the ECS (design-003 §5.7): the vellum the marks draw (design-015 D4a). */\n readonly readMarquee: () => InteractionStack[\"marqueeBuffer\"];\n /** The one spatial index (design-015 §2.5; D6): the renderer's cull rides it with a hysteresis margin — no per-entity rect test on a pan. */\n readonly spatial: InteractionStack[\"index\"];\n /** The engine's device (`engine.compositorDevice`), when the app passed one (design-015 D7): the layer draws with it — ONE device per engine. */\n readonly gpu?: EngineGpu;\n /**\n * The frame gate's sleep (K7a, `@ice/core` frame-control.ts): the layer registers when it is next due (`wakeWhen`), wakes a\n * sleeping loop for what it hears outside a step (`wake` — an asset landing, a theme, a pin), and asks whether the step in\n * progress is a registered time's alone (`settled` — then it ticks only what is due).\n */\n readonly frame?: Pick<CanvasEngine[\"engine\"][\"frame\"], \"wake\" | \"wakeWhen\" | \"settled\">;\n /**\n * The engine's ops a layer runs on its host's word (petition I37): the tray's keyboard lay — `ops.layFromTray`, behind the desk\n * handle's `tray.lay`. Absent (a bare host), that door throws.\n */\n readonly ops?: Pick<CanvasEngine[\"ops\"], \"layFromTray\">;\n}\n\nexport type LayerFactory<H extends LayerHandle = LayerHandle> = (ctx: LayerContext) => H;\n\nexport interface DeskHostOptions<H extends LayerHandle = LayerHandle> {\n /** The viewport element; styled, not created, by the host. */\n readonly container: HTMLElement;\n /** A constructed engine (`createCanvasEngine(...)`); NOT disposed by the host. */\n readonly engine: CanvasEngine;\n /** The desk — `deskLayer({ … })` from `@vibecook/ice/desk` (`@ice/desk`'s src/host/), received opaquely. */\n readonly layer: LayerFactory<H>;\n /**\n * The loop SLEEPS when the engine is quiet (K7a — design-015 §11.4: a desk at rest costs the main thread nothing): no frame\n * until an input, a write, an arrival or a registered time wakes it. Default true; false steps every frame, as before K7a.\n */\n readonly sleep?: boolean;\n}\n\nexport interface DeskHost<H extends LayerHandle = LayerHandle> {\n readonly engine: CanvasEngine;\n readonly host: CanvasHost;\n /** The mounted layer's handle, as the factory returned it. */\n readonly layer: H;\n /**\n * The focus driver (design-007 §2.3): `blurFocus()` releases whatever keyboard claim holds\n * focus. Focus is VIEW state (design-007 §2.6) — it rides the mount, not `engine.ops`.\n */\n readonly focus: WidgetFocusHandle;\n /** Stop the loop, detach every adapter, unregister every reflector, dispose the layer and the host. Idempotent. */\n dispose(): void;\n}\n\nexport function createDeskHost<H extends LayerHandle>(opts: DeskHostOptions<H>): DeskHost<H> {\n const { container, engine } = opts;\n const { world, engine: core, stack } = engine;\n const host = createCanvasHost(container);\n\n let layer: H;\n try {\n layer = opts.layer({\n host,\n world,\n framePick: stack.framePick,\n pickAt: stack.pickAt,\n navGeometry: stack.navGeometry,\n heldPose: stack.heldPose,\n trayPose: stack.trayPose,\n transitions: engine.transitions,\n catalog: engine.catalog,\n readMarquee: () => stack.marqueeBuffer,\n spatial: stack.index,\n ...(engine.compositorDevice !== undefined ? { gpu: engine.compositorDevice } : {}),\n frame: core.frame,\n ops: engine.ops,\n });\n } catch (err) {\n host.dispose();\n throw err;\n }\n\n // Registration order = flush order: the desk draws, then the OS cursor, then\n // the room's other people on top (their plane is the container's last child) —\n // none when the layer's host draws them itself (`cursors: false`, petition I26).\n const remoteCursors = layer.cursors === false ? undefined : createRemoteCursorsReflector(host, world);\n const unregister = [\n core.registerReflector(layer.reflector),\n core.registerReflector(createCursorReflector(host, stack.readCursor)),\n ...(remoteCursors !== undefined ? [core.registerReflector(remoteCursors.reflector)] : []),\n ];\n\n // The OS's reduced-motion preference, into the transition coordinator (a nav flight snaps).\n const motionQuery = container.ownerDocument.defaultView?.matchMedia(\"(prefers-reduced-motion: reduce)\");\n const syncReducedMotion = (): void => {\n engine.transitions.setReducedMotion(motionQuery?.matches === true);\n };\n syncReducedMotion();\n motionQuery?.addEventListener(\"change\", syncReducedMotion);\n\n const detachPointer = attachPointerAdapter(host, stack.queue);\n const focus = attachWidgetFocus(host);\n\n const ratio = (): number => (typeof window !== \"undefined\" ? window.devicePixelRatio : 1);\n let synced = 0;\n // THE VIEWPORT IS THE CONTAINER'S LAYOUT SIZE (petition I39): the box the canvas fills, which no CSS transform changes — never its\n // bounding rect, which a transform on it or an ancestor scales and which re-syncs on nothing (no resize, no ratio): a desk mounted\n // under VibeField's recede transform read 1158.85 × 734.60 for a 1180 × 748 container and drew ~2 % small until something resized\n const syncViewport = (): void => {\n const size = screenSizeOf(container.getBoundingClientRect(), container);\n synced = ratio();\n writeRuntimeResource(world, Viewport, { w: size.width, h: size.height, dpr: synced });\n };\n syncViewport();\n let resizeObserver: ResizeObserver | undefined;\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(syncViewport);\n resizeObserver.observe(container);\n }\n\n // THE RATIO (ICE M21 K1): a change of the device's ratio alone — another display, the browser's zoom, an emulated ratio — resizes\n // nothing, so the observer never hears it and the desk kept drawing at the old ratio; an emulated one (DevTools' device mode, CDP)\n // fires not even the `(resolution)` media query (measured: `.matches` flips, no `change` over three rendered frames, no\n // device-pixel box moves). So the ratio is READ before every step — a property read — and the viewport re-synced when it moved.\n const stopLoop = startRafLoop(\n core,\n () => {\n if (ratio() !== synced) syncViewport();\n },\n { sleep: opts.sleep !== false },\n );\n // …and while the loop SLEEPS (K7a) no step reads it: the ratio is read twice a second instead — a property read, and the\n // viewport write wakes the loop through the world's doors when it moved\n const ratioPoll = opts.sleep !== false ? setInterval(() => { if (ratio() !== synced) syncViewport(); }, 500) : undefined;\n\n let disposed = false;\n return {\n engine,\n host,\n layer,\n focus,\n dispose() {\n if (disposed) return;\n disposed = true;\n stopLoop();\n if (ratioPoll !== undefined) clearInterval(ratioPoll);\n resizeObserver?.disconnect();\n focus.detach();\n detachPointer();\n motionQuery?.removeEventListener(\"change\", syncReducedMotion);\n engine.transitions.setReducedMotion(false);\n for (const unreg of unregister) unreg();\n remoteCursors?.destroy();\n layer.dispose();\n host.dispose();\n },\n };\n}\n","/**\n * @ice/dom — SCREEN SPACE ONLY (design-015 §3, D-D15; D5b): the canvas host, the pointer\n * adapter (events → queue ONLY), the rAF loop, input ownership + the editor's focus, the OS\n * cursor + the room's remote cursors, and `createDeskHost` — the vanilla mount that takes the\n * desk as an opaque layer factory.\n * Import wall: @ice/core + @ice/kernel down only — never react/three/desk (enforced).\n *\n * GONE at D5b — the WORLD-SPACE half (design-015 §1): the content and lifted planes, the\n * plane-transform reflector, the DOM widget hosts + writeback, the L1 source canvas\n * (`layoutsubtree`), the Widget Surface contract, measurement, the graybox and the P4 chrome\n * reflectors, and the pointer adapter's GL route. No DOM under the camera (DK-D2).\n */\nexport const DOM_VERSION = \"0.0.0\";\n\n// The canvas host (design-004 §1): the styled container — no planes.\nexport { createCanvasHost, type CanvasHost } from \"./host\";\n\n// The vanilla mount (design-015 §3): the desk's layer, the adapters, the reflectors, the loop.\nexport {\n createDeskHost,\n type DeskHost,\n type DeskHostOptions,\n type LayerContext,\n type LayerFactory,\n type LayerHandle,\n} from \"./desk-host\";\n\n// The rAF frame loop (design-002 §1: the platform owns the loop).\nexport { startRafLoop } from \"./loop\";\n\n// The L0 pointer adapter (design-003 §2–§3): DOM events → InputQueue, nothing more.\nexport { attachPointerAdapter } from \"./pointer-adapter\";\n\n// Input-ownership predicates (design-007 §4, petitions I1/I4): the shared\n// per-surface guard family — the keymap (@ice/react) reads these too.\nexport {\n CLAIM_OWNS_ESCAPE,\n KEYBOARD_CLAIM_ATTR,\n isEditableTarget,\n keyboardClaimOf,\n wheelCede,\n type KeyboardClaim,\n} from \"./input-ownership\";\n\n// The focus driver (design-007 §2.3–§2.5): click-to-focus acquisition for a\n// keyboard-claiming host + the programmatic blurFocus/focusWidget handle.\n// Focus is VIEW state (design-007 §2.6) — it lives here, not in core.\nexport {\n FOCUS_PROXY_ATTR,\n attachWidgetFocus,\n type FocusHostLookup,\n type WidgetFocusHandle,\n} from \"./widget-focus\";\n\n// L4 cursor projection output (design-003 §7: local cursor = OS cursor, one write).\nexport { createCursorReflector } from \"./reflectors/cursor\";\n// P5 remote cursors (design-004 §1: screen-space pooled nodes; M9 presence) — a room's other people.\nexport { createRemoteCursorsReflector, type RemoteCursorsReflector } from \"./reflectors/remote-cursors\";\n"],"mappings":"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","names":[]}
@@ -78,7 +78,7 @@ import {
78
78
  spring,
79
79
  themeFrom,
80
80
  tilted
81
- } from "./chunk-OHEHS2EM.js";
81
+ } from "./chunk-US7WJNR6.js";
82
82
  import {
83
83
  BLEND_OVER,
84
84
  bindGroup,
@@ -140,6 +140,7 @@ import {
140
140
  defineQuery,
141
141
  defineWidget,
142
142
  departedCameraOf,
143
+ focusTray,
143
144
  frameParent,
144
145
  gateVerdict,
145
146
  heldEntity,
@@ -154,14 +155,16 @@ import {
154
155
  trayCategory,
155
156
  trayEntity,
156
157
  trayEntryCount,
158
+ trayFocus,
157
159
  trayOpen,
158
160
  trayTakeProps,
159
161
  widgetSpawnInits,
160
162
  widgetTypeFor
161
- } from "./chunk-HSIFKQNP.js";
163
+ } from "./chunk-SPG45AVV.js";
162
164
  import {
163
165
  clientToScreen,
164
166
  cubicBezierEase,
167
+ screenToWorld,
165
168
  specimenFit,
166
169
  trayScrollMax
167
170
  } from "./chunk-RE4GDHFJ.js";
@@ -1569,6 +1572,8 @@ var MarksUniformsStruct = defineStruct("MarksUniforms", [
1569
1572
  ]);
1570
1573
  var MARK = { stroke: 0, fill: 1, brackets: 2, segment: 3, bar: 4, flare: 5, glyph: 6, tape: 7 };
1571
1574
  var LIGHT = 1;
1575
+ var NO_MARKS = (view, night = false) => ({ view, night, tape: [], objects: [], union: null, marquee: null, guides: [], bars: [], strike: 0, ruler: null });
1576
+ var unselectedMarks = (m) => ({ ...NO_MARKS(m.view, m.night), tape: m.tape });
1572
1577
  var I = MARKS.inks;
1573
1578
  var INK = {
1574
1579
  pencil: cssColor(I.pencil.css),
@@ -3366,6 +3371,7 @@ var captureNumber = (what, v, positive2) => {
3366
3371
  };
3367
3372
  function checkCapture(opts) {
3368
3373
  captureNumber("scale", opts.scale ?? 1, true);
3374
+ if (opts.marks !== void 0 && typeof opts.marks !== "boolean") throw new Error(`capture: marks must be a boolean (got ${String(opts.marks)})`);
3369
3375
  const r = opts.rect;
3370
3376
  if (r === void 0) return;
3371
3377
  captureNumber("rect.x", r.x, false);
@@ -3388,7 +3394,8 @@ function scaledInputs(inputs, scale) {
3388
3394
  }
3389
3395
  async function captureFrame(device, passes, format, inputs, cache, opts = {}) {
3390
3396
  checkCapture(opts);
3391
- const frame = scaledInputs(inputs, opts.scale ?? 1);
3397
+ const marked = opts.marks === false && inputs.marks !== void 0 ? { ...inputs, marks: unselectedMarks(inputs.marks) } : inputs;
3398
+ const frame = scaledInputs(marked, opts.scale ?? 1);
3392
3399
  const dpr = frame.view.dpr;
3393
3400
  const size = { w: Math.max(1, Math.round(frame.view.width * dpr)), h: Math.max(1, Math.round(frame.view.height * dpr)) };
3394
3401
  let box2 = { x: 0, y: 0, w: size.w, h: size.h };
@@ -5588,6 +5595,7 @@ function createDeskReflector(opts) {
5588
5595
  let trayDrawn = /* @__PURE__ */ new Set();
5589
5596
  const faced = /* @__PURE__ */ new Map();
5590
5597
  let trayShownAt = Number.NEGATIVE_INFINITY;
5598
+ let trayFocusSeen = "";
5591
5599
  let facedAt = Number.POSITIVE_INFINITY;
5592
5600
  let builderWakes = builder.wakes();
5593
5601
  const pointerNdc = (vp) => {
@@ -5671,6 +5679,12 @@ function createDeskReflector(opts) {
5671
5679
  dirty = true;
5672
5680
  wakes.tray += 1;
5673
5681
  }
5682
+ const focus = tf?.focus ?? "";
5683
+ if (focus !== trayFocusSeen) {
5684
+ trayFocusSeen = focus;
5685
+ dirty = true;
5686
+ wakes.tray += 1;
5687
+ }
5674
5688
  const pinned = tray.pinned();
5675
5689
  const showing = pinned !== null ? pinned.hidden !== true && (pinned.p ?? 1) > 0 : tf?.open === true || (tray.frame()?.p ?? 0) > 0;
5676
5690
  if (showing) trayShownAt = now;
@@ -7361,6 +7375,12 @@ function deskLayer(opts) {
7361
7375
  published = key;
7362
7376
  for (const l of [...listeners]) l();
7363
7377
  };
7378
+ const focusedOf = () => {
7379
+ const id3 = trayFocus(world);
7380
+ if (id3 === "") return null;
7381
+ const seen = compose2.traySpecimens().find((f) => f.type === id3);
7382
+ return { id: id3, rect: seen === void 0 ? null : { x0: seen.object.x0, y0: seen.object.y0, x1: seen.object.x1, y1: seen.object.y1 } };
7383
+ };
7364
7384
  const trayAnchorOf = () => {
7365
7385
  const f = compose2.tray.frame();
7366
7386
  const vp = world.getResource(Viewport);
@@ -7373,7 +7393,8 @@ function deskLayer(opts) {
7373
7393
  held: h !== void 0 && !h.landing,
7374
7394
  entries: trayEntryCount(world),
7375
7395
  category: trayCategory(world),
7376
- categories: trayCategories(world)
7396
+ categories: trayCategories(world),
7397
+ focused: focusedOf()
7377
7398
  };
7378
7399
  };
7379
7400
  const trayListeners = /* @__PURE__ */ new Set();
@@ -7671,7 +7692,7 @@ function deskLayer(opts) {
7671
7692
  };
7672
7693
  let capturing = null;
7673
7694
  let captures = Promise.resolve();
7674
- const captureKey = (o) => JSON.stringify([o.scale ?? 1, o.rect === void 0 ? null : [o.rect.x, o.rect.y, o.rect.width, o.rect.height]]);
7695
+ const captureKey = (o) => JSON.stringify([o.scale ?? 1, o.rect === void 0 ? null : [o.rect.x, o.rect.y, o.rect.width, o.rect.height], o.marks !== false]);
7675
7696
  const capture = (opts2 = {}) => {
7676
7697
  checkCapture(opts2);
7677
7698
  const key = captureKey(opts2);
@@ -7908,6 +7929,22 @@ function deskLayer(opts) {
7908
7929
  return () => {
7909
7930
  trayListeners.delete(listener);
7910
7931
  };
7932
+ },
7933
+ focus: (to) => focusTray(world, to),
7934
+ lay(id3, o = {}) {
7935
+ const ops = ctx.ops;
7936
+ if (ops === void 0) throw new Error("[ice] desk: tray.lay \u2014 the mount context has no engine ops (`createDeskHost` hands them: `LayerContext.ops`)");
7937
+ const space = o.space ?? "screen";
7938
+ if (space !== "screen" && space !== "world") throw new Error(`[ice] desk: tray.lay \u2014 space is "screen" or "world" (got ${String(space)})`);
7939
+ const vp = world.getResource(Viewport);
7940
+ const point = o.at ?? { x: (vp?.w ?? 0) / 2, y: (vp?.h ?? 0) / 2 };
7941
+ if (typeof point?.x !== "number" || typeof point.y !== "number" || !Number.isFinite(point.x) || !Number.isFinite(point.y)) {
7942
+ throw new Error(`[ice] desk: tray.lay \u2014 the point is ${JSON.stringify(point) ?? String(point)}: { x, y }, two finite numbers`);
7943
+ }
7944
+ const type = id3 ?? trayFocus(world);
7945
+ if (type === "") return void 0;
7946
+ const cam = world.getResource(Camera) ?? { x: 0, y: 0, zoom: 1 };
7947
+ return ops.layFromTray(type, space === "world" ? point : screenToWorld(point.x, point.y, cam));
7911
7948
  }
7912
7949
  },
7913
7950
  dispose() {
@@ -8245,6 +8282,9 @@ function createStill(opts) {
8245
8282
  if (!THEMES.includes(theme)) throw new Error(`createStill: theme must be "light" or "dark" (got ${String(theme)})`);
8246
8283
  const cam = opts.camera === void 0 ? { x: -width / 2, y: -height / 2, zoom: 1 } : { x: finite("camera.x", opts.camera.x), y: finite("camera.y", opts.camera.y), zoom: positive("camera.zoom", opts.camera.zoom) };
8247
8284
  if (typeof opts.stage !== "function") throw new Error("createStill: `stage` must be a function \u2014 it lays the still's world");
8285
+ if (opts.services !== void 0 && (!Array.isArray(opts.services) || !opts.services.every((s) => typeof s?.key?.name === "string"))) {
8286
+ throw new Error("createStill: `services` must be a list of lent services \u2014 `service(key, value)` entries, as `deskLayer({ services })` takes");
8287
+ }
8248
8288
  const plan = { width, height, dpr, kinds, theme, palette: opts.palette ?? STILL_PALETTE, cam };
8249
8289
  return inTurn(opts.device, () => drawStill(opts, plan));
8250
8290
  }
@@ -8257,6 +8297,7 @@ async function drawStill(opts, plan) {
8257
8297
  } });
8258
8298
  let engine;
8259
8299
  let builder;
8300
+ const locals = /* @__PURE__ */ new Map();
8260
8301
  try {
8261
8302
  const refused = desk.root.faults?.[0];
8262
8303
  if (refused !== void 0) throw new Error(`createStill: the kind "${refused.kind}" was ${refused.reason}`);
@@ -8268,12 +8309,27 @@ async function drawStill(opts, plan) {
8268
8309
  const world = engine.world;
8269
8310
  world.setResource(Viewport, { w: width, h: height, dpr });
8270
8311
  world.setResource(Camera, { x: cam.x, y: cam.y, zoom: cam.zoom, gesturing: false });
8271
- const b = createDeskBuilder(world, { objects: [...opts.objects] });
8312
+ if (opts.services !== void 0) {
8313
+ const services = createServices(opts.services, "the still's caller");
8314
+ for (const t of opts.objects) {
8315
+ const lend = hostOf(t)?.lend;
8316
+ if (lend !== void 0) services.lend(lend({ use: services.use, wake: () => {
8317
+ } }), `the object "${t.type}"`);
8318
+ }
8319
+ const children = worldChildren(world);
8320
+ for (const k of plan.kinds) {
8321
+ const local = k.local?.({ pass: () => desk.root.kinds.get(k.name)?.pass, use: services.use, children, wake: () => {
8322
+ } });
8323
+ if (local !== void 0) locals.set(k.name, local);
8324
+ }
8325
+ }
8326
+ const b = createDeskBuilder(world, { objects: [...opts.objects], ...locals.size > 0 ? { locals } : {} });
8272
8327
  builder = b;
8273
8328
  await opts.stage({ engine, world, pinAsset: (e, asset) => b.pin(e, asset), pinFlux: (e, targets) => b.pinFlux(e, targets) });
8274
8329
  engine.step(16);
8275
8330
  engine.step(32);
8276
8331
  b.changed();
8332
+ for (const local of locals.values()) local.tick?.(0);
8277
8333
  const built = b.build(cam, { width, height, dpr }, 0, theme, DEFAULT_GRID, looks, { now: 0 });
8278
8334
  const inputs = {
8279
8335
  view: { camX: cam.x, camY: cam.y, zoom: cam.zoom, width, height, dpr },
@@ -8310,6 +8366,7 @@ async function drawStill(opts, plan) {
8310
8366
  return { width: shot.width, height: shot.height, rgba, dispose() {
8311
8367
  } };
8312
8368
  } finally {
8369
+ for (const local of locals.values()) local.dispose?.();
8313
8370
  builder?.dispose();
8314
8371
  engine?.dispose();
8315
8372
  desk.dispose();
@@ -8418,4 +8475,4 @@ export {
8418
8475
  STILL_PALETTE,
8419
8476
  createStill
8420
8477
  };
8421
- //# sourceMappingURL=chunk-5QVYKU64.js.map
8478
+ //# sourceMappingURL=chunk-A6PUTIL7.js.map