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.
- package/.env.development +1 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +18 -0
- package/.prettierignore +5 -0
- package/.prettierrc +16 -0
- package/.python-version +1 -0
- package/.rlsbl/bases/.github/workflows/ci.yml +21 -0
- package/.rlsbl/bases/.github/workflows/publish.yml +18 -0
- package/.rlsbl/bases/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/bases/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/bases/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/bases/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/bases/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/bases/CHANGELOG.md +5 -0
- package/.rlsbl/bases/LICENSE +21 -0
- package/.rlsbl/changes/.validated +1 -0
- package/.rlsbl/changes/0.1.0.jsonl +85 -0
- package/.rlsbl/changes/0.1.0.md +13 -0
- package/.rlsbl/changes/unreleased.jsonl +0 -0
- package/.rlsbl/config.json +6 -0
- package/.rlsbl/hashes.json +14 -0
- package/.rlsbl/hooks/post-release.sh +8 -0
- package/.rlsbl/hooks/pre-checks.sh +5 -0
- package/.rlsbl/hooks/pre-release.sh +8 -0
- package/.rlsbl/lint/go.toml +17 -0
- package/.rlsbl/lint/npm.toml +19 -0
- package/.rlsbl/lint/python.toml +25 -0
- package/.rlsbl/releases/unreleased.toml +0 -0
- package/.rlsbl/releases/v0.1.0.toml +3 -0
- package/.rlsbl/version +1 -0
- package/.selfdoc/hashes/hashes.json +146 -0
- package/.strictcli/schema.json +227 -0
- package/CHANGELOG.md +17 -0
- package/CLAUDE.md +100 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/assets/icon.png +0 -0
- package/docs/_README.md +117 -0
- package/docs/cli-config.md +35 -0
- package/docs/cli-dev.md +21 -0
- package/docs/cli-diagnose.md +12 -0
- package/docs/cli-index.md +30 -0
- package/docs/cli-list.md +18 -0
- package/docs/cli-mcp.md +18 -0
- package/docs/cli-new.md +26 -0
- package/docs/cli-open.md +21 -0
- package/docs/cli-serve.md +21 -0
- package/docs/cli-stop.md +12 -0
- package/docs/gen-index.md +36 -0
- package/docs/index.md +13 -0
- package/docs/server-src-pixelweaver-__main__.md +12 -0
- package/docs/server-src-pixelweaver-autosave.md +12 -0
- package/docs/server-src-pixelweaver-bridge.md +12 -0
- package/docs/server-src-pixelweaver-cli.md +12 -0
- package/docs/server-src-pixelweaver-config.md +12 -0
- package/docs/server-src-pixelweaver-connections.md +12 -0
- package/docs/server-src-pixelweaver-main.md +12 -0
- package/docs/server-src-pixelweaver-mcp_bridge.md +12 -0
- package/docs/server-src-pixelweaver-mcp_drawing_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_export_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_frame_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_history_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_layer_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_lock.md +12 -0
- package/docs/server-src-pixelweaver-mcp_project_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_read_tools.md +12 -0
- package/docs/server-src-pixelweaver-mcp_registry.md +12 -0
- package/docs/server-src-pixelweaver-mcp_resources.md +12 -0
- package/docs/server-src-pixelweaver-mcp_server.md +12 -0
- package/docs/server-src-pixelweaver-protocol.md +12 -0
- package/docs/server-src-pixelweaver-state.md +12 -0
- package/docs/server-src-pixelweaver-storage.md +12 -0
- package/docs/server-src-pixelweaver-websocket.md +12 -0
- package/docs/server-src-pixelweaver.md +12 -0
- package/e2e/app-launch.test.ts +35 -0
- package/e2e/menus.test.ts +26 -0
- package/e2e/tools.test.ts +27 -0
- package/e2e/undo-redo.test.ts +11 -0
- package/eslint.config.js +62 -0
- package/index.html +13 -0
- package/package.json +48 -0
- package/playwright.config.ts +19 -0
- package/plugins/builtin/.gitkeep +0 -0
- package/plugins/builtin/advanced-fill-tool.ts +146 -0
- package/plugins/builtin/circle-tool.ts +186 -0
- package/plugins/builtin/diamond-tool.ts +182 -0
- package/plugins/builtin/dither-tool.ts +186 -0
- package/plugins/builtin/drawing-primitives-plugin.ts +362 -0
- package/plugins/builtin/drawing-utils.test.ts +495 -0
- package/plugins/builtin/drawing-utils.ts +431 -0
- package/plugins/builtin/effects/blur.ts +97 -0
- package/plugins/builtin/effects/color-effects.ts +278 -0
- package/plugins/builtin/effects/flip.ts +83 -0
- package/plugins/builtin/effects/glow.ts +118 -0
- package/plugins/builtin/effects/outline.ts +110 -0
- package/plugins/builtin/effects/rotate.ts +357 -0
- package/plugins/builtin/effects/scale.ts +258 -0
- package/plugins/builtin/effects/shadow.ts +111 -0
- package/plugins/builtin/effects/sharpen.ts +102 -0
- package/plugins/builtin/effects.test.ts +715 -0
- package/plugins/builtin/eraser-tool.ts +23 -0
- package/plugins/builtin/eyedropper-tool.ts +83 -0
- package/plugins/builtin/fill-tool.ts +93 -0
- package/plugins/builtin/gradient-tool.ts +204 -0
- package/plugins/builtin/gradient.test.ts +142 -0
- package/plugins/builtin/importers/aseprite-importer-plugin.ts +174 -0
- package/plugins/builtin/importers/aseprite-parser.ts +497 -0
- package/plugins/builtin/importers/piskel-importer-plugin.ts +222 -0
- package/plugins/builtin/importers/sky-spec-plugin.ts +409 -0
- package/plugins/builtin/line-tool.ts +267 -0
- package/plugins/builtin/make-stroke-tool.ts +271 -0
- package/plugins/builtin/noise-dither.test.ts +151 -0
- package/plugins/builtin/noise-tool.ts +131 -0
- package/plugins/builtin/pattern-stamp-tool.ts +162 -0
- package/plugins/builtin/pencil-tool.ts +25 -0
- package/plugins/builtin/rect-tool.ts +179 -0
- package/plugins/builtin/selection-tool.ts +388 -0
- package/plugins/builtin/selection.test.ts +195 -0
- package/plugins/builtin/tool-plugins.test.ts +529 -0
- package/public/favicon.svg +7 -0
- package/public/sw.js +91 -0
- package/pyproject.toml +49 -0
- package/scripts/eslint-wave-a-list.sh +24 -0
- package/scripts/eslint-wave-a-status.sh +25 -0
- package/scripts/fix-index-signature-access.py +127 -0
- package/scripts/fix-unchecked-index.py +128 -0
- package/scripts/fix-unchecked-vars.py +127 -0
- package/scripts/fix-wave-a-bangs.py +36 -0
- package/scripts/fix-wave-a-templates.py +108 -0
- package/scripts/fix-wave-b-void-expr.py +167 -0
- package/scripts/generate-command-params.py +540 -0
- package/scripts/migrate-single-frame-to-multi.py +171 -0
- package/scripts/smoke-test.sh +77 -0
- package/selfdoc.json +10 -0
- package/server/README.md +0 -0
- package/server/src/pixelweaver/__init__.py +1 -0
- package/server/src/pixelweaver/__main__.py +4 -0
- package/server/src/pixelweaver/autosave.py +114 -0
- package/server/src/pixelweaver/bridge.py +127 -0
- package/server/src/pixelweaver/cli.py +199 -0
- package/server/src/pixelweaver/config.py +24 -0
- package/server/src/pixelweaver/connections.py +54 -0
- package/server/src/pixelweaver/main.py +271 -0
- package/server/src/pixelweaver/mcp_bridge.py +189 -0
- package/server/src/pixelweaver/mcp_drawing_tools.py +178 -0
- package/server/src/pixelweaver/mcp_export_tools.py +291 -0
- package/server/src/pixelweaver/mcp_frame_tools.py +423 -0
- package/server/src/pixelweaver/mcp_history_tools.py +106 -0
- package/server/src/pixelweaver/mcp_layer_tools.py +64 -0
- package/server/src/pixelweaver/mcp_lock.py +37 -0
- package/server/src/pixelweaver/mcp_project_tools.py +302 -0
- package/server/src/pixelweaver/mcp_read_tools.py +163 -0
- package/server/src/pixelweaver/mcp_registry.py +247 -0
- package/server/src/pixelweaver/mcp_resources.py +312 -0
- package/server/src/pixelweaver/mcp_server.py +234 -0
- package/server/src/pixelweaver/protocol.py +219 -0
- package/server/src/pixelweaver/state.py +267 -0
- package/server/src/pixelweaver/storage.py +293 -0
- package/server/src/pixelweaver/websocket.py +282 -0
- package/server/tests/__init__.py +0 -0
- package/server/tests/conftest.py +17 -0
- package/server/tests/test_api.py +96 -0
- package/server/tests/test_bridge.py +161 -0
- package/server/tests/test_health.py +9 -0
- package/server/tests/test_integration.py +86 -0
- package/server/tests/test_mcp_bridge.py +293 -0
- package/server/tests/test_mcp_lock.py +34 -0
- package/server/tests/test_mcp_registry.py +679 -0
- package/server/tests/test_mcp_resources.py +648 -0
- package/server/tests/test_protocol.py +125 -0
- package/server/tests/test_state.py +87 -0
- package/server/tests/test_storage.py +306 -0
- package/server/tests/test_websocket.py +275 -0
- package/src/App.svelte +107 -0
- package/src/app.css +215 -0
- package/src/lib/animation/AnimationPreviewPanel.svelte +667 -0
- package/src/lib/animation/animation-commands.test.ts +228 -0
- package/src/lib/animation/animation-commands.ts +540 -0
- package/src/lib/animation/animation-preview-panel-plugin.ts +25 -0
- package/src/lib/animation/animation-preview.svelte.ts +151 -0
- package/src/lib/animation/clipboard.ts +134 -0
- package/src/lib/animation/frame-model.svelte.ts +437 -0
- package/src/lib/animation/frame-model.test.ts +314 -0
- package/src/lib/animation/frame-selection.svelte.ts +77 -0
- package/src/lib/animation/frame-tags.svelte.ts +238 -0
- package/src/lib/animation/frame-tags.test.ts +136 -0
- package/src/lib/animation/import.test.ts +141 -0
- package/src/lib/animation/import.ts +112 -0
- package/src/lib/animation/spritesheet-export.test.ts +239 -0
- package/src/lib/animation/spritesheet-export.ts +314 -0
- package/src/lib/canvas/CanvasViewport.svelte +216 -0
- package/src/lib/canvas/canvas-init-plugin.ts +178 -0
- package/src/lib/canvas/canvas-renderer.ts +408 -0
- package/src/lib/canvas/canvas-state.svelte.ts +232 -0
- package/src/lib/canvas/canvas-state.test.ts +139 -0
- package/src/lib/canvas/input-handler.ts +221 -0
- package/src/lib/canvas/input-plugin.ts +150 -0
- package/src/lib/canvas/onion-skin.ts +94 -0
- package/src/lib/canvas/pixel-buffer.test.ts +249 -0
- package/src/lib/canvas/pixel-buffer.ts +151 -0
- package/src/lib/canvas/render-state.svelte.ts +18 -0
- package/src/lib/canvas/shape-preview-state.svelte.ts +36 -0
- package/src/lib/canvas/tile-mode.test.ts +53 -0
- package/src/lib/canvas/tile-mode.ts +92 -0
- package/src/lib/canvas/viewport-utils.ts +24 -0
- package/src/lib/canvas/zoom-utils.ts +31 -0
- package/src/lib/color/.gitkeep +0 -0
- package/src/lib/color/color-commands.ts +87 -0
- package/src/lib/color/color-state.svelte.ts +98 -0
- package/src/lib/color/color-state.test.ts +91 -0
- package/src/lib/color/color-utils.test.ts +220 -0
- package/src/lib/color/color-utils.ts +243 -0
- package/src/lib/color/palette-state.svelte.ts +127 -0
- package/src/lib/color/palette-state.test.ts +154 -0
- package/src/lib/color/palette.ts +79 -0
- package/src/lib/core/bootstrap.ts +66 -0
- package/src/lib/core/command-params.generated.ts +1549 -0
- package/src/lib/core/command-params.ts +20 -0
- package/src/lib/core/command-runner.ts +79 -0
- package/src/lib/core/commands.ts +134 -0
- package/src/lib/core/dispatcher.test.ts +548 -0
- package/src/lib/core/dispatcher.ts +361 -0
- package/src/lib/core/index.test.ts +7 -0
- package/src/lib/core/notification-state.svelte.ts +119 -0
- package/src/lib/core/plugin-api.ts +210 -0
- package/src/lib/core/plugin-discovery.test.ts +53 -0
- package/src/lib/core/plugin-discovery.ts +65 -0
- package/src/lib/core/plugin-loader.test.ts +159 -0
- package/src/lib/core/plugin-loader.ts +240 -0
- package/src/lib/core/plugin-types.ts +286 -0
- package/src/lib/core/registries.svelte.ts +74 -0
- package/src/lib/core/tool-options-state.svelte.ts +61 -0
- package/src/lib/dock/DockLayout.svelte +375 -0
- package/src/lib/dock/TabAddMenu.svelte +90 -0
- package/src/lib/dock/dock-persistence.ts +46 -0
- package/src/lib/dock/dock-plugin.ts +49 -0
- package/src/lib/dock/dock-presets.ts +156 -0
- package/src/lib/dock/dock-state.svelte.ts +77 -0
- package/src/lib/dock/dock-types.ts +2 -0
- package/src/lib/dock/dockview-theme.css +226 -0
- package/src/lib/dock/dockview-theme.ts +17 -0
- package/src/lib/dock/header-action-renderer.ts +77 -0
- package/src/lib/edit/clipboard-state.ts +34 -0
- package/src/lib/export/download.ts +201 -0
- package/src/lib/export/png-metadata.ts +181 -0
- package/src/lib/history/ActionLogPanel.svelte +418 -0
- package/src/lib/history/action-log-panel-plugin.ts +24 -0
- package/src/lib/history/action-log.svelte.ts +172 -0
- package/src/lib/history/action-log.test.ts +168 -0
- package/src/lib/history/history-commands.ts +403 -0
- package/src/lib/history/macros.svelte.ts +320 -0
- package/src/lib/history/macros.test.ts +224 -0
- package/src/lib/history/replay-engine.test.ts +241 -0
- package/src/lib/history/replay-engine.ts +149 -0
- package/src/lib/history/spec-format.test.ts +250 -0
- package/src/lib/history/spec-format.ts +210 -0
- package/src/lib/iso/SeamCheckerPanel.svelte +251 -0
- package/src/lib/iso/iso-math.test.ts +77 -0
- package/src/lib/iso/iso-math.ts +77 -0
- package/src/lib/iso/seam-checker-panel-plugin.ts +25 -0
- package/src/lib/iso/seam-checker.test.ts +126 -0
- package/src/lib/iso/seam-checker.ts +93 -0
- package/src/lib/iso/tessellation.ts +67 -0
- package/src/lib/layers/compositor.test.ts +193 -0
- package/src/lib/layers/compositor.ts +175 -0
- package/src/lib/layers/layer-commands.test.ts +263 -0
- package/src/lib/layers/layer-commands.ts +429 -0
- package/src/lib/layers/layer-tree.svelte.ts +516 -0
- package/src/lib/layers/layer-tree.test.ts +383 -0
- package/src/lib/layers/layer-types.ts +56 -0
- package/src/lib/leveleditor/LevelEditorViewport.svelte +1808 -0
- package/src/lib/leveleditor/MapPropertiesPanel.svelte +266 -0
- package/src/lib/leveleditor/TilePickerPanel.svelte +324 -0
- package/src/lib/leveleditor/depth-sort.test.ts +70 -0
- package/src/lib/leveleditor/depth-sort.ts +39 -0
- package/src/lib/leveleditor/level-editor-commands.ts +353 -0
- package/src/lib/leveleditor/level-editor-viewport-plugin.ts +25 -0
- package/src/lib/leveleditor/map-properties-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/map-state.svelte.ts +372 -0
- package/src/lib/leveleditor/map-state.test.ts +243 -0
- package/src/lib/leveleditor/tile-picker-panel-plugin.ts +25 -0
- package/src/lib/leveleditor/tile-source-registry.svelte.ts +91 -0
- package/src/lib/leveleditor/tiled-export.test.ts +144 -0
- package/src/lib/leveleditor/tiled-export.ts +374 -0
- package/src/lib/pywebview.d.ts +15 -0
- package/src/lib/recovery/.gitkeep +0 -0
- package/src/lib/recovery/idb-store.ts +118 -0
- package/src/lib/recovery/recovery-manager.test.ts +321 -0
- package/src/lib/recovery/recovery-manager.ts +115 -0
- package/src/lib/recovery/recovery-plugin.ts +55 -0
- package/src/lib/recovery/recovery-state.svelte.ts +21 -0
- package/src/lib/recovery/sw-plugin.ts +18 -0
- package/src/lib/recovery/sw-register.ts +17 -0
- package/src/lib/save/directory-format.ts +42 -0
- package/src/lib/save/project-snapshot.ts +139 -0
- package/src/lib/save/recent-projects.ts +56 -0
- package/src/lib/save/save-state.svelte.ts +35 -0
- package/src/lib/save/storage.ts +167 -0
- package/src/lib/save/zip-format.ts +45 -0
- package/src/lib/shortcuts/.gitkeep +0 -0
- package/src/lib/shortcuts/ShortcutEditorPanel.svelte +690 -0
- package/src/lib/shortcuts/default-bindings.ts +61 -0
- package/src/lib/shortcuts/shortcut-editor-panel-plugin.ts +25 -0
- package/src/lib/shortcuts/shortcut-init.ts +380 -0
- package/src/lib/shortcuts/shortcut-manager.test.ts +466 -0
- package/src/lib/shortcuts/shortcut-manager.ts +281 -0
- package/src/lib/shortcuts/shortcut-state.svelte.ts +78 -0
- package/src/lib/shortcuts/shortcuts-plugin.ts +17 -0
- package/src/lib/sync/patch-applicator.ts +300 -0
- package/src/lib/sync/patch-types.ts +65 -0
- package/src/lib/sync/project-manager.ts +108 -0
- package/src/lib/sync/sync-init.ts +152 -0
- package/src/lib/sync/sync-plugin.ts +19 -0
- package/src/lib/sync/sync-state.svelte.ts +56 -0
- package/src/lib/sync/ws-client.test.ts +604 -0
- package/src/lib/sync/ws-client.ts +574 -0
- package/src/lib/tools/.gitkeep +0 -0
- package/src/lib/ui/.gitkeep +0 -0
- package/src/lib/ui/AboutDialog.svelte +113 -0
- package/src/lib/ui/ColorPicker.svelte +761 -0
- package/src/lib/ui/ContextMenu.svelte +216 -0
- package/src/lib/ui/ExportDialog.svelte +747 -0
- package/src/lib/ui/FrameStrip.svelte +854 -0
- package/src/lib/ui/LayerPanel.svelte +810 -0
- package/src/lib/ui/MenuBar.svelte +590 -0
- package/src/lib/ui/NewProjectDialog.svelte +803 -0
- package/src/lib/ui/PluginManagerPanel.svelte +475 -0
- package/src/lib/ui/PromptDialog.svelte +252 -0
- package/src/lib/ui/RecoveryDialog.svelte +295 -0
- package/src/lib/ui/ResizeDialog.svelte +416 -0
- package/src/lib/ui/StatusBar.svelte +145 -0
- package/src/lib/ui/ToolbarPanel.svelte +488 -0
- package/src/lib/ui/animation-menu-commands.ts +194 -0
- package/src/lib/ui/command-palette/CommandPalette.svelte +232 -0
- package/src/lib/ui/command-palette/command-palette-plugin.ts +30 -0
- package/src/lib/ui/command-palette/command-palette-state.svelte.ts +190 -0
- package/src/lib/ui/command-palette/command-palette.test.ts +129 -0
- package/src/lib/ui/dialog-state.svelte.ts +70 -0
- package/src/lib/ui/edit-commands.ts +271 -0
- package/src/lib/ui/file-commands.ts +275 -0
- package/src/lib/ui/file-open.ts +99 -0
- package/src/lib/ui/help-commands.ts +93 -0
- package/src/lib/ui/image-commands.ts +181 -0
- package/src/lib/ui/layer-menu-commands.ts +420 -0
- package/src/lib/ui/menu-builder.ts +224 -0
- package/src/lib/ui/notifications/NotificationBanner.svelte +137 -0
- package/src/lib/ui/notifications/notification-plugin.ts +29 -0
- package/src/lib/ui/notifications/notification-state.svelte.ts +9 -0
- package/src/lib/ui/plugin-manager-panel-plugin.ts +26 -0
- package/src/lib/ui/plugin-state.svelte.ts +62 -0
- package/src/lib/ui/select-commands.ts +75 -0
- package/src/lib/ui/theme-plugin.ts +18 -0
- package/src/lib/ui/theme.svelte.ts +45 -0
- package/src/lib/ui/theme.test.ts +51 -0
- package/src/lib/ui/toolbar-config.ts +90 -0
- package/src/lib/ui/toolbar-plugin.ts +39 -0
- package/src/lib/ui/view-commands.ts +629 -0
- package/src/lib/variants/BisectionExportDialog.svelte +500 -0
- package/src/lib/variants/VariantPanel.svelte +822 -0
- package/src/lib/variants/bisection-export.test.ts +113 -0
- package/src/lib/variants/bisection-export.ts +148 -0
- package/src/lib/variants/palette-extraction.test.ts +111 -0
- package/src/lib/variants/palette-extraction.ts +84 -0
- package/src/lib/variants/palette-interpolation.test.ts +113 -0
- package/src/lib/variants/palette-interpolation.ts +87 -0
- package/src/lib/variants/palette-swap.test.ts +101 -0
- package/src/lib/variants/palette-swap.ts +114 -0
- package/src/lib/variants/variant-commands.ts +594 -0
- package/src/lib/variants/variant-panel-plugin.ts +27 -0
- package/src/lib/variants/variant-randomizer.ts +101 -0
- package/src/lib/variants/variant-state.svelte.ts +166 -0
- package/src/lib/variants/variant-state.test.ts +138 -0
- package/src/main.ts +14 -0
- package/src/vite-env.d.ts +3 -0
- package/svelte.config.js +2 -0
- package/todo/.done/audit-design-decisions.md +812 -0
- package/todo/.done/audit-implementation-plan.md +1235 -0
- package/todo/.done/happy-path-polish.md +177 -0
- package/todo/.done/pixelweaver-full-build.md +937 -0
- package/todo/.done/server-multi-frame-design.md +405 -0
- package/todo/.done/typed-dispatcher-design.md +435 -0
- package/todo/.done/unified-toolbar-and-action-system.md +323 -0
- package/todo/.obsolete/comprehensive-audit-obsolete-items.md +33 -0
- package/todo/.obsolete/tauri-desktop-bundle.md +424 -0
- package/todo/comprehensive-audit.md +1085 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +20 -0
- package/uv.lock +1167 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* VariantPanel -- sidebar panel for managing variant presets.
|
|
4
|
+
*
|
|
5
|
+
* Shows a grid of variant thumbnails rendered by compositing the current
|
|
6
|
+
* frame with each preset's per-group palette swaps. Clicking a thumbnail
|
|
7
|
+
* dispatches `apply_variant`. Header buttons dispatch the corresponding
|
|
8
|
+
* batch commands so every action lands in the undo stack / action log and
|
|
9
|
+
* can be bound to shortcuts and the command palette:
|
|
10
|
+
* - Generate: `generate_variants` (undoable)
|
|
11
|
+
* - Extract: `extract_palette_from_layer` (read-only, not undoable)
|
|
12
|
+
* - Export: `export_bisection_atlas` (side effect, not undoable)
|
|
13
|
+
* - Remove: `remove_variant_preset` (undoable)
|
|
14
|
+
*
|
|
15
|
+
* Visual conventions match LayerPanel.svelte; all colors via design tokens.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { composite } from '../layers/compositor.js';
|
|
19
|
+
import { getLayers } from '../layers/layer-tree.svelte.js';
|
|
20
|
+
import {
|
|
21
|
+
getCurrentFrame,
|
|
22
|
+
getFrames,
|
|
23
|
+
} from '../animation/frame-model.svelte.js';
|
|
24
|
+
import { applyGroupPaletteSwap } from './palette-swap.js';
|
|
25
|
+
import {
|
|
26
|
+
getPresets,
|
|
27
|
+
getActivePresetId,
|
|
28
|
+
getActivePreset,
|
|
29
|
+
} from './variant-state.svelte.js';
|
|
30
|
+
import type { VariantPreset } from './variant-state.svelte.js';
|
|
31
|
+
import { canvasState } from '../canvas/canvas-state.svelte.js';
|
|
32
|
+
import type { CommandType, ParamsOf } from '../core/command-params.js';
|
|
33
|
+
import { dispatch } from '../core/dispatcher.js';
|
|
34
|
+
import type { PixelBuffer } from '../canvas/pixel-buffer.js';
|
|
35
|
+
|
|
36
|
+
import { notificationState } from '../core/notification-state.svelte.js';
|
|
37
|
+
|
|
38
|
+
import WandIcon from '~icons/lucide/wand-2';
|
|
39
|
+
import PaletteIcon from '~icons/lucide/palette';
|
|
40
|
+
import DownloadIcon from '~icons/lucide/download';
|
|
41
|
+
import FolderDownIcon from '~icons/lucide/folder-down';
|
|
42
|
+
import TrashIcon from '~icons/lucide/trash-2';
|
|
43
|
+
import ShuffleIcon from '~icons/lucide/shuffle';
|
|
44
|
+
import BlendIcon from '~icons/lucide/blend';
|
|
45
|
+
import ArrowRightIcon from '~icons/lucide/arrow-right';
|
|
46
|
+
import XIcon from '~icons/lucide/x';
|
|
47
|
+
|
|
48
|
+
// --- Command dispatch helper ---
|
|
49
|
+
// All mutations flow through the dispatcher so they appear in the action
|
|
50
|
+
// log, land in the undo stack where applicable, and can be intercepted by
|
|
51
|
+
// shortcut / palette entry points.
|
|
52
|
+
// Typed overload: compile-time param validation for known commands
|
|
53
|
+
function dispatchCmd<T extends CommandType>(type: T, params: ParamsOf<T>): void;
|
|
54
|
+
// String fallback: for dynamic dispatch where command type is a variable
|
|
55
|
+
function dispatchCmd(type: string, params?: Record<string, unknown>): void;
|
|
56
|
+
function dispatchCmd(type: string, params: Record<string, unknown> = {}) {
|
|
57
|
+
dispatch({
|
|
58
|
+
type,
|
|
59
|
+
plugin: 'ui/variants',
|
|
60
|
+
version: '1.0.0',
|
|
61
|
+
params,
|
|
62
|
+
id: crypto.randomUUID(),
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Generation mode toggle ---
|
|
68
|
+
// 'random' generates HSV-shifted variants; 'blend' interpolates between
|
|
69
|
+
// the first two existing presets.
|
|
70
|
+
type GenerateMode = 'random' | 'blend';
|
|
71
|
+
let generateMode: GenerateMode = $state('random');
|
|
72
|
+
|
|
73
|
+
/** Push a notification for in-panel feedback (e.g. insufficient presets). */
|
|
74
|
+
function panelNotify(message: string, type: 'info' | 'warning' = 'warning'): void {
|
|
75
|
+
notificationState.push({
|
|
76
|
+
id: `variant-panel-${crypto.randomUUID()}`,
|
|
77
|
+
message,
|
|
78
|
+
type,
|
|
79
|
+
autoDismissMs: 4000,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Composite the current frame with a preset's palette swaps applied.
|
|
85
|
+
* Returns null if no frame is available. Thumbnails are purely read-only
|
|
86
|
+
* rendering so we still call the swap/composite library directly here --
|
|
87
|
+
* there is no state mutation to route through a command.
|
|
88
|
+
*/
|
|
89
|
+
function compositeWithPreset(preset: VariantPreset | null): PixelBuffer | null {
|
|
90
|
+
const frames = getFrames();
|
|
91
|
+
if (frames.length === 0) return null;
|
|
92
|
+
|
|
93
|
+
const frame = getCurrentFrame();
|
|
94
|
+
const tree = getLayers();
|
|
95
|
+
const width = canvasState.canvasWidth;
|
|
96
|
+
const height = canvasState.canvasHeight;
|
|
97
|
+
|
|
98
|
+
// Start from the frame's pixel data, then overlay swapped buffers per group
|
|
99
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient compositing buffer
|
|
100
|
+
const pixelData = new Map(frame.pixelData);
|
|
101
|
+
|
|
102
|
+
if (preset) {
|
|
103
|
+
for (const [groupId, colorMap] of preset.groupOverrides) {
|
|
104
|
+
if (colorMap.size === 0) continue;
|
|
105
|
+
const swapped = applyGroupPaletteSwap(groupId, colorMap, tree, [frame]);
|
|
106
|
+
const frameSwapped = swapped.get(frame.id);
|
|
107
|
+
if (!frameSwapped) continue;
|
|
108
|
+
for (const [layerId, buffer] of frameSwapped) {
|
|
109
|
+
pixelData.set(layerId, buffer);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return composite(tree, pixelData, width, height);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- Reactive derived state ---
|
|
118
|
+
// Reads of variant state inside $derived create subscriptions.
|
|
119
|
+
|
|
120
|
+
let presets = $derived(getPresets());
|
|
121
|
+
let activeId = $derived(getActivePresetId());
|
|
122
|
+
let canvasW = $derived(canvasState.canvasWidth);
|
|
123
|
+
let canvasH = $derived(canvasState.canvasHeight);
|
|
124
|
+
|
|
125
|
+
// --- Active preset color mappings ---
|
|
126
|
+
// When a variant is selected, derive its per-group color overrides as a
|
|
127
|
+
// flat list of { groupId, original, replacement } entries for the editor UI.
|
|
128
|
+
|
|
129
|
+
let activePreset = $derived(getActivePreset());
|
|
130
|
+
|
|
131
|
+
interface ColorMapping {
|
|
132
|
+
groupId: string;
|
|
133
|
+
original: string;
|
|
134
|
+
replacement: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Derive the color mapping rows for the active preset. For each group in
|
|
139
|
+
* the preset's overrides, list every original->replacement pair. This
|
|
140
|
+
* drives the inline color editor below the thumbnail grid.
|
|
141
|
+
*/
|
|
142
|
+
let colorMappings = $derived.by<ColorMapping[]>(() => {
|
|
143
|
+
const preset = activePreset;
|
|
144
|
+
if (!preset) return [];
|
|
145
|
+
const rows: ColorMapping[] = [];
|
|
146
|
+
for (const [groupId, colorMap] of preset.groupOverrides) {
|
|
147
|
+
for (const [original, replacement] of colorMap) {
|
|
148
|
+
rows.push({ groupId, original, replacement });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return rows;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/** Handle a color override change from the inline <input type="color">. */
|
|
155
|
+
function handleColorChange(
|
|
156
|
+
presetId: string,
|
|
157
|
+
groupId: string,
|
|
158
|
+
originalColor: string,
|
|
159
|
+
newColor: string,
|
|
160
|
+
): void {
|
|
161
|
+
dispatchCmd('set_color_override', {
|
|
162
|
+
presetId,
|
|
163
|
+
groupId,
|
|
164
|
+
originalColor,
|
|
165
|
+
newColor: newColor.toUpperCase(),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Remove a single color override (revert to original). */
|
|
170
|
+
function handleRemoveOverride(
|
|
171
|
+
e: MouseEvent,
|
|
172
|
+
presetId: string,
|
|
173
|
+
groupId: string,
|
|
174
|
+
originalColor: string,
|
|
175
|
+
): void {
|
|
176
|
+
e.stopPropagation();
|
|
177
|
+
dispatchCmd('remove_color_override', {
|
|
178
|
+
presetId,
|
|
179
|
+
groupId,
|
|
180
|
+
originalColor,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// --- Thumbnail rendering ---
|
|
185
|
+
// One <canvas> per preset. We collect refs via {@attach} -- Svelte 5's
|
|
186
|
+
// attachment primitive runs on mount and returns a cleanup on unmount.
|
|
187
|
+
|
|
188
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- attachment refs, bumped via thumbVersion
|
|
189
|
+
const thumbEls = new Map<string, HTMLCanvasElement>();
|
|
190
|
+
|
|
191
|
+
// Bump this counter whenever the thumbEls map changes so the redraw
|
|
192
|
+
// effect re-runs after new canvases mount / old ones unmount.
|
|
193
|
+
let thumbVersion = $state(0);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns an attachment function for `{@attach thumbAttach(preset.id)}`.
|
|
197
|
+
* Registers the canvas element with our local map and cleans up on unmount.
|
|
198
|
+
*/
|
|
199
|
+
function thumbAttach(id: string) {
|
|
200
|
+
return (node: Element) => {
|
|
201
|
+
thumbEls.set(id, node as HTMLCanvasElement);
|
|
202
|
+
thumbVersion += 1;
|
|
203
|
+
return () => {
|
|
204
|
+
thumbEls.delete(id);
|
|
205
|
+
thumbVersion += 1;
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Redraw thumbnails on any state change that affects what they show.
|
|
211
|
+
$effect(() => {
|
|
212
|
+
// Dependency touches: presets list, active id (for highlight), canvas
|
|
213
|
+
// size, thumbnail element set, and the current frame id.
|
|
214
|
+
void presets;
|
|
215
|
+
void activeId;
|
|
216
|
+
void canvasW;
|
|
217
|
+
void canvasH;
|
|
218
|
+
void thumbVersion;
|
|
219
|
+
const frames = getFrames();
|
|
220
|
+
if (frames.length === 0) return;
|
|
221
|
+
void getCurrentFrame().id;
|
|
222
|
+
|
|
223
|
+
for (const preset of presets) {
|
|
224
|
+
const el = thumbEls.get(preset.id);
|
|
225
|
+
if (!el) continue;
|
|
226
|
+
const buffer = compositeWithPreset(preset);
|
|
227
|
+
if (!buffer) continue;
|
|
228
|
+
el.width = buffer.width;
|
|
229
|
+
el.height = buffer.height;
|
|
230
|
+
const ctx = el.getContext('2d');
|
|
231
|
+
if (!ctx) continue;
|
|
232
|
+
ctx.imageSmoothingEnabled = false;
|
|
233
|
+
ctx.putImageData(buffer.toImageData(), 0, 0);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// --- Actions ---
|
|
238
|
+
// Every handler dispatches a command; the variant-commands plugin is
|
|
239
|
+
// responsible for targeting groups, emitting notifications, and managing
|
|
240
|
+
// undo snapshots. Keeping this panel dumb means shortcuts/palette hooks
|
|
241
|
+
// trigger identical behaviour to button clicks.
|
|
242
|
+
|
|
243
|
+
function handleGenerate(): void {
|
|
244
|
+
if (generateMode === 'blend') {
|
|
245
|
+
if (presets.length < 2) {
|
|
246
|
+
panelNotify('Need at least 2 variants to blend');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
dispatchCmd('generate_variants', { count: 3, mode: 'interpolate' });
|
|
250
|
+
} else {
|
|
251
|
+
dispatchCmd('generate_variants', { count: 1, mode: 'random' });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleExtractPalette(): void {
|
|
256
|
+
dispatchCmd('extract_palette_from_layer', {});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handleExportBisection(): void {
|
|
260
|
+
dispatchCmd('export_bisection_atlas', {});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleExportBatch(): void {
|
|
264
|
+
dispatchCmd('export_variants_batch', {});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handleApply(preset: VariantPreset): void {
|
|
268
|
+
// Toggle: clicking the active preset clears it (restores original).
|
|
269
|
+
const nextId = activeId === preset.id ? null : preset.id;
|
|
270
|
+
dispatchCmd('apply_variant', { presetId: nextId });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleRemove(e: MouseEvent, preset: VariantPreset): void {
|
|
274
|
+
e.stopPropagation();
|
|
275
|
+
dispatchCmd('remove_variant_preset', { id: preset.id });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function handleKeydown(e: KeyboardEvent, preset: VariantPreset): void {
|
|
279
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
handleApply(preset);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
</script>
|
|
285
|
+
|
|
286
|
+
<div class="variant-panel">
|
|
287
|
+
<!-- Header -->
|
|
288
|
+
<div class="variant-header">
|
|
289
|
+
<span class="variant-title">Variants</span>
|
|
290
|
+
<div class="variant-actions">
|
|
291
|
+
<!-- Mode toggle: Random vs Blend -->
|
|
292
|
+
<div class="mode-toggle" role="radiogroup" aria-label="Generation mode">
|
|
293
|
+
<button
|
|
294
|
+
class="mode-seg"
|
|
295
|
+
class:mode-seg--active={generateMode === 'random'}
|
|
296
|
+
role="radio"
|
|
297
|
+
aria-checked={generateMode === 'random'}
|
|
298
|
+
title="Random mode"
|
|
299
|
+
aria-label="Random mode"
|
|
300
|
+
onclick={() => (generateMode = 'random')}
|
|
301
|
+
><ShuffleIcon /></button>
|
|
302
|
+
<button
|
|
303
|
+
class="mode-seg"
|
|
304
|
+
class:mode-seg--active={generateMode === 'blend'}
|
|
305
|
+
role="radio"
|
|
306
|
+
aria-checked={generateMode === 'blend'}
|
|
307
|
+
title="Blend mode"
|
|
308
|
+
aria-label="Blend mode"
|
|
309
|
+
onclick={() => (generateMode = 'blend')}
|
|
310
|
+
><BlendIcon /></button>
|
|
311
|
+
</div>
|
|
312
|
+
<button
|
|
313
|
+
class="action-btn"
|
|
314
|
+
title={generateMode === 'blend' ? 'Generate blended variants' : 'Generate random variant'}
|
|
315
|
+
aria-label={generateMode === 'blend' ? 'Generate blended variants' : 'Generate random variant'}
|
|
316
|
+
onclick={handleGenerate}
|
|
317
|
+
><WandIcon /></button>
|
|
318
|
+
<button
|
|
319
|
+
class="action-btn"
|
|
320
|
+
title="Extract palette from active group"
|
|
321
|
+
aria-label="Extract palette from active group"
|
|
322
|
+
onclick={handleExtractPalette}
|
|
323
|
+
><PaletteIcon /></button>
|
|
324
|
+
<button
|
|
325
|
+
class="action-btn"
|
|
326
|
+
title="Export bisection atlas"
|
|
327
|
+
aria-label="Export bisection atlas"
|
|
328
|
+
onclick={handleExportBisection}
|
|
329
|
+
disabled={presets.length === 0}
|
|
330
|
+
><DownloadIcon /></button>
|
|
331
|
+
<button
|
|
332
|
+
class="action-btn"
|
|
333
|
+
title="Export variants individually"
|
|
334
|
+
aria-label="Export variants individually"
|
|
335
|
+
onclick={handleExportBatch}
|
|
336
|
+
disabled={presets.length === 0}
|
|
337
|
+
><FolderDownIcon /></button>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
<!-- Grid or empty state -->
|
|
342
|
+
{#if presets.length === 0}
|
|
343
|
+
<div class="empty-state">
|
|
344
|
+
<p class="empty-title">No variants yet</p>
|
|
345
|
+
<p class="empty-hint">
|
|
346
|
+
Use the wand button to generate a random variant for the active layer group.
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
{:else}
|
|
350
|
+
<div class="variant-grid">
|
|
351
|
+
{#each presets as preset (preset.id)}
|
|
352
|
+
<div
|
|
353
|
+
class="variant-cell"
|
|
354
|
+
class:variant-cell--active={preset.id === activeId}
|
|
355
|
+
role="button"
|
|
356
|
+
tabindex="0"
|
|
357
|
+
aria-pressed={preset.id === activeId}
|
|
358
|
+
aria-label={`Apply variant ${preset.name}`}
|
|
359
|
+
title={preset.name}
|
|
360
|
+
onclick={() => { handleApply(preset); }}
|
|
361
|
+
onkeydown={(e) => { handleKeydown(e, preset); }}
|
|
362
|
+
>
|
|
363
|
+
<div class="thumb-wrap">
|
|
364
|
+
<canvas
|
|
365
|
+
class="thumb"
|
|
366
|
+
{@attach thumbAttach(preset.id)}
|
|
367
|
+
></canvas>
|
|
368
|
+
<button
|
|
369
|
+
class="remove-btn"
|
|
370
|
+
title="Remove variant"
|
|
371
|
+
aria-label={`Remove variant ${preset.name}`}
|
|
372
|
+
onclick={(e) => { handleRemove(e, preset); }}
|
|
373
|
+
><TrashIcon /></button>
|
|
374
|
+
</div>
|
|
375
|
+
<span class="variant-name">{preset.name}</span>
|
|
376
|
+
</div>
|
|
377
|
+
{/each}
|
|
378
|
+
</div>
|
|
379
|
+
{/if}
|
|
380
|
+
|
|
381
|
+
<!-- Per-color editor for the active variant -->
|
|
382
|
+
<div class="color-editor">
|
|
383
|
+
{#if activePreset && colorMappings.length > 0}
|
|
384
|
+
<div class="color-editor-header">
|
|
385
|
+
<span class="color-editor-title">Color Overrides</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div class="color-rows">
|
|
388
|
+
{#each colorMappings as mapping (mapping.groupId + ':' + mapping.original)}
|
|
389
|
+
<div class="color-row">
|
|
390
|
+
<span
|
|
391
|
+
class="color-swatch"
|
|
392
|
+
style:background-color={mapping.original}
|
|
393
|
+
title={mapping.original}
|
|
394
|
+
></span>
|
|
395
|
+
<span class="color-arrow"><ArrowRightIcon /></span>
|
|
396
|
+
<label class="color-input-wrap" title="Click to change">
|
|
397
|
+
<span
|
|
398
|
+
class="color-swatch color-swatch--editable"
|
|
399
|
+
style:background-color={mapping.replacement}
|
|
400
|
+
></span>
|
|
401
|
+
<input
|
|
402
|
+
type="color"
|
|
403
|
+
class="color-input-hidden"
|
|
404
|
+
value={mapping.replacement}
|
|
405
|
+
onchange={(e) => {
|
|
406
|
+
// activePreset is a `$derived` value -- its declared type
|
|
407
|
+
// ignores the surrounding `{#if activePreset}` narrowing
|
|
408
|
+
// by the time this callback actually runs, so we keep
|
|
409
|
+
// the runtime guard.
|
|
410
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
411
|
+
if (!activePreset) return;
|
|
412
|
+
handleColorChange(
|
|
413
|
+
activePreset.id,
|
|
414
|
+
mapping.groupId,
|
|
415
|
+
mapping.original,
|
|
416
|
+
(e.target as HTMLInputElement).value,
|
|
417
|
+
);
|
|
418
|
+
}}
|
|
419
|
+
/>
|
|
420
|
+
</label>
|
|
421
|
+
<button
|
|
422
|
+
class="color-remove-btn"
|
|
423
|
+
title="Revert to original"
|
|
424
|
+
aria-label={`Remove override for ${mapping.original}`}
|
|
425
|
+
onclick={(e) => {
|
|
426
|
+
// See onchange above: $derived narrowing does not
|
|
427
|
+
// survive the nested callback boundary.
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
429
|
+
if (!activePreset) return;
|
|
430
|
+
handleRemoveOverride(e, activePreset.id, mapping.groupId, mapping.original);
|
|
431
|
+
}}
|
|
432
|
+
><XIcon /></button>
|
|
433
|
+
</div>
|
|
434
|
+
{/each}
|
|
435
|
+
</div>
|
|
436
|
+
{:else if activePreset}
|
|
437
|
+
<div class="color-editor-hint">No color overrides in this variant.</div>
|
|
438
|
+
{:else}
|
|
439
|
+
<div class="color-editor-hint">Select a variant to edit colors</div>
|
|
440
|
+
{/if}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
<style>
|
|
445
|
+
.variant-panel {
|
|
446
|
+
display: flex;
|
|
447
|
+
flex-direction: column;
|
|
448
|
+
width: 100%;
|
|
449
|
+
height: 100%;
|
|
450
|
+
background: var(--bg-panel);
|
|
451
|
+
color: var(--text-primary);
|
|
452
|
+
font-size: var(--text-base);
|
|
453
|
+
user-select: none;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.variant-header {
|
|
457
|
+
display: flex;
|
|
458
|
+
align-items: center;
|
|
459
|
+
justify-content: space-between;
|
|
460
|
+
padding: 6px var(--space-3);
|
|
461
|
+
border-bottom: 1px solid var(--border);
|
|
462
|
+
background: var(--bg-toolbar);
|
|
463
|
+
flex-shrink: 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.variant-title {
|
|
467
|
+
font-weight: 600;
|
|
468
|
+
font-size: var(--text-sm);
|
|
469
|
+
text-transform: uppercase;
|
|
470
|
+
letter-spacing: 0.5px;
|
|
471
|
+
color: var(--text-secondary);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.variant-actions {
|
|
475
|
+
display: flex;
|
|
476
|
+
gap: var(--space-1);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.action-btn {
|
|
480
|
+
background: none;
|
|
481
|
+
border: 1px solid transparent;
|
|
482
|
+
border-radius: var(--radius-sm);
|
|
483
|
+
color: var(--text-secondary);
|
|
484
|
+
cursor: pointer;
|
|
485
|
+
width: 24px;
|
|
486
|
+
height: 24px;
|
|
487
|
+
display: flex;
|
|
488
|
+
align-items: center;
|
|
489
|
+
justify-content: center;
|
|
490
|
+
font-size: var(--text-xl);
|
|
491
|
+
padding: 0;
|
|
492
|
+
line-height: 1;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.action-btn:hover:not(:disabled) {
|
|
496
|
+
background: var(--bg-primary);
|
|
497
|
+
color: var(--text-primary);
|
|
498
|
+
border-color: var(--border);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.action-btn:disabled {
|
|
502
|
+
opacity: 0.3;
|
|
503
|
+
cursor: default;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.action-btn :global(svg) {
|
|
507
|
+
width: 14px;
|
|
508
|
+
height: 14px;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* Mode toggle -- compact two-segment radio group */
|
|
512
|
+
.mode-toggle {
|
|
513
|
+
display: flex;
|
|
514
|
+
border: 1px solid var(--border);
|
|
515
|
+
border-radius: var(--radius-sm);
|
|
516
|
+
overflow: hidden;
|
|
517
|
+
margin-right: var(--space-1);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.mode-seg {
|
|
521
|
+
background: none;
|
|
522
|
+
border: none;
|
|
523
|
+
color: var(--text-secondary);
|
|
524
|
+
cursor: pointer;
|
|
525
|
+
width: 22px;
|
|
526
|
+
height: 22px;
|
|
527
|
+
display: flex;
|
|
528
|
+
align-items: center;
|
|
529
|
+
justify-content: center;
|
|
530
|
+
padding: 0;
|
|
531
|
+
line-height: 1;
|
|
532
|
+
transition: background var(--transition-fast), color var(--transition-fast);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.mode-seg + .mode-seg {
|
|
536
|
+
border-left: 1px solid var(--border);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.mode-seg:hover:not(.mode-seg--active) {
|
|
540
|
+
background: var(--bg-primary);
|
|
541
|
+
color: var(--text-primary);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.mode-seg--active {
|
|
545
|
+
background: var(--accent);
|
|
546
|
+
color: var(--bg-panel);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.mode-seg :global(svg) {
|
|
550
|
+
width: 12px;
|
|
551
|
+
height: 12px;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* Empty state */
|
|
555
|
+
.empty-state {
|
|
556
|
+
flex: 1;
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-direction: column;
|
|
559
|
+
align-items: center;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
padding: var(--space-4);
|
|
562
|
+
text-align: center;
|
|
563
|
+
color: var(--text-secondary);
|
|
564
|
+
gap: var(--space-2);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.empty-title {
|
|
568
|
+
font-size: var(--text-base);
|
|
569
|
+
font-weight: 600;
|
|
570
|
+
margin: 0;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.empty-hint {
|
|
574
|
+
font-size: var(--text-sm);
|
|
575
|
+
margin: 0;
|
|
576
|
+
line-height: 1.4;
|
|
577
|
+
max-width: 220px;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Grid container */
|
|
581
|
+
.variant-grid {
|
|
582
|
+
flex: 1;
|
|
583
|
+
min-height: 0;
|
|
584
|
+
overflow-y: auto;
|
|
585
|
+
overflow-x: hidden;
|
|
586
|
+
display: grid;
|
|
587
|
+
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
|
588
|
+
gap: var(--space-2);
|
|
589
|
+
padding: var(--space-2);
|
|
590
|
+
align-content: start;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/* Individual cell */
|
|
594
|
+
.variant-cell {
|
|
595
|
+
display: flex;
|
|
596
|
+
flex-direction: column;
|
|
597
|
+
align-items: stretch;
|
|
598
|
+
gap: var(--space-1);
|
|
599
|
+
cursor: pointer;
|
|
600
|
+
padding: var(--space-1);
|
|
601
|
+
border: 1px solid transparent;
|
|
602
|
+
border-radius: var(--radius-sm);
|
|
603
|
+
transition: background var(--transition-fast), border-color var(--transition-fast);
|
|
604
|
+
min-width: 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.variant-cell:hover {
|
|
608
|
+
background: rgba(255, 255, 255, 0.04);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
:global([data-theme="light"]) .variant-cell:hover {
|
|
612
|
+
background: rgba(0, 0, 0, 0.04);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.variant-cell:focus-visible {
|
|
616
|
+
outline: 2px solid var(--accent);
|
|
617
|
+
outline-offset: 1px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
.variant-cell--active {
|
|
621
|
+
border-color: var(--accent);
|
|
622
|
+
background: rgba(255, 255, 255, 0.06);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
:global([data-theme="light"]) .variant-cell--active {
|
|
626
|
+
background: rgba(0, 0, 0, 0.06);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/* Thumbnail wrapper (square) */
|
|
630
|
+
.thumb-wrap {
|
|
631
|
+
position: relative;
|
|
632
|
+
width: 100%;
|
|
633
|
+
aspect-ratio: 1 / 1;
|
|
634
|
+
background: var(--bg-primary);
|
|
635
|
+
border: 1px solid var(--border);
|
|
636
|
+
border-radius: var(--radius-sm);
|
|
637
|
+
overflow: hidden;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.thumb {
|
|
641
|
+
width: 100%;
|
|
642
|
+
height: 100%;
|
|
643
|
+
display: block;
|
|
644
|
+
/* Pixelated scaling keeps thumbnails crisp at any size. */
|
|
645
|
+
image-rendering: pixelated;
|
|
646
|
+
image-rendering: crisp-edges;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/* Remove button -- top-right over the thumbnail */
|
|
650
|
+
.remove-btn {
|
|
651
|
+
position: absolute;
|
|
652
|
+
top: 2px;
|
|
653
|
+
right: 2px;
|
|
654
|
+
background: var(--bg-panel);
|
|
655
|
+
border: 1px solid var(--border);
|
|
656
|
+
border-radius: var(--radius-sm);
|
|
657
|
+
color: var(--text-secondary);
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
width: 18px;
|
|
660
|
+
height: 18px;
|
|
661
|
+
display: none;
|
|
662
|
+
align-items: center;
|
|
663
|
+
justify-content: center;
|
|
664
|
+
padding: 0;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.variant-cell:hover .remove-btn,
|
|
668
|
+
.variant-cell:focus-within .remove-btn {
|
|
669
|
+
display: flex;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.remove-btn:hover {
|
|
673
|
+
color: var(--text-primary);
|
|
674
|
+
border-color: var(--accent);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.remove-btn :global(svg) {
|
|
678
|
+
width: 10px;
|
|
679
|
+
height: 10px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* Variant name label */
|
|
683
|
+
.variant-name {
|
|
684
|
+
font-size: var(--text-xs);
|
|
685
|
+
color: var(--text-secondary);
|
|
686
|
+
overflow: hidden;
|
|
687
|
+
text-overflow: ellipsis;
|
|
688
|
+
white-space: nowrap;
|
|
689
|
+
text-align: center;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.variant-cell--active .variant-name {
|
|
693
|
+
color: var(--text-primary);
|
|
694
|
+
font-weight: 600;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* --- Color editor section --- */
|
|
698
|
+
|
|
699
|
+
.color-editor {
|
|
700
|
+
flex-shrink: 0;
|
|
701
|
+
border-top: 1px solid var(--border);
|
|
702
|
+
max-height: 40%;
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-direction: column;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.color-editor-header {
|
|
708
|
+
display: flex;
|
|
709
|
+
align-items: center;
|
|
710
|
+
padding: 4px var(--space-3);
|
|
711
|
+
background: var(--bg-toolbar);
|
|
712
|
+
flex-shrink: 0;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.color-editor-title {
|
|
716
|
+
font-weight: 600;
|
|
717
|
+
font-size: var(--text-xs);
|
|
718
|
+
text-transform: uppercase;
|
|
719
|
+
letter-spacing: 0.5px;
|
|
720
|
+
color: var(--text-secondary);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.color-rows {
|
|
724
|
+
overflow-y: auto;
|
|
725
|
+
padding: var(--space-1) var(--space-2);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.color-row {
|
|
729
|
+
display: flex;
|
|
730
|
+
align-items: center;
|
|
731
|
+
gap: 4px;
|
|
732
|
+
height: 22px;
|
|
733
|
+
padding: 1px 0;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.color-swatch {
|
|
737
|
+
width: 16px;
|
|
738
|
+
height: 16px;
|
|
739
|
+
border-radius: 2px;
|
|
740
|
+
border: 1px solid var(--border);
|
|
741
|
+
flex-shrink: 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.color-swatch--editable {
|
|
745
|
+
cursor: pointer;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
.color-swatch--editable:hover {
|
|
749
|
+
border-color: var(--accent);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.color-arrow {
|
|
753
|
+
display: flex;
|
|
754
|
+
align-items: center;
|
|
755
|
+
color: var(--text-secondary);
|
|
756
|
+
flex-shrink: 0;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.color-arrow :global(svg) {
|
|
760
|
+
width: 10px;
|
|
761
|
+
height: 10px;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/* Wrap the editable swatch + hidden native color input in a label so
|
|
765
|
+
clicking the swatch opens the picker. The native input is visually
|
|
766
|
+
hidden but remains accessible for keyboard / screen-reader use. */
|
|
767
|
+
.color-input-wrap {
|
|
768
|
+
position: relative;
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: center;
|
|
771
|
+
cursor: pointer;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.color-input-hidden {
|
|
775
|
+
position: absolute;
|
|
776
|
+
width: 1px;
|
|
777
|
+
height: 1px;
|
|
778
|
+
overflow: hidden;
|
|
779
|
+
clip: rect(0, 0, 0, 0);
|
|
780
|
+
border: 0;
|
|
781
|
+
padding: 0;
|
|
782
|
+
margin: 0;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.color-remove-btn {
|
|
786
|
+
background: none;
|
|
787
|
+
border: none;
|
|
788
|
+
color: var(--text-secondary);
|
|
789
|
+
cursor: pointer;
|
|
790
|
+
width: 16px;
|
|
791
|
+
height: 16px;
|
|
792
|
+
display: flex;
|
|
793
|
+
align-items: center;
|
|
794
|
+
justify-content: center;
|
|
795
|
+
padding: 0;
|
|
796
|
+
margin-left: auto;
|
|
797
|
+
border-radius: 2px;
|
|
798
|
+
opacity: 0;
|
|
799
|
+
transition: opacity var(--transition-fast);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.color-row:hover .color-remove-btn {
|
|
803
|
+
opacity: 1;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.color-remove-btn:hover {
|
|
807
|
+
color: var(--text-primary);
|
|
808
|
+
background: var(--bg-primary);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.color-remove-btn :global(svg) {
|
|
812
|
+
width: 10px;
|
|
813
|
+
height: 10px;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
.color-editor-hint {
|
|
817
|
+
padding: var(--space-2) var(--space-3);
|
|
818
|
+
font-size: var(--text-xs);
|
|
819
|
+
color: var(--text-secondary);
|
|
820
|
+
text-align: center;
|
|
821
|
+
}
|
|
822
|
+
</style>
|