···146N1. Define the StatusBar view model (core, pure TS)
147------------------------------------------------------------------------------
148149-/packages/core/src/ui/statusbar/types.ts
150-[ ] Define StatusBarVM (single object the UI renders):
151 - cursorWorld: { x, y } " world coords (always)
152 - cursorScreen: { x, y }? " optional dev-only
153 - zoomPct: number " e.g. 100, 67, 250
···179N2. Provide selectors / derivations for StatusBarVM
180------------------------------------------------------------------------------
181182-/packages/core/src/ui/statusbar/selectors.ts
183-[ ] Implement pure functions:
184 - getZoomPct(state) -> number
185 - getToolId(state) -> ToolId
186 - getSelectionSummary(state) -> { count, kind?, bounds? }
187 - getSnapSummary(state) -> snap summary (default safe values)
188189Cursor position source:
190-[ ] Define a minimal CursorState in core (NOT persisted):
191 - cursorWorld: Vec2
192 - cursorScreen?: Vec2
193 - lastMoveAt: number
194195-[ ] Add updateCursor(world, screen?) action + reducer handler (or direct setter)
196 that ONLY touches CursorState (no history command, no persistence).
197198(DoD):
···202N3. Wire cursor updates from pointer movement (apps/web)
203------------------------------------------------------------------------------
204205-/apps/web/src/lib/pointer.ts
206-[ ] On pointermove (or mousemove when not captured):
207 - compute world coords using camera.screenToWorld
208 - dispatch updateCursor(world, screen)
209210Performance:
211-[ ] Throttle cursor updates:
212 - v0: requestAnimationFrame coalescing (only publish latest per frame)
213 - avoid flooding render/history/persistence
214···224done; persistence is already hooked to history in Milestone M).
225226/apps/web/src/lib/status.ts
227-[ ] Extend your persistence sink (from Milestone M) to expose a small status:
228 - pendingWrites counter (increment on enqueue, decrement on commit)
229 - lastSavedAt timestamp (set on successful commit)
230 - lastError (set on failed commit)
231-[ ] Use Dexie liveQuery to observe the current board’s updatedAt from IndexedDB
232 and reflect it in the UI (helps confirm persisted state across tabs).
233234(DoD):
···146N1. Define the StatusBar view model (core, pure TS)
147------------------------------------------------------------------------------
148149+/packages/core/src/ui/statusbar.ts
150+[x] Define StatusBarVM (single object the UI renders):
151 - cursorWorld: { x, y } " world coords (always)
152 - cursorScreen: { x, y }? " optional dev-only
153 - zoomPct: number " e.g. 100, 67, 250
···179N2. Provide selectors / derivations for StatusBarVM
180------------------------------------------------------------------------------
181182+/packages/core/src/ui/statusbar.ts
183+[x] Implement pure functions:
184 - getZoomPct(state) -> number
185 - getToolId(state) -> ToolId
186 - getSelectionSummary(state) -> { count, kind?, bounds? }
187 - getSnapSummary(state) -> snap summary (default safe values)
188189Cursor position source:
190+[x] Define a minimal CursorState in core (NOT persisted):
191 - cursorWorld: Vec2
192 - cursorScreen?: Vec2
193 - lastMoveAt: number
194195+[x] Add updateCursor(world, screen?) action + reducer handler (or direct setter)
196 that ONLY touches CursorState (no history command, no persistence).
197198(DoD):
···202N3. Wire cursor updates from pointer movement (apps/web)
203------------------------------------------------------------------------------
204205+/apps/web/src/lib/input.ts
206+[x] On pointermove (or mousemove when not captured):
207 - compute world coords using camera.screenToWorld
208 - dispatch updateCursor(world, screen)
209210Performance:
211+[x] Throttle cursor updates:
212 - v0: requestAnimationFrame coalescing (only publish latest per frame)
213 - avoid flooding render/history/persistence
214···224done; persistence is already hooked to history in Milestone M).
225226/apps/web/src/lib/status.ts
227+[x] Extend your persistence sink (from Milestone M) to expose a small status:
228 - pendingWrites counter (increment on enqueue, decrement on commit)
229 - lastSavedAt timestamp (set on successful commit)
230 - lastError (set on failed commit)
231+[x] Use Dexie liveQuery to observe the current board’s updatedAt from IndexedDB
232 and reflect it in the UI (helps confirm persisted state across tabs).
233234(DoD):
+18-4
apps/web/src/lib/canvas/Canvas.svelte
···2 import HistoryViewer from '$lib/components/HistoryViewer.svelte';
3 import Toolbar from '$lib/components/Toolbar.svelte';
4 import { createInputAdapter, type InputAdapter } from '$lib/input';
05 import {
6 ArrowTool,
07 EditorState,
8 EllipseTool,
9 InkfiniteDB,
···13 SnapshotCommand,
14 Store,
15 TextTool,
16- createPersistenceSink,
17 createToolMap,
18 createWebDocRepo,
19 diffDoc,
···22 type Action,
23 type CommandKind,
24 type LoadedDoc,
025 type ToolId,
26 type Viewport
27 } from 'inkfinite-core';
···29 import { onDestroy, onMount } from 'svelte';
3031 let repo: ReturnType<typeof createWebDocRepo> | null = null;
32- let sink: ReturnType<typeof createPersistenceSink> | null = null;
033 let activeBoardId: string | null = null;
3435 const store = new Store(undefined, {
···41 sink.enqueueDocPatch(activeBoardId, patch);
42 }
43 });
04445 function applyLoadedDoc(doc: LoadedDoc) {
46 const firstPageId = doc.order.pageIds[0] ?? Object.keys(doc.pages)[0] ?? null;
···151 onMount(() => {
152 const db = new InkfiniteDB();
153 repo = createWebDocRepo(db);
154- sink = createPersistenceSink(repo, { debounceMs: 200 });
0155 let disposed = false;
156157 const hydrate = async () => {
···168 activeBoardId = id;
169 const loaded = await repoInstance.loadDoc(id);
170 if (!disposed) {
0171 applyLoadedDoc(loaded);
172 }
173 } catch (error) {
···187 }
188189 renderer = createRenderer(canvas, store);
190- inputAdapter = createInputAdapter({ canvas, getCamera, getViewport, onAction: handleAction });
000000191192 function handleBeforeUnload() {
193 if (sink) {
···212 repo = null;
213 sink = null;
214 activeBoardId = null;
00215 });
216</script>
217