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,548 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import type { Command, CommandDefinition, UndoTier } from './commands.js';
3
+ import type { CommandType, ParamsOf } from './command-params.js';
4
+ import { commandRegistry } from './registries.svelte.js';
5
+ import {
6
+ dispatch,
7
+ undoLast,
8
+ redoLast,
9
+ getSemanticLog,
10
+ getUndoStack,
11
+ getRedoStack,
12
+ canUndo,
13
+ canRedo,
14
+ setMaxUndoSize,
15
+ getMaxUndoSize,
16
+ onExecute,
17
+ onUndo,
18
+ onLog,
19
+ onSpecExport,
20
+ _resetForTesting,
21
+ } from './dispatcher.js';
22
+
23
+ // --- Helpers ---
24
+
25
+ /** Create a minimal command for testing */
26
+ // Typed overload: validates params when type is a known command literal
27
+ function makeCommand<T extends CommandType>(type: T, params: ParamsOf<T>): Command;
28
+ // String fallback: for dynamic/unknown command types in tests
29
+ function makeCommand(type: string, params?: Record<string, unknown>): Command;
30
+ function makeCommand(type: string, params: Record<string, unknown> = {}): Command {
31
+ return {
32
+ type,
33
+ plugin: 'test/plugin',
34
+ version: '1.0.0',
35
+ params,
36
+ timestamp: Date.now(),
37
+ id: crypto.randomUUID(),
38
+ };
39
+ }
40
+
41
+ /** Create a command definition with spies, optionally specifying a tier */
42
+ function makeDefinition(
43
+ descriptionText = 'test command',
44
+ tier?: UndoTier,
45
+ ): CommandDefinition & { execute: ReturnType<typeof vi.fn>; undo: ReturnType<typeof vi.fn> } {
46
+ return {
47
+ execute: vi.fn(),
48
+ undo: vi.fn(),
49
+ describe: () => descriptionText,
50
+ ...(tier !== undefined ? { tier } : {}),
51
+ };
52
+ }
53
+
54
+ // --- Tests ---
55
+
56
+ describe('Command Dispatcher', () => {
57
+ beforeEach(() => {
58
+ _resetForTesting();
59
+ // Clear command registry between tests by removing known keys
60
+ // (registry has no reset, so we delete entries we add)
61
+ });
62
+
63
+ it('should execute a dispatched command with correct params', () => {
64
+ const def = makeDefinition();
65
+ commandRegistry.set('test_cmd', def);
66
+
67
+ const cmd = makeCommand('test_cmd', { x: 10, y: 20 });
68
+ dispatch(cmd);
69
+
70
+ expect(def.execute).toHaveBeenCalledOnce();
71
+ expect(def.execute).toHaveBeenCalledWith({ x: 10, y: 20 }, expect.any(Object));
72
+ });
73
+
74
+ it('should throw when dispatching an unknown command type', () => {
75
+ const cmd = makeCommand('nonexistent_cmd');
76
+ expect(() => { dispatch(cmd); }).toThrow('Unknown command type "nonexistent_cmd"');
77
+ });
78
+
79
+ it('should fire the onExecute consumer hook', () => {
80
+ const def = makeDefinition();
81
+ commandRegistry.set('test_cmd', def);
82
+
83
+ const callback = vi.fn();
84
+ onExecute(callback);
85
+
86
+ const cmd = makeCommand('test_cmd');
87
+ dispatch(cmd);
88
+
89
+ expect(callback).toHaveBeenCalledOnce();
90
+ expect(callback).toHaveBeenCalledWith(cmd);
91
+ });
92
+
93
+ it('should fire the onLog consumer hook with a semantic log entry', () => {
94
+ const def = makeDefinition('Drew a pixel');
95
+ commandRegistry.set('test_cmd', def);
96
+
97
+ const callback = vi.fn();
98
+ onLog(callback);
99
+
100
+ const cmd = makeCommand('test_cmd');
101
+ dispatch(cmd);
102
+
103
+ expect(callback).toHaveBeenCalledOnce();
104
+ expect(callback).toHaveBeenCalledWith(
105
+ expect.objectContaining({
106
+ description: 'Drew a pixel',
107
+ commandId: cmd.id,
108
+ }),
109
+ );
110
+ });
111
+
112
+ it('should fire the onSpecExport consumer hook', () => {
113
+ const def = makeDefinition();
114
+ commandRegistry.set('test_cmd', def);
115
+
116
+ const callback = vi.fn();
117
+ onSpecExport(callback);
118
+
119
+ const cmd = makeCommand('test_cmd');
120
+ dispatch(cmd);
121
+
122
+ expect(callback).toHaveBeenCalledOnce();
123
+ expect(callback).toHaveBeenCalledWith(cmd);
124
+ });
125
+
126
+ it('should fire all 4 consumer hooks on dispatch', () => {
127
+ const def = makeDefinition();
128
+ commandRegistry.set('test_cmd', def);
129
+
130
+ const executeCb = vi.fn();
131
+ const undoCb = vi.fn();
132
+ const logCb = vi.fn();
133
+ const specCb = vi.fn();
134
+
135
+ onExecute(executeCb);
136
+ onUndo(undoCb);
137
+ onLog(logCb);
138
+ onSpecExport(specCb);
139
+
140
+ const cmd = makeCommand('test_cmd');
141
+ dispatch(cmd);
142
+
143
+ // Execute, log, and spec-export fire on dispatch; undo does not
144
+ expect(executeCb).toHaveBeenCalledOnce();
145
+ expect(logCb).toHaveBeenCalledOnce();
146
+ expect(specCb).toHaveBeenCalledOnce();
147
+ expect(undoCb).not.toHaveBeenCalled();
148
+ });
149
+
150
+ it('should create a semantic log entry on dispatch', () => {
151
+ const def = makeDefinition('Added layer "Background"');
152
+ commandRegistry.set('test_cmd', def);
153
+
154
+ const cmd = makeCommand('test_cmd');
155
+ dispatch(cmd);
156
+
157
+ const log = getSemanticLog();
158
+ expect(log).toHaveLength(1);
159
+ expect(log[0]).toEqual({
160
+ description: 'Added layer "Background"',
161
+ timestamp: cmd.timestamp,
162
+ commandId: cmd.id,
163
+ });
164
+ });
165
+
166
+ it('should undo the last command by calling its undo function', () => {
167
+ const def = makeDefinition();
168
+ commandRegistry.set('test_cmd', def);
169
+
170
+ const cmd = makeCommand('test_cmd', { x: 5 });
171
+ dispatch(cmd);
172
+
173
+ const undone = undoLast();
174
+
175
+ expect(undone).toBe(cmd);
176
+ expect(def.undo).toHaveBeenCalledOnce();
177
+ expect(def.undo).toHaveBeenCalledWith({ x: 5 }, expect.any(Object), undefined);
178
+ });
179
+
180
+ it('should fire the onUndo consumer hook when undoing', () => {
181
+ const def = makeDefinition();
182
+ commandRegistry.set('test_cmd', def);
183
+
184
+ const callback = vi.fn();
185
+ onUndo(callback);
186
+
187
+ const cmd = makeCommand('test_cmd');
188
+ dispatch(cmd);
189
+ undoLast();
190
+
191
+ expect(callback).toHaveBeenCalledOnce();
192
+ expect(callback).toHaveBeenCalledWith(cmd);
193
+ });
194
+
195
+ it('should return undefined when undoing with an empty stack', () => {
196
+ const result = undoLast();
197
+ expect(result).toBeUndefined();
198
+ });
199
+
200
+ it('should redo the last undone command', () => {
201
+ const def = makeDefinition();
202
+ commandRegistry.set('test_cmd', def);
203
+
204
+ const cmd = makeCommand('test_cmd', { value: 42 });
205
+ dispatch(cmd);
206
+ undoLast();
207
+
208
+ def.execute.mockClear();
209
+ const redone = redoLast();
210
+
211
+ expect(redone).toBe(cmd);
212
+ expect(def.execute).toHaveBeenCalledOnce();
213
+ expect(def.execute).toHaveBeenCalledWith({ value: 42 }, expect.any(Object));
214
+ });
215
+
216
+ it('should return undefined when redoing with an empty stack', () => {
217
+ const result = redoLast();
218
+ expect(result).toBeUndefined();
219
+ });
220
+
221
+ it('should clear the redo stack when a new command is dispatched', () => {
222
+ const def = makeDefinition();
223
+ commandRegistry.set('test_cmd', def);
224
+
225
+ dispatch(makeCommand('test_cmd'));
226
+ undoLast();
227
+
228
+ // Now dispatch a new command -- redo should be cleared
229
+ dispatch(makeCommand('test_cmd'));
230
+ const result = redoLast();
231
+ expect(result).toBeUndefined();
232
+ });
233
+
234
+ it('should undo multiple commands in LIFO order', () => {
235
+ const defA = makeDefinition('Command A');
236
+ const defB = makeDefinition('Command B');
237
+ const defC = makeDefinition('Command C');
238
+
239
+ commandRegistry.set('cmd_a', defA);
240
+ commandRegistry.set('cmd_b', defB);
241
+ commandRegistry.set('cmd_c', defC);
242
+
243
+ const cmdA = makeCommand('cmd_a');
244
+ const cmdB = makeCommand('cmd_b');
245
+ const cmdC = makeCommand('cmd_c');
246
+
247
+ dispatch(cmdA);
248
+ dispatch(cmdB);
249
+ dispatch(cmdC);
250
+
251
+ expect(undoLast()).toBe(cmdC);
252
+ expect(undoLast()).toBe(cmdB);
253
+ expect(undoLast()).toBe(cmdA);
254
+ expect(undoLast()).toBeUndefined();
255
+
256
+ expect(defC.undo).toHaveBeenCalledOnce();
257
+ expect(defB.undo).toHaveBeenCalledOnce();
258
+ expect(defA.undo).toHaveBeenCalledOnce();
259
+ });
260
+
261
+ it('should allow unsubscribing from consumer hooks', () => {
262
+ const def = makeDefinition();
263
+ commandRegistry.set('test_cmd', def);
264
+
265
+ const callback = vi.fn();
266
+ const unsub = onExecute(callback);
267
+
268
+ dispatch(makeCommand('test_cmd'));
269
+ expect(callback).toHaveBeenCalledOnce();
270
+
271
+ unsub();
272
+
273
+ dispatch(makeCommand('test_cmd'));
274
+ // Should still be 1 call, not 2
275
+ expect(callback).toHaveBeenCalledOnce();
276
+ });
277
+ });
278
+
279
+ describe('Two-Tier Undo System', () => {
280
+ beforeEach(() => {
281
+ _resetForTesting();
282
+ });
283
+
284
+ it('should assign tier from command definition', () => {
285
+ const frameDef = makeDefinition('draw', 'frame');
286
+ const projectDef = makeDefinition('add layer', 'project');
287
+ commandRegistry.set('draw_pixel', frameDef);
288
+ commandRegistry.set('add_layer', projectDef);
289
+
290
+ dispatch(makeCommand('draw_pixel'));
291
+ dispatch(makeCommand('add_layer'));
292
+
293
+ const stack = getUndoStack();
294
+ expect(stack).toHaveLength(2);
295
+ expect(stack[0]?.tier).toBe('frame');
296
+ expect(stack[1]?.tier).toBe('project');
297
+ });
298
+
299
+ it('should default tier to "frame" when not specified in definition', () => {
300
+ const def = makeDefinition('no tier');
301
+ commandRegistry.set('untagged_cmd', def);
302
+
303
+ dispatch(makeCommand('untagged_cmd'));
304
+
305
+ const stack = getUndoStack();
306
+ expect(stack[0]?.tier).toBe('frame');
307
+ });
308
+
309
+ it('should preserve tier when moving entries to redo stack', () => {
310
+ const projectDef = makeDefinition('project cmd', 'project');
311
+ commandRegistry.set('project_cmd', projectDef);
312
+
313
+ dispatch(makeCommand('project_cmd'));
314
+ undoLast();
315
+
316
+ const redo = getRedoStack();
317
+ expect(redo).toHaveLength(1);
318
+ expect(redo[0]?.tier).toBe('project');
319
+ });
320
+
321
+ it('should preserve tier when redoing', () => {
322
+ const projectDef = makeDefinition('project cmd', 'project');
323
+ commandRegistry.set('project_cmd', projectDef);
324
+
325
+ dispatch(makeCommand('project_cmd'));
326
+ undoLast();
327
+ redoLast();
328
+
329
+ const stack = getUndoStack();
330
+ expect(stack).toHaveLength(1);
331
+ expect(stack[0]?.tier).toBe('project');
332
+ });
333
+ });
334
+
335
+ describe('canUndo / canRedo', () => {
336
+ beforeEach(() => {
337
+ _resetForTesting();
338
+ });
339
+
340
+ it('should return false when stacks are empty', () => {
341
+ expect(canUndo()).toBe(false);
342
+ expect(canRedo()).toBe(false);
343
+ });
344
+
345
+ it('should return true for canUndo after dispatch', () => {
346
+ const def = makeDefinition();
347
+ commandRegistry.set('test_cmd', def);
348
+ dispatch(makeCommand('test_cmd'));
349
+
350
+ expect(canUndo()).toBe(true);
351
+ expect(canRedo()).toBe(false);
352
+ });
353
+
354
+ it('should return true for canRedo after undo', () => {
355
+ const def = makeDefinition();
356
+ commandRegistry.set('test_cmd', def);
357
+ dispatch(makeCommand('test_cmd'));
358
+ undoLast();
359
+
360
+ expect(canUndo()).toBe(false);
361
+ expect(canRedo()).toBe(true);
362
+ });
363
+
364
+ it('should return correct values after mixed operations', () => {
365
+ const def = makeDefinition();
366
+ commandRegistry.set('test_cmd', def);
367
+
368
+ dispatch(makeCommand('test_cmd'));
369
+ dispatch(makeCommand('test_cmd'));
370
+
371
+ expect(canUndo()).toBe(true);
372
+ expect(canRedo()).toBe(false);
373
+
374
+ undoLast();
375
+ expect(canUndo()).toBe(true);
376
+ expect(canRedo()).toBe(true);
377
+
378
+ undoLast();
379
+ expect(canUndo()).toBe(false);
380
+ expect(canRedo()).toBe(true);
381
+
382
+ redoLast();
383
+ expect(canUndo()).toBe(true);
384
+ expect(canRedo()).toBe(true);
385
+ });
386
+ });
387
+
388
+ describe('getUndoStack / getRedoStack', () => {
389
+ beforeEach(() => {
390
+ _resetForTesting();
391
+ });
392
+
393
+ it('should return empty arrays initially', () => {
394
+ expect(getUndoStack()).toHaveLength(0);
395
+ expect(getRedoStack()).toHaveLength(0);
396
+ });
397
+
398
+ it('should return undo entries with correct command references', () => {
399
+ const def = makeDefinition();
400
+ commandRegistry.set('test_cmd', def);
401
+
402
+ const cmd = makeCommand('test_cmd');
403
+ dispatch(cmd);
404
+
405
+ const stack = getUndoStack();
406
+ expect(stack).toHaveLength(1);
407
+ expect(stack[0]?.command).toBe(cmd);
408
+ expect(stack[0]?.id).toBe(cmd.id);
409
+ });
410
+
411
+ it('should move entries between stacks on undo/redo', () => {
412
+ const def = makeDefinition();
413
+ commandRegistry.set('test_cmd', def);
414
+
415
+ const cmd = makeCommand('test_cmd');
416
+ dispatch(cmd);
417
+
418
+ expect(getUndoStack()).toHaveLength(1);
419
+ expect(getRedoStack()).toHaveLength(0);
420
+
421
+ undoLast();
422
+ expect(getUndoStack()).toHaveLength(0);
423
+ expect(getRedoStack()).toHaveLength(1);
424
+
425
+ redoLast();
426
+ expect(getUndoStack()).toHaveLength(1);
427
+ expect(getRedoStack()).toHaveLength(0);
428
+ });
429
+ });
430
+
431
+ describe('Undo Stack Size Limits', () => {
432
+ beforeEach(() => {
433
+ _resetForTesting();
434
+ });
435
+
436
+ it('should enforce max undo size by dropping oldest entries', () => {
437
+ setMaxUndoSize(3);
438
+ const def = makeDefinition();
439
+ commandRegistry.set('test_cmd', def);
440
+
441
+ const cmds = Array.from({ length: 5 }, (_, i) =>
442
+ makeCommand('test_cmd', { index: i }),
443
+ );
444
+ for (const cmd of cmds) dispatch(cmd);
445
+
446
+ const stack = getUndoStack();
447
+ expect(stack).toHaveLength(3);
448
+ // Oldest two (index 0, 1) should be dropped; remaining are index 2, 3, 4
449
+ expect(stack[0]?.command.params["index"]).toBe(2);
450
+ expect(stack[1]?.command.params["index"]).toBe(3);
451
+ expect(stack[2]?.command.params["index"]).toBe(4);
452
+ });
453
+
454
+ it('should trim existing stack when max size is reduced', () => {
455
+ const def = makeDefinition();
456
+ commandRegistry.set('test_cmd', def);
457
+
458
+ for (let i = 0; i < 10; i++) dispatch(makeCommand('test_cmd', { i }));
459
+ expect(getUndoStack()).toHaveLength(10);
460
+
461
+ setMaxUndoSize(5);
462
+ expect(getUndoStack()).toHaveLength(5);
463
+ // The 5 most recent entries should remain
464
+ expect(getUndoStack()[0]?.command.params["i"]).toBe(5);
465
+ });
466
+
467
+ it('should throw when setting max size to zero or negative', () => {
468
+ expect(() => { setMaxUndoSize(0); }).toThrow('Max undo size must be at least 1');
469
+ expect(() => { setMaxUndoSize(-1); }).toThrow('Max undo size must be at least 1');
470
+ });
471
+
472
+ it('should report the current max size', () => {
473
+ expect(getMaxUndoSize()).toBe(500); // default
474
+ setMaxUndoSize(100);
475
+ expect(getMaxUndoSize()).toBe(100);
476
+ });
477
+
478
+ it('should reset max size on _resetForTesting', () => {
479
+ setMaxUndoSize(10);
480
+ _resetForTesting();
481
+ expect(getMaxUndoSize()).toBe(500);
482
+ });
483
+ });
484
+
485
+ describe('Parent Pointers', () => {
486
+ beforeEach(() => {
487
+ _resetForTesting();
488
+ });
489
+
490
+ it('should set parentId to null for the first entry', () => {
491
+ const def = makeDefinition();
492
+ commandRegistry.set('test_cmd', def);
493
+
494
+ dispatch(makeCommand('test_cmd'));
495
+
496
+ const stack = getUndoStack();
497
+ expect(stack[0]?.parentId).toBeNull();
498
+ });
499
+
500
+ it('should chain parent pointers through a sequence of dispatches', () => {
501
+ const def = makeDefinition();
502
+ commandRegistry.set('test_cmd', def);
503
+
504
+ dispatch(makeCommand('test_cmd'));
505
+ dispatch(makeCommand('test_cmd'));
506
+ dispatch(makeCommand('test_cmd'));
507
+
508
+ const stack = getUndoStack();
509
+ expect(stack[0]?.parentId).toBeNull();
510
+ expect(stack[1]?.parentId).toBe(stack[0]?.id);
511
+ expect(stack[2]?.parentId).toBe(stack[1]?.id);
512
+ });
513
+
514
+ it('should update parent pointer on redo to reflect current stack top', () => {
515
+ const def = makeDefinition();
516
+ commandRegistry.set('test_cmd', def);
517
+
518
+ const cmd1 = makeCommand('test_cmd');
519
+ const cmd2 = makeCommand('test_cmd');
520
+ dispatch(cmd1);
521
+ dispatch(cmd2);
522
+
523
+ // Undo cmd2, then redo it -- its parentId should point to cmd1
524
+ undoLast();
525
+ redoLast();
526
+
527
+ const stack = getUndoStack();
528
+ expect(stack[1]?.parentId).toBe(stack[0]?.id);
529
+ expect(stack[1]?.command).toBe(cmd2);
530
+ });
531
+
532
+ it('should maintain correct parent pointers when oldest entries are dropped', () => {
533
+ setMaxUndoSize(2);
534
+ const def = makeDefinition();
535
+ commandRegistry.set('test_cmd', def);
536
+
537
+ dispatch(makeCommand('test_cmd')); // entry 0
538
+ dispatch(makeCommand('test_cmd')); // entry 1 (parent -> 0)
539
+ dispatch(makeCommand('test_cmd')); // entry 2 (parent -> 1); entry 0 is dropped
540
+
541
+ const stack = getUndoStack();
542
+ expect(stack).toHaveLength(2);
543
+ // The remaining entries should still have their original parent pointers
544
+ // (entry 1's parentId points to a now-dropped entry, which is fine --
545
+ // the pointer is a historical reference for future branching)
546
+ expect(stack[1]?.parentId).toBe(stack[0]?.id);
547
+ });
548
+ });