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,295 @@
1
+ <script lang="ts">
2
+ /**
3
+ * RecoveryDialog -- modal shown on startup when crash recovery data
4
+ * is available in IndexedDB.
5
+ *
6
+ * Reads recoveryState reactively. When `.available` is true, shows a
7
+ * dialog with the recovery timestamp and Restore / Discard buttons.
8
+ * "Restore" loads the snapshot and applies it to canvas, layer, and
9
+ * frame state. "Discard" clears the IDB data. Both close the dialog.
10
+ */
11
+
12
+ import { recoveryState } from '../recovery/recovery-state.svelte.js';
13
+ import {
14
+ loadSnapshot,
15
+ clearSnapshot,
16
+ } from '../recovery/idb-store.js';
17
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
18
+ import * as layerTree from '../layers/layer-tree.svelte.js';
19
+ import * as frameModel from '../animation/frame-model.svelte.js';
20
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
21
+ import { SvelteMap } from 'svelte/reactivity';
22
+
23
+ // --- Local state ---
24
+
25
+ let dialogEl: HTMLDialogElement | undefined = $state();
26
+ let restoring = $state(false);
27
+ let errorMessage = $state('');
28
+
29
+ let open = $derived(recoveryState.available);
30
+
31
+ /** Format the recovery timestamp for display. */
32
+ let formattedTime = $derived.by(() => {
33
+ const ts = recoveryState.timestamp;
34
+ if (!ts) return 'Unknown';
35
+ const d = new Date(ts);
36
+ return d.toLocaleString();
37
+ });
38
+
39
+ /** Close the dialog by clearing the recovery-available flag. */
40
+ function close() {
41
+ recoveryState.setRecoveryAvailable(false);
42
+ }
43
+
44
+ /** Restore the snapshot: load from IDB, apply to app state, then close. */
45
+ async function handleRestore() {
46
+ if (restoring) return;
47
+ restoring = true;
48
+ errorMessage = '';
49
+
50
+ try {
51
+ const snapshot = await loadSnapshot();
52
+ if (!snapshot) {
53
+ throw new Error('Recovery data could not be loaded.');
54
+ }
55
+
56
+ // Apply the first (and currently only) canvas state from the snapshot.
57
+ const canvasId = Object.keys(snapshot.canvasStates)[0];
58
+ if (!canvasId) {
59
+ throw new Error('Recovery snapshot contains no canvas data.');
60
+ }
61
+
62
+ const saved = snapshot.canvasStates[canvasId];
63
+ if (!saved) throw new Error(`Recovery snapshot missing canvas "${canvasId}".`);
64
+
65
+ // 1. Restore canvas dimensions
66
+ canvasState.canvasWidth = saved.width;
67
+ canvasState.canvasHeight = saved.height;
68
+
69
+ // 2. Restore layer tree
70
+ layerTree.deserialize(saved.layerTree as Parameters<typeof layerTree.deserialize>[0]);
71
+
72
+ // 3. Rebuild frames with PixelBuffer instances from raw ArrayBuffers.
73
+ // The snapshot stores one frame's worth of pixelData as
74
+ // Record<string, ArrayBuffer> (raw RGBA bytes). We wrap each in a
75
+ // PixelBuffer and construct a single frame.
76
+ const pixelData = new SvelteMap<string, PixelBuffer>();
77
+ for (const [layerId, rawBuffer] of Object.entries(saved.pixelData)) {
78
+ const bytes = new Uint8ClampedArray(rawBuffer);
79
+ pixelData.set(layerId, new PixelBuffer(saved.width, saved.height, bytes));
80
+ }
81
+
82
+ // Deserialize to empty state, then add a frame with the recovered data
83
+ frameModel.deserialize({
84
+ frames: [],
85
+ currentFrameIndex: 0,
86
+ globalFps: 12,
87
+ originX: 0,
88
+ originY: 0,
89
+ });
90
+
91
+ const frame = frameModel.addFrame();
92
+ for (const [layerId, buffer] of pixelData) {
93
+ frame.pixelData.set(layerId, buffer);
94
+ }
95
+
96
+ // 4. Clear the IDB data now that it has been restored
97
+ await clearSnapshot();
98
+ close();
99
+ } catch (err) {
100
+ errorMessage = err instanceof Error ? err.message : 'Restore failed.';
101
+ } finally {
102
+ restoring = false;
103
+ }
104
+ }
105
+
106
+ /** Discard the recovery data and close the dialog. */
107
+ async function handleDiscard() {
108
+ try {
109
+ await clearSnapshot();
110
+ } catch (err) {
111
+ errorMessage = err instanceof Error ? err.message : 'Failed to discard recovery data.';
112
+ return;
113
+ }
114
+ close();
115
+ }
116
+
117
+ // Sync native <dialog> open state with the derived `open` flag.
118
+ $effect(() => {
119
+ if (!dialogEl) return;
120
+ if (open && !dialogEl.open) {
121
+ dialogEl.showModal();
122
+ } else if (!open && dialogEl.open) {
123
+ dialogEl.close();
124
+ }
125
+ });
126
+
127
+ // Close on backdrop click without destroying recovery data -- the user
128
+ // may have clicked accidentally. Data stays in IDB for next launch.
129
+ function handleDialogClick(e: MouseEvent) {
130
+ if (e.target === dialogEl) {
131
+ close();
132
+ }
133
+ }
134
+ </script>
135
+
136
+ <dialog
137
+ bind:this={dialogEl}
138
+ class="pw-dialog"
139
+ onclick={handleDialogClick}
140
+ onclose={close}
141
+ >
142
+ {#if open}
143
+ <div class="modal">
144
+ <h2 class="modal-title">Recover Unsaved Work</h2>
145
+
146
+ <p class="modal-message">
147
+ Recovery data was found from a previous session. Would you like to
148
+ restore it?
149
+ </p>
150
+
151
+ <div class="timestamp-row">
152
+ <span class="timestamp-label">Saved at</span>
153
+ <span class="timestamp-value">{formattedTime}</span>
154
+ </div>
155
+
156
+ {#if errorMessage}
157
+ <div class="error-msg">{errorMessage}</div>
158
+ {/if}
159
+
160
+ <div class="modal-actions">
161
+ <button class="btn btn--cancel" onclick={handleDiscard}>Discard</button>
162
+ <button
163
+ class="btn btn--primary"
164
+ onclick={handleRestore}
165
+ disabled={restoring}
166
+ >
167
+ {restoring ? 'Restoring...' : 'Restore'}
168
+ </button>
169
+ </div>
170
+ </div>
171
+ {/if}
172
+ </dialog>
173
+
174
+ <style>
175
+ .pw-dialog {
176
+ margin: 0;
177
+ padding: 0;
178
+ border: none;
179
+ background: transparent;
180
+ max-width: none;
181
+ max-height: none;
182
+ width: 100vw;
183
+ height: 100vh;
184
+ color: inherit;
185
+ }
186
+
187
+ .pw-dialog[open] {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ }
192
+
193
+ .pw-dialog::backdrop {
194
+ background: rgba(0, 0, 0, 0.5);
195
+ }
196
+
197
+ .modal {
198
+ background: var(--bg-panel);
199
+ border: 1px solid var(--border);
200
+ border-radius: 6px;
201
+ box-shadow: var(--shadow-md);
202
+ width: 420px;
203
+ max-width: 90vw;
204
+ padding: 24px;
205
+ color: var(--text-primary);
206
+ }
207
+
208
+ .modal-title {
209
+ font-size: 18px;
210
+ font-weight: 600;
211
+ margin-bottom: 12px;
212
+ color: var(--text-primary);
213
+ }
214
+
215
+ .modal-message {
216
+ font-size: var(--text-lg);
217
+ color: var(--text-secondary);
218
+ line-height: 1.5;
219
+ margin-bottom: 16px;
220
+ }
221
+
222
+ .timestamp-row {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 8px;
226
+ margin-bottom: 16px;
227
+ padding: 10px 12px;
228
+ background: var(--bg-toolbar);
229
+ border: 1px solid var(--border);
230
+ border-radius: var(--radius-md);
231
+ }
232
+
233
+ .timestamp-label {
234
+ font-size: var(--text-base);
235
+ color: var(--text-muted);
236
+ flex-shrink: 0;
237
+ }
238
+
239
+ .timestamp-value {
240
+ font-size: var(--text-lg);
241
+ color: var(--text-primary);
242
+ font-weight: 500;
243
+ }
244
+
245
+ .error-msg {
246
+ background: rgba(255, 74, 106, 0.1);
247
+ border: 1px solid var(--error);
248
+ border-radius: var(--radius-md);
249
+ color: var(--error);
250
+ font-size: var(--text-base);
251
+ padding: 8px 10px;
252
+ margin-bottom: 12px;
253
+ }
254
+
255
+ .modal-actions {
256
+ display: flex;
257
+ justify-content: flex-end;
258
+ gap: 8px;
259
+ margin-top: 20px;
260
+ }
261
+
262
+ .btn {
263
+ border: none;
264
+ border-radius: var(--radius-md);
265
+ font-size: var(--text-lg);
266
+ padding: 8px 20px;
267
+ cursor: pointer;
268
+ font-weight: 500;
269
+ }
270
+
271
+ .btn:disabled {
272
+ opacity: 0.5;
273
+ cursor: default;
274
+ }
275
+
276
+ .btn--cancel {
277
+ background: var(--bg-toolbar);
278
+ color: var(--text-secondary);
279
+ border: 1px solid var(--border);
280
+ }
281
+
282
+ .btn--cancel:hover {
283
+ background: var(--bg-primary);
284
+ color: var(--text-primary);
285
+ }
286
+
287
+ .btn--primary {
288
+ background: var(--accent);
289
+ color: #ffffff;
290
+ }
291
+
292
+ .btn--primary:hover:not(:disabled) {
293
+ background: var(--accent-hover);
294
+ }
295
+ </style>
@@ -0,0 +1,416 @@
1
+ <script lang="ts">
2
+ /**
3
+ * ResizeDialog -- modal for resizing the canvas with anchor placement.
4
+ *
5
+ * Allows the user to set new width/height and choose where the old
6
+ * content is anchored within the new canvas (3x3 grid). All pixel
7
+ * buffers across all frames and layers are resized accordingly.
8
+ */
9
+
10
+ import { canvasState, MAX_CANVAS_SIZE, MIN_CANVAS_SIZE } from '../canvas/canvas-state.svelte.js';
11
+ import { PixelBuffer } from '../canvas/pixel-buffer.js';
12
+ import * as layerTree from '../layers/layer-tree.svelte.js';
13
+ import * as frameModel from '../animation/frame-model.svelte.js';
14
+
15
+ // --- Props ---
16
+
17
+ interface Props {
18
+ open: boolean;
19
+ onClose: () => void;
20
+ }
21
+
22
+ let { open, onClose }: Props = $props();
23
+
24
+ // --- Anchor type ---
25
+
26
+ type Anchor =
27
+ | 'top-left' | 'top-center' | 'top-right'
28
+ | 'middle-left' | 'center' | 'middle-right'
29
+ | 'bottom-left' | 'bottom-center' | 'bottom-right';
30
+
31
+ // --- Local state ---
32
+
33
+ let dialogEl: HTMLDialogElement | undefined = $state();
34
+
35
+ let width = $state(32);
36
+ let height = $state(32);
37
+ let anchor = $state<Anchor>('center');
38
+
39
+ // --- Anchor grid definition ---
40
+
41
+ const anchorRows: Anchor[][] = [
42
+ ['top-left', 'top-center', 'top-right'],
43
+ ['middle-left', 'center', 'middle-right'],
44
+ ['bottom-left', 'bottom-center', 'bottom-right'],
45
+ ];
46
+
47
+ // --- Clamping helpers ---
48
+
49
+ function clampWidth(e: Event) {
50
+ const val = parseInt((e.target as HTMLInputElement).value, 10);
51
+ width = isNaN(val)
52
+ ? canvasState.canvasWidth
53
+ : Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, val));
54
+ }
55
+
56
+ function clampHeight(e: Event) {
57
+ const val = parseInt((e.target as HTMLInputElement).value, 10);
58
+ height = isNaN(val)
59
+ ? canvasState.canvasHeight
60
+ : Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, val));
61
+ }
62
+
63
+ // --- Anchor offset calculation ---
64
+
65
+ /**
66
+ * Compute the (x, y) offset where the old content should be placed
67
+ * in the new canvas, based on the anchor selection.
68
+ */
69
+ function getAnchorOffset(
70
+ oldW: number, oldH: number, newW: number, newH: number, a: Anchor,
71
+ ): { x: number; y: number } {
72
+ let x = 0;
73
+ let y = 0;
74
+
75
+ // Horizontal placement
76
+ if (a.includes('left')) {
77
+ x = 0;
78
+ } else if (a.includes('right')) {
79
+ x = newW - oldW;
80
+ } else {
81
+ // center
82
+ x = Math.floor((newW - oldW) / 2);
83
+ }
84
+
85
+ // Vertical placement
86
+ if (a.startsWith('top')) {
87
+ y = 0;
88
+ } else if (a.startsWith('bottom')) {
89
+ y = newH - oldH;
90
+ } else {
91
+ // middle / center
92
+ y = Math.floor((newH - oldH) / 2);
93
+ }
94
+
95
+ return { x, y };
96
+ }
97
+
98
+ // --- Resize logic ---
99
+
100
+ function handleResize() {
101
+ const oldW = canvasState.canvasWidth;
102
+ const oldH = canvasState.canvasHeight;
103
+ const newW = width;
104
+ const newH = height;
105
+
106
+ // Nothing to do if dimensions unchanged
107
+ if (newW === oldW && newH === oldH) {
108
+ onClose();
109
+ return;
110
+ }
111
+
112
+ const offset = getAnchorOffset(oldW, oldH, newW, newH, anchor);
113
+
114
+ // Resize all pixel buffers across all frames and layers
115
+ const allFrames = frameModel.getFrames();
116
+ const allPixelLayers = layerTree.getAllPixelLayers();
117
+
118
+ for (const frame of allFrames) {
119
+ for (const layer of allPixelLayers) {
120
+ const oldBuffer = frame.pixelData.get(layer.id);
121
+ if (!oldBuffer) continue;
122
+
123
+ const newBuffer = new PixelBuffer(newW, newH);
124
+
125
+ // Copy old pixels into the new buffer at the anchor offset.
126
+ // We iterate over old buffer pixels and place them at the
127
+ // offset position, clipping to new buffer bounds.
128
+ for (let sy = 0; sy < oldBuffer.height; sy++) {
129
+ for (let sx = 0; sx < oldBuffer.width; sx++) {
130
+ const dx = sx + offset.x;
131
+ const dy = sy + offset.y;
132
+ if (dx < 0 || dx >= newW || dy < 0 || dy >= newH) continue;
133
+ const [r, g, b, a] = oldBuffer.getPixel(sx, sy);
134
+ newBuffer.setPixel(dx, dy, r, g, b, a);
135
+ }
136
+ }
137
+
138
+ frame.pixelData.set(layer.id, newBuffer);
139
+ }
140
+ }
141
+
142
+ // Update canvas dimensions
143
+ canvasState.canvasWidth = newW;
144
+ canvasState.canvasHeight = newH;
145
+
146
+ onClose();
147
+ }
148
+
149
+ // Reset form values when dialog opens
150
+ $effect(() => {
151
+ if (open) {
152
+ width = canvasState.canvasWidth;
153
+ height = canvasState.canvasHeight;
154
+ anchor = 'center';
155
+ }
156
+ });
157
+
158
+ // Sync native <dialog> open state with the `open` prop.
159
+ $effect(() => {
160
+ if (!dialogEl) return;
161
+ if (open && !dialogEl.open) {
162
+ dialogEl.showModal();
163
+ } else if (!open && dialogEl.open) {
164
+ dialogEl.close();
165
+ }
166
+ });
167
+
168
+ // Close on backdrop click
169
+ function handleDialogClick(e: MouseEvent) {
170
+ if (e.target === dialogEl) {
171
+ onClose();
172
+ }
173
+ }
174
+ </script>
175
+
176
+ <dialog
177
+ bind:this={dialogEl}
178
+ class="pw-dialog"
179
+ onclick={handleDialogClick}
180
+ onclose={onClose}
181
+ >
182
+ {#if open}
183
+ <div class="modal">
184
+ <h2 class="modal-title">Resize Canvas</h2>
185
+
186
+ <!-- Dimensions -->
187
+ <div class="form-row">
188
+ <label class="form-label" for="rc-width">Width</label>
189
+ <input
190
+ id="rc-width"
191
+ class="form-input"
192
+ type="number"
193
+ min={MIN_CANVAS_SIZE}
194
+ max={MAX_CANVAS_SIZE}
195
+ step="1"
196
+ bind:value={width}
197
+ onchange={clampWidth}
198
+ />
199
+ <span class="form-unit">px</span>
200
+ </div>
201
+
202
+ <div class="form-row">
203
+ <label class="form-label" for="rc-height">Height</label>
204
+ <input
205
+ id="rc-height"
206
+ class="form-input"
207
+ type="number"
208
+ min={MIN_CANVAS_SIZE}
209
+ max={MAX_CANVAS_SIZE}
210
+ step="1"
211
+ bind:value={height}
212
+ onchange={clampHeight}
213
+ />
214
+ <span class="form-unit">px</span>
215
+ </div>
216
+
217
+ <!-- Anchor selector -->
218
+ <div class="form-section">
219
+ <span class="form-label">Anchor</span>
220
+ <div class="anchor-grid">
221
+ {#each anchorRows as row, rowIdx (rowIdx)}
222
+ {#each row as cell (cell)}
223
+ <button
224
+ class="anchor-btn"
225
+ class:anchor-btn--active={anchor === cell}
226
+ title={cell}
227
+ aria-label={cell}
228
+ aria-pressed={anchor === cell}
229
+ onclick={() => anchor = cell}
230
+ >
231
+ <span class="anchor-dot"></span>
232
+ </button>
233
+ {/each}
234
+ {/each}
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Actions -->
239
+ <div class="modal-actions">
240
+ <button class="btn btn--cancel" onclick={onClose}>Cancel</button>
241
+ <button class="btn btn--primary" onclick={handleResize}>Resize</button>
242
+ </div>
243
+ </div>
244
+ {/if}
245
+ </dialog>
246
+
247
+ <style>
248
+ /* Reuse the same dialog reset pattern as NewProjectDialog */
249
+ .pw-dialog {
250
+ margin: 0;
251
+ padding: 0;
252
+ border: none;
253
+ background: transparent;
254
+ max-width: none;
255
+ max-height: none;
256
+ width: 100vw;
257
+ height: 100vh;
258
+ color: inherit;
259
+ }
260
+
261
+ .pw-dialog[open] {
262
+ display: flex;
263
+ align-items: center;
264
+ justify-content: center;
265
+ }
266
+
267
+ .pw-dialog::backdrop {
268
+ background: rgba(0, 0, 0, 0.5);
269
+ }
270
+
271
+ .modal {
272
+ background: var(--bg-panel);
273
+ border: 1px solid var(--border);
274
+ border-radius: 6px;
275
+ box-shadow: var(--shadow-md);
276
+ width: 420px;
277
+ max-width: 90vw;
278
+ padding: 24px;
279
+ color: var(--text-primary);
280
+ }
281
+
282
+ .modal-title {
283
+ font-size: 18px;
284
+ font-weight: 600;
285
+ margin-bottom: 20px;
286
+ color: var(--text-primary);
287
+ }
288
+
289
+ /* Form layout */
290
+ .form-row {
291
+ display: flex;
292
+ align-items: center;
293
+ gap: 8px;
294
+ margin-bottom: 12px;
295
+ }
296
+
297
+ .form-section {
298
+ margin-bottom: 16px;
299
+ }
300
+
301
+ .form-label {
302
+ width: 80px;
303
+ flex-shrink: 0;
304
+ font-size: var(--text-lg);
305
+ color: var(--text-secondary);
306
+ }
307
+
308
+ .form-input {
309
+ flex: 1;
310
+ background: var(--bg-toolbar);
311
+ border: 1px solid var(--border);
312
+ border-radius: var(--radius-md);
313
+ color: var(--text-primary);
314
+ font-size: var(--text-lg);
315
+ padding: 6px 8px;
316
+ max-width: 120px;
317
+ }
318
+
319
+ .form-input:focus-visible {
320
+ border-color: var(--accent);
321
+ }
322
+
323
+ .form-unit {
324
+ font-size: var(--text-base);
325
+ color: var(--text-muted);
326
+ }
327
+
328
+ /* 3x3 anchor grid */
329
+ .anchor-grid {
330
+ display: grid;
331
+ grid-template-columns: repeat(3, 28px);
332
+ grid-template-rows: repeat(3, 28px);
333
+ gap: 4px;
334
+ margin-top: 6px;
335
+ }
336
+
337
+ .anchor-btn {
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ width: 28px;
342
+ height: 28px;
343
+ border: 1px solid var(--border);
344
+ border-radius: var(--radius-md);
345
+ background: var(--bg-toolbar);
346
+ cursor: pointer;
347
+ padding: 0;
348
+ transition: background var(--transition-fast), border-color var(--transition-fast);
349
+ }
350
+
351
+ .anchor-btn:hover {
352
+ background: var(--bg-primary);
353
+ border-color: var(--accent);
354
+ }
355
+
356
+ .anchor-btn--active {
357
+ background: var(--accent);
358
+ border-color: var(--accent);
359
+ }
360
+
361
+ .anchor-dot {
362
+ width: 8px;
363
+ height: 8px;
364
+ border-radius: 50%;
365
+ background: var(--text-muted);
366
+ }
367
+
368
+ .anchor-btn--active .anchor-dot {
369
+ background: #ffffff;
370
+ }
371
+
372
+ .anchor-btn:hover .anchor-dot {
373
+ background: var(--text-primary);
374
+ }
375
+
376
+ .anchor-btn--active:hover .anchor-dot {
377
+ background: #ffffff;
378
+ }
379
+
380
+ /* Action buttons */
381
+ .modal-actions {
382
+ display: flex;
383
+ justify-content: flex-end;
384
+ gap: 8px;
385
+ margin-top: 20px;
386
+ }
387
+
388
+ .btn {
389
+ border: none;
390
+ border-radius: var(--radius-md);
391
+ font-size: var(--text-lg);
392
+ padding: 8px 20px;
393
+ cursor: pointer;
394
+ font-weight: 500;
395
+ }
396
+
397
+ .btn--cancel {
398
+ background: var(--bg-toolbar);
399
+ color: var(--text-secondary);
400
+ border: 1px solid var(--border);
401
+ }
402
+
403
+ .btn--cancel:hover {
404
+ background: var(--bg-primary);
405
+ color: var(--text-primary);
406
+ }
407
+
408
+ .btn--primary {
409
+ background: var(--accent);
410
+ color: #ffffff;
411
+ }
412
+
413
+ .btn--primary:hover {
414
+ background: var(--accent-hover);
415
+ }
416
+ </style>