gloomberb 0.11.2 → 0.12.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 +7 -4
- package/README.zh-CN.md +4 -2
- package/package.json +28 -1
- package/src/api-client/auth.ts +10 -0
- package/src/api-client/data.ts +108 -1
- package/src/api-client/index.test.ts +127 -0
- package/src/api-client/index.ts +134 -1
- package/src/api-client/normalizers.ts +39 -0
- package/src/api-client/paths.ts +85 -1
- package/src/api-client/types.ts +220 -0
- package/src/app/pane-runtime/index.tsx +15 -12
- package/src/app/pane-runtime/layout-placement.test.ts +27 -0
- package/src/app/pane-runtime/layout-placement.ts +13 -0
- package/src/app/pane-runtime/plugin-bindings.ts +62 -0
- package/src/app.tsx +60 -18
- package/src/architecture/browser-entry-graph.test.ts +96 -0
- package/src/brokers/profile-form.test.ts +25 -27
- package/src/brokers/test-broker.ts +68 -0
- package/src/capabilities/chart-series.ts +3 -2
- package/src/cli/commands/market.ts +2 -2
- package/src/cli/commands/plugins.ts +39 -11
- package/src/cli/desktop-pane-shot.ts +285 -16
- package/src/cli/index.ts +1 -1
- package/src/cli/pane-functions/capabilities.ts +61 -31
- package/src/cli/pane-functions/catalog.ts +16 -4
- package/src/cli/pane-functions/discovery.ts +1 -0
- package/src/cli/pane-functions/headless.test.ts +247 -0
- package/src/cli/pane-functions/headless.ts +423 -0
- package/src/cli/pane-functions/index.test.ts +9 -0
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +27 -2
- package/src/cli/pane-functions/report.ts +4 -0
- package/src/cli/pane-functions/resolver.ts +26 -8
- package/src/cli/pane-functions/screenshot.test.ts +76 -0
- package/src/cli/pane-functions/screenshot.ts +236 -20
- package/src/cli/registry.ts +0 -22
- package/src/cli/restore-plugins.ts +34 -0
- package/src/cli/result.ts +7 -10
- package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
- package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
- package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -20
- package/src/components/chart/composite/composite-chart.tsx +301 -204
- package/src/components/chart/composite/format.ts +8 -2
- package/src/components/chart/composite/interactions.test.ts +68 -102
- package/src/components/chart/composite/interactions.ts +214 -264
- package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
- package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
- package/src/components/chart/composite/scene.ts +57 -3
- package/src/components/chart/composite/text-renderer.ts +3 -2
- package/src/components/chart/composite/time-scale.ts +138 -57
- package/src/components/chart/composite/types.ts +25 -1
- package/src/components/chart/core/data.ts +9 -13
- package/src/components/chart/core/date-window.ts +7 -1
- package/src/components/chart/core/pointer.ts +3 -0
- package/src/components/chart/core/types.ts +0 -45
- package/src/components/chart/native/chart-rasterizer.ts +0 -1
- package/src/components/chart/native/kitty/index.test.ts +0 -188
- package/src/components/chart/native/raster/crosshair.ts +6 -5
- package/src/components/chart/native/raster/primitives.ts +0 -23
- package/src/components/chart/native/renderer-selection.ts +81 -31
- package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
- package/src/components/chart/static/chart-surface.tsx +150 -0
- package/src/components/chart/static/index.ts +8 -0
- package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
- package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
- package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
- package/src/components/chart/static/series.ts +49 -0
- package/src/components/command-bar/action-runtime.ts +1 -3
- package/src/components/command-bar/assist/model.test.ts +17 -2
- package/src/components/command-bar/assist/model.ts +40 -9
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/commands/direct/runtime.ts +1 -1
- package/src/components/command-bar/commands/registry.ts +0 -11
- package/src/components/command-bar/helpers.test.ts +2 -2
- package/src/components/command-bar/helpers.ts +2 -6
- package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
- package/src/components/command-bar/list/badge.test.ts +54 -0
- package/src/components/command-bar/list/badge.ts +62 -0
- package/src/components/command-bar/list/model.test.ts +103 -0
- package/src/components/command-bar/list/model.ts +61 -4
- package/src/components/command-bar/list/view.test.tsx +113 -0
- package/src/components/command-bar/list/view.tsx +147 -131
- package/src/components/command-bar/pane-templates/workflow.ts +1 -1
- package/src/components/command-bar/panel/body.tsx +51 -65
- package/src/components/command-bar/panel/index.tsx +48 -13
- package/src/components/command-bar/panel/layout.test.ts +139 -0
- package/src/components/command-bar/panel/layout.ts +65 -28
- package/src/components/command-bar/panel/native-surface.ts +24 -0
- package/src/components/command-bar/panel/prompt-binding.ts +54 -0
- package/src/components/command-bar/panel/runtime.ts +6 -3
- package/src/components/command-bar/panel/state.ts +41 -7
- package/src/components/command-bar/panel/types.ts +7 -1
- package/src/components/command-bar/routes/root/provider-search.ts +34 -15
- package/src/components/command-bar/routes/root/results.test.ts +161 -0
- package/src/components/command-bar/routes/root/results.ts +30 -28
- package/src/components/command-bar/routes/root/runtime.ts +37 -20
- package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
- package/src/components/command-bar/routes/root/search-providers.ts +205 -0
- package/src/components/command-bar/routes/root/selection.ts +1 -12
- package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
- package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
- package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
- package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
- package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
- package/src/components/command-bar/routing/actions.ts +1 -81
- package/src/components/command-bar/routing/launch-request.ts +1 -1
- package/src/components/command-bar/routing/list-state.ts +6 -21
- package/src/components/command-bar/selection-runtime.ts +1 -1
- package/src/components/command-bar/surface/assist.test.tsx +14 -10
- package/src/components/command-bar/surface/environment.ts +8 -1
- package/src/components/command-bar/surface/index.test.tsx +84 -68
- package/src/components/command-bar/surface/index.tsx +33 -36
- package/src/components/command-bar/surface/test-harness.tsx +15 -3
- package/src/components/command-bar/theme-picker.tsx +43 -15
- package/src/components/command-bar/view-model.test.ts +53 -8
- package/src/components/command-bar/view-model.ts +43 -14
- package/src/components/command-bar/workflow/tickers.ts +1 -0
- package/src/components/command-bar/workflow/types.ts +1 -1
- package/src/components/data-table/export.test.ts +21 -0
- package/src/components/data-table/export.ts +21 -0
- package/src/components/data-table/view.test.tsx +45 -6
- package/src/components/data-table/view.tsx +55 -9
- package/src/components/index.ts +10 -2
- package/src/components/input-search-bar.test.tsx +36 -2
- package/src/components/input-search-bar.tsx +18 -1
- package/src/components/layout/header.test.tsx +57 -10
- package/src/components/layout/header.tsx +292 -149
- package/src/components/layout/market-summary.test.ts +81 -0
- package/src/components/layout/market-summary.ts +109 -0
- package/src/components/layout/shell/chrome.ts +96 -1
- package/src/components/layout/shell/index.tsx +11 -3
- package/src/components/layout/shell/menu.ts +9 -0
- package/src/components/layout/shell/pane/actions.ts +29 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
- package/src/components/layout/shell/shortcuts.ts +3 -0
- package/src/components/layout/status-bar.test.tsx +9 -8
- package/src/components/layout/status-bar.tsx +85 -54
- package/src/components/onboarding/onboarding-frame.tsx +2 -26
- package/src/components/price-sparkline/index.ts +61 -34
- package/src/components/price-sparkline/view.tsx +1 -1
- package/src/components/sign-in-gate.test.tsx +128 -0
- package/src/components/sign-in-gate.tsx +149 -0
- package/src/components/speedometer-gauge/desktop.tsx +2 -12
- package/src/components/ticker/badge/list.tsx +17 -3
- package/src/components/ticker/list-table-view.tsx +6 -3
- package/src/components/ui/data-table/index.tsx +22 -4
- package/src/components/ui/data-table/opentui/index.tsx +165 -120
- package/src/components/ui/frame.tsx +28 -0
- package/src/components/ui/index.ts +3 -0
- package/src/components/ui/native-select.tsx +20 -10
- package/src/components/ui/select-button.tsx +138 -0
- package/src/components/ui/status.tsx +2 -2
- package/src/components/ui/table-layout.ts +1 -1
- package/src/components/use-double-click-activation.ts +7 -3
- package/src/core/app-services.ts +3 -3
- package/src/data/config/chart-settings.ts +4 -4
- package/src/data/config/layout.ts +12 -6
- package/src/data/config/store/index.test.ts +97 -0
- package/src/data/config/store/migrations.ts +41 -0
- package/src/data/config/store/normalize.ts +5 -3
- package/src/i18n/es.ts +6 -2
- package/src/i18n/ja.ts +6 -2
- package/src/i18n/ko.ts +6 -2
- package/src/i18n/zh-cn.ts +6 -2
- package/src/i18n/zh-tw.ts +6 -2
- package/src/layout-marketplace/gallery-terminal.tsx +126 -96
- package/src/layout-marketplace/gallery.test.tsx +65 -1
- package/src/layout-marketplace/pane-imagery.ts +1 -0
- package/src/layout-marketplace/payload.test.ts +17 -0
- package/src/layout-marketplace/payload.ts +40 -5
- package/src/market-data/coordinator/index.ts +3 -2
- package/src/market-data/coordinator/quotes.ts +173 -66
- package/src/market-data/coordinator/subscriptions.test.ts +215 -37
- package/src/pane-settings.ts +1 -1
- package/src/plugins/builtin/analytics/pane-model.ts +1 -1
- package/src/plugins/builtin/analytics/view.tsx +1 -1
- package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
- package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
- package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
- package/src/plugins/builtin/chart-composer/settings.ts +2 -13
- package/src/plugins/builtin/chat/content/composer.tsx +8 -4
- package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
- package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
- package/src/plugins/builtin/cloud/plugin.tsx +1 -0
- package/src/plugins/builtin/composite-plugins.ts +16 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
- package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
- package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
- package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
- package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
- package/src/plugins/builtin/earnings/index.tsx +1 -0
- package/src/plugins/builtin/earnings-calls/data.ts +223 -0
- package/src/plugins/builtin/earnings-calls/format.ts +49 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
- package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
- package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
- package/src/plugins/builtin/econ/detail-view.tsx +1 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
- package/src/plugins/builtin/econ/index.tsx +3 -1
- package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
- package/src/plugins/builtin/econ-statistics/client.ts +92 -0
- package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
- package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
- package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
- package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
- package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
- package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
- package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
- package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
- package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
- package/src/plugins/builtin/econ-statistics/view.ts +90 -0
- package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
- package/src/plugins/builtin/fear-greed/data.ts +6 -1
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
- package/src/plugins/builtin/help/index.tsx +4 -0
- package/src/plugins/builtin/holders/index.tsx +1 -0
- package/src/plugins/builtin/insider/index.tsx +1 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
- package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
- package/src/plugins/builtin/market-halts/index.tsx +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +1 -0
- package/src/plugins/builtin/market-valuation/align.ts +122 -0
- package/src/plugins/builtin/market-valuation/cache.ts +32 -0
- package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
- package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
- package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
- package/src/plugins/builtin/market-valuation/client.ts +116 -0
- package/src/plugins/builtin/market-valuation/defs.ts +251 -0
- package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
- package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
- package/src/plugins/builtin/market-valuation/headless.ts +170 -0
- package/src/plugins/builtin/market-valuation/index.tsx +64 -0
- package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
- package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
- package/src/plugins/builtin/market-valuation/model.ts +8 -0
- package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
- package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
- package/src/plugins/builtin/market-valuation/series.ts +12 -0
- package/src/plugins/builtin/market-valuation/settings.ts +38 -0
- package/src/plugins/builtin/market-valuation/sources.ts +130 -0
- package/src/plugins/builtin/market-valuation/trend.ts +89 -0
- package/src/plugins/builtin/market-valuation/view.ts +137 -0
- package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
- package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
- package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
- package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
- package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
- package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
- package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
- package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
- package/src/plugins/builtin/news/wire/index.tsx +31 -44
- package/src/plugins/builtin/options/analytics.test.ts +83 -0
- package/src/plugins/builtin/options/analytics.ts +110 -0
- package/src/plugins/builtin/options/index.tsx +18 -2
- package/src/plugins/builtin/options/table.ts +121 -51
- package/src/plugins/builtin/options/types.ts +22 -15
- package/src/plugins/builtin/options/view.test.tsx +53 -1
- package/src/plugins/builtin/options/view.tsx +92 -41
- package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
- package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
- package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
- package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
- package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
- package/src/plugins/builtin/plugin-module.test.ts +2 -4
- package/src/plugins/builtin/plugin-module.ts +9 -8
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
- package/src/plugins/builtin/research/index.tsx +4 -0
- package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
- package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
- package/src/plugins/builtin/research-search/data.ts +109 -0
- package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
- package/src/plugins/builtin/research-search/document-view.tsx +254 -0
- package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
- package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
- package/src/plugins/builtin/research-search/index.tsx +85 -0
- package/src/plugins/builtin/research-search/model.test.ts +40 -0
- package/src/plugins/builtin/research-search/model.ts +291 -0
- package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
- package/src/plugins/builtin/research-search/pane.tsx +678 -0
- package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
- package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
- package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
- package/src/plugins/builtin/research-search/snippet.ts +134 -0
- package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
- package/src/plugins/builtin/scanner/flow-model.ts +29 -1
- package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
- package/src/plugins/builtin/scanner/index.ts +2 -0
- package/src/plugins/builtin/sec/index.tsx +1 -0
- package/src/plugins/builtin/sectors/index.tsx +1 -0
- package/src/plugins/builtin/shared/series-cache.ts +93 -0
- package/src/plugins/builtin/short-interest/index.tsx +1 -0
- package/src/plugins/builtin/short-interest/pane.tsx +1 -1
- package/src/plugins/builtin/thirteenf/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
- package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
- package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
- package/src/plugins/builtin/volatility/index.tsx +1 -1
- package/src/plugins/builtin/world-indices/index.tsx +1 -0
- package/src/plugins/builtin/world-venue-map/index.ts +27 -0
- package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
- package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
- package/src/plugins/builtin/world-venue-map/model.ts +268 -0
- package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
- package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/bundle.test.ts +109 -0
- package/src/plugins/bundle.ts +139 -0
- package/src/plugins/catalog-backend.ts +6 -44
- package/src/plugins/catalog-browser.test.ts +1 -0
- package/src/plugins/catalog-browser.ts +12 -0
- package/src/plugins/catalog-manifest.test.ts +43 -0
- package/src/plugins/catalog-manifest.ts +92 -0
- package/src/plugins/catalog-ui.ts +20 -4
- package/src/plugins/catalog.ts +9 -2
- package/src/plugins/current-target.ts +28 -0
- package/src/plugins/host-contract.ts +37 -0
- package/src/plugins/host-link.ts +158 -0
- package/src/plugins/host-modules.ts +61 -0
- package/src/plugins/loader.ts +41 -22
- package/src/plugins/ownership.ts +1 -0
- package/src/plugins/prediction-markets/index.tsx +1 -0
- package/src/plugins/registry/context.ts +3 -0
- package/src/plugins/registry/contributions.ts +27 -0
- package/src/plugins/registry/index.test.ts +25 -0
- package/src/plugins/registry/index.ts +8 -0
- package/src/plugins/registry/pane-settings.test.ts +53 -0
- package/src/plugins/registry/pane-settings.ts +29 -3
- package/src/plugins/seed.test.ts +83 -0
- package/src/plugins/seed.ts +80 -0
- package/src/public/broker.ts +31 -0
- package/src/public/market-data.ts +9 -0
- package/src/public/pane-hooks.ts +57 -0
- package/src/public/public-api.test.ts +285 -0
- package/src/public/react.ts +62 -0
- package/src/public/test-support.ts +23 -0
- package/src/public/time-series.ts +8 -0
- package/src/public/utils.ts +77 -0
- package/src/remote/layout-helpers.ts +6 -8
- package/src/remote/resources.ts +2 -0
- package/src/remote/schema.ts +1 -0
- package/src/remote/semantic-tree.test.tsx +105 -0
- package/src/remote/semantic-tree.tsx +53 -35
- package/src/renderers/browser/main.tsx +6 -0
- package/src/renderers/browser/ui-host.tsx +11 -0
- package/src/renderers/cloudflare/worker.test.ts +19 -0
- package/src/renderers/cloudflare/worker.ts +1 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
- package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
- package/src/renderers/electrobun/bun/index.ts +6 -2
- package/src/renderers/electrobun/shared/protocol.ts +25 -0
- package/src/renderers/electrobun/view/build-assets.ts +0 -1
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
- package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
- package/src/renderers/electrobun/view/external-plugins.ts +63 -0
- package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
- package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
- package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
- package/src/renderers/electrobun/view/host/mouse.ts +16 -2
- package/src/renderers/electrobun/view/main.tsx +27 -2
- package/src/renderers/electrobun/view/styles.css +5 -0
- package/src/renderers/electrobun/view/toast-host.tsx +18 -0
- package/src/renderers/electrobun/view/ui-host.tsx +3 -0
- package/src/renderers/opentui/chart-surface.tsx +5 -1
- package/src/renderers/opentui/host.tsx +20 -2
- package/src/renderers/opentui/interaction-performance.test.ts +79 -0
- package/src/renderers/opentui/interaction-performance.ts +188 -0
- package/src/renderers/opentui/start.tsx +26 -1
- package/src/renderers/opentui/test-utils.tsx +13 -0
- package/src/renderers/opentui/toast-host.tsx +26 -2
- package/src/renderers/opentui/ui-host.tsx +1 -0
- package/src/sources/provider-router/search.test.ts +191 -0
- package/src/sources/provider-router/search.ts +92 -39
- package/src/state/app/context/index.tsx +2 -0
- package/src/state/app/context/selector.test.tsx +33 -1
- package/src/state/hooks/quote-streaming.test.tsx +93 -0
- package/src/state/hooks/quote-streaming.ts +106 -30
- package/src/state/pane-table-export-registry.ts +50 -0
- package/src/theme/color-utils.ts +1 -1
- package/src/theme/colors.ts +50 -0
- package/src/theme/themes.test.ts +15 -2
- package/src/theme/themes.ts +19 -1
- package/src/tickers/search/index.test.ts +26 -0
- package/src/tickers/search/index.ts +42 -17
- package/src/tickers/search/ranking.ts +14 -0
- package/src/time-series/chart-data.test.ts +216 -0
- package/src/time-series/range.ts +12 -0
- package/src/time-series/resolution.test.ts +25 -26
- package/src/time-series/resolution.ts +44 -25
- package/src/time-series/resolve.ts +7 -0
- package/src/time-series/spec.ts +5 -5
- package/src/time-series/types.ts +3 -1
- package/src/time-series/use-chart-resolution.ts +5 -0
- package/src/types/config.ts +123 -3
- package/src/types/data-provider.ts +11 -0
- package/src/types/headless.ts +161 -0
- package/src/types/plugin.ts +104 -20
- package/src/ui/host.tsx +19 -1
- package/src/ui/index.tsx +1 -0
- package/src/ui/toast.tsx +8 -4
- package/src/utils/csv.test.ts +13 -0
- package/src/utils/csv.ts +37 -0
- package/src/utils/format.ts +35 -0
- package/src/utils/fuzzy-search.test.ts +94 -0
- package/src/utils/fuzzy-search.ts +81 -22
- package/src/utils/save-text-file.test.ts +24 -0
- package/src/utils/save-text-file.ts +36 -0
- package/src/utils/shortcut-labels.ts +14 -0
- package/src/components/chart/core/axis-format.ts +0 -157
- package/src/components/chart/core/index.test.ts +0 -1075
- package/src/components/chart/core/projection.ts +0 -357
- package/src/components/chart/core/render-utils.ts +0 -4
- package/src/components/chart/core/renderer.ts +0 -244
- package/src/components/chart/core/scene.ts +0 -180
- package/src/components/chart/core/time-axis.ts +0 -330
- package/src/components/chart/cursor-motion.ts +0 -47
- package/src/components/chart/indicators/types.ts +0 -26
- package/src/components/chart/market-session.ts +0 -176
- package/src/components/chart/multi-line/renderer.test.ts +0 -83
- package/src/components/chart/multi-line/renderer.ts +0 -161
- package/src/components/chart/multi-line/scene.ts +0 -188
- package/src/components/chart/native/raster/layout.ts +0 -129
- package/src/components/chart/native/raster/price-chart.ts +0 -244
- package/src/components/chart/rendering/chart-draw.ts +0 -270
- package/src/components/chart/rendering/geometry.ts +0 -95
- package/src/components/chart/rendering/pixel-buffer.ts +0 -267
- package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
- package/src/components/chart/scatter-chart-renderer.ts +0 -223
- package/src/components/chart/static/chart/surface.tsx +0 -358
- package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
- package/src/components/command-bar/routes/root/article-results.ts +0 -55
- package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
- package/src/plugins/builtin/news/wire/article-search.ts +0 -140
- package/src/plugins/builtin/substack/api/auth.ts +0 -185
- package/src/plugins/builtin/substack/api/cache.ts +0 -82
- package/src/plugins/builtin/substack/api/loaders.ts +0 -170
- package/src/plugins/builtin/substack/api/store.ts +0 -186
- package/src/plugins/builtin/substack/api/types.ts +0 -42
- package/src/plugins/builtin/substack/api.test.ts +0 -298
- package/src/plugins/builtin/substack/article-detail.tsx +0 -397
- package/src/plugins/builtin/substack/article-stack.tsx +0 -133
- package/src/plugins/builtin/substack/content.ts +0 -478
- package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
- package/src/plugins/builtin/substack/index.tsx +0 -49
- package/src/plugins/builtin/substack/login-view.tsx +0 -126
- package/src/plugins/builtin/substack/model.test.ts +0 -155
- package/src/plugins/builtin/substack/normalize.ts +0 -343
- package/src/plugins/builtin/substack/pane-footer.ts +0 -52
- package/src/plugins/builtin/substack/pane-state.ts +0 -170
- package/src/plugins/builtin/substack/pane.tsx +0 -547
- package/src/plugins/builtin/substack/read-state.ts +0 -30
- package/src/plugins/builtin/substack/table.ts +0 -111
- package/src/plugins/builtin/substack/types.ts +0 -71
- package/src/plugins/builtin/substack/utils.ts +0 -131
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
- package/src/plugins/ibkr/account-cache.test.ts +0 -102
- package/src/plugins/ibkr/account-cache.ts +0 -24
- package/src/plugins/ibkr/broker-adapter.ts +0 -286
- package/src/plugins/ibkr/config.test.ts +0 -79
- package/src/plugins/ibkr/config.ts +0 -250
- package/src/plugins/ibkr/dialogs.tsx +0 -40
- package/src/plugins/ibkr/flex/index.test.ts +0 -264
- package/src/plugins/ibkr/flex/index.ts +0 -367
- package/src/plugins/ibkr/flex/statement-client.ts +0 -235
- package/src/plugins/ibkr/fundamental-parser.ts +0 -149
- package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
- package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
- package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
- package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
- package/src/plugins/ibkr/gateway/connection.ts +0 -152
- package/src/plugins/ibkr/gateway/contracts.ts +0 -132
- package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
- package/src/plugins/ibkr/gateway/helpers.ts +0 -33
- package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
- package/src/plugins/ibkr/gateway/history.ts +0 -120
- package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
- package/src/plugins/ibkr/gateway/market-data.ts +0 -345
- package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
- package/src/plugins/ibkr/gateway/orders.ts +0 -63
- package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
- package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
- package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
- package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
- package/src/plugins/ibkr/gateway/selection.ts +0 -51
- package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
- package/src/plugins/ibkr/gateway/service/index.ts +0 -429
- package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
- package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
- package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
- package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
- package/src/plugins/ibkr/gateway/service/native.ts +0 -543
- package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
- package/src/plugins/ibkr/gateway/types.ts +0 -27
- package/src/plugins/ibkr/index.test.tsx +0 -162
- package/src/plugins/ibkr/index.tsx +0 -159
- package/src/plugins/ibkr/instance-selection.test.ts +0 -120
- package/src/plugins/ibkr/instance-selection.ts +0 -36
- package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
- package/src/plugins/ibkr/portfolio-performance.ts +0 -129
- package/src/plugins/ibkr/trade/badge.tsx +0 -28
- package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
- package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
- package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
- package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
- package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
- package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
- package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
- package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
- package/src/plugins/ibkr/trade/tab/model.ts +0 -159
- package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
- package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
- package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
- package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
- package/src/plugins/ibkr/trade/utils.test.ts +0 -175
- package/src/plugins/ibkr/trade/utils.ts +0 -144
- package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
- package/src/plugins/ibkr/trading/pane.tsx +0 -332
- package/src/plugins/ibkr/trading/state.test.ts +0 -109
- package/src/plugins/ibkr/trading/state.ts +0 -335
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { readdir, stat } from "fs/promises";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
import type { DesktopExternalPluginBundle } from "../shared/protocol";
|
|
6
|
+
import { bundleExternalPlugin, pluginBundleCacheDir } from "../../../plugins/bundle";
|
|
7
|
+
import { linkHostPackages } from "../../../plugins/host-link";
|
|
8
|
+
import { getPluginsDir, resolvePluginEntry } from "../../../plugins/loader";
|
|
9
|
+
import type { GloomPlugin } from "../../../types/plugin";
|
|
10
|
+
import { debugLog } from "../../../utils/debug-log";
|
|
11
|
+
|
|
12
|
+
const log = debugLog.createLogger("desktop-plugins");
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Prepares external plugins for the desktop view.
|
|
16
|
+
*
|
|
17
|
+
* The view is a browser context and cannot read `~/.gloomberb/plugins`, so the
|
|
18
|
+
* Bun process does both halves here: it imports each plugin natively to read
|
|
19
|
+
* its metadata, and compiles it to an ES module the view can evaluate.
|
|
20
|
+
*
|
|
21
|
+
* Bundling is cached against the newest mtime in the plugin directory. Without
|
|
22
|
+
* that, every window open would recompile every plugin, which is slow enough to
|
|
23
|
+
* be visible at startup.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface CacheEntry {
|
|
27
|
+
mtimeMs: number;
|
|
28
|
+
code: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const bundleCache = new Map<string, CacheEntry>();
|
|
32
|
+
|
|
33
|
+
async function newestMtime(dir: string): Promise<number> {
|
|
34
|
+
let newest = 0;
|
|
35
|
+
const walk = async (current: string, depth: number): Promise<void> => {
|
|
36
|
+
if (depth > 6) return;
|
|
37
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
38
|
+
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
39
|
+
const full = join(current, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
await walk(full, depth + 1);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const info = await stat(full);
|
|
45
|
+
if (info.mtimeMs > newest) newest = info.mtimeMs;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
await walk(dir, 0);
|
|
49
|
+
return newest;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readPluginMetadata(entryFile: string): Promise<GloomPlugin | null> {
|
|
53
|
+
try {
|
|
54
|
+
const mod = await import(entryFile);
|
|
55
|
+
const plugin: GloomPlugin = mod.default ?? mod.plugin;
|
|
56
|
+
return plugin?.id && plugin?.name ? plugin : null;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
log.error(`Metadata read failed for ${entryFile}: ${error}`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function collectExternalPluginBundles(): Promise<DesktopExternalPluginBundle[]> {
|
|
64
|
+
const pluginsDir = getPluginsDir();
|
|
65
|
+
if (!existsSync(pluginsDir)) return [];
|
|
66
|
+
|
|
67
|
+
const outDir = pluginBundleCacheDir(join(pluginsDir, ".cache"));
|
|
68
|
+
const bundles: DesktopExternalPluginBundle[] = [];
|
|
69
|
+
|
|
70
|
+
for (const entry of await readdir(pluginsDir, { withFileTypes: true })) {
|
|
71
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
72
|
+
const pluginDir = join(pluginsDir, entry.name);
|
|
73
|
+
|
|
74
|
+
const entryFile = await resolvePluginEntry(pluginDir);
|
|
75
|
+
if (!entryFile) continue;
|
|
76
|
+
|
|
77
|
+
linkHostPackages(pluginDir);
|
|
78
|
+
|
|
79
|
+
const plugin = await readPluginMetadata(entryFile);
|
|
80
|
+
const base = {
|
|
81
|
+
id: plugin?.id ?? entry.name,
|
|
82
|
+
name: plugin?.name ?? entry.name,
|
|
83
|
+
version: plugin?.version ?? "0.0.0",
|
|
84
|
+
path: pluginDir,
|
|
85
|
+
...(plugin?.targets ? { targets: plugin.targets } : {}),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (!plugin) {
|
|
89
|
+
bundles.push({ ...base, error: "Plugin did not export a valid GloomPlugin." });
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Skip compiling something the desktop cannot run anyway; the marketplace
|
|
94
|
+
// still lists it, explaining why it is inert.
|
|
95
|
+
if (plugin.targets && !plugin.targets.includes("desktop")) {
|
|
96
|
+
bundles.push({ ...base, error: `${plugin.name} does not support the desktop app.` });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const mtimeMs = await newestMtime(pluginDir);
|
|
102
|
+
const cached = bundleCache.get(pluginDir);
|
|
103
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
104
|
+
bundles.push({ ...base, code: cached.code });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await bundleExternalPlugin(pluginDir, join(outDir, entry.name));
|
|
109
|
+
const code = await Bun.file(result.outputPath).text();
|
|
110
|
+
bundleCache.set(pluginDir, { mtimeMs, code });
|
|
111
|
+
log.info(`Bundled ${plugin.id} (${Math.round(code.length / 1024)}KB, shared: ${result.shared.join(", ")})`);
|
|
112
|
+
bundles.push({ ...base, code });
|
|
113
|
+
} catch (error) {
|
|
114
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
115
|
+
log.error(`Bundling ${plugin.id} failed: ${message}`);
|
|
116
|
+
bundles.push({ ...base, error: message });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return bundles;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Installs a plugin on behalf of the desktop view, which cannot run git or bun
|
|
125
|
+
* itself. Errors are returned rather than thrown so the marketplace can show
|
|
126
|
+
* them next to the plugin instead of surfacing an RPC failure.
|
|
127
|
+
*
|
|
128
|
+
* The bundle cache is cleared so the next `plugins.listExternal` compiles the
|
|
129
|
+
* newly installed plugin rather than serving a stale set.
|
|
130
|
+
*/
|
|
131
|
+
export async function installExternalPlugin(ref: string): Promise<{ ok: boolean; error?: string }> {
|
|
132
|
+
try {
|
|
133
|
+
const { installPlugin } = await import("../../../cli/commands/plugins");
|
|
134
|
+
await installPlugin(ref, { quiet: true });
|
|
135
|
+
bundleCache.clear();
|
|
136
|
+
return { ok: true };
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -25,7 +25,6 @@ import type { DesktopWorkspace } from "./desktop/workspace";
|
|
|
25
25
|
import { buildDesktopApplicationMenu } from "./application-menu";
|
|
26
26
|
import { applicationMenuCommand } from "./application-menu/click";
|
|
27
27
|
import { registerElectrobunCoreCapabilities } from "./core-capabilities";
|
|
28
|
-
import { setNativeIbkrGatewayModuleLoader } from "../../../plugins/ibkr/gateway/service";
|
|
29
28
|
import {
|
|
30
29
|
runElectrobunDesktopUpdate,
|
|
31
30
|
} from "./desktop/update";
|
|
@@ -37,6 +36,7 @@ import {
|
|
|
37
36
|
} from "./window/frame";
|
|
38
37
|
import { MAIN_WINDOW_RPC_KEY } from "./window/focus";
|
|
39
38
|
import { handleHttpFetch } from "./desktop/http-fetch";
|
|
39
|
+
import { collectExternalPluginBundles, installExternalPlugin } from "./external-plugins";
|
|
40
40
|
import { handleDesktopPluginStateRequest } from "./desktop/plugin-state";
|
|
41
41
|
import { scheduleDesktopRelaunch } from "./desktop/relaunch";
|
|
42
42
|
import {
|
|
@@ -72,7 +72,6 @@ console.info = (...args) => console.error(...args);
|
|
|
72
72
|
console.warn = (...args) => console.error(...args);
|
|
73
73
|
|
|
74
74
|
setConfigStoreHost(nodeConfigStoreHost);
|
|
75
|
-
setNativeIbkrGatewayModuleLoader(() => import("../../../plugins/ibkr/gateway/service/native"));
|
|
76
75
|
|
|
77
76
|
let currentConfig: AppConfig | null = null;
|
|
78
77
|
let services: AppServices | null = null;
|
|
@@ -468,6 +467,10 @@ async function handleBackendRequest(
|
|
|
468
467
|
case "pluginState.setMany":
|
|
469
468
|
case "pluginState.delete":
|
|
470
469
|
return handleDesktopPluginStateRequest(requireServices().persistence.pluginState, request);
|
|
470
|
+
case "plugins.listExternal":
|
|
471
|
+
return collectExternalPluginBundles();
|
|
472
|
+
case "plugins.install":
|
|
473
|
+
return installExternalPlugin(request.payload.ref);
|
|
471
474
|
case "host.restart":
|
|
472
475
|
case "host.exit":
|
|
473
476
|
case "host.windowControl":
|
|
@@ -476,6 +479,7 @@ async function handleBackendRequest(
|
|
|
476
479
|
case "host.focusWindow":
|
|
477
480
|
case "host.copyPngImage":
|
|
478
481
|
case "host.readText":
|
|
482
|
+
case "host.saveTextFile":
|
|
479
483
|
case "host.notify":
|
|
480
484
|
case "host.showContextMenu":
|
|
481
485
|
return handleDesktopHostRequest({
|
|
@@ -83,6 +83,25 @@ export interface DesktopCapabilitySubscribeRequest extends DesktopCapabilityInvo
|
|
|
83
83
|
subscriptionId: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* One external plugin, compiled for the view.
|
|
88
|
+
*
|
|
89
|
+
* The Bun process owns the filesystem, so it reads and bundles the plugin and
|
|
90
|
+
* hands the view executable module text. `error` is carried rather than thrown
|
|
91
|
+
* so a single broken plugin surfaces in the marketplace instead of taking down
|
|
92
|
+
* the renderer.
|
|
93
|
+
*/
|
|
94
|
+
export interface DesktopExternalPluginBundle {
|
|
95
|
+
id: string;
|
|
96
|
+
name: string;
|
|
97
|
+
version: string;
|
|
98
|
+
path: string;
|
|
99
|
+
/** ES module source, absent when `error` is set. */
|
|
100
|
+
code?: string;
|
|
101
|
+
targets?: readonly ("cli" | "tui" | "desktop" | "web")[];
|
|
102
|
+
error?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
export interface DesktopBackendRequestMap {
|
|
87
106
|
init: {
|
|
88
107
|
request: { kind?: "main" | "detached"; paneId?: string };
|
|
@@ -111,6 +130,8 @@ export interface DesktopBackendRequestMap {
|
|
|
111
130
|
"pluginState.set": { request: DesktopPluginStateSetEntry; response: null };
|
|
112
131
|
"pluginState.setMany": { request: { entries: DesktopPluginStateSetEntry[] }; response: null };
|
|
113
132
|
"pluginState.delete": { request: { pluginId: string; key: string }; response: null };
|
|
133
|
+
"plugins.listExternal": { request: null; response: DesktopExternalPluginBundle[] };
|
|
134
|
+
"plugins.install": { request: { ref: string }; response: { ok: boolean; error?: string } };
|
|
114
135
|
"host.restart": { request: DesktopRestartMessage; response: null };
|
|
115
136
|
"host.exit": { request: null; response: null };
|
|
116
137
|
"host.windowControl": { request: { action: DesktopWindowControlAction }; response: null };
|
|
@@ -119,6 +140,10 @@ export interface DesktopBackendRequestMap {
|
|
|
119
140
|
"host.focusWindow": { request: null; response: null };
|
|
120
141
|
"host.copyPngImage": { request: { pngBase64: string }; response: null };
|
|
121
142
|
"host.readText": { request: null; response: string };
|
|
143
|
+
"host.saveTextFile": {
|
|
144
|
+
request: { name: string; text: string; mimeType: string };
|
|
145
|
+
response: string;
|
|
146
|
+
};
|
|
122
147
|
"host.notify": {
|
|
123
148
|
request: { title?: string; body?: string; subtitle?: string; sound?: string };
|
|
124
149
|
response: null;
|
|
@@ -23,7 +23,6 @@ const COMMON_ALIAS_RULES: AliasRule[] = [
|
|
|
23
23
|
["./kitty/support", "components/chart/native/renderer-selection.ts", "native-stubs/chart/kitty-support.ts"],
|
|
24
24
|
["native/surface/manager", "native-stubs/chart/surface-manager.ts"],
|
|
25
25
|
["native/surface/sync", "native-stubs/chart/surface-sync.ts"],
|
|
26
|
-
["./native-loader", "plugins/ibkr/gateway/service/index.ts", "native-stubs/ibkr-native-loader.ts"],
|
|
27
26
|
["./native-loader", "plugins/broker-sync/robinhood.ts", "native-stubs/broker-sync-native-loader.ts"],
|
|
28
27
|
["./native-loader", "plugins/broker-sync/simplefin.ts", "native-stubs/broker-sync-native-loader.ts"],
|
|
29
28
|
];
|
|
@@ -41,9 +41,14 @@ import type { AppState, PaneRuntimeState } from "../../../core/state/app/state";
|
|
|
41
41
|
import type { PaneDef } from "../../../types/plugin";
|
|
42
42
|
import { canonicalTickerKey, parsePublicTickerKey } from "../../../utils/exchanges";
|
|
43
43
|
import { hydrateFredSeries, type FredSeriesCacheEntry } from "../../../data/fred-series";
|
|
44
|
+
import { hydrateValuationSeries } from "../../../plugins/builtin/market-valuation/cache";
|
|
45
|
+
import { statsCache } from "../../../plugins/builtin/econ-statistics/cache";
|
|
46
|
+
import type { DatedObservation } from "../../../plugins/builtin/market-valuation/series";
|
|
44
47
|
import { clipPriceHistoryToRange } from "../../../time-series/history-window";
|
|
45
48
|
import type { ResolvedSeries } from "../../../time-series/types";
|
|
46
49
|
import { chartSeriesSourceKey } from "../../../capabilities";
|
|
50
|
+
import { apiClient, setCloudApiFetchTransport } from "../../../api-client";
|
|
51
|
+
import { createGloomberbCloudProvider } from "../../../sources/gloomberb-cloud";
|
|
47
52
|
|
|
48
53
|
interface CliPaneShotPayload {
|
|
49
54
|
config: AppConfig;
|
|
@@ -54,6 +59,8 @@ interface CliPaneShotPayload {
|
|
|
54
59
|
financials: Array<[string, TickerFinancials]>;
|
|
55
60
|
optionsChains: Array<[string, OptionsChain]>;
|
|
56
61
|
fredSeries: Array<[string, FredSeriesCacheEntry]>;
|
|
62
|
+
valuationSeries: Array<[string, DatedObservation[]]>;
|
|
63
|
+
statSeries: Array<[string, DatedObservation[]]>;
|
|
57
64
|
capabilitySeries: Array<[string, ResolvedSeries]>;
|
|
58
65
|
paneState: Record<string, PaneRuntimeState>;
|
|
59
66
|
}
|
|
@@ -77,6 +84,7 @@ const SHOT_READY_STABLE_FRAMES = 10;
|
|
|
77
84
|
// content contains the word "loading" (a changelog release note, a news
|
|
78
85
|
// headline) wait forever and time out.
|
|
79
86
|
const SHOT_LOADING_SELECTOR = "[data-gloom-status=\"loading\"]";
|
|
87
|
+
const SHOT_API_PROXY_PREFIX = "/__gloom_cli_api__";
|
|
80
88
|
const TRACKED_RESPONSE_METHODS = new Set<PropertyKey>([
|
|
81
89
|
"arrayBuffer",
|
|
82
90
|
"blob",
|
|
@@ -141,6 +149,29 @@ function installShotFetchTracker(): void {
|
|
|
141
149
|
)) as typeof fetch;
|
|
142
150
|
}
|
|
143
151
|
|
|
152
|
+
function installShotCloudApiTransport(): void {
|
|
153
|
+
apiClient.setCookieSessionMode(true);
|
|
154
|
+
setCloudApiFetchTransport((url, init = {}) => {
|
|
155
|
+
const upstream = new URL(url);
|
|
156
|
+
const proxyUrl = new URL(`${SHOT_API_PROXY_PREFIX}${upstream.pathname}`, window.location.origin);
|
|
157
|
+
proxyUrl.search = upstream.search;
|
|
158
|
+
const headers = new Headers(init.headers);
|
|
159
|
+
// Bun owns the real session cookie. The browser only talks to the local
|
|
160
|
+
// same-origin proxy and never receives or serializes the credential.
|
|
161
|
+
headers.delete("Cookie");
|
|
162
|
+
headers.delete("Origin");
|
|
163
|
+
return window.fetch(proxyUrl, {
|
|
164
|
+
...init,
|
|
165
|
+
headers,
|
|
166
|
+
credentials: "same-origin",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function restoreShotCloudSession(): Promise<void> {
|
|
172
|
+
await apiClient.getSession().catch(() => null);
|
|
173
|
+
}
|
|
174
|
+
|
|
144
175
|
function resolveShotWork<T>(value: T): Promise<T> {
|
|
145
176
|
return trackShotWork(Promise.resolve(value));
|
|
146
177
|
}
|
|
@@ -249,7 +280,7 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
249
280
|
return data;
|
|
250
281
|
};
|
|
251
282
|
|
|
252
|
-
|
|
283
|
+
const injectedProvider: DataProvider = {
|
|
253
284
|
id: "cli-shot",
|
|
254
285
|
name: "CLI screenshot data",
|
|
255
286
|
getTickerFinancials(ticker, exchange) {
|
|
@@ -330,6 +361,19 @@ function createShotDataProvider(payload: CliPaneShotPayload): DataProvider {
|
|
|
330
361
|
return () => {};
|
|
331
362
|
},
|
|
332
363
|
};
|
|
364
|
+
|
|
365
|
+
// Keep deterministic payload data for the mapped screenshot capabilities,
|
|
366
|
+
// then fall through to the authenticated cloud provider for operations such
|
|
367
|
+
// as SEC filings that live panes request through market-data hooks.
|
|
368
|
+
const cloudProvider = createGloomberbCloudProvider();
|
|
369
|
+
return new Proxy(injectedProvider, {
|
|
370
|
+
get(target, property, receiver) {
|
|
371
|
+
const local = Reflect.get(target, property, receiver);
|
|
372
|
+
if (local !== undefined) return local;
|
|
373
|
+
const cloud = Reflect.get(cloudProvider, property, cloudProvider);
|
|
374
|
+
return typeof cloud === "function" ? cloud.bind(cloudProvider) : cloud;
|
|
375
|
+
},
|
|
376
|
+
});
|
|
333
377
|
}
|
|
334
378
|
|
|
335
379
|
function installShotMarketData(payload: CliPaneShotPayload): void {
|
|
@@ -586,11 +630,17 @@ async function render() {
|
|
|
586
630
|
if (!payload) throw new Error("Missing CLI pane screenshot payload.");
|
|
587
631
|
revivePayloadDates(payload);
|
|
588
632
|
installShotFetchTracker();
|
|
633
|
+
installShotCloudApiTransport();
|
|
589
634
|
hydrateFredSeries(payload.fredSeries ?? []);
|
|
635
|
+
hydrateValuationSeries(payload.valuationSeries ?? []);
|
|
636
|
+
statsCache.hydrate(payload.statSeries ?? []);
|
|
590
637
|
installShotMarketData(payload);
|
|
591
638
|
// Panes contributed from an async setup() only exist once every plugin has
|
|
592
639
|
// finished registering, so the tree cannot mount before that resolves.
|
|
593
640
|
await installShotPluginRegistry(payload);
|
|
641
|
+
// Plugin setup hydrates its in-memory persistence and may reset apiClient.
|
|
642
|
+
// Restore the proxied cloud session only after registration is complete.
|
|
643
|
+
await restoreShotCloudSession();
|
|
594
644
|
|
|
595
645
|
const rootElement = document.getElementById("root");
|
|
596
646
|
if (!rootElement) throw new Error("Missing root element.");
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { loadDesktopExternalPlugins } from "./external-plugins";
|
|
4
|
+
import type { DesktopExternalPluginBundle } from "../shared/protocol";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The guarantee under test is containment: a plugin that fails to compile, ships
|
|
8
|
+
* no bundle, or exports the wrong shape must surface as a broken entry the
|
|
9
|
+
* marketplace can explain, never as an exception that stops the desktop app from
|
|
10
|
+
* starting. Every case here is one a user can hit by installing a bad plugin.
|
|
11
|
+
*/
|
|
12
|
+
function bundle(overrides: Partial<DesktopExternalPluginBundle>): DesktopExternalPluginBundle {
|
|
13
|
+
return { id: "x", name: "X", version: "1.0.0", path: "/tmp/x", ...overrides };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("loadDesktopExternalPlugins", () => {
|
|
17
|
+
test("does nothing when there is nothing to load", async () => {
|
|
18
|
+
expect(await loadDesktopExternalPlugins([])).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("carries a compile error through instead of throwing", async () => {
|
|
22
|
+
const [entry] = await loadDesktopExternalPlugins([
|
|
23
|
+
bundle({ id: "broken", error: "Could not resolve ./nope" }),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
expect(entry?.error).toBe("Could not resolve ./nope");
|
|
27
|
+
expect(entry?.plugin.id).toBe("broken");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("reports a bundle that arrived without code", async () => {
|
|
31
|
+
const [entry] = await loadDesktopExternalPlugins([bundle({ id: "empty" })]);
|
|
32
|
+
|
|
33
|
+
expect(entry?.error).toContain("no bundle");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("keeps loading the rest after one plugin fails", async () => {
|
|
37
|
+
const entries = await loadDesktopExternalPlugins([
|
|
38
|
+
bundle({ id: "first", error: "boom" }),
|
|
39
|
+
bundle({ id: "second", error: "also boom" }),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
expect(entries.map((entry) => entry.plugin.id)).toEqual(["first", "second"]);
|
|
43
|
+
expect(entries.every((entry) => entry.error)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { DesktopExternalPluginBundle } from "../shared/protocol";
|
|
2
|
+
import { installPluginHostModules } from "../../../plugins/host-modules";
|
|
3
|
+
import type { LoadedExternalPlugin } from "../../../plugins/loader";
|
|
4
|
+
import type { GloomPlugin } from "../../../types/plugin";
|
|
5
|
+
import { debugLog } from "../../../utils/debug-log";
|
|
6
|
+
|
|
7
|
+
const log = debugLog.createLogger("desktop-plugins");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Evaluates the plugin bundles the Bun process compiled for this view.
|
|
11
|
+
*
|
|
12
|
+
* The host's shared modules have to be published before any bundle is imported:
|
|
13
|
+
* a compiled plugin's `react` and `gloomberb/*` imports read from that registry
|
|
14
|
+
* rather than carrying their own copies (see `plugins/bundle.ts`).
|
|
15
|
+
*
|
|
16
|
+
* Bundles are imported from blob URLs. The desktop view has no origin to fetch
|
|
17
|
+
* from and no filesystem, and writing them to a served directory would mean
|
|
18
|
+
* managing a cache the renderer cannot clean up reliably.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadDesktopExternalPlugins(
|
|
21
|
+
bundles: readonly DesktopExternalPluginBundle[],
|
|
22
|
+
): Promise<LoadedExternalPlugin[]> {
|
|
23
|
+
if (bundles.length === 0) return [];
|
|
24
|
+
|
|
25
|
+
await installPluginHostModules();
|
|
26
|
+
|
|
27
|
+
const loaded: LoadedExternalPlugin[] = [];
|
|
28
|
+
|
|
29
|
+
for (const bundle of bundles) {
|
|
30
|
+
const fallback = {
|
|
31
|
+
plugin: { id: bundle.id, name: bundle.name, version: bundle.version } as GloomPlugin,
|
|
32
|
+
path: bundle.path,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (bundle.error || !bundle.code) {
|
|
36
|
+
loaded.push({ ...fallback, error: bundle.error ?? "Plugin produced no bundle." });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let objectUrl: string | null = null;
|
|
41
|
+
try {
|
|
42
|
+
objectUrl = URL.createObjectURL(new Blob([bundle.code], { type: "text/javascript" }));
|
|
43
|
+
const mod = await import(/* @vite-ignore */ objectUrl);
|
|
44
|
+
const plugin: GloomPlugin = mod.default ?? mod.plugin;
|
|
45
|
+
if (!plugin?.id || !plugin?.name) {
|
|
46
|
+
loaded.push({ ...fallback, error: "Bundle did not export a valid GloomPlugin." });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
log.info(`Loaded external plugin: ${plugin.id} v${plugin.version ?? "0.0.0"}`);
|
|
50
|
+
loaded.push({ plugin, path: bundle.path });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
log.error(`Evaluating ${bundle.id} failed: ${message}`);
|
|
54
|
+
loaded.push({ ...fallback, error: message });
|
|
55
|
+
} finally {
|
|
56
|
+
// The module graph keeps its own reference once imported, so the URL can
|
|
57
|
+
// be released immediately; leaving it would leak for the session.
|
|
58
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return loaded;
|
|
63
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { webAsciiTextWordmarkVariant } from "./ascii-text";
|
|
3
|
+
|
|
4
|
+
const globalWithNavigator = globalThis as { navigator?: unknown };
|
|
5
|
+
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
|
6
|
+
|
|
7
|
+
function stubNavigator(platform: string): void {
|
|
8
|
+
Object.defineProperty(globalThis, "navigator", {
|
|
9
|
+
value: { platform, userAgent: platform },
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (originalNavigator) Object.defineProperty(globalThis, "navigator", originalNavigator);
|
|
17
|
+
else delete globalWithNavigator.navigator;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The block-glyph wordmark only renders on macOS fonts, so every other platform
|
|
22
|
+
* gets the `/ \ | _` fallback. The DOM host is shared by Electrobun, which knows
|
|
23
|
+
* its `process.platform`, and the hosted web build, which does not and passes a
|
|
24
|
+
* sentinel instead. Reading that sentinel as "not macOS" is what shipped the
|
|
25
|
+
* wrong wordmark to Mac browsers.
|
|
26
|
+
*/
|
|
27
|
+
describe("web wordmark variant", () => {
|
|
28
|
+
test("trusts a real platform from the desktop host over the navigator", () => {
|
|
29
|
+
stubNavigator("MacIntel");
|
|
30
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "darwin")).toBe("legacy");
|
|
31
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "win32")).toBe("compat");
|
|
32
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "linux")).toBe("compat");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("falls back to the navigator when the host names no OS", () => {
|
|
36
|
+
stubNavigator("MacIntel");
|
|
37
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "browser")).toBe("legacy");
|
|
38
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "")).toBe("legacy");
|
|
39
|
+
stubNavigator("Win32");
|
|
40
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "wordmark", "browser")).toBe("compat");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("leaves anything that is not the wordmark alone", () => {
|
|
44
|
+
stubNavigator("MacIntel");
|
|
45
|
+
expect(webAsciiTextWordmarkVariant("Gloomberb", "tiny", "darwin")).toBeNull();
|
|
46
|
+
expect(webAsciiTextWordmarkVariant("Portfolio", "wordmark", "darwin")).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -25,8 +25,18 @@ function currentDesktopPlatform(): string {
|
|
|
25
25
|
].filter(Boolean).join(" ");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* A platform string worth trusting. The DOM host is shared by Electrobun, which
|
|
30
|
+
* passes a real `process.platform`, and the browser build, which passes the
|
|
31
|
+
* sentinel `"browser"` because a web page has no OS of its own. Only the former
|
|
32
|
+
* answers the question we are asking, so anything that does not name an OS
|
|
33
|
+
* falls through to sniffing the navigator.
|
|
34
|
+
*/
|
|
35
|
+
const OS_PLATFORM = /(darwin|mac|iphone|ipad|ipod|win|linux|android|freebsd|openbsd|netbsd|aix|sunos|cros|x11)/i;
|
|
36
|
+
|
|
28
37
|
function isMacDesktopPlatform(desktopPlatform?: string): boolean {
|
|
29
|
-
const
|
|
38
|
+
const hint = desktopPlatform?.trim();
|
|
39
|
+
const raw = hint && OS_PLATFORM.test(hint) ? hint : currentDesktopPlatform();
|
|
30
40
|
return /(darwin|mac|iphone|ipad|ipod)/i.test(raw);
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -145,9 +145,11 @@ function ChartCrosshair({
|
|
|
145
145
|
}) {
|
|
146
146
|
if (!bitmap || !crosshair) return null;
|
|
147
147
|
const x = bitmap.width <= 1 ? 0 : (crosshair.pixelX / (bitmap.width - 1)) * 100;
|
|
148
|
-
const y =
|
|
148
|
+
const y = crosshair.pixelY === null
|
|
149
|
+
? null
|
|
150
|
+
: bitmap.height <= 1 ? 0 : (crosshair.pixelY / (bitmap.height - 1)) * 100;
|
|
149
151
|
const clampedX = Math.max(0, Math.min(100, x));
|
|
150
|
-
const clampedY = Math.max(0, Math.min(100, y));
|
|
152
|
+
const clampedY = y === null ? null : Math.max(0, Math.min(100, y));
|
|
151
153
|
return (
|
|
152
154
|
<>
|
|
153
155
|
<div
|
|
@@ -164,36 +166,40 @@ function ChartCrosshair({
|
|
|
164
166
|
zIndex: 10,
|
|
165
167
|
}}
|
|
166
168
|
/>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
169
|
+
{clampedY === null ? null : (
|
|
170
|
+
<>
|
|
171
|
+
<div
|
|
172
|
+
style={{
|
|
173
|
+
position: "absolute",
|
|
174
|
+
left: 0,
|
|
175
|
+
right: 0,
|
|
176
|
+
top: `${clampedY}%`,
|
|
177
|
+
height: 1,
|
|
178
|
+
backgroundColor: crosshair.color,
|
|
179
|
+
opacity: 0.78,
|
|
180
|
+
transform: "translateY(-0.5px)",
|
|
181
|
+
pointerEvents: "none",
|
|
182
|
+
zIndex: 10,
|
|
183
|
+
}}
|
|
184
|
+
/>
|
|
185
|
+
<div
|
|
186
|
+
style={{
|
|
187
|
+
position: "absolute",
|
|
188
|
+
left: `${clampedX}%`,
|
|
189
|
+
top: `${clampedY}%`,
|
|
190
|
+
width: 7,
|
|
191
|
+
height: 7,
|
|
192
|
+
borderRadius: 7,
|
|
193
|
+
border: `1px solid ${crosshair.color}`,
|
|
194
|
+
backgroundColor: `color-mix(in srgb, ${crosshair.color} 16%, transparent)`,
|
|
195
|
+
boxSizing: "border-box",
|
|
196
|
+
transform: "translate(-50%, -50%)",
|
|
197
|
+
pointerEvents: "none",
|
|
198
|
+
zIndex: 11,
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
</>
|
|
202
|
+
)}
|
|
197
203
|
</>
|
|
198
204
|
);
|
|
199
205
|
}
|
|
@@ -27,16 +27,30 @@ export function cancelWebFrame(id: number): void {
|
|
|
27
27
|
clearTimeout(id as unknown as ReturnType<typeof setTimeout>);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Line and page deltas come from wheel mice; trackpads report pixels.
|
|
31
|
+
const WHEEL_LINE_PIXELS = 16;
|
|
32
|
+
const WHEEL_PAGE_PIXELS = 400;
|
|
33
|
+
|
|
34
|
+
function wheelPixels(delta: number, deltaMode: number): number {
|
|
35
|
+
if (deltaMode === 1) return delta * WHEEL_LINE_PIXELS;
|
|
36
|
+
if (deltaMode === 2) return delta * WHEEL_PAGE_PIXELS;
|
|
37
|
+
return delta;
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function resolveScroll(event: MouseLikeEvent) {
|
|
31
41
|
if (!("deltaX" in event) && !("deltaY" in event)) return undefined;
|
|
32
42
|
const wheel = event as WheelEvent;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
43
|
+
const deltaX = wheelPixels(wheel.deltaX, wheel.deltaMode);
|
|
44
|
+
const deltaY = wheelPixels(wheel.deltaY, wheel.deltaMode);
|
|
45
|
+
const useHorizontal = Math.abs(deltaX) > Math.abs(deltaY);
|
|
46
|
+
const delta = useHorizontal ? deltaX : deltaY;
|
|
35
47
|
return {
|
|
36
48
|
direction: useHorizontal
|
|
37
49
|
? (delta > 0 ? "right" : "left")
|
|
38
50
|
: (delta > 0 ? "down" : "up"),
|
|
39
51
|
delta: Math.abs(delta),
|
|
52
|
+
deltaX,
|
|
53
|
+
deltaY,
|
|
40
54
|
};
|
|
41
55
|
}
|
|
42
56
|
|