@vibecook/ice 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/{chunk-IASTQMU5.js → chunk-3PHPWRB2.js} +146 -11
  2. package/dist/chunk-3PHPWRB2.js.map +1 -0
  3. package/dist/{chunk-DXU3RZZS.js → chunk-7LZAXQAO.js} +649 -261
  4. package/dist/chunk-7LZAXQAO.js.map +1 -0
  5. package/dist/{chunk-5SJYHQI4.js → chunk-ENMRSITH.js} +38 -11
  6. package/dist/chunk-ENMRSITH.js.map +1 -0
  7. package/dist/devtools.js +1 -1
  8. package/dist/dom.js +16 -2
  9. package/dist/ground.js +1 -1
  10. package/dist/index.js +17 -1
  11. package/dist/r3f.js +9 -3
  12. package/dist/r3f.js.map +1 -1
  13. package/dist/react.js +5 -3
  14. package/dist/types/packages/core/src/catalog/camera-derived.d.ts +26 -2
  15. package/dist/types/packages/core/src/catalog/gesture.d.ts +1 -1
  16. package/dist/types/packages/core/src/catalog/pointer.d.ts +11 -0
  17. package/dist/types/packages/core/src/catalog/selection-presence.d.ts +10 -0
  18. package/dist/types/packages/core/src/doc/autosave.d.ts +34 -0
  19. package/dist/types/packages/core/src/doc/rename-sweep.d.ts +3 -0
  20. package/dist/types/packages/core/src/doc/rename.d.ts +7 -0
  21. package/dist/types/packages/core/src/doc/version-gate.d.ts +14 -1
  22. package/dist/types/packages/core/src/engine/engine.d.ts +11 -0
  23. package/dist/types/packages/core/src/engine/frame-control.d.ts +71 -0
  24. package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +9 -0
  25. package/dist/types/packages/core/src/index.d.ts +3 -1
  26. package/dist/types/packages/core/src/input/queue.d.ts +9 -0
  27. package/dist/types/packages/core/src/interaction/gesture-status.d.ts +13 -0
  28. package/dist/types/packages/core/src/schema/meta.d.ts +12 -0
  29. package/dist/types/packages/core/src/widget/define-widget.d.ts +61 -0
  30. package/dist/types/packages/dom/src/index.d.ts +2 -0
  31. package/dist/types/packages/dom/src/input-ownership.d.ts +52 -0
  32. package/dist/types/packages/dom/src/loop.d.ts +11 -0
  33. package/dist/types/packages/dom/src/pointer-adapter.d.ts +16 -3
  34. package/dist/types/packages/dom/src/widget-focus.d.ts +58 -0
  35. package/dist/types/packages/react/src/engine-context.d.ts +13 -0
  36. package/dist/types/packages/react/src/index.d.ts +1 -1
  37. package/dist/types/packages/react/src/infinite-canvas.d.ts +20 -2
  38. package/dist/types/packages/react/src/keymap.d.ts +23 -4
  39. package/package.json +4 -4
  40. package/dist/chunk-5SJYHQI4.js.map +0 -1
  41. package/dist/chunk-DXU3RZZS.js.map +0 -1
  42. package/dist/chunk-IASTQMU5.js.map +0 -1
package/dist/react.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  useBreakpoint,
14
14
  useCanvasEngine,
15
15
  useCommit,
16
+ useFrameFreeze,
16
17
  useOps,
17
18
  usePresencePeers,
18
19
  useSelected,
@@ -23,9 +24,9 @@ import {
23
24
  useWidgetProps,
24
25
  useWorld,
25
26
  useWorldComponent
26
- } from "./chunk-5SJYHQI4.js";
27
- import "./chunk-IASTQMU5.js";
28
- import "./chunk-DXU3RZZS.js";
27
+ } from "./chunk-ENMRSITH.js";
28
+ import "./chunk-3PHPWRB2.js";
29
+ import "./chunk-7LZAXQAO.js";
29
30
  import "./chunk-OR5FNVUE.js";
