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,529 @@
1
+ /**
2
+ * Tests for drawing tool plugins.
3
+ *
4
+ * Each test registers the plugin, dispatches its command with a mocked context
5
+ * containing a real PixelBuffer, and verifies that execute modifies pixels
6
+ * correctly and undo restores the original state.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach } from 'vitest';
10
+ import { PixelBuffer } from '../../src/lib/canvas/pixel-buffer.js';
11
+ import { commandRegistry } from '../../src/lib/core/registries.svelte.js';
12
+ import { dispatch, undoLast, _resetForTesting as resetDispatcher, setContext } from '../../src/lib/core/dispatcher.js';
13
+ import type { Command } from '../../src/lib/core/commands.js';
14
+ import type { CommandType, ParamsOf } from '../../src/lib/core/command-params.js';
15
+
16
+ import { pencilToolPlugin } from './pencil-tool.js';
17
+ import { eraserToolPlugin } from './eraser-tool.js';
18
+ import { fillToolPlugin } from './fill-tool.js';
19
+ import { lineToolPlugin } from './line-tool.js';
20
+ import { rectToolPlugin } from './rect-tool.js';
21
+ import { circleToolPlugin } from './circle-tool.js';
22
+ import { diamondToolPlugin } from './diamond-tool.js';
23
+
24
+ import { createPluginAPI } from '../../src/lib/core/plugin-api.js';
25
+
26
+ // --- Helpers ---
27
+
28
+ /** Create a minimal command for dispatching. */
29
+ // Typed overload: validates params when type is a known command literal
30
+ function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
31
+ // String fallback: for dynamic/unknown command types in tests
32
+ function makeCommand(type: string, params?: Record<string, unknown>): Command;
33
+ function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
34
+ return {
35
+ type,
36
+ plugin: 'test',
37
+ version: '1.0.0',
38
+ params,
39
+ timestamp: Date.now(),
40
+ id: crypto.randomUUID(),
41
+ };
42
+ }
43
+
44
+ /** Set up a fresh PixelBuffer and wire it into the dispatcher context. */
45
+ function setupBuffer(width = 16, height = 16): PixelBuffer {
46
+ const buffer = new PixelBuffer(width, height);
47
+ setContext({ getActiveBuffer: () => buffer });
48
+ return buffer;
49
+ }
50
+
51
+ // --- Pencil Tool ---
52
+
53
+ describe('Pencil Tool Plugin', () => {
54
+ beforeEach(() => {
55
+ resetDispatcher();
56
+ const api = createPluginAPI('builtin/pencil');
57
+ pencilToolPlugin.register(api);
58
+ });
59
+
60
+ it('should register the draw_pixels command', () => {
61
+ expect(commandRegistry.get('draw_pixels')).toBeDefined();
62
+ });
63
+
64
+ it('should draw a single pixel', () => {
65
+ const buffer = setupBuffer();
66
+
67
+ dispatch(makeCommand('draw_pixels', {
68
+ pixels: [{ x: 5, y: 5, r: 255, g: 0, b: 0, a: 255 }],
69
+ layerId: '',
70
+ }));
71
+
72
+ expect(buffer.getPixel(5, 5)).toEqual([255, 0, 0, 255]);
73
+ });
74
+
75
+ it('should draw multiple pixels (simulating drag)', () => {
76
+ const buffer = setupBuffer();
77
+ const pixels = [
78
+ { x: 0, y: 0, r: 255, g: 0, b: 0, a: 255 },
79
+ { x: 1, y: 0, r: 255, g: 0, b: 0, a: 255 },
80
+ { x: 2, y: 0, r: 255, g: 0, b: 0, a: 255 },
81
+ ];
82
+
83
+ dispatch(makeCommand('draw_pixels', { pixels, layerId: '' }));
84
+
85
+ expect(buffer.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
86
+ expect(buffer.getPixel(1, 0)).toEqual([255, 0, 0, 255]);
87
+ expect(buffer.getPixel(2, 0)).toEqual([255, 0, 0, 255]);
88
+ });
89
+
90
+ it('should undo pixel drawing by restoring snapshot', () => {
91
+ const buffer = setupBuffer();
92
+ buffer.setPixel(3, 3, 100, 100, 100, 255);
93
+
94
+ dispatch(makeCommand('draw_pixels', {
95
+ pixels: [{ x: 3, y: 3, r: 255, g: 0, b: 0, a: 255 }],
96
+ layerId: '',
97
+ }));
98
+
99
+ expect(buffer.getPixel(3, 3)).toEqual([255, 0, 0, 255]);
100
+
101
+ undoLast();
102
+
103
+ expect(buffer.getPixel(3, 3)).toEqual([100, 100, 100, 255]);
104
+ });
105
+
106
+ it('should describe the command correctly', () => {
107
+ const def = commandRegistry.get('draw_pixels');
108
+ if (!def) throw new Error('draw_pixels command not registered');
109
+ const desc = def.describe({
110
+ pixels: [{ x: 0, y: 0, r: 0, g: 0, b: 0, a: 0 }],
111
+ });
112
+ expect(desc).toBe('Drew 1 pixel(s)');
113
+ });
114
+ });
115
+
116
+ // --- Eraser Tool ---
117
+
118
+ describe('Eraser Tool Plugin', () => {
119
+ beforeEach(() => {
120
+ resetDispatcher();
121
+ const api = createPluginAPI('builtin/eraser');
122
+ eraserToolPlugin.register(api);
123
+ });
124
+
125
+ it('should register the erase_pixels command', () => {
126
+ expect(commandRegistry.get('erase_pixels')).toBeDefined();
127
+ });
128
+
129
+ it('should erase pixels by setting them to transparent', () => {
130
+ const buffer = setupBuffer();
131
+ buffer.setPixel(4, 4, 255, 128, 64, 255);
132
+
133
+ dispatch(makeCommand('erase_pixels', {
134
+ pixels: [{ x: 4, y: 4, r: 0, g: 0, b: 0, a: 0 }],
135
+ layerId: '',
136
+ }));
137
+
138
+ expect(buffer.getPixel(4, 4)).toEqual([0, 0, 0, 0]);
139
+ });
140
+
141
+ it('should undo erasure by restoring snapshot', () => {
142
+ const buffer = setupBuffer();
143
+ buffer.setPixel(4, 4, 200, 100, 50, 255);
144
+
145
+ dispatch(makeCommand('erase_pixels', {
146
+ pixels: [{ x: 4, y: 4, r: 0, g: 0, b: 0, a: 0 }],
147
+ layerId: '',
148
+ }));
149
+
150
+ undoLast();
151
+
152
+ expect(buffer.getPixel(4, 4)).toEqual([200, 100, 50, 255]);
153
+ });
154
+ });
155
+
156
+ // --- Fill Tool ---
157
+
158
+ describe('Fill Tool Plugin', () => {
159
+ beforeEach(() => {
160
+ resetDispatcher();
161
+ const api = createPluginAPI('builtin/fill');
162
+ fillToolPlugin.register(api);
163
+ });
164
+
165
+ it('should register the flood_fill command', () => {
166
+ expect(commandRegistry.get('flood_fill')).toBeDefined();
167
+ });
168
+
169
+ it('should flood fill a bounded region', () => {
170
+ const buffer = setupBuffer(8, 8);
171
+ // Create a box
172
+ for (let i = 0; i < 8; i++) {
173
+ buffer.setPixel(i, 0, 255, 0, 0, 255);
174
+ buffer.setPixel(i, 7, 255, 0, 0, 255);
175
+ buffer.setPixel(0, i, 255, 0, 0, 255);
176
+ buffer.setPixel(7, i, 255, 0, 0, 255);
177
+ }
178
+
179
+ dispatch(makeCommand('flood_fill', {
180
+ x: 3,
181
+ y: 3,
182
+ color: '#00FF00',
183
+ layerId: '',
184
+ }));
185
+
186
+ // Interior should be green
187
+ expect(buffer.getPixel(3, 3)).toEqual([0, 255, 0, 255]);
188
+ expect(buffer.getPixel(1, 1)).toEqual([0, 255, 0, 255]);
189
+ expect(buffer.getPixel(6, 6)).toEqual([0, 255, 0, 255]);
190
+
191
+ // Walls should still be red
192
+ expect(buffer.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
193
+ });
194
+
195
+ it('should undo flood fill', () => {
196
+ const buffer = setupBuffer(4, 4);
197
+
198
+ dispatch(makeCommand('flood_fill', {
199
+ x: 0,
200
+ y: 0,
201
+ color: '#0000FF',
202
+ layerId: '',
203
+ }));
204
+
205
+ // All pixels should be blue
206
+ expect(buffer.getPixel(0, 0)).toEqual([0, 0, 255, 255]);
207
+ expect(buffer.getPixel(3, 3)).toEqual([0, 0, 255, 255]);
208
+
209
+ undoLast();
210
+
211
+ // All pixels should be back to transparent
212
+ expect(buffer.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
213
+ expect(buffer.getPixel(3, 3)).toEqual([0, 0, 0, 0]);
214
+ });
215
+
216
+ it('should not fill if clicked pixel already matches fill color', () => {
217
+ const buffer = setupBuffer(4, 4);
218
+ buffer.fill(0, 255, 0, 255);
219
+
220
+ dispatch(makeCommand('flood_fill', {
221
+ x: 0,
222
+ y: 0,
223
+ color: '#00FF00',
224
+ layerId: '',
225
+ }));
226
+
227
+ // All pixels should still be green (the fill is idempotent)
228
+ expect(buffer.getPixel(0, 0)).toEqual([0, 255, 0, 255]);
229
+ expect(buffer.getPixel(3, 3)).toEqual([0, 255, 0, 255]);
230
+ });
231
+ });
232
+
233
+ // --- Line Tool ---
234
+
235
+ describe('Line Tool Plugin', () => {
236
+ beforeEach(() => {
237
+ resetDispatcher();
238
+ const api = createPluginAPI('builtin/line');
239
+ lineToolPlugin.register(api);
240
+ });
241
+
242
+ it('should register the draw_line command', () => {
243
+ expect(commandRegistry.get('draw_line')).toBeDefined();
244
+ });
245
+
246
+ it('should draw a horizontal line', () => {
247
+ const buffer = setupBuffer();
248
+
249
+ dispatch(makeCommand('draw_line', {
250
+ x0: 0, y0: 5, x1: 7, y1: 5,
251
+ color: '#FF0000',
252
+ algorithm: 'standard',
253
+ layerId: '',
254
+ }));
255
+
256
+ for (let x = 0; x <= 7; x++) {
257
+ expect(buffer.getPixel(x, 5)).toEqual([255, 0, 0, 255]);
258
+ }
259
+ // Pixel outside should be untouched
260
+ expect(buffer.getPixel(8, 5)).toEqual([0, 0, 0, 0]);
261
+ });
262
+
263
+ it('should draw a vertical line', () => {
264
+ const buffer = setupBuffer();
265
+
266
+ dispatch(makeCommand('draw_line', {
267
+ x0: 3, y0: 0, x1: 3, y1: 6,
268
+ color: '#00FF00',
269
+ algorithm: 'standard',
270
+ layerId: '',
271
+ }));
272
+
273
+ for (let y = 0; y <= 6; y++) {
274
+ expect(buffer.getPixel(3, y)).toEqual([0, 255, 0, 255]);
275
+ }
276
+ });
277
+
278
+ it('should draw a diagonal line', () => {
279
+ const buffer = setupBuffer();
280
+
281
+ dispatch(makeCommand('draw_line', {
282
+ x0: 0, y0: 0, x1: 4, y1: 4,
283
+ color: '#0000FF',
284
+ algorithm: 'standard',
285
+ layerId: '',
286
+ }));
287
+
288
+ for (let i = 0; i <= 4; i++) {
289
+ expect(buffer.getPixel(i, i)).toEqual([0, 0, 255, 255]);
290
+ }
291
+ });
292
+
293
+ it('should draw with no-doubles algorithm', () => {
294
+ const buffer = setupBuffer();
295
+
296
+ dispatch(makeCommand('draw_line', {
297
+ x0: 0, y0: 0, x1: 10, y1: 3,
298
+ color: '#FF00FF',
299
+ algorithm: 'nodoubles',
300
+ layerId: '',
301
+ }));
302
+
303
+ // Start and end should be drawn
304
+ expect(buffer.getPixel(0, 0)).toEqual([255, 0, 255, 255]);
305
+ expect(buffer.getPixel(10, 3)).toEqual([255, 0, 255, 255]);
306
+ });
307
+
308
+ it('should undo a line', () => {
309
+ const buffer = setupBuffer();
310
+
311
+ dispatch(makeCommand('draw_line', {
312
+ x0: 0, y0: 0, x1: 3, y1: 0,
313
+ color: '#FF0000',
314
+ algorithm: 'standard',
315
+ layerId: '',
316
+ }));
317
+
318
+ expect(buffer.getPixel(0, 0)).toEqual([255, 0, 0, 255]);
319
+
320
+ undoLast();
321
+
322
+ expect(buffer.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
323
+ expect(buffer.getPixel(1, 0)).toEqual([0, 0, 0, 0]);
324
+ expect(buffer.getPixel(2, 0)).toEqual([0, 0, 0, 0]);
325
+ expect(buffer.getPixel(3, 0)).toEqual([0, 0, 0, 0]);
326
+ });
327
+ });
328
+
329
+ // --- Rectangle Tool ---
330
+
331
+ describe('Rectangle Tool Plugin', () => {
332
+ beforeEach(() => {
333
+ resetDispatcher();
334
+ const api = createPluginAPI('builtin/rect');
335
+ rectToolPlugin.register(api);
336
+ });
337
+
338
+ it('should register the draw_rect command', () => {
339
+ expect(commandRegistry.get('draw_rect')).toBeDefined();
340
+ });
341
+
342
+ it('should draw a rectangle outline', () => {
343
+ const buffer = setupBuffer();
344
+
345
+ dispatch(makeCommand('draw_rect', {
346
+ x: 2, y: 2, w: 4, h: 3,
347
+ filled: false,
348
+ color: '#FF0000',
349
+ layerId: '',
350
+ }));
351
+
352
+ // Top-left corner
353
+ expect(buffer.getPixel(2, 2)).toEqual([255, 0, 0, 255]);
354
+ // Top-right corner
355
+ expect(buffer.getPixel(5, 2)).toEqual([255, 0, 0, 255]);
356
+ // Bottom-left corner
357
+ expect(buffer.getPixel(2, 4)).toEqual([255, 0, 0, 255]);
358
+ // Bottom-right corner
359
+ expect(buffer.getPixel(5, 4)).toEqual([255, 0, 0, 255]);
360
+ // Interior should be empty
361
+ expect(buffer.getPixel(3, 3)).toEqual([0, 0, 0, 0]);
362
+ });
363
+
364
+ it('should draw a filled rectangle', () => {
365
+ const buffer = setupBuffer();
366
+
367
+ dispatch(makeCommand('draw_rect', {
368
+ x: 1, y: 1, w: 3, h: 2,
369
+ filled: true,
370
+ color: '#00FF00',
371
+ layerId: '',
372
+ }));
373
+
374
+ // All interior and border pixels should be filled
375
+ for (let y = 1; y <= 2; y++) {
376
+ for (let x = 1; x <= 3; x++) {
377
+ expect(buffer.getPixel(x, y)).toEqual([0, 255, 0, 255]);
378
+ }
379
+ }
380
+ // Outside should be empty
381
+ expect(buffer.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
382
+ });
383
+
384
+ it('should undo a rectangle', () => {
385
+ const buffer = setupBuffer();
386
+
387
+ dispatch(makeCommand('draw_rect', {
388
+ x: 0, y: 0, w: 3, h: 3,
389
+ filled: true,
390
+ color: '#0000FF',
391
+ layerId: '',
392
+ }));
393
+
394
+ expect(buffer.getPixel(1, 1)).toEqual([0, 0, 255, 255]);
395
+
396
+ undoLast();
397
+
398
+ expect(buffer.getPixel(0, 0)).toEqual([0, 0, 0, 0]);
399
+ expect(buffer.getPixel(1, 1)).toEqual([0, 0, 0, 0]);
400
+ expect(buffer.getPixel(2, 2)).toEqual([0, 0, 0, 0]);
401
+ });
402
+ });
403
+
404
+ // --- Circle/Ellipse Tool ---
405
+
406
+ describe('Circle/Ellipse Tool Plugin', () => {
407
+ beforeEach(() => {
408
+ resetDispatcher();
409
+ const api = createPluginAPI('builtin/circle');
410
+ circleToolPlugin.register(api);
411
+ });
412
+
413
+ it('should register the draw_ellipse command', () => {
414
+ expect(commandRegistry.get('draw_ellipse')).toBeDefined();
415
+ });
416
+
417
+ it('should draw an ellipse outline', () => {
418
+ const buffer = setupBuffer();
419
+
420
+ dispatch(makeCommand('draw_ellipse', {
421
+ cx: 8, cy: 8, rx: 3, ry: 2,
422
+ filled: false,
423
+ color: '#FF0000',
424
+ layerId: '',
425
+ }));
426
+
427
+ // Cardinal points should be drawn
428
+ expect(buffer.getPixel(11, 8)).toEqual([255, 0, 0, 255]); // right
429
+ expect(buffer.getPixel(5, 8)).toEqual([255, 0, 0, 255]); // left
430
+ expect(buffer.getPixel(8, 10)).toEqual([255, 0, 0, 255]); // bottom
431
+ expect(buffer.getPixel(8, 6)).toEqual([255, 0, 0, 255]); // top
432
+ });
433
+
434
+ it('should draw a filled ellipse including center', () => {
435
+ const buffer = setupBuffer();
436
+
437
+ dispatch(makeCommand('draw_ellipse', {
438
+ cx: 8, cy: 8, rx: 2, ry: 2,
439
+ filled: true,
440
+ color: '#00FF00',
441
+ layerId: '',
442
+ }));
443
+
444
+ expect(buffer.getPixel(8, 8)).toEqual([0, 255, 0, 255]); // center
445
+ expect(buffer.getPixel(10, 8)).toEqual([0, 255, 0, 255]); // right edge
446
+ });
447
+
448
+ it('should undo an ellipse', () => {
449
+ const buffer = setupBuffer();
450
+
451
+ dispatch(makeCommand('draw_ellipse', {
452
+ cx: 8, cy: 8, rx: 3, ry: 3,
453
+ filled: true,
454
+ color: '#FF0000',
455
+ layerId: '',
456
+ }));
457
+
458
+ expect(buffer.getPixel(8, 8)).toEqual([255, 0, 0, 255]);
459
+
460
+ undoLast();
461
+
462
+ expect(buffer.getPixel(8, 8)).toEqual([0, 0, 0, 0]);
463
+ expect(buffer.getPixel(11, 8)).toEqual([0, 0, 0, 0]);
464
+ });
465
+ });
466
+
467
+ // --- Diamond Tool ---
468
+
469
+ describe('Diamond Tool Plugin', () => {
470
+ beforeEach(() => {
471
+ resetDispatcher();
472
+ const api = createPluginAPI('builtin/diamond');
473
+ diamondToolPlugin.register(api);
474
+ });
475
+
476
+ it('should register the draw_diamond command', () => {
477
+ expect(commandRegistry.get('draw_diamond')).toBeDefined();
478
+ });
479
+
480
+ it('should draw a diamond outline with vertices at cardinal points', () => {
481
+ const buffer = setupBuffer();
482
+
483
+ dispatch(makeCommand('draw_diamond', {
484
+ cx: 8, cy: 8, radiusX: 3, radiusY: 3,
485
+ filled: false,
486
+ color: '#FF0000',
487
+ layerId: '',
488
+ }));
489
+
490
+ // Top vertex
491
+ expect(buffer.getPixel(8, 5)).toEqual([255, 0, 0, 255]);
492
+ // Right vertex
493
+ expect(buffer.getPixel(11, 8)).toEqual([255, 0, 0, 255]);
494
+ // Bottom vertex
495
+ expect(buffer.getPixel(8, 11)).toEqual([255, 0, 0, 255]);
496
+ // Left vertex
497
+ expect(buffer.getPixel(5, 8)).toEqual([255, 0, 0, 255]);
498
+ });
499
+
500
+ it('should draw a filled diamond including center', () => {
501
+ const buffer = setupBuffer();
502
+
503
+ dispatch(makeCommand('draw_diamond', {
504
+ cx: 8, cy: 8, radiusX: 3, radiusY: 3,
505
+ filled: true,
506
+ color: '#00FF00',
507
+ layerId: '',
508
+ }));
509
+
510
+ expect(buffer.getPixel(8, 8)).toEqual([0, 255, 0, 255]);
511
+ });
512
+
513
+ it('should undo a diamond', () => {
514
+ const buffer = setupBuffer();
515
+
516
+ dispatch(makeCommand('draw_diamond', {
517
+ cx: 8, cy: 8, radiusX: 2, radiusY: 2,
518
+ filled: true,
519
+ color: '#0000FF',
520
+ layerId: '',
521
+ }));
522
+
523
+ expect(buffer.getPixel(8, 8)).toEqual([0, 0, 255, 255]);
524
+
525
+ undoLast();
526
+
527
+ expect(buffer.getPixel(8, 8)).toEqual([0, 0, 0, 0]);
528
+ });
529
+ });
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <rect width="32" height="32" rx="4" fill="#1a1a2e"/>
3
+ <rect x="8" y="8" width="6" height="6" fill="#4a9eff"/>
4
+ <rect x="18" y="8" width="6" height="6" fill="#4a9eff" opacity="0.7"/>
5
+ <rect x="8" y="18" width="6" height="6" fill="#4a9eff" opacity="0.5"/>
6
+ <rect x="18" y="18" width="6" height="6" fill="#4a9eff" opacity="0.3"/>
7
+ </svg>
package/public/sw.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * PixelWeaver Service Worker -- caches the app shell for fast reload
3
+ * and offline resilience after a tab crash.
4
+ *
5
+ * Strategy: stale-while-revalidate for app shell assets.
6
+ * API requests (/api/*, /ws) are never cached.
7
+ */
8
+
9
+ const CACHE_NAME = 'pixelweaver-v1';
10
+
11
+ // Core app shell URLs to pre-cache on install.
12
+ // Vite-built assets are hashed and will be added to the cache on first fetch.
13
+ const SHELL_URLS = [
14
+ '/',
15
+ '/index.html',
16
+ ];
17
+
18
+ // --- Install: pre-cache the app shell ---
19
+
20
+ self.addEventListener('install', (event) => {
21
+ event.waitUntil(
22
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(SHELL_URLS))
23
+ );
24
+ // Activate immediately without waiting for existing tabs to close
25
+ self.skipWaiting();
26
+ });
27
+
28
+ // --- Activate: clean up old caches ---
29
+
30
+ self.addEventListener('activate', (event) => {
31
+ event.waitUntil(
32
+ caches.keys().then((keys) =>
33
+ Promise.all(
34
+ keys
35
+ .filter((key) => key !== CACHE_NAME)
36
+ .map((key) => caches.delete(key))
37
+ )
38
+ )
39
+ );
40
+ // Take control of all open tabs immediately
41
+ self.clients.claim();
42
+ });
43
+
44
+ // --- Fetch: stale-while-revalidate for GET requests to app shell ---
45
+
46
+ self.addEventListener('fetch', (event) => {
47
+ const url = new URL(event.request.url);
48
+
49
+ // Never cache API or WebSocket requests
50
+ if (url.pathname.startsWith('/api/') || url.pathname.startsWith('/ws')) {
51
+ return;
52
+ }
53
+
54
+ // Only handle GET requests
55
+ if (event.request.method !== 'GET') {
56
+ return;
57
+ }
58
+
59
+ event.respondWith(
60
+ caches.open(CACHE_NAME).then(async (cache) => {
61
+ const cached = await cache.match(event.request);
62
+
63
+ // Start a network fetch to update the cache in the background
64
+ const networkFetch = fetch(event.request)
65
+ .then((response) => {
66
+ // Only cache successful responses
67
+ if (response.ok) {
68
+ cache.put(event.request, response.clone());
69
+ }
70
+ return response;
71
+ })
72
+ .catch(() => {
73
+ // Network failed; cached response (if any) is the only option
74
+ return undefined;
75
+ });
76
+
77
+ // Return cached version immediately if available, otherwise wait for network
78
+ if (cached) {
79
+ return cached;
80
+ }
81
+
82
+ const networkResponse = await networkFetch;
83
+ if (networkResponse) {
84
+ return networkResponse;
85
+ }
86
+
87
+ // Both cache and network failed -- return a basic offline response
88
+ return new Response('Offline', { status: 503, statusText: 'Service Unavailable' });
89
+ })
90
+ );
91
+ });
package/pyproject.toml ADDED
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "pixelweaver"
3
+ version = "0.1.0"
4
+ description = "PixelWeaver collaboration server"
5
+ readme = "server/README.md"
6
+ requires-python = ">=3.13"
7
+ dependencies = [
8
+ "wesktop",
9
+ "strictcli",
10
+ "pydantic>=2.12.5",
11
+ "pillow>=12.2.0",
12
+ "mcp>=1.27.0",
13
+ "httpx>=0.28.1",
14
+ ]
15
+ keywords = ["rlsbl"]
16
+
17
+ [project.scripts]
18
+ # CLI entry point: `uv run pixelweaver serve` starts the server
19
+ pixelweaver = "pixelweaver.cli:main"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pytest>=8.0.0",
24
+ "httpx>=0.28.0",
25
+ "pytest-asyncio>=0.26.0",
26
+ "ruff>=0.11.0",
27
+ ]
28
+ gui = [
29
+ "PyGObject>=3.50",
30
+ "pycairo>=1.26",
31
+ ]
32
+
33
+ [tool.uv.build-backend]
34
+ module-root = "server/src"
35
+
36
+ [build-system]
37
+ requires = ["uv_build>=0.9.17,<0.10.0"]
38
+ build-backend = "uv_build"
39
+
40
+ [tool.ruff]
41
+ line-length = 100
42
+ target-version = "py313"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "F", "I", "N", "W", "UP"]
46
+
47
+ [tool.pytest.ini_options]
48
+ asyncio_mode = "auto"
49
+ testpaths = ["server/tests"]
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ # Lists Wave A violations (file:line:col rule message) for given paths.
3
+ # Usage: ./scripts/eslint-wave-a-list.sh <path> [path...]
4
+ set -euo pipefail
5
+ if [ $# -eq 0 ]; then
6
+ echo "Usage: $0 <path> [path...]" >&2
7
+ exit 2
8
+ fi
9
+ npx eslint "$@" -f json 2>/dev/null | node -e '
10
+ let input = "";
11
+ process.stdin.on("data", d => input += d);
12
+ process.stdin.on("end", () => {
13
+ const data = JSON.parse(input);
14
+ for (const f of data) {
15
+ const rel = f.filePath.replace(process.cwd() + "/", "");
16
+ for (const m of f.messages) {
17
+ if (m.severity !== 2) continue;
18
+ if (m.ruleId !== "@typescript-eslint/no-non-null-assertion" && m.ruleId !== "@typescript-eslint/restrict-template-expressions") continue;
19
+ const short = m.ruleId.replace("@typescript-eslint/", "");
20
+ console.log(`${rel}:${m.line}:${m.column} [${short}] ${m.message}`);
21
+ }
22
+ }
23
+ });
24
+ '