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,275 @@
1
+ """Tests for the WebSocket message handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from typing import Any
7
+ from unittest.mock import AsyncMock
8
+
9
+ import pytest
10
+
11
+ from pixelweaver.connections import ConnectionManager
12
+ from pixelweaver.state import ServerState
13
+ from pixelweaver.websocket import handle_message
14
+
15
+ # Shorter aliases for type hints in test signatures
16
+ SS = ServerState
17
+ CM = ConnectionManager
18
+
19
+
20
+ def _make_ws() -> AsyncMock:
21
+ """Create a mock WebSocket."""
22
+ ws = AsyncMock()
23
+ ws.send_json = AsyncMock()
24
+ return ws
25
+
26
+
27
+ def _make_command_msg(
28
+ msg_id: str | None = None,
29
+ cmd_id: str | None = None,
30
+ ) -> dict[str, Any]:
31
+ """Build a valid command message dict."""
32
+ return {
33
+ "type": "command",
34
+ "id": msg_id or str(uuid.uuid4()),
35
+ "command": {
36
+ "type": "draw_pixels",
37
+ "plugin": "builtin/pencil",
38
+ "version": "1.0.0",
39
+ "params": {"x": 5, "y": 10},
40
+ "id": cmd_id or str(uuid.uuid4()),
41
+ "timestamp": 1234567890,
42
+ },
43
+ }
44
+
45
+
46
+ @pytest.fixture()
47
+ def state_with_project() -> ServerState:
48
+ """ServerState with one active project."""
49
+ state = ServerState()
50
+ state.create_project("test", 32, 32)
51
+ return state
52
+
53
+
54
+ @pytest.fixture()
55
+ def mgr() -> ConnectionManager:
56
+ return ConnectionManager()
57
+
58
+
59
+ class TestCommandHandling:
60
+ @pytest.mark.asyncio
61
+ async def test_command_ack(self, state_with_project: SS, mgr: CM):
62
+ ws = _make_ws()
63
+ msg = _make_command_msg(msg_id="m1", cmd_id="c1")
64
+ await handle_message(ws, msg, state_with_project, mgr)
65
+
66
+ # Should have sent an ack
67
+ ws.send_json.assert_called()
68
+ sent = ws.send_json.call_args_list[0][0][0]
69
+ assert sent["type"] == "command_ack"
70
+ assert sent["id"] == "m1"
71
+ assert sent["command_id"] == "c1"
72
+ assert sent["success"] is True
73
+
74
+ @pytest.mark.asyncio
75
+ async def test_command_stored_in_history(
76
+ self, state_with_project: SS, mgr: CM,
77
+ ):
78
+ ws = _make_ws()
79
+ msg = _make_command_msg()
80
+ await handle_message(ws, msg, state_with_project, mgr)
81
+
82
+ project = state_with_project.get_active_project()
83
+ assert project is not None
84
+ assert len(project.command_history) == 1
85
+ assert project.command_history[0]["type"] == "draw_pixels"
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_command_broadcast_to_others(
89
+ self, state_with_project: SS, mgr: CM,
90
+ ):
91
+ ws1 = _make_ws()
92
+ ws2 = _make_ws()
93
+ # Register ws2 as another connected client
94
+ mgr.active_connections.append(ws2)
95
+
96
+ msg = _make_command_msg()
97
+ await handle_message(ws1, msg, state_with_project, mgr)
98
+
99
+ # ws2 should have received a broadcast
100
+ ws2.send_json.assert_called_once()
101
+ broadcast = ws2.send_json.call_args[0][0]
102
+ assert broadcast["type"] == "command_broadcast"
103
+ assert broadcast["source"] == "client"
104
+
105
+ @pytest.mark.asyncio
106
+ async def test_command_reject_no_project(self, mgr: CM):
107
+ """Command without active project should be rejected."""
108
+ state = ServerState()
109
+ ws = _make_ws()
110
+ msg = _make_command_msg(msg_id="m1", cmd_id="c1")
111
+ await handle_message(ws, msg, state, mgr)
112
+
113
+ sent = ws.send_json.call_args[0][0]
114
+ assert sent["type"] == "command_reject"
115
+ assert sent["error"] == "No active project"
116
+
117
+
118
+ class TestSyncRequest:
119
+ @pytest.mark.asyncio
120
+ async def test_sync_returns_state(
121
+ self, state_with_project: SS, mgr: CM,
122
+ ):
123
+ ws = _make_ws()
124
+ msg = {"type": "sync_request", "id": "s1"}
125
+ await handle_message(ws, msg, state_with_project, mgr)
126
+
127
+ sent = ws.send_json.call_args[0][0]
128
+ assert sent["type"] == "state_sync"
129
+ assert sent["id"] == "s1"
130
+ assert "project" in sent["state"]
131
+ assert sent["state"]["project"]["name"] == "test"
132
+
133
+
134
+ class TestUndoRedo:
135
+ @pytest.mark.asyncio
136
+ async def test_undo_pops_command(
137
+ self, state_with_project: SS, mgr: CM,
138
+ ):
139
+ ws = _make_ws()
140
+
141
+ # First add a command
142
+ msg = _make_command_msg(msg_id="m1", cmd_id="c1")
143
+ await handle_message(ws, msg, state_with_project, mgr)
144
+
145
+ project = state_with_project.get_active_project()
146
+ assert project is not None
147
+ assert len(project.command_history) == 1
148
+
149
+ # Now undo
150
+ ws.send_json.reset_mock()
151
+ undo_msg = {"type": "undo", "id": "u1"}
152
+ await handle_message(ws, undo_msg, state_with_project, mgr)
153
+
154
+ assert len(project.command_history) == 0
155
+ assert len(project.redo_stack) == 1
156
+
157
+ sent = ws.send_json.call_args[0][0]
158
+ assert sent["type"] == "command_ack"
159
+ assert sent["id"] == "u1"
160
+ assert sent["command_id"] == "c1"
161
+
162
+ @pytest.mark.asyncio
163
+ async def test_redo_restores_command(
164
+ self, state_with_project: SS, mgr: CM,
165
+ ):
166
+ ws = _make_ws()
167
+
168
+ # Add then undo a command
169
+ msg = _make_command_msg(msg_id="m1", cmd_id="c1")
170
+ await handle_message(ws, msg, state_with_project, mgr)
171
+ undo_msg = {"type": "undo", "id": "u1"}
172
+ await handle_message(ws, undo_msg, state_with_project, mgr)
173
+
174
+ project = state_with_project.get_active_project()
175
+ assert project is not None
176
+ assert len(project.command_history) == 0
177
+
178
+ # Redo
179
+ ws.send_json.reset_mock()
180
+ redo_msg = {"type": "redo", "id": "r1"}
181
+ await handle_message(ws, redo_msg, state_with_project, mgr)
182
+
183
+ assert len(project.command_history) == 1
184
+ assert len(project.redo_stack) == 0
185
+
186
+ sent = ws.send_json.call_args[0][0]
187
+ assert sent["type"] == "command_ack"
188
+ assert sent["command_id"] == "c1"
189
+
190
+ @pytest.mark.asyncio
191
+ async def test_undo_empty_history_returns_error(
192
+ self, state_with_project: SS, mgr: CM,
193
+ ):
194
+ ws = _make_ws()
195
+ undo_msg = {"type": "undo", "id": "u1"}
196
+ await handle_message(ws, undo_msg, state_with_project, mgr)
197
+
198
+ sent = ws.send_json.call_args[0][0]
199
+ assert sent["type"] == "error"
200
+ assert "Nothing to undo" in sent["error"]
201
+
202
+ @pytest.mark.asyncio
203
+ async def test_redo_empty_stack_returns_error(
204
+ self, state_with_project: SS, mgr: CM,
205
+ ):
206
+ ws = _make_ws()
207
+ redo_msg = {"type": "redo", "id": "r1"}
208
+ await handle_message(ws, redo_msg, state_with_project, mgr)
209
+
210
+ sent = ws.send_json.call_args[0][0]
211
+ assert sent["type"] == "error"
212
+ assert "Nothing to redo" in sent["error"]
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_new_command_clears_redo_stack(
216
+ self, state_with_project: SS, mgr: CM,
217
+ ):
218
+ ws = _make_ws()
219
+
220
+ # Add, undo, then add new command
221
+ msg1 = _make_command_msg(cmd_id="c1")
222
+ await handle_message(ws, msg1, state_with_project, mgr)
223
+ undo_msg = {"type": "undo", "id": "u1"}
224
+ await handle_message(ws, undo_msg, state_with_project, mgr)
225
+
226
+ project = state_with_project.get_active_project()
227
+ assert project is not None
228
+ assert len(project.redo_stack) == 1
229
+
230
+ msg2 = _make_command_msg(cmd_id="c2")
231
+ await handle_message(ws, msg2, state_with_project, mgr)
232
+
233
+ # Redo stack should be cleared after a new command
234
+ assert len(project.redo_stack) == 0
235
+
236
+
237
+ class TestInvalidMessages:
238
+ @pytest.mark.asyncio
239
+ async def test_unknown_type_returns_error(
240
+ self, state_with_project: SS, mgr: CM,
241
+ ):
242
+ ws = _make_ws()
243
+ bad_msg = {"type": "bogus", "id": "x"}
244
+ await handle_message(ws, bad_msg, state_with_project, mgr)
245
+
246
+ sent = ws.send_json.call_args[0][0]
247
+ assert sent["type"] == "error"
248
+
249
+ @pytest.mark.asyncio
250
+ async def test_missing_fields_returns_error(
251
+ self, state_with_project: SS, mgr: CM,
252
+ ):
253
+ ws = _make_ws()
254
+ bad_msg = {"type": "command"}
255
+ await handle_message(ws, bad_msg, state_with_project, mgr)
256
+
257
+ sent = ws.send_json.call_args[0][0]
258
+ assert sent["type"] == "error"
259
+
260
+ @pytest.mark.asyncio
261
+ async def test_dirty_callback_called(
262
+ self, state_with_project: SS, mgr: CM,
263
+ ):
264
+ ws = _make_ws()
265
+ dirty_called = False
266
+
267
+ def on_dirty():
268
+ nonlocal dirty_called
269
+ dirty_called = True
270
+
271
+ msg = _make_command_msg()
272
+ await handle_message(
273
+ ws, msg, state_with_project, mgr, on_dirty=on_dirty,
274
+ )
275
+ assert dirty_called
package/src/App.svelte ADDED
@@ -0,0 +1,107 @@
1
+ <!-- PixelWeaver main application shell -->
2
+ <script lang="ts">
3
+ import MenuBar from './lib/ui/MenuBar.svelte';
4
+ import NotificationBanner from './lib/ui/notifications/NotificationBanner.svelte';
5
+ import StatusBar from './lib/ui/StatusBar.svelte';
6
+ import NewProjectDialog from './lib/ui/NewProjectDialog.svelte';
7
+ import ExportDialog from './lib/ui/ExportDialog.svelte';
8
+ import ResizeDialog from './lib/ui/ResizeDialog.svelte';
9
+ import PromptDialog from './lib/ui/PromptDialog.svelte';
10
+ import RecoveryDialog from './lib/ui/RecoveryDialog.svelte';
11
+ import CommandPalette from './lib/ui/command-palette/CommandPalette.svelte';
12
+ import BisectionExportDialog from './lib/variants/BisectionExportDialog.svelte';
13
+ import { dialogState } from './lib/ui/dialog-state.svelte.js';
14
+ import { hasUnsavedChanges } from './lib/core/dispatcher.js';
15
+ import type { Component } from 'svelte';
16
+
17
+ // Lazy-load DockLayout so dockview-core (209KB) is deferred from the initial bundle.
18
+ let DockLayout: Component | null = $state(null);
19
+
20
+ $effect(() => {
21
+ import('./lib/dock/DockLayout.svelte').then(mod => {
22
+ DockLayout = mod.default;
23
+ });
24
+ });
25
+
26
+ // Warn the user before closing the tab if there are unsaved changes.
27
+ $effect(() => {
28
+ function onBeforeUnload(e: BeforeUnloadEvent) {
29
+ if (hasUnsavedChanges()) {
30
+ e.preventDefault();
31
+ }
32
+ }
33
+ window.addEventListener('beforeunload', onBeforeUnload);
34
+ return () => window.removeEventListener('beforeunload', onBeforeUnload);
35
+ });
36
+ </script>
37
+
38
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
39
+ <div class="app-shell" oncontextmenu={(e) => { e.preventDefault(); }}>
40
+ <MenuBar />
41
+ <NotificationBanner />
42
+
43
+ <main class="workspace">
44
+ <div class="dock-area">
45
+ {#if DockLayout}
46
+ <DockLayout />
47
+ {:else}
48
+ <div class="dock-loading">Loading workspace...</div>
49
+ {/if}
50
+ </div>
51
+ </main>
52
+
53
+ <StatusBar />
54
+ </div>
55
+
56
+ <NewProjectDialog open={dialogState.newProjectOpen} onClose={() => { dialogState.closeNewProject(); }} />
57
+ <ExportDialog open={dialogState.exportOpen} onClose={() => { dialogState.closeExport(); }} />
58
+ <ResizeDialog open={dialogState.resizeCanvasOpen} onClose={() => { dialogState.closeResizeCanvas(); }} />
59
+ <PromptDialog
60
+ open={dialogState.promptOpen}
61
+ title={dialogState.promptConfig?.title ?? ''}
62
+ message={dialogState.promptConfig?.message ?? ''}
63
+ defaultValue={dialogState.promptConfig?.defaultValue ?? ''}
64
+ placeholder={dialogState.promptConfig?.placeholder ?? ''}
65
+ validate={dialogState.promptConfig?.validate}
66
+ onConfirm={(value: string) => dialogState.promptConfig?.onConfirm(value)}
67
+ onCancel={() => { dialogState.closePrompt(); }}
68
+ />
69
+ <BisectionExportDialog
70
+ open={dialogState.bisectionExportOpen}
71
+ groupId={dialogState.bisectionExportGroupId}
72
+ onClose={() => { dialogState.closeBisectionExport(); }}
73
+ />
74
+ <RecoveryDialog />
75
+ <CommandPalette />
76
+
77
+ <style>
78
+ .app-shell {
79
+ display: flex;
80
+ flex-direction: column;
81
+ height: 100%;
82
+ }
83
+
84
+ .workspace {
85
+ flex: 1;
86
+ display: flex;
87
+ overflow: hidden;
88
+ min-height: 0;
89
+ }
90
+
91
+ .dock-area {
92
+ flex: 1;
93
+ min-width: 0;
94
+ overflow: hidden;
95
+ }
96
+
97
+ .dock-loading {
98
+ width: 100%;
99
+ height: 100%;
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ color: var(--text-secondary, #888);
104
+ font-size: 14px;
105
+ background: var(--surface-primary, #1e1e1e);
106
+ }
107
+ </style>
package/src/app.css ADDED
@@ -0,0 +1,215 @@
1
+ /* PixelWeaver global styles and theme variables */
2
+
3
+ /* --- Font import --- */
4
+ @import '@fontsource-variable/inter';
5
+
6
+ /* --- Design tokens (theme-independent) --- */
7
+ :root {
8
+ --font-ui: 'Inter Variable', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
9
+ --font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
10
+
11
+ --space-1: 2px;
12
+ --space-2: 4px;
13
+ --space-3: 8px;
14
+ --space-4: 12px;
15
+ --space-5: 16px;
16
+ --space-6: 24px;
17
+ --space-7: 32px;
18
+ --space-8: 48px;
19
+
20
+ --radius-sm: 2px;
21
+ --radius-md: 4px;
22
+ --radius-lg: 8px;
23
+
24
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
25
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
26
+
27
+ --transition-fast: 100ms ease;
28
+ --transition-normal: 200ms ease;
29
+
30
+ --text-xs: 10px;
31
+ --text-sm: 11px;
32
+ --text-base: 12px;
33
+ --text-lg: 13px;
34
+ --text-xl: 14px;
35
+ }
36
+
37
+ /* --- Dark theme (default) --- */
38
+ :root,
39
+ [data-theme="dark"] {
40
+ --bg-primary: #1e1e1e;
41
+ --bg-secondary: #252525;
42
+ --bg-canvas: #121212;
43
+ --bg-panel: #272727;
44
+ --bg-toolbar: #1a1a1a;
45
+ --text-primary: #e0e0e0;
46
+ --text-secondary: #a0a0a0;
47
+ --text-muted: #666666;
48
+ --accent: #4a9eff;
49
+ --accent-hover: #6ab4ff;
50
+ --text-on-accent: #ffffff;
51
+ --border: #3a3a3a;
52
+ --error: #e5534b;
53
+ --warning: #d4a04a;
54
+ --success: #34d67c;
55
+ }
56
+
57
+ /* --- Light theme --- */
58
+ [data-theme="light"] {
59
+ --bg-primary: #f0f0f0;
60
+ --bg-secondary: #e4e4e4;
61
+ --bg-canvas: #ffffff;
62
+ --bg-panel: #f5f5f5;
63
+ --bg-toolbar: #eaeaea;
64
+ --text-primary: #1a1a1a;
65
+ --text-secondary: #666666;
66
+ --text-muted: #999999;
67
+ --accent: #2563eb;
68
+ --accent-hover: #3b82f6;
69
+ --text-on-accent: #ffffff;
70
+ --border: #d0d0d0;
71
+ --error: #dc2626;
72
+ --warning: #d97706;
73
+ --success: #16a34a;
74
+ }
75
+
76
+ /* --- Base typography and rendering --- */
77
+ :root {
78
+ font-family: var(--font-ui);
79
+ font-size: var(--text-base);
80
+ line-height: 1.5;
81
+ font-weight: 400;
82
+ color: var(--text-primary);
83
+ background-color: var(--bg-primary);
84
+ font-synthesis: none;
85
+ text-rendering: optimizeLegibility;
86
+ -webkit-font-smoothing: antialiased;
87
+ -moz-osx-font-smoothing: grayscale;
88
+ }
89
+
90
+ /* --- CSS reset --- */
91
+ *,
92
+ *::before,
93
+ *::after {
94
+ box-sizing: border-box;
95
+ margin: 0;
96
+ padding: 0;
97
+ }
98
+
99
+ /*
100
+ * Smooth theme transitions -- scoped to UI chrome only.
101
+ * Applying to * caused unnecessary transition overhead on canvas elements,
102
+ * SVG icons, dockview internals, color pickers, and animations.
103
+ */
104
+ body,
105
+ #app,
106
+ div,
107
+ span,
108
+ header,
109
+ nav,
110
+ section,
111
+ aside,
112
+ footer,
113
+ button,
114
+ input,
115
+ textarea,
116
+ select,
117
+ label,
118
+ a,
119
+ li,
120
+ ul,
121
+ ol,
122
+ table,
123
+ th,
124
+ td {
125
+ transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
126
+ }
127
+
128
+ /* SVG icons get fill transition for theme switch, but not their child shapes */
129
+ :where(svg) {
130
+ transition: fill 150ms ease;
131
+ }
132
+
133
+ /* Performance-critical elements must never have transitions from the global rule */
134
+ canvas,
135
+ video {
136
+ transition: none;
137
+ }
138
+
139
+ /* Form elements inherit font from parent instead of browser defaults */
140
+ input,
141
+ button,
142
+ textarea,
143
+ select {
144
+ font: inherit;
145
+ color: inherit;
146
+ }
147
+
148
+ /* Media elements default to block */
149
+ img,
150
+ picture,
151
+ video,
152
+ canvas,
153
+ svg {
154
+ display: block;
155
+ max-width: 100%;
156
+ }
157
+
158
+ /* --- Scrollbar styling (dual approach for Tauri cross-platform) --- */
159
+
160
+ /* Standard scrollbar (Chrome 121+, Firefox 64+) */
161
+ * {
162
+ scrollbar-width: thin;
163
+ scrollbar-color: var(--border) transparent;
164
+ }
165
+
166
+ /* WebKit fallback (WebKitGTK, older WKWebView) */
167
+ ::-webkit-scrollbar {
168
+ width: 6px;
169
+ height: 6px;
170
+ }
171
+
172
+ ::-webkit-scrollbar-track {
173
+ background: transparent;
174
+ }
175
+
176
+ ::-webkit-scrollbar-thumb {
177
+ background: var(--border);
178
+ border-radius: 3px;
179
+ }
180
+
181
+ ::-webkit-scrollbar-thumb:hover {
182
+ background: var(--text-muted);
183
+ }
184
+
185
+ /* --- Layout --- */
186
+ html,
187
+ body {
188
+ height: 100%;
189
+ overflow: hidden;
190
+ }
191
+
192
+ #app {
193
+ height: 100%;
194
+ display: flex;
195
+ flex-direction: column;
196
+ user-select: none;
197
+ }
198
+
199
+ /* Allow text selection in input fields */
200
+ input, textarea, [contenteditable] {
201
+ user-select: text;
202
+ }
203
+
204
+ /* --- Focus indicators for keyboard accessibility --- */
205
+
206
+ /* Show a visible outline when an element receives keyboard focus */
207
+ :focus-visible {
208
+ outline: 2px solid var(--accent);
209
+ outline-offset: 2px;
210
+ }
211
+
212
+ /* Suppress the always-visible :focus outline for mouse/pointer users */
213
+ :focus:not(:focus-visible) {
214
+ outline: none;
215
+ }