30
31
  export {
31
32
  EngineProvider,
@@ -42,6 +43,7 @@ export {
42
43
  useBreakpoint,
43
44
  useCanvasEngine,
44
45
  useCommit,
46
+ useFrameFreeze,
45
47
  useOps,
46
48
  usePresencePeers,
47
49
  useSelected,
@@ -39,12 +39,36 @@ export declare const ActiveTool: import("@vibecook/strata-ecs").Resource<import(
39
39
  * NOT a full step-pause: a menu panel floats over a still-visible canvas,
40
40
  * and a frozen world would leave undo/collab visually stale (rev-2 decision,
41
41
  * recorded — a stricter frozen-world mode would be a separate concept).
42
+ * That separate concept is now BUILT (2026-08-04): see {@link FrameMode}.
43
+ * Holds keep their exact meaning — nothing here stretched.
42
44
  */
43
45
  export declare const StageMode: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
44
46
  readonly backgroundHolds: import("@vibecook/strata-ecs").FieldSpec<"u32", true>;
45
47
  }>, {
46
48
  readonly backgroundHolds: import("@vibecook/strata-ecs").FieldSpec<"u32", true>;
47
49
  }>;
50
+ /**
51
+ * Frame gate mirror (2026-08-04) — the stricter sibling {@link StageMode}'s
52
+ * rev-2 decision named and left for later.
53
+ *
54
+ * A background hold is presentation policy over a LIVE world. A freeze
55
+ * (`engine.frame.freeze(name)`) stops the host loop outright: no step, so no
56
+ * systems, no publish, no notify, no reflectors, no rAF — for chrome that
57
+ * COVERS the canvas, where every frame drawn behind it is work nobody can see.
58
+ * This resource is the world-truth mirror of that refcount; the tokens live on
59
+ * the control, out of ECS, exactly as stage holds do.
60
+ *
61
+ * Reading it tells you the engine is frozen. It cannot tell you a frame is
62
+ * coming, because while the count is non-zero none is — and for the same
63
+ * reason nothing can OBSERVE it while frozen (a parked world fires no
64
+ * observers). It exists for inspection: devtools, tests, and consumers that
65
+ * want the fact in hand on thaw.
66
+ */
67
+ export declare const FrameMode: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
68
+ readonly freezeHolds: import("@vibecook/strata-ecs").FieldSpec<"u32", true>;
69
+ }>, {
70
+ readonly freezeHolds: import("@vibecook/strata-ecs").FieldSpec<"u32", true>;
71
+ }>;
48
72
  /** Global snap toggle + threshold in screen px (scaled by zoom at use, design-003 §5.2). */
49
73
  export declare const SnapConfig: import("@vibecook/strata-ecs").Resource<import("@vibecook/strata-ecs").ValueOf<{
50
74
  readonly enabled: import("@vibecook/strata-ecs").FieldSpec<"bool", true>;
@@ -101,9 +125,9 @@ export declare const SelectionBox: import("@vibecook/strata-ecs").Component<impo
101
125
  }>;
102
126
  /** A resize handle's anchor (the one chrome kind behaviors capture). */
103
127
  export declare const HandleSpec: import("@vibecook/strata-ecs").Component<import("@vibecook/strata-ecs").ValueOf<{
104
- readonly anchor: import("@vibecook/strata-ecs").EnumType<"s" | "w" | "n" | "e" | "nw" | "ne" | "se" | "sw">;
128
+ readonly anchor: import("@vibecook/strata-ecs").EnumType<"s" | "w" | "nw" | "n" | "ne" | "e" | "se" | "sw">;
105
129
  }>, {
106
- readonly anchor: import("@vibecook/strata-ecs").EnumType<"s" | "w" | "n" | "e" | "nw" | "ne" | "se" | "sw">;
130
+ readonly anchor: import("@vibecook/strata-ecs").EnumType<"s" | "w" | "nw" | "n" | "ne" | "e" | "se" | "sw">;
107
131
  }>;
108
132
  /** A snap guide line along one axis. `from == to` ⇒ full-viewport line (v1's full-canvas alignment look — design-003 §6). */
109
133
  export declare const GuideLine: import("@vibecook/strata-ecs").Component<import("@vibecook/strata-ecs").ValueOf<{
@@ -115,7 +115,7 @@ export declare const WheelZoom: import("@vibecook/strata-ecs").Component<import(
115
115
  * `Sequence`/`RequiresFail` edge. NEVER reaped; exempt from integrity's watched-pointer
116
116
  * count (a pending multi-tap legitimately outlives its lifted touch pointer).
117
117
  */
118
- export declare const GesturePhases: import("..").PhaseSet<"Possible" | "Active" | "Pending" | "Recognized" | "Ended" | "Failed" | "Cancelled">;
118
+ export declare const GesturePhases: import("..").PhaseSet<"Active" | "Possible" | "Pending" | "Recognized" | "Ended" | "Failed" | "Cancelled">;
119
119
  /** Recognizer alive, not yet claiming. */
120
120
  export declare const GesturePossible: import("@vibecook/strata-ecs").Tag;
121
121
  /** Continuous kinds' claiming phase + per-frame work gate. */
@@ -66,6 +66,17 @@ export declare const WentUp: import("@vibecook/strata-ecs").Tag;
66
66
  export declare const WentCancelled: import("@vibecook/strata-ecs").Tag;
67
67
  /** The down was consumed by a widget's own DOM handler — recognizers skip it. */
68
68
  export declare const HandledByWidget: import("@vibecook/strata-ecs").Tag;
69
+ /**
70
+ * ONE-TICK: this tick's wheel deltas were ceded to scrollable widget content
71
+ * (design-007 §3.5, petition I4) — the adapter let native scroll proceed and
72
+ * flagged the fact `wheelHandled`. Deliberately NOT the shared `HandledByWidget`
73
+ * latch: that tag rides the persistent mouse-pointer entity, and a same-tick
74
+ * wheel-over-widget + down-over-canvas collision would silently drop the
75
+ * legitimate canvas gesture (design-007 F6). Both wheel consumers honor it —
76
+ * `wheelSpawn` skips the tick, `wheelSystem` treats it as wheel silence.
77
+ * Cleared in cleanup like the Went* transition tags.
78
+ */
79
+ export declare const WheelHandled: import("@vibecook/strata-ecs").Tag;
69
80
  /**
70
81
  * PERSISTENT (not one-tick): the pointer is hovering content that would opt a
71
82
  * down out — a native interactive / `[data-canvas-interactive]` (DOM) or
@@ -12,6 +12,16 @@ export declare const SnapTarget: import("@vibecook/strata-ecs").Tag;
12
12
  * path). Drag a member out and it simply stops being inside.
13
13
  */
14
14
  export declare const SweepsContained: import("@vibecook/strata-ecs").Tag;
15
+ /**
16
+ * `interaction.keyboard: "exclusive"` (design-007 §3.1, petitions I1/I4): while
17
+ * a node inside this widget holds browser focus, the engine's keyboard surfaces
18
+ * (keymap shortcuts, the adapter's Space pan modifier) stand down. The ROUTING
19
+ * truth is the DOM — the dom-widgets reflector writes a `data-canvas-keyboard`
20
+ * marker on the widget host and the keymap/adapter read the event-target chain
21
+ * against `document.activeElement`; this tag is the ECS-side mirror of the
22
+ * declaration (queryable by devtools/presence), never a routing input.
23
+ */
24
+ export declare const KeyboardExclusive: import("@vibecook/strata-ecs").Tag;
15
25
  /**
16
26
  * Marks an ephemeral presence-peer entity (design-001 §5.6). Rides the presence
17
27
  * blob so remote peers project it — a peer query is `[PresencePeer, Not(Local)]`
@@ -1,3 +1,37 @@
1
+ /**
2
+ * The autosave kit (design-005 §6.6; design-001 §6.5 envelope-at-rest).
3
+ *
4
+ * Persists a `DocSession` to app-owned storage on a debounce (default 800 ms)
5
+ * with a max-wait ceiling (default 10 s), DEFERRED while the user is mid-gesture
6
+ * so a save never captures a half-applied interaction. What it writes is an
7
+ * ENVELOPE (`session.exportEnvelope(savedAt)`) — never raw Loro bytes — so the
8
+ * restore path can read the header and gate/quarantine BEFORE any import.
9
+ *
10
+ * CHANGE-SUBSCRIPTION SURFACE: two triggers, plus an initial arm.
11
+ * - `DurableStore.subscribeOutbound` (durable-store.ts) fires SYNCHRONOUSLY on
12
+ * each sealed LOCAL commit and never on remote imports.
13
+ * - `DocSession.subscribeRemote` (doc-kit) fires after each successful
14
+ * `applyRemote`. The original design left remote changes to "whichever peer
15
+ * originated them" — but that peer persists to ITS storage, not this one's:
16
+ * a joiner that received edits and closed restored a STALE local backup
17
+ * (2026-07-13 review). Local autosave must reflect the last-seen document.
18
+ * - Autosave starts DIRTY: state that predates startAutosave() (a seeded or
19
+ * freshly-joined doc) reaches storage after the first debounce, not only
20
+ * after the next edit (same review — a seeded doc could close unsaved).
21
+ * No polling — the design's `store.exportSnapshot()` length/frontier poll is
22
+ * the last-resort fallback and is not needed here.
23
+ *
24
+ * Clock and scheduler are injectable so tests are deterministic with NO real
25
+ * timers; the design's `requestIdleCallback` optimization is just an alternate
26
+ * `schedule` an app can pass. `flush()` forces an awaited save; `close()`
27
+ * captures and durably serializes the final revision; `stop()` explicitly
28
+ * discards pending state.
29
+ *
30
+ * RESTORE (`restoreAutosave`): reads storage → `openDocSession`. Incompatible or
31
+ * corrupt bytes NEVER throw and NEVER brick boot (the M5 exit criterion) —
32
+ * they come back as `{ status: "quarantined", reason, bytes }` so the app can
33
+ * stash the bytes aside and start fresh.
34
+ */
1
35
  import type { World } from "@vibecook/strata-ecs";
2
36
  import { type DocSession, type DocSessionOpts } from "./doc-kit";
3
37
  import type { DocVersionReport } from "./version-gate";
@@ -0,0 +1,3 @@
1
+ import type { World } from "@vibecook/strata-ecs";
2
+ import type { DurableStore } from "@vibecook/strata-ecs/durable";
3
+ export declare function armRenameSweep(world: World, store: DurableStore): () => void;
@@ -0,0 +1,7 @@
1
+ import type { MigrationCtx } from "./migrate";
2
+ import type { DocVersionReport } from "./version-gate";
3
+ export interface RenameOutcome {
4
+ /** Old ids folded and tombstoned this open (marker-only docs still stamp). */
5
+ readonly renamed: string[];
6
+ }
7
+ export declare function runRenames(ctx: MigrationCtx, report: DocVersionReport): RenameOutcome;
@@ -36,6 +36,14 @@ export interface DocVersionReport {
36
36
  readonly newerInDoc: readonly string[];
37
37
  /** Prefab ids whose doc marker is OLDER than local (M9 migration input). */
38
38
  readonly olderInDoc: readonly string[];
39
+ /**
40
+ * Old ids with a REGISTERED rename, present in the doc and not yet
41
+ * tombstoned → the doc pack version their markers carry (design-008 §4).
42
+ * The rename runner's exact input; non-empty forces at least "migrate".
43
+ * Meta has no delete, so a stale old marker without this classification
44
+ * would brick the doc `readOnly` via `newerInDoc` — the I5 failure mode.
45
+ */
46
+ readonly renamedInDoc: Readonly<Record<string, number>>;
39
47
  }
40
48
  export type GateVerdict = "ok" | "readOnly" | "migrate" | "reject";
41
49
  /**
@@ -46,7 +54,12 @@ export type GateVerdict = "ok" | "readOnly" | "migrate" | "reject";
46
54
  export declare function stampEngineMeta(store: DurableStore): void;
47
55
  /** Read the marker keys off a raw (imported, unattached) doc and compare. */
48
56
  export declare function readDocVersionReport(doc: LoroDoc): DocVersionReport;
49
- /** Build the same comparison report from an envelope header, before Loro import. */
57
+ /**
58
+ * Build the same comparison report from an envelope header, before Loro
59
+ * import. The header carries no meta, so no tombstone knowledge here —
60
+ * registry aliasing only. This preflight is coarse by design; the
61
+ * authoritative in-document gate re-runs right after import.
62
+ */
50
63
  export declare function readEnvelopeVersionReport(header: EnvelopeHeader): DocVersionReport;
51
64
  /** The default M5 policy. A facade policy hook may override (design-005 §6.3). */
52
65
  export declare function gateVerdict(report: DocVersionReport): GateVerdict;
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import type { World } from "@vibecook/strata-ecs";
23
23
  import type { AnySystem } from "./pipeline";
24
+ import { type FrameControl } from "./frame-control";
24
25
  import { type PhaseGroup } from "./pipeline";
25
26
  import { type ReflectorDef } from "./reflectors";
26
27
  export type PublishHook = (world: World) => void;
@@ -54,6 +55,16 @@ export interface Engine {
54
55
  onPublish(hook: PublishHook): () => void;
55
56
  /** Run one full frame. `now` in ms (rAF timestamp or a test counter). */
56
57
  step(now: number): void;
58
+ /**
59
+ * The frame gate (2026-08-04): freeze/thaw plus the settle protocol a host
60
+ * loop consults before each frame. It lives HERE, next to the step it
61
+ * governs, so every host gets it without a new argument — and so a
62
+ * subsystem holding only the `Engine` (the GL compositor) can report itself
63
+ * busy. `step()` itself stays unconditional: the gate governs the loop, not
64
+ * the primitive, which keeps headless hosts and traces driving frames by
65
+ * hand exactly as before.
66
+ */
67
+ readonly frame: FrameControl;
57
68
  /** Arm per-system run/skip + µs collection (idempotent; costs timer calls per system). */
58
69
  enableTelemetry(): void;
59
70
  /** The most recent completed frame's readout (undefined until telemetry armed + one step). */
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The frame gate (design-002 §1 amendment, 2026-08-04) — the frozen-world mode
3
+ * design-005 §7 named and deliberately left unbuilt.
4
+ *
5
+ * Stage holds are PRESENTATION policy: `StageMode.backgroundHolds` freezes the
6
+ * compositor on its retained textures while the world keeps stepping, which is
7
+ * exactly what makes a hold safe under a PARTIAL overlay — a menu panel floats
8
+ * over a still-live canvas, undo reflects, collab applies, tweens land. The
9
+ * rev-2 decision recorded that a stricter mode "would be a SEPARATE concept".
10
+ * This is that concept: while a freeze is held the host loop stops calling
11
+ * `step` at all — no systems, no publish, no notify, no reflectors, no rAF
12
+ * callback. It is for chrome that COVERS the canvas outright, where every
13
+ * frame drawn behind it is work nobody can see.
14
+ *
15
+ * THE SETTLE — why a freeze does not park on the spot. Parking the instant a
16
+ * freeze is taken would strand the canvas mid-churn: a cancelled gesture that
17
+ * has not reached its commit tick, islands still owed a first paint, a
18
+ * reflector holding a write that landed this frame. So a freeze walks the loop
19
+ * to QUIET — every registered settle reporter idle — then takes ONE further
20
+ * step so the quiet state itself reflects, and only then closes the gate. The
21
+ * walk is bounded by {@link SETTLE_CAP}: a reporter that never goes quiet makes
22
+ * a freeze park LATE, never never.
23
+ *
24
+ * Core stays scheduler-free (design-002 §1). This module owns no timer and
25
+ * starts nothing — it answers {@link FrameControl.claimStep} and announces
26
+ * transitions. The DOM package's rAF loop is what actually parks and restarts;
27
+ * `engine.step(now)` itself stays callable by hand (headless hosts, traces),
28
+ * because the gate governs the LOOP, not the primitive.
29
+ */
30
+ import type { World } from "@vibecook/strata-ecs";
31
+ /**
32
+ * Frames a freeze may spend walking to quiet before it parks regardless.
33
+ * Two seconds at 60 Hz: comfortably longer than a cancelled gesture's path to
34
+ * its commit tick or a cold board's first paints, short enough that a wedged
35
+ * reporter reads as a hiccup rather than a hang.
36
+ */
37
+ export declare const SETTLE_CAP = 120;
38
+ export interface FrameControl {
39
+ /**
40
+ * Take a named freeze. Returns an IDEMPOTENT thaw — the disposer pattern
41
+ * that makes overlay lifecycles leak-proof (release on unmount), the name
42
+ * that makes "why is the engine parked?" answerable. Refcounted: the loop
43
+ * resumes when the LAST freeze thaws.
44
+ */
45
+ freeze(name: string): () => void;
46
+ /** A freeze is held — the loop is settling or already parked. */
47
+ isFrozen(): boolean;
48
+ /** The settle finished and the gate closed: no further steps are being run. */
49
+ isParked(): boolean;
50
+ /** Live freeze names, in take order (debug/devtools). */
51
+ holds(): readonly string[];
52
+ /**
53
+ * Register a settle reporter: while `busy()` is true, a taken freeze keeps
54
+ * stepping instead of parking. This is how a subsystem that owes visible
55
+ * work — the GL compositor's pending first paints, a gesture walking to its
56
+ * commit — keeps a freeze from landing on a half-finished image. Returns an
57
+ * idempotent unregister.
58
+ */
59
+ settleWhile(name: string, busy: () => boolean): () => void;
60
+ /** Reporters answering busy right now ("why hasn't it parked yet?"). */
61
+ settling(): readonly string[];
62
+ /**
63
+ * The loop gate. CONSUMING — each call advances the settle walk, so it is
64
+ * for frame hosts only, never a predicate to poll. Use {@link isFrozen} /
65
+ * {@link isParked} to ask questions.
66
+ */
67
+ claimStep(): boolean;
68
+ /** Wake on any freeze/thaw transition (the host loop restarts here). */
69
+ onChange(fn: () => void): () => void;
70
+ }
71
+ export declare function createFrameControl(world: World): FrameControl;
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import type { Entity, World } from "@vibecook/strata-ecs";
22
22
  import { type Engine } from "../engine/engine";
23
+ import type { FrameControl } from "../engine/frame-control";
23
24
  import { type InteractionStack } from "../interaction/install";
24
25
  import { type NavOpts, type NestedCanvas } from "../nav/nested-canvas";
25
26
  import { type ArrangeOpts } from "../ops/arrange";
@@ -173,6 +174,14 @@ export interface CanvasEngine {
173
174
  readonly ops: CanvasOps;
174
175
  readonly docs: CanvasDocs;
175
176
  readonly stage: StageControl;
177
+ /**
178
+ * The frame gate (2026-08-04) — `stage`'s stricter sibling. A background
179
+ * hold is presentation policy over a LIVE world; a freeze stops the host
180
+ * loop outright. Reach for `stage` when chrome RECEDES the canvas, `frame`
181
+ * when chrome COVERS it. Passthrough of `engine.frame`, so there is one gate
182
+ * whether you hold the facade or the raw engine.
183
+ */
184
+ readonly frame: FrameControl;
176
185
  readonly budgets: {
177
186
  readonly keepMounted: number;
178
187
  readonly fboBytes: number;
@@ -14,6 +14,7 @@ export { FrameInfo, setFrameInfo } from "./engine/frame-info";
14
14
  export { createPipelineRegistry, PHASE_GROUPS, type AnySystem, type PhaseGroup, type PipelineRegistry } from "./engine/pipeline";
15
15
  export { createReflectorRegistry, type ReflectorDef, type ReflectorObserve, type ReflectorQuerySpec, type ReflectorRegistry, } from "./engine/reflectors";
16
16
  export { createEngine, type Engine, type EngineOpts, type FrameTelemetry, type PublishHook, type SystemRunRecord, } from "./engine/engine";
17
+ export { createFrameControl, SETTLE_CAP, type FrameControl } from "./engine/frame-control";
17
18
  export { devGuardsEnabled, setDevGuards } from "./guards/dev";
18
19
  export { createLiveWriter, type LiveWriter, type LiveWriterOpts } from "./guards/live-writer";
19
20
  export { guardedTransaction, type GuardedTx, type GuardedTxOpts } from "./guards/guarded-tx";
@@ -32,6 +33,7 @@ export { portSlots, portSlotOf } from "./ops/port-geometry";
32
33
  export { createInputQueue, NO_MODS, type InputEvent, type InputEventKind, type InputMods, type InputQueue } from "./input/queue";
33
34
  export { createRecordingCommitSink, type CommitIntent, type CommitCreate, type CommitOrder, type CommitReparent, type CommitSink, type CommitWrite, } from "./engine/commit-sink";
34
35
  export { ensureCanvasSurface, installInteractionCore, installInteractionStack, type InteractionCore, type InteractionCoreOpts, type InteractionStack, } from "./interaction/install";
36
+ export { anyGestureNonTerminal, isMidGesture } from "./interaction/gesture-status";
35
37
  export type { MarqueeBuffer } from "./systems/l3-marquee";
36
38
  export { DEFAULT_SPAWN_PROFILES, type SingleKindName, type SpawnProfiles } from "./systems/l2-recognize";
37
39
  export { createCursorSync } from "./systems/l4-cursor";
@@ -46,7 +48,7 @@ export type { DurableUndoStatusValue } from "@vibecook/strata-ecs/durable";
46
48
  export { createDocSession, openDocSession, type DocSession, type DocSessionOpts, type OpenDocResult, } from "./doc/doc-kit";
47
49
  export { runMigrations, type MigrationCtx, type MigrationOutcome } from "./doc/migrate";
48
50
  export { p, type JsonShape, type PropSpec, type PropsDecl, type StandardSchemaV1 } from "./widget/props";
49
- export { WidgetEquipped, defineWidget, widgets, type SizeMode, type WidgetDef, type WidgetGroup, type WidgetInteraction, type WidgetPortDecl, type WidgetSurface, type WidgetType, } from "./widget/define-widget";
51
+ export { WidgetEquipped, defineWidget, renames, widgets, type SizeMode, type WidgetDef, type WidgetGroup, type WidgetInteraction, type WidgetPortDecl, type WidgetRename, type WidgetRenameEntry, type WidgetSurface, type WidgetType, } from "./widget/define-widget";
50
52
  export { createWidgetEquipSystem } from "./widget/equip";
51
53
  export { attachSpawnParent, spawnWidget, widgetSpawnInits, type SpawnWidgetOpts } from "./widget/spawn";
52
54
  export { setWidgetProps } from "./widget/set-props";
@@ -40,6 +40,15 @@ export interface InputEvent {
40
40
  };
41
41
  /** GL-island opt-out (pinned widget-event contract, design-002 §8): fact still lands, recognizers skip. */
42
42
  readonly surfaceHandled?: boolean;
43
+ /**
44
+ * Wheel-only opt-out (design-007 §3.5, petition I4): this wheel's deltas were
45
+ * ceded to scrollable widget content — the adapter skipped `preventDefault`
46
+ * and native scroll consumed them. The fact still lands (one input path);
47
+ * ingest stamps the one-tick `WheelHandled` tag so the wheel recognizers skip
48
+ * the tick. DISTINCT from `surfaceHandled` on purpose: the shared latch on
49
+ * the persistent mouse pointer would collide with a same-tick canvas down.
50
+ */
51
+ readonly wheelHandled?: boolean;
43
52
  /**
44
53
  * Hover-time widget-boundary fact (design-002 §8 amendment, 2026-07-18): the
45
54
  * pointer is currently over content that would opt a down out — a native
@@ -0,0 +1,13 @@
1
+ import type { World } from "@vibecook/strata-ecs";
2
+ /**
3
+ * Any recognizer NOT in a terminal phase ⇒ a gesture is in flight (mirrors the
4
+ * cleanup.ts terminal definition: Failed/Cancelled/Ended, plus Recognized-Tap).
5
+ */
6
+ export declare function anyGestureNonTerminal(world: World): boolean;
7
+ /**
8
+ * The full mid-interaction read: a live camera gesture (pan/zoom, which writes
9
+ * `Camera.gesturing` without a recognizer entity of its own) OR any recognizer
10
+ * still walking its phases. Either means runtime edits are outstanding and the
11
+ * durable transaction that will carry them has not been sealed yet.
12
+ */
13
+ export declare function isMidGesture(world: World): boolean;
@@ -28,6 +28,18 @@ export interface ResourceMeta {
28
28
  durable: boolean;
29
29
  }
30
30
  export declare function defineComponent<const Sch extends RawSchema>(name: string, schema: Sch): ReturnType<typeof strataDefineComponent<Sch>>;
31
+ /**
32
+ * Reuse-or-define by NAME (design-008 §3.1): strata's schema registry throws
33
+ * on duplicate identifiers (its registry.ts:38), and rename legacy components
34
+ * (`<oldType>:<group>`) can legitimately re-enter the process — a test that
35
+ * defined the old widget before resetting ice registries, or a hot-reload
36
+ * double-eval — while strata's global registry survives. An existing
37
+ * SAME-SHAPED component is the same component: reuse its handle. A mismatched
38
+ * shape under the same name is a real collision: throw. The shape compare is
39
+ * JSON-stringify over the recorded raw schema — sound here because both sides
40
+ * are built by the same `strataFieldOf` constructors in the same field order.
41
+ */
42
+ export declare function ensureComponent<const Sch extends RawSchema>(name: string, schema: Sch): Component;
31
43
  export declare function defineTag(name: string): Tag;
32
44
  export declare function defineRelation(name: string, opts?: {
33
45
  arity?: "one" | "many";
@@ -11,6 +11,32 @@ export interface WidgetPortDecl {
11
11
  /** Compatibility keys (empty = connects to anything). */
12
12
  readonly accepts?: readonly string[];
13
13
  }
14
+ /**
15
+ * A prior durable id of this widget type (design-008 §3, petition I5). Docs
16
+ * written under the old id fold to this type at open (doc/rename.ts) and via
17
+ * the live zombie sweep (doc/rename-sweep.ts) — replicas converge through
18
+ * ordinary CRDT delivery, retiring offline byte surgery. Declares group-NAME
19
+ * continuity at the rename boundary (the pack runner's existing fence).
20
+ */
21
+ export interface WidgetRename {
22
+ /** The old durable type id (`PrefabId.id` in pre-rename docs). */
23
+ readonly type: string;
24
+ /**
25
+ * The pack version old-build writers of `type` were at (default 1). Only
26
+ * the SWEEP consumes it — a late old-shape delivery carries no marker, so
27
+ * its values chain-fold from here; the open-path runner reads the version
28
+ * off the doc's own markers instead (design-008 §6.3).
29
+ */
30
+ readonly atVersion?: number;
31
+ }
32
+ /** A rename registry entry (doc/rename runner + sweep input). */
33
+ export interface WidgetRenameEntry {
34
+ readonly widget: WidgetType;
35
+ readonly oldType: string;
36
+ readonly atVersion: number;
37
+ /** Legacy `<oldType>:<group>` components, index-aligned with `widget.groups`. */
38
+ readonly legacyGroups: readonly Component[];
39
+ }
14
40
  export interface WidgetInteraction {
15
41
  readonly selectable?: boolean;
16
42
  readonly movable?: boolean;
@@ -26,6 +52,25 @@ export interface WidgetInteraction {
26
52
  * membership — never reparenting). Default false.
27
53
  */
28
54
  readonly sweepContained?: boolean;
55
+ /**
56
+ * Keyboard claim (design-007 §3.1, petitions I1/I4). `"exclusive"`: while a
57
+ * node inside this widget holds browser focus, the engine keymap and the
58
+ * adapter's Space pan modifier stand down — keys flow to the widget's own
59
+ * handlers (the engine stops competing; it never delivers behavior). The
60
+ * dom-widgets reflector marks the host `data-canvas-keyboard` + `tabindex=-1`
61
+ * so a click anywhere in the widget acquires focus, and plain wheel over the
62
+ * widget's scrollable content cedes to native scroll (ctrl/pinch stays
63
+ * canvas zoom always). Default `"shared"` — today's behavior, byte-identical.
64
+ */
65
+ readonly keyboard?: "shared" | "exclusive";
66
+ /**
67
+ * Escape ownership under an exclusive claim (design-007 §3.3 / §7-Q1).
68
+ * `"release"` (default): Escape is the engine-reserved release gesture — it
69
+ * blurs the widget; the NEXT Escape, with focus gone, cancels gestures.
70
+ * `"widget"`: even Escape flows to the widget (vim-grade terminals); release
71
+ * is click-away or `blurFocus()`. Ignored unless `keyboard: "exclusive"`.
72
+ */
73
+ readonly keyboardEscape?: "release" | "widget";
29
74
  }
30
75
  export interface WidgetDef {
31
76
  readonly type: string;
@@ -101,6 +146,13 @@ export interface WidgetDef {
101
146
  * (its `provides` wins).
102
147
  */
103
148
  readonly provides?: readonly string[];
149
+ /**
150
+ * Prior durable ids folded to this type (design-008; petition I5). Additive:
151
+ * registers read-only LEGACY group components under the old names so
152
+ * pre-rename cells project, gates pre-rename docs "migrate" instead of
153
+ * read-only, and arms the live zombie sweep on every writable session.
154
+ */
155
+ readonly renamedFrom?: readonly WidgetRename[];
104
156
  /**
105
157
  * fromVersion → idempotent absolute transform (M9's `runMigrations` runs the
106
158
  * chain at open; `prev`/return are flat prop records spanning every group).
@@ -146,6 +198,10 @@ export interface WidgetType {
146
198
  readonly ports: readonly WidgetPortDecl[];
147
199
  /** Runtime capability tags the equip system stamps at projection. */
148
200
  readonly capabilityTags: readonly Tag[];
201
+ /** Keyboard claim (design-007): the dom layer reads THIS (registry truth), not the tag. */
202
+ readonly keyboard: "shared" | "exclusive";
203
+ /** Escape ownership under an exclusive claim ("release" = engine-reserved). */
204
+ readonly keyboardEscape: "release" | "widget";
149
205
  readonly migrate: Readonly<Record<number, (prev: Record<string, unknown>) => Record<string, unknown>>>;
150
206
  /** Normalized author preview component (opaque; react narrows). null = none declared. */
151
207
  readonly previewComponent: unknown;
@@ -158,6 +214,11 @@ export declare const widgets: {
158
214
  get(type: string): WidgetType | undefined;
159
215
  all(): WidgetType[];
160
216
  };
217
+ /** The rename registry (design-008 §3): old durable id → fold target. */
218
+ export declare const renames: {
219
+ get(oldType: string): WidgetRenameEntry | undefined;
220
+ all(): WidgetRenameEntry[];
221
+ };
161
222
  export declare function defineWidget(def: WidgetDef): WidgetType;
162
223
  /** TEST-ONLY wipe (mirrors __resetPrefabsForTests; not on the barrel). */
163
224
  export declare function __resetWidgetsForTests(): void;
@@ -10,6 +10,8 @@ export { createGrayboxReflector } from "./reflectors/graybox";
10
10
  export { createDomWidgetsReflector, type DomWidgetsHost, type DomWidgetsReflector, } from "./reflectors/dom-widgets";
11
11
  export { startRafLoop } from "./loop";
12
12
  export { attachPointerAdapter, type GLRoute, type GLRouteVerdict, type PointerAdapterOpts } from "./pointer-adapter";
13
+ export { CLAIM_OWNS_ESCAPE, KEYBOARD_CLAIM_ATTR, isEditableTarget, keyboardClaimOf, wheelCede, type KeyboardClaim, } from "./input-ownership";
14
+ export { FOCUS_PROXY_ATTR, attachWidgetFocus, type FocusHostLookup, type WidgetFocusHandle, } from "./widget-focus";
13
15
  export { createCursorReflector } from "./reflectors/cursor";
14
16
  export { createRemoteCursorsReflector, type RemoteCursorsReflector } from "./reflectors/remote-cursors";
15
17
  export { createChromeReflector } from "./reflectors/chrome";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Input-ownership predicates (design-007 §4 — one predicate FAMILY, not one
3
+ * predicate; petitions I1/I4). "Does the widget own input here" is answered
4
+ * per-surface at the right scope, sharing the DOM-walk mechanics:
5
+ *
6
+ * - `isEditableTarget` — the 4-class editable gate (keymap default + the
7
+ * adapter's Space-types fix). Kept narrow so UNDECLARED widgets behave
8
+ * exactly as before (design-007 F5).
9
+ * - `keyboardClaimOf` — the `data-canvas-keyboard` claim marker written by the
10
+ * dom-widgets reflector for `interaction.keyboard: "exclusive"` widgets.
11
+ * Read UNBOUNDED (page-global) on purpose: `document.activeElement` is one
12
+ * per page, so engine B's keymap must stand down while you type into engine
13
+ * A's claiming widget (design-007 F2 — only the DOM is page-global).
14
+ * - `wheelCede` — the NARROW, DYNAMIC scroll opt-out (design-007 §3.4/§4):
15
+ * plain wheel cedes to native scroll only when a scroller WITH ROOM in the
16
+ * wheel's dominant axis sits inside a claim-marked / `data-canvas-
17
+ * interactive` / editable subtree. At the scroll bounds the predicate turns
18
+ * false and the wheel falls through to canvas pan/zoom — the familiar
19
+ * nested-scroll feel — which also keeps scroll-chaining from ever reaching
20
+ * the page. Deliberately NOT the full `crossesInteractive` set: hovering a
21
+ * bare `<button>` must not kill canvas zoom (design-007 F4).
22
+ *
23
+ * The keymap (`@ice/react`) imports these — the two historically disagreeing
24
+ * guards now share one implementation (the I1 unification).
25
+ */
26
+ /** The claim marker the dom-widgets reflector writes on an exclusive widget's host. */
27
+ export declare const KEYBOARD_CLAIM_ATTR = "data-canvas-keyboard";
28
+ /** Marker value for `keyboardEscape: "widget"` (the widget receives even Escape). */
29
+ export declare const CLAIM_OWNS_ESCAPE = "escape";
30
+ export interface KeyboardClaim {
31
+ /** The event-target chain crosses a claim marker (⇒ engine keyboard stands down). */
32
+ readonly claimed: boolean;
33
+ /** The claiming widget owns Escape too (`keyboardEscape: "widget"`). */
34
+ readonly ownsEscape: boolean;
35
+ }
36
+ /** The keymap's editable gate — exactly INPUT/TEXTAREA/SELECT/contentEditable. */
37
+ export declare function isEditableTarget(target: EventTarget | null): boolean;
38
+ /**
39
+ * Resolve the keyboard claim for an event target (keydown targets ARE
40
+ * `document.activeElement`, so this reads browser focus — the single source of
41
+ * truth, design-007 §2.1). Unbounded `closest` by design (see header).
42
+ */
43
+ export declare function keyboardClaimOf(target: EventTarget | null): KeyboardClaim;
44
+ /**
45
+ * Should this plain (non-pinch) wheel be ceded to native widget scroll?
46
+ * Walks target → container: remembers any scroller-with-room, cedes on the
47
+ * first opt-out root at or above it. Walking up meets scrollers BEFORE their
48
+ * widget's marker (the marker sits on the host), so "a scroller inside an
49
+ * opt-out subtree" is exactly "saw a scroller by the time the marker matches".
50
+ * ctrl/pinch wheels never reach this — they are ALWAYS the canvas's zoom.
51
+ */
52
+ export declare function wheelCede(target: EventTarget | null, container: HTMLElement, dx: number, dy: number): boolean;
@@ -6,6 +6,17 @@
6
6
  * timestamp straight into the frame clock (so FrameInfo.dt is real wall time,
7
7
  * clamped in core). The returned stop fn cancels the pending frame AND latches
8
8
  * so a frame already in flight cannot re-schedule — calling it is idempotent.
9
+ *
10
+ * THE FREEZE GATE (2026-08-04). Before each frame the loop claims a step from
11
+ * `engine.frame`. While a freeze is held the gate walks its settle and then
12
+ * refuses, at which point the loop PARKS: it stops scheduling rAF at all
13
+ * rather than spinning a callback that does nothing — "the engine is stopped"
14
+ * should mean the browser has no work queued for it either. Thaw arrives
15
+ * through the gate's change subscription, which schedules the next frame.
16
+ *
17
+ * The dt clamp is what makes resume safe: a freeze that lasted an hour hands
18
+ * `step` an enormous `now`, and core clamps the delta (frame-info.ts), so
19
+ * tweens advance by one ordinary frame instead of teleporting to their end.
9
20
  */
10
21
  import type { Engine } from "@ice/core";
11
22
  export declare function startRafLoop(engine: Engine): () => void;
@@ -13,9 +13,22 @@
13
13
  * - `setPointerCapture` on down so a drag keeps delivering outside the element;
14
14
  * - Space is the pan modifier (design-003 §4.4): tracked here and threaded onto
15
15
  * every event's `mods.space`; keydown/keyup emit a `key` fact only when the
16
- * modifier tuple actually changes, and Space `preventDefault`s to stop page scroll;
17
- * - wheel `preventDefault`s: a ctrl-wheel / trackpad pinch becomes `wheel.pinch`
18
- * (the zoom signal), a plain wheel becomes dx/dy (deltaMode lines → px ×16);
16
+ * modifier tuple actually changes. Space's preventDefault/latch is OWNERSHIP-
17
+ * AWARE (design-007 §3.4, petition I1): into an editable it neither latches
18
+ * nor preventDefaults (Space must TYPE — the §1.6 live-bug fix); into a
19
+ * `data-canvas-keyboard` claim it cedes the pan SEMANTICS but keeps killing
20
+ * the page-scroll default (the key still reaches the widget's own listener —
21
+ * preventDefault is not stopPropagation); elsewhere it pans, as ever;
22
+ * - wheel: ctrl-wheel / trackpad pinch is ALWAYS `preventDefault` + the canvas
23
+ * zoom signal (`wheel.pinch`) — a browser page-zoom over a widget is
24
+ * catastrophic, claimed or not. A PLAIN wheel over a scroller-with-room
25
+ * inside a claim/`data-canvas-interactive`/editable subtree (`wheelCede`,
26
+ * design-007 §3.4/§3.5) is ceded: NO preventDefault (native scroll
27
+ * proceeds), the fact still lands flagged `wheelHandled` so ingest stamps
28
+ * the one-tick `WheelHandled` and the wheel recognizers skip the tick. At
29
+ * the scroll bounds the predicate flips and the wheel falls through to
30
+ * canvas pan/zoom. Everything else: `preventDefault`, dx/dy (deltaMode
31
+ * lines → px ×16) — unchanged;
19
32
  * - window blur enqueues a synthetic `cancel` for every pointer still down
20
33
  * (design-003 §8) — the browser will not send the pointerups.
21
34
  *