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,275 @@
|
|
|
1
|
+
"""Tests for the WebSocket message handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from typing import Any
|
|
7
|
+
from unittest.mock import AsyncMock
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from pixelweaver.connections import ConnectionManager
|
|
12
|
+
from pixelweaver.state import ServerState
|
|
13
|
+
from pixelweaver.websocket import handle_message
|
|
14
|
+
|
|
15
|
+
# Shorter aliases for type hints in test signatures
|
|
16
|
+
SS = ServerState
|
|
17
|
+
CM = ConnectionManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _make_ws() -> AsyncMock:
|
|
21
|
+
"""Create a mock WebSocket."""
|
|
22
|
+
ws = AsyncMock()
|
|
23
|
+
ws.send_json = AsyncMock()
|
|
24
|
+
return ws
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _make_command_msg(
|
|
28
|
+
msg_id: str | None = None,
|
|
29
|
+
cmd_id: str | None = None,
|
|
30
|
+
) -> dict[str, Any]:
|
|
31
|
+
"""Build a valid command message dict."""
|
|
32
|
+
return {
|
|
33
|
+
"type": "command",
|
|
34
|
+
"id": msg_id or str(uuid.uuid4()),
|
|
35
|
+
"command": {
|
|
36
|
+
"type": "draw_pixels",
|
|
37
|
+
"plugin": "builtin/pencil",
|
|
38
|
+
"version": "1.0.0",
|
|
39
|
+
"params": {"x": 5, "y": 10},
|
|
40
|
+
"id": cmd_id or str(uuid.uuid4()),
|
|
41
|
+
"timestamp": 1234567890,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture()
|
|
47
|
+
def state_with_project() -> ServerState:
|
|
48
|
+
"""ServerState with one active project."""
|
|
49
|
+
state = ServerState()
|
|
50
|
+
state.create_project("test", 32, 32)
|
|
51
|
+
return state
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture()
|
|
55
|
+
def mgr() -> ConnectionManager:
|
|
56
|
+
return ConnectionManager()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestCommandHandling:
|
|
60
|
+
@pytest.mark.asyncio
|
|
61
|
+
async def test_command_ack(self, state_with_project: SS, mgr: CM):
|
|
62
|
+
ws = _make_ws()
|
|
63
|
+
msg = _make_command_msg(msg_id="m1", cmd_id="c1")
|
|
64
|
+
await handle_message(ws, msg, state_with_project, mgr)
|
|
65
|
+
|
|
66
|
+
# Should have sent an ack
|
|
67
|
+
ws.send_json.assert_called()
|
|
68
|
+
sent = ws.send_json.call_args_list[0][0][0]
|
|
69
|
+
assert sent["type"] == "command_ack"
|
|
70
|
+
assert sent["id"] == "m1"
|
|
71
|
+
assert sent["command_id"] == "c1"
|
|
72
|
+
assert sent["success"] is True
|
|
73
|
+
|
|
74
|
+
@pytest.mark.asyncio
|
|
75
|
+
async def test_command_stored_in_history(
|
|
76
|
+
self, state_with_project: SS, mgr: CM,
|
|
77
|
+
):
|
|
78
|
+
ws = _make_ws()
|
|
79
|
+
msg = _make_command_msg()
|
|
80
|
+
await handle_message(ws, msg, state_with_project, mgr)
|
|
81
|
+
|
|
82
|
+
project = state_with_project.get_active_project()
|
|
83
|
+
assert project is not None
|
|
84
|
+
assert len(project.command_history) == 1
|
|
85
|
+
assert project.command_history[0]["type"] == "draw_pixels"
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_command_broadcast_to_others(
|
|
89
|
+
self, state_with_project: SS, mgr: CM,
|
|
90
|
+
):
|
|
91
|
+
ws1 = _make_ws()
|
|
92
|
+
ws2 = _make_ws()
|
|
93
|
+
# Register ws2 as another connected client
|
|
94
|
+
mgr.active_connections.append(ws2)
|
|
95
|
+
|
|
96
|
+
msg = _make_command_msg()
|
|
97
|
+
await handle_message(ws1, msg, state_with_project, mgr)
|
|
98
|
+
|
|
99
|
+
# ws2 should have received a broadcast
|
|
100
|
+
ws2.send_json.assert_called_once()
|
|
101
|
+
broadcast = ws2.send_json.call_args[0][0]
|
|
102
|
+
assert broadcast["type"] == "command_broadcast"
|
|
103
|
+
assert broadcast["source"] == "client"
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_command_reject_no_project(self, mgr: CM):
|
|
107
|
+
"""Command without active project should be rejected."""
|
|
108
|
+
state = ServerState()
|
|
109
|
+
ws = _make_ws()
|
|
110
|
+
msg = _make_command_msg(msg_id="m1", cmd_id="c1")
|
|
111
|
+
await handle_message(ws, msg, state, mgr)
|
|
112
|
+
|
|
113
|
+
sent = ws.send_json.call_args[0][0]
|
|
114
|
+
assert sent["type"] == "command_reject"
|
|
115
|
+
assert sent["error"] == "No active project"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestSyncRequest:
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_sync_returns_state(
|
|
121
|
+
self, state_with_project: SS, mgr: CM,
|
|
122
|
+
):
|
|
123
|
+
ws = _make_ws()
|
|
124
|
+
msg = {"type": "sync_request", "id": "s1"}
|
|
125
|
+
await handle_message(ws, msg, state_with_project, mgr)
|
|
126
|
+
|
|
127
|
+
sent = ws.send_json.call_args[0][0]
|
|
128
|
+
assert sent["type"] == "state_sync"
|
|
129
|
+
assert sent["id"] == "s1"
|
|
130
|
+
assert "project" in sent["state"]
|
|
131
|
+
assert sent["state"]["project"]["name"] == "test"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class TestUndoRedo:
|
|
135
|
+
@pytest.mark.asyncio
|
|
136
|
+
async def test_undo_pops_command(
|
|
137
|
+
self, state_with_project: SS, mgr: CM,
|
|
138
|
+
):
|
|
139
|
+
ws = _make_ws()
|
|
140
|
+
|
|
141
|
+
# First add a command
|
|
142
|
+
msg = _make_command_msg(msg_id="m1", cmd_id="c1")
|
|
143
|
+
await handle_message(ws, msg, state_with_project, mgr)
|
|
144
|
+
|
|
145
|
+
project = state_with_project.get_active_project()
|
|
146
|
+
assert project is not None
|
|
147
|
+
assert len(project.command_history) == 1
|
|
148
|
+
|
|
149
|
+
# Now undo
|
|
150
|
+
ws.send_json.reset_mock()
|
|
151
|
+
undo_msg = {"type": "undo", "id": "u1"}
|
|
152
|
+
await handle_message(ws, undo_msg, state_with_project, mgr)
|
|
153
|
+
|
|
154
|
+
assert len(project.command_history) == 0
|
|
155
|
+
assert len(project.redo_stack) == 1
|
|
156
|
+
|
|
157
|
+
sent = ws.send_json.call_args[0][0]
|
|
158
|
+
assert sent["type"] == "command_ack"
|
|
159
|
+
assert sent["id"] == "u1"
|
|
160
|
+
assert sent["command_id"] == "c1"
|
|
161
|
+
|
|
162
|
+
@pytest.mark.asyncio
|
|
163
|
+
async def test_redo_restores_command(
|
|
164
|
+
self, state_with_project: SS, mgr: CM,
|
|
165
|
+
):
|
|
166
|
+
ws = _make_ws()
|
|
167
|
+
|
|
168
|
+
# Add then undo a command
|
|
169
|
+
msg = _make_command_msg(msg_id="m1", cmd_id="c1")
|
|
170
|
+
await handle_message(ws, msg, state_with_project, mgr)
|
|
171
|
+
undo_msg = {"type": "undo", "id": "u1"}
|
|
172
|
+
await handle_message(ws, undo_msg, state_with_project, mgr)
|
|
173
|
+
|
|
174
|
+
project = state_with_project.get_active_project()
|
|
175
|
+
assert project is not None
|
|
176
|
+
assert len(project.command_history) == 0
|
|
177
|
+
|
|
178
|
+
# Redo
|
|
179
|
+
ws.send_json.reset_mock()
|
|
180
|
+
redo_msg = {"type": "redo", "id": "r1"}
|
|
181
|
+
await handle_message(ws, redo_msg, state_with_project, mgr)
|
|
182
|
+
|
|
183
|
+
assert len(project.command_history) == 1
|
|
184
|
+
assert len(project.redo_stack) == 0
|
|
185
|
+
|
|
186
|
+
sent = ws.send_json.call_args[0][0]
|
|
187
|
+
assert sent["type"] == "command_ack"
|
|
188
|
+
assert sent["command_id"] == "c1"
|
|
189
|
+
|
|
190
|
+
@pytest.mark.asyncio
|
|
191
|
+
async def test_undo_empty_history_returns_error(
|
|
192
|
+
self, state_with_project: SS, mgr: CM,
|
|
193
|
+
):
|
|
194
|
+
ws = _make_ws()
|
|
195
|
+
undo_msg = {"type": "undo", "id": "u1"}
|
|
196
|
+
await handle_message(ws, undo_msg, state_with_project, mgr)
|
|
197
|
+
|
|
198
|
+
sent = ws.send_json.call_args[0][0]
|
|
199
|
+
assert sent["type"] == "error"
|
|
200
|
+
assert "Nothing to undo" in sent["error"]
|
|
201
|
+
|
|
202
|
+
@pytest.mark.asyncio
|
|
203
|
+
async def test_redo_empty_stack_returns_error(
|
|
204
|
+
self, state_with_project: SS, mgr: CM,
|
|
205
|
+
):
|
|
206
|
+
ws = _make_ws()
|
|
207
|
+
redo_msg = {"type": "redo", "id": "r1"}
|
|
208
|
+
await handle_message(ws, redo_msg, state_with_project, mgr)
|
|
209
|
+
|
|
210
|
+
sent = ws.send_json.call_args[0][0]
|
|
211
|
+
assert sent["type"] == "error"
|
|
212
|
+
assert "Nothing to redo" in sent["error"]
|
|
213
|
+
|
|
214
|
+
@pytest.mark.asyncio
|
|
215
|
+
async def test_new_command_clears_redo_stack(
|
|
216
|
+
self, state_with_project: SS, mgr: CM,
|
|
217
|
+
):
|
|
218
|
+
ws = _make_ws()
|
|
219
|
+
|
|
220
|
+
# Add, undo, then add new command
|
|
221
|
+
msg1 = _make_command_msg(cmd_id="c1")
|
|
222
|
+
await handle_message(ws, msg1, state_with_project, mgr)
|
|
223
|
+
undo_msg = {"type": "undo", "id": "u1"}
|
|
224
|
+
await handle_message(ws, undo_msg, state_with_project, mgr)
|
|
225
|
+
|
|
226
|
+
project = state_with_project.get_active_project()
|
|
227
|
+
assert project is not None
|
|
228
|
+
assert len(project.redo_stack) == 1
|
|
229
|
+
|
|
230
|
+
msg2 = _make_command_msg(cmd_id="c2")
|
|
231
|
+
await handle_message(ws, msg2, state_with_project, mgr)
|
|
232
|
+
|
|
233
|
+
# Redo stack should be cleared after a new command
|
|
234
|
+
assert len(project.redo_stack) == 0
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class TestInvalidMessages:
|
|
238
|
+
@pytest.mark.asyncio
|
|
239
|
+
async def test_unknown_type_returns_error(
|
|
240
|
+
self, state_with_project: SS, mgr: CM,
|
|
241
|
+
):
|
|
242
|
+
ws = _make_ws()
|
|
243
|
+
bad_msg = {"type": "bogus", "id": "x"}
|
|
244
|
+
await handle_message(ws, bad_msg, state_with_project, mgr)
|
|
245
|
+
|
|
246
|
+
sent = ws.send_json.call_args[0][0]
|
|
247
|
+
assert sent["type"] == "error"
|
|
248
|
+
|
|
249
|
+
@pytest.mark.asyncio
|
|
250
|
+
async def test_missing_fields_returns_error(
|
|
251
|
+
self, state_with_project: SS, mgr: CM,
|
|
252
|
+
):
|
|
253
|
+
ws = _make_ws()
|
|
254
|
+
bad_msg = {"type": "command"}
|
|
255
|
+
await handle_message(ws, bad_msg, state_with_project, mgr)
|
|
256
|
+
|
|
257
|
+
sent = ws.send_json.call_args[0][0]
|
|
258
|
+
assert sent["type"] == "error"
|
|
259
|
+
|
|
260
|
+
@pytest.mark.asyncio
|
|
261
|
+
async def test_dirty_callback_called(
|
|
262
|
+
self, state_with_project: SS, mgr: CM,
|
|
263
|
+
):
|
|
264
|
+
ws = _make_ws()
|
|
265
|
+
dirty_called = False
|
|
266
|
+
|
|
267
|
+
def on_dirty():
|
|
268
|
+
nonlocal dirty_called
|
|
269
|
+
dirty_called = True
|
|
270
|
+
|
|
271
|
+
msg = _make_command_msg()
|
|
272
|
+
await handle_message(
|
|
273
|
+
ws, msg, state_with_project, mgr, on_dirty=on_dirty,
|
|
274
|
+
)
|
|
275
|
+
assert dirty_called
|
package/src/App.svelte
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- PixelWeaver main application shell -->
|
|
2
|
+
<script lang="ts">
|
|
3
|
+
import MenuBar from './lib/ui/MenuBar.svelte';
|
|
4
|
+
import NotificationBanner from './lib/ui/notifications/NotificationBanner.svelte';
|
|
5
|
+
import StatusBar from './lib/ui/StatusBar.svelte';
|
|
6
|
+
import NewProjectDialog from './lib/ui/NewProjectDialog.svelte';
|
|
7
|
+
import ExportDialog from './lib/ui/ExportDialog.svelte';
|
|
8
|
+
import ResizeDialog from './lib/ui/ResizeDialog.svelte';
|
|
9
|
+
import PromptDialog from './lib/ui/PromptDialog.svelte';
|
|
10
|
+
import RecoveryDialog from './lib/ui/RecoveryDialog.svelte';
|
|
11
|
+
import CommandPalette from './lib/ui/command-palette/CommandPalette.svelte';
|
|
12
|
+
import BisectionExportDialog from './lib/variants/BisectionExportDialog.svelte';
|
|
13
|
+
import { dialogState } from './lib/ui/dialog-state.svelte.js';
|
|
14
|
+
import { hasUnsavedChanges } from './lib/core/dispatcher.js';
|
|
15
|
+
import type { Component } from 'svelte';
|
|
16
|
+
|
|
17
|
+
// Lazy-load DockLayout so dockview-core (209KB) is deferred from the initial bundle.
|
|
18
|
+
let DockLayout: Component | null = $state(null);
|
|
19
|
+
|
|
20
|
+
$effect(() => {
|
|
21
|
+
import('./lib/dock/DockLayout.svelte').then(mod => {
|
|
22
|
+
DockLayout = mod.default;
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Warn the user before closing the tab if there are unsaved changes.
|
|
27
|
+
$effect(() => {
|
|
28
|
+
function onBeforeUnload(e: BeforeUnloadEvent) {
|
|
29
|
+
if (hasUnsavedChanges()) {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
window.addEventListener('beforeunload', onBeforeUnload);
|
|
34
|
+
return () => window.removeEventListener('beforeunload', onBeforeUnload);
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
39
|
+
<div class="app-shell" oncontextmenu={(e) => { e.preventDefault(); }}>
|
|
40
|
+
<MenuBar />
|
|
41
|
+
<NotificationBanner />
|
|
42
|
+
|
|
43
|
+
<main class="workspace">
|
|
44
|
+
<div class="dock-area">
|
|
45
|
+
{#if DockLayout}
|
|
46
|
+
<DockLayout />
|
|
47
|
+
{:else}
|
|
48
|
+
<div class="dock-loading">Loading workspace...</div>
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
</main>
|
|
52
|
+
|
|
53
|
+
<StatusBar />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<NewProjectDialog open={dialogState.newProjectOpen} onClose={() => { dialogState.closeNewProject(); }} />
|
|
57
|
+
<ExportDialog open={dialogState.exportOpen} onClose={() => { dialogState.closeExport(); }} />
|
|
58
|
+
<ResizeDialog open={dialogState.resizeCanvasOpen} onClose={() => { dialogState.closeResizeCanvas(); }} />
|
|
59
|
+
<PromptDialog
|
|
60
|
+
open={dialogState.promptOpen}
|
|
61
|
+
title={dialogState.promptConfig?.title ?? ''}
|
|
62
|
+
message={dialogState.promptConfig?.message ?? ''}
|
|
63
|
+
defaultValue={dialogState.promptConfig?.defaultValue ?? ''}
|
|
64
|
+
placeholder={dialogState.promptConfig?.placeholder ?? ''}
|
|
65
|
+
validate={dialogState.promptConfig?.validate}
|
|
66
|
+
onConfirm={(value: string) => dialogState.promptConfig?.onConfirm(value)}
|
|
67
|
+
onCancel={() => { dialogState.closePrompt(); }}
|
|
68
|
+
/>
|
|
69
|
+
<BisectionExportDialog
|
|
70
|
+
open={dialogState.bisectionExportOpen}
|
|
71
|
+
groupId={dialogState.bisectionExportGroupId}
|
|
72
|
+
onClose={() => { dialogState.closeBisectionExport(); }}
|
|
73
|
+
/>
|
|
74
|
+
<RecoveryDialog />
|
|
75
|
+
<CommandPalette />
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.app-shell {
|
|
79
|
+
display: flex;
|
|
80
|
+
flex-direction: column;
|
|
81
|
+
height: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.workspace {
|
|
85
|
+
flex: 1;
|
|
86
|
+
display: flex;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
min-height: 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.dock-area {
|
|
92
|
+
flex: 1;
|
|
93
|
+
min-width: 0;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dock-loading {
|
|
98
|
+
width: 100%;
|
|
99
|
+
height: 100%;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
color: var(--text-secondary, #888);
|
|
104
|
+
font-size: 14px;
|
|
105
|
+
background: var(--surface-primary, #1e1e1e);
|
|
106
|
+
}
|
|
107
|
+
</style>
|
package/src/app.css
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/* PixelWeaver global styles and theme variables */
|
|
2
|
+
|
|
3
|
+
/* --- Font import --- */
|
|
4
|
+
@import '@fontsource-variable/inter';
|
|
5
|
+
|
|
6
|
+
/* --- Design tokens (theme-independent) --- */
|
|
7
|
+
:root {
|
|
8
|
+
--font-ui: 'Inter Variable', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
9
|
+
--font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
|
|
10
|
+
|
|
11
|
+
--space-1: 2px;
|
|
12
|
+
--space-2: 4px;
|
|
13
|
+
--space-3: 8px;
|
|
14
|
+
--space-4: 12px;
|
|
15
|
+
--space-5: 16px;
|
|
16
|
+
--space-6: 24px;
|
|
17
|
+
--space-7: 32px;
|
|
18
|
+
--space-8: 48px;
|
|
19
|
+
|
|
20
|
+
--radius-sm: 2px;
|
|
21
|
+
--radius-md: 4px;
|
|
22
|
+
--radius-lg: 8px;
|
|
23
|
+
|
|
24
|
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
|
|
25
|
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
26
|
+
|
|
27
|
+
--transition-fast: 100ms ease;
|
|
28
|
+
--transition-normal: 200ms ease;
|
|
29
|
+
|
|
30
|
+
--text-xs: 10px;
|
|
31
|
+
--text-sm: 11px;
|
|
32
|
+
--text-base: 12px;
|
|
33
|
+
--text-lg: 13px;
|
|
34
|
+
--text-xl: 14px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* --- Dark theme (default) --- */
|
|
38
|
+
:root,
|
|
39
|
+
[data-theme="dark"] {
|
|
40
|
+
--bg-primary: #1e1e1e;
|
|
41
|
+
--bg-secondary: #252525;
|
|
42
|
+
--bg-canvas: #121212;
|
|
43
|
+
--bg-panel: #272727;
|
|
44
|
+
--bg-toolbar: #1a1a1a;
|
|
45
|
+
--text-primary: #e0e0e0;
|
|
46
|
+
--text-secondary: #a0a0a0;
|
|
47
|
+
--text-muted: #666666;
|
|
48
|
+
--accent: #4a9eff;
|
|
49
|
+
--accent-hover: #6ab4ff;
|
|
50
|
+
--text-on-accent: #ffffff;
|
|
51
|
+
--border: #3a3a3a;
|
|
52
|
+
--error: #e5534b;
|
|
53
|
+
--warning: #d4a04a;
|
|
54
|
+
--success: #34d67c;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* --- Light theme --- */
|
|
58
|
+
[data-theme="light"] {
|
|
59
|
+
--bg-primary: #f0f0f0;
|
|
60
|
+
--bg-secondary: #e4e4e4;
|
|
61
|
+
--bg-canvas: #ffffff;
|
|
62
|
+
--bg-panel: #f5f5f5;
|
|
63
|
+
--bg-toolbar: #eaeaea;
|
|
64
|
+
--text-primary: #1a1a1a;
|
|
65
|
+
--text-secondary: #666666;
|
|
66
|
+
--text-muted: #999999;
|
|
67
|
+
--accent: #2563eb;
|
|
68
|
+
--accent-hover: #3b82f6;
|
|
69
|
+
--text-on-accent: #ffffff;
|
|
70
|
+
--border: #d0d0d0;
|
|
71
|
+
--error: #dc2626;
|
|
72
|
+
--warning: #d97706;
|
|
73
|
+
--success: #16a34a;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* --- Base typography and rendering --- */
|
|
77
|
+
:root {
|
|
78
|
+
font-family: var(--font-ui);
|
|
79
|
+
font-size: var(--text-base);
|
|
80
|
+
line-height: 1.5;
|
|
81
|
+
font-weight: 400;
|
|
82
|
+
color: var(--text-primary);
|
|
83
|
+
background-color: var(--bg-primary);
|
|
84
|
+
font-synthesis: none;
|
|
85
|
+
text-rendering: optimizeLegibility;
|
|
86
|
+
-webkit-font-smoothing: antialiased;
|
|
87
|
+
-moz-osx-font-smoothing: grayscale;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* --- CSS reset --- */
|
|
91
|
+
*,
|
|
92
|
+
*::before,
|
|
93
|
+
*::after {
|
|
94
|
+
box-sizing: border-box;
|
|
95
|
+
margin: 0;
|
|
96
|
+
padding: 0;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* Smooth theme transitions -- scoped to UI chrome only.
|
|
101
|
+
* Applying to * caused unnecessary transition overhead on canvas elements,
|
|
102
|
+
* SVG icons, dockview internals, color pickers, and animations.
|
|
103
|
+
*/
|
|
104
|
+
body,
|
|
105
|
+
#app,
|
|
106
|
+
div,
|
|
107
|
+
span,
|
|
108
|
+
header,
|
|
109
|
+
nav,
|
|
110
|
+
section,
|
|
111
|
+
aside,
|
|
112
|
+
footer,
|
|
113
|
+
button,
|
|
114
|
+
input,
|
|
115
|
+
textarea,
|
|
116
|
+
select,
|
|
117
|
+
label,
|
|
118
|
+
a,
|
|
119
|
+
li,
|
|
120
|
+
ul,
|
|
121
|
+
ol,
|
|
122
|
+
table,
|
|
123
|
+
th,
|
|
124
|
+
td {
|
|
125
|
+
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* SVG icons get fill transition for theme switch, but not their child shapes */
|
|
129
|
+
:where(svg) {
|
|
130
|
+
transition: fill 150ms ease;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Performance-critical elements must never have transitions from the global rule */
|
|
134
|
+
canvas,
|
|
135
|
+
video {
|
|
136
|
+
transition: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* Form elements inherit font from parent instead of browser defaults */
|
|
140
|
+
input,
|
|
141
|
+
button,
|
|
142
|
+
textarea,
|
|
143
|
+
select {
|
|
144
|
+
font: inherit;
|
|
145
|
+
color: inherit;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* Media elements default to block */
|
|
149
|
+
img,
|
|
150
|
+
picture,
|
|
151
|
+
video,
|
|
152
|
+
canvas,
|
|
153
|
+
svg {
|
|
154
|
+
display: block;
|
|
155
|
+
max-width: 100%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* --- Scrollbar styling (dual approach for Tauri cross-platform) --- */
|
|
159
|
+
|
|
160
|
+
/* Standard scrollbar (Chrome 121+, Firefox 64+) */
|
|
161
|
+
* {
|
|
162
|
+
scrollbar-width: thin;
|
|
163
|
+
scrollbar-color: var(--border) transparent;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* WebKit fallback (WebKitGTK, older WKWebView) */
|
|
167
|
+
::-webkit-scrollbar {
|
|
168
|
+
width: 6px;
|
|
169
|
+
height: 6px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
::-webkit-scrollbar-track {
|
|
173
|
+
background: transparent;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
::-webkit-scrollbar-thumb {
|
|
177
|
+
background: var(--border);
|
|
178
|
+
border-radius: 3px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
::-webkit-scrollbar-thumb:hover {
|
|
182
|
+
background: var(--text-muted);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* --- Layout --- */
|
|
186
|
+
html,
|
|
187
|
+
body {
|
|
188
|
+
height: 100%;
|
|
189
|
+
overflow: hidden;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#app {
|
|
193
|
+
height: 100%;
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-direction: column;
|
|
196
|
+
user-select: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* Allow text selection in input fields */
|
|
200
|
+
input, textarea, [contenteditable] {
|
|
201
|
+
user-select: text;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* --- Focus indicators for keyboard accessibility --- */
|
|
205
|
+
|
|
206
|
+
/* Show a visible outline when an element receives keyboard focus */
|
|
207
|
+
:focus-visible {
|
|
208
|
+
outline: 2px solid var(--accent);
|
|
209
|
+
outline-offset: 2px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* Suppress the always-visible :focus outline for mouse/pointer users */
|
|
213
|
+
:focus:not(:focus-visible) {
|
|
214
|
+
outline: none;
|
|
215
|
+
}
|