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,590 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
MenuBar -- dynamic, registry-driven horizontal menu bar with submenu support.
|
|
3
|
+
|
|
4
|
+
Reads from menuRegistry via buildMenuBar() to construct menus reactively.
|
|
5
|
+
Supports nested submenus, group separators, disabled items, and keyboard
|
|
6
|
+
shortcuts. Retains click-to-open, hover-to-switch, escape-to-close, and
|
|
7
|
+
click-outside-to-close behavior.
|
|
8
|
+
|
|
9
|
+
Keyboard navigation (delegated to a single keydown listener on the menubar):
|
|
10
|
+
- Left/Right on a closed top-level trigger: move focus between top-level
|
|
11
|
+
triggers. If a menu is already open, switch to the next/prev menu and
|
|
12
|
+
keep it open.
|
|
13
|
+
- Down on a focused top-level trigger: open the menu and focus its first
|
|
14
|
+
non-separator, non-disabled item.
|
|
15
|
+
- Down/Up inside a menu: move focus to the next/previous focusable item
|
|
16
|
+
(wraps). Home/End jump to first/last focusable item.
|
|
17
|
+
- Right on a submenu-parent item: open the submenu and focus its first
|
|
18
|
+
item. On a regular item inside a top-level menu, Right moves to the
|
|
19
|
+
next top-level menu (standard menubar behavior).
|
|
20
|
+
- Left inside a submenu: close the submenu and restore focus to its
|
|
21
|
+
parent item.
|
|
22
|
+
- Left on a regular item of a top-level menu (not a submenu parent):
|
|
23
|
+
move to the previous top-level menu.
|
|
24
|
+
- Enter/Space: activate the focused item (same as click).
|
|
25
|
+
- Escape: close the current level; if nested, go up one level; if a
|
|
26
|
+
top-level menu, close it and return focus to the trigger.
|
|
27
|
+
- Character typing: jumps to the next focusable item in the current
|
|
28
|
+
level whose label starts with that letter (case-insensitive).
|
|
29
|
+
- Roving tabindex: exactly one element in the currently-active level
|
|
30
|
+
has tabindex=0; the rest have tabindex=-1. DOM focus is moved via an
|
|
31
|
+
$effect that watches the focusedItemId.
|
|
32
|
+
-->
|
|
33
|
+
<script lang="ts">
|
|
34
|
+
import { onMount, tick } from 'svelte';
|
|
35
|
+
import { buildMenuBar, type MenuItem } from './menu-builder.js';
|
|
36
|
+
import ChevronRight from '~icons/lucide/chevron-right';
|
|
37
|
+
|
|
38
|
+
// --- Reactive menu tree ---
|
|
39
|
+
|
|
40
|
+
let menus = $derived(buildMenuBar());
|
|
41
|
+
|
|
42
|
+
// --- Dropdown state ---
|
|
43
|
+
|
|
44
|
+
let openMenu: string | null = $state(null);
|
|
45
|
+
let openSubmenu: string | null = $state(null);
|
|
46
|
+
|
|
47
|
+
// Roving tabindex: id of the currently focused item at the deepest open
|
|
48
|
+
// level. When no menu is open, this is the label of the focused top-level
|
|
49
|
+
// trigger (or null if nothing in the menubar has focus). When a menu is
|
|
50
|
+
// open with no submenu, it's the item id in the top-level dropdown. When
|
|
51
|
+
// a submenu is open, it's the child item id in the submenu.
|
|
52
|
+
let focusedItemId: string | null = $state(null);
|
|
53
|
+
|
|
54
|
+
let menuBarRef: HTMLElement | undefined = $state();
|
|
55
|
+
|
|
56
|
+
// --- Focusable-item helpers ---
|
|
57
|
+
|
|
58
|
+
/** Items that can receive focus/activation (skips separators and disabled items). */
|
|
59
|
+
function focusableItems(items: MenuItem[]): MenuItem[] {
|
|
60
|
+
return items.filter((i) => !i.separator && !i.disabled);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Get the focusable list for the current level (submenu > menu). */
|
|
64
|
+
function currentLevelItems(): MenuItem[] {
|
|
65
|
+
if (openMenu === null) return [];
|
|
66
|
+
const menu = menus.find((m) => m.label === openMenu);
|
|
67
|
+
if (!menu) return [];
|
|
68
|
+
if (openSubmenu !== null) {
|
|
69
|
+
const parent = menu.items.find((i) => i.id === openSubmenu);
|
|
70
|
+
if (parent?.children) return focusableItems(parent.children);
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
return focusableItems(menu.items);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Opening / closing helpers ---
|
|
77
|
+
|
|
78
|
+
/** Open the given top-level menu and focus its first focusable item. */
|
|
79
|
+
function openTopMenu(label: string, focusFirst = true): void {
|
|
80
|
+
openMenu = label;
|
|
81
|
+
openSubmenu = null;
|
|
82
|
+
if (focusFirst) {
|
|
83
|
+
const menu = menus.find((m) => m.label === label);
|
|
84
|
+
const first = menu ? focusableItems(menu.items)[0] : undefined;
|
|
85
|
+
focusedItemId = first ? first.id : null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Close all menus and return focus to the trigger of the menu that was open. */
|
|
90
|
+
function closeAllMenus(restoreFocusToTrigger = true): void {
|
|
91
|
+
const restoreLabel = openMenu;
|
|
92
|
+
openMenu = null;
|
|
93
|
+
openSubmenu = null;
|
|
94
|
+
if (restoreFocusToTrigger && restoreLabel !== null) {
|
|
95
|
+
focusedItemId = restoreLabel; // trigger row
|
|
96
|
+
} else if (!restoreFocusToTrigger) {
|
|
97
|
+
focusedItemId = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Open a submenu and focus its first child. */
|
|
102
|
+
function openSubmenuAt(parentId: string): void {
|
|
103
|
+
openSubmenu = parentId;
|
|
104
|
+
const menu = menus.find((m) => m.label === openMenu);
|
|
105
|
+
const parent = menu?.items.find((i) => i.id === parentId);
|
|
106
|
+
const first = parent?.children ? focusableItems(parent.children)[0] : undefined;
|
|
107
|
+
focusedItemId = first ? first.id : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Close the open submenu and restore focus to the parent item. */
|
|
111
|
+
function closeSubmenu(): void {
|
|
112
|
+
if (openSubmenu === null) return;
|
|
113
|
+
const parentId = openSubmenu;
|
|
114
|
+
openSubmenu = null;
|
|
115
|
+
focusedItemId = parentId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Mouse handlers (retain prior behavior) ---
|
|
119
|
+
|
|
120
|
+
function toggleMenu(label: string): void {
|
|
121
|
+
if (openMenu === label) {
|
|
122
|
+
closeAllMenus(true);
|
|
123
|
+
} else {
|
|
124
|
+
openTopMenu(label, true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function handleMenuHover(label: string): void {
|
|
129
|
+
// Only switch on hover if a menu is already open
|
|
130
|
+
if (openMenu !== null && openMenu !== label) {
|
|
131
|
+
openTopMenu(label, true);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleTriggerFocus(label: string): void {
|
|
136
|
+
// When no menu is open, track which trigger has DOM focus so that
|
|
137
|
+
// Down/Enter act on it. Do NOT open the menu on focus alone.
|
|
138
|
+
if (openMenu === null) {
|
|
139
|
+
focusedItemId = label;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function activateItem(item: MenuItem): void {
|
|
144
|
+
if (item.separator || item.disabled) return;
|
|
145
|
+
if (item.children) {
|
|
146
|
+
openSubmenuAt(item.id);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
item.action?.();
|
|
150
|
+
closeAllMenus(false);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleItemClick(item: MenuItem): void {
|
|
154
|
+
activateItem(item);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// --- Character typing (type-ahead) ---
|
|
158
|
+
|
|
159
|
+
/** Find the next focusable item whose label starts with `ch` (case-insensitive). */
|
|
160
|
+
function typeAheadJump(ch: string): void {
|
|
161
|
+
const items = currentLevelItems();
|
|
162
|
+
if (items.length === 0) return;
|
|
163
|
+
const lower = ch.toLowerCase();
|
|
164
|
+
const currentIdx = items.findIndex((i) => i.id === focusedItemId);
|
|
165
|
+
// Search starting AFTER current, wrap around.
|
|
166
|
+
for (let step = 1; step <= items.length; step++) {
|
|
167
|
+
const it = items[(currentIdx + step) % items.length];
|
|
168
|
+
if (it && it.label.toLowerCase().startsWith(lower)) {
|
|
169
|
+
focusedItemId = it.id;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Central keydown handler ---
|
|
176
|
+
|
|
177
|
+
function handleKeydown(e: KeyboardEvent): void {
|
|
178
|
+
// Ignore unless focus is somewhere in the menubar DOM subtree.
|
|
179
|
+
const target = e.target as Node | null;
|
|
180
|
+
if (!menuBarRef || !target || !menuBarRef.contains(target)) return;
|
|
181
|
+
|
|
182
|
+
const key = e.key;
|
|
183
|
+
|
|
184
|
+
// --- Case A: no menu open, focus on a top-level trigger ---
|
|
185
|
+
if (openMenu === null) {
|
|
186
|
+
if (focusedItemId === null) return;
|
|
187
|
+
const idx = menus.findIndex((m) => m.label === focusedItemId);
|
|
188
|
+
if (idx < 0) return;
|
|
189
|
+
|
|
190
|
+
if (key === 'ArrowRight' || key === 'ArrowLeft') {
|
|
191
|
+
const delta = key === 'ArrowRight' ? 1 : -1;
|
|
192
|
+
const next = menus[(idx + delta + menus.length) % menus.length];
|
|
193
|
+
if (next) {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
focusedItemId = next.label;
|
|
196
|
+
}
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (key === 'ArrowDown' || key === 'Enter' || key === ' ') {
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
const m = menus[idx];
|
|
202
|
+
if (m) openTopMenu(m.label, true);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (key === 'Home') {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
const first = menus[0];
|
|
208
|
+
if (first) focusedItemId = first.label;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (key === 'End') {
|
|
212
|
+
e.preventDefault();
|
|
213
|
+
const last = menus[menus.length - 1];
|
|
214
|
+
if (last) focusedItemId = last.label;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- Case B: a top-level menu is open (possibly with a submenu). ---
|
|
221
|
+
|
|
222
|
+
if (key === 'Escape') {
|
|
223
|
+
e.preventDefault();
|
|
224
|
+
if (openSubmenu !== null) {
|
|
225
|
+
closeSubmenu();
|
|
226
|
+
} else {
|
|
227
|
+
closeAllMenus(true);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const items = currentLevelItems();
|
|
233
|
+
|
|
234
|
+
if (key === 'ArrowDown') {
|
|
235
|
+
e.preventDefault();
|
|
236
|
+
if (items.length === 0) return;
|
|
237
|
+
const i = items.findIndex((it) => it.id === focusedItemId);
|
|
238
|
+
const next = items[(i + 1 + items.length) % items.length];
|
|
239
|
+
if (next) focusedItemId = next.id;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (key === 'ArrowUp') {
|
|
243
|
+
e.preventDefault();
|
|
244
|
+
if (items.length === 0) return;
|
|
245
|
+
const i = items.findIndex((it) => it.id === focusedItemId);
|
|
246
|
+
const prev = items[(i - 1 + items.length) % items.length];
|
|
247
|
+
if (prev) focusedItemId = prev.id;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (key === 'Home') {
|
|
251
|
+
e.preventDefault();
|
|
252
|
+
const first = items[0];
|
|
253
|
+
if (first) focusedItemId = first.id;
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (key === 'End') {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
const last = items[items.length - 1];
|
|
259
|
+
if (last) focusedItemId = last.id;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (key === 'Enter' || key === ' ') {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
const focused = items.find((it) => it.id === focusedItemId);
|
|
265
|
+
if (focused) activateItem(focused);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (key === 'ArrowRight') {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
// If current item is a submenu parent, open it.
|
|
271
|
+
const focused = items.find((it) => it.id === focusedItemId);
|
|
272
|
+
if (focused?.children) {
|
|
273
|
+
openSubmenuAt(focused.id);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Otherwise, if inside a submenu, Right does nothing (no deeper level).
|
|
277
|
+
if (openSubmenu !== null) return;
|
|
278
|
+
// Inside a top-level menu, Right cycles to the next top-level menu.
|
|
279
|
+
const idx = menus.findIndex((m) => m.label === openMenu);
|
|
280
|
+
if (idx < 0) return;
|
|
281
|
+
const next = menus[(idx + 1) % menus.length];
|
|
282
|
+
if (next) openTopMenu(next.label, true);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (key === 'ArrowLeft') {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
// If inside a submenu, Left closes it and returns to parent.
|
|
288
|
+
if (openSubmenu !== null) {
|
|
289
|
+
closeSubmenu();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Inside a top-level menu: cycle to the previous top-level menu.
|
|
293
|
+
const idx = menus.findIndex((m) => m.label === openMenu);
|
|
294
|
+
if (idx < 0) return;
|
|
295
|
+
const prev = menus[(idx - 1 + menus.length) % menus.length];
|
|
296
|
+
if (prev) openTopMenu(prev.label, true);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Type-ahead: single printable character, no modifiers.
|
|
301
|
+
if (
|
|
302
|
+
key.length === 1 &&
|
|
303
|
+
!e.ctrlKey &&
|
|
304
|
+
!e.metaKey &&
|
|
305
|
+
!e.altKey &&
|
|
306
|
+
/\S/.test(key)
|
|
307
|
+
) {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
typeAheadJump(key);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Close menus when the user clicks outside the menu bar.
|
|
315
|
+
* Uses pointerdown in the capture phase so it fires before any
|
|
316
|
+
* stopPropagation() calls in child components (e.g. dockview panels).
|
|
317
|
+
*/
|
|
318
|
+
function handleOutsidePointerDown(e: PointerEvent): void {
|
|
319
|
+
if (menuBarRef && !menuBarRef.contains(e.target as Node)) {
|
|
320
|
+
closeAllMenus(false);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
onMount(() => {
|
|
325
|
+
window.addEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
326
|
+
return () => {
|
|
327
|
+
window.removeEventListener('pointerdown', handleOutsidePointerDown, true);
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// --- Focus effect: move DOM focus to the currently focused item ---
|
|
332
|
+
|
|
333
|
+
// After every state change that updates focusedItemId, wait for the DOM
|
|
334
|
+
// to update and then focus the matching element. We look up by the
|
|
335
|
+
// data-menu-item-id attribute within the menubar.
|
|
336
|
+
$effect(() => {
|
|
337
|
+
// Re-run when any of these change.
|
|
338
|
+
const id = focusedItemId;
|
|
339
|
+
void openMenu;
|
|
340
|
+
void openSubmenu;
|
|
341
|
+
if (!menuBarRef || id === null) return;
|
|
342
|
+
void tick().then(() => {
|
|
343
|
+
// menuBarRef is a `$state`-backed `bind:this` target; the tick()
|
|
344
|
+
// boundary loses the narrowing and the component may have
|
|
345
|
+
// unmounted between scheduling and execution.
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
347
|
+
if (!menuBarRef) return;
|
|
348
|
+
const el = menuBarRef.querySelector<HTMLElement>(
|
|
349
|
+
`[data-menu-item-id="${CSS.escape(id)}"]`,
|
|
350
|
+
);
|
|
351
|
+
if (el && document.activeElement !== el) {
|
|
352
|
+
el.focus();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
</script>
|
|
357
|
+
|
|
358
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
359
|
+
|
|
360
|
+
<nav class="menubar" bind:this={menuBarRef} role="menubar" aria-label="Application">
|
|
361
|
+
<span class="brand">PixelWeaver</span>
|
|
362
|
+
|
|
363
|
+
{#each menus as menu (menu.label)}
|
|
364
|
+
<div class="menu-trigger-wrapper" role="none">
|
|
365
|
+
<button
|
|
366
|
+
class="menu-trigger"
|
|
367
|
+
class:active={openMenu === menu.label}
|
|
368
|
+
role="menuitem"
|
|
369
|
+
aria-haspopup="menu"
|
|
370
|
+
aria-expanded={openMenu === menu.label}
|
|
371
|
+
data-menu-item-id={menu.label}
|
|
372
|
+
tabindex={focusedItemId === menu.label && openMenu === null ? 0 : -1}
|
|
373
|
+
onclick={() => { toggleMenu(menu.label); }}
|
|
374
|
+
onmouseenter={() => { handleMenuHover(menu.label); }}
|
|
375
|
+
onfocus={() => { handleTriggerFocus(menu.label); }}
|
|
376
|
+
>
|
|
377
|
+
{menu.label}
|
|
378
|
+
</button>
|
|
379
|
+
|
|
380
|
+
{#if openMenu === menu.label}
|
|
381
|
+
<div class="dropdown" role="menu" aria-label={menu.label}>
|
|
382
|
+
{#each menu.items as item (item.id)}
|
|
383
|
+
{#if item.separator}
|
|
384
|
+
<div class="separator" role="separator"></div>
|
|
385
|
+
{:else if item.children}
|
|
386
|
+
<!-- Submenu trigger -->
|
|
387
|
+
<div
|
|
388
|
+
class="dropdown-item submenu-trigger"
|
|
389
|
+
class:focused={focusedItemId === item.id && openSubmenu !== item.id}
|
|
390
|
+
role="menuitem"
|
|
391
|
+
tabindex={focusedItemId === item.id && openSubmenu === null ? 0 : -1}
|
|
392
|
+
data-menu-item-id={item.id}
|
|
393
|
+
aria-haspopup="menu"
|
|
394
|
+
aria-expanded={openSubmenu === item.id}
|
|
395
|
+
onmouseenter={() => { openSubmenu = item.id; focusedItemId = item.id; }}
|
|
396
|
+
onmouseleave={() => { if (openSubmenu === item.id) openSubmenu = null; }}
|
|
397
|
+
>
|
|
398
|
+
<span class="item-label">{item.label}</span>
|
|
399
|
+
<span class="submenu-arrow"><ChevronRight /></span>
|
|
400
|
+
{#if openSubmenu === item.id}
|
|
401
|
+
<div class="dropdown submenu" role="menu" aria-label={item.label}>
|
|
402
|
+
{#each item.children as child (child.id)}
|
|
403
|
+
{#if child.separator}
|
|
404
|
+
<div class="separator" role="separator"></div>
|
|
405
|
+
{:else}
|
|
406
|
+
<button
|
|
407
|
+
class="dropdown-item"
|
|
408
|
+
class:disabled={child.disabled ?? false}
|
|
409
|
+
class:focused={focusedItemId === child.id}
|
|
410
|
+
role="menuitem"
|
|
411
|
+
data-menu-item-id={child.id}
|
|
412
|
+
tabindex={focusedItemId === child.id ? 0 : -1}
|
|
413
|
+
onclick={() => { handleItemClick(child); }}
|
|
414
|
+
disabled={child.disabled ?? false}
|
|
415
|
+
>
|
|
416
|
+
<span class="item-label">{child.label}</span>
|
|
417
|
+
{#if child.shortcut}
|
|
418
|
+
<span class="item-shortcut">{child.shortcut}</span>
|
|
419
|
+
{/if}
|
|
420
|
+
</button>
|
|
421
|
+
{/if}
|
|
422
|
+
{/each}
|
|
423
|
+
</div>
|
|
424
|
+
{/if}
|
|
425
|
+
</div>
|
|
426
|
+
{:else}
|
|
427
|
+
<button
|
|
428
|
+
class="dropdown-item"
|
|
429
|
+
class:disabled={item.disabled ?? false}
|
|
430
|
+
class:focused={focusedItemId === item.id}
|
|
431
|
+
role="menuitem"
|
|
432
|
+
data-menu-item-id={item.id}
|
|
433
|
+
tabindex={focusedItemId === item.id ? 0 : -1}
|
|
434
|
+
onclick={() => { handleItemClick(item); }}
|
|
435
|
+
disabled={item.disabled ?? false}
|
|
436
|
+
>
|
|
437
|
+
<span class="item-label">{item.label}</span>
|
|
438
|
+
{#if item.shortcut}
|
|
439
|
+
<span class="item-shortcut">{item.shortcut}</span>
|
|
440
|
+
{/if}
|
|
441
|
+
</button>
|
|
442
|
+
{/if}
|
|
443
|
+
{/each}
|
|
444
|
+
</div>
|
|
445
|
+
{/if}
|
|
446
|
+
</div>
|
|
447
|
+
{/each}
|
|
448
|
+
</nav>
|
|
449
|
+
|
|
450
|
+
<style>
|
|
451
|
+
.menubar {
|
|
452
|
+
height: 36px;
|
|
453
|
+
background: var(--bg-toolbar);
|
|
454
|
+
border-bottom: 1px solid var(--border);
|
|
455
|
+
display: flex;
|
|
456
|
+
align-items: center;
|
|
457
|
+
padding: 0 12px;
|
|
458
|
+
flex-shrink: 0;
|
|
459
|
+
gap: 2px;
|
|
460
|
+
user-select: none;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.brand {
|
|
464
|
+
font-size: var(--text-xl);
|
|
465
|
+
font-weight: 600;
|
|
466
|
+
color: var(--accent);
|
|
467
|
+
letter-spacing: 0.5px;
|
|
468
|
+
margin-right: 12px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.menu-trigger-wrapper {
|
|
472
|
+
position: relative;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.menu-trigger {
|
|
476
|
+
height: 26px;
|
|
477
|
+
padding: 0 10px;
|
|
478
|
+
background: none;
|
|
479
|
+
border: none;
|
|
480
|
+
border-radius: var(--radius-sm);
|
|
481
|
+
color: var(--text-primary);
|
|
482
|
+
font-size: var(--text-lg);
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
display: flex;
|
|
485
|
+
align-items: center;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.menu-trigger:hover {
|
|
489
|
+
background: var(--bg-secondary);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.menu-trigger.active {
|
|
493
|
+
background: var(--bg-secondary);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.menu-trigger:focus-visible {
|
|
497
|
+
outline: 2px solid var(--accent);
|
|
498
|
+
outline-offset: -2px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/* --- Dropdown --- */
|
|
502
|
+
|
|
503
|
+
.dropdown {
|
|
504
|
+
position: absolute;
|
|
505
|
+
top: 100%;
|
|
506
|
+
left: 0;
|
|
507
|
+
min-width: 160px;
|
|
508
|
+
background: var(--bg-panel);
|
|
509
|
+
border: 1px solid var(--border);
|
|
510
|
+
border-radius: var(--radius-md);
|
|
511
|
+
box-shadow: var(--shadow-md);
|
|
512
|
+
padding: 4px 0;
|
|
513
|
+
z-index: 1000;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.dropdown-item {
|
|
517
|
+
display: flex;
|
|
518
|
+
align-items: center;
|
|
519
|
+
justify-content: space-between;
|
|
520
|
+
width: 100%;
|
|
521
|
+
padding: 5px 12px;
|
|
522
|
+
background: none;
|
|
523
|
+
border: none;
|
|
524
|
+
color: var(--text-primary);
|
|
525
|
+
font-size: var(--text-lg);
|
|
526
|
+
cursor: pointer;
|
|
527
|
+
text-align: left;
|
|
528
|
+
gap: 24px;
|
|
529
|
+
white-space: nowrap;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.dropdown-item:hover:not(:disabled) {
|
|
533
|
+
background: var(--bg-secondary);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* Keyboard-focused item highlight (matches hover). */
|
|
537
|
+
.dropdown-item.focused:not(:disabled) {
|
|
538
|
+
background: var(--bg-secondary);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.dropdown-item:focus {
|
|
542
|
+
outline: none;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.dropdown-item:focus-visible {
|
|
546
|
+
outline: 2px solid var(--accent);
|
|
547
|
+
outline-offset: -2px;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.dropdown-item:disabled,
|
|
551
|
+
.dropdown-item.disabled {
|
|
552
|
+
color: var(--text-secondary);
|
|
553
|
+
opacity: 0.5;
|
|
554
|
+
cursor: default;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.item-shortcut {
|
|
558
|
+
color: var(--text-secondary);
|
|
559
|
+
font-size: var(--text-sm);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.separator {
|
|
563
|
+
height: 1px;
|
|
564
|
+
background: var(--border);
|
|
565
|
+
margin: 4px 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* --- Submenu --- */
|
|
569
|
+
|
|
570
|
+
.submenu-trigger {
|
|
571
|
+
position: relative;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.submenu-arrow {
|
|
575
|
+
margin-left: auto;
|
|
576
|
+
display: flex;
|
|
577
|
+
align-items: center;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.submenu-arrow :global(svg) {
|
|
581
|
+
width: 12px;
|
|
582
|
+
height: 12px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.submenu {
|
|
586
|
+
position: absolute;
|
|
587
|
+
left: 100%;
|
|
588
|
+
top: -4px;
|
|
589
|
+
}
|
|
590
|
+
</style>
|