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,803 @@
1
+ <script lang="ts">
2
+ /**
3
+ * NewProjectDialog -- modal for creating a new project.
4
+ *
5
+ * Layout: Templates grid -> Recent Projects (if any) -> Custom size inputs.
6
+ * Resets canvas dimensions, clears all layers and frames, and
7
+ * optionally fills the initial buffer with a background color.
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
+ import { hasUnsavedChanges, clearHistory } from '../core/dispatcher.js';
15
+ import { actionLog } from '../history/action-log.svelte.js';
16
+ import { saveState } from '../save/save-state.svelte.js';
17
+ import { getRecentProjects, type RecentProject } from '../save/recent-projects.js';
18
+ import { openProject } from '../save/storage.js';
19
+ import { applySnapshot } from '../save/project-snapshot.js';
20
+ import { addRecentProject } from '../save/recent-projects.js';
21
+ import { composite } from '../layers/compositor.js';
22
+ import { getLayers } from '../layers/layer-tree.svelte.js';
23
+ import { getCurrentFrame } from '../animation/frame-model.svelte.js';
24
+ import { setRenderBuffer } from '../canvas/render-state.svelte.js';
25
+
26
+ // --- Props ---
27
+
28
+ interface Props {
29
+ open: boolean;
30
+ onClose: () => void;
31
+ }
32
+
33
+ let { open, onClose }: Props = $props();
34
+
35
+ // --- Local state ---
36
+
37
+ let dialogEl: HTMLDialogElement | undefined = $state();
38
+
39
+ let width = $state(32);
40
+ let height = $state(32);
41
+ // "transparent" | "white" | "black"
42
+ let bgColor = $state<'transparent' | 'white' | 'black'>('transparent');
43
+
44
+ // Whether the unsaved-changes warning is shown instead of the main form
45
+ let showWarning = $state(false);
46
+ // When set, the warning will open the recent project instead of creating new
47
+ let pendingRecentProject = $state<RecentProject | null>(null);
48
+
49
+ let recentProjects = $state<RecentProject[]>([]);
50
+
51
+ // --- Templates ---
52
+
53
+ interface Template {
54
+ label: string;
55
+ w: number;
56
+ h: number;
57
+ bg: 'transparent' | 'white' | 'black';
58
+ /** Layer names to create (bottom to top). Omit for default single layer. */
59
+ layers?: string[];
60
+ /** Number of frames to create. Defaults to 1. */
61
+ frames?: number;
62
+ }
63
+
64
+ const templates: Template[] = [
65
+ { label: 'Character Sprite', w: 16, h: 32, bg: 'transparent', layers: ['fill', 'outline'], frames: 4 },
66
+ { label: 'Platformer Tile', w: 16, h: 16, bg: 'transparent', layers: ['background', 'foreground'] },
67
+ { label: 'Icon', w: 32, h: 32, bg: 'transparent', layers: ['base'] },
68
+ { label: 'Isometric Tile', w: 64, h: 32, bg: 'transparent', layers: ['base', 'detail'] },
69
+ { label: 'HD Sprite', w: 64, h: 64, bg: 'transparent', layers: ['fill', 'outline'], frames: 2 },
70
+ { label: 'Landscape', w: 128, h: 64, bg: 'transparent', layers: ['sky', 'terrain', 'detail'] },
71
+ ];
72
+
73
+ // Track which template is currently selected (null = custom/preset)
74
+ let selectedTemplate = $state<Template | null>(null);
75
+
76
+ function applyTemplate(t: Template) {
77
+ width = t.w;
78
+ height = t.h;
79
+ bgColor = t.bg;
80
+ selectedTemplate = t;
81
+ }
82
+
83
+ // --- Size presets ---
84
+
85
+ const presets = [
86
+ { label: '8x8', w: 8, h: 8 },
87
+ { label: '16x16', w: 16, h: 16 },
88
+ { label: '32x32', w: 32, h: 32 },
89
+ { label: '64x64', w: 64, h: 64 },
90
+ { label: '128x128', w: 128, h: 128 },
91
+ { label: '256x256', w: 256, h: 256 },
92
+ ];
93
+
94
+ function applyPreset(w: number, h: number) {
95
+ width = w;
96
+ height = h;
97
+ selectedTemplate = null;
98
+ }
99
+
100
+ // --- Clamping helpers ---
101
+
102
+ function clampWidth(e: Event) {
103
+ const val = parseInt((e.target as HTMLInputElement).value, 10);
104
+ width = isNaN(val) ? 32 : Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, val));
105
+ selectedTemplate = null;
106
+ }
107
+
108
+ function clampHeight(e: Event) {
109
+ const val = parseInt((e.target as HTMLInputElement).value, 10);
110
+ height = isNaN(val) ? 32 : Math.max(MIN_CANVAS_SIZE, Math.min(MAX_CANVAS_SIZE, val));
111
+ selectedTemplate = null;
112
+ }
113
+
114
+ // --- Create project ---
115
+
116
+ /**
117
+ * Gate creation behind an unsaved-changes check.
118
+ * If the undo stack has entries the project was modified; show warning first.
119
+ */
120
+ function handleCreateClick() {
121
+ if (hasUnsavedChanges()) {
122
+ pendingRecentProject = null;
123
+ showWarning = true;
124
+ return;
125
+ }
126
+ handleCreate();
127
+ }
128
+
129
+ function handleCreate() {
130
+ const tpl = selectedTemplate;
131
+ const layerNames = tpl?.layers ?? ['Layer 1'];
132
+ const frameCount = tpl?.frames ?? 1;
133
+
134
+ // 1. Set canvas dimensions
135
+ canvasState.canvasWidth = width;
136
+ canvasState.canvasHeight = height;
137
+
138
+ // 2. Reset layers to empty, then create template layers (bottom to top)
139
+ layerTree.deserialize({ layers: [], activeLayerId: '' });
140
+ const createdLayers = layerNames.map((name) => layerTree.addLayer(name));
141
+
142
+ // 3. Reset frames to empty, then create the required number of frames
143
+ // with PixelBuffers for each layer
144
+ frameModel.deserialize({
145
+ frames: [],
146
+ currentFrameIndex: 0,
147
+ globalFps: 12,
148
+ originX: 0,
149
+ originY: 0,
150
+ });
151
+
152
+ for (let i = 0; i < frameCount; i++) {
153
+ const frame = frameModel.addFrame();
154
+ for (const layer of createdLayers) {
155
+ const buffer = new PixelBuffer(width, height);
156
+
157
+ // Fill background on the bottom-most layer only
158
+ if (layer === createdLayers[0]) {
159
+ if (bgColor === 'white') {
160
+ buffer.fill(255, 255, 255, 255);
161
+ } else if (bgColor === 'black') {
162
+ buffer.fill(0, 0, 0, 255);
163
+ }
164
+ // transparent: buffer is already zeroed
165
+ }
166
+
167
+ frame.pixelData.set(layer.id, buffer);
168
+ }
169
+ }
170
+
171
+ // 4. Clear undo/redo history and action log so stale commands from
172
+ // the previous project don't trigger false unsaved-changes warnings
173
+ // or corrupt state via undo replay.
174
+ clearHistory();
175
+ actionLog.clearAll();
176
+ saveState.reset();
177
+
178
+ // 5. Close the dialog
179
+ onClose();
180
+ }
181
+
182
+ // --- Open recent project ---
183
+
184
+ /** Flatten visible layers into the render buffer after loading a project. */
185
+ function recompositeAfterLoad(): void {
186
+ const currentFrame = getCurrentFrame();
187
+ const layers = getLayers();
188
+ const result = composite(
189
+ layers,
190
+ currentFrame.pixelData,
191
+ canvasState.canvasWidth,
192
+ canvasState.canvasHeight,
193
+ );
194
+ setRenderBuffer(result);
195
+ }
196
+
197
+ function handleRecentClick(project: RecentProject) {
198
+ if (hasUnsavedChanges()) {
199
+ pendingRecentProject = project;
200
+ showWarning = true;
201
+ return;
202
+ }
203
+ void openRecentProject(project);
204
+ }
205
+
206
+ async function openRecentProject(project: RecentProject) {
207
+ try {
208
+ // Use the storage open flow -- the path is stored but we need to
209
+ // re-read it from disk (desktop) or prompt (browser).
210
+ const result = await openProject();
211
+ if (!result) return; // user cancelled
212
+
213
+ applySnapshot(result.snapshot);
214
+ saveState.savePath = result.path;
215
+ saveState.saveFormat = result.format;
216
+ saveState.projectName = result.snapshot.name || 'Untitled';
217
+
218
+ recompositeAfterLoad();
219
+
220
+ const snap = result.snapshot;
221
+ addRecentProject({
222
+ name: snap.name || 'Untitled',
223
+ path: result.path,
224
+ timestamp: Date.now(),
225
+ dimensions: `${(snap.canvas as { width: number; height: number }).width}x${(snap.canvas as { width: number; height: number }).height}`,
226
+ });
227
+
228
+ onClose();
229
+ } catch (err) {
230
+ console.error('Failed to open recent project:', err);
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Handle the warning confirmation -- either create new or open recent,
236
+ * depending on what triggered the warning.
237
+ */
238
+ function handleWarningConfirm() {
239
+ if (pendingRecentProject) {
240
+ const project = pendingRecentProject;
241
+ pendingRecentProject = null;
242
+ showWarning = false;
243
+ void openRecentProject(project);
244
+ } else {
245
+ handleCreate();
246
+ }
247
+ }
248
+
249
+ // --- Formatting helpers ---
250
+
251
+ function formatTimestamp(ts: number): string {
252
+ const now = Date.now();
253
+ const diffMs = now - ts;
254
+ const diffMin = Math.floor(diffMs / 60_000);
255
+ if (diffMin < 1) return 'Just now';
256
+ if (diffMin < 60) return `${diffMin}m ago`;
257
+ const diffHr = Math.floor(diffMin / 60);
258
+ if (diffHr < 24) return `${diffHr}h ago`;
259
+ const diffDay = Math.floor(diffHr / 24);
260
+ if (diffDay < 30) return `${diffDay}d ago`;
261
+ // Fall back to date string
262
+ return new Date(ts).toLocaleDateString();
263
+ }
264
+
265
+ // Reset form values and warning state when dialog opens
266
+ $effect(() => {
267
+ if (open) {
268
+ width = 32;
269
+ height = 32;
270
+ bgColor = 'transparent';
271
+ selectedTemplate = null;
272
+ showWarning = false;
273
+ pendingRecentProject = null;
274
+ recentProjects = getRecentProjects();
275
+ }
276
+ });
277
+
278
+ // Sync native <dialog> open state with the `open` prop.
279
+ // Calling showModal() on an already-open dialog throws; guard with `dialogEl.open`.
280
+ $effect(() => {
281
+ if (!dialogEl) return;
282
+ if (open && !dialogEl.open) {
283
+ dialogEl.showModal();
284
+ } else if (!open && dialogEl.open) {
285
+ dialogEl.close();
286
+ }
287
+ });
288
+
289
+ // Close on backdrop click: native <dialog> fires clicks on itself for the
290
+ // backdrop area (events on child content have a different target).
291
+ function handleDialogClick(e: MouseEvent) {
292
+ if (e.target === dialogEl) {
293
+ onClose();
294
+ }
295
+ }
296
+ </script>
297
+
298
+ <!--
299
+ The native <dialog> element handles focus trap, Escape key (cancel event),
300
+ and backdrop rendering for free. `onclose` fires on both user-initiated
301
+ (Escape) and programmatic close, which lets us keep the parent `open` prop
302
+ in sync. `onClose` is idempotent w.r.t. already-closed state, so the echo
303
+ from our own dialogEl.close() call is harmless.
304
+ -->
305
+ <dialog
306
+ bind:this={dialogEl}
307
+ class="pw-dialog"
308
+ onclick={handleDialogClick}
309
+ onclose={onClose}
310
+ >
311
+ {#if open}
312
+ <div class="modal">
313
+ {#if showWarning}
314
+ <!-- Unsaved-changes confirmation step -->
315
+ <h2 class="modal-title">Unsaved Changes</h2>
316
+ <p class="warning-text">
317
+ You have unsaved changes. {pendingRecentProject ? 'Open this project' : 'Create new project'} anyway?
318
+ </p>
319
+ <div class="modal-actions">
320
+ <button class="btn btn--cancel" onclick={() => { showWarning = false; pendingRecentProject = null; }}>Cancel</button>
321
+ <button class="btn btn--danger" onclick={handleWarningConfirm}>
322
+ {pendingRecentProject ? 'Open' : 'Create'}
323
+ </button>
324
+ </div>
325
+ {:else}
326
+ <!-- Normal new-project form -->
327
+ <h2 class="modal-title">New Project</h2>
328
+
329
+ <!-- Templates -->
330
+ <div class="form-section">
331
+ <span class="section-heading">Templates</span>
332
+ <div class="template-grid">
333
+ {#each templates as t (t.label)}
334
+ <button
335
+ class="template-card"
336
+ class:template-card--active={selectedTemplate === t}
337
+ onclick={() => applyTemplate(t)}
338
+ >
339
+ <span class="template-name">{t.label}</span>
340
+ <span class="template-dims">{t.w}x{t.h}</span>
341
+ <span class="template-info">{t.layers?.length ?? 1}L / {t.frames ?? 1}F</span>
342
+ </button>
343
+ {/each}
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Recent Projects -->
348
+ {#if recentProjects.length > 0}
349
+ <div class="form-section">
350
+ <span class="section-heading">Recent Projects</span>
351
+ <div class="recent-list">
352
+ {#each recentProjects as project (project.path)}
353
+ <button
354
+ class="recent-item"
355
+ onclick={() => handleRecentClick(project)}
356
+ title={project.path}
357
+ >
358
+ <span class="recent-name">{project.name}</span>
359
+ <span class="recent-meta">
360
+ <span class="recent-dims">{project.dimensions}</span>
361
+ <span class="recent-time">{formatTimestamp(project.timestamp)}</span>
362
+ </span>
363
+ </button>
364
+ {/each}
365
+ </div>
366
+ </div>
367
+ {/if}
368
+
369
+ <!-- Custom size heading -->
370
+ <span class="section-heading">Custom Size</span>
371
+
372
+ <!-- Dimensions -->
373
+ <div class="form-row">
374
+ <label class="form-label" for="np-width">Width</label>
375
+ <input
376
+ id="np-width"
377
+ class="form-input"
378
+ type="number"
379
+ min={MIN_CANVAS_SIZE}
380
+ max={MAX_CANVAS_SIZE}
381
+ step="1"
382
+ bind:value={width}
383
+ onchange={clampWidth}
384
+ />
385
+ <span class="form-unit">px</span>
386
+ </div>
387
+
388
+ <div class="form-row">
389
+ <label class="form-label" for="np-height">Height</label>
390
+ <input
391
+ id="np-height"
392
+ class="form-input"
393
+ type="number"
394
+ min={MIN_CANVAS_SIZE}
395
+ max={MAX_CANVAS_SIZE}
396
+ step="1"
397
+ bind:value={height}
398
+ onchange={clampHeight}
399
+ />
400
+ <span class="form-unit">px</span>
401
+ </div>
402
+
403
+ <!-- Presets -->
404
+ <div class="form-section">
405
+ <span class="form-label">Presets</span>
406
+ <div class="preset-group">
407
+ {#each presets as preset (preset.label)}
408
+ <button
409
+ class="preset-btn"
410
+ class:preset-btn--active={width === preset.w && height === preset.h}
411
+ onclick={() => { applyPreset(preset.w, preset.h); }}
412
+ >{preset.label}</button>
413
+ {/each}
414
+ </div>
415
+ </div>
416
+
417
+ <!-- Background color -->
418
+ <div class="form-section">
419
+ <span class="form-label">Background</span>
420
+ <div class="bg-options">
421
+ <button
422
+ class="bg-btn bg-btn--transparent"
423
+ class:bg-btn--active={bgColor === 'transparent'}
424
+ title="Transparent"
425
+ aria-label="Transparent background"
426
+ aria-pressed={bgColor === 'transparent'}
427
+ onclick={() => bgColor = 'transparent'}
428
+ >
429
+ <!-- Checkerboard pattern for transparent -->
430
+ <span class="checkerboard"></span>
431
+ </button>
432
+ <button
433
+ class="bg-btn bg-btn--white"
434
+ class:bg-btn--active={bgColor === 'white'}
435
+ title="White"
436
+ aria-label="White background"
437
+ aria-pressed={bgColor === 'white'}
438
+ onclick={() => bgColor = 'white'}
439
+ ></button>
440
+ <button
441
+ class="bg-btn bg-btn--black"
442
+ class:bg-btn--active={bgColor === 'black'}
443
+ title="Black"
444
+ aria-label="Black background"
445
+ aria-pressed={bgColor === 'black'}
446
+ onclick={() => bgColor = 'black'}
447
+ ></button>
448
+ </div>
449
+ </div>
450
+
451
+ <!-- Actions -->
452
+ <div class="modal-actions">
453
+ <button class="btn btn--cancel" onclick={onClose}>Cancel</button>
454
+ <button class="btn btn--primary" onclick={handleCreateClick}>Create</button>
455
+ </div>
456
+ {/if}
457
+ </div>
458
+ {/if}
459
+ </dialog>
460
+
461
+ <style>
462
+ /* Reset native <dialog> UA styles so it fills the viewport and lets us
463
+ center the inner .modal via flexbox. The backdrop is styled separately. */
464
+ .pw-dialog {
465
+ margin: 0;
466
+ padding: 0;
467
+ border: none;
468
+ background: transparent;
469
+ max-width: none;
470
+ max-height: none;
471
+ width: 100vw;
472
+ height: 100vh;
473
+ color: inherit;
474
+ }
475
+
476
+ .pw-dialog[open] {
477
+ display: flex;
478
+ align-items: center;
479
+ justify-content: center;
480
+ }
481
+
482
+ .pw-dialog::backdrop {
483
+ background: rgba(0, 0, 0, 0.5);
484
+ }
485
+
486
+ .modal {
487
+ background: var(--bg-panel);
488
+ border: 1px solid var(--border);
489
+ border-radius: 6px;
490
+ box-shadow: var(--shadow-md);
491
+ width: 480px;
492
+ max-width: 90vw;
493
+ max-height: 85vh;
494
+ overflow-y: auto;
495
+ padding: 24px;
496
+ color: var(--text-primary);
497
+ }
498
+
499
+ .modal-title {
500
+ font-size: 18px;
501
+ font-weight: 600;
502
+ margin-bottom: 20px;
503
+ color: var(--text-primary);
504
+ }
505
+
506
+ /* Section headings used for Templates, Recent, Custom Size */
507
+ .section-heading {
508
+ display: block;
509
+ font-size: var(--text-lg);
510
+ font-weight: 500;
511
+ color: var(--text-secondary);
512
+ margin-bottom: 8px;
513
+ }
514
+
515
+ /* Form layout */
516
+ .form-row {
517
+ display: flex;
518
+ align-items: center;
519
+ gap: 8px;
520
+ margin-bottom: 12px;
521
+ }
522
+
523
+ .form-section {
524
+ margin-bottom: 16px;
525
+ }
526
+
527
+ .form-label {
528
+ width: 80px;
529
+ flex-shrink: 0;
530
+ font-size: var(--text-lg);
531
+ color: var(--text-secondary);
532
+ }
533
+
534
+ .form-input {
535
+ flex: 1;
536
+ background: var(--bg-toolbar);
537
+ border: 1px solid var(--border);
538
+ border-radius: var(--radius-md);
539
+ color: var(--text-primary);
540
+ font-size: var(--text-lg);
541
+ padding: 6px 8px;
542
+ max-width: 120px;
543
+ }
544
+
545
+ .form-input:focus-visible {
546
+ border-color: var(--accent);
547
+ }
548
+
549
+ .form-unit {
550
+ font-size: var(--text-base);
551
+ color: var(--text-muted);
552
+ }
553
+
554
+ /* Templates grid */
555
+ .template-grid {
556
+ display: grid;
557
+ grid-template-columns: repeat(3, 1fr);
558
+ gap: 8px;
559
+ }
560
+
561
+ .template-card {
562
+ display: flex;
563
+ flex-direction: column;
564
+ align-items: center;
565
+ justify-content: center;
566
+ gap: 4px;
567
+ padding: 10px 6px;
568
+ background: var(--bg-toolbar);
569
+ border: 1px solid var(--border);
570
+ border-radius: var(--radius-md);
571
+ cursor: pointer;
572
+ transition: background var(--transition-fast), border-color var(--transition-fast);
573
+ }
574
+
575
+ .template-card:hover {
576
+ background: var(--bg-primary);
577
+ border-color: var(--accent);
578
+ }
579
+
580
+ .template-card--active {
581
+ background: var(--accent);
582
+ border-color: var(--accent);
583
+ }
584
+
585
+ .template-card--active .template-name,
586
+ .template-card--active .template-dims,
587
+ .template-card--active .template-info {
588
+ color: #ffffff;
589
+ }
590
+
591
+ .template-name {
592
+ font-size: var(--text-base);
593
+ font-weight: 500;
594
+ color: var(--text-primary);
595
+ text-align: center;
596
+ line-height: 1.2;
597
+ }
598
+
599
+ .template-dims {
600
+ font-size: var(--text-sm, 11px);
601
+ color: var(--text-muted);
602
+ }
603
+
604
+ .template-info {
605
+ font-size: var(--text-sm, 11px);
606
+ color: var(--text-muted);
607
+ }
608
+
609
+ /* Recent projects list */
610
+ .recent-list {
611
+ display: flex;
612
+ flex-direction: column;
613
+ gap: 2px;
614
+ max-height: 160px;
615
+ overflow-y: auto;
616
+ }
617
+
618
+ .recent-item {
619
+ display: flex;
620
+ align-items: center;
621
+ justify-content: space-between;
622
+ gap: 8px;
623
+ padding: 6px 10px;
624
+ background: transparent;
625
+ border: 1px solid transparent;
626
+ border-radius: var(--radius-md);
627
+ cursor: pointer;
628
+ text-align: left;
629
+ transition: background var(--transition-fast), border-color var(--transition-fast);
630
+ }
631
+
632
+ .recent-item:hover {
633
+ background: var(--bg-toolbar);
634
+ border-color: var(--border);
635
+ }
636
+
637
+ .recent-name {
638
+ font-size: var(--text-base);
639
+ color: var(--text-primary);
640
+ overflow: hidden;
641
+ text-overflow: ellipsis;
642
+ white-space: nowrap;
643
+ min-width: 0;
644
+ }
645
+
646
+ .recent-meta {
647
+ display: flex;
648
+ align-items: center;
649
+ gap: 8px;
650
+ flex-shrink: 0;
651
+ }
652
+
653
+ .recent-dims {
654
+ font-size: var(--text-sm, 11px);
655
+ color: var(--text-muted);
656
+ }
657
+
658
+ .recent-time {
659
+ font-size: var(--text-sm, 11px);
660
+ color: var(--text-muted);
661
+ }
662
+
663
+ /* Presets */
664
+ .preset-group {
665
+ display: flex;
666
+ flex-wrap: wrap;
667
+ gap: 6px;
668
+ margin-top: 6px;
669
+ }
670
+
671
+ .preset-btn {
672
+ background: var(--bg-toolbar);
673
+ border: 1px solid var(--border);
674
+ border-radius: var(--radius-md);
675
+ color: var(--text-secondary);
676
+ font-size: var(--text-base);
677
+ padding: 4px 10px;
678
+ cursor: pointer;
679
+ transition: background var(--transition-fast), border-color var(--transition-fast);
680
+ }
681
+
682
+ .preset-btn:hover {
683
+ background: var(--bg-primary);
684
+ color: var(--text-primary);
685
+ border-color: var(--accent);
686
+ }
687
+
688
+ .preset-btn--active {
689
+ background: var(--accent);
690
+ color: #ffffff;
691
+ border-color: var(--accent);
692
+ }
693
+
694
+ .preset-btn--active:hover {
695
+ background: var(--accent-hover);
696
+ }
697
+
698
+ /* Background color options */
699
+ .bg-options {
700
+ display: flex;
701
+ gap: 8px;
702
+ margin-top: 6px;
703
+ }
704
+
705
+ .bg-btn {
706
+ width: 32px;
707
+ height: 32px;
708
+ border: 2px solid var(--border);
709
+ border-radius: var(--radius-md);
710
+ cursor: pointer;
711
+ padding: 0;
712
+ position: relative;
713
+ overflow: hidden;
714
+ }
715
+
716
+ .bg-btn:hover {
717
+ border-color: var(--text-secondary);
718
+ }
719
+
720
+ .bg-btn--active {
721
+ border-color: var(--accent);
722
+ box-shadow: 0 0 0 1px var(--accent);
723
+ }
724
+
725
+ .bg-btn--white {
726
+ background: #ffffff;
727
+ }
728
+
729
+ .bg-btn--black {
730
+ background: #000000;
731
+ }
732
+
733
+ .bg-btn--transparent {
734
+ background: #ffffff;
735
+ }
736
+
737
+ /* Checkerboard pattern for transparent indicator */
738
+ .checkerboard {
739
+ display: block;
740
+ width: 100%;
741
+ height: 100%;
742
+ background-image:
743
+ linear-gradient(45deg, #ccc 25%, transparent 25%),
744
+ linear-gradient(-45deg, #ccc 25%, transparent 25%),
745
+ linear-gradient(45deg, transparent 75%, #ccc 75%),
746
+ linear-gradient(-45deg, transparent 75%, #ccc 75%);
747
+ background-size: 8px 8px;
748
+ background-position: 0 0, 0 4px, 4px -4px, -4px 0;
749
+ }
750
+
751
+ /* Action buttons */
752
+ .modal-actions {
753
+ display: flex;
754
+ justify-content: flex-end;
755
+ gap: 8px;
756
+ margin-top: 20px;
757
+ }
758
+
759
+ .btn {
760
+ border: none;
761
+ border-radius: var(--radius-md);
762
+ font-size: var(--text-lg);
763
+ padding: 8px 20px;
764
+ cursor: pointer;
765
+ font-weight: 500;
766
+ }
767
+
768
+ .btn--cancel {
769
+ background: var(--bg-toolbar);
770
+ color: var(--text-secondary);
771
+ border: 1px solid var(--border);
772
+ }
773
+
774
+ .btn--cancel:hover {
775
+ background: var(--bg-primary);
776
+ color: var(--text-primary);
777
+ }
778
+
779
+ .btn--primary {
780
+ background: var(--accent);
781
+ color: #ffffff;
782
+ }
783
+
784
+ .btn--primary:hover {
785
+ background: var(--accent-hover);
786
+ }
787
+
788
+ .btn--danger {
789
+ background: var(--danger, #d44);
790
+ color: #ffffff;
791
+ }
792
+
793
+ .btn--danger:hover {
794
+ background: var(--danger-hover, #b33);
795
+ }
796
+
797
+ .warning-text {
798
+ font-size: var(--text-lg);
799
+ color: var(--text-secondary);
800
+ margin-bottom: 16px;
801
+ line-height: 1.5;
802
+ }
803
+ </style>