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,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation Menu Commands -- Play/Pause, Set Frame Duration dialog, plus
|
|
3
|
+
* menu items pointing at the add/duplicate/delete frame commands
|
|
4
|
+
* registered by the animation-commands plugin.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { PluginModule } from '../core/plugin-loader.js';
|
|
8
|
+
import { dispatch as dispatchCommand } from '../core/dispatcher.js';
|
|
9
|
+
import { dialogState } from './dialog-state.svelte.js';
|
|
10
|
+
import * as animPreview from '../animation/animation-preview.svelte.js';
|
|
11
|
+
import { getCurrentFrameIndex } from '../animation/frame-model.svelte.js';
|
|
12
|
+
import { hasFrameClipboard } from '../animation/animation-commands.js';
|
|
13
|
+
import Clock from '~icons/lucide/clock';
|
|
14
|
+
import Play from '~icons/lucide/play';
|
|
15
|
+
|
|
16
|
+
export const animationMenuCommandsPlugin: PluginModule = {
|
|
17
|
+
name: 'ui/animation-menu-commands',
|
|
18
|
+
version: '1.0.0',
|
|
19
|
+
dependencies: [],
|
|
20
|
+
register(api) {
|
|
21
|
+
// add_frame, duplicate_frame, remove_frame, set_frame_duration are registered by
|
|
22
|
+
// animation-commands plugin; we only add menu items for them.
|
|
23
|
+
|
|
24
|
+
api.addMenuItem('menu:animation:add-frame', {
|
|
25
|
+
commandId: 'add_frame',
|
|
26
|
+
menuPath: 'animation',
|
|
27
|
+
group: 'frames',
|
|
28
|
+
order: 10,
|
|
29
|
+
label: 'Add Frame',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
api.addMenuItem('menu:animation:duplicate-frame', {
|
|
33
|
+
commandId: 'duplicate_frame',
|
|
34
|
+
menuPath: 'animation',
|
|
35
|
+
group: 'frames',
|
|
36
|
+
order: 20,
|
|
37
|
+
label: 'Duplicate Frame',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
api.addMenuItem('menu:animation:delete-frame', {
|
|
41
|
+
commandId: 'remove_frame',
|
|
42
|
+
menuPath: 'animation',
|
|
43
|
+
group: 'frames',
|
|
44
|
+
order: 30,
|
|
45
|
+
label: 'Delete Frame',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
api.addMenuItem('menu:animation:delete-selected', {
|
|
49
|
+
commandId: 'delete_selected_frames',
|
|
50
|
+
menuPath: 'animation',
|
|
51
|
+
group: 'frames',
|
|
52
|
+
order: 35,
|
|
53
|
+
label: 'Delete Selected Frames',
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
api.addMenuItem('menu:animation:copy-frame', {
|
|
57
|
+
commandId: 'copy_frame',
|
|
58
|
+
menuPath: 'animation',
|
|
59
|
+
group: 'clipboard',
|
|
60
|
+
order: 10,
|
|
61
|
+
label: 'Copy Frame',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
api.addMenuItem('menu:animation:paste-frame', {
|
|
65
|
+
commandId: 'paste_frame',
|
|
66
|
+
menuPath: 'animation',
|
|
67
|
+
group: 'clipboard',
|
|
68
|
+
order: 20,
|
|
69
|
+
label: 'Paste Frame',
|
|
70
|
+
enabled: () => hasFrameClipboard(),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
api.addCommand('play_animation', {
|
|
74
|
+
tier: 'project',
|
|
75
|
+
execute() {
|
|
76
|
+
if (animPreview.isPlaying()) {
|
|
77
|
+
animPreview.pause();
|
|
78
|
+
} else {
|
|
79
|
+
animPreview.play();
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
undo() {},
|
|
83
|
+
describe() { return 'Toggled animation playback'; },
|
|
84
|
+
label: 'Play/Pause',
|
|
85
|
+
category: 'Animation',
|
|
86
|
+
defaultShortcut: 'Space',
|
|
87
|
+
icon: Play,
|
|
88
|
+
});
|
|
89
|
+
api.addMenuItem('menu:animation:play', {
|
|
90
|
+
commandId: 'play_animation',
|
|
91
|
+
menuPath: 'animation',
|
|
92
|
+
group: 'playback',
|
|
93
|
+
order: 40,
|
|
94
|
+
label: 'Play/Pause',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Wrapper command that prompts for duration then dispatches to the real
|
|
98
|
+
// set_frame_duration command (which expects index + durationMs params).
|
|
99
|
+
api.addCommand('set_frame_duration_dialog', {
|
|
100
|
+
tier: 'project',
|
|
101
|
+
execute() {
|
|
102
|
+
const frameIdx = getCurrentFrameIndex();
|
|
103
|
+
dialogState.openPrompt({
|
|
104
|
+
title: 'Set Frame Duration',
|
|
105
|
+
message: 'Enter frame duration in ms (or leave empty for global FPS):',
|
|
106
|
+
defaultValue: '',
|
|
107
|
+
placeholder: 'e.g. 100',
|
|
108
|
+
validate: (value) => {
|
|
109
|
+
const trimmed = value.trim();
|
|
110
|
+
// Empty is allowed -- means "reset to global FPS".
|
|
111
|
+
if (trimmed === '') return null;
|
|
112
|
+
const ms = parseInt(trimmed, 10);
|
|
113
|
+
if (isNaN(ms) || ms <= 0 || String(ms) !== trimmed) {
|
|
114
|
+
return 'Must be a positive integer (or empty for global FPS)';
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
},
|
|
118
|
+
onConfirm: (value) => {
|
|
119
|
+
const trimmed = value.trim();
|
|
120
|
+
// Empty input resets the per-frame override so the frame follows
|
|
121
|
+
// the project-wide globalFps again.
|
|
122
|
+
const durationMs = trimmed === '' ? null : parseInt(trimmed, 10);
|
|
123
|
+
dispatchCommand({
|
|
124
|
+
type: 'set_frame_duration',
|
|
125
|
+
plugin: 'ui/animation-menu-commands',
|
|
126
|
+
version: '1.0.0',
|
|
127
|
+
params: { index: frameIdx, durationMs },
|
|
128
|
+
timestamp: Date.now(),
|
|
129
|
+
id: crypto.randomUUID(),
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
undo() {},
|
|
135
|
+
describe() { return 'Opened frame duration dialog'; },
|
|
136
|
+
label: 'Set Frame Duration...',
|
|
137
|
+
category: 'Animation',
|
|
138
|
+
icon: Clock,
|
|
139
|
+
});
|
|
140
|
+
api.addMenuItem('menu:animation:frame-duration', {
|
|
141
|
+
commandId: 'set_frame_duration_dialog',
|
|
142
|
+
menuPath: 'animation',
|
|
143
|
+
group: 'playback',
|
|
144
|
+
order: 50,
|
|
145
|
+
label: 'Set Frame Duration...',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- Context menu: frame ---
|
|
149
|
+
|
|
150
|
+
api.addMenuItem('ctx:frame:add', {
|
|
151
|
+
commandId: 'add_frame',
|
|
152
|
+
menuPath: 'context/frame',
|
|
153
|
+
group: 'create',
|
|
154
|
+
order: 10,
|
|
155
|
+
label: 'Add Frame',
|
|
156
|
+
});
|
|
157
|
+
api.addMenuItem('ctx:frame:duplicate', {
|
|
158
|
+
commandId: 'duplicate_frame',
|
|
159
|
+
menuPath: 'context/frame',
|
|
160
|
+
group: 'create',
|
|
161
|
+
order: 20,
|
|
162
|
+
label: 'Duplicate Frame',
|
|
163
|
+
});
|
|
164
|
+
api.addMenuItem('ctx:frame:copy', {
|
|
165
|
+
commandId: 'copy_frame',
|
|
166
|
+
menuPath: 'context/frame',
|
|
167
|
+
group: 'clipboard',
|
|
168
|
+
order: 10,
|
|
169
|
+
label: 'Copy Frame',
|
|
170
|
+
});
|
|
171
|
+
api.addMenuItem('ctx:frame:paste', {
|
|
172
|
+
commandId: 'paste_frame',
|
|
173
|
+
menuPath: 'context/frame',
|
|
174
|
+
group: 'clipboard',
|
|
175
|
+
order: 20,
|
|
176
|
+
label: 'Paste Frame',
|
|
177
|
+
enabled: () => hasFrameClipboard(),
|
|
178
|
+
});
|
|
179
|
+
api.addMenuItem('ctx:frame:delete', {
|
|
180
|
+
commandId: 'remove_frame',
|
|
181
|
+
menuPath: 'context/frame',
|
|
182
|
+
group: 'manage',
|
|
183
|
+
order: 10,
|
|
184
|
+
label: 'Delete Frame',
|
|
185
|
+
});
|
|
186
|
+
api.addMenuItem('ctx:frame:delete-selected', {
|
|
187
|
+
commandId: 'delete_selected_frames',
|
|
188
|
+
menuPath: 'context/frame',
|
|
189
|
+
group: 'manage',
|
|
190
|
+
order: 15,
|
|
191
|
+
label: 'Delete Selected Frames',
|
|
192
|
+
});
|
|
193
|
+
},
|
|
194
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* CommandPalette -- overlay modal for searching and executing commands,
|
|
4
|
+
* tools, and shortcuts. Opened via Ctrl+P (bound in the plugin).
|
|
5
|
+
*
|
|
6
|
+
* Uses the native <dialog> element for focus trap, ARIA semantics, and
|
|
7
|
+
* automatic Escape handling. Escape triggers the `cancel` event which we
|
|
8
|
+
* forward to commandPaletteState.close(). Arrow/Enter keys are still
|
|
9
|
+
* handled manually since those control our custom list selection.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { commandPaletteState } from './command-palette-state.svelte.js';
|
|
13
|
+
|
|
14
|
+
let dialogEl: HTMLDialogElement | undefined = $state();
|
|
15
|
+
let inputEl: HTMLInputElement | undefined = $state();
|
|
16
|
+
|
|
17
|
+
// Sync native <dialog> open state with the palette state.
|
|
18
|
+
// Auto-focus the input once the dialog is open.
|
|
19
|
+
$effect(() => {
|
|
20
|
+
if (!dialogEl) return;
|
|
21
|
+
if (commandPaletteState.isOpen && !dialogEl.open) {
|
|
22
|
+
dialogEl.showModal();
|
|
23
|
+
// Use a microtask so the DOM has settled after showModal()
|
|
24
|
+
queueMicrotask(() => inputEl?.focus());
|
|
25
|
+
} else if (!commandPaletteState.isOpen && dialogEl.open) {
|
|
26
|
+
dialogEl.close();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Let <dialog> handle Escape via its cancel event. We only need to handle
|
|
31
|
+
// the custom list-navigation keys here.
|
|
32
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
33
|
+
if (e.key === 'ArrowDown') {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
commandPaletteState.moveDown();
|
|
36
|
+
} else if (e.key === 'ArrowUp') {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
commandPaletteState.moveUp();
|
|
39
|
+
} else if (e.key === 'Enter') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
commandPaletteState.executeSelected();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Click on the <dialog> itself (not children) means the user clicked the
|
|
46
|
+
// backdrop area -- close the palette.
|
|
47
|
+
function handleDialogClick(e: MouseEvent) {
|
|
48
|
+
if (e.target === dialogEl) {
|
|
49
|
+
commandPaletteState.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleInput(e: Event) {
|
|
54
|
+
commandPaletteState.query = (e.target as HTMLInputElement).value;
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<dialog
|
|
59
|
+
bind:this={dialogEl}
|
|
60
|
+
class="cp-dialog"
|
|
61
|
+
aria-label="Command palette"
|
|
62
|
+
onclick={handleDialogClick}
|
|
63
|
+
onkeydown={handleKeydown}
|
|
64
|
+
onclose={() => { commandPaletteState.close(); }}
|
|
65
|
+
>
|
|
66
|
+
{#if commandPaletteState.isOpen}
|
|
67
|
+
<div class="cp-panel">
|
|
68
|
+
<input
|
|
69
|
+
bind:this={inputEl}
|
|
70
|
+
class="cp-input"
|
|
71
|
+
type="text"
|
|
72
|
+
placeholder="Type a command..."
|
|
73
|
+
role="combobox"
|
|
74
|
+
aria-controls="cp-listbox"
|
|
75
|
+
aria-expanded={commandPaletteState.results.length > 0}
|
|
76
|
+
aria-autocomplete="list"
|
|
77
|
+
aria-activedescendant={commandPaletteState.results.length > 0
|
|
78
|
+
? `cp-option-${String(commandPaletteState.selectedIndex)}`
|
|
79
|
+
: undefined}
|
|
80
|
+
value={commandPaletteState.query}
|
|
81
|
+
oninput={handleInput}
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<ul id="cp-listbox" class="cp-list" role="listbox">
|
|
85
|
+
{#each commandPaletteState.results as item, i (item.id)}
|
|
86
|
+
<li
|
|
87
|
+
id="cp-option-{i}"
|
|
88
|
+
class="cp-item"
|
|
89
|
+
class:cp-item--selected={i === commandPaletteState.selectedIndex}
|
|
90
|
+
role="option"
|
|
91
|
+
aria-selected={i === commandPaletteState.selectedIndex}
|
|
92
|
+
onclick={() => {
|
|
93
|
+
commandPaletteState.selectedIndex = i;
|
|
94
|
+
commandPaletteState.executeSelected();
|
|
95
|
+
}}
|
|
96
|
+
onmouseenter={() => { commandPaletteState.selectedIndex = i; }}
|
|
97
|
+
>
|
|
98
|
+
<span class="cp-item-name">{item.name}</span>
|
|
99
|
+
{#if item.description}
|
|
100
|
+
<span class="cp-item-desc">{item.description}</span>
|
|
101
|
+
{/if}
|
|
102
|
+
{#if item.shortcutKey}
|
|
103
|
+
<kbd class="cp-item-kbd">{item.shortcutKey}</kbd>
|
|
104
|
+
{/if}
|
|
105
|
+
</li>
|
|
106
|
+
{/each}
|
|
107
|
+
|
|
108
|
+
{#if commandPaletteState.results.length === 0}
|
|
109
|
+
<li class="cp-empty">No matching commands</li>
|
|
110
|
+
{/if}
|
|
111
|
+
</ul>
|
|
112
|
+
</div>
|
|
113
|
+
{/if}
|
|
114
|
+
</dialog>
|
|
115
|
+
|
|
116
|
+
<style>
|
|
117
|
+
/* Reset native <dialog> UA styles. The palette is positioned near the top
|
|
118
|
+
of the viewport (not centered), matching the previous overlay layout. */
|
|
119
|
+
.cp-dialog {
|
|
120
|
+
margin: 0;
|
|
121
|
+
padding: 0;
|
|
122
|
+
border: none;
|
|
123
|
+
background: transparent;
|
|
124
|
+
max-width: none;
|
|
125
|
+
max-height: none;
|
|
126
|
+
width: 100vw;
|
|
127
|
+
height: 100vh;
|
|
128
|
+
color: inherit;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.cp-dialog[open] {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: flex-start;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
padding-top: 15vh;
|
|
136
|
+
box-sizing: border-box;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.cp-dialog::backdrop {
|
|
140
|
+
background: rgba(0, 0, 0, 0.5);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.cp-panel {
|
|
144
|
+
background: var(--bg-panel);
|
|
145
|
+
border: 1px solid var(--border);
|
|
146
|
+
border-radius: 8px;
|
|
147
|
+
width: min(500px, 90vw);
|
|
148
|
+
max-height: 60vh;
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-direction: column;
|
|
151
|
+
overflow: hidden;
|
|
152
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.cp-input {
|
|
156
|
+
width: 100%;
|
|
157
|
+
padding: 12px 16px;
|
|
158
|
+
border: none;
|
|
159
|
+
border-bottom: 1px solid var(--border);
|
|
160
|
+
background: transparent;
|
|
161
|
+
color: var(--text-primary);
|
|
162
|
+
font-size: 14px;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.cp-input::placeholder {
|
|
166
|
+
color: var(--text-muted);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.cp-list {
|
|
170
|
+
list-style: none;
|
|
171
|
+
overflow-y: auto;
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 4px 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.cp-item {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 8px;
|
|
180
|
+
padding: 8px 16px;
|
|
181
|
+
cursor: pointer;
|
|
182
|
+
color: var(--text-primary);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.cp-item--selected {
|
|
186
|
+
background: var(--accent);
|
|
187
|
+
color: #ffffff;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.cp-item-name {
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
white-space: nowrap;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.cp-item-desc {
|
|
196
|
+
flex: 1;
|
|
197
|
+
color: var(--text-secondary);
|
|
198
|
+
font-size: 12px;
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
text-overflow: ellipsis;
|
|
201
|
+
white-space: nowrap;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.cp-item--selected .cp-item-desc {
|
|
205
|
+
color: rgba(255, 255, 255, 0.7);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.cp-item-kbd {
|
|
209
|
+
margin-left: auto;
|
|
210
|
+
padding: 2px 6px;
|
|
211
|
+
border-radius: 3px;
|
|
212
|
+
border: 1px solid var(--border);
|
|
213
|
+
background: var(--bg-secondary);
|
|
214
|
+
color: var(--text-secondary);
|
|
215
|
+
font-family: var(--font-mono);
|
|
216
|
+
font-size: 11px;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cp-item--selected .cp-item-kbd {
|
|
221
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
222
|
+
background: rgba(255, 255, 255, 0.15);
|
|
223
|
+
color: #ffffff;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.cp-empty {
|
|
227
|
+
padding: 16px;
|
|
228
|
+
text-align: center;
|
|
229
|
+
color: var(--text-muted);
|
|
230
|
+
font-style: italic;
|
|
231
|
+
}
|
|
232
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Palette Plugin -- binds the Ctrl+P shortcut to toggle the
|
|
3
|
+
* palette overlay (rendered directly in App.svelte).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PluginModule } from '../../core/plugin-loader.js';
|
|
7
|
+
import { commandPaletteState } from './command-palette-state.svelte.js';
|
|
8
|
+
|
|
9
|
+
export const commandPalettePlugin: PluginModule = {
|
|
10
|
+
name: 'builtin/command-palette',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
dependencies: [],
|
|
13
|
+
|
|
14
|
+
register(api) {
|
|
15
|
+
// Command Palette is rendered as a floating overlay in App.svelte,
|
|
16
|
+
// not as a docked panel. Only the shortcut is registered here.
|
|
17
|
+
|
|
18
|
+
api.addShortcut('command-palette.toggle', {
|
|
19
|
+
key: 'Ctrl+P',
|
|
20
|
+
description: 'Open command palette',
|
|
21
|
+
action() {
|
|
22
|
+
if (commandPaletteState.isOpen) {
|
|
23
|
+
commandPaletteState.close();
|
|
24
|
+
} else {
|
|
25
|
+
commandPaletteState.open();
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Palette State -- reactive singleton for the command palette UI.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates commands, tools, and shortcuts from the registries and provides
|
|
5
|
+
* fuzzy filtering based on user input. Uses Svelte 5 runes for reactivity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
commandRegistry,
|
|
10
|
+
toolRegistry,
|
|
11
|
+
shortcutRegistry,
|
|
12
|
+
} from '../../core/registries.svelte.js';
|
|
13
|
+
import { executeOrDispatch } from '../../core/command-runner.js';
|
|
14
|
+
import type { CommandDefinition } from '../../core/commands.js';
|
|
15
|
+
import { getActiveTool, setActiveTool } from '../../shortcuts/shortcut-state.svelte.js';
|
|
16
|
+
|
|
17
|
+
// --- Types ---
|
|
18
|
+
|
|
19
|
+
export interface PaletteItem {
|
|
20
|
+
/** Unique key used for dedup and execution */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Display name */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Optional description text */
|
|
25
|
+
description: string;
|
|
26
|
+
/** Category tag shown in the UI */
|
|
27
|
+
category: 'command' | 'tool' | 'shortcut';
|
|
28
|
+
/** Keyboard shortcut string if one is bound (e.g. "Ctrl+Z") */
|
|
29
|
+
shortcutKey?: string;
|
|
30
|
+
/** Callback to execute this item */
|
|
31
|
+
execute: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Reactive state ---
|
|
35
|
+
|
|
36
|
+
let isOpen = $state(false);
|
|
37
|
+
let query = $state('');
|
|
38
|
+
let selectedIndex = $state(0);
|
|
39
|
+
|
|
40
|
+
// --- Helpers ---
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Collect every registered command, tool, and shortcut into a flat list of
|
|
44
|
+
* PaletteItems. Called on every filter pass so that newly-registered
|
|
45
|
+
* extensions appear immediately.
|
|
46
|
+
*/
|
|
47
|
+
function gatherItems(): PaletteItem[] {
|
|
48
|
+
const items: PaletteItem[] = [];
|
|
49
|
+
|
|
50
|
+
for (const [name, rawDef] of commandRegistry.getAll()) {
|
|
51
|
+
// Registry stores CommandDefinition<any> as a type-erasure seam.
|
|
52
|
+
// Narrow through unknown to the default-P shape.
|
|
53
|
+
const def = rawDef as unknown as CommandDefinition;
|
|
54
|
+
items.push({
|
|
55
|
+
id: `cmd:${name}`,
|
|
56
|
+
name: def.label ?? name.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()),
|
|
57
|
+
description: def.category ?? def.describe({}),
|
|
58
|
+
category: 'command',
|
|
59
|
+
execute: () => { executeOrDispatch(name, def); },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [name] of toolRegistry.getAll()) {
|
|
64
|
+
const displayName = name.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
65
|
+
items.push({
|
|
66
|
+
id: `tool:${name}`,
|
|
67
|
+
name: displayName,
|
|
68
|
+
description: 'Activate tool',
|
|
69
|
+
category: 'tool',
|
|
70
|
+
execute: () => {
|
|
71
|
+
const prev = getActiveTool();
|
|
72
|
+
if (prev) {
|
|
73
|
+
const prevTool = toolRegistry.get(prev);
|
|
74
|
+
prevTool?.onDeactivate?.();
|
|
75
|
+
}
|
|
76
|
+
setActiveTool(name);
|
|
77
|
+
const tool = toolRegistry.get(name);
|
|
78
|
+
tool?.onActivate?.();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const [name, def] of shortcutRegistry.getAll()) {
|
|
84
|
+
items.push({
|
|
85
|
+
id: `shortcut:${name}`,
|
|
86
|
+
name,
|
|
87
|
+
description: def.description,
|
|
88
|
+
category: 'shortcut',
|
|
89
|
+
shortcutKey: def.key,
|
|
90
|
+
execute: def.action,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return items;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Fuzzy match: split the query into whitespace-separated words. Each word
|
|
99
|
+
* must appear as a substring (case-insensitive) somewhere in either the item
|
|
100
|
+
* name or its description.
|
|
101
|
+
*/
|
|
102
|
+
export function fuzzyMatch(item: PaletteItem, q: string): boolean {
|
|
103
|
+
if (q === '') return true;
|
|
104
|
+
const words = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
105
|
+
const haystack = `${item.name} ${item.description}`.toLowerCase();
|
|
106
|
+
return words.every((w) => haystack.includes(w));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Score an item against the query for ranking. Lower score = better match.
|
|
111
|
+
* - Exact prefix match on the name is best (score 0)
|
|
112
|
+
* - Name contains query as substring (score 1)
|
|
113
|
+
* - Matched via description only (score 2)
|
|
114
|
+
*/
|
|
115
|
+
export function relevanceScore(item: PaletteItem, q: string): number {
|
|
116
|
+
if (q === '') return 0;
|
|
117
|
+
const lowerQ = q.toLowerCase();
|
|
118
|
+
const lowerName = item.name.toLowerCase();
|
|
119
|
+
|
|
120
|
+
if (lowerName.startsWith(lowerQ)) return 0;
|
|
121
|
+
if (lowerName.includes(lowerQ)) return 1;
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Derived: filtered & sorted results ---
|
|
126
|
+
|
|
127
|
+
// NOTE: $derived cannot be used at module scope in .svelte.ts files in Svelte 5,
|
|
128
|
+
// so we expose a getter that performs the derivation on access.
|
|
129
|
+
|
|
130
|
+
function getFilteredResults(): PaletteItem[] {
|
|
131
|
+
const q = query;
|
|
132
|
+
const all = gatherItems();
|
|
133
|
+
const matched = all.filter((item) => fuzzyMatch(item, q));
|
|
134
|
+
matched.sort((a, b) => relevanceScore(a, q) - relevanceScore(b, q));
|
|
135
|
+
return matched;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Public API ---
|
|
139
|
+
|
|
140
|
+
export const commandPaletteState = {
|
|
141
|
+
get isOpen() { return isOpen; },
|
|
142
|
+
get query() { return query; },
|
|
143
|
+
set query(v: string) {
|
|
144
|
+
query = v;
|
|
145
|
+
// Reset selection whenever the query changes
|
|
146
|
+
selectedIndex = 0;
|
|
147
|
+
},
|
|
148
|
+
get selectedIndex() { return selectedIndex; },
|
|
149
|
+
set selectedIndex(v: number) { selectedIndex = v; },
|
|
150
|
+
|
|
151
|
+
get results() { return getFilteredResults(); },
|
|
152
|
+
|
|
153
|
+
open() {
|
|
154
|
+
isOpen = true;
|
|
155
|
+
query = '';
|
|
156
|
+
selectedIndex = 0;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
close() {
|
|
160
|
+
isOpen = false;
|
|
161
|
+
query = '';
|
|
162
|
+
selectedIndex = 0;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/** Execute the currently selected item and close the palette. */
|
|
166
|
+
executeSelected() {
|
|
167
|
+
const results = getFilteredResults();
|
|
168
|
+
const item = results[selectedIndex];
|
|
169
|
+
if (item) {
|
|
170
|
+
item.execute();
|
|
171
|
+
}
|
|
172
|
+
isOpen = false;
|
|
173
|
+
query = '';
|
|
174
|
+
selectedIndex = 0;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/** Move selection up by one, wrapping around. */
|
|
178
|
+
moveUp() {
|
|
179
|
+
const count = getFilteredResults().length;
|
|
180
|
+
if (count === 0) return;
|
|
181
|
+
selectedIndex = (selectedIndex - 1 + count) % count;
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/** Move selection down by one, wrapping around. */
|
|
185
|
+
moveDown() {
|
|
186
|
+
const count = getFilteredResults().length;
|
|
187
|
+
if (count === 0) return;
|
|
188
|
+
selectedIndex = (selectedIndex + 1) % count;
|
|
189
|
+
},
|
|
190
|
+
};
|