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,810 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LayerPanel -- right sidebar panel for managing layers.
4
+ *
5
+ * Displays layers in reverse visual order (top layer first) with controls
6
+ * for visibility, opacity, locking, renaming, and drag-to-reorder.
7
+ * All mutations dispatch commands through the dispatcher for undo/redo.
8
+ */
9
+
10
+ import type { CommandType, ParamsOf } from '../core/command-params.js';
11
+ import type { Layer } from '../layers/layer-types.js';
12
+ import { dispatch } from '../core/dispatcher.js';
13
+ import {
14
+ getLayers,
15
+ getActiveLayerId,
16
+ getFlatList,
17
+ } from '../layers/layer-tree.svelte.js';
18
+ import { getCurrentFrame } from '../animation/frame-model.svelte.js';
19
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
20
+ import ContextMenu from './ContextMenu.svelte';
21
+ import PlusIcon from '~icons/lucide/plus';
22
+ import CopyIcon from '~icons/lucide/copy-plus';
23
+ import TrashIcon from '~icons/lucide/trash-2';
24
+ import ChevronDown from '~icons/lucide/chevron-down';
25
+ import ChevronRight from '~icons/lucide/chevron-right';
26
+
27
+ // --- Command dispatch helper ---
28
+
29
+ // Typed overload: compile-time param validation for known commands
30
+ function dispatchCmd<T extends CommandType>(type: T, params: ParamsOf<T>): void;
31
+ // String fallback: for dynamic dispatch where command type is a variable
32
+ function dispatchCmd(type: string, params?: Record<string, unknown>): void;
33
+ function dispatchCmd(type: string, params: Record<string, unknown> = {}) {
34
+ dispatch({
35
+ type,
36
+ plugin: 'ui/layers',
37
+ version: '1.0.0',
38
+ params,
39
+ id: crypto.randomUUID(),
40
+ timestamp: Date.now(),
41
+ });
42
+ }
43
+
44
+ // --- Local UI state ---
45
+
46
+ let editingLayerId = $state<string | null>(null);
47
+ let editingName = $state('');
48
+ let editInputEl = $state<HTMLInputElement | null>(null);
49
+
50
+ // Drag state
51
+ let dragLayerId = $state<string | null>(null);
52
+ let dragOverLayerId = $state<string | null>(null);
53
+ let dragInsertPosition = $state<'above' | 'below' | null>(null);
54
+
55
+ // --- Layer thumbnail rendering ---
56
+
57
+ /** Thumbnail canvas size in CSS/device pixels. */
58
+ const THUMB_SIZE = 24;
59
+
60
+ /**
61
+ * Map of layer ID -> canvas element ref for thumbnails.
62
+ * Populated by bind:this in the template.
63
+ */
64
+ let thumbCanvasEls = $state<Map<string, HTMLCanvasElement>>(new Map());
65
+
66
+ /**
67
+ * Render a single layer's thumbnail onto its canvas element.
68
+ * Reads the layer's PixelBuffer from the current frame, converts to
69
+ * ImageData, and scales down with nearest-neighbor interpolation.
70
+ */
71
+ function renderLayerThumb(layerId: string, canvasEl: HTMLCanvasElement) {
72
+ const w = canvasState.canvasWidth;
73
+ const h = canvasState.canvasHeight;
74
+
75
+ canvasEl.width = THUMB_SIZE;
76
+ canvasEl.height = THUMB_SIZE;
77
+ const ctx = canvasEl.getContext('2d');
78
+ if (!ctx) return;
79
+ ctx.imageSmoothingEnabled = false;
80
+ ctx.clearRect(0, 0, THUMB_SIZE, THUMB_SIZE);
81
+
82
+ const frame = getCurrentFrame();
83
+ const buffer = frame.pixelData.get(layerId);
84
+ if (!buffer) return;
85
+
86
+ const imgData = buffer.toImageData();
87
+
88
+ // Draw full-size onto an offscreen canvas, then scale down
89
+ const offscreen = new OffscreenCanvas(w, h);
90
+ const offCtx = offscreen.getContext('2d');
91
+ if (!offCtx) return;
92
+ offCtx.putImageData(imgData, 0, 0);
93
+
94
+ // Scale proportionally to fit within THUMB_SIZE, centered
95
+ const scale = Math.min(THUMB_SIZE / w, THUMB_SIZE / h);
96
+ const dw = Math.round(w * scale);
97
+ const dh = Math.round(h * scale);
98
+ const dx = Math.round((THUMB_SIZE - dw) / 2);
99
+ const dy = Math.round((THUMB_SIZE - dh) / 2);
100
+
101
+ ctx.drawImage(offscreen, 0, 0, w, h, dx, dy, dw, dh);
102
+ }
103
+
104
+ /**
105
+ * Svelte action that registers a canvas element in the thumbCanvasEls map
106
+ * on mount and removes it on destroy. Used via use:trackThumbCanvas={layerId}.
107
+ */
108
+ function trackThumbCanvas(node: HTMLCanvasElement, layerId: string) {
109
+ thumbCanvasEls.set(layerId, node);
110
+ return {
111
+ destroy() {
112
+ thumbCanvasEls.delete(layerId);
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Re-render all layer thumbnails when frame data, layer list, or
119
+ * canvas dimensions change. Follows the same $effect pattern as
120
+ * FrameStrip.svelte.
121
+ */
122
+ $effect(() => {
123
+ // Read reactive deps to trigger re-runs
124
+ const _layers = getLayers();
125
+ const _frame = getCurrentFrame();
126
+ const _pixelData = _frame.pixelData;
127
+ const _w = canvasState.canvasWidth;
128
+ const _h = canvasState.canvasHeight;
129
+
130
+ for (const [layerId, el] of thumbCanvasEls) {
131
+ renderLayerThumb(layerId, el);
132
+ }
133
+ });
134
+
135
+ // --- Derived state ---
136
+
137
+ // Layers in visual order (reversed: last array element = top layer shown first)
138
+ let reversedLayers = $derived([...getLayers()].reverse());
139
+
140
+ // Total pixel layer count (to prevent deleting the last one)
141
+ let pixelLayerCount = $derived(
142
+ getFlatList().filter((l) => l.type === 'pixel').length,
143
+ );
144
+
145
+ // --- Actions ---
146
+
147
+ function handleAddLayer() {
148
+ const count = getFlatList().length;
149
+ dispatchCmd('add_layer', { name: `Layer ${String(count + 1)}` });
150
+ }
151
+
152
+ function handleDeleteLayer() {
153
+ const activeId = getActiveLayerId();
154
+ if (!activeId) return;
155
+ // Prevent deleting the last pixel layer
156
+ if (pixelLayerCount <= 1) return;
157
+ dispatchCmd('remove_layer', { id: activeId });
158
+ }
159
+
160
+ function handleDuplicateLayer() {
161
+ const activeId = getActiveLayerId();
162
+ if (!activeId) return;
163
+ dispatchCmd('duplicate_layer', { id: activeId });
164
+ }
165
+
166
+ function handleSelectLayer(layer: Layer) {
167
+ if (layer.type === 'pixel') {
168
+ dispatchCmd('set_active_layer', { id: layer.id });
169
+ }
170
+ }
171
+
172
+ function handleToggleVisibility(e: MouseEvent, layer: Layer) {
173
+ e.stopPropagation();
174
+ dispatchCmd('set_layer_visibility', {
175
+ id: layer.id,
176
+ visible: !layer.visible,
177
+ });
178
+ }
179
+
180
+ function handleToggleLocked(e: MouseEvent, layer: Layer) {
181
+ e.stopPropagation();
182
+ dispatchCmd('set_layer_locked', { id: layer.id, locked: !layer.locked });
183
+ }
184
+
185
+ function handleToggleExpanded(e: MouseEvent, layer: Layer) {
186
+ e.stopPropagation();
187
+ dispatchCmd('toggle_expanded', { id: layer.id });
188
+ }
189
+
190
+ function handleOpacityChange(e: Event, layer: Layer) {
191
+ e.stopPropagation();
192
+ const value = parseInt((e.target as HTMLInputElement).value, 10);
193
+ dispatchCmd('set_layer_opacity', { id: layer.id, opacity: value });
194
+ }
195
+
196
+ function handleBlendModeChange(e: Event, layer: Layer) {
197
+ e.stopPropagation();
198
+ const mode = (e.target as HTMLSelectElement).value;
199
+ dispatchCmd('set_layer_blend_mode', { id: layer.id, mode });
200
+ }
201
+
202
+ // --- Inline rename ---
203
+
204
+ function startRename(layer: Layer) {
205
+ editingLayerId = layer.id;
206
+ editingName = layer.name;
207
+ // Focus the input after it renders
208
+ queueMicrotask(() => {
209
+ editInputEl?.focus();
210
+ editInputEl?.select();
211
+ });
212
+ }
213
+
214
+ function commitRename() {
215
+ if (editingLayerId && editingName.trim()) {
216
+ dispatchCmd('rename_layer', {
217
+ id: editingLayerId,
218
+ name: editingName.trim(),
219
+ });
220
+ }
221
+ editingLayerId = null;
222
+ editingName = '';
223
+ }
224
+
225
+ function handleRenameKeydown(e: KeyboardEvent) {
226
+ if (e.key === 'Enter') {
227
+ e.preventDefault();
228
+ commitRename();
229
+ } else if (e.key === 'Escape') {
230
+ e.preventDefault();
231
+ editingLayerId = null;
232
+ editingName = '';
233
+ }
234
+ }
235
+
236
+ // --- Drag and drop reordering ---
237
+
238
+ /**
239
+ * Compute the actual array index from a visual (reversed) position.
240
+ * Visual index 0 = top of panel = last element in the layers array.
241
+ * We need to map to the real array index for move_layer.
242
+ */
243
+
244
+ function handlePointerDown(e: PointerEvent, layer: Layer) {
245
+ // Only start drag on primary button, and not on interactive child elements
246
+ if (e.button !== 0) return;
247
+ const target = e.target as HTMLElement;
248
+ if (target.closest('button, input, select')) return;
249
+
250
+ dragLayerId = layer.id;
251
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
252
+ }
253
+
254
+ function handlePointerMove(e: PointerEvent, layer: Layer) {
255
+ if (!dragLayerId || dragLayerId === layer.id) {
256
+ // Compute insertion position relative to the row midpoint
257
+ if (dragLayerId && dragLayerId !== layer.id) {
258
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
259
+ const midY = rect.top + rect.height / 2;
260
+ dragOverLayerId = layer.id;
261
+ dragInsertPosition = e.clientY < midY ? 'above' : 'below';
262
+ }
263
+ return;
264
+ }
265
+ const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
266
+ const midY = rect.top + rect.height / 2;
267
+ dragOverLayerId = layer.id;
268
+ dragInsertPosition = e.clientY < midY ? 'above' : 'below';
269
+ }
270
+
271
+ function handlePointerUp(_e: PointerEvent) {
272
+ if (dragLayerId && dragOverLayerId && dragInsertPosition) {
273
+ // Determine the target index in the real (non-reversed) layers array.
274
+ // The visual list is reversed, so "above" in the UI means a higher
275
+ // index in the real array, and "below" means a lower index.
276
+ const realLayers = getLayers();
277
+ const targetRealIndex = realLayers.findIndex(
278
+ (l) => l.id === dragOverLayerId,
279
+ );
280
+
281
+ if (targetRealIndex !== -1) {
282
+ // "above" visually = after in the real array (higher index)
283
+ // "below" visually = at the target's position in the real array
284
+ let newIndex: number;
285
+ if (dragInsertPosition === 'above') {
286
+ newIndex = targetRealIndex + 1;
287
+ } else {
288
+ newIndex = targetRealIndex;
289
+ }
290
+
291
+ // Adjust: if dragging from below the target (lower real index),
292
+ // the removal shifts indices down by 1
293
+ const sourceRealIndex = realLayers.findIndex(
294
+ (l) => l.id === dragLayerId,
295
+ );
296
+ if (sourceRealIndex < targetRealIndex) {
297
+ newIndex -= 1;
298
+ }
299
+
300
+ // Only dispatch if position actually changes
301
+ if (sourceRealIndex !== newIndex) {
302
+ dispatchCmd('move_layer', {
303
+ id: dragLayerId,
304
+ newParentId: null,
305
+ newIndex,
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ dragLayerId = null;
312
+ dragOverLayerId = null;
313
+ dragInsertPosition = null;
314
+ }
315
+
316
+ // --- Context menu state ---
317
+ let contextMenu = $state<{ x: number; y: number } | null>(null);
318
+
319
+ function handleContextMenu(e: MouseEvent) {
320
+ e.preventDefault();
321
+ contextMenu = { x: e.clientX, y: e.clientY };
322
+ }
323
+ </script>
324
+
325
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
326
+ <div class="layer-panel" oncontextmenu={handleContextMenu}>
327
+ <!-- Header -->
328
+ <div class="layer-header">
329
+ <span class="layer-title">Layers</span>
330
+ <div class="layer-actions">
331
+ <button
332
+ class="action-btn"
333
+ title="Add Layer"
334
+ aria-label="Add Layer"
335
+ onclick={handleAddLayer}
336
+ ><PlusIcon /></button>
337
+ <button
338
+ class="action-btn"
339
+ title="Duplicate Layer"
340
+ aria-label="Duplicate Layer"
341
+ onclick={handleDuplicateLayer}
342
+ disabled={!getActiveLayerId()}
343
+ ><CopyIcon /></button>
344
+ <button
345
+ class="action-btn"
346
+ title="Delete Layer"
347
+ aria-label="Delete Layer"
348
+ onclick={handleDeleteLayer}
349
+ disabled={!getActiveLayerId() || pixelLayerCount <= 1}
350
+ ><TrashIcon /></button>
351
+ </div>
352
+ </div>
353
+
354
+ <!-- Layer list -->
355
+ <div class="layer-list">
356
+ {#each reversedLayers as layer (layer.id)}
357
+ <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
358
+ {@render layerRow(layer, 0)}
359
+ {/each}
360
+ </div>
361
+ </div>
362
+
363
+ {#if contextMenu}
364
+ <ContextMenu
365
+ menuPath="context/layer"
366
+ x={contextMenu.x}
367
+ y={contextMenu.y}
368
+ onClose={() => contextMenu = null}
369
+ />
370
+ {/if}
371
+
372
+ <!-- Recursive layer row snippet (handles groups and nesting) -->
373
+ {#snippet layerRow(layer: Layer, depth: number)}
374
+ <div
375
+ class="layer-row"
376
+ class:layer-row--active={layer.id === getActiveLayerId()}
377
+ class:layer-row--drag-above={dragOverLayerId === layer.id && dragInsertPosition === 'above'}
378
+ class:layer-row--drag-below={dragOverLayerId === layer.id && dragInsertPosition === 'below'}
379
+ class:layer-row--dragging={dragLayerId === layer.id}
380
+ style:padding-left="{8 + depth * 16}px"
381
+ role="option"
382
+ aria-selected={layer.id === getActiveLayerId()}
383
+ tabindex="0"
384
+ onclick={() => { handleSelectLayer(layer); }}
385
+ ondblclick={() => { startRename(layer); }}
386
+ onpointerdown={(e) => { handlePointerDown(e, layer); }}
387
+ onpointermove={(e) => { handlePointerMove(e, layer); }}
388
+ onpointerup={handlePointerUp}
389
+ >
390
+ <!-- Layer thumbnail preview -->
391
+ {#if layer.type === 'pixel'}
392
+ <canvas
393
+ class="layer-thumb"
394
+ width={THUMB_SIZE}
395
+ height={THUMB_SIZE}
396
+ use:trackThumbCanvas={layer.id}
397
+ ></canvas>
398
+ {:else}
399
+ <!-- Group: small folder icon placeholder -->
400
+ <div class="layer-thumb layer-thumb--group">
401
+ <svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
402
+ <path d="M3 7v10a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-6l-2-2H5a2 2 0 0 0-2 2z"/>
403
+ </svg>
404
+ </div>
405
+ {/if}
406
+
407
+ <!-- Group expand/collapse arrow -->
408
+ {#if layer.type === 'group'}
409
+ <button
410
+ class="expand-btn"
411
+ title={layer.expanded ? 'Collapse' : 'Expand'}
412
+ aria-label={layer.expanded ? 'Collapse group' : 'Expand group'}
413
+ onclick={(e) => { handleToggleExpanded(e, layer); }}
414
+ >
415
+ {#if layer.expanded}
416
+ <ChevronDown />
417
+ {:else}
418
+ <ChevronRight />
419
+ {/if}
420
+ </button>
421
+ {/if}
422
+
423
+ <!-- Visibility toggle -->
424
+ <button
425
+ class="vis-btn"
426
+ title={layer.visible ? 'Hide' : 'Show'}
427
+ aria-label={layer.visible ? `Hide layer ${layer.name}` : `Show layer ${layer.name}`}
428
+ aria-pressed={layer.visible}
429
+ onclick={(e) => { handleToggleVisibility(e, layer); }}
430
+ >
431
+ {#if layer.visible}
432
+ <svg class="icon" viewBox="0 0 16 16" width="14" height="14">
433
+ <path d="M8 3C4 3 1 8 1 8s3 5 7 5 7-5 7-5-3-5-7-5z" fill="none" stroke="currentColor" stroke-width="1.5"/>
434
+ <circle cx="8" cy="8" r="2" fill="currentColor"/>
435
+ </svg>
436
+ {:else}
437
+ <svg class="icon" viewBox="0 0 16 16" width="14" height="14">
438
+ <path d="M8 3C4 3 1 8 1 8s3 5 7 5 7-5 7-5-3-5-7-5z" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"/>
439
+ <line x1="2" y1="2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5"/>
440
+ </svg>
441
+ {/if}
442
+ </button>
443
+
444
+ <!-- Layer name (inline edit on double-click) -->
445
+ {#if editingLayerId === layer.id}
446
+ <input
447
+ bind:this={editInputEl}
448
+ class="rename-input"
449
+ type="text"
450
+ bind:value={editingName}
451
+ onblur={commitRename}
452
+ onkeydown={handleRenameKeydown}
453
+ onclick={(e) => { e.stopPropagation(); }}
454
+ />
455
+ {:else}
456
+ <span class="layer-name" class:layer-name--group={layer.type === 'group'}>
457
+ {layer.name}
458
+ </span>
459
+ {/if}
460
+
461
+ <!-- Blend mode and opacity (only for pixel layers) -->
462
+ {#if layer.type === 'pixel'}
463
+ <select
464
+ class="blend-select"
465
+ value={layer.blendMode}
466
+ title="Blend mode: {layer.blendMode}"
467
+ onchange={(e) => { handleBlendModeChange(e, layer); }}
468
+ onclick={(e) => { e.stopPropagation(); }}
469
+ >
470
+ <option value="normal">Normal</option>
471
+ <option value="multiply">Multiply</option>
472
+ <option value="screen">Screen</option>
473
+ <option value="overlay">Overlay</option>
474
+ </select>
475
+ <input
476
+ class="opacity-slider"
477
+ type="range"
478
+ min="0"
479
+ max="100"
480
+ value={layer.opacity}
481
+ title="Opacity: {layer.opacity}%"
482
+ oninput={(e) => { handleOpacityChange(e, layer); }}
483
+ onclick={(e) => { e.stopPropagation(); }}
484
+ />
485
+ <span class="opacity-value">{layer.opacity}</span>
486
+ {/if}
487
+
488
+ <!-- Lock toggle -->
489
+ <button
490
+ class="lock-btn"
491
+ title={layer.locked ? 'Unlock' : 'Lock'}
492
+ aria-label={layer.locked ? `Unlock layer ${layer.name}` : `Lock layer ${layer.name}`}
493
+ aria-pressed={layer.locked}
494
+ onclick={(e) => { handleToggleLocked(e, layer); }}
495
+ >
496
+ {#if layer.locked}
497
+ <svg class="icon" viewBox="0 0 16 16" width="12" height="12">
498
+ <rect x="3" y="7" width="10" height="7" rx="1" fill="currentColor" opacity="0.7"/>
499
+ <path d="M5 7V5a3 3 0 0 1 6 0v2" fill="none" stroke="currentColor" stroke-width="1.5"/>
500
+ </svg>
501
+ {:else}
502
+ <svg class="icon" viewBox="0 0 16 16" width="12" height="12">
503
+ <rect x="3" y="7" width="10" height="7" rx="1" fill="none" stroke="currentColor" stroke-width="1" opacity="0.3"/>
504
+ <path d="M5 7V5a3 3 0 0 1 6 0" fill="none" stroke="currentColor" stroke-width="1" opacity="0.3"/>
505
+ </svg>
506
+ {/if}
507
+ </button>
508
+ </div>
509
+
510
+ <!-- Render group children (also reversed for visual order) -->
511
+ {#if layer.type === 'group' && layer.expanded && layer.children}
512
+ {#each [...layer.children].reverse() as child (child.id)}
513
+ <!-- eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -->
514
+ {@render layerRow(child, depth + 1)}
515
+ {/each}
516
+ {/if}
517
+ {/snippet}
518
+
519
+ <style>
520
+ .layer-panel {
521
+ display: flex;
522
+ flex-direction: column;
523
+ width: 100%;
524
+ height: 100%;
525
+ background: var(--bg-panel);
526
+ color: var(--text-primary);
527
+ font-size: var(--text-base);
528
+ user-select: none;
529
+ }
530
+
531
+ .layer-header {
532
+ display: flex;
533
+ align-items: center;
534
+ justify-content: space-between;
535
+ padding: 6px var(--space-3);
536
+ border-bottom: 1px solid var(--border);
537
+ background: var(--bg-toolbar);
538
+ flex-shrink: 0;
539
+ }
540
+
541
+ .layer-title {
542
+ font-weight: 600;
543
+ font-size: var(--text-sm);
544
+ text-transform: uppercase;
545
+ letter-spacing: 0.5px;
546
+ color: var(--text-secondary);
547
+ }
548
+
549
+ .layer-actions {
550
+ display: flex;
551
+ gap: var(--space-1);
552
+ }
553
+
554
+ .action-btn {
555
+ background: none;
556
+ border: 1px solid transparent;
557
+ border-radius: var(--radius-sm);
558
+ color: var(--text-secondary);
559
+ cursor: pointer;
560
+ width: 24px;
561
+ height: 24px;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ font-size: var(--text-xl);
566
+ padding: 0;
567
+ line-height: 1;
568
+ }
569
+
570
+ .action-btn:hover:not(:disabled) {
571
+ background: var(--bg-primary);
572
+ color: var(--text-primary);
573
+ border-color: var(--border);
574
+ }
575
+
576
+ .action-btn:disabled {
577
+ opacity: 0.3;
578
+ cursor: default;
579
+ }
580
+
581
+ /* Layer list container -- scrollable */
582
+ .layer-list {
583
+ flex: 1;
584
+ overflow-y: auto;
585
+ overflow-x: hidden;
586
+ }
587
+
588
+ /* Individual layer row */
589
+ .layer-row {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: var(--space-2);
593
+ height: 34px;
594
+ padding-right: 6px;
595
+ cursor: pointer;
596
+ border-bottom: 1px solid transparent;
597
+ border-top: 1px solid transparent;
598
+ transition: background var(--transition-fast);
599
+ }
600
+
601
+ .layer-row:hover {
602
+ background: rgba(255, 255, 255, 0.04);
603
+ }
604
+
605
+ :global([data-theme="light"]) .layer-row:hover {
606
+ background: rgba(0, 0, 0, 0.04);
607
+ }
608
+
609
+ .layer-row--active {
610
+ background: var(--accent);
611
+ color: #ffffff;
612
+ }
613
+
614
+ .layer-row--active:hover {
615
+ background: var(--accent-hover);
616
+ }
617
+
618
+ .layer-row--dragging {
619
+ opacity: 0.4;
620
+ }
621
+
622
+ .layer-row--drag-above {
623
+ border-top: 2px solid var(--accent);
624
+ }
625
+
626
+ .layer-row--drag-below {
627
+ border-bottom: 2px solid var(--accent);
628
+ }
629
+
630
+ /* Expand/collapse button for groups */
631
+ .expand-btn {
632
+ background: none;
633
+ border: none;
634
+ color: inherit;
635
+ cursor: pointer;
636
+ width: 16px;
637
+ height: 16px;
638
+ display: flex;
639
+ align-items: center;
640
+ justify-content: center;
641
+ font-size: 8px;
642
+ padding: 0;
643
+ flex-shrink: 0;
644
+ }
645
+
646
+ /* Visibility button */
647
+ .vis-btn {
648
+ background: none;
649
+ border: none;
650
+ color: inherit;
651
+ cursor: pointer;
652
+ width: 20px;
653
+ height: 20px;
654
+ display: flex;
655
+ align-items: center;
656
+ justify-content: center;
657
+ padding: 0;
658
+ flex-shrink: 0;
659
+ }
660
+
661
+ .vis-btn:hover {
662
+ color: var(--accent);
663
+ }
664
+
665
+ .layer-row--active .vis-btn:hover {
666
+ color: #ffffff;
667
+ }
668
+
669
+ /* Layer name */
670
+ .layer-name {
671
+ flex: 1;
672
+ overflow: hidden;
673
+ text-overflow: ellipsis;
674
+ white-space: nowrap;
675
+ font-size: var(--text-base);
676
+ min-width: 0;
677
+ }
678
+
679
+ .layer-name--group {
680
+ font-weight: 600;
681
+ }
682
+
683
+ /* Inline rename input */
684
+ .rename-input {
685
+ flex: 1;
686
+ background: var(--bg-primary);
687
+ border: 1px solid var(--accent);
688
+ border-radius: var(--radius-sm);
689
+ color: var(--text-primary);
690
+ font-size: var(--text-base);
691
+ padding: 1px var(--space-2);
692
+ height: 22px;
693
+ min-width: 0;
694
+ }
695
+
696
+ /* Blend mode dropdown */
697
+ .blend-select {
698
+ flex-shrink: 0;
699
+ width: 56px;
700
+ height: 18px;
701
+ font-size: var(--text-xs);
702
+ background: var(--bg-surface);
703
+ color: var(--text-primary);
704
+ border: 1px solid var(--border);
705
+ border-radius: var(--radius-sm);
706
+ padding: 0 2px;
707
+ cursor: pointer;
708
+ appearance: none;
709
+ -webkit-appearance: none;
710
+ /* Tiny dropdown arrow via inline SVG */
711
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 3L4 5.5L6.5 3' stroke='%23999' fill='none' stroke-width='1.2'/%3E%3C/svg%3E");
712
+ background-repeat: no-repeat;
713
+ background-position: right 2px center;
714
+ padding-right: 12px;
715
+ }
716
+
717
+ .blend-select:hover {
718
+ border-color: var(--accent);
719
+ }
720
+
721
+ .blend-select:focus {
722
+ outline: 1px solid var(--accent);
723
+ outline-offset: -1px;
724
+ }
725
+
726
+ .layer-row--active .blend-select {
727
+ background: rgba(255, 255, 255, 0.15);
728
+ color: #ffffff;
729
+ border-color: rgba(255, 255, 255, 0.3);
730
+ }
731
+
732
+ /* Opacity slider */
733
+ .opacity-slider {
734
+ width: 40px;
735
+ height: 12px;
736
+ flex-shrink: 0;
737
+ accent-color: var(--accent);
738
+ cursor: pointer;
739
+ }
740
+
741
+ .opacity-value {
742
+ width: 22px;
743
+ text-align: right;
744
+ font-size: var(--text-xs);
745
+ color: var(--text-secondary);
746
+ flex-shrink: 0;
747
+ }
748
+
749
+ .layer-row--active .opacity-value {
750
+ color: rgba(255, 255, 255, 0.7);
751
+ }
752
+
753
+ /* Lock button */
754
+ .lock-btn {
755
+ background: none;
756
+ border: none;
757
+ color: inherit;
758
+ cursor: pointer;
759
+ width: 18px;
760
+ height: 18px;
761
+ display: flex;
762
+ align-items: center;
763
+ justify-content: center;
764
+ padding: 0;
765
+ flex-shrink: 0;
766
+ }
767
+
768
+ .lock-btn:hover {
769
+ color: var(--accent);
770
+ }
771
+
772
+ .layer-row--active .lock-btn:hover {
773
+ color: #ffffff;
774
+ }
775
+
776
+ /* SVG icons */
777
+ .icon {
778
+ display: block;
779
+ }
780
+
781
+ .action-btn :global(svg) {
782
+ width: 14px;
783
+ height: 14px;
784
+ }
785
+
786
+ .expand-btn :global(svg) {
787
+ width: 12px;
788
+ height: 12px;
789
+ }
790
+
791
+ /* Layer thumbnail preview */
792
+ .layer-thumb {
793
+ width: 24px;
794
+ height: 24px;
795
+ flex-shrink: 0;
796
+ border: 1px solid var(--border);
797
+ border-radius: var(--radius-sm);
798
+ image-rendering: pixelated;
799
+ background: var(--bg-canvas);
800
+ }
801
+
802
+ .layer-thumb--group {
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ color: var(--text-secondary);
807
+ background: none;
808
+ border-color: transparent;
809
+ }
810
+ </style>