pixelweaver 0.1.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 (392) hide show
  1. package/.env.development +1 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/.github/workflows/publish.yml +18 -0
  4. package/.prettierignore +5 -0
  5. package/.prettierrc +16 -0
  6. package/.python-version +1 -0
  7. package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
  8. package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
  9. package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
  10. package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
  11. package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
  12. package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
  13. package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
  14. package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
  15. package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
  16. package/.rlsbl/bases/CHANGELOG.md +5 -0
  17. package/.rlsbl/bases/LICENSE +21 -0
  18. package/.rlsbl/changes/.validated +1 -0
  19. package/.rlsbl/changes/0.1.0.jsonl +85 -0
  20. package/.rlsbl/changes/0.1.0.md +13 -0
  21. package/.rlsbl/changes/unreleased.jsonl +0 -0
  22. package/.rlsbl/config.json +6 -0
  23. package/.rlsbl/hashes.json +14 -0
  24. package/.rlsbl/hooks/post-release.sh +8 -0
  25. package/.rlsbl/hooks/pre-checks.sh +5 -0
  26. package/.rlsbl/hooks/pre-release.sh +8 -0
  27. package/.rlsbl/lint/go.toml +17 -0
  28. package/.rlsbl/lint/npm.toml +19 -0
  29. package/.rlsbl/lint/python.toml +25 -0
  30. package/.rlsbl/releases/unreleased.toml +0 -0
  31. package/.rlsbl/releases/v0.1.0.toml +3 -0
  32. package/.rlsbl/version +1 -0
  33. package/.selfdoc/hashes/hashes.json +146 -0
  34. package/.strictcli/schema.json +227 -0
  35. package/CHANGELOG.md +17 -0
  36. package/CLAUDE.md +100 -0
  37. package/LICENSE +21 -0
  38. package/README.md +116 -0
  39. package/assets/icon.png +0 -0
  40. package/docs/_README.md +117 -0
  41. package/docs/cli-config.md +35 -0
  42. package/docs/cli-dev.md +21 -0
  43. package/docs/cli-diagnose.md +12 -0
  44. package/docs/cli-index.md +30 -0
  45. package/docs/cli-list.md +18 -0
  46. package/docs/cli-mcp.md +18 -0
  47. package/docs/cli-new.md +26 -0
  48. package/docs/cli-open.md +21 -0
  49. package/docs/cli-serve.md +21 -0
  50. package/docs/cli-stop.md +12 -0
  51. package/docs/gen-index.md +36 -0
  52. package/docs/index.md +13 -0
  53. package/docs/server-src-pixelweaver-__main__.md +12 -0
  54. package/docs/server-src-pixelweaver-autosave.md +12 -0
  55. package/docs/server-src-pixelweaver-bridge.md +12 -0
  56. package/docs/server-src-pixelweaver-cli.md +12 -0
  57. package/docs/server-src-pixelweaver-config.md +12 -0
  58. package/docs/server-src-pixelweaver-connections.md +12 -0
  59. package/docs/server-src-pixelweaver-main.md +12 -0
  60. package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
  61. package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
  62. package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
  63. package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
  64. package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
  65. package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
  66. package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
  67. package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
  68. package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
  69. package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
  70. package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
  71. package/docs/server-src-pixelweaver-mcp_server.md +12 -0
  72. package/docs/server-src-pixelweaver-protocol.md +12 -0
  73. package/docs/server-src-pixelweaver-state.md +12 -0
  74. package/docs/server-src-pixelweaver-storage.md +12 -0
  75. package/docs/server-src-pixelweaver-websocket.md +12 -0
  76. package/docs/server-src-pixelweaver.md +12 -0
  77. package/e2e/app-launch.test.ts +35 -0
  78. package/e2e/menus.test.ts +26 -0
  79. package/e2e/tools.test.ts +27 -0
  80. package/e2e/undo-redo.test.ts +11 -0
  81. package/eslint.config.js +62 -0
  82. package/index.html +13 -0
  83. package/package.json +48 -0
  84. package/playwright.config.ts +19 -0
  85. package/plugins/builtin/.gitkeep +0 -0
  86. package/plugins/builtin/advanced-fill-tool.ts +146 -0
  87. package/plugins/builtin/circle-tool.ts +186 -0
  88. package/plugins/builtin/diamond-tool.ts +182 -0
  89. package/plugins/builtin/dither-tool.ts +186 -0
  90. package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
  91. package/plugins/builtin/drawing-utils.test.ts +495 -0
  92. package/plugins/builtin/drawing-utils.ts +431 -0
  93. package/plugins/builtin/effects/blur.ts +97 -0
  94. package/plugins/builtin/effects/color-effects.ts +278 -0
  95. package/plugins/builtin/effects/flip.ts +83 -0
  96. package/plugins/builtin/effects/glow.ts +118 -0
  97. package/plugins/builtin/effects/outline.ts +110 -0
  98. package/plugins/builtin/effects/rotate.ts +357 -0
  99. package/plugins/builtin/effects/scale.ts +258 -0
  100. package/plugins/builtin/effects/shadow.ts +111 -0
  101. package/plugins/builtin/effects/sharpen.ts +102 -0
  102. package/plugins/builtin/effects.test.ts +715 -0
  103. package/plugins/builtin/eraser-tool.ts +23 -0
  104. package/plugins/builtin/eyedropper-tool.ts +83 -0
  105. package/plugins/builtin/fill-tool.ts +93 -0
  106. package/plugins/builtin/gradient-tool.ts +204 -0
  107. package/plugins/builtin/gradient.test.ts +142 -0
  108. package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
  109. package/plugins/builtin/importers/aseprite-parser.ts +497 -0
  110. package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
  111. package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
  112. package/plugins/builtin/line-tool.ts +267 -0
  113. package/plugins/builtin/make-stroke-tool.ts +271 -0
  114. package/plugins/builtin/noise-dither.test.ts +151 -0
  115. package/plugins/builtin/noise-tool.ts +131 -0
  116. package/plugins/builtin/pattern-stamp-tool.ts +162 -0
  117. package/plugins/builtin/pencil-tool.ts +25 -0
  118. package/plugins/builtin/rect-tool.ts +179 -0
  119. package/plugins/builtin/selection-tool.ts +388 -0
  120. package/plugins/builtin/selection.test.ts +195 -0
  121. package/plugins/builtin/tool-plugins.test.ts +529 -0
  122. package/public/favicon.svg +7 -0
  123. package/public/sw.js +91 -0
  124. package/pyproject.toml +49 -0
  125. package/scripts/eslint-wave-a-list.sh +24 -0
  126. package/scripts/eslint-wave-a-status.sh +25 -0
  127. package/scripts/fix-index-signature-access.py +127 -0
  128. package/scripts/fix-unchecked-index.py +128 -0
  129. package/scripts/fix-unchecked-vars.py +127 -0
  130. package/scripts/fix-wave-a-bangs.py +36 -0
  131. package/scripts/fix-wave-a-templates.py +108 -0
  132. package/scripts/fix-wave-b-void-expr.py +167 -0
  133. package/scripts/generate-command-params.py +540 -0
  134. package/scripts/migrate-single-frame-to-multi.py +171 -0
  135. package/scripts/smoke-test.sh +77 -0
  136. package/selfdoc.json +10 -0
  137. package/server/README.md +0 -0
  138. package/server/src/pixelweaver/__init__.py +1 -0
  139. package/server/src/pixelweaver/__main__.py +4 -0
  140. package/server/src/pixelweaver/autosave.py +114 -0
  141. package/server/src/pixelweaver/bridge.py +127 -0
  142. package/server/src/pixelweaver/cli.py +199 -0
  143. package/server/src/pixelweaver/config.py +24 -0
  144. package/server/src/pixelweaver/connections.py +54 -0
  145. package/server/src/pixelweaver/main.py +271 -0
  146. package/server/src/pixelweaver/mcp_bridge.py +189 -0
  147. package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
  148. package/server/src/pixelweaver/mcp_export_tools.py +291 -0
  149. package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
  150. package/server/src/pixelweaver/mcp_history_tools.py +106 -0
  151. package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
  152. package/server/src/pixelweaver/mcp_lock.py +37 -0
  153. package/server/src/pixelweaver/mcp_project_tools.py +302 -0
  154. package/server/src/pixelweaver/mcp_read_tools.py +163 -0
  155. package/server/src/pixelweaver/mcp_registry.py +247 -0
  156. package/server/src/pixelweaver/mcp_resources.py +312 -0
  157. package/server/src/pixelweaver/mcp_server.py +234 -0
  158. package/server/src/pixelweaver/protocol.py +219 -0
  159. package/server/src/pixelweaver/state.py +267 -0
  160. package/server/src/pixelweaver/storage.py +293 -0
  161. package/server/src/pixelweaver/websocket.py +282 -0
  162. package/server/tests/__init__.py +0 -0
  163. package/server/tests/conftest.py +17 -0
  164. package/server/tests/test_api.py +96 -0
  165. package/server/tests/test_bridge.py +161 -0
  166. package/server/tests/test_health.py +9 -0
  167. package/server/tests/test_integration.py +86 -0
  168. package/server/tests/test_mcp_bridge.py +293 -0
  169. package/server/tests/test_mcp_lock.py +34 -0
  170. package/server/tests/test_mcp_registry.py +679 -0
  171. package/server/tests/test_mcp_resources.py +648 -0
  172. package/server/tests/test_protocol.py +125 -0
  173. package/server/tests/test_state.py +87 -0
  174. package/server/tests/test_storage.py +306 -0
  175. package/server/tests/test_websocket.py +275 -0
  176. package/src/App.svelte +107 -0
  177. package/src/app.css +215 -0
  178. package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
  179. package/src/lib/animation/animation-commands.test.ts +228 -0
  180. package/src/lib/animation/animation-commands.ts +540 -0
  181. package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
  182. package/src/lib/animation/animation-preview.svelte.ts +151 -0
  183. package/src/lib/animation/clipboard.ts +134 -0
  184. package/src/lib/animation/frame-model.svelte.ts +437 -0
  185. package/src/lib/animation/frame-model.test.ts +314 -0
  186. package/src/lib/animation/frame-selection.svelte.ts +77 -0
  187. package/src/lib/animation/frame-tags.svelte.ts +238 -0
  188. package/src/lib/animation/frame-tags.test.ts +136 -0
  189. package/src/lib/animation/import.test.ts +141 -0
  190. package/src/lib/animation/import.ts +112 -0
  191. package/src/lib/animation/spritesheet-export.test.ts +239 -0
  192. package/src/lib/animation/spritesheet-export.ts +314 -0
  193. package/src/lib/canvas/CanvasViewport.svelte +216 -0
  194. package/src/lib/canvas/canvas-init-plugin.ts +178 -0
  195. package/src/lib/canvas/canvas-renderer.ts +408 -0
  196. package/src/lib/canvas/canvas-state.svelte.ts +232 -0
  197. package/src/lib/canvas/canvas-state.test.ts +139 -0
  198. package/src/lib/canvas/input-handler.ts +221 -0
  199. package/src/lib/canvas/input-plugin.ts +150 -0
  200. package/src/lib/canvas/onion-skin.ts +94 -0
  201. package/src/lib/canvas/pixel-buffer.test.ts +249 -0
  202. package/src/lib/canvas/pixel-buffer.ts +151 -0
  203. package/src/lib/canvas/render-state.svelte.ts +18 -0
  204. package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
  205. package/src/lib/canvas/tile-mode.test.ts +53 -0
  206. package/src/lib/canvas/tile-mode.ts +92 -0
  207. package/src/lib/canvas/viewport-utils.ts +24 -0
  208. package/src/lib/canvas/zoom-utils.ts +31 -0
  209. package/src/lib/color/.gitkeep +0 -0
  210. package/src/lib/color/color-commands.ts +87 -0
  211. package/src/lib/color/color-state.svelte.ts +98 -0
  212. package/src/lib/color/color-state.test.ts +91 -0
  213. package/src/lib/color/color-utils.test.ts +220 -0
  214. package/src/lib/color/color-utils.ts +243 -0
  215. package/src/lib/color/palette-state.svelte.ts +127 -0
  216. package/src/lib/color/palette-state.test.ts +154 -0
  217. package/src/lib/color/palette.ts +79 -0
  218. package/src/lib/core/bootstrap.ts +66 -0
  219. package/src/lib/core/command-params.generated.ts +1549 -0
  220. package/src/lib/core/command-params.ts +20 -0
  221. package/src/lib/core/command-runner.ts +79 -0
  222. package/src/lib/core/commands.ts +134 -0
  223. package/src/lib/core/dispatcher.test.ts +548 -0
  224. package/src/lib/core/dispatcher.ts +361 -0
  225. package/src/lib/core/index.test.ts +7 -0
  226. package/src/lib/core/notification-state.svelte.ts +119 -0
  227. package/src/lib/core/plugin-api.ts +210 -0
  228. package/src/lib/core/plugin-discovery.test.ts +53 -0
  229. package/src/lib/core/plugin-discovery.ts +65 -0
  230. package/src/lib/core/plugin-loader.test.ts +159 -0
  231. package/src/lib/core/plugin-loader.ts +240 -0
  232. package/src/lib/core/plugin-types.ts +286 -0
  233. package/src/lib/core/registries.svelte.ts +74 -0
  234. package/src/lib/core/tool-options-state.svelte.ts +61 -0
  235. package/src/lib/dock/DockLayout.svelte +375 -0
  236. package/src/lib/dock/TabAddMenu.svelte +90 -0
  237. package/src/lib/dock/dock-persistence.ts +46 -0
  238. package/src/lib/dock/dock-plugin.ts +49 -0
  239. package/src/lib/dock/dock-presets.ts +156 -0
  240. package/src/lib/dock/dock-state.svelte.ts +77 -0
  241. package/src/lib/dock/dock-types.ts +2 -0
  242. package/src/lib/dock/dockview-theme.css +226 -0
  243. package/src/lib/dock/dockview-theme.ts +17 -0
  244. package/src/lib/dock/header-action-renderer.ts +77 -0
  245. package/src/lib/edit/clipboard-state.ts +34 -0
  246. package/src/lib/export/download.ts +201 -0
  247. package/src/lib/export/png-metadata.ts +181 -0
  248. package/src/lib/history/ActionLogPanel.svelte +418 -0
  249. package/src/lib/history/action-log-panel-plugin.ts +24 -0
  250. package/src/lib/history/action-log.svelte.ts +172 -0
  251. package/src/lib/history/action-log.test.ts +168 -0
  252. package/src/lib/history/history-commands.ts +403 -0
  253. package/src/lib/history/macros.svelte.ts +320 -0
  254. package/src/lib/history/macros.test.ts +224 -0
  255. package/src/lib/history/replay-engine.test.ts +241 -0
  256. package/src/lib/history/replay-engine.ts +149 -0
  257. package/src/lib/history/spec-format.test.ts +250 -0
  258. package/src/lib/history/spec-format.ts +210 -0
  259. package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
  260. package/src/lib/iso/iso-math.test.ts +77 -0
  261. package/src/lib/iso/iso-math.ts +77 -0
  262. package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
  263. package/src/lib/iso/seam-checker.test.ts +126 -0
  264. package/src/lib/iso/seam-checker.ts +93 -0
  265. package/src/lib/iso/tessellation.ts +67 -0
  266. package/src/lib/layers/compositor.test.ts +193 -0
  267. package/src/lib/layers/compositor.ts +175 -0
  268. package/src/lib/layers/layer-commands.test.ts +263 -0
  269. package/src/lib/layers/layer-commands.ts +429 -0
  270. package/src/lib/layers/layer-tree.svelte.ts +516 -0
  271. package/src/lib/layers/layer-tree.test.ts +383 -0
  272. package/src/lib/layers/layer-types.ts +56 -0
  273. package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
  274. package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
  275. package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
  276. package/src/lib/leveleditor/depth-sort.test.ts +70 -0
  277. package/src/lib/leveleditor/depth-sort.ts +39 -0
  278. package/src/lib/leveleditor/level-editor-commands.ts +353 -0
  279. package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
  280. package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
  281. package/src/lib/leveleditor/map-state.svelte.ts +372 -0
  282. package/src/lib/leveleditor/map-state.test.ts +243 -0
  283. package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
  284. package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
  285. package/src/lib/leveleditor/tiled-export.test.ts +144 -0
  286. package/src/lib/leveleditor/tiled-export.ts +374 -0
  287. package/src/lib/pywebview.d.ts +15 -0
  288. package/src/lib/recovery/.gitkeep +0 -0
  289. package/src/lib/recovery/idb-store.ts +118 -0
  290. package/src/lib/recovery/recovery-manager.test.ts +321 -0
  291. package/src/lib/recovery/recovery-manager.ts +115 -0
  292. package/src/lib/recovery/recovery-plugin.ts +55 -0
  293. package/src/lib/recovery/recovery-state.svelte.ts +21 -0
  294. package/src/lib/recovery/sw-plugin.ts +18 -0
  295. package/src/lib/recovery/sw-register.ts +17 -0
  296. package/src/lib/save/directory-format.ts +42 -0
  297. package/src/lib/save/project-snapshot.ts +139 -0
  298. package/src/lib/save/recent-projects.ts +56 -0
  299. package/src/lib/save/save-state.svelte.ts +35 -0
  300. package/src/lib/save/storage.ts +167 -0
  301. package/src/lib/save/zip-format.ts +45 -0
  302. package/src/lib/shortcuts/.gitkeep +0 -0
  303. package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
  304. package/src/lib/shortcuts/default-bindings.ts +61 -0
  305. package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
  306. package/src/lib/shortcuts/shortcut-init.ts +380 -0
  307. package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
  308. package/src/lib/shortcuts/shortcut-manager.ts +281 -0
  309. package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
  310. package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
  311. package/src/lib/sync/patch-applicator.ts +300 -0
  312. package/src/lib/sync/patch-types.ts +65 -0
  313. package/src/lib/sync/project-manager.ts +108 -0
  314. package/src/lib/sync/sync-init.ts +152 -0
  315. package/src/lib/sync/sync-plugin.ts +19 -0
  316. package/src/lib/sync/sync-state.svelte.ts +56 -0
  317. package/src/lib/sync/ws-client.test.ts +604 -0
  318. package/src/lib/sync/ws-client.ts +574 -0
  319. package/src/lib/tools/.gitkeep +0 -0
  320. package/src/lib/ui/.gitkeep +0 -0
  321. package/src/lib/ui/AboutDialog.svelte +113 -0
  322. package/src/lib/ui/ColorPicker.svelte +761 -0
  323. package/src/lib/ui/ContextMenu.svelte +216 -0
  324. package/src/lib/ui/ExportDialog.svelte +747 -0
  325. package/src/lib/ui/FrameStrip.svelte +854 -0
  326. package/src/lib/ui/LayerPanel.svelte +810 -0
  327. package/src/lib/ui/MenuBar.svelte +590 -0
  328. package/src/lib/ui/NewProjectDialog.svelte +803 -0
  329. package/src/lib/ui/PluginManagerPanel.svelte +475 -0
  330. package/src/lib/ui/PromptDialog.svelte +252 -0
  331. package/src/lib/ui/RecoveryDialog.svelte +295 -0
  332. package/src/lib/ui/ResizeDialog.svelte +416 -0
  333. package/src/lib/ui/StatusBar.svelte +145 -0
  334. package/src/lib/ui/ToolbarPanel.svelte +488 -0
  335. package/src/lib/ui/animation-menu-commands.ts +194 -0
  336. package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
  337. package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
  338. package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
  339. package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
  340. package/src/lib/ui/dialog-state.svelte.ts +70 -0
  341. package/src/lib/ui/edit-commands.ts +271 -0
  342. package/src/lib/ui/file-commands.ts +275 -0
  343. package/src/lib/ui/file-open.ts +99 -0
  344. package/src/lib/ui/help-commands.ts +93 -0
  345. package/src/lib/ui/image-commands.ts +181 -0
  346. package/src/lib/ui/layer-menu-commands.ts +420 -0
  347. package/src/lib/ui/menu-builder.ts +224 -0
  348. package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
  349. package/src/lib/ui/notifications/notification-plugin.ts +29 -0
  350. package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
  351. package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
  352. package/src/lib/ui/plugin-state.svelte.ts +62 -0
  353. package/src/lib/ui/select-commands.ts +75 -0
  354. package/src/lib/ui/theme-plugin.ts +18 -0
  355. package/src/lib/ui/theme.svelte.ts +45 -0
  356. package/src/lib/ui/theme.test.ts +51 -0
  357. package/src/lib/ui/toolbar-config.ts +90 -0
  358. package/src/lib/ui/toolbar-plugin.ts +39 -0
  359. package/src/lib/ui/view-commands.ts +629 -0
  360. package/src/lib/variants/BisectionExportDialog.svelte +500 -0
  361. package/src/lib/variants/VariantPanel.svelte +822 -0
  362. package/src/lib/variants/bisection-export.test.ts +113 -0
  363. package/src/lib/variants/bisection-export.ts +148 -0
  364. package/src/lib/variants/palette-extraction.test.ts +111 -0
  365. package/src/lib/variants/palette-extraction.ts +84 -0
  366. package/src/lib/variants/palette-interpolation.test.ts +113 -0
  367. package/src/lib/variants/palette-interpolation.ts +87 -0
  368. package/src/lib/variants/palette-swap.test.ts +101 -0
  369. package/src/lib/variants/palette-swap.ts +114 -0
  370. package/src/lib/variants/variant-commands.ts +594 -0
  371. package/src/lib/variants/variant-panel-plugin.ts +27 -0
  372. package/src/lib/variants/variant-randomizer.ts +101 -0
  373. package/src/lib/variants/variant-state.svelte.ts +166 -0
  374. package/src/lib/variants/variant-state.test.ts +138 -0
  375. package/src/main.ts +14 -0
  376. package/src/vite-env.d.ts +3 -0
  377. package/svelte.config.js +2 -0
  378. package/todo/.done/audit-design-decisions.md +812 -0
  379. package/todo/.done/audit-implementation-plan.md +1235 -0
  380. package/todo/.done/happy-path-polish.md +177 -0
  381. package/todo/.done/pixelweaver-full-build.md +937 -0
  382. package/todo/.done/server-multi-frame-design.md +405 -0
  383. package/todo/.done/typed-dispatcher-design.md +435 -0
  384. package/todo/.done/unified-toolbar-and-action-system.md +323 -0
  385. package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
  386. package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
  387. package/todo/comprehensive-audit.md +1085 -0
  388. package/tsconfig.app.json +26 -0
  389. package/tsconfig.json +7 -0
  390. package/tsconfig.node.json +20 -0
  391. package/uv.lock +1167 -0
  392. package/vite.config.ts +32 -0
