···1-=============================================================================
02Author intent:
3- Build a Svelte-native editor core (TS) + renderer + UI.
4- Keep the "engine" framework-agnostic so Web + Tauri share it.
···8- [ ] Task
9- (DoD) Definition of Done for the milestone
10- Files shown are ideas, not requirements
11-==============================================================================
12-1. Milestone A: Repo skeleton + dev loop *wb-A*
13-==============================================================================
01415Goal: a monorepo that can run a blank canvas in web + desktop.
16···40- `pnpm dev:desktop` launches a Tauri window that shows the same canvas.
41- `pnpm test` runs at least 1 passing core test.
4243-==============================================================================
44-2. Milestone B: Math + coordinate systems *wb-B*
45-==============================================================================
46-47-Goal: a correct, testable camera transform (world <-> screen).
48-49-Core primitives (/packages/core/src/math.ts):
50-[x] Define Vec2 { x, y } + helpers:
51- - add, sub, mulScalar, len, normalize, dot
52-[x] Define Box2 { min: Vec2, max: Vec2 }:
53- - fromPoints, containsPoint, intersectsBox
54-[x] Define Mat3 (2D affine) or equivalent:
55- - identity
56- - translate(tx, ty)
57- - scale(sx, sy)
58- - rotate(theta)
59- - multiply(a, b)
60- - transformPoint(m, p)
61-62-Camera (/packages/core/src/camera.ts):
63-[x] Define Camera { x, y, zoom } (world origin + scale)
64-[x] Implement worldToScreen(camera, p)
65-[x] Implement screenToWorld(camera, p)
66-[x] Implement cameraPan(camera, deltaScreen) -> camera'
67-[x] Implement cameraZoomAt(camera, factor, anchorScreenPoint) -> camera'
68-69-Tests (/packages/core/tests/camera.test.ts):
70-[x] worldToScreen(screenToWorld(p)) round-trip within epsilon
71-[x] zoomAt keeps anchor point stable (screen position unchanged)
72-[x] pan moves world under cursor as expected
7374-(DoD):
75-- All math/camera functions are unit-tested and pass.
7677-==============================================================================
78-3. Milestone C: Document model (records) *wb-C*
79-==============================================================================
8081-Goal: define the minimal data model that can represent a drawing.
08283-Records & ID (/packages/core/src/model):
84-[x] Implement createId(prefix) -> uuid (v4)
85-[x] Define PageRecord { id, name, shapeIds: string[] }
86-[x] Define ShapeRecord base:
87- - id, type, pageId
88- - x, y, rot
89- - props: object (type-specific)
9091-[x] Define shape types (minimal):
92- - rect: { w, h, fill, stroke, radius }
93- - ellipse: { w, h, fill, stroke }
94- - line: { a: Vec2, b: Vec2, stroke, width }
95- - arrow: { a: Vec2, b: Vec2, stroke, width }
96- - text: { text, fontSize, fontFamily, color, w? }
9798-[x] Define BindingRecord (for arrow endpoints):
99- - id, type: "arrow-end"
100- - fromShapeId (arrow id)
101- - toShapeId (target shape id)
102- - handle: "start" | "end"
103- - anchor: e.g. { kind: "center" } for v0
104105-Validation:
106-[x] validateDoc(doc) -> { ok | errors[] }
107108-(DoD):
109-- You can serialize a doc with a page + 1 shape to JSON and validate it.
0110111-==============================================================================
112-4. Milestone D: Store + selectors (reactive core) *wb-D*
113-==============================================================================
114115-Goal: a fast, deterministic state container for the editor using RxJS
00116117-Store (/packages/core/src/reactivity.ts) - RxJS + SvelteKit (runes) friendly
0118119-Core types:
120-[x] Define EditorState:
121- - doc: { pages, shapes, bindings }
122- - ui: { currentPageId, selectionIds: string[], toolId: ToolId }
123- - camera: { x, y, zoom }
124125-RxJS store (BehaviorSubject-backed):
126-[x] Implement createEditorStore(initial: EditorState) that exposes:
127- - state$: Observable<EditorState> (read stream)
128- - getState(): EditorState (sync snapshot)
129- - setState(updater: (s) => s): void (mutation API)
130- - subscribe(listener): () => void (Svelte-compatible subscribe)
131- - select(selector, eq?): Observable<T> (derived streams)
132133- Notes:
134- - Use BehaviorSubject so new subscribers immediately get the current value.
135- - subscribe must return an unsubscribe function.
136137-Selectors (pure functions, no RxJS):
138-[x] Implement selectors
139- - getCurrentPage(state)
140- - getShapesOnCurrentPage(state)
141- - getSelectedShapes(state)
142143-Invariants (pick "repair" and test it):
144-[x] Implement enforceInvariants(state): EditorState (repair strategy):
145- - selectionIds := selectionIds filtered to existing shapes
146- - currentPageId must exist:
147- - if missing, set to first existing page
148- - if no pages exist, create a default page and set it
149-[x] Ensure setState always runs enforceInvariants before publishing next state
150151-Tests
152-[x] subscribe immediately receives current state upon subscription (BehaviorSubject behavior)
153-[x] subscribe fires exactly once per setState call
154-[x] invariants are enforced on any update (selection filtered, page fixed/created)
155-156-(DoD):
157-- Renderer can subscribe to state$ (or subscribe()) and redraw on any change.
158-- SvelteKit can bridge to runes with $effect unsubscribe cleanup.
159-160-==============================================================================
161-5. Milestone E: Canvas renderer (read-only) *wb-E*
162-==============================================================================
163-164-Goal: draw the document from state, no interactivity yet.
165-166-Renderer (/packages/renderer):
167-[x] createRenderer(canvas, store) -> { dispose() }
168-[x] Implement render loop strategy:
169- - requestAnimationFrame redraw on "dirty" flag
170- - mark dirty on store updates
171-[x] Implement draw pipeline:
172- - clear canvas
173- - apply camera transform
174- - draw shapes (rect/ellipse/line/arrow/text)
175- - draw selection outline if selectionIds non-empty
176-[x] Implement text measurement fallback:
177- - if text shape has w? else measureText and derive bounds
178-[x] Implement pixel ratio handling:
179- - set canvas width/height by devicePixelRatio
180- - scale context accordingly
181-182-(DoD):
183-- With a hardcoded doc in the store, shapes appear at correct coordinates
184- and stay stable while resizing the browser window.
185-186-==============================================================================
187-6. Milestone F: Hit testing (picking) *wb-F*
188-==============================================================================
189-190-Goal: determine what the cursor is over.
191-192-Geometry (/packages/core/src/geom):
193-[x] shapeBounds(shape) -> Box2
194-[x] pointInRect(p, rectShape) -> bool
195-[x] pointInEllipse(p, ellipseShape) -> bool
196-[x] pointNearSegment(p, a, b, tolerance) -> bool
197-[x] hitTestPoint(state, worldPoint) -> shapeId? (topmost wins)
198-199-Layering:
200-[x] Define draw order = page.shapeIds order
201-[x] hitTest uses reverse order for topmost selection
202-203-(DoD):
204-- You can hover shapes and log the hit shape id (no selection yet).
205-206-==============================================================================
207-7. Milestone G: Input system (pointer + keyboard) *wb-G*
208-==============================================================================
209-210-Goal: normalize events into editor actions.
211-212-Input adapter (/apps/web/src/lib/input):
213-[x] Capture pointerdown/move/up
214-[x] Convert screen coords -> world coords using camera
215-[x] Track pointer state: isDown, startWorld, lastWorld, buttons
216-217-Keyboard:
218-[x] Capture keydown/keyup
219-[x] Normalize modifiers (ctrl/cmd, shift, alt)
220-221-Action bus (/packages/core/src/actions.ts):
222-[x] Define Action union:
223- - PointerDown, PointerMove, PointerUp
224- - KeyDown, KeyUp
225- - Wheel (for zoom)
226-[x] dispatch(action) -> store updates via tool state machine (next milestone)
227-228-(DoD):
229-- You can pan/zoom camera via wheel/drag with a temporary "camera tool"
230- (even before selection tool exists).
231-232-==============================================================================
233-8. Milestone H: Tool state machine (foundation) *wb-H*
234-==============================================================================
235-236-Goal: tools are explicit, testable state machines (RxJS based)
237-238-Tools (/packages/core/src/tools):
239-[x] Define ToolId: "select" | "rect" | "ellipse" | "line" | "arrow" | "text" | "pen"
240-[x] Define Tool interface:
241- - id
242- - onEnter(state)
243- - onAction(state, action) -> newState
244- - onExit(state)
245-246-Tool router:
247-[x] routeAction(state, action) -> newState (delegates to active tool)
248-249-(DoD):
250-- A dummy tool can consume pointer events and update state deterministically.
251-252-253-==============================================================================
254-9. Milestone I: Select/move tool (MVP interaction) *wb-I*
255-==============================================================================
256-257-Goal: select shapes and drag them.
258-259-Selection:
260-[x] PointerDown:
261- - if hit shape: selection = [shapeId] (or add with shift)
262- - else selection = []
263-[x] PointerMove while dragging selected:
264- - translate selected shapes by deltaWorld
265-[x] PointerUp:
266- - end drag
267-268-Marquee select (smallest slices):
269-[x] Implement marquee start (on empty canvas pointerdown)
270-[x] Render marquee rectangle overlay
271-[x] On pointerup, select shapes whose bounds intersect marquee
272-273-UX:
274-[x] Escape clears selection
275-[x] Delete removes selected shapes
276-277-(DoD):
278-- You can select and move shapes reliably.
279-280-==============================================================================
281-10. Milestone J: Create basic shapes via tools *wb-J*
282-==============================================================================
283-284-Goal: place shapes with dedicated tools.
285-286-Rect tool (repeat pattern for others):
287-[x] PointerDown on canvas:
288- - create a draft rect shape with w/h=0 at startWorld
289- - selection = [newId]
290-[x] PointerMove:
291- - update w/h based on currentWorld - startWorld
292-[x] PointerUp:
293- - if too small, delete it (click-cancel behavior)
294- - else finalize
295-296-[x] Implement ellipse tool (same mechanics)
297-[x] Implement line tool (a=startWorld, b=currentWorld)
298-[x] Implement arrow tool (same as line but type="arrow")
299-[x] Implement text tool:
300- - click to create text shape
301- - open in-place editor overlay (contenteditable) in Svelte
302303-(DoD):
304-- You can draw rect/ellipse/line/arrow/text on the canvas.
305306================================================================================
30711. Milestone K: Bindings for arrows (v0) *wb-K*
308================================================================================
309310-Goal: arrow endpoints can "stick" to shapes.
311-312-Binding creation:
313-[x] On arrow finalize:
314- - hit test start/end points
315- - if point hits a target shape, create binding record for that handle
316-317-Binding resolution:
318-[x] resolveArrowEndpoints(state, arrowId) -> { a, b } in world coords
319- - if bound, compute endpoint at target shape bounds center (v0)
320- - else use arrow props a/b
321-322-Live update:
323-[x] When a target shape moves, bound arrow rerenders automatically
324- - no mutation required if resolve happens during render
325-326-(DoD):
327-- Arrows remain connected to moved shapes (center-to-center is fine for v0).
328329================================================================================
33012. Milestone L: History (undo/redo) *wb-L*
331================================================================================
332333-Goal: every user-visible change is undoable.
334-335-History model (/packages/core/src/history):
336-[x] Define Command:
337- - do(state) -> state
338- - undo(state) -> state
339-[x] Wrap mutations as commands:
340- - CreateShapeCommand
341- - UpdateShapeCommand (with before/after snapshot)
342- - DeleteShapesCommand
343- - SetSelectionCommand
344- - SetCameraCommand
345-[x] Implement stacks:
346- - undoStack, redoStack
347-[x] Wire shortcuts:
348- - Ctrl/Cmd+Z undo
349- - Ctrl/Cmd+Shift+Z redo
350-351-(DoD):
352-- Undo/redo works for create/move/delete and camera changes.
353-354-==============================================================================
355-13. Milestone M: Persistence (web) via Dexie + History integration *wb-M*
356-==============================================================================
357-358-Goal:
359-- Persist boards to IndexedDB using Dexie (with schema versions + data upgrades).
360-- Integrate with the (already implemented) history/command system so:
361- - do/undo/redo that changes the document is persisted
362- - non-document UI changes (selection/tool/camera, etc.) are NOT persisted
363-364-Storage shape (v0 choice): Normalized tables
365-- boards, pages, shapes, bindings + meta + migrations
366-367-------------------------------------------------------------------------------
368-M1. Dexie DB + schema v1
369-------------------------------------------------------------------------------
370-371-/apps/web/src/lib/db.ts
372-[ ] Create a Dexie DB class `InkfiniteDB` extending Dexie.
373-[ ] db.version(1).stores({
374- boards: 'id, name, createdAt, updatedAt',
375- pages: '[boardId+id], boardId, updatedAt',
376- shapes: '[boardId+id], boardId, type, updatedAt',
377- bindings: '[boardId+id], boardId, type, updatedAt',
378- meta: 'key',
379- migrations: 'id, appliedAt'
380- })
381-382-[ ] Implement schema upgrade hook(s):
383- - db.version(N).upgrade(tx => runMigrations(tx))
384-385-(DoD):
386-- DB opens; can insert + read a boards row.
387-388-------------------------------------------------------------------------------
389-M2. Repo API (what the editor uses)
390-------------------------------------------------------------------------------
391-392-/packages/core/src/persist/web.ts
393-[ ] Define BoardMeta: { id, name, createdAt, updatedAt }.
394-[ ] Implement DocRepo methods:
395- - listBoards(): Promise<BoardMeta[]>
396- - createBoard(name): Promise<boardId>
397- - renameBoard(boardId, name): Promise<void>
398- - deleteBoard(boardId): Promise<void>
399- - loadDoc(boardId): Promise<{ pages, shapes, bindings, order }>
400- - applyDocPatch(boardId, patch): Promise<void>
401-402-Transactions:
403-[ ] Ensure create/delete/applyDocPatch are atomic using db.transaction('rw', ...).
404-405-Bulk writes:
406-[ ] applyDocPatch uses bulkPut / bulkAdd when writing many shapes/pages/bindings.
407-408-(DoD):
409-- createBoard -> applyDocPatch -> loadDoc round-trips a simple doc.
410-411-------------------------------------------------------------------------------
412-M3. Migration system (schema + logical migrations)
413-------------------------------------------------------------------------------
414-415-Schema versions:
416-[ ] When adding indexes/stores:
417- - bump db.version(N).stores(...)
418- - attach .upgrade(tx => ...) for data backfill/reshape
419-420-Logical migrations:
421-[ ] Create `runMigrations(tx)` called from Version.upgrade():
422- - reads applied ids from migrations table
423- - runs missing migrations in order
424- - writes (id, appliedAt) as each completes
425426-Smallest initial migrations:
427-[ ] MIG-0001: backfill boards.createdAt / updatedAt if missing
428-[ ] MIG-0002: ensure every board has a default page row
429430-(DoD):
431-- Upgrading applies each logical migration exactly once.
432433-------------------------------------------------------------------------------
434-M4. History integration (persist on do/undo/redo)
435-------------------------------------------------------------------------------
436-437-Goal:
438-- Any history step that changes the document produces a persistence write.
439-- Undo/redo produces persistence writes too.
440-- UI-only commands do not touch IndexedDB.
441-442-Step 1: Tag commands with persistence intent
443-[ ] In your Command types (history layer), add a doc-impact tag:
444- - affectsDoc: boolean
445- - OR kind: 'doc' | 'ui' | 'camera'
446-447-Rules (v0):
448-- 'doc' commands persist
449-- 'ui' and 'camera' do not persist
450-451-Step 2: Provide a single hook point in history
452-[ ] Add a history callback or event:
453- - onApplied({ kind, beforeState, afterState, commandId, op: 'do'|'undo'|'redo' })
454- This MUST fire after every do/undo/redo.
455-456-Step 3: Compute a persistence patch (smallest practical approach)
457-[ ] Implement `diffDoc(before.doc, after.doc) -> DocPatch` that outputs:
458- - upserts: pages[], shapes[], bindings[]
459- - deletes: pageIds[], shapeIds[], bindingIds[]
460- - order updates (page order, per-page shape order)
461-462- Note:
463- - Keep it minimal: only compare ids + updatedAt (or stable hash) initially.
464- - If this gets complicated, fall back to “persist full doc snapshot” v0 and
465- switch to patching later (but still behind applyDocPatch).
466-467-Step 4: Persist after history events (with batching)
468-[ ] Implement `createPersistenceSink(repo)` that exposes:
469- - enqueueDocPatch(boardId, patch): void
470- - flush(): Promise<void>
471-472-[ ] Wire history -> sink:
473- - if event.kind === 'doc':
474- sink.enqueueDocPatch(boardId, diffDoc(before, after))
475- - else:
476- no-op
477-478-[ ] Batch + flush policy:
479- - debounce 100–250ms
480- - force flush on:
481- - board switch
482- - page unload (best-effort)
483- - explicit Save action
484-485-Step 5: Update updatedAt correctly
486-[ ] On any persisted doc change, update:
487- - boards.updatedAt = now()
488- - updatedAt on modified rows (pages/shapes/bindings) if you store it
489-490-(DoD):
491-- Creating/moving/deleting shapes persists through refresh.
492-- Undo and redo also persist through refresh.
493-- Selection changes do NOT write to Dexie.
494-495-------------------------------------------------------------------------------
496-M5. Export / Import (backups + portability)
497-------------------------------------------------------------------------------
498-499-[ ] Export board:
500- - loadDoc(boardId) -> JSON (doc + meta)
501-[ ] Import board:
502- - createBoard(name)
503- - applyDocPatch(full snapshot as upserts)
504-505-(DoD):
506-- Export -> Import recreates the same drawing.
507-508-------------------------------------------------------------------------------
509-Tests (vitest; use fake IndexedDB)
510-------------------------------------------------------------------------------
511-512-Dexie correctness:
513-[ ] applyDocPatch uses a single transaction for multi-table writes
514-[ ] deleteBoard deletes boards + related rows atomically
515-516-History integration:
517-[ ] doc command do => exactly 1 persistence flush (after debounce)
518-[ ] undo => persists (document matches expected)
519-[ ] redo => persists (document matches expected)
520-[ ] ui-only command => 0 DB writes
521-[ ] batching: 10 rapid doc commands => <= 2 writes (depending on debounce window)
522-523-------------------------------------------------------------------------------
524-Definition of Done
525-------------------------------------------------------------------------------
526-527-- Web: create board, draw, refresh -> content persists.
528-- Undo/redo across refresh works.
529-- Migration system exists and is exercised by at least one schema bump + upgrade.
530-- Renderer redraws from in-memory state; persistence is driven by history events.
531-532-==============================================================================
533-14. Milestone N: Desktop packaging (Tauri) *wb-N*
534-==============================================================================
535536Goal: same app works as a desktop app with filesystem access.
537···548(DoD):
549- Desktop app opens/saves JSON files on disk and reopens them correctly.
550551-==============================================================================
552-15. Milestone O: Export (PNG/SVG) *wb-O*
553-==============================================================================
554555Goal: export drawings as shareable artifacts.
556···567- One-click export works in both web and desktop.
568569570-==============================================================================
571-16. Milestone P: Performance + big docs (pragmatic) *wb-P*
572-==============================================================================
573574Goal: the editor stays responsive with many shapes.
575···592(DoD):
593- 10k simple shapes pans/zooms smoothly on a typical machine.
594595-==============================================================================
596-17. Milestone Q: File Browser (web: Dexie inspector, desktop: FS) *wb-Q*
597-==============================================================================
598599Goal: A unified “Open board” experience:
600- Web: browse Dexie-backed boards + a useful persistence/migration inspector
601- Desktop: browse real directories/files (native file browser semantics)
602603-------------------------------------------------------------------------------
604Q1. Shared UX contracts
605-------------------------------------------------------------------------------
606607/packages/core/src/persist/DocRepo.ts:
608[ ] Define DocRepo interface (web + desktop):
···619(DoD):
620- Svelte UI can render the browser purely from the ViewModel.
621622-------------------------------------------------------------------------------
623Q2. Web: Boards list + Dexie “Inspector” drawer
624-------------------------------------------------------------------------------
625626/apps/web/src/lib/filebrowser/FileBrowser.svelte:
627[ ] Boards panel:
···648(DoD):
649- Web: you can browse boards, open one, and verify migrations + row counts.
650651-------------------------------------------------------------------------------
652Q3. Desktop: real directory + files (Tauri)
653-------------------------------------------------------------------------------
654655[ ] Add “Workspace folder” concept:
656 - pick directory
···668(DoD):
669- Desktop: pick a folder, browse files, open/save boards from disk.
670671-------------------------------------------------------------------------------
672Q4. Parity behaviors
673-------------------------------------------------------------------------------
674675[ ] Same shortcuts:
676 - Ctrl/Cmd+O opens file browser
···681(DoD):
682- Web and desktop feel like the same app, with storage differences made explicit.
683684-==============================================================================
685-18. Milestone R: Quality polish (what makes it feel "real") *wb-R*
686-==============================================================================
687688Goal: the UX crosses the "this is legit" threshold.
689···706(DoD):
707- A user can comfortably draw and edit without surprises.
708709-==============================================================================
710-References (URLs) *wb-refs*
711-==============================================================================
712713tldraw conceptual references (inspiration only):
714- https://tldraw.dev/docs/shapes
···1+================================================================================
2+3Author intent:
4- Build a Svelte-native editor core (TS) + renderer + UI.
5- Keep the "engine" framework-agnostic so Web + Tauri share it.
···9- [ ] Task
10- (DoD) Definition of Done for the milestone
11- Files shown are ideas, not requirements
12+13+================================================================================
14+1. Milestone A: Repo skeleton + dev loop *wb-A*
15+================================================================================
1617Goal: a monorepo that can run a blank canvas in web + desktop.
18···42- `pnpm dev:desktop` launches a Tauri window that shows the same canvas.
43- `pnpm test` runs at least 1 passing core test.
4445+================================================================================
46+2. Milestone B: Math + coordinate systems *wb-B*
47+================================================================================
0000000000000000000000000004849+Camera math, matrix utilities, and transforms are fully implemented and verified
50+so world and screen coordinates map precisely.
5152+================================================================================
53+3. Milestone C: Document model (records) *wb-C*
54+================================================================================
5556+Document/page/shape/binding records plus validation let the editor serialize and
57+reason about drawings safely.
5859+================================================================================
60+4. Milestone D: Store + selectors (reactive core) *wb-D*
61+================================================================================
00006263+The reactive store, invariants, and selectors supply deterministic state streams
64+for both renderer and UI subscribers.
00006566+================================================================================
67+5. Milestone E: Canvas renderer (read-only) *wb-E*
68+================================================================================
0006970+The renderer now draws the document via Canvas2D with camera transforms,
71+DPI scaling, text sizing, and selection outlines.
7273+================================================================================
74+6. Milestone F: Hit testing (picking) *wb-F*
75+================================================================================
7677+Geometry helpers compute bounds and intersections so hit testing can always
78+return the topmost shape under the cursor.
07980+================================================================================
81+7. Milestone G: Input system (pointer + keyboard) *wb-G*
82+================================================================================
8384+Pointer and keyboard adapters now normalize events, map them into actions, and
85+feed the editor consistently across platforms.
8687+================================================================================
88+8. Milestone H: Tool state machine (foundation) *wb-H*
89+================================================================================
009091+Tool interfaces and the router manage lifecycle hooks so each tool is an
92+explicit, testable state machine.
00000930009495+================================================================================
96+9. Milestone I: Select/move tool (MVP interaction) *wb-I*
97+================================================================================
009899+Selection logic handles hit selection, marquee, dragging, deletion, and escape
100+so shapes can be moved reliably.
00000101102+================================================================================
103+10. Milestone J: Create basic shapes via tools *wb-J*
104+================================================================================
0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000105106+Rect, ellipse, line, arrow, and text tools now create shapes via click-drag
107+interactions with proper finalize/cancel behavior.
108109================================================================================
11011. Milestone K: Bindings for arrows (v0) *wb-K*
111================================================================================
112113+Arrow endpoints bind to target shapes and stay attached by recalculating anchors
114+whenever shapes move.
0000000000000000115116================================================================================
11712. Milestone L: History (undo/redo) *wb-L*
118================================================================================
119120+All document-affecting actions run through undoable commands with history stacks
121+and keyboard shortcuts.
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000122123+================================================================================
124+13. Milestone M: Persistence (web) via Dexie + History integration *wb-M*
125+================================================================================
126127+Document changes now persist to IndexedDB via Dexie with migrations, repo API,
128+and history-driven syncing.
129130+================================================================================
131+14. Milestone N: Desktop packaging (Tauri) *wb-N*
132+================================================================================
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000133134Goal: same app works as a desktop app with filesystem access.
135···146(DoD):
147- Desktop app opens/saves JSON files on disk and reopens them correctly.
148149+================================================================================
150+15. Milestone O: Export (PNG/SVG) *wb-O*
151+================================================================================
152153Goal: export drawings as shareable artifacts.
154···165- One-click export works in both web and desktop.
166167168+================================================================================
169+16. Milestone P: Performance + big docs (pragmatic) *wb-P*
170+================================================================================
171172Goal: the editor stays responsive with many shapes.
173···190(DoD):
191- 10k simple shapes pans/zooms smoothly on a typical machine.
192193+================================================================================
194+17. Milestone Q: File Browser (web: Dexie inspector, desktop: FS) *wb-Q*
195+================================================================================
196197Goal: A unified “Open board” experience:
198- Web: browse Dexie-backed boards + a useful persistence/migration inspector
199- Desktop: browse real directories/files (native file browser semantics)
200201+--------------------------------------------------------------------------------
202Q1. Shared UX contracts
203+--------------------------------------------------------------------------------
204205/packages/core/src/persist/DocRepo.ts:
206[ ] Define DocRepo interface (web + desktop):
···217(DoD):
218- Svelte UI can render the browser purely from the ViewModel.
219220+--------------------------------------------------------------------------------
221Q2. Web: Boards list + Dexie “Inspector” drawer
222+--------------------------------------------------------------------------------
223224/apps/web/src/lib/filebrowser/FileBrowser.svelte:
225[ ] Boards panel:
···246(DoD):
247- Web: you can browse boards, open one, and verify migrations + row counts.
248249+--------------------------------------------------------------------------------
250Q3. Desktop: real directory + files (Tauri)
251+--------------------------------------------------------------------------------
252253[ ] Add “Workspace folder” concept:
254 - pick directory
···266(DoD):
267- Desktop: pick a folder, browse files, open/save boards from disk.
268269+--------------------------------------------------------------------------------
270Q4. Parity behaviors
271+--------------------------------------------------------------------------------
272273[ ] Same shortcuts:
274 - Ctrl/Cmd+O opens file browser
···279(DoD):
280- Web and desktop feel like the same app, with storage differences made explicit.
281282+================================================================================
283+18. Milestone R: Quality polish (what makes it feel "real") *wb-R*
284+================================================================================
285286Goal: the UX crosses the "this is legit" threshold.
287···304(DoD):
305- A user can comfortably draw and edit without surprises.
306307+================================================================================
308+References (URLs) *wb-refs*
309+================================================================================
310311tldraw conceptual references (inspiration only):
312- https://tldraw.dev/docs/shapes
···2import type { ShapeRecord } from "./model";
3import type { EditorState } from "./reactivity";
400000000000005/**
6 * Command interface for undo/redo operations
7 *
···10export interface Command {
11 /** Display name for this command (shown in history UI) */
12 readonly name: string;
001314 /**
15 * Execute the command and return the new state
···31 */
32export class CreateShapeCommand implements Command {
33 readonly name: string;
03435 constructor(private readonly shape: ShapeRecord, private readonly pageId: string) {
36 this.name = `Create ${shape.type}`;
···79 */
80export class UpdateShapeCommand implements Command {
81 readonly name: string;
08283 constructor(
84 private readonly shapeId: string,
···102 */
103export class DeleteShapesCommand implements Command {
104 readonly name: string;
0105106 constructor(private readonly shapes: ShapeRecord[], private readonly pageId: string) {
107 this.name = shapes.length === 1 ? `Delete ${shapes[0].type}` : `Delete ${shapes.length} shapes`;
···162 */
163export class SetSelectionCommand implements Command {
164 readonly name = "Change selection";
0165166 constructor(private readonly before: string[], private readonly after: string[]) {}
167···179 */
180export class SetCameraCommand implements Command {
181 readonly name = "Move camera";
0182183 constructor(private readonly before: Camera, private readonly after: Camera) {}
184
···2import type { ShapeRecord } from "./model";
3import type { EditorState } from "./reactivity";
45+export type CommandKind = "doc" | "ui" | "camera";
6+7+export type HistoryOperation = "do" | "undo" | "redo";
8+9+export type HistoryAppliedEvent = {
10+ op: HistoryOperation;
11+ commandId: number;
12+ command: Command;
13+ kind: CommandKind;
14+ beforeState: EditorState;
15+ afterState: EditorState;
16+};
17+18/**
19 * Command interface for undo/redo operations
20 *
···23export interface Command {
24 /** Display name for this command (shown in history UI) */
25 readonly name: string;
26+ /** Command category, used for persistence decisions */
27+ readonly kind: CommandKind;
2829 /**
30 * Execute the command and return the new state
···46 */
47export class CreateShapeCommand implements Command {
48 readonly name: string;
49+ readonly kind = "doc" as const;
5051 constructor(private readonly shape: ShapeRecord, private readonly pageId: string) {
52 this.name = `Create ${shape.type}`;
···95 */
96export class UpdateShapeCommand implements Command {
97 readonly name: string;
98+ readonly kind = "doc" as const;
99100 constructor(
101 private readonly shapeId: string,
···119 */
120export class DeleteShapesCommand implements Command {
121 readonly name: string;
122+ readonly kind = "doc" as const;
123124 constructor(private readonly shapes: ShapeRecord[], private readonly pageId: string) {
125 this.name = shapes.length === 1 ? `Delete ${shapes[0].type}` : `Delete ${shapes.length} shapes`;
···180 */
181export class SetSelectionCommand implements Command {
182 readonly name = "Change selection";
183+ readonly kind = "ui" as const;
184185 constructor(private readonly before: string[], private readonly after: string[]) {}
186···198 */
199export class SetCameraCommand implements Command {
200 readonly name = "Move camera";
201+ readonly kind = "camera" as const;
202203 constructor(private readonly before: Camera, private readonly after: Camera) {}
204
+2
packages/core/src/index.ts
···4export * from "./history";
5export * from "./math";
6export * from "./model";
007export * from "./reactivity";
8export * from "./tools";
···4export * from "./history";
5export * from "./math";
6export * from "./model";
7+export * from "./persistence/db";
8+export * from "./persistence/web";
9export * from "./reactivity";
10export * from "./tools";