gloomberb 0.5.0 → 0.6.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/README.md +91 -11
- package/package.json +12 -5
- package/src/app.tsx +414 -191
- package/src/architecture/import-boundaries.test.ts +118 -0
- package/src/assets/gloomberb-logo.png +0 -0
- package/src/assets/gloomberb-logo.svg +73 -0
- package/src/brokers/account-cache.ts +157 -0
- package/src/brokers/profile-form.test.ts +73 -0
- package/src/brokers/profile-form.ts +109 -0
- package/src/brokers/remote-broker-adapter.ts +91 -0
- package/src/brokers/sync-broker-instance.test.ts +124 -1
- package/src/brokers/sync-broker-instance.ts +113 -17
- package/src/capabilities/factories.ts +100 -0
- package/src/capabilities/index.ts +4 -0
- package/src/capabilities/registry.test.ts +137 -0
- package/src/capabilities/registry.ts +131 -0
- package/src/capabilities/service-capabilities.ts +27 -0
- package/src/capabilities/types.ts +84 -0
- package/src/cli/commands/ticker.ts +11 -3
- package/src/cli/context.ts +19 -13
- package/src/cli/helpers.ts +1 -0
- package/src/cli/types.ts +2 -2
- package/src/components/chart/chart-controller.ts +4 -24
- package/src/components/chart/chart-data.ts +3 -14
- package/src/components/chart/chart-indicator-selector.tsx +28 -6
- package/src/components/chart/chart-pane-settings.ts +15 -28
- package/src/components/chart/chart-renderer.ts +74 -99
- package/src/components/chart/chart-resolution.ts +10 -10
- package/src/components/chart/chart-scroll.test.ts +6 -5
- package/src/components/chart/chart-scroll.ts +2 -2
- package/src/components/chart/chart-types.ts +0 -1
- package/src/components/chart/chart-viewport.ts +2 -1
- package/src/components/chart/chart.test.ts +37 -0
- package/src/components/chart/comparison-chart-data.ts +3 -17
- package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
- package/src/components/chart/comparison-chart-renderer.ts +1 -1
- package/src/components/chart/comparison-stock-chart.tsx +304 -108
- package/src/components/chart/cursor-motion.ts +3 -2
- package/src/components/chart/indicators/bands.test.ts +0 -14
- package/src/components/chart/indicators/moving-averages.test.ts +0 -15
- package/src/components/chart/indicators/options.ts +3 -3
- package/src/components/chart/indicators/oscillators.test.ts +0 -21
- package/src/components/chart/indicators/types.ts +1 -2
- package/src/components/chart/native/chart-rasterizer.ts +37 -44
- package/src/components/chart/native/kitty-adapter.ts +2 -5
- package/src/components/chart/native/kitty-manager.ts +1 -1
- package/src/components/chart/native/kitty-protocol.ts +0 -1
- package/src/components/chart/native/kitty-support.ts +1 -1
- package/src/components/chart/native/kitty.test.ts +30 -13
- package/src/components/chart/native/renderer-selection.ts +1 -1
- package/src/components/chart/native/surface-manager.ts +1 -1
- package/src/components/chart/static-chart-surface.test.tsx +51 -0
- package/src/components/chart/static-chart-surface.tsx +191 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
- package/src/components/chart/stock-chart.tsx +438 -141
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +269 -20
- package/src/components/command-bar/command-bar.tsx +830 -353
- package/src/components/command-bar/command-registry.ts +10 -20
- package/src/components/command-bar/helpers.test.ts +1 -1
- package/src/components/command-bar/helpers.ts +9 -11
- package/src/components/command-bar/pane-template-display.ts +4 -8
- package/src/components/command-bar/root-shortcuts.ts +12 -11
- package/src/components/command-bar/view-model.test.ts +8 -42
- package/src/components/command-bar/view-model.ts +8 -36
- package/src/components/command-bar/workflow-ops.test.ts +76 -2
- package/src/components/command-bar/workflow-ops.ts +40 -9
- package/src/components/command-bar/workflow-types.ts +2 -1
- package/src/components/data-table-stack-view.test.tsx +206 -0
- package/src/components/data-table-stack-view.tsx +152 -0
- package/src/components/data-table-view.test.tsx +188 -0
- package/src/components/data-table-view.tsx +279 -0
- package/src/components/feed-data-table-stack-view.test.tsx +225 -0
- package/src/components/feed-data-table-stack-view.tsx +365 -0
- package/src/components/index.ts +26 -31
- package/src/components/layout/detached-pane-shell.tsx +185 -0
- package/src/components/layout/floating-pane.tsx +87 -13
- package/src/components/layout/header.tsx +139 -49
- package/src/components/layout/pane-content.tsx +40 -0
- package/src/components/layout/pane-footer.test.tsx +195 -0
- package/src/components/layout/pane-footer.tsx +410 -0
- package/src/components/layout/pane-header.test.tsx +39 -0
- package/src/components/layout/pane-header.tsx +192 -14
- package/src/components/layout/pane-sizing.ts +13 -4
- package/src/components/layout/pane-title.ts +35 -0
- package/src/components/layout/pane.tsx +56 -8
- package/src/components/layout/shell.test.tsx +322 -19
- package/src/components/layout/shell.tsx +1010 -366
- package/src/components/layout/status-bar.test.tsx +17 -10
- package/src/components/layout/status-bar.tsx +367 -100
- package/src/components/layout/titlebar-overlay.ts +2 -0
- package/src/components/markdown-editor.tsx +29 -14
- package/src/components/markdown-text.tsx +25 -24
- package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
- package/src/components/onboarding/onboarding-wizard.tsx +347 -473
- package/src/components/pane-settings-dialog.tsx +521 -89
- package/src/components/pane-template-wizard.test.tsx +1 -1
- package/src/components/pane-template-wizard.tsx +38 -37
- package/src/components/price-selector-dialog.tsx +22 -21
- package/src/components/ticker-badge-list.tsx +53 -0
- package/src/components/ticker-badge-text.test.tsx +44 -1
- package/src/components/ticker-badge-text.tsx +78 -38
- package/src/components/ticker-badge.tsx +28 -7
- package/src/components/ticker-list-table-view.tsx +248 -0
- package/src/components/ticker-list-table.test.tsx +47 -1
- package/src/components/ticker-list-table.tsx +212 -41
- package/src/components/toggle-list.tsx +114 -8
- package/src/components/ui/button.tsx +24 -16
- package/src/components/ui/choice-dialog.tsx +89 -0
- package/src/components/ui/data-table.tsx +313 -114
- package/src/components/ui/external-link.tsx +52 -13
- package/src/components/ui/fields.tsx +44 -30
- package/src/components/ui/frame.tsx +24 -57
- package/src/components/ui/index.ts +15 -50
- package/src/components/ui/list-view.tsx +65 -25
- package/src/components/ui/loading.tsx +5 -60
- package/src/components/ui/message-composer.test.tsx +81 -0
- package/src/components/ui/message-composer.tsx +139 -0
- package/src/components/ui/multi-select-dialog.tsx +100 -32
- package/src/components/ui/multi-select.ts +112 -0
- package/src/components/ui/native-select.tsx +120 -0
- package/src/components/ui/page-stack-view.tsx +49 -19
- package/src/components/ui/remote-image.tsx +75 -0
- package/src/components/ui/row-value-cache.test.ts +17 -0
- package/src/components/ui/row-value-cache.ts +41 -0
- package/src/components/ui/shortcut-hint.tsx +55 -0
- package/src/components/ui/status.tsx +12 -67
- package/src/components/ui/table-layout.ts +59 -0
- package/src/components/ui/tabs.tsx +249 -27
- package/src/components/ui/toggle.tsx +19 -127
- package/src/components/ui/ui.test.tsx +266 -53
- package/src/components/use-double-click-activation.ts +7 -2
- package/src/components/use-external-link-footer.ts +79 -0
- package/src/core/app-services.ts +95 -0
- package/src/core/state/app-state.ts +886 -0
- package/src/{state → core/state}/session-persistence.ts +7 -6
- package/src/data/config-layout.ts +225 -0
- package/src/data/config-store-node.ts +390 -0
- package/src/data/config-store.test.ts +72 -0
- package/src/data/config-store.ts +35 -478
- package/src/data/sqlite-json.ts +2 -5
- package/src/data/sqlite-retry.ts +1 -0
- package/src/index.tsx +2 -68
- package/src/market-data/coordinator-subscriptions.test.ts +125 -0
- package/src/market-data/coordinator.test.ts +73 -4
- package/src/market-data/coordinator.ts +190 -24
- package/src/market-data/hooks.test.tsx +1 -1
- package/src/market-data/hooks.tsx +109 -87
- package/src/market-data/query-store.ts +3 -3
- package/src/market-data/request-types.test.ts +0 -14
- package/src/market-data/result-types.ts +1 -0
- package/src/news/aggregator.test.ts +267 -0
- package/src/news/aggregator.ts +452 -0
- package/src/news/hooks.ts +39 -0
- package/src/news/ticker-symbols.ts +21 -0
- package/src/news/types.ts +77 -0
- package/src/notifications/app-notifier.ts +21 -10
- package/src/pane-settings.test.ts +0 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
- package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
- package/src/plugins/builtin/ai/providers.ts +1 -9
- package/src/plugins/builtin/ai/runner.ts +41 -5
- package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
- package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
- package/src/plugins/builtin/ai/settings.ts +0 -1
- package/src/plugins/builtin/ai/utils.ts +1 -71
- package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
- package/src/plugins/builtin/alerts/index.test.tsx +61 -82
- package/src/plugins/builtin/alerts/index.tsx +263 -155
- package/src/plugins/builtin/alerts/types.ts +7 -0
- package/src/plugins/builtin/analytics/index.test.tsx +13 -8
- package/src/plugins/builtin/analytics/index.tsx +78 -120
- package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
- package/src/plugins/builtin/ask-ai.test.tsx +55 -54
- package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
- package/src/plugins/builtin/broker-manager/index.tsx +759 -0
- package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
- package/src/plugins/builtin/broker-manager/model.ts +142 -0
- package/src/plugins/builtin/chat-controller.test.ts +150 -0
- package/src/plugins/builtin/chat-controller.ts +88 -16
- package/src/plugins/builtin/chat.test.tsx +283 -36
- package/src/plugins/builtin/chat.tsx +373 -172
- package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
- package/src/plugins/builtin/cloud-tweets.tsx +896 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
- package/src/plugins/builtin/comparison-chart.tsx +11 -11
- package/src/plugins/builtin/correlation/compute.test.ts +0 -17
- package/src/plugins/builtin/correlation/index.tsx +51 -46
- package/src/plugins/builtin/correlation/settings.ts +1 -2
- package/src/plugins/builtin/debug.tsx +99 -70
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
- package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
- package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
- package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
- package/src/plugins/builtin/earnings/index.tsx +187 -94
- package/src/plugins/builtin/econ/calendar-source.ts +9 -116
- package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
- package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
- package/src/plugins/builtin/econ/index.tsx +236 -421
- package/src/plugins/builtin/econ/types.ts +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
- package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
- package/src/plugins/builtin/help.test.tsx +120 -60
- package/src/plugins/builtin/help.tsx +266 -82
- package/src/plugins/builtin/holders/index.test.ts +45 -0
- package/src/plugins/builtin/holders/index.tsx +949 -0
- package/src/plugins/builtin/insider/index.tsx +207 -253
- package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
- package/src/plugins/builtin/insider/insider-data.ts +1 -0
- package/src/plugins/builtin/layout-manager.tsx +12 -40
- package/src/plugins/builtin/market-movers/index.tsx +70 -144
- package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
- package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
- package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
- package/src/plugins/builtin/news-wire/categories.ts +114 -0
- package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
- package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
- package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
- package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
- package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
- package/src/plugins/builtin/news-wire/hash.ts +9 -0
- package/src/plugins/builtin/news-wire/index.tsx +92 -0
- package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
- package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
- package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
- package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
- package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
- package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
- package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
- package/src/plugins/builtin/news-wire/read-state.ts +76 -0
- package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
- package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
- package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
- package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
- package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
- package/src/plugins/builtin/news.tsx +98 -25
- package/src/plugins/builtin/notes-files.ts +51 -10
- package/src/plugins/builtin/notes.tsx +332 -131
- package/src/plugins/builtin/options.test.tsx +185 -0
- package/src/plugins/builtin/options.tsx +346 -206
- package/src/plugins/builtin/plugin-groups.ts +92 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
- package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
- package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
- package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
- package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
- package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
- package/src/plugins/builtin/portfolio-list.test.ts +60 -1
- package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
- package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
- package/src/plugins/builtin/research/index.tsx +724 -0
- package/src/plugins/builtin/sec.tsx +67 -19
- package/src/plugins/builtin/sectors/index.tsx +42 -80
- package/src/plugins/builtin/sectors/sector-data.ts +1 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
- package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
- package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
- package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
- package/src/plugins/builtin/ticker-surface.ts +68 -0
- package/src/plugins/builtin/world-indices/index.tsx +44 -82
- package/src/plugins/builtin/world-indices/indices.ts +1 -0
- package/src/plugins/builtin/yahoo.ts +55 -1
- package/src/plugins/builtin/yield-curve/index.tsx +87 -145
- package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
- package/src/plugins/catalog-ui.test.ts +24 -0
- package/src/plugins/catalog-ui.ts +44 -0
- package/src/plugins/catalog.ts +2 -50
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/account-cache.test.ts +15 -14
- package/src/plugins/ibkr/account-cache.ts +15 -120
- package/src/plugins/ibkr/broker-adapter.ts +67 -0
- package/src/plugins/ibkr/config.test.ts +0 -18
- package/src/plugins/ibkr/dialogs.tsx +14 -56
- package/src/plugins/ibkr/flex.test.ts +57 -2
- package/src/plugins/ibkr/flex.ts +116 -12
- package/src/plugins/ibkr/gateway-helpers.ts +8 -3
- package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
- package/src/plugins/ibkr/gateway-service.test.ts +47 -9
- package/src/plugins/ibkr/gateway-service.ts +264 -2235
- package/src/plugins/ibkr/gateway-types.ts +27 -0
- package/src/plugins/ibkr/index.test.tsx +3 -11
- package/src/plugins/ibkr/index.tsx +1 -0
- package/src/plugins/ibkr/price-normalization.test.ts +1 -6
- package/src/plugins/ibkr/price-normalization.ts +1 -0
- package/src/plugins/ibkr/trade-badge.tsx +6 -5
- package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
- package/src/plugins/ibkr/trade-tab.tsx +164 -117
- package/src/plugins/ibkr/trading-pane.tsx +117 -83
- package/src/plugins/pane-manager.test.ts +3 -1
- package/src/plugins/pane-manager.ts +93 -19
- package/src/plugins/plugin-runtime.test.tsx +176 -74
- package/src/plugins/plugin-runtime.tsx +228 -10
- package/src/plugins/prediction-markets/cache.ts +49 -0
- package/src/plugins/prediction-markets/categories.ts +0 -10
- package/src/plugins/prediction-markets/chart.test.tsx +1 -1
- package/src/plugins/prediction-markets/chart.tsx +21 -20
- package/src/plugins/prediction-markets/cli.ts +1 -0
- package/src/plugins/prediction-markets/controller-data.ts +135 -89
- package/src/plugins/prediction-markets/controller.ts +37 -108
- package/src/plugins/prediction-markets/detail/book.tsx +23 -22
- package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
- package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
- package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
- package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
- package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
- package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
- package/src/plugins/prediction-markets/detail.test.tsx +1 -1
- package/src/plugins/prediction-markets/keyboard.ts +1 -0
- package/src/plugins/prediction-markets/launch.test.ts +1 -1
- package/src/plugins/prediction-markets/launch.ts +1 -1
- package/src/plugins/prediction-markets/metrics.ts +25 -12
- package/src/plugins/prediction-markets/pane.test.tsx +16 -7
- package/src/plugins/prediction-markets/pane.tsx +180 -82
- package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
- package/src/plugins/prediction-markets/services/fetch.ts +27 -10
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/settings.ts +0 -5
- package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
- package/src/plugins/registry.test.ts +307 -0
- package/src/plugins/registry.ts +293 -82
- package/src/react/input.ts +53 -0
- package/src/react/plugins/plugin-slot.tsx +21 -0
- package/src/react/use-raf-callback.ts +44 -0
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
- package/src/renderers/electrobun/bun/application-menu.ts +118 -0
- package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
- package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
- package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
- package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
- package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
- package/src/renderers/electrobun/bun/index.ts +1149 -0
- package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
- package/src/renderers/electrobun/bun/window-frame.ts +46 -0
- package/src/renderers/electrobun/shared/protocol.ts +72 -0
- package/src/renderers/electrobun/view/ai-host.ts +93 -0
- package/src/renderers/electrobun/view/app-services.ts +348 -0
- package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
- package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
- package/src/renderers/electrobun/view/config-host.ts +35 -0
- package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
- package/src/renderers/electrobun/view/context-menu.ts +164 -0
- package/src/renderers/electrobun/view/data-table.tsx +773 -0
- package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
- package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
- package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
- package/src/renderers/electrobun/view/http-fetch.ts +113 -0
- package/src/renderers/electrobun/view/input-host.tsx +65 -0
- package/src/renderers/electrobun/view/key-event.test.ts +34 -0
- package/src/renderers/electrobun/view/key-event.ts +134 -0
- package/src/renderers/electrobun/view/main.tsx +104 -0
- package/src/renderers/electrobun/view/native-renderer.ts +110 -0
- package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
- package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
- package/src/renderers/electrobun/view/notes-files.ts +56 -0
- package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
- package/src/renderers/electrobun/view/resource-store.ts +106 -0
- package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
- package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
- package/src/renderers/electrobun/view/toast-host.tsx +65 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
- package/src/renderers/electrobun/view/update-host.ts +38 -0
- package/src/renderers/opentui/dialog-host.test.tsx +44 -0
- package/src/renderers/opentui/dialog-host.tsx +56 -0
- package/src/renderers/opentui/host.tsx +134 -0
- package/src/renderers/opentui/input-host.tsx +24 -0
- package/src/renderers/opentui/start.tsx +71 -0
- package/src/renderers/opentui/test-utils.tsx +76 -0
- package/src/renderers/opentui/toast-host.tsx +10 -0
- package/src/renderers/opentui/ui-host.tsx +97 -0
- package/src/sources/gloomberb-cloud.test.ts +288 -17
- package/src/sources/gloomberb-cloud.ts +176 -9
- package/src/sources/provider-errors.ts +1 -0
- package/src/sources/provider-router.test.ts +220 -81
- package/src/sources/provider-router.ts +294 -111
- package/src/sources/sec-edgar.ts +19 -6
- package/src/sources/yahoo-finance.test.ts +0 -5
- package/src/sources/yahoo-finance.ts +420 -11
- package/src/state/app-activity.ts +7 -7
- package/src/state/app-bootstrap.ts +114 -40
- package/src/state/app-context-selector.test.tsx +70 -1
- package/src/state/app-context.test.ts +71 -3
- package/src/state/app-context.test.tsx +14 -7
- package/src/state/app-context.tsx +205 -869
- package/src/state/config-save-scheduler.ts +35 -0
- package/src/state/persist-scheduler.test.ts +67 -0
- package/src/state/persist-scheduler.ts +69 -0
- package/src/state/selectors-ui.ts +55 -0
- package/src/state/selectors.ts +15 -71
- package/src/state/session-persistence.test.ts +1 -1
- package/src/state/ticker-refresh-queue.ts +52 -3
- package/src/state/use-inline-tickers.ts +18 -16
- package/src/state/use-quote-streaming.test.ts +0 -6
- package/src/state/use-quote-streaming.test.tsx +1 -1
- package/src/state/use-quote-streaming.ts +3 -2
- package/src/state/use-session-snapshot.ts +32 -17
- package/src/test-support/plugin-runtime.ts +149 -0
- package/src/theme/color-utils.ts +1 -0
- package/src/theme/colors.ts +16 -0
- package/src/types/broker.ts +16 -1
- package/src/types/capability-route-source.ts +23 -0
- package/src/types/config.test.ts +23 -0
- package/src/types/config.ts +145 -10
- package/src/types/context-menu.ts +95 -0
- package/src/types/data-provider.ts +34 -3
- package/src/types/desktop-menu.ts +13 -0
- package/src/types/desktop-window.ts +29 -0
- package/src/types/financials.ts +123 -2
- package/src/types/instrument.ts +1 -0
- package/src/types/news-source.ts +9 -0
- package/src/types/persistence.ts +1 -0
- package/src/types/plugin.ts +32 -8
- package/src/ui/ascii-font.ts +85 -0
- package/src/ui/context-menu.test.ts +94 -0
- package/src/ui/context-menu.tsx +433 -0
- package/src/ui/dialog.tsx +55 -0
- package/src/ui/host.tsx +361 -0
- package/src/ui/index.tsx +126 -0
- package/src/ui/toast.tsx +40 -0
- package/src/updater.test.ts +69 -0
- package/src/updater.ts +61 -18
- package/src/utils/api-client.test.ts +186 -6
- package/src/utils/api-client.ts +433 -33
- package/src/utils/back-navigation.ts +6 -0
- package/src/utils/cli-output.ts +1 -0
- package/src/utils/currency-units.test.ts +1 -6
- package/src/utils/currency-units.ts +1 -0
- package/src/utils/debug-log.ts +68 -0
- package/src/utils/dom-screenshot.ts +326 -0
- package/src/utils/exchange-rate-map.ts +1 -0
- package/src/utils/exchanges.ts +214 -0
- package/src/utils/format.test.ts +7 -0
- package/src/utils/format.ts +1 -0
- package/src/utils/fuzzy-search.ts +1 -0
- package/src/utils/html-entities.test.ts +8 -0
- package/src/utils/html-entities.ts +32 -0
- package/src/utils/http-transport.ts +11 -0
- package/src/utils/main-thread-monitor.ts +127 -0
- package/src/utils/perf-marks.ts +54 -0
- package/src/utils/quote-freshness.ts +27 -50
- package/src/utils/quote-resolution.test.ts +5 -5
- package/src/utils/quote-resolution.ts +0 -1
- package/src/utils/quote-time.ts +1 -5
- package/src/utils/sec.test.ts +0 -12
- package/src/utils/selection-clipboard.ts +14 -6
- package/src/utils/shortcut-labels.test.ts +20 -0
- package/src/utils/shortcut-labels.ts +67 -0
- package/src/utils/slugify.ts +1 -0
- package/src/utils/terminal-input-reset.ts +1 -0
- package/src/utils/text-wrap.ts +82 -0
- package/src/utils/throttled-fetch.test.ts +19 -0
- package/src/utils/throttled-fetch.ts +11 -9
- package/src/utils/ticker-list.test.ts +1 -7
- package/src/utils/ticker-list.ts +1 -0
- package/src/utils/timestamp.ts +29 -0
- package/src/utils/tweet-text.test.ts +13 -0
- package/src/utils/tweet-text.ts +18 -0
- package/src/utils/url.ts +11 -0
- package/src/version.ts +1 -1
- package/src/components/chart/chart-control-hint.tsx +0 -39
- package/src/components/detail-feed-view.test.tsx +0 -180
- package/src/components/detail-feed-view.tsx +0 -531
- package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
- package/src/components/layout/pixel-mouse.test.ts +0 -40
- package/src/components/spinner.tsx +0 -2
- package/src/components/tab-bar.tsx +0 -17
- package/src/components/ui/multi-select-chips.tsx +0 -69
- package/src/plugins/builtin/ai/index.test.tsx +0 -139
- package/src/plugins/builtin/ask-ai.tsx +0 -9
- package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
- package/src/plugins/builtin/correlation/settings.test.ts +0 -34
- package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
- package/src/plugins/builtin/econ/fred-client.ts +0 -79
- package/src/plugins/builtin/fred-settings.ts +0 -10
- package/src/plugins/builtin/options-availability.test.tsx +0 -179
- package/src/plugins/builtin/options-availability.ts +0 -158
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
- package/src/plugins/prediction-markets/status.ts +0 -24
- package/src/plugins/prediction-markets/table.tsx +0 -87
- package/src/utils/quote-time.test.ts +0 -22
- package/src/utils/selection-clipboard.test.ts +0 -43
- package/src/utils/terminal-input-reset.test.ts +0 -50
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost } from "../../ui";
|
|
1
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useNativeRenderer, useRendererHost, useUiCapabilities } from "../../ui";
|
|
4
|
+
import { useShortcut, useViewport, type KeyEventLike } from "../../react/input";
|
|
5
|
+
import { useDialogState } from "../../ui/dialog";
|
|
6
|
+
import { scheduleConfigSave } from "../../state/config-save-scheduler";
|
|
7
|
+
import type { DesktopDockPreviewState, DesktopWindowBridge } from "../../types/desktop-window";
|
|
5
8
|
import {
|
|
6
9
|
MIN_FLOAT_HEIGHT,
|
|
7
10
|
MIN_FLOAT_WIDTH,
|
|
@@ -12,6 +15,7 @@ import {
|
|
|
12
15
|
getDockDividerLayouts,
|
|
13
16
|
getDockLeafLayouts,
|
|
14
17
|
getRememberedFloatingRect,
|
|
18
|
+
gridlockAllPanes,
|
|
15
19
|
isPaneInLayout,
|
|
16
20
|
removePane,
|
|
17
21
|
resizeSplitAtPath,
|
|
@@ -28,21 +32,39 @@ import {
|
|
|
28
32
|
} from "../../plugins/pane-manager";
|
|
29
33
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
30
34
|
import { removePaneInstances, type LayoutConfig } from "../../types/config";
|
|
35
|
+
import { contextMenuDivider, type ContextMenuItem } from "../../types/context-menu";
|
|
31
36
|
import {
|
|
32
|
-
PaneInstanceProvider,
|
|
33
|
-
resolveCollectionForPane,
|
|
34
37
|
resolveTickerForPane,
|
|
35
|
-
|
|
38
|
+
useAppDispatch,
|
|
39
|
+
useAppSelector,
|
|
36
40
|
} from "../../state/app-context";
|
|
41
|
+
import {
|
|
42
|
+
selectCommandBarOpen,
|
|
43
|
+
selectFocusedPaneId,
|
|
44
|
+
selectLayout,
|
|
45
|
+
selectStatusBarVisible,
|
|
46
|
+
} from "../../state/selectors-ui";
|
|
37
47
|
import { colors } from "../../theme/colors";
|
|
38
48
|
import { PANE_HEADER_ACTION, PANE_HEADER_CLOSE } from "./pane-header";
|
|
39
49
|
import { getNativeSurfaceManager, type NativeOccluder, type NativePaneLayer } from "../chart/native/surface-manager";
|
|
40
50
|
import { FloatingPaneWrapper } from "./floating-pane";
|
|
51
|
+
import { PaneContent } from "./pane-content";
|
|
41
52
|
import { PaneWrapper } from "./pane";
|
|
42
|
-
import {
|
|
53
|
+
import { hasPaneFooterContent, PaneFooterProvider } from "./pane-footer";
|
|
54
|
+
import { getPaneBodyHeight, getPaneBodyWidth, shouldReservePaneFooter } from "./pane-sizing";
|
|
55
|
+
import { getPaneDisplayTitle } from "./pane-title";
|
|
56
|
+
import { TITLEBAR_OVERLAY_HEIGHT_PX } from "./titlebar-overlay";
|
|
57
|
+
import { capturePaneScreenshotPngBase64 } from "../../utils/dom-screenshot";
|
|
58
|
+
import {
|
|
59
|
+
formatPlatformShortcutLabel,
|
|
60
|
+
getShortcutDisplayMode,
|
|
61
|
+
type ShortcutDisplayMode,
|
|
62
|
+
} from "../../utils/shortcut-labels";
|
|
43
63
|
|
|
44
64
|
interface ShellProps {
|
|
45
65
|
pluginRegistry: PluginRegistry;
|
|
66
|
+
desktopWindowBridge?: DesktopWindowBridge;
|
|
67
|
+
desktopDockPreview?: DesktopDockPreviewState | null;
|
|
46
68
|
}
|
|
47
69
|
|
|
48
70
|
interface HoverOverlay {
|
|
@@ -83,7 +105,8 @@ interface ActionMenuState {
|
|
|
83
105
|
paneId: string;
|
|
84
106
|
x: number;
|
|
85
107
|
y: number;
|
|
86
|
-
|
|
108
|
+
width: number;
|
|
109
|
+
items: Array<{ id: string; label: string; accelerator?: string; action: () => void }>;
|
|
87
110
|
}
|
|
88
111
|
|
|
89
112
|
interface FloatingPreviewRect {
|
|
@@ -114,6 +137,13 @@ interface PaneDragReleaseResult {
|
|
|
114
137
|
shouldShowGridlockTip: boolean;
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
export interface PaneDragRectState {
|
|
141
|
+
mode: "docked" | "floating";
|
|
142
|
+
startX: number;
|
|
143
|
+
startY: number;
|
|
144
|
+
origRect: FloatingRect;
|
|
145
|
+
}
|
|
146
|
+
|
|
117
147
|
type DragMode =
|
|
118
148
|
| {
|
|
119
149
|
type: "divider";
|
|
@@ -127,11 +157,7 @@ type DragMode =
|
|
|
127
157
|
| {
|
|
128
158
|
type: "pane-drag";
|
|
129
159
|
paneId: string;
|
|
130
|
-
|
|
131
|
-
startX: number;
|
|
132
|
-
startY: number;
|
|
133
|
-
origRect: FloatingRect;
|
|
134
|
-
}
|
|
160
|
+
} & PaneDragRectState
|
|
135
161
|
| {
|
|
136
162
|
type: "float-resize";
|
|
137
163
|
paneId: string;
|
|
@@ -140,35 +166,157 @@ type DragMode =
|
|
|
140
166
|
origRect: FloatingRect;
|
|
141
167
|
};
|
|
142
168
|
|
|
143
|
-
|
|
144
|
-
|
|
169
|
+
interface ShellMouseEvent {
|
|
170
|
+
type: string;
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
button?: number;
|
|
174
|
+
preciseX?: number;
|
|
175
|
+
preciseY?: number;
|
|
176
|
+
stopPropagation: () => void;
|
|
177
|
+
preventDefault: () => void;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const DEFAULT_HEADER_HEIGHT = 1;
|
|
181
|
+
const MENU_MIN_WIDTH = 18;
|
|
182
|
+
const MENU_MAX_WIDTH = 44;
|
|
183
|
+
const MENU_Z_INDEX = 10_000;
|
|
145
184
|
const PANE_DRAG_THRESHOLD = 2;
|
|
185
|
+
const PRECISE_PANE_DRAG_THRESHOLD = 0.15;
|
|
186
|
+
const PANE_MANAGEMENT_ACCELERATORS = {
|
|
187
|
+
settings: "CmdOrCtrl+,",
|
|
188
|
+
toggleFloating: "CmdOrCtrl+Shift+D",
|
|
189
|
+
popOut: "CmdOrCtrl+Shift+O",
|
|
190
|
+
copyScreenshot: "CmdOrCtrl+Shift+C",
|
|
191
|
+
close: "CmdOrCtrl+W",
|
|
192
|
+
layoutActions: "CmdOrCtrl+Shift+L",
|
|
193
|
+
gridlockAll: "CmdOrCtrl+Shift+G",
|
|
194
|
+
} as const;
|
|
195
|
+
|
|
196
|
+
type PaneManagementShortcut =
|
|
197
|
+
| "settings"
|
|
198
|
+
| "toggle-floating"
|
|
199
|
+
| "pop-out"
|
|
200
|
+
| "copy-screenshot"
|
|
201
|
+
| "close"
|
|
202
|
+
| "layout-actions"
|
|
203
|
+
| "gridlock-all";
|
|
204
|
+
|
|
205
|
+
export function resolvePaneManagementShortcut(
|
|
206
|
+
event: Pick<KeyEventLike, "name" | "key" | "ctrl" | "meta" | "super" | "shift" | "alt">,
|
|
207
|
+
): PaneManagementShortcut | null {
|
|
208
|
+
if (!event.ctrl && !event.meta && !event.super) return null;
|
|
209
|
+
const name = (event.name ?? event.key ?? "").toLowerCase();
|
|
210
|
+
if (name === "w") return "close";
|
|
211
|
+
if (!event.shift && name === ",") return "settings";
|
|
212
|
+
if (!event.shift || event.alt) return null;
|
|
213
|
+
if (name === "c") return "copy-screenshot";
|
|
214
|
+
if (name === "d") return "toggle-floating";
|
|
215
|
+
if (name === "o") return "pop-out";
|
|
216
|
+
if (name === "l") return "layout-actions";
|
|
217
|
+
if (name === "g") return "gridlock-all";
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveAppHeaderHeightCells(options: { titleBarOverlay?: boolean; cellHeightPx?: number }): number {
|
|
222
|
+
if (!options.titleBarOverlay || !options.cellHeightPx || options.cellHeightPx <= 0) return DEFAULT_HEADER_HEIGHT;
|
|
223
|
+
return TITLEBAR_OVERLAY_HEIGHT_PX / options.cellHeightPx;
|
|
224
|
+
}
|
|
146
225
|
|
|
147
226
|
function pointInRect(rect: LayoutBounds, x: number, y: number): boolean {
|
|
148
227
|
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
|
149
228
|
}
|
|
150
229
|
|
|
151
|
-
function isMeaningfulPaneDrag(startX: number, startY: number, currentX: number, currentY: number): boolean {
|
|
152
|
-
return Math.max(Math.abs(currentX - startX), Math.abs(currentY - startY)) >=
|
|
230
|
+
function isMeaningfulPaneDrag(startX: number, startY: number, currentX: number, currentY: number, threshold = PANE_DRAG_THRESHOLD): boolean {
|
|
231
|
+
return Math.max(Math.abs(currentX - startX), Math.abs(currentY - startY)) >= threshold;
|
|
153
232
|
}
|
|
154
233
|
|
|
155
|
-
function
|
|
234
|
+
function clampFinite(value: number, min: number, max: number, fallback = min): number {
|
|
235
|
+
const normalized = Number.isFinite(value) ? value : fallback;
|
|
236
|
+
return Math.max(min, Math.min(max, normalized));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function fitFloatingDimension(value: number, min: number, available: number): number {
|
|
240
|
+
const max = Math.max(1, Number.isFinite(available) ? available : 1);
|
|
241
|
+
const boundedMin = Math.min(min, max);
|
|
242
|
+
return clampFinite(value, boundedMin, max, boundedMin);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function constrainFloatingRectToBounds(
|
|
156
246
|
rect: FloatingRect,
|
|
157
|
-
drag: Extract<DragMode, { type: "pane-drag" }>,
|
|
158
|
-
pointerX: number,
|
|
159
|
-
pointerY: number,
|
|
160
247
|
totalWidth: number,
|
|
161
248
|
totalHeight: number,
|
|
162
249
|
): FloatingRect {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
250
|
+
const boundsWidth = Math.max(1, Number.isFinite(totalWidth) ? totalWidth : 1);
|
|
251
|
+
const boundsHeight = Math.max(1, Number.isFinite(totalHeight) ? totalHeight : 1);
|
|
252
|
+
const width = fitFloatingDimension(rect.width, MIN_FLOAT_WIDTH, boundsWidth);
|
|
253
|
+
const height = fitFloatingDimension(rect.height, MIN_FLOAT_HEIGHT, boundsHeight);
|
|
254
|
+
const maxX = Math.max(0, boundsWidth - width);
|
|
255
|
+
const maxY = Math.max(0, boundsHeight - height);
|
|
256
|
+
|
|
165
257
|
return {
|
|
166
258
|
...rect,
|
|
167
|
-
x:
|
|
168
|
-
y:
|
|
259
|
+
x: clampFinite(rect.x, 0, maxX, 0),
|
|
260
|
+
y: clampFinite(rect.y, 0, maxY, 0),
|
|
261
|
+
width,
|
|
262
|
+
height,
|
|
169
263
|
};
|
|
170
264
|
}
|
|
171
265
|
|
|
266
|
+
function positionFloatingRectUnderPointer(
|
|
267
|
+
rect: FloatingRect,
|
|
268
|
+
drag: PaneDragRectState,
|
|
269
|
+
pointerX: number,
|
|
270
|
+
pointerY: number,
|
|
271
|
+
totalWidth: number,
|
|
272
|
+
totalHeight: number,
|
|
273
|
+
): FloatingRect {
|
|
274
|
+
const fittedRect = constrainFloatingRectToBounds(rect, totalWidth, totalHeight);
|
|
275
|
+
const pointerOffsetX = Math.max(0, Math.min(fittedRect.width - 1, drag.startX - drag.origRect.x));
|
|
276
|
+
const pointerOffsetY = Math.max(0, Math.min(fittedRect.height - 1, drag.startY - drag.origRect.y));
|
|
277
|
+
return constrainFloatingRectToBounds({
|
|
278
|
+
...fittedRect,
|
|
279
|
+
x: pointerX - pointerOffsetX,
|
|
280
|
+
y: pointerY - pointerOffsetY,
|
|
281
|
+
}, totalWidth, totalHeight);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function resolvePaneDragFloatingRect(
|
|
285
|
+
drag: PaneDragRectState,
|
|
286
|
+
baseRect: FloatingRect,
|
|
287
|
+
pointerX: number,
|
|
288
|
+
pointerY: number,
|
|
289
|
+
totalWidth: number,
|
|
290
|
+
totalHeight: number,
|
|
291
|
+
): FloatingRect {
|
|
292
|
+
if (drag.mode === "docked") {
|
|
293
|
+
return positionFloatingRectUnderPointer(baseRect, drag, pointerX, pointerY, totalWidth, totalHeight);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return constrainFloatingRectToBounds({
|
|
297
|
+
x: drag.origRect.x + (pointerX - drag.startX),
|
|
298
|
+
y: drag.origRect.y + (pointerY - drag.startY),
|
|
299
|
+
width: drag.origRect.width,
|
|
300
|
+
height: drag.origRect.height,
|
|
301
|
+
}, totalWidth, totalHeight);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function resolveFloatResizeRect(
|
|
305
|
+
drag: Extract<DragMode, { type: "float-resize" }>,
|
|
306
|
+
pointerX: number,
|
|
307
|
+
pointerY: number,
|
|
308
|
+
totalWidth: number,
|
|
309
|
+
totalHeight: number,
|
|
310
|
+
): FloatingRect {
|
|
311
|
+
return constrainFloatingRectToBounds({
|
|
312
|
+
x: drag.origRect.x,
|
|
313
|
+
y: drag.origRect.y,
|
|
314
|
+
width: Math.max(MIN_FLOAT_WIDTH, Math.min(totalWidth - drag.origRect.x, drag.origRect.width + (pointerX - drag.startX))),
|
|
315
|
+
height: Math.max(MIN_FLOAT_HEIGHT, Math.min(totalHeight - drag.origRect.y, drag.origRect.height + (pointerY - drag.startY))),
|
|
316
|
+
zIndex: drag.origRect.zIndex,
|
|
317
|
+
}, totalWidth, totalHeight);
|
|
318
|
+
}
|
|
319
|
+
|
|
172
320
|
function centerRectWithin(parent: LayoutBounds, width: number, height: number): LayoutBounds {
|
|
173
321
|
const nextWidth = Math.max(1, Math.min(parent.width, width));
|
|
174
322
|
const nextHeight = Math.max(1, Math.min(parent.height, height));
|
|
@@ -248,6 +396,7 @@ export function buildNativeWindowState(
|
|
|
248
396
|
overlay: { open: boolean; width: number; contentHeight: number },
|
|
249
397
|
transientOccluders: readonly NativeTransientOccluder[] = [],
|
|
250
398
|
dockDividers: readonly DockDividerLayout[] = [],
|
|
399
|
+
appHeaderHeight: number = DEFAULT_HEADER_HEIGHT,
|
|
251
400
|
): { paneLayers: NativePaneLayer[]; occluders: NativeOccluder[] } {
|
|
252
401
|
const previewedFloatingPanes = floatingPanes.map((pane) => (
|
|
253
402
|
dragFloatingRect?.paneId === pane.paneId
|
|
@@ -265,7 +414,7 @@ export function buildNativeWindowState(
|
|
|
265
414
|
paneId: pane.paneId,
|
|
266
415
|
rect: {
|
|
267
416
|
x: pane.rect.x,
|
|
268
|
-
y: pane.rect.y +
|
|
417
|
+
y: pane.rect.y + appHeaderHeight,
|
|
269
418
|
width: pane.rect.width,
|
|
270
419
|
height: pane.rect.height,
|
|
271
420
|
},
|
|
@@ -278,7 +427,7 @@ export function buildNativeWindowState(
|
|
|
278
427
|
paneId: null,
|
|
279
428
|
rect: {
|
|
280
429
|
x: divider.rect.x,
|
|
281
|
-
y: divider.rect.y +
|
|
430
|
+
y: divider.rect.y + appHeaderHeight,
|
|
282
431
|
width: divider.rect.width,
|
|
283
432
|
height: divider.rect.height,
|
|
284
433
|
},
|
|
@@ -293,7 +442,7 @@ export function buildNativeWindowState(
|
|
|
293
442
|
paneId: null,
|
|
294
443
|
rect: {
|
|
295
444
|
x: occluder.rect.x,
|
|
296
|
-
y: occluder.rect.y +
|
|
445
|
+
y: occluder.rect.y + appHeaderHeight,
|
|
297
446
|
width: occluder.rect.width,
|
|
298
447
|
height: occluder.rect.height,
|
|
299
448
|
},
|
|
@@ -307,7 +456,7 @@ export function buildNativeWindowState(
|
|
|
307
456
|
paneId: null,
|
|
308
457
|
rect: {
|
|
309
458
|
x: 0,
|
|
310
|
-
y:
|
|
459
|
+
y: appHeaderHeight,
|
|
311
460
|
width: overlay.width,
|
|
312
461
|
height: overlay.contentHeight,
|
|
313
462
|
},
|
|
@@ -362,7 +511,7 @@ function makeSnapGuides(width: number, height: number): SnapGuide[] {
|
|
|
362
511
|
const cornerWidth = Math.max(8, Math.min(16, Math.floor(width * 0.2)));
|
|
363
512
|
const cornerHeight = Math.max(4, Math.min(8, Math.floor(height * 0.22)));
|
|
364
513
|
const edgeWidth = Math.max(6, Math.min(10, Math.floor(width * 0.1)));
|
|
365
|
-
const
|
|
514
|
+
const topBottomEdgeHeight = Math.max(2, Math.min(4, Math.floor(height * 0.1)));
|
|
366
515
|
|
|
367
516
|
return [
|
|
368
517
|
{
|
|
@@ -412,16 +561,16 @@ function makeSnapGuides(width: number, height: number): SnapGuide[] {
|
|
|
412
561
|
},
|
|
413
562
|
{
|
|
414
563
|
position: "top",
|
|
415
|
-
triggerRect: { x: cornerWidth, y: 0, width: Math.max(1, width - (cornerWidth * 2)), height:
|
|
564
|
+
triggerRect: { x: cornerWidth, y: 0, width: Math.max(1, width - (cornerWidth * 2)), height: topBottomEdgeHeight },
|
|
416
565
|
previewRect: { x: 0, y: 0, width, height: halfHeight },
|
|
417
566
|
},
|
|
418
567
|
{
|
|
419
568
|
position: "bottom",
|
|
420
569
|
triggerRect: {
|
|
421
570
|
x: cornerWidth,
|
|
422
|
-
y: Math.max(0, height -
|
|
571
|
+
y: Math.max(0, height - topBottomEdgeHeight),
|
|
423
572
|
width: Math.max(1, width - (cornerWidth * 2)),
|
|
424
|
-
height:
|
|
573
|
+
height: topBottomEdgeHeight,
|
|
425
574
|
},
|
|
426
575
|
previewRect: { x: 0, y: Math.max(0, height - halfHeight), width, height: halfHeight },
|
|
427
576
|
},
|
|
@@ -463,21 +612,33 @@ function menuForPane(
|
|
|
463
612
|
persistLayout: (nextLayout: LayoutConfig, options?: { pushHistory?: boolean }) => void,
|
|
464
613
|
focusPane: (paneId: string) => void,
|
|
465
614
|
openPaneSettings: (paneId: string) => void,
|
|
466
|
-
|
|
467
|
-
|
|
615
|
+
desktopWindowBridge?: DesktopWindowBridge,
|
|
616
|
+
copyPaneScreenshot?: (paneId: string) => void | Promise<void>,
|
|
617
|
+
): ContextMenuItem[] {
|
|
618
|
+
const baseActions: ContextMenuItem[] = [];
|
|
468
619
|
if (pluginRegistry.hasPaneSettings(pane.instance.instanceId)) {
|
|
469
620
|
baseActions.push({
|
|
470
621
|
id: "settings",
|
|
471
622
|
label: "Settings",
|
|
472
|
-
|
|
623
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.settings,
|
|
624
|
+
onSelect: () => openPaneSettings(pane.instance.instanceId),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (copyPaneScreenshot) {
|
|
628
|
+
baseActions.push({
|
|
629
|
+
id: "copy-screenshot",
|
|
630
|
+
label: "Copy Screenshot",
|
|
631
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.copyScreenshot,
|
|
632
|
+
onSelect: () => copyPaneScreenshot(pane.instance.instanceId),
|
|
473
633
|
});
|
|
474
634
|
}
|
|
475
635
|
|
|
476
636
|
if (pane.floating) {
|
|
477
637
|
baseActions.push({
|
|
478
638
|
id: "dock",
|
|
479
|
-
label: "Dock",
|
|
480
|
-
|
|
639
|
+
label: "Dock Pane",
|
|
640
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.toggleFloating,
|
|
641
|
+
onSelect: () => {
|
|
481
642
|
persistLayout(applyDrop(layout, pane.instance.instanceId, { kind: "frame", edge: "right" }));
|
|
482
643
|
focusPane(pane.instance.instanceId);
|
|
483
644
|
},
|
|
@@ -485,59 +646,195 @@ function menuForPane(
|
|
|
485
646
|
} else {
|
|
486
647
|
baseActions.push({
|
|
487
648
|
id: "float",
|
|
488
|
-
label: "Float",
|
|
489
|
-
|
|
649
|
+
label: "Float Pane",
|
|
650
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.toggleFloating,
|
|
651
|
+
onSelect: () => {
|
|
490
652
|
persistLayout(floatPane(layout, pane.instance.instanceId, width, contentHeight, pane.def));
|
|
491
653
|
focusPane(pane.instance.instanceId);
|
|
492
654
|
},
|
|
493
655
|
});
|
|
494
656
|
}
|
|
495
657
|
|
|
658
|
+
if (desktopWindowBridge?.kind === "main" && desktopWindowBridge.popOutPane) {
|
|
659
|
+
baseActions.push({
|
|
660
|
+
id: "pop-out",
|
|
661
|
+
label: "Pop Out",
|
|
662
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.popOut,
|
|
663
|
+
onSelect: () => {
|
|
664
|
+
void desktopWindowBridge.popOutPane?.(pane.instance.instanceId);
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
baseActions.push({
|
|
670
|
+
id: "close-pane",
|
|
671
|
+
label: "Close Pane",
|
|
672
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.close,
|
|
673
|
+
onSelect: () => persistLayout(removePane(layout, pane.instance.instanceId)),
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
baseActions.push(
|
|
677
|
+
contextMenuDivider("pane:layout-divider"),
|
|
678
|
+
{
|
|
679
|
+
id: "layout-actions",
|
|
680
|
+
label: "Layout Actions...",
|
|
681
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.layoutActions,
|
|
682
|
+
onSelect: () => pluginRegistry.openCommandBar("LAY "),
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: "gridlock-all",
|
|
686
|
+
label: "Gridlock All Windows",
|
|
687
|
+
accelerator: PANE_MANAGEMENT_ACCELERATORS.gridlockAll,
|
|
688
|
+
onSelect: () => {
|
|
689
|
+
persistLayout(gridlockAllPanes(layout, { x: 0, y: 0, width, height: contentHeight }));
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
|
|
496
694
|
return baseActions;
|
|
497
695
|
}
|
|
498
696
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
697
|
+
function menuItemsForFallback(
|
|
698
|
+
items: ContextMenuItem[],
|
|
699
|
+
shortcutDisplayMode: ShortcutDisplayMode,
|
|
700
|
+
): Array<{ id: string; label: string; accelerator?: string; action: () => void }> {
|
|
701
|
+
return items.flatMap((item) => {
|
|
702
|
+
if (item.type === "divider" || item.type === "role" || item.enabled === false || item.hidden === true) return [];
|
|
703
|
+
if (!item.onSelect) return [];
|
|
704
|
+
return [{
|
|
705
|
+
id: item.id,
|
|
706
|
+
label: item.label,
|
|
707
|
+
accelerator: item.accelerator
|
|
708
|
+
? formatPlatformShortcutLabel(item.accelerator, undefined, shortcutDisplayMode)
|
|
709
|
+
: undefined,
|
|
710
|
+
action: () => { void item.onSelect?.(); },
|
|
711
|
+
}];
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function actionMenuWidth(
|
|
716
|
+
items: Array<{ label: string; accelerator?: string }>,
|
|
717
|
+
availableWidth: number,
|
|
718
|
+
): number {
|
|
719
|
+
const requested = Math.max(
|
|
720
|
+
MENU_MIN_WIDTH,
|
|
721
|
+
...items.map((item) => item.label.length + (item.accelerator ? item.accelerator.length + 3 : 0) + 2),
|
|
722
|
+
);
|
|
723
|
+
return Math.max(MENU_MIN_WIDTH, Math.min(MENU_MAX_WIDTH, availableWidth, requested));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function truncateMenuText(text: string, width: number): string {
|
|
727
|
+
if (width <= 0) return "";
|
|
728
|
+
if (text.length <= width) return text;
|
|
729
|
+
if (width <= 3) return ".".repeat(width);
|
|
730
|
+
return `${text.slice(0, width - 3)}...`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function resolveExternalDockPreview(
|
|
734
|
+
preview: DesktopDockPreviewState | null | undefined,
|
|
735
|
+
bounds: LayoutBounds,
|
|
736
|
+
): DragPreview | null {
|
|
737
|
+
if (!preview?.paneId || !preview.edge) return null;
|
|
738
|
+
|
|
739
|
+
switch (preview.edge) {
|
|
740
|
+
case "left":
|
|
741
|
+
return {
|
|
742
|
+
kind: "dock",
|
|
743
|
+
target: { kind: "frame", edge: "left" },
|
|
744
|
+
rect: { x: bounds.x, y: bounds.y, width: Math.max(1, Math.floor(bounds.width / 2)), height: bounds.height },
|
|
745
|
+
};
|
|
746
|
+
case "right": {
|
|
747
|
+
const width = Math.max(1, Math.floor(bounds.width / 2));
|
|
748
|
+
return {
|
|
749
|
+
kind: "dock",
|
|
750
|
+
target: { kind: "frame", edge: "right" },
|
|
751
|
+
rect: { x: bounds.x + Math.max(0, bounds.width - width), y: bounds.y, width, height: bounds.height },
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
case "top":
|
|
755
|
+
return {
|
|
756
|
+
kind: "dock",
|
|
757
|
+
target: { kind: "frame", edge: "top" },
|
|
758
|
+
rect: { x: bounds.x, y: bounds.y, width: bounds.width, height: Math.max(1, Math.floor(bounds.height / 2)) },
|
|
759
|
+
};
|
|
760
|
+
case "bottom": {
|
|
761
|
+
const height = Math.max(1, Math.floor(bounds.height / 2));
|
|
762
|
+
return {
|
|
763
|
+
kind: "dock",
|
|
764
|
+
target: { kind: "frame", edge: "bottom" },
|
|
765
|
+
rect: { x: bounds.x, y: bounds.y + Math.max(0, bounds.height - height), width: bounds.width, height },
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
default:
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview }: ShellProps) {
|
|
774
|
+
const dispatch = useAppDispatch();
|
|
775
|
+
const config = useAppSelector((state) => state.config);
|
|
776
|
+
const paneState = useAppSelector((state) => state.paneState);
|
|
777
|
+
const focusedPaneId = useAppSelector(selectFocusedPaneId);
|
|
778
|
+
const commandBarOpen = useAppSelector(selectCommandBarOpen);
|
|
779
|
+
const inputCaptured = useAppSelector((state) => state.inputCaptured);
|
|
780
|
+
const statusBarVisible = useAppSelector(selectStatusBarVisible);
|
|
781
|
+
const renderer = useNativeRenderer();
|
|
782
|
+
const rendererHost = useRendererHost();
|
|
783
|
+
const uiKind = useUiHost().kind;
|
|
784
|
+
const shortcutDisplayMode = getShortcutDisplayMode(uiKind);
|
|
785
|
+
const { nativePaneChrome, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
|
|
786
|
+
const { showContextMenu } = useContextMenu();
|
|
787
|
+
const { width, height } = useViewport();
|
|
503
788
|
const nativeSurfaceManager = useMemo(() => getNativeSurfaceManager(renderer), [renderer]);
|
|
504
789
|
|
|
505
|
-
const
|
|
790
|
+
const appHeaderHeight = resolveAppHeaderHeightCells({ titleBarOverlay, cellHeightPx });
|
|
791
|
+
const contentHeight = Math.max(1, height - appHeaderHeight - (statusBarVisible ? 1 : 0));
|
|
506
792
|
pluginRegistry.getTermSizeFn = () => ({ width, height: contentHeight });
|
|
507
793
|
|
|
508
|
-
const layout =
|
|
794
|
+
const layout = useAppSelector(selectLayout);
|
|
509
795
|
const dialogOpen = useDialogState((dialog) => dialog.isOpen);
|
|
510
796
|
const [hoveredPaneId, setHoveredPaneId] = useState<string | null>(null);
|
|
797
|
+
const setHoveredPaneIfChanged = useCallback((paneId: string | null) => {
|
|
798
|
+
setHoveredPaneId((current) => (current === paneId ? current : paneId));
|
|
799
|
+
}, []);
|
|
511
800
|
const [menuState, setMenuState] = useState<ActionMenuState | null>(null);
|
|
512
|
-
const
|
|
801
|
+
const [hoveredMenuItemId, setHoveredMenuItemId] = useState<string | null>(null);
|
|
802
|
+
const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
|
|
513
803
|
|
|
514
804
|
const dragRef = useRef<DragMode | null>(null);
|
|
515
805
|
const [dragFloatingRect, setDragFloatingRect] = useState<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
516
806
|
const [dragCursor, setDragCursor] = useState<{ x: number; y: number } | null>(null);
|
|
517
807
|
const [dividerPreview, setDividerPreview] = useState<DividerPreviewState | null>(null);
|
|
518
808
|
const [dockPreview, setDockPreview] = useState<DragPreview | null>(null);
|
|
809
|
+
const dividerPreviewRef = useRef<DividerPreviewState | null>(null);
|
|
810
|
+
const dockPreviewRef = useRef<DragPreview | null>(null);
|
|
811
|
+
|
|
812
|
+
const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
|
|
813
|
+
setDragFloatingRect(next
|
|
814
|
+
? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
|
|
815
|
+
: null);
|
|
816
|
+
}, [contentHeight, width]);
|
|
817
|
+
|
|
818
|
+
const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
|
|
819
|
+
dividerPreviewRef.current = next;
|
|
820
|
+
setDividerPreview(next);
|
|
821
|
+
}, []);
|
|
822
|
+
|
|
823
|
+
const updateDockPreview = useCallback((next: DragPreview | null) => {
|
|
824
|
+
dockPreviewRef.current = next;
|
|
825
|
+
setDockPreview(next);
|
|
826
|
+
}, []);
|
|
519
827
|
|
|
520
828
|
const cancelActiveDrag = useCallback(() => {
|
|
521
829
|
dragRef.current = null;
|
|
522
|
-
|
|
830
|
+
updateDragFloatingRect(null);
|
|
523
831
|
setDragCursor(null);
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}, []);
|
|
527
|
-
|
|
528
|
-
useKeyboard((event) => {
|
|
529
|
-
if (event.name === "escape") {
|
|
530
|
-
if (!dragRef.current) return;
|
|
531
|
-
cancelActiveDrag();
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
if (event.name !== "w" || !event.ctrl) return;
|
|
535
|
-
if (dragRef.current || overlayOpen || state.inputCaptured) return;
|
|
536
|
-
closeFocusedPane();
|
|
537
|
-
});
|
|
832
|
+
updateDividerPreview(null);
|
|
833
|
+
updateDockPreview(null);
|
|
834
|
+
}, [updateDividerPreview, updateDockPreview, updateDragFloatingRect]);
|
|
538
835
|
|
|
539
836
|
const disabledPaneIds = useMemo(() => {
|
|
540
|
-
const disabledPlugins = new Set(
|
|
837
|
+
const disabledPlugins = new Set(config.disabledPlugins);
|
|
541
838
|
const ids = new Set<string>();
|
|
542
839
|
for (const pluginId of disabledPlugins) {
|
|
543
840
|
for (const paneId of pluginRegistry.getPluginPaneIds(pluginId)) {
|
|
@@ -545,7 +842,7 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
545
842
|
}
|
|
546
843
|
}
|
|
547
844
|
return ids;
|
|
548
|
-
}, [
|
|
845
|
+
}, [config.disabledPlugins, pluginRegistry]);
|
|
549
846
|
|
|
550
847
|
const hiddenInstanceIds = useMemo(() => (
|
|
551
848
|
layout.instances
|
|
@@ -559,34 +856,53 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
559
856
|
);
|
|
560
857
|
|
|
561
858
|
const persistLayout = useCallback((nextLayout: LayoutConfig, options?: { pushHistory?: boolean }) => {
|
|
562
|
-
const layouts =
|
|
563
|
-
index ===
|
|
859
|
+
const layouts = config.layouts.map((savedLayout, index) => (
|
|
860
|
+
index === config.activeLayoutIndex ? { ...savedLayout, layout: nextLayout } : savedLayout
|
|
564
861
|
));
|
|
565
862
|
if (options?.pushHistory !== false) {
|
|
566
863
|
dispatch({ type: "PUSH_LAYOUT_HISTORY" });
|
|
567
864
|
}
|
|
568
865
|
dispatch({ type: "UPDATE_LAYOUT", layout: nextLayout });
|
|
569
|
-
|
|
570
|
-
}, [
|
|
866
|
+
scheduleConfigSave({ ...config, layout: nextLayout, layouts });
|
|
867
|
+
}, [config, dispatch]);
|
|
571
868
|
|
|
572
869
|
const closeFocusedPane = useCallback(() => {
|
|
573
|
-
if (!
|
|
574
|
-
persistLayout(removePane(visibleLayout,
|
|
575
|
-
|
|
870
|
+
if (!focusedPaneId || !isPaneInLayout(visibleLayout, focusedPaneId)) return false;
|
|
871
|
+
persistLayout(removePane(visibleLayout, focusedPaneId));
|
|
872
|
+
return true;
|
|
873
|
+
}, [focusedPaneId, persistLayout, visibleLayout]);
|
|
576
874
|
|
|
577
875
|
const focusPane = useCallback((paneId: string) => {
|
|
578
876
|
dispatch({ type: "FOCUS_PANE", paneId });
|
|
579
877
|
}, [dispatch]);
|
|
580
878
|
|
|
581
879
|
const openLayoutMenu = useCallback(() => {
|
|
582
|
-
pluginRegistry.
|
|
880
|
+
pluginRegistry.openCommandBar("LAY ");
|
|
583
881
|
}, [pluginRegistry]);
|
|
584
882
|
|
|
585
883
|
const openPaneSettings = useCallback((paneId: string) => {
|
|
586
884
|
pluginRegistry.openPaneSettingsFn(paneId);
|
|
587
885
|
setMenuState(null);
|
|
886
|
+
setHoveredMenuItemId(null);
|
|
588
887
|
}, [pluginRegistry]);
|
|
589
888
|
|
|
889
|
+
const copyPaneScreenshot = useCallback(async (paneId: string) => {
|
|
890
|
+
setMenuState(null);
|
|
891
|
+
setHoveredMenuItemId(null);
|
|
892
|
+
try {
|
|
893
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
894
|
+
if (!rendererHost.copyPngImage) {
|
|
895
|
+
throw new Error("Image clipboard is unavailable.");
|
|
896
|
+
}
|
|
897
|
+
const screenshot = await capturePaneScreenshotPngBase64(paneId);
|
|
898
|
+
await rendererHost.copyPngImage(screenshot.pngBase64);
|
|
899
|
+
pluginRegistry.notify({ body: "Pane screenshot copied", type: "success" });
|
|
900
|
+
} catch (error) {
|
|
901
|
+
const message = error instanceof Error ? error.message : "Could not copy pane screenshot.";
|
|
902
|
+
pluginRegistry.notify({ body: message, type: "error" });
|
|
903
|
+
}
|
|
904
|
+
}, [pluginRegistry, rendererHost]);
|
|
905
|
+
|
|
590
906
|
const bounds = useMemo<LayoutBounds>(() => ({ x: 0, y: 0, width, height: contentHeight }), [contentHeight, width]);
|
|
591
907
|
const dockedPanes = useMemo(
|
|
592
908
|
() => resolveDocked(visibleLayout, pluginRegistry.panes).filter((pane) => !disabledPaneIds.has(pane.def.id)),
|
|
@@ -596,14 +912,106 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
596
912
|
() => resolveFloating(visibleLayout, pluginRegistry.panes).filter((pane) => !disabledPaneIds.has(pane.def.id)),
|
|
597
913
|
[disabledPaneIds, pluginRegistry.panes, visibleLayout],
|
|
598
914
|
);
|
|
915
|
+
const visibleFloatingPanes = useMemo(
|
|
916
|
+
() => floatingPanes.map((pane) => ({
|
|
917
|
+
pane,
|
|
918
|
+
rect: constrainFloatingRectToBounds(pane.floating!, width, contentHeight),
|
|
919
|
+
})),
|
|
920
|
+
[contentHeight, floatingPanes, width],
|
|
921
|
+
);
|
|
599
922
|
const paneMap = useMemo(() => new Map([...dockedPanes, ...floatingPanes].map((pane) => [pane.instance.instanceId, pane])), [dockedPanes, floatingPanes]);
|
|
600
|
-
|
|
601
|
-
const
|
|
923
|
+
|
|
924
|
+
const copyFocusedPaneScreenshot = useCallback(() => {
|
|
925
|
+
if (!focusedPaneId || !nativePaneChrome || !rendererHost.copyPngImage) return false;
|
|
926
|
+
if (!paneMap.has(focusedPaneId)) return false;
|
|
927
|
+
void copyPaneScreenshot(focusedPaneId);
|
|
928
|
+
return true;
|
|
929
|
+
}, [copyPaneScreenshot, focusedPaneId, nativePaneChrome, paneMap, rendererHost.copyPngImage]);
|
|
930
|
+
|
|
931
|
+
const openFocusedPaneSettings = useCallback(() => {
|
|
932
|
+
if (!focusedPaneId || !pluginRegistry.hasPaneSettings(focusedPaneId)) return false;
|
|
933
|
+
openPaneSettings(focusedPaneId);
|
|
934
|
+
return true;
|
|
935
|
+
}, [focusedPaneId, openPaneSettings, pluginRegistry]);
|
|
936
|
+
|
|
937
|
+
const toggleFocusedPaneFloating = useCallback(() => {
|
|
938
|
+
if (!focusedPaneId) return false;
|
|
939
|
+
const pane = paneMap.get(focusedPaneId);
|
|
940
|
+
if (!pane) return false;
|
|
941
|
+
const nextLayout = pane.floating
|
|
942
|
+
? applyDrop(visibleLayout, pane.instance.instanceId, { kind: "frame", edge: "right" })
|
|
943
|
+
: floatPane(visibleLayout, pane.instance.instanceId, width, contentHeight, pane.def);
|
|
944
|
+
persistLayout(nextLayout);
|
|
945
|
+
focusPane(pane.instance.instanceId);
|
|
946
|
+
return true;
|
|
947
|
+
}, [contentHeight, focusPane, focusedPaneId, paneMap, persistLayout, visibleLayout, width]);
|
|
948
|
+
|
|
949
|
+
const popOutFocusedPane = useCallback(() => {
|
|
950
|
+
if (!focusedPaneId || desktopWindowBridge?.kind !== "main" || !desktopWindowBridge.popOutPane) return false;
|
|
951
|
+
if (!isPaneInLayout(visibleLayout, focusedPaneId)) return false;
|
|
952
|
+
void desktopWindowBridge.popOutPane(focusedPaneId);
|
|
953
|
+
return true;
|
|
954
|
+
}, [desktopWindowBridge, focusedPaneId, visibleLayout]);
|
|
955
|
+
|
|
956
|
+
const gridlockVisiblePanes = useCallback(() => {
|
|
957
|
+
persistLayout(gridlockAllPanes(visibleLayout, { x: 0, y: 0, width, height: contentHeight }));
|
|
958
|
+
return true;
|
|
959
|
+
}, [contentHeight, persistLayout, visibleLayout, width]);
|
|
960
|
+
|
|
961
|
+
useShortcut((event) => {
|
|
962
|
+
if (event.name === "escape") {
|
|
963
|
+
if (!dragRef.current) return;
|
|
964
|
+
cancelActiveDrag();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const shortcut = resolvePaneManagementShortcut(event);
|
|
969
|
+
if (!shortcut || dragRef.current || overlayOpen || inputCaptured) return;
|
|
970
|
+
|
|
971
|
+
let handled = false;
|
|
972
|
+
switch (shortcut) {
|
|
973
|
+
case "close":
|
|
974
|
+
handled = closeFocusedPane();
|
|
975
|
+
break;
|
|
976
|
+
case "settings":
|
|
977
|
+
handled = openFocusedPaneSettings();
|
|
978
|
+
break;
|
|
979
|
+
case "toggle-floating":
|
|
980
|
+
handled = toggleFocusedPaneFloating();
|
|
981
|
+
break;
|
|
982
|
+
case "pop-out":
|
|
983
|
+
handled = popOutFocusedPane();
|
|
984
|
+
break;
|
|
985
|
+
case "copy-screenshot":
|
|
986
|
+
handled = copyFocusedPaneScreenshot();
|
|
987
|
+
break;
|
|
988
|
+
case "layout-actions":
|
|
989
|
+
openLayoutMenu();
|
|
990
|
+
handled = true;
|
|
991
|
+
break;
|
|
992
|
+
case "gridlock-all":
|
|
993
|
+
handled = gridlockVisiblePanes();
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (!handled) return;
|
|
998
|
+
event.preventDefault();
|
|
999
|
+
event.stopPropagation();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
const dockGeometryOptions = useMemo(() => (nativePaneChrome ? { precise: true } : undefined), [nativePaneChrome]);
|
|
1003
|
+
const dockLeafLayouts = useMemo(() => getDockLeafLayouts(visibleLayout, bounds, dockGeometryOptions), [bounds, dockGeometryOptions, visibleLayout]);
|
|
1004
|
+
const dockDividerLayouts = useMemo(() => getDockDividerLayouts(visibleLayout, bounds, dockGeometryOptions), [bounds, dockGeometryOptions, visibleLayout]);
|
|
602
1005
|
const snapGuides = useMemo(() => makeSnapGuides(width, contentHeight), [contentHeight, width]);
|
|
1006
|
+
const externalDockPreview = useMemo(
|
|
1007
|
+
() => resolveExternalDockPreview(desktopDockPreview, bounds),
|
|
1008
|
+
[bounds, desktopDockPreview],
|
|
1009
|
+
);
|
|
603
1010
|
const activePaneDrag = dragRef.current?.type === "pane-drag" ? dragRef.current : null;
|
|
604
1011
|
const activeHoverOverlay = activePaneDrag && dragCursor
|
|
605
1012
|
? resolveHoverOverlay(dragCursor.x, dragCursor.y, dockLeafLayouts, activePaneDrag.paneId)
|
|
606
1013
|
: null;
|
|
1014
|
+
const effectiveDockPreview = dockPreview ?? externalDockPreview;
|
|
607
1015
|
const nativeTransientOccluders = useMemo<NativeTransientOccluder[]>(() => {
|
|
608
1016
|
const occluders: NativeTransientOccluder[] = [];
|
|
609
1017
|
|
|
@@ -620,7 +1028,7 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
620
1028
|
if (activePaneDrag
|
|
621
1029
|
&& activePaneDrag.mode === "docked"
|
|
622
1030
|
&& dragFloatingRect?.paneId === activePaneDrag.paneId
|
|
623
|
-
&& !
|
|
1031
|
+
&& !effectiveDockPreview) {
|
|
624
1032
|
occluders.push({
|
|
625
1033
|
id: `drag-preview:${activePaneDrag.paneId}`,
|
|
626
1034
|
rect: dragFloatingRect.rect,
|
|
@@ -628,16 +1036,16 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
628
1036
|
});
|
|
629
1037
|
}
|
|
630
1038
|
|
|
631
|
-
if (
|
|
1039
|
+
if (effectiveDockPreview) {
|
|
632
1040
|
occluders.push({
|
|
633
|
-
id: `dock-preview:${
|
|
634
|
-
rect:
|
|
1041
|
+
id: `dock-preview:${effectiveDockPreview.kind}`,
|
|
1042
|
+
rect: effectiveDockPreview.rect,
|
|
635
1043
|
zIndex: 96,
|
|
636
1044
|
});
|
|
637
1045
|
}
|
|
638
1046
|
|
|
639
1047
|
return occluders;
|
|
640
|
-
}, [activeHoverOverlay, activePaneDrag,
|
|
1048
|
+
}, [activeHoverOverlay, activePaneDrag, dragFloatingRect, effectiveDockPreview]);
|
|
641
1049
|
const nativeDockDividers = useMemo(
|
|
642
1050
|
() => resolveNativeDockDividers(dockDividerLayouts, dividerPreview),
|
|
643
1051
|
[dividerPreview, dockDividerLayouts],
|
|
@@ -645,97 +1053,252 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
645
1053
|
const nativeWindowState = useMemo(
|
|
646
1054
|
() => buildNativeWindowState(
|
|
647
1055
|
dockedPanes.map((pane) => pane.instance.instanceId),
|
|
648
|
-
|
|
1056
|
+
visibleFloatingPanes.map(({ pane, rect }) => ({
|
|
649
1057
|
paneId: pane.instance.instanceId,
|
|
650
|
-
rect
|
|
1058
|
+
rect,
|
|
651
1059
|
zIndex: pane.floating?.zIndex ?? 50,
|
|
652
1060
|
})),
|
|
653
1061
|
dragFloatingRect,
|
|
654
1062
|
{ open: overlayOpen, width, contentHeight },
|
|
655
1063
|
nativeTransientOccluders,
|
|
656
1064
|
nativeDockDividers,
|
|
1065
|
+
appHeaderHeight,
|
|
657
1066
|
),
|
|
658
|
-
[contentHeight, dockedPanes, dragFloatingRect,
|
|
1067
|
+
[appHeaderHeight, contentHeight, dockedPanes, dragFloatingRect, nativeDockDividers, nativeTransientOccluders, overlayOpen, visibleFloatingPanes, width],
|
|
659
1068
|
);
|
|
660
1069
|
|
|
661
1070
|
useEffect(() => {
|
|
662
1071
|
nativeSurfaceManager.setWindowState(nativeWindowState);
|
|
663
1072
|
}, [nativeSurfaceManager, nativeWindowState]);
|
|
664
1073
|
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
?? pane.def.name;
|
|
674
|
-
}
|
|
675
|
-
if (pane.instance.title) return pane.instance.title;
|
|
676
|
-
if (pane.instance.paneId === "portfolio-list") {
|
|
677
|
-
const collectionId = resolveCollectionForPane(state, pane.instance.instanceId);
|
|
678
|
-
return state.config.portfolios.find((portfolio) => portfolio.id === collectionId)?.name
|
|
679
|
-
?? state.config.watchlists.find((watchlist) => watchlist.id === collectionId)?.name
|
|
680
|
-
?? pane.def.name;
|
|
681
|
-
}
|
|
682
|
-
const ticker = resolveTickerForPane(state, pane.instance.instanceId);
|
|
683
|
-
return ticker ? `${pane.def.name}: ${ticker}` : pane.def.name;
|
|
684
|
-
}, [state]);
|
|
1074
|
+
const titleState = useMemo(
|
|
1075
|
+
() => ({ config, paneState }) as Parameters<typeof resolveTickerForPane>[0],
|
|
1076
|
+
[config, paneState],
|
|
1077
|
+
);
|
|
1078
|
+
const getPaneTitle = useCallback(
|
|
1079
|
+
(pane: ResolvedPane): string => getPaneDisplayTitle(titleState, pane.instance, pane.def),
|
|
1080
|
+
[titleState],
|
|
1081
|
+
);
|
|
685
1082
|
|
|
686
|
-
const openPaneMenu = useCallback((paneId: string, rect: LayoutBounds) => {
|
|
1083
|
+
const openPaneMenu = useCallback((paneId: string, rect: LayoutBounds, event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
687
1084
|
const pane = paneMap.get(paneId);
|
|
688
1085
|
if (!pane) return;
|
|
689
1086
|
focusPane(paneId);
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
setMenuState({
|
|
1087
|
+
const context = {
|
|
1088
|
+
kind: "pane" as const,
|
|
693
1089
|
paneId,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1090
|
+
paneType: pane.instance.paneId,
|
|
1091
|
+
title: getPaneTitle(pane),
|
|
1092
|
+
floating: !!pane.floating,
|
|
1093
|
+
};
|
|
1094
|
+
const items = menuForPane(
|
|
1095
|
+
pane,
|
|
1096
|
+
visibleLayout,
|
|
1097
|
+
width,
|
|
1098
|
+
contentHeight,
|
|
1099
|
+
pluginRegistry,
|
|
1100
|
+
persistLayout,
|
|
1101
|
+
focusPane,
|
|
1102
|
+
openPaneSettings,
|
|
1103
|
+
desktopWindowBridge,
|
|
1104
|
+
nativePaneChrome && rendererHost.copyPngImage ? copyPaneScreenshot : undefined,
|
|
1105
|
+
);
|
|
1106
|
+
void showContextMenu(context, items, event).then((shown) => {
|
|
1107
|
+
if (shown) return;
|
|
1108
|
+
const pluginItems = pluginRegistry.getContextMenuItems?.(context) ?? [];
|
|
1109
|
+
const fallbackSourceItems = compactContextMenuItems([
|
|
1110
|
+
...items,
|
|
1111
|
+
...(items.length > 0 && pluginItems.length > 0 ? [contextMenuDivider(`${context.kind}:plugin-divider`)] : []),
|
|
1112
|
+
...pluginItems,
|
|
1113
|
+
]);
|
|
1114
|
+
const fallbackItems = menuItemsForFallback(fallbackSourceItems, shortcutDisplayMode);
|
|
1115
|
+
if (fallbackItems.length === 0) return;
|
|
1116
|
+
const menuWidth = actionMenuWidth(fallbackItems, width);
|
|
1117
|
+
const menuX = Math.max(0, Math.min(width - menuWidth, rect.x + Math.max(0, rect.width - menuWidth)));
|
|
1118
|
+
const menuY = Math.max(0, Math.min(contentHeight - 1, rect.y + 1));
|
|
1119
|
+
setHoveredMenuItemId(fallbackItems[0]?.id ?? null);
|
|
1120
|
+
setMenuState({
|
|
1121
|
+
paneId,
|
|
1122
|
+
x: menuX,
|
|
1123
|
+
y: menuY,
|
|
1124
|
+
width: menuWidth,
|
|
1125
|
+
items: fallbackItems,
|
|
1126
|
+
});
|
|
706
1127
|
});
|
|
707
|
-
}, [contentHeight, focusPane, openPaneSettings, paneMap, persistLayout, pluginRegistry, visibleLayout, width]);
|
|
1128
|
+
}, [contentHeight, copyPaneScreenshot, desktopWindowBridge, focusPane, getPaneTitle, nativePaneChrome, openPaneSettings, paneMap, persistLayout, pluginRegistry, rendererHost.copyPngImage, shortcutDisplayMode, showContextMenu, visibleLayout, width]);
|
|
708
1129
|
|
|
709
1130
|
const handleFloatingClose = useCallback((paneId: string) => {
|
|
710
1131
|
persistLayout(removePane(visibleLayout, paneId));
|
|
711
1132
|
}, [persistLayout, visibleLayout]);
|
|
712
1133
|
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
x
|
|
716
|
-
y
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
const
|
|
1134
|
+
const handleActiveDrag = useCallback((event: ShellMouseEvent) => {
|
|
1135
|
+
const shellY = event.y - appHeaderHeight;
|
|
1136
|
+
const preciseX = event.preciseX ?? event.x;
|
|
1137
|
+
const preciseShellY = (event.preciseY ?? event.y) - appHeaderHeight;
|
|
1138
|
+
const hitX = precisePointer ? preciseX : event.x;
|
|
1139
|
+
const hitShellY = precisePointer ? preciseShellY : shellY;
|
|
1140
|
+
const dragThreshold = precisePointer ? PRECISE_PANE_DRAG_THRESHOLD : PANE_DRAG_THRESHOLD;
|
|
1141
|
+
const drag = dragRef.current;
|
|
1142
|
+
if (!drag) return;
|
|
1143
|
+
|
|
1144
|
+
if (event.type === "drag") {
|
|
1145
|
+
if (drag.type === "divider") {
|
|
1146
|
+
const total = drag.axis === "horizontal" ? drag.bounds.width : drag.bounds.height;
|
|
1147
|
+
const delta = drag.axis === "horizontal" ? preciseX - drag.startX : preciseShellY - drag.startY;
|
|
1148
|
+
const nextRatio = Math.max(0.1, Math.min(0.9, drag.startRatio + (delta / Math.max(1, total))));
|
|
1149
|
+
const offset = drag.axis === "horizontal"
|
|
1150
|
+
? drag.bounds.width * nextRatio
|
|
1151
|
+
: drag.bounds.height * nextRatio;
|
|
1152
|
+
const nextRect = drag.axis === "horizontal"
|
|
1153
|
+
? {
|
|
1154
|
+
x: nativePaneChrome ? drag.bounds.x + offset - 0.5 : drag.bounds.x + Math.round(offset) - 1,
|
|
1155
|
+
y: drag.bounds.y,
|
|
1156
|
+
width: 1,
|
|
1157
|
+
height: drag.bounds.height,
|
|
1158
|
+
}
|
|
1159
|
+
: {
|
|
1160
|
+
x: drag.bounds.x,
|
|
1161
|
+
y: nativePaneChrome ? drag.bounds.y + offset - 0.5 : drag.bounds.y + Math.round(offset) - 1,
|
|
1162
|
+
width: drag.bounds.width,
|
|
1163
|
+
height: 1,
|
|
1164
|
+
};
|
|
1165
|
+
updateDividerPreview({ pathKey: drag.path.join("."), rect: nextRect, ratio: nextRatio });
|
|
1166
|
+
} else if (drag.type === "pane-drag") {
|
|
1167
|
+
if (!isMeaningfulPaneDrag(drag.startX, drag.startY, preciseX, preciseShellY, dragThreshold)) {
|
|
1168
|
+
updateDockPreview(null);
|
|
1169
|
+
setDragCursor(null);
|
|
1170
|
+
if (drag.mode === "floating") {
|
|
1171
|
+
updateDragFloatingRect({ paneId: drag.paneId, rect: drag.origRect });
|
|
1172
|
+
}
|
|
1173
|
+
event.stopPropagation();
|
|
1174
|
+
event.preventDefault();
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const pane = paneMap.get(drag.paneId);
|
|
1179
|
+
const baseRect = drag.mode === "docked"
|
|
1180
|
+
? getRememberedFloatingRect(visibleLayout, drag.paneId, width, contentHeight, pane?.def)
|
|
1181
|
+
: drag.origRect;
|
|
1182
|
+
const nextRect = resolvePaneDragFloatingRect(drag, baseRect, preciseX, preciseShellY, width, contentHeight);
|
|
1183
|
+
updateDragFloatingRect({ paneId: drag.paneId, rect: nextRect });
|
|
1184
|
+
setDragCursor({ x: hitX, y: hitShellY });
|
|
1185
|
+
|
|
1186
|
+
const hoveredOverlay = resolveHoverOverlay(hitX, hitShellY, dockLeafLayouts, drag.paneId);
|
|
1187
|
+
if (hoveredOverlay) {
|
|
1188
|
+
const hoveredCell = hoveredOverlay.cells.find((cell) => pointInRect(cell.rect, hitX, hitShellY));
|
|
1189
|
+
if (hoveredCell) {
|
|
1190
|
+
const target: DropTarget = { kind: "leaf", targetId: hoveredOverlay.targetId, position: hoveredCell.position };
|
|
1191
|
+
const simulation = simulateDrop(visibleLayout, drag.paneId, target, bounds);
|
|
1192
|
+
if (simulation.previewRect) {
|
|
1193
|
+
updateDockPreview({ kind: "dock", target, rect: simulation.previewRect });
|
|
1194
|
+
} else {
|
|
1195
|
+
updateDockPreview(null);
|
|
1196
|
+
}
|
|
1197
|
+
} else {
|
|
1198
|
+
updateDockPreview(null);
|
|
1199
|
+
}
|
|
1200
|
+
} else {
|
|
1201
|
+
const snapGuide = resolveSnapGuide(hitX, hitShellY, snapGuides);
|
|
1202
|
+
updateDockPreview(snapGuide ? { kind: "snap", position: snapGuide.position, rect: snapGuide.previewRect } : null);
|
|
1203
|
+
}
|
|
1204
|
+
} else if (drag.type === "float-resize") {
|
|
1205
|
+
updateDragFloatingRect({
|
|
1206
|
+
paneId: drag.paneId,
|
|
1207
|
+
rect: resolveFloatResizeRect(drag, preciseX, preciseShellY, width, contentHeight),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
event.stopPropagation();
|
|
1211
|
+
event.preventDefault();
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (event.type === "up" || event.type === "drag-end") {
|
|
1216
|
+
if (drag.type === "divider") {
|
|
1217
|
+
const preview = dividerPreviewRef.current;
|
|
1218
|
+
if (preview) {
|
|
1219
|
+
persistLayout(resizeSplitAtPath(visibleLayout, drag.path, preview.ratio));
|
|
1220
|
+
}
|
|
1221
|
+
updateDividerPreview(null);
|
|
1222
|
+
} else if (drag.type === "pane-drag") {
|
|
1223
|
+
const movedEnough = isMeaningfulPaneDrag(drag.startX, drag.startY, preciseX, preciseShellY, dragThreshold);
|
|
1224
|
+
if (!movedEnough) {
|
|
1225
|
+
updateDockPreview(null);
|
|
1226
|
+
setDragCursor(null);
|
|
1227
|
+
updateDragFloatingRect(null);
|
|
1228
|
+
} else {
|
|
1229
|
+
const pane = paneMap.get(drag.paneId);
|
|
1230
|
+
const baseRect = drag.mode === "docked"
|
|
1231
|
+
? getRememberedFloatingRect(visibleLayout, drag.paneId, width, contentHeight, pane?.def)
|
|
1232
|
+
: drag.origRect;
|
|
1233
|
+
const releaseRect = resolvePaneDragFloatingRect(drag, baseRect, preciseX, preciseShellY, width, contentHeight);
|
|
1234
|
+
const releaseResult = finalizePaneDragRelease(visibleLayout, drag.paneId, releaseRect, dockPreviewRef.current);
|
|
1235
|
+
persistLayout(releaseResult.nextLayout);
|
|
1236
|
+
focusPane(drag.paneId);
|
|
1237
|
+
if (releaseResult.shouldShowGridlockTip) {
|
|
1238
|
+
dispatch({ type: "SHOW_GRIDLOCK_TIP" });
|
|
1239
|
+
}
|
|
1240
|
+
updateDockPreview(null);
|
|
1241
|
+
setDragCursor(null);
|
|
1242
|
+
updateDragFloatingRect(null);
|
|
1243
|
+
}
|
|
1244
|
+
} else if (drag.type === "float-resize") {
|
|
1245
|
+
const releaseRect = resolveFloatResizeRect(drag, preciseX, preciseShellY, width, contentHeight);
|
|
1246
|
+
persistLayout(floatAtRect(visibleLayout, drag.paneId, releaseRect));
|
|
1247
|
+
updateDragFloatingRect(null);
|
|
1248
|
+
setDragCursor(null);
|
|
1249
|
+
}
|
|
1250
|
+
dragRef.current = null;
|
|
1251
|
+
event.stopPropagation();
|
|
1252
|
+
event.preventDefault();
|
|
1253
|
+
}
|
|
1254
|
+
}, [
|
|
1255
|
+
bounds,
|
|
1256
|
+
appHeaderHeight,
|
|
1257
|
+
contentHeight,
|
|
1258
|
+
dockLeafLayouts,
|
|
1259
|
+
dispatch,
|
|
1260
|
+
focusPane,
|
|
1261
|
+
nativePaneChrome,
|
|
1262
|
+
paneMap,
|
|
1263
|
+
persistLayout,
|
|
1264
|
+
precisePointer,
|
|
1265
|
+
snapGuides,
|
|
1266
|
+
updateDividerPreview,
|
|
1267
|
+
updateDockPreview,
|
|
1268
|
+
updateDragFloatingRect,
|
|
1269
|
+
visibleLayout,
|
|
1270
|
+
width,
|
|
1271
|
+
]);
|
|
1272
|
+
|
|
1273
|
+
const handleMouse = useCallback((event: ShellMouseEvent) => {
|
|
1274
|
+
const shellY = event.y - appHeaderHeight;
|
|
1275
|
+
const preciseX = event.preciseX ?? event.x;
|
|
1276
|
+
const preciseShellY = (event.preciseY ?? event.y) - appHeaderHeight;
|
|
721
1277
|
if (shellY < 0) return;
|
|
722
1278
|
|
|
723
1279
|
if (event.type === "down") {
|
|
724
1280
|
if (menuState) {
|
|
725
1281
|
setMenuState(null);
|
|
1282
|
+
setHoveredMenuItemId(null);
|
|
726
1283
|
}
|
|
727
1284
|
|
|
728
|
-
for (const pane of [...
|
|
729
|
-
const rect = dragFloatingRect?.paneId === pane.instance.instanceId
|
|
1285
|
+
for (const { pane, rect: visibleRect } of [...visibleFloatingPanes].sort((a, b) => (b.pane.floating?.zIndex ?? 50) - (a.pane.floating?.zIndex ?? 50))) {
|
|
1286
|
+
const rect = dragFloatingRect?.paneId === pane.instance.instanceId
|
|
1287
|
+
? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
|
|
1288
|
+
: visibleRect;
|
|
730
1289
|
if (!pointInRect({ x: rect.x, y: rect.y, width: rect.width, height: rect.height }, event.x, shellY)) continue;
|
|
731
1290
|
const relativeX = event.x - rect.x;
|
|
732
1291
|
const relativeY = shellY - rect.y;
|
|
733
|
-
const isFocused =
|
|
1292
|
+
const isFocused = focusedPaneId === pane.instance.instanceId;
|
|
734
1293
|
const headerAreas = resolveHeaderHitAreas(rect.width, {
|
|
735
1294
|
floating: true,
|
|
736
1295
|
focused: isFocused,
|
|
737
1296
|
});
|
|
738
1297
|
focusPane(pane.instance.instanceId);
|
|
1298
|
+
if (event.button === 2 && relativeY === 0) {
|
|
1299
|
+
openPaneMenu(pane.instance.instanceId, rect, event);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
739
1302
|
if (relativeY === 0 && headerAreas.closeStart != null && relativeX >= headerAreas.closeStart && relativeX < rect.width) {
|
|
740
1303
|
handleFloatingClose(pane.instance.instanceId);
|
|
741
1304
|
event.stopPropagation();
|
|
@@ -747,7 +1310,7 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
747
1310
|
&& headerAreas.closeStart != null
|
|
748
1311
|
&& relativeX >= headerAreas.actionStart
|
|
749
1312
|
&& relativeX < headerAreas.closeStart) {
|
|
750
|
-
openPaneMenu(pane.instance.instanceId, rect);
|
|
1313
|
+
openPaneMenu(pane.instance.instanceId, rect, event);
|
|
751
1314
|
event.stopPropagation();
|
|
752
1315
|
event.preventDefault();
|
|
753
1316
|
return;
|
|
@@ -756,11 +1319,11 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
756
1319
|
dragRef.current = {
|
|
757
1320
|
type: "float-resize",
|
|
758
1321
|
paneId: pane.instance.instanceId,
|
|
759
|
-
startX:
|
|
760
|
-
startY:
|
|
1322
|
+
startX: preciseX,
|
|
1323
|
+
startY: preciseShellY,
|
|
761
1324
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
762
1325
|
};
|
|
763
|
-
|
|
1326
|
+
updateDragFloatingRect({ paneId: pane.instance.instanceId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
764
1327
|
event.stopPropagation();
|
|
765
1328
|
event.preventDefault();
|
|
766
1329
|
return;
|
|
@@ -770,11 +1333,11 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
770
1333
|
type: "pane-drag",
|
|
771
1334
|
paneId: pane.instance.instanceId,
|
|
772
1335
|
mode: "floating",
|
|
773
|
-
startX:
|
|
774
|
-
startY:
|
|
1336
|
+
startX: preciseX,
|
|
1337
|
+
startY: preciseShellY,
|
|
775
1338
|
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
776
1339
|
};
|
|
777
|
-
|
|
1340
|
+
updateDragFloatingRect({ paneId: pane.instance.instanceId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
778
1341
|
event.stopPropagation();
|
|
779
1342
|
event.preventDefault();
|
|
780
1343
|
return;
|
|
@@ -790,12 +1353,12 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
790
1353
|
type: "divider",
|
|
791
1354
|
path: divider.path,
|
|
792
1355
|
axis: divider.axis,
|
|
793
|
-
startX:
|
|
794
|
-
startY:
|
|
1356
|
+
startX: preciseX,
|
|
1357
|
+
startY: preciseShellY,
|
|
795
1358
|
startRatio: divider.ratio,
|
|
796
1359
|
bounds: divider.bounds,
|
|
797
1360
|
};
|
|
798
|
-
|
|
1361
|
+
updateDividerPreview({
|
|
799
1362
|
pathKey: divider.path.join("."),
|
|
800
1363
|
rect: divider.rect,
|
|
801
1364
|
ratio: divider.ratio,
|
|
@@ -811,16 +1374,20 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
811
1374
|
if (!pane) continue;
|
|
812
1375
|
const relativeX = event.x - leaf.rect.x;
|
|
813
1376
|
const relativeY = shellY - leaf.rect.y;
|
|
814
|
-
const isFocused =
|
|
1377
|
+
const isFocused = focusedPaneId === leaf.instanceId;
|
|
815
1378
|
const headerAreas = resolveHeaderHitAreas(leaf.rect.width, {
|
|
816
1379
|
floating: false,
|
|
817
1380
|
focused: isFocused,
|
|
818
1381
|
});
|
|
819
1382
|
focusPane(leaf.instanceId);
|
|
1383
|
+
if (event.button === 2 && relativeY === 0) {
|
|
1384
|
+
openPaneMenu(leaf.instanceId, leaf.rect, event);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
820
1387
|
if (relativeY === 0
|
|
821
1388
|
&& headerAreas.actionStart != null
|
|
822
1389
|
&& relativeX >= headerAreas.actionStart) {
|
|
823
|
-
openPaneMenu(leaf.instanceId, leaf.rect);
|
|
1390
|
+
openPaneMenu(leaf.instanceId, leaf.rect, event);
|
|
824
1391
|
event.stopPropagation();
|
|
825
1392
|
event.preventDefault();
|
|
826
1393
|
return;
|
|
@@ -830,8 +1397,8 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
830
1397
|
type: "pane-drag",
|
|
831
1398
|
paneId: leaf.instanceId,
|
|
832
1399
|
mode: "docked",
|
|
833
|
-
startX:
|
|
834
|
-
startY:
|
|
1400
|
+
startX: preciseX,
|
|
1401
|
+
startY: preciseShellY,
|
|
835
1402
|
origRect: { x: leaf.rect.x, y: leaf.rect.y, width: leaf.rect.width, height: leaf.rect.height },
|
|
836
1403
|
};
|
|
837
1404
|
event.stopPropagation();
|
|
@@ -846,149 +1413,179 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
846
1413
|
return;
|
|
847
1414
|
}
|
|
848
1415
|
|
|
849
|
-
|
|
850
|
-
if (!drag) return;
|
|
851
|
-
|
|
852
|
-
if (event.type === "drag") {
|
|
853
|
-
if (drag.type === "divider") {
|
|
854
|
-
const total = drag.axis === "horizontal" ? drag.bounds.width : drag.bounds.height;
|
|
855
|
-
const delta = drag.axis === "horizontal" ? event.x - drag.startX : shellY - drag.startY;
|
|
856
|
-
const nextRatio = Math.max(0.1, Math.min(0.9, drag.startRatio + (delta / Math.max(1, total))));
|
|
857
|
-
const nextRect = drag.axis === "horizontal"
|
|
858
|
-
? { x: drag.bounds.x + Math.round(drag.bounds.width * nextRatio) - 1, y: drag.bounds.y, width: 1, height: drag.bounds.height }
|
|
859
|
-
: { x: drag.bounds.x, y: drag.bounds.y + Math.round(drag.bounds.height * nextRatio) - 1, width: drag.bounds.width, height: 1 };
|
|
860
|
-
setDividerPreview({ pathKey: drag.path.join("."), rect: nextRect, ratio: nextRatio });
|
|
861
|
-
} else if (drag.type === "pane-drag") {
|
|
862
|
-
if (!isMeaningfulPaneDrag(drag.startX, drag.startY, event.x, shellY)) {
|
|
863
|
-
setDockPreview(null);
|
|
864
|
-
setDragCursor(null);
|
|
865
|
-
if (drag.mode === "floating") {
|
|
866
|
-
setDragFloatingRect({ paneId: drag.paneId, rect: drag.origRect });
|
|
867
|
-
}
|
|
868
|
-
event.stopPropagation();
|
|
869
|
-
event.preventDefault();
|
|
870
|
-
return;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
const pane = paneMap.get(drag.paneId);
|
|
874
|
-
const nextRect = drag.mode === "docked"
|
|
875
|
-
? positionFloatingRectUnderPointer(
|
|
876
|
-
getRememberedFloatingRect(visibleLayout, drag.paneId, width, contentHeight, pane?.def),
|
|
877
|
-
drag,
|
|
878
|
-
event.x,
|
|
879
|
-
shellY,
|
|
880
|
-
width,
|
|
881
|
-
contentHeight,
|
|
882
|
-
)
|
|
883
|
-
: {
|
|
884
|
-
x: Math.max(0, Math.min(width - drag.origRect.width, drag.origRect.x + (event.x - drag.startX))),
|
|
885
|
-
y: Math.max(0, Math.min(contentHeight - drag.origRect.height, drag.origRect.y + (shellY - drag.startY))),
|
|
886
|
-
width: drag.origRect.width,
|
|
887
|
-
height: drag.origRect.height,
|
|
888
|
-
};
|
|
889
|
-
setDragFloatingRect({ paneId: drag.paneId, rect: nextRect });
|
|
890
|
-
setDragCursor({ x: event.x, y: shellY });
|
|
891
|
-
|
|
892
|
-
const hoveredOverlay = resolveHoverOverlay(event.x, shellY, dockLeafLayouts, drag.paneId);
|
|
893
|
-
if (hoveredOverlay) {
|
|
894
|
-
const hoveredCell = hoveredOverlay.cells.find((cell) => pointInRect(cell.rect, event.x, shellY));
|
|
895
|
-
if (hoveredCell) {
|
|
896
|
-
const target: DropTarget = { kind: "leaf", targetId: hoveredOverlay.targetId, position: hoveredCell.position };
|
|
897
|
-
const simulation = simulateDrop(visibleLayout, drag.paneId, target, bounds);
|
|
898
|
-
if (simulation.previewRect) {
|
|
899
|
-
setDockPreview({ kind: "dock", target, rect: simulation.previewRect });
|
|
900
|
-
} else {
|
|
901
|
-
setDockPreview(null);
|
|
902
|
-
}
|
|
903
|
-
} else {
|
|
904
|
-
setDockPreview(null);
|
|
905
|
-
}
|
|
906
|
-
} else {
|
|
907
|
-
const snapGuide = resolveSnapGuide(event.x, shellY, snapGuides);
|
|
908
|
-
setDockPreview(snapGuide ? { kind: "snap", position: snapGuide.position, rect: snapGuide.previewRect } : null);
|
|
909
|
-
}
|
|
910
|
-
} else if (drag.type === "float-resize") {
|
|
911
|
-
setDragFloatingRect({
|
|
912
|
-
paneId: drag.paneId,
|
|
913
|
-
rect: {
|
|
914
|
-
x: drag.origRect.x,
|
|
915
|
-
y: drag.origRect.y,
|
|
916
|
-
width: Math.max(MIN_FLOAT_WIDTH, Math.min(width - drag.origRect.x, drag.origRect.width + (event.x - drag.startX))),
|
|
917
|
-
height: Math.max(MIN_FLOAT_HEIGHT, Math.min(contentHeight - drag.origRect.y, drag.origRect.height + (shellY - drag.startY))),
|
|
918
|
-
},
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
event.stopPropagation();
|
|
922
|
-
event.preventDefault();
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
if (event.type === "up" || event.type === "drag-end") {
|
|
927
|
-
if (drag.type === "divider") {
|
|
928
|
-
if (dividerPreview) {
|
|
929
|
-
persistLayout(resizeSplitAtPath(visibleLayout, drag.path, dividerPreview.ratio));
|
|
930
|
-
}
|
|
931
|
-
setDividerPreview(null);
|
|
932
|
-
} else if (drag.type === "pane-drag") {
|
|
933
|
-
const movedEnough = isMeaningfulPaneDrag(drag.startX, drag.startY, event.x, shellY);
|
|
934
|
-
const previewRect = dragFloatingRect?.paneId === drag.paneId ? dragFloatingRect.rect : drag.origRect;
|
|
935
|
-
if (!movedEnough) {
|
|
936
|
-
setDockPreview(null);
|
|
937
|
-
setDragCursor(null);
|
|
938
|
-
setDragFloatingRect(null);
|
|
939
|
-
} else {
|
|
940
|
-
const releaseResult = finalizePaneDragRelease(visibleLayout, drag.paneId, previewRect, dockPreview);
|
|
941
|
-
persistLayout(releaseResult.nextLayout);
|
|
942
|
-
focusPane(drag.paneId);
|
|
943
|
-
if (releaseResult.shouldShowGridlockTip) {
|
|
944
|
-
dispatch({ type: "SHOW_GRIDLOCK_TIP" });
|
|
945
|
-
}
|
|
946
|
-
setDockPreview(null);
|
|
947
|
-
setDragCursor(null);
|
|
948
|
-
setDragFloatingRect(null);
|
|
949
|
-
}
|
|
950
|
-
} else if (drag.type === "float-resize") {
|
|
951
|
-
if (dragFloatingRect?.paneId === drag.paneId) {
|
|
952
|
-
persistLayout(floatAtRect(visibleLayout, drag.paneId, dragFloatingRect.rect));
|
|
953
|
-
}
|
|
954
|
-
setDragFloatingRect(null);
|
|
955
|
-
setDragCursor(null);
|
|
956
|
-
}
|
|
957
|
-
dragRef.current = null;
|
|
958
|
-
event.stopPropagation();
|
|
959
|
-
event.preventDefault();
|
|
960
|
-
}
|
|
1416
|
+
handleActiveDrag(event);
|
|
961
1417
|
}, [
|
|
962
1418
|
bounds,
|
|
1419
|
+
appHeaderHeight,
|
|
963
1420
|
closeFocusedPane,
|
|
964
1421
|
contentHeight,
|
|
965
1422
|
dividerPreview,
|
|
966
1423
|
dockDividerLayouts,
|
|
967
1424
|
dockLeafLayouts,
|
|
968
|
-
|
|
1425
|
+
effectiveDockPreview,
|
|
969
1426
|
dragFloatingRect,
|
|
970
|
-
|
|
1427
|
+
focusedPaneId,
|
|
971
1428
|
focusPane,
|
|
1429
|
+
handleActiveDrag,
|
|
972
1430
|
handleFloatingClose,
|
|
973
1431
|
menuState,
|
|
1432
|
+
nativePaneChrome,
|
|
974
1433
|
openPaneMenu,
|
|
975
1434
|
paneMap,
|
|
976
1435
|
persistLayout,
|
|
1436
|
+
precisePointer,
|
|
977
1437
|
snapGuides,
|
|
1438
|
+
updateDividerPreview,
|
|
1439
|
+
updateDockPreview,
|
|
1440
|
+
updateDragFloatingRect,
|
|
1441
|
+
visibleFloatingPanes,
|
|
978
1442
|
visibleLayout,
|
|
979
1443
|
width,
|
|
980
1444
|
]);
|
|
981
1445
|
|
|
1446
|
+
const getShellPointer = useCallback((event: ShellMouseEvent) => ({
|
|
1447
|
+
x: event.preciseX ?? event.x,
|
|
1448
|
+
y: (event.preciseY ?? event.y) - appHeaderHeight,
|
|
1449
|
+
}), [appHeaderHeight]);
|
|
1450
|
+
|
|
1451
|
+
const focusNativePane = useCallback((paneId: string) => {
|
|
1452
|
+
if (menuState) {
|
|
1453
|
+
setMenuState(null);
|
|
1454
|
+
setHoveredMenuItemId(null);
|
|
1455
|
+
}
|
|
1456
|
+
focusPane(paneId);
|
|
1457
|
+
}, [focusPane, menuState]);
|
|
1458
|
+
|
|
1459
|
+
const startNativeFloatingDrag = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
|
|
1460
|
+
if (!nativePaneChrome) return;
|
|
1461
|
+
if (event.button === 2) {
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const pointer = getShellPointer(event);
|
|
1465
|
+
focusNativePane(paneId);
|
|
1466
|
+
dragRef.current = {
|
|
1467
|
+
type: "pane-drag",
|
|
1468
|
+
paneId,
|
|
1469
|
+
mode: "floating",
|
|
1470
|
+
startX: pointer.x,
|
|
1471
|
+
startY: pointer.y,
|
|
1472
|
+
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
1473
|
+
};
|
|
1474
|
+
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
1475
|
+
event.preventDefault();
|
|
1476
|
+
}, [focusNativePane, getShellPointer, nativePaneChrome, updateDragFloatingRect]);
|
|
1477
|
+
|
|
1478
|
+
const startNativeDockedDrag = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
|
|
1479
|
+
if (!nativePaneChrome) return;
|
|
1480
|
+
if (event.button === 2) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const pointer = getShellPointer(event);
|
|
1484
|
+
focusNativePane(paneId);
|
|
1485
|
+
dragRef.current = {
|
|
1486
|
+
type: "pane-drag",
|
|
1487
|
+
paneId,
|
|
1488
|
+
mode: "docked",
|
|
1489
|
+
startX: pointer.x,
|
|
1490
|
+
startY: pointer.y,
|
|
1491
|
+
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
1492
|
+
};
|
|
1493
|
+
event.preventDefault();
|
|
1494
|
+
}, [focusNativePane, getShellPointer, nativePaneChrome]);
|
|
1495
|
+
|
|
1496
|
+
const startNativeFloatResize = useCallback((paneId: string, rect: FloatingRect, event: ShellMouseEvent) => {
|
|
1497
|
+
if (!nativePaneChrome) return;
|
|
1498
|
+
const pointer = getShellPointer(event);
|
|
1499
|
+
focusNativePane(paneId);
|
|
1500
|
+
dragRef.current = {
|
|
1501
|
+
type: "float-resize",
|
|
1502
|
+
paneId,
|
|
1503
|
+
startX: pointer.x,
|
|
1504
|
+
startY: pointer.y,
|
|
1505
|
+
origRect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
1506
|
+
};
|
|
1507
|
+
updateDragFloatingRect({ paneId, rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } });
|
|
1508
|
+
event.preventDefault();
|
|
1509
|
+
}, [focusNativePane, getShellPointer, nativePaneChrome, updateDragFloatingRect]);
|
|
1510
|
+
|
|
1511
|
+
const startNativeDividerDrag = useCallback((divider: DockDividerLayout, event: ShellMouseEvent) => {
|
|
1512
|
+
if (!nativePaneChrome) return;
|
|
1513
|
+
const pointer = getShellPointer(event);
|
|
1514
|
+
if (menuState) {
|
|
1515
|
+
setMenuState(null);
|
|
1516
|
+
setHoveredMenuItemId(null);
|
|
1517
|
+
}
|
|
1518
|
+
dragRef.current = {
|
|
1519
|
+
type: "divider",
|
|
1520
|
+
path: divider.path,
|
|
1521
|
+
axis: divider.axis,
|
|
1522
|
+
startX: pointer.x,
|
|
1523
|
+
startY: pointer.y,
|
|
1524
|
+
startRatio: divider.ratio,
|
|
1525
|
+
bounds: divider.bounds,
|
|
1526
|
+
};
|
|
1527
|
+
updateDividerPreview({
|
|
1528
|
+
pathKey: divider.path.join("."),
|
|
1529
|
+
rect: divider.rect,
|
|
1530
|
+
ratio: divider.ratio,
|
|
1531
|
+
});
|
|
1532
|
+
event.preventDefault();
|
|
1533
|
+
}, [getShellPointer, menuState, nativePaneChrome, updateDividerPreview]);
|
|
1534
|
+
|
|
1535
|
+
const handleNativeDrag = useCallback((event: ShellMouseEvent) => {
|
|
1536
|
+
handleActiveDrag(event);
|
|
1537
|
+
}, [handleActiveDrag]);
|
|
1538
|
+
|
|
1539
|
+
const handlePaneAction = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
|
|
1540
|
+
if (event.button === 2) return;
|
|
1541
|
+
event.stopPropagation();
|
|
1542
|
+
event.preventDefault();
|
|
1543
|
+
openPaneMenu(paneId, rect, event);
|
|
1544
|
+
}, [openPaneMenu]);
|
|
1545
|
+
|
|
1546
|
+
const handleNativePaneContextMenu = useCallback((paneId: string, rect: LayoutBounds, event: ShellMouseEvent) => {
|
|
1547
|
+
openPaneMenu(paneId, rect, event);
|
|
1548
|
+
}, [openPaneMenu]);
|
|
1549
|
+
|
|
1550
|
+
const handleFloatingCloseMouseDown = useCallback((paneId: string, event: ShellMouseEvent) => {
|
|
1551
|
+
event.stopPropagation();
|
|
1552
|
+
event.preventDefault();
|
|
1553
|
+
handleFloatingClose(paneId);
|
|
1554
|
+
}, [handleFloatingClose]);
|
|
1555
|
+
|
|
982
1556
|
return (
|
|
983
|
-
<
|
|
1557
|
+
<Box
|
|
1558
|
+
flexDirection="row"
|
|
1559
|
+
flexGrow={1}
|
|
1560
|
+
height={nativePaneChrome ? undefined : contentHeight}
|
|
1561
|
+
position={nativePaneChrome ? "relative" : undefined}
|
|
1562
|
+
overflow="hidden"
|
|
1563
|
+
{...(!nativePaneChrome ? { onMouse: handleMouse } : {})}
|
|
1564
|
+
>
|
|
1565
|
+
<Box
|
|
1566
|
+
position="absolute"
|
|
1567
|
+
left={0}
|
|
1568
|
+
top={0}
|
|
1569
|
+
width={width}
|
|
1570
|
+
height={contentHeight}
|
|
1571
|
+
alignItems="center"
|
|
1572
|
+
justifyContent="center"
|
|
1573
|
+
>
|
|
1574
|
+
<Box flexDirection="column" alignItems="center">
|
|
1575
|
+
<AsciiText text="Gloomberb" font="wordmark" color={colors.textMuted} />
|
|
1576
|
+
<Box height={1} />
|
|
1577
|
+
<Text fg={colors.textDim}>Ctrl+P to get started.</Text>
|
|
1578
|
+
</Box>
|
|
1579
|
+
</Box>
|
|
1580
|
+
|
|
984
1581
|
{dockLeafLayouts.map((leaf) => {
|
|
985
1582
|
const pane = paneMap.get(leaf.instanceId);
|
|
986
1583
|
if (!pane) return null;
|
|
987
|
-
const focused =
|
|
1584
|
+
const focused = focusedPaneId === leaf.instanceId && (!overlayOpen || menuState?.paneId === leaf.instanceId);
|
|
988
1585
|
const showActions = focused || hoveredPaneId === leaf.instanceId || menuState?.paneId === leaf.instanceId;
|
|
989
|
-
const
|
|
1586
|
+
const bodyWidth = nativePaneChrome ? Math.max(1, Math.floor(leaf.rect.width)) : getPaneBodyWidth(leaf.rect.width);
|
|
990
1587
|
return (
|
|
991
|
-
<
|
|
1588
|
+
<Box
|
|
992
1589
|
key={`dock:${leaf.instanceId}`}
|
|
993
1590
|
position="absolute"
|
|
994
1591
|
left={leaf.rect.x}
|
|
@@ -996,71 +1593,100 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
996
1593
|
width={leaf.rect.width}
|
|
997
1594
|
height={leaf.rect.height}
|
|
998
1595
|
>
|
|
999
|
-
<
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1596
|
+
<PaneFooterProvider>
|
|
1597
|
+
{(footer) => {
|
|
1598
|
+
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
|
|
1599
|
+
const bodyHeight = getPaneBodyHeight(leaf.rect.height, reserveFooter);
|
|
1600
|
+
return (
|
|
1601
|
+
<PaneWrapper
|
|
1602
|
+
paneId={leaf.instanceId}
|
|
1603
|
+
title={getPaneTitle(pane)}
|
|
1604
|
+
focused={focused}
|
|
1605
|
+
width={leaf.rect.width}
|
|
1606
|
+
height={leaf.rect.height}
|
|
1607
|
+
showActions={showActions}
|
|
1608
|
+
footer={footer}
|
|
1609
|
+
onMouseDown={nativePaneChrome ? () => focusNativePane(leaf.instanceId) : undefined}
|
|
1610
|
+
onMouseMove={() => setHoveredPaneIfChanged(leaf.instanceId)}
|
|
1611
|
+
onHeaderMouseDown={nativePaneChrome ? (event) => startNativeDockedDrag(leaf.instanceId, leaf.rect, event) : undefined}
|
|
1612
|
+
onHeaderMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1613
|
+
onHeaderMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1614
|
+
onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(leaf.instanceId, leaf.rect, event) : undefined}
|
|
1615
|
+
onActionMouseDown={(event) => handlePaneAction(leaf.instanceId, leaf.rect, event)}
|
|
1616
|
+
>
|
|
1617
|
+
<PaneContent
|
|
1618
|
+
component={pane.def.component}
|
|
1619
|
+
paneId={pane.instance.instanceId}
|
|
1620
|
+
paneType={pane.instance.paneId}
|
|
1621
|
+
focused={focused}
|
|
1622
|
+
width={bodyWidth}
|
|
1623
|
+
height={bodyHeight}
|
|
1624
|
+
/>
|
|
1625
|
+
</PaneWrapper>
|
|
1626
|
+
);
|
|
1627
|
+
}}
|
|
1628
|
+
</PaneFooterProvider>
|
|
1629
|
+
</Box>
|
|
1018
1630
|
);
|
|
1019
1631
|
})}
|
|
1020
1632
|
|
|
1021
|
-
{
|
|
1022
|
-
const preview = dragFloatingRect?.paneId === pane.instance.instanceId
|
|
1023
|
-
|
|
1633
|
+
{visibleFloatingPanes.map(({ pane, rect }) => {
|
|
1634
|
+
const preview = dragFloatingRect?.paneId === pane.instance.instanceId
|
|
1635
|
+
? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
|
|
1636
|
+
: rect;
|
|
1637
|
+
const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuState?.paneId === pane.instance.instanceId);
|
|
1024
1638
|
const showActions = focused || hoveredPaneId === pane.instance.instanceId || menuState?.paneId === pane.instance.instanceId;
|
|
1025
|
-
const
|
|
1639
|
+
const bodyWidth = nativePaneChrome ? Math.max(1, Math.floor(preview.width)) : getPaneBodyWidth(preview.width);
|
|
1026
1640
|
return (
|
|
1027
|
-
<
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1641
|
+
<PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
|
|
1642
|
+
{(footer) => {
|
|
1643
|
+
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
|
|
1644
|
+
const bodyHeight = getPaneBodyHeight(preview.height, reserveFooter);
|
|
1645
|
+
return (
|
|
1646
|
+
<FloatingPaneWrapper
|
|
1647
|
+
paneId={pane.instance.instanceId}
|
|
1648
|
+
title={getPaneTitle(pane)}
|
|
1649
|
+
x={preview.x}
|
|
1650
|
+
y={preview.y}
|
|
1651
|
+
width={preview.width}
|
|
1652
|
+
height={preview.height}
|
|
1653
|
+
zIndex={pane.floating?.zIndex ?? 50}
|
|
1654
|
+
focused={focused}
|
|
1655
|
+
showActions={showActions}
|
|
1656
|
+
footer={footer}
|
|
1657
|
+
onMouseDown={nativePaneChrome ? () => focusNativePane(pane.instance.instanceId) : undefined}
|
|
1658
|
+
onMouseMove={() => setHoveredPaneIfChanged(pane.instance.instanceId)}
|
|
1659
|
+
onHeaderMouseDown={nativePaneChrome ? (event) => startNativeFloatingDrag(pane.instance.instanceId, preview, event) : undefined}
|
|
1660
|
+
onHeaderMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1661
|
+
onHeaderMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1662
|
+
onHeaderContextMenu={nativePaneChrome && nativeContextMenu === true ? (event) => handleNativePaneContextMenu(pane.instance.instanceId, preview, event) : undefined}
|
|
1663
|
+
onActionMouseDown={(event) => handlePaneAction(pane.instance.instanceId, preview, event)}
|
|
1664
|
+
onCloseMouseDown={(event) => handleFloatingCloseMouseDown(pane.instance.instanceId, event)}
|
|
1665
|
+
onResizeMouseDown={nativePaneChrome ? (event) => startNativeFloatResize(pane.instance.instanceId, preview, event) : undefined}
|
|
1666
|
+
onResizeMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1667
|
+
onResizeMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1668
|
+
>
|
|
1669
|
+
<PaneContent
|
|
1670
|
+
component={pane.def.component}
|
|
1671
|
+
paneId={pane.instance.instanceId}
|
|
1672
|
+
paneType={pane.instance.paneId}
|
|
1673
|
+
focused={focused}
|
|
1674
|
+
width={bodyWidth}
|
|
1675
|
+
height={bodyHeight}
|
|
1676
|
+
onClose={handleFloatingClose}
|
|
1677
|
+
/>
|
|
1678
|
+
</FloatingPaneWrapper>
|
|
1679
|
+
);
|
|
1680
|
+
}}
|
|
1681
|
+
</PaneFooterProvider>
|
|
1050
1682
|
);
|
|
1051
1683
|
})}
|
|
1052
1684
|
|
|
1053
|
-
{dockLeafLayouts.length === 0 && floatingPanes.length === 0 && (
|
|
1054
|
-
<box flexGrow={1} alignItems="center" justifyContent="center">
|
|
1055
|
-
<text fg={colors.textDim}>No panes configured. Press Ctrl+P to get started.</text>
|
|
1056
|
-
</box>
|
|
1057
|
-
)}
|
|
1058
|
-
|
|
1059
1685
|
{dockDividerLayouts.map((divider) => {
|
|
1060
1686
|
const active = dividerPreview?.pathKey === divider.path.join(".");
|
|
1061
1687
|
const rect = active ? dividerPreview.rect : divider.rect;
|
|
1062
1688
|
return (
|
|
1063
|
-
<
|
|
1689
|
+
<Box
|
|
1064
1690
|
key={`divider:${divider.path.join(".")}`}
|
|
1065
1691
|
position="absolute"
|
|
1066
1692
|
left={rect.x}
|
|
@@ -1069,57 +1695,59 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
1069
1695
|
height={rect.height}
|
|
1070
1696
|
zIndex={active ? 2 : 1}
|
|
1071
1697
|
backgroundColor={active ? colors.borderFocused : colors.border}
|
|
1698
|
+
{...(nativePaneChrome ? {
|
|
1699
|
+
"data-gloom-role": "dock-divider",
|
|
1700
|
+
"data-axis": divider.axis,
|
|
1701
|
+
"data-active": active ? "true" : "false",
|
|
1702
|
+
style: { "--divider-color": active ? colors.borderFocused : colors.border } as any,
|
|
1703
|
+
} : {})}
|
|
1704
|
+
onMouseDown={nativePaneChrome ? (event) => startNativeDividerDrag(divider, event) : undefined}
|
|
1705
|
+
onMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1706
|
+
onMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1072
1707
|
/>
|
|
1073
1708
|
);
|
|
1074
1709
|
})}
|
|
1075
1710
|
|
|
1076
1711
|
{/* Focus border overlay — rendered on top of the focused pane */}
|
|
1077
1712
|
{(() => {
|
|
1078
|
-
if (!
|
|
1713
|
+
if (!focusedPaneId) return null;
|
|
1714
|
+
if (nativePaneChrome) return null;
|
|
1079
1715
|
// Hide border when command bar or dialog is open, but keep it when just the pane menu is open
|
|
1080
1716
|
if (overlayOpen && !menuState) return null;
|
|
1081
1717
|
let rect: { x: number; y: number; width: number; height: number } | null = null;
|
|
1082
1718
|
let z = 3;
|
|
1083
1719
|
// Check floating panes first (they render on top of docked panes)
|
|
1084
|
-
const floatingPane =
|
|
1720
|
+
const floatingPane = visibleFloatingPanes.find((entry) => entry.pane.instance.instanceId === focusedPaneId);
|
|
1085
1721
|
if (floatingPane) {
|
|
1086
|
-
rect = dragFloatingRect?.paneId === floatingPane.instance.instanceId
|
|
1087
|
-
? dragFloatingRect.rect
|
|
1088
|
-
: floatingPane.
|
|
1089
|
-
z = (floatingPane.floating?.zIndex ?? 50) + 1;
|
|
1722
|
+
rect = dragFloatingRect?.paneId === floatingPane.pane.instance.instanceId
|
|
1723
|
+
? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
|
|
1724
|
+
: floatingPane.rect;
|
|
1725
|
+
z = (floatingPane.pane.floating?.zIndex ?? 50) + 1;
|
|
1090
1726
|
} else {
|
|
1091
1727
|
// Check docked panes
|
|
1092
|
-
const dockedLeaf = dockLeafLayouts.find((l) => l.instanceId ===
|
|
1728
|
+
const dockedLeaf = dockLeafLayouts.find((l) => l.instanceId === focusedPaneId);
|
|
1093
1729
|
if (dockedLeaf) {
|
|
1094
1730
|
rect = dockedLeaf.rect;
|
|
1095
1731
|
}
|
|
1096
1732
|
}
|
|
1097
1733
|
if (!rect || rect.height < 2) return null;
|
|
1098
1734
|
const bc = colors.borderFocused;
|
|
1099
|
-
const isFloating = !!floatingPane;
|
|
1100
1735
|
const bodyTop = rect.y + 1; // below header (header renders its own border edges)
|
|
1101
|
-
const bodyH = rect.height - 2; // rows between header and
|
|
1736
|
+
const bodyH = rect.height - 2; // rows between header and footer
|
|
1102
1737
|
return (
|
|
1103
1738
|
<>
|
|
1104
1739
|
{/* Left edge — body only */}
|
|
1105
1740
|
{bodyH > 0 && (
|
|
1106
|
-
<
|
|
1107
|
-
<
|
|
1108
|
-
</
|
|
1741
|
+
<Box key="focus-l" position="absolute" left={rect.x} top={bodyTop} width={1} height={bodyH} zIndex={z}>
|
|
1742
|
+
<Text fg={bc} selectable={false}>{"│".repeat(bodyH)}</Text>
|
|
1743
|
+
</Box>
|
|
1109
1744
|
)}
|
|
1110
1745
|
{/* Right edge — body only */}
|
|
1111
1746
|
{bodyH > 0 && (
|
|
1112
|
-
<
|
|
1113
|
-
<
|
|
1114
|
-
</
|
|
1747
|
+
<Box key="focus-r" position="absolute" left={rect.x + rect.width - 1} top={bodyTop} width={1} height={bodyH} zIndex={z}>
|
|
1748
|
+
<Text fg={bc} selectable={false}>{"│".repeat(bodyH)}</Text>
|
|
1749
|
+
</Box>
|
|
1115
1750
|
)}
|
|
1116
|
-
{/* Bottom edge — for floating, leave last 2 chars for resize handle (rendered by FloatingPaneWrapper) */}
|
|
1117
|
-
<box key="focus-b" position="absolute" left={rect.x} top={rect.y + rect.height - 1} width={isFloating ? Math.max(0, rect.width - 2) : rect.width} height={1} zIndex={z}>
|
|
1118
|
-
<text fg={bc} selectable={false}>{isFloating
|
|
1119
|
-
? `└${"─".repeat(Math.max(0, rect.width - 4))}─`
|
|
1120
|
-
: `└${"─".repeat(Math.max(0, rect.width - 2))}┘`
|
|
1121
|
-
}</text>
|
|
1122
|
-
</box>
|
|
1123
1751
|
</>
|
|
1124
1752
|
);
|
|
1125
1753
|
})()}
|
|
@@ -1130,7 +1758,7 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
1130
1758
|
&& dockPreview.target.targetId === activeHoverOverlay.targetId
|
|
1131
1759
|
&& dockPreview.target.position === cell.position;
|
|
1132
1760
|
return (
|
|
1133
|
-
<
|
|
1761
|
+
<Box
|
|
1134
1762
|
key={`cell:${activeHoverOverlay.targetId}:${cell.position}`}
|
|
1135
1763
|
position="absolute"
|
|
1136
1764
|
left={cell.rect.x}
|
|
@@ -1149,9 +1777,9 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
1149
1777
|
{activePaneDrag
|
|
1150
1778
|
&& activePaneDrag.mode === "docked"
|
|
1151
1779
|
&& dragFloatingRect?.paneId === activePaneDrag.paneId
|
|
1152
|
-
&& !
|
|
1780
|
+
&& !effectiveDockPreview
|
|
1153
1781
|
&& (
|
|
1154
|
-
<
|
|
1782
|
+
<Box
|
|
1155
1783
|
position="absolute"
|
|
1156
1784
|
left={dragFloatingRect.rect.x}
|
|
1157
1785
|
top={dragFloatingRect.rect.y}
|
|
@@ -1165,13 +1793,13 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
1165
1793
|
/>
|
|
1166
1794
|
)}
|
|
1167
1795
|
|
|
1168
|
-
{
|
|
1169
|
-
<
|
|
1796
|
+
{effectiveDockPreview && (
|
|
1797
|
+
<Box
|
|
1170
1798
|
position="absolute"
|
|
1171
|
-
left={
|
|
1172
|
-
top={
|
|
1173
|
-
width={
|
|
1174
|
-
height={
|
|
1799
|
+
left={effectiveDockPreview.rect.x}
|
|
1800
|
+
top={effectiveDockPreview.rect.y}
|
|
1801
|
+
width={effectiveDockPreview.rect.width}
|
|
1802
|
+
height={effectiveDockPreview.rect.height}
|
|
1175
1803
|
border
|
|
1176
1804
|
borderStyle="single"
|
|
1177
1805
|
borderColor={colors.borderFocused}
|
|
@@ -1181,36 +1809,52 @@ export function Shell({ pluginRegistry }: ShellProps) {
|
|
|
1181
1809
|
)}
|
|
1182
1810
|
|
|
1183
1811
|
{menuState && (
|
|
1184
|
-
<
|
|
1812
|
+
<Box
|
|
1185
1813
|
position="absolute"
|
|
1186
1814
|
left={menuState.x}
|
|
1187
1815
|
top={menuState.y}
|
|
1188
|
-
width={
|
|
1816
|
+
width={menuState.width}
|
|
1189
1817
|
height={menuState.items.length + 2}
|
|
1190
1818
|
backgroundColor={colors.panel}
|
|
1191
1819
|
border
|
|
1192
1820
|
borderStyle="single"
|
|
1193
1821
|
borderColor={colors.borderFocused}
|
|
1194
|
-
zIndex={
|
|
1822
|
+
zIndex={MENU_Z_INDEX}
|
|
1195
1823
|
flexDirection="column"
|
|
1196
1824
|
>
|
|
1197
|
-
{menuState.items.map((item) =>
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1825
|
+
{menuState.items.map((item) => {
|
|
1826
|
+
const hovered = hoveredMenuItemId === item.id;
|
|
1827
|
+
const innerWidth = Math.max(1, menuState.width - 2);
|
|
1828
|
+
const accelerator = item.accelerator ?? "";
|
|
1829
|
+
const acceleratorWidth = accelerator.length;
|
|
1830
|
+
const labelWidth = accelerator ? Math.max(1, innerWidth - acceleratorWidth - 1) : innerWidth;
|
|
1831
|
+
const label = truncateMenuText(item.label, labelWidth);
|
|
1832
|
+
const spacer = accelerator ? " ".repeat(Math.max(1, innerWidth - label.length - acceleratorWidth)) : "";
|
|
1833
|
+
const line = truncateMenuText(`${label}${spacer}${accelerator}`, innerWidth).padEnd(innerWidth, " ");
|
|
1834
|
+
return (
|
|
1835
|
+
<Box
|
|
1836
|
+
key={item.id}
|
|
1837
|
+
height={1}
|
|
1838
|
+
width={innerWidth}
|
|
1839
|
+
backgroundColor={hovered ? colors.selected : colors.panel}
|
|
1840
|
+
onMouseMove={() => setHoveredMenuItemId(item.id)}
|
|
1841
|
+
onMouseDown={(mouseEvent: any) => {
|
|
1842
|
+
mouseEvent.stopPropagation();
|
|
1843
|
+
mouseEvent.preventDefault();
|
|
1844
|
+
setMenuState(null);
|
|
1845
|
+
setHoveredMenuItemId(null);
|
|
1846
|
+
item.action();
|
|
1847
|
+
}}
|
|
1848
|
+
data-gloom-interactive="true"
|
|
1849
|
+
>
|
|
1850
|
+
<Text fg={hovered ? colors.selectedText : colors.text}>
|
|
1851
|
+
{line}
|
|
1852
|
+
</Text>
|
|
1853
|
+
</Box>
|
|
1854
|
+
);
|
|
1855
|
+
})}
|
|
1856
|
+
</Box>
|
|
1213
1857
|
)}
|
|
1214
|
-
</
|
|
1858
|
+
</Box>
|
|
1215
1859
|
);
|
|
1216
1860
|
}
|