web based infinite canvas
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: unify persistence layer

+16 -30
+8 -8
TODO.txt
··· 192 192 -------------------------------------------------------------------------------- 193 193 194 194 /apps/web/src/lib/filebrowser/FileBrowser.svelte: 195 - [ ] Boards panel: 195 + [x] Boards panel: 196 196 - listBoards() -> render 197 197 - search filter 198 198 - open / create / rename / delete 199 199 200 200 Inspector drawer (selected board): 201 - [ ] Show "Storage" 202 - [ ] Show schema info: 201 + [x] Show "Storage" 202 + [x] Show schema info: 203 203 - declared schema version 204 204 - installed schema version 205 - [ ] Show board-level stats (computed live): 205 + [x] Show board-level stats (computed live): 206 206 - row counts: pages/shapes/bindings for this board & doc size bytes for docs row 207 207 - last updatedAt 208 - [ ] Show migration info: 208 + [x] Show migration info: 209 209 - list applied migrations from migrations table (id + appliedAt) 210 210 - show "pending" migrations if any (based on known list vs applied) 211 211 212 212 Safe deletes: 213 - [ ] deleteBoard must be a single atomic transaction (boards + related tables) 213 + [x] deleteBoard must be a single atomic transaction (boards + related tables) 214 214 215 215 (DoD): 216 216 - Web: you can browse boards, open one, and verify migrations + row counts. ··· 227 227 - tree view with folders 228 228 [ ] Implement file actions: 229 229 - [x] New: create new file 230 - - [ ] Rename: rename file 231 - - [ ] Delete: delete file 230 + - [ ] Rename: rename file (Tauri command needed) 231 + - [ ] Delete: delete file (Tauri command needed) 232 232 - [x] Open: load file into editor 233 233 - [x] Export: save JSON 234 234
-1
apps/web/src/lib/canvas/canvas-store.svelte.ts
··· 430 430 let inputAdapter: InputAdapter | null = null; 431 431 let canvasInitialized = false; 432 432 433 - // Initialize canvas-dependent systems when canvas becomes available 434 433 $effect(() => { 435 434 if (!canvas || canvasInitialized) return; 436 435
+1 -1
apps/web/src/lib/tests/Canvas.history.test.ts
··· 332 332 333 333 it("wraps pointer actions in SnapshotCommands and enqueues persistence", async () => { 334 334 render(Canvas); 335 - // Wait for onMount to complete and input adapter to be created 335 + 336 336 await vi.waitFor(() => { 337 337 expect(actionHandlers.length).toBeGreaterThan(0); 338 338 });
-12
packages/core/src/export.ts
··· 59 59 return null; 60 60 } 61 61 62 - // Calculate combined bounds 63 62 const bounds = combineBounds(shapes.map((s) => shapeBounds(s))); 64 63 if (!bounds) { 65 64 return null; 66 65 } 67 66 68 - // Add padding 69 67 const padding = 20; 70 68 const width = Box2Ops.width(bounds) + padding * 2; 71 69 const height = Box2Ops.height(bounds) + padding * 2; 72 70 73 - // Create temporary canvas 74 71 const canvas = document.createElement("canvas"); 75 72 canvas.width = width; 76 73 canvas.height = height; ··· 80 77 throw new Error("Failed to get 2D context"); 81 78 } 82 79 83 - // Clear background (white) 84 80 context.fillStyle = "white"; 85 81 context.fillRect(0, 0, width, height); 86 82 87 - // Translate to handle bounds offset + padding 88 83 context.save(); 89 84 context.translate(-bounds.min.x + padding, -bounds.min.y + padding); 90 85 91 - // Let caller render the shapes 92 86 renderFunction(context, shapes, bounds); 93 87 94 88 context.restore(); 95 89 96 - // Export to PNG 97 90 return new Promise((resolve, reject) => { 98 91 canvas.toBlob((blob) => { 99 92 if (blob) { ··· 122 115 return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"></svg>"; 123 116 } 124 117 125 - // Calculate bounds 126 118 const bounds = combineBounds(shapes.map((s) => shapeBounds(s))); 127 119 if (!bounds) { 128 120 return "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"></svg>"; ··· 136 128 137 129 const elements: string[] = [`<rect x="${offsetX}" y="${offsetY}" width="${width}" height="${height}" fill="white"/>`]; 138 130 139 - // Add white background 140 - 141 - // Render each shape 142 131 for (const shape of shapes) { 143 132 const svg = shapeToSVG(shape, state); 144 133 if (svg) { ··· 217 206 function arrowToSVG(shape: ArrowShape, transform: string, _state: EditorState): string { 218 207 const { a, b, stroke, width } = shape.props; 219 208 220 - // Calculate arrow head 221 209 const angle = Math.atan2(b.y - a.y, b.x - a.x); 222 210 const arrowLength = 15; 223 211 const arrowAngle = Math.PI / 6;
+1 -1
packages/core/src/index.ts
··· 6 6 export * from "./history"; 7 7 export * from "./math"; 8 8 export * from "./model"; 9 - export * from "./persist/DocRepo"; 10 9 export * from "./persistence/db"; 11 10 export * from "./persistence/desktop"; 11 + export * from "./persistence/repo"; 12 12 export * from "./persistence/stats"; 13 13 export * from "./persistence/web"; 14 14 export * from "./reactivity";
-1
packages/core/src/persist/DocRepo.ts packages/core/src/persistence/repo.ts
··· 4 4 5 5 /** 6 6 * Shared document repository contract used by both web and desktop persistence layers. 7 - * Provides the minimal operations required for listing and managing boards. 8 7 */ 9 8 export interface DocRepo { 10 9 /**
+1 -1
packages/core/src/persistence/db.ts
··· 1 1 import Dexie, { type Transaction } from "dexie"; 2 2 import { PageRecord as PageOps } from "../model"; 3 - import type { BoardMeta, Timestamp } from "../persist/DocRepo"; 3 + import type { BoardMeta, Timestamp } from "./repo"; 4 4 import type { BindingRow, MetaRow, MigrationRow, PageRow, ShapeRow } from "./web"; 5 5 6 6 export const DB_NAME = "inkfinite";
+1 -1
packages/core/src/persistence/desktop.ts
··· 1 1 import type { BindingRecord, Document, PageRecord, ShapeRecord } from "../model"; 2 - import type { BoardMeta } from "../persist/DocRepo"; 2 + import type { BoardMeta } from "./repo"; 3 3 import type { DocOrder, LoadedDoc } from "./web"; 4 4 5 5 /**
+1 -1
packages/core/src/persistence/stats.ts
··· 1 - import type { Timestamp } from "../persist/DocRepo"; 1 + import type { Timestamp } from "./repo"; 2 2 3 3 export type BoardStats = { 4 4 pageCount: number;
+1 -1
packages/core/src/persistence/web.ts
··· 9 9 type ShapeRecord, 10 10 ShapeRecord as ShapeOps, 11 11 } from "../model"; 12 - import type { BoardMeta, DocRepo, Timestamp } from "../persist/DocRepo"; 12 + import type { BoardMeta, DocRepo, Timestamp } from "./repo"; 13 13 import type { BoardInspectorData, BoardStats, MigrationInfo, SchemaInfo } from "./stats"; 14 14 import { BoardStatsOps, getPendingMigrations } from "./stats"; 15 15
+1 -1
packages/core/src/ui/filebrowser.ts
··· 1 - import type { BoardMeta, DocRepo } from "../persist/DocRepo"; 1 + import type { BoardMeta, DocRepo } from "../persistence/repo"; 2 2 3 3 export type FileBrowserActions = { 4 4 open(boardId: string): Promise<void>;
+1 -1
packages/core/tests/filebrowser.test.ts
··· 1 1 import { describe, expect, it, vi } from "vitest"; 2 - import type { DocRepo } from "../src/persist/DocRepo"; 2 + import type { DocRepo } from "../src/persistence/repo"; 3 3 import { FileBrowserVM } from "../src/ui/filebrowser"; 4 4 5 5 function createRepoMock(): DocRepo {