@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.
- package/dist/{chunk-IASTQMU5.js → chunk-3PHPWRB2.js} +146 -11
- package/dist/chunk-3PHPWRB2.js.map +1 -0
- package/dist/{chunk-DXU3RZZS.js → chunk-7LZAXQAO.js} +649 -261
- package/dist/chunk-7LZAXQAO.js.map +1 -0
- package/dist/{chunk-5SJYHQI4.js → chunk-ENMRSITH.js} +38 -11
- package/dist/chunk-ENMRSITH.js.map +1 -0
- package/dist/devtools.js +1 -1
- package/dist/dom.js +16 -2
- package/dist/ground.js +1 -1
- package/dist/index.js +17 -1
- package/dist/r3f.js +9 -3
- package/dist/r3f.js.map +1 -1
- package/dist/react.js +5 -3
- package/dist/types/packages/core/src/catalog/camera-derived.d.ts +26 -2
- package/dist/types/packages/core/src/catalog/gesture.d.ts +1 -1
- package/dist/types/packages/core/src/catalog/pointer.d.ts +11 -0
- package/dist/types/packages/core/src/catalog/selection-presence.d.ts +10 -0
- package/dist/types/packages/core/src/doc/autosave.d.ts +34 -0
- package/dist/types/packages/core/src/doc/rename-sweep.d.ts +3 -0
- package/dist/types/packages/core/src/doc/rename.d.ts +7 -0
- package/dist/types/packages/core/src/doc/version-gate.d.ts +14 -1
- package/dist/types/packages/core/src/engine/engine.d.ts +11 -0
- package/dist/types/packages/core/src/engine/frame-control.d.ts +71 -0
- package/dist/types/packages/core/src/facade/create-canvas-engine.d.ts +9 -0
- package/dist/types/packages/core/src/index.d.ts +3 -1
- package/dist/types/packages/core/src/input/queue.d.ts +9 -0
- package/dist/types/packages/core/src/interaction/gesture-status.d.ts +13 -0
- package/dist/types/packages/core/src/schema/meta.d.ts +12 -0
- package/dist/types/packages/core/src/widget/define-widget.d.ts +61 -0
- package/dist/types/packages/dom/src/index.d.ts +2 -0
- package/dist/types/packages/dom/src/input-ownership.d.ts +52 -0
- package/dist/types/packages/dom/src/loop.d.ts +11 -0
- package/dist/types/packages/dom/src/pointer-adapter.d.ts +16 -3
- package/dist/types/packages/dom/src/widget-focus.d.ts +58 -0
- package/dist/types/packages/react/src/engine-context.d.ts +13 -0
- package/dist/types/packages/react/src/index.d.ts +1 -1
- package/dist/types/packages/react/src/infinite-canvas.d.ts +20 -2
- package/dist/types/packages/react/src/keymap.d.ts +23 -4
- package/package.json +4 -4
- package/dist/chunk-5SJYHQI4.js.map +0 -1
- package/dist/chunk-DXU3RZZS.js.map +0 -1
- 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-
|
|
27
|
-
import "./chunk-
|
|
28
|
-
import "./chunk-
|
|
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" | "
|
|
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" | "
|
|
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<"
|
|
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,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
|
-
/**
|
|
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
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
*
|