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,761 @@
1
+ <!--
2
+ ColorPicker -- right sidebar panel for picking and managing colors.
3
+
4
+ Four sections:
5
+ 1. Foreground/Background swatches (Photoshop-style overlapping squares)
6
+ 2. HSV saturation-value square + hue bar (canvas-rendered)
7
+ 3. Hex/RGB text inputs
8
+ 4. Palette grid or recent colors
9
+ -->
10
+ <script lang="ts">
11
+ import {
12
+ getForegroundColor,
13
+ setForegroundColor,
14
+ getBackgroundColor,
15
+ setBackgroundColor,
16
+ getRecentColors,
17
+ addRecentColor,
18
+ } from '../color/color-state.svelte.js';
19
+ import {
20
+ hexToRgb,
21
+ rgbToHex,
22
+ rgbToHsv,
23
+ hsvToRgb,
24
+ } from '../color/color-utils.js';
25
+ import {
26
+ getActivePalette,
27
+ } from '../color/palette-state.svelte.js';
28
+ import { dispatch } from '../core/dispatcher.js';
29
+ import ArrowsIcon from '~icons/lucide/arrow-right-left';
30
+ import RotateCcwIcon from '~icons/lucide/rotate-ccw';
31
+
32
+ // --- Constants ---
33
+
34
+ const HUE_HEIGHT = 20;
35
+
36
+ // Internal padding of .color-picker (must match CSS)
37
+ const CONTAINER_PADDING = 8;
38
+
39
+ // --- State ---
40
+
41
+ // Which color is being edited: foreground or background
42
+ let editTarget = $state<'fg' | 'bg'>('fg');
43
+
44
+ // Container element ref for ResizeObserver
45
+ let containerEl = $state<HTMLDivElement | null>(null);
46
+
47
+ // Tracks the container's width; updated by ResizeObserver
48
+ let containerWidth = $state(0);
49
+
50
+ // Responsive dimensions derived from container width
51
+ // svWidth fills the container minus padding on both sides
52
+ let svWidth = $derived(Math.max(100, containerWidth - CONTAINER_PADDING * 2));
53
+ let svHeight = $derived(Math.round(svWidth * 0.85));
54
+ let hueWidth = $derived(svWidth);
55
+
56
+ // Canvas element references
57
+ let svCanvas = $state<HTMLCanvasElement | null>(null);
58
+ let hueCanvas = $state<HTMLCanvasElement | null>(null);
59
+
60
+ // Whether the user is currently dragging on the SV area or hue bar
61
+ let draggingSV = $state(false);
62
+ let draggingHue = $state(false);
63
+
64
+ // The hex color before the current drag started (for command dispatch)
65
+ let dragStartColor = $state('');
66
+
67
+ // Internal HSV state -- kept in sync with the active color via $effect,
68
+ // but updated directly during drag for responsive preview.
69
+ let internalH = $state(0);
70
+ let internalS = $state(0);
71
+ let internalV = $state(100);
72
+
73
+ // Hex input field value (may be temporarily invalid while typing).
74
+ // Seeded lazily from activeColor so it stays in sync with the swatch,
75
+ // but writable so the input onchange/typing can override it.
76
+ // The rule wants writable-$derived to replace the $state + $effect pair.
77
+ // eslint-disable-next-line svelte/prefer-writable-derived -- hexInputValue diverges during typing
78
+ let hexInputValue = $state('');
79
+
80
+ // --- Derived values ---
81
+
82
+ let activeColor = $derived(editTarget === 'fg' ? getForegroundColor() : getBackgroundColor());
83
+
84
+ let activeRgb = $derived(hexToRgb(activeColor));
85
+
86
+ // Derive HSV from the active color, but only when NOT dragging
87
+ // (during drag, internalH/S/V are updated directly)
88
+ let activeHsv = $derived(rgbToHsv(activeRgb.r, activeRgb.g, activeRgb.b));
89
+
90
+ let palette = $derived(getActivePalette());
91
+ let recentColors = $derived(getRecentColors());
92
+
93
+ // --- Helpers ---
94
+
95
+ function setActiveColor(hex: string): void {
96
+ if (editTarget === 'fg') {
97
+ setForegroundColor(hex);
98
+ } else {
99
+ setBackgroundColor(hex);
100
+ }
101
+ }
102
+
103
+ /** Dispatch a command for undo/redo support. Called on pointer-up after a drag. */
104
+ function dispatchColorCommand(newHex: string): void {
105
+ const commandType = editTarget === 'fg' ? 'set_foreground_color' : 'set_background_color';
106
+ dispatch({
107
+ type: commandType,
108
+ plugin: 'ui/color',
109
+ version: '1.0.0',
110
+ params: { color: newHex },
111
+ id: crypto.randomUUID(),
112
+ timestamp: Date.now(),
113
+ });
114
+ addRecentColor(newHex);
115
+ }
116
+
117
+ /** Convert internal HSV to hex string */
118
+ function hsvToHex(h: number, s: number, v: number): string {
119
+ const rgb = hsvToRgb(h, s, v);
120
+ return rgbToHex(rgb.r, rgb.g, rgb.b);
121
+ }
122
+
123
+ // --- Canvas drawing ---
124
+
125
+ function drawSVSquare(ctx: CanvasRenderingContext2D, hue: number, width: number, height: number): void {
126
+ const hueColor = hsvToRgb(hue, 100, 100);
127
+ // Horizontal gradient: white -> full-saturation hue color
128
+ const gradH = ctx.createLinearGradient(0, 0, width, 0);
129
+ gradH.addColorStop(0, '#ffffff');
130
+ gradH.addColorStop(1, `rgb(${String(hueColor.r)}, ${String(hueColor.g)}, ${String(hueColor.b)})`);
131
+ ctx.fillStyle = gradH;
132
+ ctx.fillRect(0, 0, width, height);
133
+ // Vertical gradient overlay: transparent -> black
134
+ const gradV = ctx.createLinearGradient(0, 0, 0, height);
135
+ gradV.addColorStop(0, 'rgba(0,0,0,0)');
136
+ gradV.addColorStop(1, 'rgba(0,0,0,1)');
137
+ ctx.fillStyle = gradV;
138
+ ctx.fillRect(0, 0, width, height);
139
+ }
140
+
141
+ function drawHueBar(ctx: CanvasRenderingContext2D, width: number, height: number): void {
142
+ const grad = ctx.createLinearGradient(0, 0, width, 0);
143
+ for (let i = 0; i <= 6; i++) {
144
+ const hue = i * 60;
145
+ const { r, g, b } = hsvToRgb(hue, 100, 100);
146
+ grad.addColorStop(i / 6, `rgb(${String(r)},${String(g)},${String(b)})`);
147
+ }
148
+ ctx.fillStyle = grad;
149
+ ctx.fillRect(0, 0, width, height);
150
+ }
151
+
152
+ // --- Effects ---
153
+
154
+ // Sync internal HSV from external color changes (when not dragging)
155
+ $effect(() => {
156
+ if (!draggingSV && !draggingHue) {
157
+ internalH = activeHsv.h;
158
+ internalS = activeHsv.s;
159
+ internalV = activeHsv.v;
160
+ }
161
+ });
162
+
163
+ // Sync hex input when the active color changes (when not focused)
164
+ $effect(() => {
165
+ hexInputValue = activeColor;
166
+ });
167
+
168
+ // Redraw SV square when hue, canvas ref, or dimensions change
169
+ $effect(() => {
170
+ if (!svCanvas || svWidth <= 0 || svHeight <= 0) return;
171
+ const ctx = svCanvas.getContext('2d');
172
+ if (!ctx) return;
173
+ drawSVSquare(ctx, internalH, svWidth, svHeight);
174
+ });
175
+
176
+ // Redraw hue bar when canvas ref or width changes
177
+ $effect(() => {
178
+ if (!hueCanvas || hueWidth <= 0) return;
179
+ const ctx = hueCanvas.getContext('2d');
180
+ if (!ctx) return;
181
+ drawHueBar(ctx, hueWidth, HUE_HEIGHT);
182
+ });
183
+
184
+ // ResizeObserver to track container width
185
+ $effect(() => {
186
+ if (!containerEl) return;
187
+ const observer = new ResizeObserver((entries) => {
188
+ for (const entry of entries) {
189
+ containerWidth = entry.contentBoxSize[0].inlineSize;
190
+ }
191
+ });
192
+ observer.observe(containerEl);
193
+ return () => { observer.disconnect(); };
194
+ });
195
+
196
+ // --- SV square pointer handling ---
197
+
198
+ function clamp(val: number, min: number, max: number): number {
199
+ return Math.min(max, Math.max(min, val));
200
+ }
201
+
202
+ function svFromPointer(e: PointerEvent): { s: number; v: number } {
203
+ if (!svCanvas) return { s: 0, v: 0 };
204
+ const rect = svCanvas.getBoundingClientRect();
205
+ const x = clamp(e.clientX - rect.left, 0, svWidth);
206
+ const y = clamp(e.clientY - rect.top, 0, svHeight);
207
+ return {
208
+ s: Math.round((x / svWidth) * 100),
209
+ v: Math.round((1 - y / svHeight) * 100),
210
+ };
211
+ }
212
+
213
+ function onSVPointerDown(e: PointerEvent): void {
214
+ if (e.button !== 0) return;
215
+ if (!svCanvas) return;
216
+ draggingSV = true;
217
+ dragStartColor = activeColor;
218
+ svCanvas.setPointerCapture(e.pointerId);
219
+ const { s, v } = svFromPointer(e);
220
+ internalS = s;
221
+ internalV = v;
222
+ setActiveColor(hsvToHex(internalH, s, v));
223
+ }
224
+
225
+ function onSVPointerMove(e: PointerEvent): void {
226
+ if (!draggingSV) return;
227
+ const { s, v } = svFromPointer(e);
228
+ internalS = s;
229
+ internalV = v;
230
+ // Live preview: set color directly (no command dispatch)
231
+ setActiveColor(hsvToHex(internalH, s, v));
232
+ }
233
+
234
+ function onSVPointerUp(_e: PointerEvent): void {
235
+ if (!draggingSV) return;
236
+ draggingSV = false;
237
+ const finalHex = hsvToHex(internalH, internalS, internalV);
238
+ // Restore to pre-drag color, then dispatch command so undo works correctly
239
+ setActiveColor(dragStartColor);
240
+ dispatchColorCommand(finalHex);
241
+ }
242
+
243
+ // --- Hue bar pointer handling ---
244
+
245
+ function hueFromPointer(e: PointerEvent): number {
246
+ if (!hueCanvas) return 0;
247
+ const rect = hueCanvas.getBoundingClientRect();
248
+ const x = clamp(e.clientX - rect.left, 0, hueWidth);
249
+ return Math.round((x / hueWidth) * 360);
250
+ }
251
+
252
+ function onHuePointerDown(e: PointerEvent): void {
253
+ if (e.button !== 0) return;
254
+ if (!hueCanvas) return;
255
+ draggingHue = true;
256
+ dragStartColor = activeColor;
257
+ hueCanvas.setPointerCapture(e.pointerId);
258
+ const h = hueFromPointer(e);
259
+ internalH = h;
260
+ setActiveColor(hsvToHex(h, internalS, internalV));
261
+ }
262
+
263
+ function onHuePointerMove(e: PointerEvent): void {
264
+ if (!draggingHue) return;
265
+ const h = hueFromPointer(e);
266
+ internalH = h;
267
+ setActiveColor(hsvToHex(h, internalS, internalV));
268
+ }
269
+
270
+ function onHuePointerUp(_e: PointerEvent): void {
271
+ if (!draggingHue) return;
272
+ draggingHue = false;
273
+ const finalHex = hsvToHex(internalH, internalS, internalV);
274
+ setActiveColor(dragStartColor);
275
+ dispatchColorCommand(finalHex);
276
+ }
277
+
278
+ // --- Hex input handling ---
279
+
280
+ function onHexInput(e: Event): void {
281
+ hexInputValue = (e.target as HTMLInputElement).value;
282
+ }
283
+
284
+ function applyHexInput(): void {
285
+ let val = hexInputValue.trim();
286
+ if (!val.startsWith('#')) val = '#' + val;
287
+ // Validate: must be 6-digit hex
288
+ if (/^#[0-9a-fA-F]{6}$/.test(val)) {
289
+ const normalized = val.toUpperCase();
290
+ dispatchColorCommand(normalized);
291
+ } else {
292
+ // Revert to current color on invalid input
293
+ hexInputValue = activeColor;
294
+ }
295
+ }
296
+
297
+ function onHexKeyDown(e: KeyboardEvent): void {
298
+ if (e.key === 'Enter') {
299
+ applyHexInput();
300
+ (e.target as HTMLInputElement).blur();
301
+ }
302
+ }
303
+
304
+ // --- RGB input handling ---
305
+
306
+ function onRgbChange(channel: 'r' | 'g' | 'b', value: string): void {
307
+ const num = parseInt(value, 10);
308
+ if (isNaN(num)) return;
309
+ const clamped = clamp(num, 0, 255);
310
+ const rgb = hexToRgb(activeColor);
311
+ rgb[channel] = clamped;
312
+ const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
313
+ dispatchColorCommand(hex);
314
+ }
315
+
316
+ // --- Swatch click ---
317
+
318
+ function onSwatchClick(hex: string): void {
319
+ dispatchColorCommand(hex);
320
+ }
321
+
322
+ // --- Reset to defaults ---
323
+
324
+ function resetDefaults(): void {
325
+ // Set fg to black, bg to white via commands for undo support
326
+ if (getForegroundColor() !== '#000000') {
327
+ dispatch({
328
+ type: 'set_foreground_color',
329
+ plugin: 'ui/color',
330
+ version: '1.0.0',
331
+ params: { color: '#000000' },
332
+ id: crypto.randomUUID(),
333
+ timestamp: Date.now(),
334
+ });
335
+ }
336
+ if (getBackgroundColor() !== '#FFFFFF') {
337
+ dispatch({
338
+ type: 'set_background_color',
339
+ plugin: 'ui/color',
340
+ version: '1.0.0',
341
+ params: { color: '#FFFFFF' },
342
+ id: crypto.randomUUID(),
343
+ timestamp: Date.now(),
344
+ });
345
+ }
346
+ }
347
+
348
+ // --- Swap colors ---
349
+
350
+ function onSwapColors(): void {
351
+ dispatch({
352
+ type: 'swap_colors',
353
+ plugin: 'ui/color',
354
+ version: '1.0.0',
355
+ params: {},
356
+ id: crypto.randomUUID(),
357
+ timestamp: Date.now(),
358
+ });
359
+ }
360
+
361
+ // --- SV marker position ---
362
+
363
+ let svMarkerX = $derived((internalS / 100) * svWidth);
364
+ let svMarkerY = $derived((1 - internalV / 100) * svHeight);
365
+
366
+ // --- Hue marker position ---
367
+
368
+ let hueMarkerX = $derived((internalH / 360) * hueWidth);
369
+ </script>
370
+
371
+ <div class="color-picker" bind:this={containerEl}>
372
+ <!-- Section 1: Foreground / Background swatches -->
373
+ <div class="swatch-section">
374
+ <div class="swatch-pair">
375
+ <!-- Background swatch (behind) -->
376
+ <button
377
+ class="swatch bg-swatch"
378
+ class:active={editTarget === 'bg'}
379
+ style:background-color={getBackgroundColor()}
380
+ title="Background color"
381
+ aria-label="Background color"
382
+ onclick={() => (editTarget = 'bg')}
383
+ ></button>
384
+ <!-- Foreground swatch (in front) -->
385
+ <button
386
+ class="swatch fg-swatch"
387
+ class:active={editTarget === 'fg'}
388
+ style:background-color={getForegroundColor()}
389
+ title="Foreground color"
390
+ aria-label="Foreground color"
391
+ onclick={() => (editTarget = 'fg')}
392
+ ></button>
393
+ </div>
394
+ <div class="swatch-controls">
395
+ <button class="mini-btn" title="Swap colors (X)" aria-label="Swap colors" onclick={onSwapColors}>
396
+ <ArrowsIcon />
397
+ </button>
398
+ <button class="mini-btn" title="Reset to defaults" aria-label="Reset to defaults" onclick={resetDefaults}>
399
+ <RotateCcwIcon />
400
+ </button>
401
+ </div>
402
+ </div>
403
+
404
+ <!-- Section 2: HSV color area -->
405
+ <div class="hsv-section">
406
+ <!-- Saturation-Value square -->
407
+ <div class="sv-container">
408
+ <canvas
409
+ bind:this={svCanvas}
410
+ width={svWidth}
411
+ height={svHeight}
412
+ class="sv-canvas"
413
+ onpointerdown={onSVPointerDown}
414
+ onpointermove={onSVPointerMove}
415
+ onpointerup={onSVPointerUp}
416
+ ></canvas>
417
+ <!-- Crosshair marker -->
418
+ <div
419
+ class="sv-marker"
420
+ style:left="{svMarkerX}px"
421
+ style:top="{svMarkerY}px"
422
+ ></div>
423
+ </div>
424
+ <!-- Hue bar -->
425
+ <div class="hue-container">
426
+ <canvas
427
+ bind:this={hueCanvas}
428
+ width={hueWidth}
429
+ height={HUE_HEIGHT}
430
+ class="hue-canvas"
431
+ onpointerdown={onHuePointerDown}
432
+ onpointermove={onHuePointerMove}
433
+ onpointerup={onHuePointerUp}
434
+ ></canvas>
435
+ <!-- Hue marker -->
436
+ <div
437
+ class="hue-marker"
438
+ style:left="{hueMarkerX}px"
439
+ ></div>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Section 3: Hex / RGB inputs -->
444
+ <div class="input-section">
445
+ <div class="hex-row">
446
+ <label class="input-label" for="hex-input">#</label>
447
+ <input
448
+ id="hex-input"
449
+ class="hex-input"
450
+ type="text"
451
+ maxlength="7"
452
+ value={hexInputValue.replace('#', '')}
453
+ oninput={onHexInput}
454
+ onblur={applyHexInput}
455
+ onkeydown={onHexKeyDown}
456
+ />
457
+ </div>
458
+ <div class="rgb-row">
459
+ {#each ['r', 'g', 'b'] as channel (channel)}
460
+ <div class="rgb-field">
461
+ <label class="input-label" for="rgb-{channel}">{channel.toUpperCase()}</label>
462
+ <input
463
+ id="rgb-{channel}"
464
+ class="rgb-input"
465
+ type="number"
466
+ min="0"
467
+ max="255"
468
+ value={activeRgb[channel as 'r' | 'g' | 'b']}
469
+ onchange={(e) => { onRgbChange(channel as 'r' | 'g' | 'b', (e.target as HTMLInputElement).value); }}
470
+ />
471
+ </div>
472
+ {/each}
473
+ </div>
474
+ </div>
475
+
476
+ <!-- Section 4: Palette grid or recent colors -->
477
+ <div class="palette-section">
478
+ {#if palette}
479
+ <div class="palette-name">{palette.name}</div>
480
+ <div class="palette-grid">
481
+ {#each palette.colors as color, i (i)}
482
+ <button
483
+ class="palette-swatch"
484
+ style:background-color={color}
485
+ title={color}
486
+ aria-label="Select color {color}"
487
+ onclick={() => { onSwatchClick(color); }}
488
+ ></button>
489
+ {/each}
490
+ </div>
491
+ {:else if recentColors.length > 0}
492
+ <div class="palette-name">Recent Colors</div>
493
+ <div class="palette-grid">
494
+ {#each recentColors as color, i (i)}
495
+ <button
496
+ class="palette-swatch"
497
+ style:background-color={color}
498
+ title={color}
499
+ aria-label="Select color {color}"
500
+ onclick={() => { onSwatchClick(color); }}
501
+ ></button>
502
+ {/each}
503
+ </div>
504
+ {:else}
505
+ <div class="palette-name empty">No palette or recent colors</div>
506
+ {/if}
507
+ </div>
508
+ </div>
509
+
510
+ <style>
511
+ .color-picker {
512
+ width: 100%;
513
+ display: flex;
514
+ flex-direction: column;
515
+ gap: 10px;
516
+ padding: 8px;
517
+ user-select: none;
518
+ }
519
+
520
+ /* --- Section 1: Swatches --- */
521
+
522
+ .swatch-section {
523
+ display: flex;
524
+ align-items: flex-end;
525
+ gap: 8px;
526
+ }
527
+
528
+ .swatch-pair {
529
+ position: relative;
530
+ width: 60px;
531
+ height: 60px;
532
+ }
533
+
534
+ .swatch {
535
+ position: absolute;
536
+ width: 40px;
537
+ height: 40px;
538
+ border: 2px solid var(--border);
539
+ border-radius: var(--radius-sm);
540
+ cursor: pointer;
541
+ padding: 0;
542
+ }
543
+
544
+ .swatch.active {
545
+ border-color: var(--accent);
546
+ z-index: 2;
547
+ }
548
+
549
+ .bg-swatch {
550
+ right: 0;
551
+ bottom: 0;
552
+ }
553
+
554
+ .fg-swatch {
555
+ left: 0;
556
+ top: 0;
557
+ z-index: 1;
558
+ }
559
+
560
+ .swatch-controls {
561
+ display: flex;
562
+ flex-direction: column;
563
+ gap: 2px;
564
+ }
565
+
566
+ .mini-btn {
567
+ width: 20px;
568
+ height: 20px;
569
+ border: 1px solid var(--border);
570
+ border-radius: var(--radius-sm);
571
+ background: var(--bg-panel);
572
+ color: var(--text-secondary);
573
+ cursor: pointer;
574
+ display: flex;
575
+ align-items: center;
576
+ justify-content: center;
577
+ font-size: var(--text-sm);
578
+ padding: 0;
579
+ line-height: 1;
580
+ }
581
+
582
+ .mini-btn:hover {
583
+ background: var(--bg-secondary);
584
+ color: var(--text-primary);
585
+ }
586
+
587
+ .mini-btn :global(svg) {
588
+ width: 12px;
589
+ height: 12px;
590
+ }
591
+
592
+ /* --- Section 2: HSV area --- */
593
+
594
+ .hsv-section {
595
+ display: flex;
596
+ flex-direction: column;
597
+ gap: 6px;
598
+ }
599
+
600
+ .sv-container {
601
+ position: relative;
602
+ width: 100%;
603
+ cursor: crosshair;
604
+ }
605
+
606
+ .sv-canvas {
607
+ display: block;
608
+ border: 1px solid var(--border);
609
+ /* Prevent sub-pixel scaling artifacts */
610
+ image-rendering: pixelated;
611
+ }
612
+
613
+ .sv-marker {
614
+ position: absolute;
615
+ width: 10px;
616
+ height: 10px;
617
+ border: 2px solid #fff;
618
+ border-radius: 50%;
619
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
620
+ transform: translate(-50%, -50%);
621
+ pointer-events: none;
622
+ }
623
+
624
+ .hue-container {
625
+ position: relative;
626
+ width: 100%;
627
+ height: 20px;
628
+ cursor: crosshair;
629
+ }
630
+
631
+ .hue-canvas {
632
+ display: block;
633
+ border: 1px solid var(--border);
634
+ image-rendering: pixelated;
635
+ }
636
+
637
+ .hue-marker {
638
+ position: absolute;
639
+ top: -1px;
640
+ width: 4px;
641
+ height: 22px;
642
+ border: 1px solid #fff;
643
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
644
+ border-radius: 1px;
645
+ transform: translateX(-50%);
646
+ pointer-events: none;
647
+ }
648
+
649
+ /* --- Section 3: Hex/RGB inputs --- */
650
+
651
+ .input-section {
652
+ display: flex;
653
+ flex-direction: column;
654
+ gap: 4px;
655
+ }
656
+
657
+ .hex-row {
658
+ display: flex;
659
+ align-items: center;
660
+ gap: 2px;
661
+ }
662
+
663
+ .input-label {
664
+ color: var(--text-secondary);
665
+ font-size: var(--text-sm);
666
+ font-family: var(--font-mono);
667
+ min-width: 12px;
668
+ text-align: center;
669
+ }
670
+
671
+ .hex-input {
672
+ flex: 1;
673
+ background: var(--bg-secondary);
674
+ border: 1px solid var(--border);
675
+ border-radius: var(--radius-sm);
676
+ color: var(--text-primary);
677
+ font-family: var(--font-mono);
678
+ font-size: var(--text-base);
679
+ padding: 2px 4px;
680
+ }
681
+
682
+ .hex-input:focus-visible {
683
+ border-color: var(--accent);
684
+ }
685
+
686
+ .rgb-row {
687
+ display: flex;
688
+ gap: 4px;
689
+ }
690
+
691
+ .rgb-field {
692
+ display: flex;
693
+ align-items: center;
694
+ gap: 2px;
695
+ flex: 1;
696
+ }
697
+
698
+ .rgb-input {
699
+ width: 100%;
700
+ background: var(--bg-secondary);
701
+ border: 1px solid var(--border);
702
+ border-radius: var(--radius-sm);
703
+ color: var(--text-primary);
704
+ font-family: var(--font-mono);
705
+ font-size: var(--text-sm);
706
+ padding: 2px 3px;
707
+ /* Hide spinner arrows for a cleaner look */
708
+ -moz-appearance: textfield;
709
+ appearance: textfield;
710
+ }
711
+
712
+ .rgb-input::-webkit-inner-spin-button,
713
+ .rgb-input::-webkit-outer-spin-button {
714
+ -webkit-appearance: none;
715
+ margin: 0;
716
+ }
717
+
718
+ .rgb-input:focus-visible {
719
+ border-color: var(--accent);
720
+ }
721
+
722
+ /* --- Section 4: Palette grid --- */
723
+
724
+ .palette-section {
725
+ display: flex;
726
+ flex-direction: column;
727
+ gap: 4px;
728
+ }
729
+
730
+ .palette-name {
731
+ font-size: var(--text-sm);
732
+ color: var(--text-secondary);
733
+ letter-spacing: 0.02em;
734
+ }
735
+
736
+ .palette-name.empty {
737
+ color: var(--text-muted);
738
+ font-style: italic;
739
+ }
740
+
741
+ .palette-grid {
742
+ display: flex;
743
+ flex-wrap: wrap;
744
+ gap: 2px;
745
+ }
746
+
747
+ .palette-swatch {
748
+ width: 16px;
749
+ height: 16px;
750
+ border: 1px solid var(--border);
751
+ border-radius: 1px;
752
+ cursor: pointer;
753
+ padding: 0;
754
+ flex-shrink: 0;
755
+ }
756
+
757
+ .palette-swatch:hover {
758
+ border-color: var(--accent);
759
+ transform: scale(1.15);
760
+ }
761
+ </style>