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,145 @@
1
+ <!--
2
+ StatusBar -- bottom bar showing at-a-glance editor state.
3
+ Displays tool, cursor, canvas size, zoom, frame info, color, and sync status.
4
+ -->
5
+ <script lang="ts">
6
+ import { canvasState } from '../canvas/canvas-state.svelte.js';
7
+ import { getActiveTool } from '../shortcuts/shortcut-state.svelte.js';
8
+ import { syncState } from '../sync/sync-state.svelte.js';
9
+ import { getCurrentFrameIndex, getFrames } from '../animation/frame-model.svelte.js';
10
+ import { getForegroundColor } from '../color/color-state.svelte.js';
11
+
12
+ // Cursor display: show coordinates when in bounds, dashes otherwise
13
+ let cursorText = $derived(
14
+ canvasState.cursorInBounds
15
+ ? `X: ${String(canvasState.cursorX)}, Y: ${String(canvasState.cursorY)}`
16
+ : 'X: --, Y: --'
17
+ );
18
+
19
+ let canvasSizeText = $derived(`${String(canvasState.canvasWidth)} x ${String(canvasState.canvasHeight)}`);
20
+ let zoomText = $derived(`${String(Math.round(canvasState.zoom * 100))}%`);
21
+ let toolName = $derived(getActiveTool() || 'none');
22
+
23
+ // Frame info: 1-indexed for display
24
+ let frameText = $derived.by(() => {
25
+ const allFrames = getFrames();
26
+ const total = allFrames.length || 1;
27
+ const current = getCurrentFrameIndex() + 1;
28
+ return `Frame ${String(current)}/${String(total)}`;
29
+ });
30
+
31
+ let fgColor = $derived(getForegroundColor());
32
+
33
+ // Sync status: derive label and CSS class from connection state
34
+ let syncLabel = $derived.by(() => {
35
+ if (syncState.connected) return 'Connected';
36
+ if (syncState.connecting || syncState.reconnecting) return 'Connecting...';
37
+ return 'Offline';
38
+ });
39
+
40
+ let syncClass = $derived.by(() => {
41
+ if (syncState.connected) return 'sync-connected';
42
+ if (syncState.connecting || syncState.reconnecting) return 'sync-connecting';
43
+ return 'sync-offline';
44
+ });
45
+
46
+ let syncTooltip = $derived(syncState.lastError ?? '');
47
+ </script>
48
+
49
+ <footer class="statusbar">
50
+ <div class="statusbar-left">
51
+ <span class="status-item">Tool: {toolName}</span>
52
+ <span class="status-item status-mono">{cursorText}</span>
53
+ <span class="status-divider"></span>
54
+ <span class="status-item status-mono">{canvasSizeText}</span>
55
+ <span class="status-item status-mono">{zoomText}</span>
56
+ </div>
57
+
58
+ <div class="statusbar-right">
59
+ <span class="status-item">{frameText}</span>
60
+ <span class="status-divider"></span>
61
+ <span class="status-item status-color">
62
+ <span class="color-swatch" style:background-color={fgColor}></span>
63
+ <span class="status-mono">{fgColor}</span>
64
+ </span>
65
+ <span class="status-divider"></span>
66
+ <span class="status-item status-sync {syncClass}" title={syncTooltip}>
67
+ <span class="sync-dot"></span>
68
+ {syncLabel}
69
+ </span>
70
+ </div>
71
+ </footer>
72
+
73
+ <style>
74
+ .statusbar {
75
+ height: 24px;
76
+ background: var(--bg-toolbar);
77
+ border-top: 1px solid var(--border);
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: space-between;
81
+ padding: 0 12px;
82
+ flex-shrink: 0;
83
+ font-size: var(--text-sm);
84
+ color: var(--text-secondary);
85
+ user-select: none;
86
+ }
87
+
88
+ .statusbar-left,
89
+ .statusbar-right {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 10px;
93
+ min-width: 0;
94
+ }
95
+
96
+ .status-item {
97
+ white-space: nowrap;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 4px;
101
+ }
102
+
103
+ .status-mono {
104
+ font-family: var(--font-mono);
105
+ }
106
+
107
+ /* Thin vertical divider between groups */
108
+ .status-divider {
109
+ width: 1px;
110
+ height: 12px;
111
+ background: var(--border);
112
+ flex-shrink: 0;
113
+ }
114
+
115
+ /* Foreground color swatch */
116
+ .color-swatch {
117
+ display: inline-block;
118
+ width: 12px;
119
+ height: 12px;
120
+ border: 1px solid var(--border);
121
+ border-radius: var(--radius-sm);
122
+ flex-shrink: 0;
123
+ }
124
+
125
+ /* Sync status dot */
126
+ .sync-dot {
127
+ display: inline-block;
128
+ width: 7px;
129
+ height: 7px;
130
+ border-radius: 50%;
131
+ flex-shrink: 0;
132
+ }
133
+
134
+ .sync-connected .sync-dot {
135
+ background: var(--success);
136
+ }
137
+
138
+ .sync-connecting .sync-dot {
139
+ background: var(--warning);
140
+ }
141
+
142
+ .sync-offline .sync-dot {
143
+ background: var(--error);
144
+ }
145
+ </style>
@@ -0,0 +1,488 @@
1
+ <!--
2
+ ToolbarPanel -- generic, registry-driven toolbar component.
3
+
4
+ Reads toolbar contributions from toolbarRegistry filtered by toolbarId,
5
+ groups and sorts them, and renders tool/command/separator/widget items.
6
+ Orientation is derived from container aspect ratio via ResizeObserver.
7
+ Always 1 item per row/column. Only iconSize is user-configurable.
8
+ -->
9
+ <script lang="ts">
10
+ import { toolbarRegistry, toolRegistry, commandRegistry } from '../core/registries.svelte.js';
11
+ import { executeOrDispatch, getCommandForDispatch } from '../core/command-runner.js';
12
+ import { getActiveTool, setActiveTool, getBindings } from '../shortcuts/shortcut-state.svelte.js';
13
+ import { getToolbarConfig } from './toolbar-config.js';
14
+ import { getToolOptionValue, setToolOptionValue, toggleToolOption } from '../core/tool-options-state.svelte.js';
15
+ import type { ToolbarContribution, ToolOption } from '../core/plugin-types.js';
16
+
17
+ interface Props {
18
+ toolbarId: string;
19
+ }
20
+
21
+ let { toolbarId }: Props = $props();
22
+
23
+ // Only iconSize is persisted; orientation is derived from container aspect ratio
24
+ let config = $derived(getToolbarConfig(toolbarId));
25
+ let effectiveIconSize = $derived(config.iconSize);
26
+
27
+ let containerEl: HTMLDivElement | undefined = $state();
28
+ let orientation: 'horizontal' | 'vertical' = $state('vertical');
29
+ let hoveredItem: string | null = $state(null);
30
+ let tooltipPos: { x: number; y: number } | null = $state(null);
31
+
32
+ /** Show tooltip positioned relative to the viewport using the button's bounding rect. */
33
+ function showTooltip(event: MouseEvent, itemName: string): void {
34
+ hoveredItem = itemName;
35
+ const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
36
+ if (orientation === 'vertical') {
37
+ tooltipPos = { x: rect.right + 8, y: rect.top + rect.height / 2 };
38
+ } else {
39
+ tooltipPos = { x: rect.left + rect.width / 2, y: rect.bottom + 8 };
40
+ }
41
+ }
42
+
43
+ function hideTooltip(): void {
44
+ hoveredItem = null;
45
+ tooltipPos = null;
46
+ }
47
+
48
+ /** Resolve display label and shortcut for the currently hovered item. */
49
+ let tooltipContent = $derived.by(() => {
50
+ if (!hoveredItem) return null;
51
+ for (const [, items] of groupedItems) {
52
+ for (const item of items) {
53
+ if (item.name === hoveredItem) {
54
+ const label = item.label ?? (item.targetId ? formatName(item.targetId) : '');
55
+ const shortcut = getShortcut(item);
56
+ return { label, shortcut };
57
+ }
58
+ }
59
+ }
60
+ return null;
61
+ });
62
+
63
+ // Derive orientation from container aspect ratio
64
+ $effect(() => {
65
+ if (!containerEl) return;
66
+ const observer = new ResizeObserver((entries) => {
67
+ const entry = entries[0];
68
+ if (!entry) return;
69
+ const { width, height } = entry.contentRect;
70
+ orientation = width > height ? 'horizontal' : 'vertical';
71
+ });
72
+ observer.observe(containerEl);
73
+ return () => { observer.disconnect(); };
74
+ });
75
+
76
+ // Augmented contribution with its registry key for identification
77
+ type NamedContribution = ToolbarContribution & { name: string };
78
+
79
+ /**
80
+ * Collect contributions for this toolbar, group by `group` field,
81
+ * sort groups by minimum order, sort items within groups by order.
82
+ */
83
+ let groupedItems = $derived.by(() => {
84
+ const all: NamedContribution[] = [...toolbarRegistry.getAll().entries()]
85
+ .map(([name, contrib]) => ({ name, ...contrib }))
86
+ .filter((item) => item.toolbarId === toolbarId);
87
+
88
+ // Bucket items by group
89
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient grouping inside a $derived
90
+ const groups = new Map<string, NamedContribution[]>();
91
+ for (const item of all) {
92
+ const list = groups.get(item.group) ?? [];
93
+ list.push(item);
94
+ groups.set(item.group, list);
95
+ }
96
+
97
+ // Sort items within each group by order
98
+ for (const list of groups.values()) {
99
+ list.sort((a, b) => a.order - b.order);
100
+ }
101
+
102
+ // Sort groups by the minimum order of their items
103
+ const sortedGroups = [...groups.entries()].sort(
104
+ (a, b) => Math.min(...a[1].map((i) => i.order)) - Math.min(...b[1].map((i) => i.order))
105
+ );
106
+
107
+ return sortedGroups;
108
+ });
109
+
110
+ /**
111
+ * Resolve the icon for a toolbar item. Priority:
112
+ * 1. Contribution-level icon override
113
+ * 2. Tool or command registry icon
114
+ */
115
+ function resolveIcon(item: NamedContribution): string | import('svelte').Component | undefined {
116
+ if (item.icon) return item.icon;
117
+ if (item.kind === 'tool' && item.targetId) {
118
+ return toolRegistry.get(item.targetId)?.icon;
119
+ }
120
+ if (item.kind === 'command' && item.targetId) {
121
+ return commandRegistry.get(item.targetId)?.icon;
122
+ }
123
+ return undefined;
124
+ }
125
+
126
+ /**
127
+ * Format a targetId for display: replace hyphens with spaces,
128
+ * capitalize first letter of each word.
129
+ */
130
+ function formatName(name: string): string {
131
+ return name
132
+ .split(/[-_]/)
133
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
134
+ .join(' ');
135
+ }
136
+
137
+ /**
138
+ * Build a tooltip string for an item: display name + keyboard shortcut.
139
+ */
140
+ function getTooltip(item: NamedContribution): string {
141
+ const label = item.label ?? (item.targetId ? formatName(item.targetId) : '');
142
+ let shortcut = '';
143
+ if (item.kind === 'tool' && item.targetId) {
144
+ shortcut = getBindings().get(`tool.${item.targetId}`)?.key ?? '';
145
+ } else if (item.kind === 'command' && item.targetId) {
146
+ shortcut = commandRegistry.get(item.targetId)?.defaultShortcut ?? '';
147
+ }
148
+ return label + (shortcut ? ` (${shortcut})` : '');
149
+ }
150
+
151
+ /**
152
+ * Get the keyboard shortcut string for an item (for the <kbd> element).
153
+ */
154
+ function getShortcut(item: NamedContribution): string {
155
+ if (item.kind === 'tool' && item.targetId) {
156
+ return getBindings().get(`tool.${item.targetId}`)?.key ?? '';
157
+ }
158
+ if (item.kind === 'command' && item.targetId) {
159
+ return commandRegistry.get(item.targetId)?.defaultShortcut ?? '';
160
+ }
161
+ return '';
162
+ }
163
+
164
+ /**
165
+ * Handle tool selection: deactivate previous tool, activate new one.
166
+ */
167
+ function selectTool(targetId: string): void {
168
+ const prevName = getActiveTool();
169
+ if (prevName === targetId) return;
170
+
171
+ // Deactivate previous tool if it exists
172
+ if (prevName) {
173
+ const prevTool = toolRegistry.get(prevName);
174
+ prevTool?.onDeactivate?.();
175
+ }
176
+
177
+ setActiveTool(targetId);
178
+ toolRegistry.get(targetId)?.onActivate?.();
179
+ }
180
+
181
+ /**
182
+ * Handle command execution.
183
+ */
184
+ function executeCommand(targetId: string): void {
185
+ const cmd = getCommandForDispatch(targetId);
186
+ if (cmd) executeOrDispatch(targetId, cmd);
187
+ }
188
+
189
+ /**
190
+ * Handle click for any item based on its kind.
191
+ */
192
+ function handleClick(item: NamedContribution): void {
193
+ if (item.kind === 'tool' && item.targetId) {
194
+ selectTool(item.targetId);
195
+ } else if (item.kind === 'command' && item.targetId) {
196
+ executeCommand(item.targetId);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get the declared options for a tool (if any).
202
+ * Returns an empty array when the tool has no options.
203
+ */
204
+ function getToolOptions(toolName: string): ToolOption[] {
205
+ return toolRegistry.get(toolName)?.options ?? [];
206
+ }
207
+ </script>
208
+
209
+ <div
210
+ class="toolbar-panel"
211
+ class:horizontal={orientation === 'horizontal'}
212
+ class:vertical={orientation === 'vertical'}
213
+ bind:this={containerEl}
214
+ role="toolbar"
215
+ aria-label="Toolbar"
216
+ aria-orientation={orientation}
217
+ style:--icon-size="{effectiveIconSize}px"
218
+ style:--btn-size="{effectiveIconSize + 16}px"
219
+ >
220
+ {#each groupedItems as [_groupName, items], groupIdx (groupIdx)}
221
+ {#if groupIdx > 0}
222
+ <div class="separator" role="separator"></div>
223
+ {/if}
224
+ {#each items as item (item.name)}
225
+ {#if item.kind === 'separator'}
226
+ <div class="separator" role="separator"></div>
227
+ {:else if item.kind === 'widget' && item.widget}
228
+ {@const Widget = item.widget}
229
+ <Widget />
230
+ {:else if item.kind === 'tool' || item.kind === 'command'}
231
+ {@const icon = resolveIcon(item)}
232
+ {@const active = item.kind === 'tool' && item.targetId
233
+ ? getActiveTool() === item.targetId
234
+ : false}
235
+ {@const displayLabel = item.label ?? (item.targetId ? formatName(item.targetId) : '')}
236
+ <button
237
+ class="toolbar-btn"
238
+ class:active
239
+ title={getTooltip(item)}
240
+ aria-pressed={item.kind === 'tool' ? active : undefined}
241
+ aria-label={displayLabel}
242
+ onmouseenter={(e) => { showTooltip(e, item.name); }}
243
+ onmouseleave={hideTooltip}
244
+ onclick={() => { handleClick(item); }}
245
+ >
246
+ <span class="icon-wrapper">
247
+ {#if icon}
248
+ {#if typeof icon === 'string'}
249
+ {icon}
250
+ {:else}
251
+ {@const IconComp = icon}
252
+ <IconComp />
253
+ {/if}
254
+ {/if}
255
+ </span>
256
+ </button>
257
+ <!-- Sub-buttons for tool options (shown only when this tool is active) -->
258
+ {#if active && item.kind === 'tool' && item.targetId}
259
+ {@const targetId = item.targetId}
260
+ {@const toolOptions = getToolOptions(targetId)}
261
+ {#each toolOptions as opt (opt.id)}
262
+ {#if opt.type === 'toggle'}
263
+ {@const optActive = getToolOptionValue(targetId, opt.id)}
264
+ <button
265
+ class="toolbar-btn sub-btn"
266
+ class:active={optActive}
267
+ title={opt.label}
268
+ aria-pressed={optActive}
269
+ aria-label={opt.label}
270
+ onclick={() => toggleToolOption(targetId, opt.id)}
271
+ >
272
+ <span class="icon-wrapper">
273
+ {#if opt.icon}
274
+ {#if typeof opt.icon === 'string'}
275
+ {opt.icon}
276
+ {:else}
277
+ {@const OptIcon = opt.icon}
278
+ <OptIcon />
279
+ {/if}
280
+ {/if}
281
+ </span>
282
+ </button>
283
+ {:else if opt.type === 'select'}
284
+ <!-- Row of choice buttons for select-type options -->
285
+ {@const currentValue = getToolOptionValue(targetId, opt.id)}
286
+ {#each opt.choices as choice (choice.value)}
287
+ <button
288
+ class="toolbar-btn sub-btn"
289
+ class:active={currentValue === choice.value}
290
+ title="{opt.label}: {choice.label}"
291
+ aria-pressed={currentValue === choice.value}
292
+ aria-label="{opt.label}: {choice.label}"
293
+ onclick={() => { setToolOptionValue(targetId, opt.id, choice.value); }}
294
+ >
295
+ <span class="icon-wrapper select-label">{choice.label}</span>
296
+ </button>
297
+ {/each}
298
+ {/if}
299
+ {/each}
300
+ {/if}
301
+ {/if}
302
+ {/each}
303
+ {/each}
304
+ </div>
305
+
306
+ {#if tooltipContent && tooltipPos}
307
+ <div
308
+ class="tooltip"
309
+ class:tooltip-vertical={orientation === 'vertical'}
310
+ class:tooltip-horizontal={orientation === 'horizontal'}
311
+ role="tooltip"
312
+ style:position="fixed"
313
+ style:left="{tooltipPos.x}px"
314
+ style:top="{tooltipPos.y}px"
315
+ >
316
+ {tooltipContent.label}{#if tooltipContent.shortcut}<kbd>{tooltipContent.shortcut}</kbd>{/if}
317
+ </div>
318
+ {/if}
319
+
320
+ <style>
321
+ /* --- Layout --- */
322
+
323
+ .toolbar-panel {
324
+ background: var(--bg-toolbar);
325
+ border: 1px solid var(--border);
326
+ padding: var(--space-2);
327
+ gap: 3px;
328
+ overflow: hidden;
329
+ flex-shrink: 0;
330
+ font-family: var(--font-ui);
331
+ }
332
+
333
+ .toolbar-panel {
334
+ display: flex;
335
+ }
336
+ .toolbar-panel.vertical {
337
+ flex-direction: column;
338
+ align-items: center;
339
+ }
340
+ .toolbar-panel.horizontal {
341
+ flex-direction: row;
342
+ align-items: center;
343
+ }
344
+
345
+ /* --- Buttons --- */
346
+
347
+ .toolbar-btn {
348
+ position: relative;
349
+ width: var(--btn-size);
350
+ height: var(--btn-size);
351
+ border: none;
352
+ border-radius: var(--radius-sm);
353
+ background: transparent;
354
+ color: var(--text-secondary);
355
+ cursor: pointer;
356
+ display: flex;
357
+ align-items: center;
358
+ justify-content: center;
359
+ padding: 0;
360
+ transition: background var(--transition-fast), color var(--transition-fast);
361
+ }
362
+
363
+ .toolbar-btn:hover:not(.active) {
364
+ background: var(--bg-secondary);
365
+ color: var(--text-primary);
366
+ }
367
+
368
+ .toolbar-btn.active {
369
+ background: var(--accent);
370
+ color: var(--text-on-accent, #fff);
371
+ }
372
+
373
+ /* Sub-buttons for tool options -- slightly smaller, visually subordinate */
374
+ .toolbar-btn.sub-btn {
375
+ width: calc(var(--btn-size) - 6px);
376
+ height: calc(var(--btn-size) - 6px);
377
+ border: 1px solid var(--border);
378
+ background: var(--bg-secondary);
379
+ color: var(--text-secondary);
380
+ }
381
+
382
+ .toolbar-btn.sub-btn:hover:not(.active) {
383
+ background: var(--bg-tertiary, var(--bg-secondary));
384
+ color: var(--text-primary);
385
+ border-color: var(--text-secondary);
386
+ }
387
+
388
+ .toolbar-btn.sub-btn.active {
389
+ background: var(--accent);
390
+ color: var(--text-on-accent, #fff);
391
+ border-color: var(--accent);
392
+ }
393
+
394
+ .toolbar-btn.sub-btn .icon-wrapper {
395
+ width: calc(var(--icon-size) - 2px);
396
+ height: calc(var(--icon-size) - 2px);
397
+ }
398
+
399
+ .toolbar-btn.sub-btn .icon-wrapper :global(svg) {
400
+ width: calc(var(--icon-size) - 2px);
401
+ height: calc(var(--icon-size) - 2px);
402
+ }
403
+
404
+ /* Text label inside select-type option buttons */
405
+ .toolbar-btn.sub-btn .select-label {
406
+ font-size: var(--text-sm, 11px);
407
+ font-weight: 600;
408
+ font-family: var(--font-ui);
409
+ }
410
+
411
+ /* --- Icon wrapper --- */
412
+
413
+ .icon-wrapper {
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ width: var(--icon-size);
418
+ height: var(--icon-size);
419
+ line-height: 1;
420
+ user-select: none;
421
+ color: inherit;
422
+ }
423
+
424
+ .icon-wrapper :global(svg) {
425
+ width: var(--icon-size);
426
+ height: var(--icon-size);
427
+ }
428
+
429
+ /* --- Separator --- */
430
+
431
+ .separator {
432
+ background: var(--border);
433
+ flex-shrink: 0;
434
+ }
435
+
436
+ .vertical > .separator {
437
+ /* Horizontal line in a vertical toolbar */
438
+ align-self: stretch;
439
+ height: 1px;
440
+ margin: var(--space-1) var(--space-2);
441
+ }
442
+
443
+ .horizontal > .separator {
444
+ /* Vertical line in a horizontal toolbar */
445
+ align-self: stretch;
446
+ width: 1px;
447
+ margin: var(--space-2) var(--space-1);
448
+ }
449
+
450
+ /* --- Tooltip (fixed-positioned, rendered outside toolbar-panel) --- */
451
+
452
+ .tooltip {
453
+ background: var(--bg-panel);
454
+ color: var(--text-primary);
455
+ border: 1px solid var(--border);
456
+ border-radius: var(--radius-md);
457
+ padding: var(--space-2) var(--space-3);
458
+ font-size: var(--text-base);
459
+ white-space: nowrap;
460
+ pointer-events: none;
461
+ z-index: 1000;
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 6px;
465
+ box-shadow: var(--shadow-sm);
466
+ }
467
+
468
+ /* Vertical toolbar: tooltip appears to the right, vertically centered */
469
+ .tooltip-vertical {
470
+ transform: translateY(-50%);
471
+ }
472
+
473
+ /* Horizontal toolbar: tooltip appears below, horizontally centered */
474
+ .tooltip-horizontal {
475
+ transform: translateX(-50%);
476
+ }
477
+
478
+ .tooltip kbd {
479
+ font-family: inherit;
480
+ font-size: var(--text-sm);
481
+ color: var(--text-secondary);
482
+ background: var(--bg-secondary);
483
+ border: 1px solid var(--border);
484
+ border-radius: var(--radius-sm);
485
+ padding: 1px 4px;
486
+ line-height: 1.2;
487
+ }
488
+ </style>