@@ -0,0 +1,118 @@
1
+ /**
2
+ * IndexedDB Storage -- thin wrapper for storing crash-recovery snapshots.
3
+ *
4
+ * Stores a single "latest" snapshot that is overwritten on each save.
5
+ * All operations are async and handle errors gracefully by logging
6
+ * warnings rather than throwing (IndexedDB failures should not crash the app).
7
+ */
8
+
9
+ const DB_NAME = 'pixelweaver-recovery';
10
+ const STORE_NAME = 'snapshots';
11
+ const DB_VERSION = 1;
12
+
13
+ export interface RecoverySnapshot {
14
+ /** Always "latest" -- we only keep one snapshot at a time. */
15
+ id: string;
16
+ timestamp: number;
17
+ projectName: string;
18
+ /** Serialized canvas states keyed by canvas ID. */
19
+ canvasStates: Record<string, {
20
+ width: number;
21
+ height: number;
22
+ layerTree: unknown; // serialized LayerTreeState
23
+ pixelData: Record<string, ArrayBuffer>; // layer ID -> RGBA bytes
24
+ }>;
25
+ /** Commands sent to the server but not yet acknowledged. */
26
+ pendingCommands: unknown[];
27
+ }
28
+
29
+ /**
30
+ * Open (or create) the recovery database.
31
+ * Creates the object store on first run or version upgrade.
32
+ */
33
+ export async function openDB(): Promise<IDBDatabase> {
34
+ return new Promise<IDBDatabase>((resolve, reject) => {
35
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
36
+
37
+ request.onupgradeneeded = () => {
38
+ const db = request.result;
39
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
40
+ db.createObjectStore(STORE_NAME, { keyPath: 'id' });
41
+ }
42
+ };
43
+
44
+ request.onsuccess = () => { resolve(request.result); };
45
+ request.onerror = () => { reject(request.error ?? new Error('IndexedDB open failed')); };
46
+ });
47
+ }
48
+
49
+ /** Persist a recovery snapshot (overwrites any existing one). */
50
+ export async function saveSnapshot(snapshot: RecoverySnapshot): Promise<void> {
51
+ try {
52
+ const db = await openDB();
53
+ await new Promise<void>((resolve, reject) => {
54
+ const tx = db.transaction(STORE_NAME, 'readwrite');
55
+ const store = tx.objectStore(STORE_NAME);
56
+ store.put(snapshot);
57
+ tx.oncomplete = () => { db.close(); resolve(); };
58
+ tx.onerror = () => { db.close(); reject(tx.error ?? new Error('IndexedDB save transaction failed')); };
59
+ });
60
+ } catch (err) {
61
+ console.warn('[recovery] Failed to save snapshot:', err);
62
+ }
63
+ }
64
+
65
+ /** Load the most recent recovery snapshot, or null if none exists. */
66
+ export async function loadSnapshot(): Promise<RecoverySnapshot | null> {
67
+ try {
68
+ const db = await openDB();
69
+ return await new Promise<RecoverySnapshot | null>((resolve, reject) => {
70
+ const tx = db.transaction(STORE_NAME, 'readonly');
71
+ const store = tx.objectStore(STORE_NAME);
72
+ const request = store.get('latest');
73
+ request.onsuccess = () => {
74
+ db.close();
75
+ // IDBRequest.result is `any`; narrow to our shape before resolving.
76
+ resolve((request.result as RecoverySnapshot | undefined) ?? null);
77
+ };
78
+ request.onerror = () => { db.close(); reject(request.error ?? new Error('IndexedDB load request failed')); };
79
+ });
80
+ } catch (err) {
81
+ console.warn('[recovery] Failed to load snapshot:', err);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /** Delete all recovery snapshots. */
87
+ export async function clearSnapshot(): Promise<void> {
88
+ try {
89
+ const db = await openDB();
90
+ await new Promise<void>((resolve, reject) => {
91
+ const tx = db.transaction(STORE_NAME, 'readwrite');
92
+ const store = tx.objectStore(STORE_NAME);
93
+ store.clear();
94
+ tx.oncomplete = () => { db.close(); resolve(); };
95
+ tx.onerror = () => { db.close(); reject(tx.error ?? new Error('IndexedDB clear transaction failed')); };
96
+ });
97
+ } catch (err) {
98
+ console.warn('[recovery] Failed to clear snapshot:', err);
99
+ }
100
+ }
101
+
102
+ /** Check whether a recovery snapshot exists without loading its data. */
103
+ export async function hasSnapshot(): Promise<boolean> {
104
+ try {
105
+ const db = await openDB();
106
+ return await new Promise<boolean>((resolve, reject) => {
107
+ const tx = db.transaction(STORE_NAME, 'readonly');
108
+ const store = tx.objectStore(STORE_NAME);
109
+ // count with a key range of just "latest" is cheaper than get
110
+ const request = store.count(IDBKeyRange.only('latest'));
111
+ request.onsuccess = () => { db.close(); resolve(request.result > 0); };
112
+ request.onerror = () => { db.close(); reject(request.error ?? new Error('IndexedDB count request failed')); };
113
+ });
114
+ } catch (err) {
115
+ console.warn('[recovery] Failed to check snapshot:', err);
116
+ return false;
117
+ }
118
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Tests for RecoveryManager.
3
+ *
4
+ * IndexedDB is not available in jsdom, so we mock the idb-store module
5
+ * and focus on testing the manager's coordination logic:
6
+ * - pending command tracking (add, remove, list)
7
+ * - snapshot interval lifecycle (start, stop)
8
+ * - snapshot save triggered periodically and on visibility change
9
+ * - state gatherer integration
10
+ */
11
+
12
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
13
+ import { RecoveryManager } from './recovery-manager.js';
14
+ import type { RecoverySnapshot } from './idb-store.js';
15
+
16
+ // --- Mock the IndexedDB store ---
17
+
18
+ vi.mock('./idb-store.js', () => ({
19
+ saveSnapshot: vi.fn<(snapshot: RecoverySnapshot) => Promise<void>>().mockResolvedValue(undefined),
20
+ loadSnapshot: vi.fn<() => Promise<RecoverySnapshot | null>>().mockResolvedValue(null),
21
+ clearSnapshot: vi.fn<() => Promise<void>>().mockResolvedValue(undefined),
22
+ hasSnapshot: vi.fn<() => Promise<boolean>>().mockResolvedValue(false),
23
+ }));
24
+
25
+ // Import after mocking so we get the mocked versions
26
+ import { saveSnapshot, loadSnapshot, clearSnapshot, hasSnapshot } from './idb-store.js';
27
+
28
+ const mockedSaveSnapshot = vi.mocked(saveSnapshot);
29
+ const mockedLoadSnapshot = vi.mocked(loadSnapshot);
30
+ const mockedClearSnapshot = vi.mocked(clearSnapshot);
31
+ const mockedHasSnapshot = vi.mocked(hasSnapshot);
32
+
33
+ // --- Helpers ---
34
+
35
+ function makeGatherer(projectName = 'Test Project') {
36
+ return () => ({
37
+ projectName,
38
+ canvasStates: {
39
+ main: {
40
+ width: 32,
41
+ height: 32,
42
+ layerTree: { layers: [], activeLayerId: '' },
43
+ pixelData: {},
44
+ },
45
+ },
46
+ });
47
+ }
48
+
49
+ // --- Tests ---
50
+
51
+ describe('RecoveryManager', () => {
52
+ beforeEach(() => {
53
+ vi.useFakeTimers();
54
+ vi.clearAllMocks();
55
+ });
56
+
57
+ afterEach(() => {
58
+ vi.useRealTimers();
59
+ });
60
+
61
+ // --- Pending command tracking ---
62
+
63
+ describe('pending command tracking', () => {
64
+ it('should add a pending command', () => {
65
+ const manager = new RecoveryManager();
66
+ manager.addPendingCommand({ id: 'cmd-1', type: 'draw_pixel', params: {} });
67
+
68
+ const pending = manager.getPendingCommands();
69
+ expect(pending).toHaveLength(1);
70
+ expect(pending[0]?.id).toBe('cmd-1');
71
+ });
72
+
73
+ it('should add multiple pending commands', () => {
74
+ const manager = new RecoveryManager();
75
+ manager.addPendingCommand({ id: 'cmd-1', type: 'draw' });
76
+ manager.addPendingCommand({ id: 'cmd-2', type: 'fill' });
77
+ manager.addPendingCommand({ id: 'cmd-3', type: 'erase' });
78
+
79
+ expect(manager.getPendingCommands()).toHaveLength(3);
80
+ });
81
+
82
+ it('should remove a pending command by ID', () => {
83
+ const manager = new RecoveryManager();
84
+ manager.addPendingCommand({ id: 'cmd-1', type: 'draw' });
85
+ manager.addPendingCommand({ id: 'cmd-2', type: 'fill' });
86
+
87
+ manager.removePendingCommand('cmd-1');
88
+
89
+ const pending = manager.getPendingCommands();
90
+ expect(pending).toHaveLength(1);
91
+ expect(pending[0]?.id).toBe('cmd-2');
92
+ });
93
+
94
+ it('should be a no-op when removing a non-existent command', () => {
95
+ const manager = new RecoveryManager();
96
+ manager.addPendingCommand({ id: 'cmd-1', type: 'draw' });
97
+
98
+ manager.removePendingCommand('cmd-999');
99
+
100
+ expect(manager.getPendingCommands()).toHaveLength(1);
101
+ });
102
+
103
+ it('should include pending commands in the snapshot', async () => {
104
+ const manager = new RecoveryManager();
105
+ manager.setStateGatherer(makeGatherer());
106
+ manager.addPendingCommand({ id: 'cmd-1', type: 'draw' });
107
+ manager.addPendingCommand({ id: 'cmd-2', type: 'fill' });
108
+
109
+ await manager.saveSnapshot();
110
+
111
+ expect(mockedSaveSnapshot).toHaveBeenCalledOnce();
112
+ const saved = mockedSaveSnapshot.mock.calls[0]?.[0];
113
+ expect(saved?.pendingCommands).toHaveLength(2);
114
+ });
115
+ });
116
+
117
+ // --- Snapshot interval lifecycle ---
118
+
119
+ describe('snapshot interval', () => {
120
+ it('should start periodic saves at the configured interval', async () => {
121
+ const manager = new RecoveryManager(1000);
122
+ manager.setStateGatherer(makeGatherer());
123
+ manager.start();
124
+
125
+ // No save yet at t=0
126
+ expect(mockedSaveSnapshot).not.toHaveBeenCalled();
127
+
128
+ // Advance past one interval
129
+ await vi.advanceTimersByTimeAsync(1000);
130
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
131
+
132
+ // Advance past another interval
133
+ await vi.advanceTimersByTimeAsync(1000);
134
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(2);
135
+
136
+ manager.stop();
137
+ });
138
+
139
+ it('should stop periodic saves when stop() is called', async () => {
140
+ const manager = new RecoveryManager(1000);
141
+ manager.setStateGatherer(makeGatherer());
142
+ manager.start();
143
+
144
+ await vi.advanceTimersByTimeAsync(1000);
145
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
146
+
147
+ manager.stop();
148
+
149
+ // Further time advancement should not trigger more saves
150
+ await vi.advanceTimersByTimeAsync(5000);
151
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ it('should not start a second interval if already running', async () => {
155
+ const manager = new RecoveryManager(1000);
156
+ manager.setStateGatherer(makeGatherer());
157
+ manager.start();
158
+ manager.start(); // should be a no-op
159
+
160
+ await vi.advanceTimersByTimeAsync(1000);
161
+ // If a second interval were started, we'd see 2 calls instead of 1
162
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
163
+
164
+ manager.stop();
165
+ });
166
+
167
+ it('should use the default 5000ms interval', async () => {
168
+ const manager = new RecoveryManager();
169
+ manager.setStateGatherer(makeGatherer());
170
+ manager.start();
171
+
172
+ // At 4999ms, no save yet
173
+ await vi.advanceTimersByTimeAsync(4999);
174
+ expect(mockedSaveSnapshot).not.toHaveBeenCalled();
175
+
176
+ // At 5000ms, first save
177
+ await vi.advanceTimersByTimeAsync(1);
178
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
179
+
180
+ manager.stop();
181
+ });
182
+ });
183
+
184
+ // --- Snapshot save content ---
185
+
186
+ describe('saveSnapshot', () => {
187
+ it('should save a snapshot with correct structure', async () => {
188
+ const manager = new RecoveryManager();
189
+ manager.setStateGatherer(makeGatherer('My Project'));
190
+
191
+ await manager.saveSnapshot();
192
+
193
+ expect(mockedSaveSnapshot).toHaveBeenCalledOnce();
194
+ const saved = mockedSaveSnapshot.mock.calls[0]?.[0];
195
+ expect(saved?.id).toBe('latest');
196
+ expect(saved?.projectName).toBe('My Project');
197
+ expect(saved?.canvasStates).toHaveProperty('main');
198
+ expect(saved?.canvasStates["main"]?.width).toBe(32);
199
+ expect(saved?.pendingCommands).toEqual([]);
200
+ expect(typeof saved?.timestamp).toBe('number');
201
+ });
202
+
203
+ it('should not save if no state gatherer is set', async () => {
204
+ const manager = new RecoveryManager();
205
+
206
+ await manager.saveSnapshot();
207
+
208
+ expect(mockedSaveSnapshot).not.toHaveBeenCalled();
209
+ });
210
+ });
211
+
212
+ // --- Visibility change ---
213
+
214
+ describe('visibility change', () => {
215
+ it('should save a snapshot when tab becomes hidden', async () => {
216
+ const manager = new RecoveryManager();
217
+ manager.setStateGatherer(makeGatherer());
218
+ manager.start();
219
+
220
+ // Simulate the tab being hidden
221
+ Object.defineProperty(document, 'visibilityState', {
222
+ value: 'hidden',
223
+ writable: true,
224
+ configurable: true,
225
+ });
226
+ document.dispatchEvent(new Event('visibilitychange'));
227
+
228
+ // saveSnapshot is async; flush microtasks
229
+ await vi.advanceTimersByTimeAsync(0);
230
+
231
+ expect(mockedSaveSnapshot).toHaveBeenCalledTimes(1);
232
+
233
+ // Restore
234
+ Object.defineProperty(document, 'visibilityState', {
235
+ value: 'visible',
236
+ writable: true,
237
+ configurable: true,
238
+ });
239
+ manager.stop();
240
+ });
241
+
242
+ it('should not save when tab becomes visible', async () => {
243
+ const manager = new RecoveryManager();
244
+ manager.setStateGatherer(makeGatherer());
245
+ manager.start();
246
+
247
+ Object.defineProperty(document, 'visibilityState', {
248
+ value: 'visible',
249
+ writable: true,
250
+ configurable: true,
251
+ });
252
+ document.dispatchEvent(new Event('visibilitychange'));
253
+
254
+ await vi.advanceTimersByTimeAsync(0);
255
+
256
+ expect(mockedSaveSnapshot).not.toHaveBeenCalled();
257
+
258
+ manager.stop();
259
+ });
260
+
261
+ it('should stop listening for visibility changes after stop()', async () => {
262
+ const manager = new RecoveryManager();
263
+ manager.setStateGatherer(makeGatherer());
264
+ manager.start();
265
+ manager.stop();
266
+
267
+ Object.defineProperty(document, 'visibilityState', {
268
+ value: 'hidden',
269
+ writable: true,
270
+ configurable: true,
271
+ });
272
+ document.dispatchEvent(new Event('visibilitychange'));
273
+
274
+ await vi.advanceTimersByTimeAsync(0);
275
+
276
+ expect(mockedSaveSnapshot).not.toHaveBeenCalled();
277
+
278
+ Object.defineProperty(document, 'visibilityState', {
279
+ value: 'visible',
280
+ writable: true,
281
+ configurable: true,
282
+ });
283
+ });
284
+ });
285
+
286
+ // --- Delegated IDB operations ---
287
+
288
+ describe('delegated IDB operations', () => {
289
+ it('should delegate hasRecoveryData to idb-store', async () => {
290
+ const manager = new RecoveryManager();
291
+
292
+ mockedHasSnapshot.mockResolvedValueOnce(true);
293
+ expect(await manager.hasRecoveryData()).toBe(true);
294
+ expect(mockedHasSnapshot).toHaveBeenCalledOnce();
295
+ });
296
+
297
+ it('should delegate loadRecoveryData to idb-store', async () => {
298
+ const manager = new RecoveryManager();
299
+
300
+ const fakeSnapshot: RecoverySnapshot = {
301
+ id: 'latest',
302
+ timestamp: 1234567890,
303
+ projectName: 'Recovered',
304
+ canvasStates: {},
305
+ pendingCommands: [{ id: 'cmd-1' }],
306
+ };
307
+ mockedLoadSnapshot.mockResolvedValueOnce(fakeSnapshot);
308
+
309
+ const loaded = await manager.loadRecoveryData();
310
+ expect(loaded).toBe(fakeSnapshot);
311
+ expect(mockedLoadSnapshot).toHaveBeenCalledOnce();
312
+ });
313
+
314
+ it('should delegate clearRecoveryData to idb-store', async () => {
315
+ const manager = new RecoveryManager();
316
+
317
+ await manager.clearRecoveryData();
318
+ expect(mockedClearSnapshot).toHaveBeenCalledOnce();
319
+ });
320
+ });
321
+ });
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Recovery Manager -- coordinates periodic snapshot saves and crash recovery.
3
+ *
4
+ * Saves a lightweight state snapshot to IndexedDB every N seconds, and also
5
+ * on `visibilitychange` (tab-switch / minimize). On next load, the app can
6
+ * check for recovery data and replay any commands the server never acked.
7
+ *
8
+ * Designed as a class so multiple instances can coexist in tests, but the
9
+ * app typically creates a single global instance.
10
+ */
11
+
12
+ import type { RecoverySnapshot } from './idb-store.js';
13
+ import {
14
+ saveSnapshot as idbSave,
15
+ loadSnapshot as idbLoad,
16
+ clearSnapshot as idbClear,
17
+ hasSnapshot as idbHas,
18
+ } from './idb-store.js';
19
+
20
+ /** Function that produces the current app state for a snapshot. */
21
+ export type StateGatherer = () => Omit<RecoverySnapshot, 'id' | 'timestamp' | 'pendingCommands'>;
22
+
23
+ interface PendingCommand {
24
+ id: string;
25
+ data: unknown;
26
+ }
27
+
28
+ export class RecoveryManager {
29
+ private intervalId: ReturnType<typeof setInterval> | null = null;
30
+ private readonly saveIntervalMs: number;
31
+ private pendingCommands: PendingCommand[] = [];
32
+ private gatherState: StateGatherer | null = null;
33
+ private readonly handleVisibilityChange: () => void;
34
+
35
+ constructor(saveIntervalMs = 5000) {
36
+ this.saveIntervalMs = saveIntervalMs;
37
+
38
+ // Bind once so we can remove the same listener reference later
39
+ this.handleVisibilityChange = () => {
40
+ if (document.visibilityState === 'hidden') {
41
+ // Fire-and-forget -- we do our best to save before the tab goes away
42
+ void this.saveSnapshot();
43
+ }
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Provide the function that gathers current app state for snapshots.
49
+ * Must be called before start() so that saveSnapshot knows what to persist.
50
+ */
51
+ setStateGatherer(gatherer: StateGatherer): void {
52
+ this.gatherState = gatherer;
53
+ }
54
+
55
+ /** Start periodic snapshots and listen for visibility changes. */
56
+ start(): void {
57
+ if (this.intervalId !== null) return; // already running
58
+ this.intervalId = setInterval(() => { void this.saveSnapshot(); }, this.saveIntervalMs);
59
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
60
+ }
61
+
62
+ /** Stop periodic snapshots and remove the visibility listener. */
63
+ stop(): void {
64
+ if (this.intervalId !== null) {
65
+ clearInterval(this.intervalId);
66
+ this.intervalId = null;
67
+ }
68
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
69
+ }
70
+
71
+ /** Save a snapshot now. Called periodically by the interval and on visibility change. */
72
+ async saveSnapshot(): Promise<void> {
73
+ if (!this.gatherState) return;
74
+
75
+ const state = this.gatherState();
76
+ const snapshot: RecoverySnapshot = {
77
+ id: 'latest',
78
+ timestamp: Date.now(),
79
+ projectName: state.projectName,
80
+ canvasStates: state.canvasStates,
81
+ pendingCommands: this.pendingCommands.map((pc) => pc.data),
82
+ };
83
+ await idbSave(snapshot);
84
+ }
85
+
86
+ /** Check whether recovery data exists from a previous session. */
87
+ async hasRecoveryData(): Promise<boolean> {
88
+ return idbHas();
89
+ }
90
+
91
+ /** Load recovery data from a previous session. */
92
+ async loadRecoveryData(): Promise<RecoverySnapshot | null> {
93
+ return idbLoad();
94
+ }
95
+
96
+ /** Clear recovery data (call after the server confirms all state is synced). */
97
+ async clearRecoveryData(): Promise<void> {
98
+ await idbClear();
99
+ }
100
+
101
+ /** Track a command that has been sent to the server but not yet acked. */
102
+ addPendingCommand(command: { id: string; [key: string]: unknown }): void {
103
+ this.pendingCommands.push({ id: command.id, data: command });
104
+ }
105
+
106
+ /** Remove a pending command once the server has acknowledged it. */
107
+ removePendingCommand(commandId: string): void {
108
+ this.pendingCommands = this.pendingCommands.filter((pc) => pc.id !== commandId);
109
+ }
110
+
111
+ /** Read-only access to the current pending commands list (for testing). */
112
+ getPendingCommands(): readonly PendingCommand[] {
113
+ return this.pendingCommands;
114
+ }
115
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Recovery Plugin -- wraps crash-recovery initialization as a plugin.
3
+ * Sets up periodic IndexedDB snapshots and checks for prior recovery data.
4
+ */
5
+
6
+ import type { PluginModule } from '../core/plugin-loader.js';
7
+ import { RecoveryManager } from './recovery-manager.js';
8
+ import { recoveryState } from './recovery-state.svelte.js';
9
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
10
+ import { serialize as serializeLayerTree } from '../layers/layer-tree.svelte.js';
11
+ import { getCurrentFrame } from '../animation/frame-model.svelte.js';
12
+
13
+ export const recoveryPlugin: PluginModule = {
14
+ name: 'builtin/recovery',
15
+ version: '1.0.0',
16
+ description: 'Periodic crash-recovery snapshots via IndexedDB',
17
+
18
+ register() {
19
+ const manager = new RecoveryManager();
20
+
21
+ manager.setStateGatherer(() => {
22
+ const frame = getCurrentFrame();
23
+ // Convert each layer's PixelBuffer to a plain ArrayBuffer copy
24
+ const pixelData: Record<string, ArrayBuffer> = {};
25
+ for (const [layerId, buffer] of frame.pixelData) {
26
+ pixelData[layerId] = buffer.data.buffer.slice(0) as ArrayBuffer;
27
+ }
28
+
29
+ return {
30
+ projectName: 'default',
31
+ canvasStates: {
32
+ default: {
33
+ width: canvasState.canvasWidth,
34
+ height: canvasState.canvasHeight,
35
+ layerTree: serializeLayerTree(),
36
+ pixelData,
37
+ },
38
+ },
39
+ };
40
+ });
41
+
42
+ manager.start();
43
+
44
+ // Check for existing recovery data from a previous session.
45
+ // Fire-and-forget: the notification is best-effort at startup.
46
+ void manager.hasRecoveryData().then(async (hasData) => {
47
+ if (hasData) {
48
+ const snapshot = await manager.loadRecoveryData();
49
+ if (snapshot) {
50
+ recoveryState.setRecoveryAvailable(true, snapshot.timestamp);
51
+ }
52
+ }
53
+ });
54
+ },
55
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Recovery State -- reactive Svelte 5 state indicating whether crash
3
+ * recovery data is available on startup.
4
+ *
5
+ * Components can read `recoveryState.available` and `.timestamp` to
6
+ * decide whether to show a recovery prompt to the user.
7
+ */
8
+
9
+ let available = $state(false);
10
+ let timestamp = $state<number | null>(null);
11
+
12
+ export const recoveryState = {
13
+ get available() { return available; },
14
+ get timestamp() { return timestamp; },
15
+
16
+ /** Mark recovery data as available (or not). */
17
+ setRecoveryAvailable(isAvailable: boolean, ts?: number): void {
18
+ available = isAvailable;
19
+ timestamp = ts ?? null;
20
+ },
21
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Service Worker Plugin -- registers the app-shell caching service worker.
3
+ * Registration is fire-and-forget; failures are logged as warnings.
4
+ */
5
+
6
+ import type { PluginModule } from '../core/plugin-loader.js';
7
+ import { registerServiceWorker } from './sw-register.js';
8
+
9
+ export const serviceWorkerPlugin: PluginModule = {
10
+ name: 'builtin/service-worker',
11
+ version: '1.0.0',
12
+ description: 'Service worker registration for offline caching',
13
+
14
+ register() {
15
+ // Async but fire-and-forget; errors are handled inside registerServiceWorker
16
+ void registerServiceWorker();
17
+ },
18
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Service Worker Registration -- registers the app-shell caching service worker.
3
+ *
4
+ * Fails silently with a console warning if the browser does not support
5
+ * service workers or if registration fails for any other reason.
6
+ */
7
+
8
+ export async function registerServiceWorker(): Promise<void> {
9
+ if (!('serviceWorker' in navigator)) return;
10
+
11
+ try {
12
+ const registration = await navigator.serviceWorker.register('/sw.js');
13
+ console.info('[sw] Service worker registered, scope:', registration.scope);
14
+ } catch (err) {
15
+ console.warn('[sw] Service worker registration failed:', err);
16
+ }
17
+ }