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,168 @@
1
+ /**
2
+ * Tests for the Action Log reactive state module.
3
+ *
4
+ * These tests exercise the action log's sync, filter, toggle, delete,
5
+ * and select operations. Since the action log syncs from the dispatcher,
6
+ * we set up the dispatcher with registered commands and dispatch them
7
+ * to populate the semantic log and undo stack.
8
+ */
9
+
10
+ import { describe, it, expect, beforeEach } from 'vitest';
11
+ import type { Command, CommandDefinition } from '../core/commands.js';
12
+ import type { CommandType, ParamsOf } from '../core/command-params.js';
13
+ import { commandRegistry } from '../core/registries.svelte.js';
14
+ import { dispatch, _resetForTesting as resetDispatcher } from '../core/dispatcher.js';
15
+ import { actionLog } from './action-log.svelte.js';
16
+
17
+ // --- Helpers ---
18
+
19
+ function registerTestCommand(name: string, description: string): void {
20
+ const def: CommandDefinition = {
21
+ execute() { /* no-op for test */ },
22
+ undo() { /* no-op for test */ },
23
+ describe: () => description,
24
+ tier: 'frame',
25
+ };
26
+ commandRegistry.set(name, def);
27
+ }
28
+
29
+ // Typed overload: validates params when type is a known command literal
30
+ function makeAndDispatch<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
31
+ // String fallback: for dynamic/unknown command types in tests
32
+ function makeAndDispatch(type: string, params?: Record<string, unknown>): Command;
33
+ function makeAndDispatch(type: string, params: Record<string, unknown> = {}): Command {
34
+ const cmd: Command = {
35
+ type,
36
+ plugin: 'test/plugin',
37
+ version: '1.0.0',
38
+ params,
39
+ timestamp: Date.now(),
40
+ id: crypto.randomUUID(),
41
+ };
42
+ dispatch(cmd);
43
+ return cmd;
44
+ }
45
+
46
+ // --- Tests ---
47
+
48
+ describe('Action Log State', () => {
49
+ beforeEach(() => {
50
+ resetDispatcher();
51
+ // Re-register test commands after dispatcher reset clears callbacks
52
+ registerTestCommand('draw_pixel', 'Drew pixel');
53
+ registerTestCommand('fill_rect', 'Filled rectangle');
54
+ registerTestCommand('set_color', 'Set color');
55
+ });
56
+
57
+ it('should sync entries from the dispatcher semantic log', () => {
58
+ const cmd1 = makeAndDispatch('draw_pixel', { x: 0, y: 0 });
59
+ const cmd2 = makeAndDispatch('fill_rect', { x: 1, y: 1, w: 5, h: 5 });
60
+
61
+ actionLog.syncFromDispatcher();
62
+
63
+ const entries = actionLog.getEntries();
64
+ expect(entries).toHaveLength(2);
65
+ expect(entries[0]?.commandId).toBe(cmd1.id);
66
+ expect(entries[0]?.type).toBe('draw_pixel');
67
+ expect(entries[0]?.description).toBe('Drew pixel');
68
+ expect(entries[1]?.commandId).toBe(cmd2.id);
69
+ expect(entries[1]?.type).toBe('fill_rect');
70
+ });
71
+
72
+ it('should filter entries by query string matching description', () => {
73
+ makeAndDispatch('draw_pixel', { x: 0, y: 0 });
74
+ makeAndDispatch('fill_rect', { x: 1, y: 1, w: 5, h: 5 });
75
+ makeAndDispatch('set_color', { color: '#ff0000' });
76
+
77
+ actionLog.syncFromDispatcher();
78
+ actionLog.setFilterQuery('pixel');
79
+
80
+ const filtered = actionLog.getFilteredEntries();
81
+ expect(filtered).toHaveLength(1);
82
+ expect(filtered[0]?.type).toBe('draw_pixel');
83
+ });
84
+
85
+ it('should filter entries by query string matching command type', () => {
86
+ makeAndDispatch('draw_pixel', { x: 0, y: 0 });
87
+ makeAndDispatch('fill_rect', { x: 1, y: 1, w: 5, h: 5 });
88
+
89
+ actionLog.syncFromDispatcher();
90
+ actionLog.setFilterQuery('fill_rect');
91
+
92
+ const filtered = actionLog.getFilteredEntries();
93
+ expect(filtered).toHaveLength(1);
94
+ expect(filtered[0]?.type).toBe('fill_rect');
95
+ });
96
+
97
+ it('should return all entries when filter query is empty', () => {
98
+ makeAndDispatch('draw_pixel');
99
+ makeAndDispatch('fill_rect');
100
+ makeAndDispatch('set_color');
101
+
102
+ actionLog.syncFromDispatcher();
103
+ actionLog.setFilterQuery('');
104
+
105
+ expect(actionLog.getFilteredEntries()).toHaveLength(3);
106
+ });
107
+
108
+ it('should toggle an entry between enabled and disabled', () => {
109
+ const cmd = makeAndDispatch('draw_pixel', { x: 5, y: 5 });
110
+ actionLog.syncFromDispatcher();
111
+
112
+ const entry = actionLog.getEntry(cmd.id);
113
+ expect(entry?.enabled).toBe(true);
114
+
115
+ actionLog.toggleEntry(cmd.id);
116
+ expect(actionLog.getEntry(cmd.id)?.enabled).toBe(false);
117
+
118
+ actionLog.toggleEntry(cmd.id);
119
+ expect(actionLog.getEntry(cmd.id)?.enabled).toBe(true);
120
+ });
121
+
122
+ it('should delete an entry from the log', () => {
123
+ const cmd1 = makeAndDispatch('draw_pixel');
124
+ const cmd2 = makeAndDispatch('fill_rect');
125
+ actionLog.syncFromDispatcher();
126
+ expect(actionLog.getEntries()).toHaveLength(2);
127
+
128
+ actionLog.deleteEntry(cmd1.id);
129
+ expect(actionLog.getEntries()).toHaveLength(1);
130
+ expect(actionLog.getEntry(cmd1.id)).toBeUndefined();
131
+ expect(actionLog.getEntry(cmd2.id)).toBeDefined();
132
+ });
133
+
134
+ it('should clear selection when deleting the selected entry', () => {
135
+ const cmd = makeAndDispatch('draw_pixel');
136
+ actionLog.syncFromDispatcher();
137
+
138
+ actionLog.selectEntry(cmd.id);
139
+ expect(actionLog.getSelectedEntryId()).toBe(cmd.id);
140
+
141
+ actionLog.deleteEntry(cmd.id);
142
+ expect(actionLog.getSelectedEntryId()).toBeNull();
143
+ });
144
+
145
+ it('should select an entry by ID', () => {
146
+ const cmd1 = makeAndDispatch('draw_pixel');
147
+ const cmd2 = makeAndDispatch('fill_rect');
148
+ actionLog.syncFromDispatcher();
149
+
150
+ actionLog.selectEntry(cmd1.id);
151
+ expect(actionLog.getSelectedEntryId()).toBe(cmd1.id);
152
+
153
+ actionLog.selectEntry(cmd2.id);
154
+ expect(actionLog.getSelectedEntryId()).toBe(cmd2.id);
155
+ });
156
+
157
+ it('should preserve enabled states across sync', () => {
158
+ const cmd = makeAndDispatch('draw_pixel');
159
+ actionLog.syncFromDispatcher();
160
+
161
+ actionLog.toggleEntry(cmd.id);
162
+ expect(actionLog.getEntry(cmd.id)?.enabled).toBe(false);
163
+
164
+ // Sync again -- should preserve the disabled state
165
+ actionLog.syncFromDispatcher();
166
+ expect(actionLog.getEntry(cmd.id)?.enabled).toBe(false);
167
+ });
168
+ });
@@ -0,0 +1,403 @@
1
+ /**
2
+ * History Commands Plugin -- registers meta-commands that manipulate the
3
+ * action log itself.
4
+ *
5
+ * These are 'project' tier commands that allow the user to reorder, edit,
6
+ * toggle, and delete entries in the semantic action log. Unlike regular
7
+ * commands that modify pixel data, these commands modify the history.
8
+ */
9
+
10
+ import type { PluginModule } from '../core/plugin-loader.js';
11
+ import type { Command } from '../core/commands.js';
12
+ import { actionLog } from './action-log.svelte.js';
13
+ import { applyReorder } from './replay-engine.js';
14
+ import { exportSpec, importSpec, type SpecDocument } from './spec-format.js';
15
+ import { macroSystem } from './macros.svelte.js';
16
+ import { dispatch } from '../core/dispatcher.js';
17
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
18
+ import { notificationState } from '../ui/notifications/notification-state.svelte.js';
19
+ import { downloadFile } from '../export/download.js';
20
+ import DownloadIcon from '~icons/lucide/download';
21
+ import UploadIcon from '~icons/lucide/upload';
22
+ import TrashIcon from '~icons/lucide/trash-2';
23
+ import CircleDotIcon from '~icons/lucide/circle-dot';
24
+ import SquareIcon from '~icons/lucide/square';
25
+
26
+ type ReorderSnapshot = { previousOrder: string[] };
27
+ type EditParamsSnapshot = { oldParams: Record<string, unknown> };
28
+ type ToggleSnapshot = { wasEnabled: boolean };
29
+ type DeleteActionSnapshot = { entry: Record<string, unknown>; index: number };
30
+
31
+ export const historyCommandsPlugin: PluginModule = {
32
+ name: 'builtin/history',
33
+ version: '1.0.0',
34
+ description: 'Meta-commands for manipulating the action log',
35
+ dependencies: [],
36
+
37
+ register(api) {
38
+ // --- reorder_actions ---
39
+ // Reorder the action log entries and replay to produce a new buffer.
40
+ api.addCommand('reorder_actions', {
41
+ tier: 'project',
42
+
43
+ execute(params) {
44
+ const newOrder = params["newOrder"];
45
+ const width = params["width"];
46
+ const height = params["height"];
47
+ const layerId = params["layerId"];
48
+
49
+ // Capture previous order as snapshot for undo
50
+ const snapshot: ReorderSnapshot = {
51
+ previousOrder: actionLog.getEntries().map((e) => e.commandId),
52
+ };
53
+
54
+ applyReorder(newOrder, width, height, layerId);
55
+ return snapshot;
56
+ },
57
+
58
+ undo(params, _ctx, snapshot) {
59
+ const typed = snapshot as ReorderSnapshot | undefined;
60
+ const width = params["width"];
61
+ const height = params["height"];
62
+ const layerId = params["layerId"];
63
+
64
+ if (typed?.previousOrder) {
65
+ applyReorder(typed.previousOrder, width, height, layerId);
66
+ }
67
+ },
68
+
69
+ describe() {
70
+ return 'Reordered action history';
71
+ },
72
+ });
73
+
74
+ // --- edit_action_params ---
75
+ // Edit the params of an existing action log entry.
76
+ api.addCommand('edit_action_params', {
77
+ tier: 'project',
78
+
79
+ execute(params) {
80
+ const entryId = params["entryId"];
81
+ const newParams = params["newParams"];
82
+
83
+ const entry = actionLog.getEntry(entryId);
84
+ if (!entry) return;
85
+
86
+ // Capture old params as snapshot for undo
87
+ const snapshot: EditParamsSnapshot = { oldParams: { ...entry.params } };
88
+
89
+ // Update the entry's params
90
+ Object.assign(entry.params, newParams);
91
+ return snapshot;
92
+ },
93
+
94
+ undo(params, _ctx, snapshot) {
95
+ const entryId = params["entryId"];
96
+ const typed = snapshot as EditParamsSnapshot | undefined;
97
+
98
+ const entry = actionLog.getEntry(entryId);
99
+ if (!entry || !typed) return;
100
+
101
+ // Restore old params (replace entirely to avoid leftover keys).
102
+ // Reflect.deleteProperty avoids the no-dynamic-delete rule which
103
+ // targets the literal `delete expr[key]` syntax only.
104
+ for (const key of Object.keys(entry.params)) {
105
+ Reflect.deleteProperty(entry.params, key);
106
+ }
107
+ Object.assign(entry.params, typed.oldParams);
108
+ },
109
+
110
+ describe(params) {
111
+ return `Edited params of action "${String(params["entryId"])}"`;
112
+ },
113
+ });
114
+
115
+ // --- toggle_action ---
116
+ // Toggle an action log entry's enabled state.
117
+ api.addCommand('toggle_action', {
118
+ tier: 'project',
119
+
120
+ execute(params) {
121
+ const entryId = params["entryId"];
122
+ const entry = actionLog.getEntry(entryId);
123
+ let snapshot: ToggleSnapshot | undefined;
124
+ if (entry) {
125
+ snapshot = { wasEnabled: entry.enabled };
126
+ }
127
+ actionLog.toggleEntry(entryId);
128
+ return snapshot;
129
+ },
130
+
131
+ undo(params, _ctx, snapshot) {
132
+ const entryId = params["entryId"];
133
+ const entry = actionLog.getEntry(entryId);
134
+ if (!entry) return;
135
+
136
+ const typed = snapshot as ToggleSnapshot | undefined;
137
+ if (!typed) return;
138
+
139
+ // Restore previous enabled state (toggle only if current differs from snapshot)
140
+ if (entry.enabled !== typed.wasEnabled) {
141
+ actionLog.toggleEntry(entryId);
142
+ }
143
+ },
144
+
145
+ describe(params) {
146
+ const entry = actionLog.getEntry(params["entryId"]);
147
+ return entry?.enabled
148
+ ? `Enabled action "${String(params["entryId"])}"`
149
+ : `Disabled action "${String(params["entryId"])}"`;
150
+ },
151
+ });
152
+
153
+ // --- delete_action ---
154
+ // Remove an action log entry.
155
+ api.addCommand('delete_action', {
156
+ tier: 'project',
157
+
158
+ execute(params) {
159
+ const entryId = params["entryId"];
160
+ const entry = actionLog.getEntry(entryId);
161
+ let snapshot: DeleteActionSnapshot | undefined;
162
+ if (entry) {
163
+ // Capture snapshot and position for undo
164
+ const entries = actionLog.getEntries();
165
+ snapshot = {
166
+ entry: { ...entry },
167
+ index: entries.findIndex((e) => e.commandId === entryId),
168
+ };
169
+ }
170
+ actionLog.deleteEntry(entryId);
171
+ return snapshot;
172
+ },
173
+
174
+ undo(_params, _ctx, snapshot) {
175
+ const typed = snapshot as DeleteActionSnapshot | undefined;
176
+ if (!typed) return;
177
+
178
+ // Re-insert the entry at its original position
179
+ const entries = actionLog.getEntries();
180
+ entries.splice(typed.index, 0, typed.entry as never);
181
+ },
182
+
183
+ describe(params) {
184
+ return `Deleted action "${String(params["entryId"])}"`;
185
+ },
186
+ });
187
+
188
+ // --- export_spec ---
189
+ // Serialize the current action log to a JSON spec document and trigger
190
+ // a browser download. Non-undoable: this is a read-only operation.
191
+ api.addCommand('export_spec', {
192
+ tier: 'project',
193
+ undoable: false,
194
+ label: 'Export Spec',
195
+ category: 'History',
196
+ icon: DownloadIcon,
197
+
198
+ async execute() {
199
+ const spec = exportSpec(
200
+ actionLog.getEntries(),
201
+ canvasState.canvasWidth,
202
+ canvasState.canvasHeight,
203
+ );
204
+ const json = JSON.stringify(spec, null, 2);
205
+ const blob = new Blob([json], { type: 'application/json' });
206
+ const filename = `pixelweaver-spec-${String(Date.now())}.json`;
207
+ await downloadFile({
208
+ blob,
209
+ filename,
210
+ filterName: 'JSON Spec',
211
+ filterExtensions: ['json'],
212
+ });
213
+
214
+ notificationState.push({
215
+ id: 'export-spec-success',
216
+ message: `Exported spec with ${String(spec.commands.length)} command(s)`,
217
+ type: 'info',
218
+ autoDismissMs: 3000,
219
+ });
220
+ },
221
+
222
+ undo() { /* non-undoable */ },
223
+
224
+ describe() {
225
+ return 'Exported spec';
226
+ },
227
+ });
228
+
229
+ // --- import_spec ---
230
+ // Import a spec document (passed as a 'spec' param) and dispatch its
231
+ // commands. The file picker UI lives in ActionLogPanel; this command
232
+ // only accepts an already-parsed SpecDocument. Non-undoable: the
233
+ // dispatched child commands are individually undoable.
234
+ api.addCommand('import_spec', {
235
+ tier: 'project',
236
+ undoable: false,
237
+ label: 'Import Spec',
238
+ category: 'History',
239
+ icon: UploadIcon,
240
+
241
+ execute(params) {
242
+ const spec = params["spec"] as SpecDocument | undefined;
243
+ if (!spec) {
244
+ notificationState.push({
245
+ id: 'import-spec-missing',
246
+ message: 'Import spec: no spec provided',
247
+ type: 'warning',
248
+ autoDismissMs: 4000,
249
+ });
250
+ return;
251
+ }
252
+
253
+ let commands: { type: string; params: Record<string, unknown> }[];
254
+ try {
255
+ commands = importSpec(spec);
256
+ } catch (err) {
257
+ notificationState.push({
258
+ id: 'import-spec-invalid',
259
+ message: `Import spec failed: ${(err as Error).message}`,
260
+ type: 'warning',
261
+ autoDismissMs: 5000,
262
+ });
263
+ return;
264
+ }
265
+
266
+ // Dispatch each imported command through the normal dispatcher
267
+ // pipeline so they are individually recorded in the undo stack.
268
+ let dispatched = 0;
269
+ for (const cmd of commands) {
270
+ const command: Command = {
271
+ type: cmd.type,
272
+ plugin: 'builtin/history',
273
+ version: '1.0.0',
274
+ params: cmd.params,
275
+ timestamp: Date.now(),
276
+ id: crypto.randomUUID(),
277
+ };
278
+ try {
279
+ dispatch(command);
280
+ dispatched++;
281
+ } catch (err) {
282
+ // Skip commands whose types are not registered; report in notification.
283
+ console.warn(`import_spec: skipping command "${cmd.type}"`, err);
284
+ }
285
+ }
286
+
287
+ notificationState.push({
288
+ id: 'import-spec-success',
289
+ message: `Imported ${String(dispatched)}/${String(commands.length)} command(s) from spec`,
290
+ type: 'info',
291
+ autoDismissMs: 3000,
292
+ });
293
+ },
294
+
295
+ undo() { /* non-undoable -- child commands handle their own undo */ },
296
+
297
+ describe() {
298
+ return 'Imported spec';
299
+ },
300
+ });
301
+
302
+ // --- clear_action_log ---
303
+ // Clear the local action log view. Non-undoable: clearing history
304
+ // should not itself be entered into history.
305
+ api.addCommand('clear_action_log', {
306
+ tier: 'project',
307
+ undoable: false,
308
+ label: 'Clear Action Log',
309
+ category: 'History',
310
+ icon: TrashIcon,
311
+
312
+ execute() {
313
+ const count = actionLog.getEntries().length;
314
+ actionLog.clearAll();
315
+ notificationState.push({
316
+ id: 'clear-action-log',
317
+ message: `Cleared ${String(count)} action log entr${count === 1 ? 'y' : 'ies'}`,
318
+ type: 'info',
319
+ autoDismissMs: 3000,
320
+ });
321
+ },
322
+
323
+ undo() { /* non-undoable */ },
324
+
325
+ describe() {
326
+ return 'Cleared action log';
327
+ },
328
+ });
329
+
330
+ // --- record_macro_start ---
331
+ // Begin recording dispatched commands into a new macro buffer.
332
+ api.addCommand('record_macro_start', {
333
+ tier: 'project',
334
+ undoable: false,
335
+ label: 'Start Recording Macro',
336
+ category: 'History',
337
+ icon: CircleDotIcon,
338
+
339
+ execute() {
340
+ if (macroSystem.recording) {
341
+ notificationState.push({
342
+ id: 'record-macro-already',
343
+ message: 'Macro recording already in progress',
344
+ type: 'info',
345
+ autoDismissMs: 3000,
346
+ });
347
+ return;
348
+ }
349
+ macroSystem.startRecording();
350
+ notificationState.push({
351
+ id: 'record-macro-start',
352
+ message: 'Macro recording started',
353
+ type: 'info',
354
+ autoDismissMs: 3000,
355
+ });
356
+ },
357
+
358
+ undo() { /* non-undoable */ },
359
+
360
+ describe() {
361
+ return 'Started macro recording';
362
+ },
363
+ });
364
+
365
+ // --- record_macro_stop ---
366
+ // Stop recording and finalize the macro. Accepts optional 'name' and
367
+ // 'coordMode' params; defaults to a timestamped name and absolute mode.
368
+ api.addCommand('record_macro_stop', {
369
+ tier: 'project',
370
+ undoable: false,
371
+ label: 'Stop Recording Macro',
372
+ category: 'History',
373
+ icon: SquareIcon,
374
+
375
+ execute(params) {
376
+ if (!macroSystem.recording) {
377
+ notificationState.push({
378
+ id: 'record-macro-not-recording',
379
+ message: 'No macro recording in progress',
380
+ type: 'info',
381
+ autoDismissMs: 3000,
382
+ });
383
+ return;
384
+ }
385
+ const name = params["name"] ?? `Macro ${new Date().toLocaleTimeString()}`;
386
+ const coordMode = params["coordMode"] ?? 'absolute';
387
+ const macro = macroSystem.stopRecording(name, coordMode);
388
+ notificationState.push({
389
+ id: 'record-macro-stop',
390
+ message: `Recorded macro "${macro.name}" with ${String(macro.commands.length)} command(s)`,
391
+ type: 'info',
392
+ autoDismissMs: 3000,
393
+ });
394
+ },
395
+
396
+ undo() { /* non-undoable */ },
397
+
398
+ describe() {
399
+ return 'Stopped macro recording';
400
+ },
401
+ });
402
+ },
403
+ };