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