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,590 @@
1
+ <!--
2
+ MenuBar -- dynamic, registry-driven horizontal menu bar with submenu support.
3
+
4
+ Reads from menuRegistry via buildMenuBar() to construct menus reactively.
5
+ Supports nested submenus, group separators, disabled items, and keyboard
6
+ shortcuts. Retains click-to-open, hover-to-switch, escape-to-close, and
7
+ click-outside-to-close behavior.
8
+
9
+ Keyboard navigation (delegated to a single keydown listener on the menubar):
10
+ - Left/Right on a closed top-level trigger: move focus between top-level
11
+ triggers. If a menu is already open, switch to the next/prev menu and
12
+ keep it open.
13
+ - Down on a focused top-level trigger: open the menu and focus its first
14
+ non-separator, non-disabled item.
15
+ - Down/Up inside a menu: move focus to the next/previous focusable item
16
+ (wraps). Home/End jump to first/last focusable item.
17
+ - Right on a submenu-parent item: open the submenu and focus its first
18
+ item. On a regular item inside a top-level menu, Right moves to the
19
+ next top-level menu (standard menubar behavior).
20
+ - Left inside a submenu: close the submenu and restore focus to its
21
+ parent item.
22
+ - Left on a regular item of a top-level menu (not a submenu parent):
23
+ move to the previous top-level menu.
24
+ - Enter/Space: activate the focused item (same as click).
25
+ - Escape: close the current level; if nested, go up one level; if a
26
+ top-level menu, close it and return focus to the trigger.
27
+ - Character typing: jumps to the next focusable item in the current
28
+ level whose label starts with that letter (case-insensitive).
29
+ - Roving tabindex: exactly one element in the currently-active level
30
+ has tabindex=0; the rest have tabindex=-1. DOM focus is moved via an
31
+ $effect that watches the focusedItemId.
32
+ -->
33
+ <script lang="ts">
34
+ import { onMount, tick } from 'svelte';
35
+ import { buildMenuBar, type MenuItem } from './menu-builder.js';
36
+ import ChevronRight from '~icons/lucide/chevron-right';
37
+
38
+ // --- Reactive menu tree ---
39
+
40
+ let menus = $derived(buildMenuBar());
41
+
42
+ // --- Dropdown state ---
43
+
44
+ let openMenu: string | null = $state(null);
45
+ let openSubmenu: string | null = $state(null);
46
+
47
+ // Roving tabindex: id of the currently focused item at the deepest open
48
+ // level. When no menu is open, this is the label of the focused top-level
49
+ // trigger (or null if nothing in the menubar has focus). When a menu is
50
+ // open with no submenu, it's the item id in the top-level dropdown. When
51
+ // a submenu is open, it's the child item id in the submenu.
52
+ let focusedItemId: string | null = $state(null);
53
+
54
+ let menuBarRef: HTMLElement | undefined = $state();
55
+
56
+ // --- Focusable-item helpers ---
57
+
58
+ /** Items that can receive focus/activation (skips separators and disabled items). */
59
+ function focusableItems(items: MenuItem[]): MenuItem[] {
60
+ return items.filter((i) => !i.separator && !i.disabled);
61
+ }
62
+
63
+ /** Get the focusable list for the current level (submenu > menu). */
64
+ function currentLevelItems(): MenuItem[] {
65
+ if (openMenu === null) return [];
66
+ const menu = menus.find((m) => m.label === openMenu);
67
+ if (!menu) return [];
68
+ if (openSubmenu !== null) {
69
+ const parent = menu.items.find((i) => i.id === openSubmenu);
70
+ if (parent?.children) return focusableItems(parent.children);
71
+ return [];
72
+ }
73
+ return focusableItems(menu.items);
74
+ }
75
+
76
+ // --- Opening / closing helpers ---
77
+
78
+ /** Open the given top-level menu and focus its first focusable item. */
79
+ function openTopMenu(label: string, focusFirst = true): void {
80
+ openMenu = label;
81
+ openSubmenu = null;
82
+ if (focusFirst) {
83
+ const menu = menus.find((m) => m.label === label);
84
+ const first = menu ? focusableItems(menu.items)[0] : undefined;
85
+ focusedItemId = first ? first.id : null;
86
+ }
87
+ }
88
+
89
+ /** Close all menus and return focus to the trigger of the menu that was open. */
90
+ function closeAllMenus(restoreFocusToTrigger = true): void {
91
+ const restoreLabel = openMenu;
92
+ openMenu = null;
93
+ openSubmenu = null;
94
+ if (restoreFocusToTrigger && restoreLabel !== null) {
95
+ focusedItemId = restoreLabel; // trigger row
96
+ } else if (!restoreFocusToTrigger) {
97
+ focusedItemId = null;
98
+ }
99
+ }
100
+
101
+ /** Open a submenu and focus its first child. */
102
+ function openSubmenuAt(parentId: string): void {
103
+ openSubmenu = parentId;
104
+ const menu = menus.find((m) => m.label === openMenu);
105
+ const parent = menu?.items.find((i) => i.id === parentId);
106
+ const first = parent?.children ? focusableItems(parent.children)[0] : undefined;
107
+ focusedItemId = first ? first.id : null;
108
+ }
109
+
110
+ /** Close the open submenu and restore focus to the parent item. */
111
+ function closeSubmenu(): void {
112
+ if (openSubmenu === null) return;
113
+ const parentId = openSubmenu;
114
+ openSubmenu = null;
115
+ focusedItemId = parentId;
116
+ }
117
+
118
+ // --- Mouse handlers (retain prior behavior) ---
119
+
120
+ function toggleMenu(label: string): void {
121
+ if (openMenu === label) {
122
+ closeAllMenus(true);
123
+ } else {
124
+ openTopMenu(label, true);
125
+ }
126
+ }
127
+
128
+ function handleMenuHover(label: string): void {
129
+ // Only switch on hover if a menu is already open
130
+ if (openMenu !== null && openMenu !== label) {
131
+ openTopMenu(label, true);
132
+ }
133
+ }
134
+
135
+ function handleTriggerFocus(label: string): void {
136
+ // When no menu is open, track which trigger has DOM focus so that
137
+ // Down/Enter act on it. Do NOT open the menu on focus alone.
138
+ if (openMenu === null) {
139
+ focusedItemId = label;
140
+ }
141
+ }
142
+
143
+ function activateItem(item: MenuItem): void {
144
+ if (item.separator || item.disabled) return;
145
+ if (item.children) {
146
+ openSubmenuAt(item.id);
147
+ return;
148
+ }
149
+ item.action?.();
150
+ closeAllMenus(false);
151
+ }
152
+
153
+ function handleItemClick(item: MenuItem): void {
154
+ activateItem(item);
155
+ }
156
+
157
+ // --- Character typing (type-ahead) ---
158
+
159
+ /** Find the next focusable item whose label starts with `ch` (case-insensitive). */
160
+ function typeAheadJump(ch: string): void {
161
+ const items = currentLevelItems();
162
+ if (items.length === 0) return;
163
+ const lower = ch.toLowerCase();
164
+ const currentIdx = items.findIndex((i) => i.id === focusedItemId);
165
+ // Search starting AFTER current, wrap around.
166
+ for (let step = 1; step <= items.length; step++) {
167
+ const it = items[(currentIdx + step) % items.length];
168
+ if (it && it.label.toLowerCase().startsWith(lower)) {
169
+ focusedItemId = it.id;
170
+ return;
171
+ }
172
+ }
173
+ }
174
+
175
+ // --- Central keydown handler ---
176
+
177
+ function handleKeydown(e: KeyboardEvent): void {
178
+ // Ignore unless focus is somewhere in the menubar DOM subtree.
179
+ const target = e.target as Node | null;
180
+ if (!menuBarRef || !target || !menuBarRef.contains(target)) return;
181
+
182
+ const key = e.key;
183
+
184
+ // --- Case A: no menu open, focus on a top-level trigger ---
185
+ if (openMenu === null) {
186
+ if (focusedItemId === null) return;
187
+ const idx = menus.findIndex((m) => m.label === focusedItemId);
188
+ if (idx < 0) return;
189
+
190
+ if (key === 'ArrowRight' || key === 'ArrowLeft') {
191
+ const delta = key === 'ArrowRight' ? 1 : -1;
192
+ const next = menus[(idx + delta + menus.length) % menus.length];
193
+ if (next) {
194
+ e.preventDefault();
195
+ focusedItemId = next.label;
196
+ }
197
+ return;
198
+ }
199
+ if (key === 'ArrowDown' || key === 'Enter' || key === ' ') {
200
+ e.preventDefault();
201
+ const m = menus[idx];
202
+ if (m) openTopMenu(m.label, true);
203
+ return;
204
+ }
205
+ if (key === 'Home') {
206
+ e.preventDefault();
207
+ const first = menus[0];
208
+ if (first) focusedItemId = first.label;
209
+ return;
210
+ }
211
+ if (key === 'End') {
212
+ e.preventDefault();
213
+ const last = menus[menus.length - 1];
214
+ if (last) focusedItemId = last.label;
215
+ return;
216
+ }
217
+ return;
218
+ }
219
+
220
+ // --- Case B: a top-level menu is open (possibly with a submenu). ---
221
+
222
+ if (key === 'Escape') {
223
+ e.preventDefault();
224
+ if (openSubmenu !== null) {
225
+ closeSubmenu();
226
+ } else {
227
+ closeAllMenus(true);
228
+ }
229
+ return;
230
+ }
231
+
232
+ const items = currentLevelItems();
233
+
234
+ if (key === 'ArrowDown') {
235
+ e.preventDefault();
236
+ if (items.length === 0) return;
237
+ const i = items.findIndex((it) => it.id === focusedItemId);
238
+ const next = items[(i + 1 + items.length) % items.length];
239
+ if (next) focusedItemId = next.id;
240
+ return;
241
+ }
242
+ if (key === 'ArrowUp') {
243
+ e.preventDefault();
244
+ if (items.length === 0) return;
245
+ const i = items.findIndex((it) => it.id === focusedItemId);
246
+ const prev = items[(i - 1 + items.length) % items.length];
247
+ if (prev) focusedItemId = prev.id;
248
+ return;
249
+ }
250
+ if (key === 'Home') {
251
+ e.preventDefault();
252
+ const first = items[0];
253
+ if (first) focusedItemId = first.id;
254
+ return;
255
+ }
256
+ if (key === 'End') {
257
+ e.preventDefault();
258
+ const last = items[items.length - 1];
259
+ if (last) focusedItemId = last.id;
260
+ return;
261
+ }
262
+ if (key === 'Enter' || key === ' ') {
263
+ e.preventDefault();
264
+ const focused = items.find((it) => it.id === focusedItemId);
265
+ if (focused) activateItem(focused);
266
+ return;
267
+ }
268
+ if (key === 'ArrowRight') {
269
+ e.preventDefault();
270
+ // If current item is a submenu parent, open it.
271
+ const focused = items.find((it) => it.id === focusedItemId);
272
+ if (focused?.children) {
273
+ openSubmenuAt(focused.id);
274
+ return;
275
+ }
276
+ // Otherwise, if inside a submenu, Right does nothing (no deeper level).
277
+ if (openSubmenu !== null) return;
278
+ // Inside a top-level menu, Right cycles to the next top-level menu.
279
+ const idx = menus.findIndex((m) => m.label === openMenu);
280
+ if (idx < 0) return;
281
+ const next = menus[(idx + 1) % menus.length];
282
+ if (next) openTopMenu(next.label, true);
283
+ return;
284
+ }
285
+ if (key === 'ArrowLeft') {
286
+ e.preventDefault();
287
+ // If inside a submenu, Left closes it and returns to parent.
288
+ if (openSubmenu !== null) {
289
+ closeSubmenu();
290
+ return;
291
+ }
292
+ // Inside a top-level menu: cycle to the previous top-level menu.
293
+ const idx = menus.findIndex((m) => m.label === openMenu);
294
+ if (idx < 0) return;
295
+ const prev = menus[(idx - 1 + menus.length) % menus.length];
296
+ if (prev) openTopMenu(prev.label, true);
297
+ return;
298
+ }
299
+
300
+ // Type-ahead: single printable character, no modifiers.
301
+ if (
302
+ key.length === 1 &&
303
+ !e.ctrlKey &&
304
+ !e.metaKey &&
305
+ !e.altKey &&
306
+ /\S/.test(key)
307
+ ) {
308
+ e.preventDefault();
309
+ typeAheadJump(key);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Close menus when the user clicks outside the menu bar.
315
+ * Uses pointerdown in the capture phase so it fires before any
316
+ * stopPropagation() calls in child components (e.g. dockview panels).
317
+ */
318
+ function handleOutsidePointerDown(e: PointerEvent): void {
319
+ if (menuBarRef && !menuBarRef.contains(e.target as Node)) {
320
+ closeAllMenus(false);
321
+ }
322
+ }
323
+
324
+ onMount(() => {
325
+ window.addEventListener('pointerdown', handleOutsidePointerDown, true);
326
+ return () => {
327
+ window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
328
+ };
329
+ });
330
+
331
+ // --- Focus effect: move DOM focus to the currently focused item ---
332
+
333
+ // After every state change that updates focusedItemId, wait for the DOM
334
+ // to update and then focus the matching element. We look up by the
335
+ // data-menu-item-id attribute within the menubar.
336
+ $effect(() => {
337
+ // Re-run when any of these change.
338
+ const id = focusedItemId;
339
+ void openMenu;
340
+ void openSubmenu;
341
+ if (!menuBarRef || id === null) return;
342
+ void tick().then(() => {
343
+ // menuBarRef is a `$state`-backed `bind:this` target; the tick()
344
+ // boundary loses the narrowing and the component may have
345
+ // unmounted between scheduling and execution.
346
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
347
+ if (!menuBarRef) return;
348
+ const el = menuBarRef.querySelector<HTMLElement>(
349
+ `[data-menu-item-id="${CSS.escape(id)}"]`,
350
+ );
351
+ if (el && document.activeElement !== el) {
352
+ el.focus();
353
+ }
354
+ });
355
+ });
356
+ </script>
357
+
358
+ <svelte:window onkeydown={handleKeydown} />
359
+
360
+ <nav class="menubar" bind:this={menuBarRef} role="menubar" aria-label="Application">
361
+ <span class="brand">PixelWeaver</span>
362
+
363
+ {#each menus as menu (menu.label)}
364
+ <div class="menu-trigger-wrapper" role="none">
365
+ <button
366
+ class="menu-trigger"
367
+ class:active={openMenu === menu.label}
368
+ role="menuitem"
369
+ aria-haspopup="menu"
370
+ aria-expanded={openMenu === menu.label}
371
+ data-menu-item-id={menu.label}
372
+ tabindex={focusedItemId === menu.label && openMenu === null ? 0 : -1}
373
+ onclick={() => { toggleMenu(menu.label); }}
374
+ onmouseenter={() => { handleMenuHover(menu.label); }}
375
+ onfocus={() => { handleTriggerFocus(menu.label); }}
376
+ >
377
+ {menu.label}
378
+ </button>
379
+
380
+ {#if openMenu === menu.label}
381
+ <div class="dropdown" role="menu" aria-label={menu.label}>
382
+ {#each menu.items as item (item.id)}
383
+ {#if item.separator}
384
+ <div class="separator" role="separator"></div>
385
+ {:else if item.children}
386
+ <!-- Submenu trigger -->
387
+ <div
388
+ class="dropdown-item submenu-trigger"
389
+ class:focused={focusedItemId === item.id && openSubmenu !== item.id}
390
+ role="menuitem"
391
+ tabindex={focusedItemId === item.id && openSubmenu === null ? 0 : -1}
392
+ data-menu-item-id={item.id}
393
+ aria-haspopup="menu"
394
+ aria-expanded={openSubmenu === item.id}
395
+ onmouseenter={() => { openSubmenu = item.id; focusedItemId = item.id; }}
396
+ onmouseleave={() => { if (openSubmenu === item.id) openSubmenu = null; }}
397
+ >
398
+ <span class="item-label">{item.label}</span>
399
+ <span class="submenu-arrow"><ChevronRight /></span>
400
+ {#if openSubmenu === item.id}
401
+ <div class="dropdown submenu" role="menu" aria-label={item.label}>
402
+ {#each item.children as child (child.id)}
403
+ {#if child.separator}
404
+ <div class="separator" role="separator"></div>
405
+ {:else}
406
+ <button
407
+ class="dropdown-item"
408
+ class:disabled={child.disabled ?? false}
409
+ class:focused={focusedItemId === child.id}
410
+ role="menuitem"
411
+ data-menu-item-id={child.id}
412
+ tabindex={focusedItemId === child.id ? 0 : -1}
413
+ onclick={() => { handleItemClick(child); }}
414
+ disabled={child.disabled ?? false}
415
+ >
416
+ <span class="item-label">{child.label}</span>
417
+ {#if child.shortcut}
418
+ <span class="item-shortcut">{child.shortcut}</span>
419
+ {/if}
420
+ </button>
421
+ {/if}
422
+ {/each}
423
+ </div>
424
+ {/if}
425
+ </div>
426
+ {:else}
427
+ <button
428
+ class="dropdown-item"
429
+ class:disabled={item.disabled ?? false}
430
+ class:focused={focusedItemId === item.id}
431
+ role="menuitem"
432
+ data-menu-item-id={item.id}
433
+ tabindex={focusedItemId === item.id ? 0 : -1}
434
+ onclick={() => { handleItemClick(item); }}
435
+ disabled={item.disabled ?? false}
436
+ >
437
+ <span class="item-label">{item.label}</span>
438
+ {#if item.shortcut}
439
+ <span class="item-shortcut">{item.shortcut}</span>
440
+ {/if}
441
+ </button>
442
+ {/if}
443
+ {/each}
444
+ </div>
445
+ {/if}
446
+ </div>
447
+ {/each}
448
+ </nav>
449
+
450
+ <style>
451
+ .menubar {
452
+ height: 36px;
453
+ background: var(--bg-toolbar);
454
+ border-bottom: 1px solid var(--border);
455
+ display: flex;
456
+ align-items: center;
457
+ padding: 0 12px;
458
+ flex-shrink: 0;
459
+ gap: 2px;
460
+ user-select: none;
461
+ }
462
+
463
+ .brand {
464
+ font-size: var(--text-xl);
465
+ font-weight: 600;
466
+ color: var(--accent);
467
+ letter-spacing: 0.5px;
468
+ margin-right: 12px;
469
+ }
470
+
471
+ .menu-trigger-wrapper {
472
+ position: relative;
473
+ }
474
+
475
+ .menu-trigger {
476
+ height: 26px;
477
+ padding: 0 10px;
478
+ background: none;
479
+ border: none;
480
+ border-radius: var(--radius-sm);
481
+ color: var(--text-primary);
482
+ font-size: var(--text-lg);
483
+ cursor: pointer;
484
+ display: flex;
485
+ align-items: center;
486
+ }
487
+
488
+ .menu-trigger:hover {
489
+ background: var(--bg-secondary);
490
+ }
491
+
492
+ .menu-trigger.active {
493
+ background: var(--bg-secondary);
494
+ }
495
+
496
+ .menu-trigger:focus-visible {
497
+ outline: 2px solid var(--accent);
498
+ outline-offset: -2px;
499
+ }
500
+
501
+ /* --- Dropdown --- */
502
+
503
+ .dropdown {
504
+ position: absolute;
505
+ top: 100%;
506
+ left: 0;
507
+ min-width: 160px;
508
+ background: var(--bg-panel);
509
+ border: 1px solid var(--border);
510
+ border-radius: var(--radius-md);
511
+ box-shadow: var(--shadow-md);
512
+ padding: 4px 0;
513
+ z-index: 1000;
514
+ }
515
+
516
+ .dropdown-item {
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: space-between;
520
+ width: 100%;
521
+ padding: 5px 12px;
522
+ background: none;
523
+ border: none;
524
+ color: var(--text-primary);
525
+ font-size: var(--text-lg);
526
+ cursor: pointer;
527
+ text-align: left;
528
+ gap: 24px;
529
+ white-space: nowrap;
530
+ }
531
+
532
+ .dropdown-item:hover:not(:disabled) {
533
+ background: var(--bg-secondary);
534
+ }
535
+
536
+ /* Keyboard-focused item highlight (matches hover). */
537
+ .dropdown-item.focused:not(:disabled) {
538
+ background: var(--bg-secondary);
539
+ }
540
+
541
+ .dropdown-item:focus {
542
+ outline: none;
543
+ }
544
+
545
+ .dropdown-item:focus-visible {
546
+ outline: 2px solid var(--accent);
547
+ outline-offset: -2px;
548
+ }
549
+
550
+ .dropdown-item:disabled,
551
+ .dropdown-item.disabled {
552
+ color: var(--text-secondary);
553
+ opacity: 0.5;
554
+ cursor: default;
555
+ }
556
+
557
+ .item-shortcut {
558
+ color: var(--text-secondary);
559
+ font-size: var(--text-sm);
560
+ }
561
+
562
+ .separator {
563
+ height: 1px;
564
+ background: var(--border);
565
+ margin: 4px 0;
566
+ }
567
+
568
+ /* --- Submenu --- */
569
+
570
+ .submenu-trigger {
571
+ position: relative;
572
+ }
573
+
574
+ .submenu-arrow {
575
+ margin-left: auto;
576
+ display: flex;
577
+ align-items: center;
578
+ }
579
+
580
+ .submenu-arrow :global(svg) {
581
+ width: 12px;
582
+ height: 12px;
583
+ }
584
+
585
+ .submenu {
586
+ position: absolute;
587
+ left: 100%;
588
+ top: -4px;
589
+ }
590
+ </style>