@gloom-sh/terminal 0.19.2 → 0.19.3
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 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +108 -1
- package/src/account/upgrade-dialog.tsx +4 -23
- package/src/api-client/coalesce.ts +118 -0
- package/src/api-client/credit-boards.ts +13 -0
- package/src/api-client/crypto-etf-flows.ts +6 -0
- package/src/api-client/crypto-markets.ts +40 -0
- package/src/api-client/data.ts +201 -14
- package/src/api-client/equity-screener.ts +10 -0
- package/src/api-client/errors.ts +20 -0
- package/src/api-client/gpu.ts +29 -0
- package/src/api-client/index.ts +5 -2
- package/src/api-client/plan-access.ts +10 -1
- package/src/api-client/plan-rules.ts +35 -0
- package/src/api-client/research-activity.ts +1 -32
- package/src/api-client/types.ts +21 -1
- package/src/app/attention-notice.tsx +94 -0
- package/src/app.tsx +23 -1
- package/src/architecture/core-builtin-allowlist.json +2 -1
- package/src/capabilities/factories.ts +1 -0
- package/src/cli/ascii-output.ts +42 -0
- package/src/cli/base-converter.ts +10 -2
- package/src/cli/command-options.ts +16 -1
- package/src/cli/commands/crypto-hints.ts +23 -4
- package/src/cli/commands/market.ts +92 -23
- package/src/cli/commands/news-search.ts +24 -5
- package/src/cli/commands/overview.ts +9 -4
- package/src/cli/commands/search.ts +71 -12
- package/src/cli/commands/system.ts +22 -30
- package/src/cli/commands/ticker.ts +49 -9
- package/src/cli/commands/version.ts +33 -0
- package/src/cli/context.ts +5 -9
- package/src/cli/data-dir.ts +7 -0
- package/src/cli/desktop-pane-shot-api-policy.ts +85 -0
- package/src/cli/desktop-pane-shot-browser.ts +320 -0
- package/src/cli/desktop-pane-shot-routes.ts +26 -1
- package/src/cli/desktop-pane-shot.ts +196 -256
- package/src/cli/early-command.ts +42 -0
- package/src/cli/earnings-calendar.ts +9 -0
- package/src/cli/entry.ts +19 -13
- package/src/cli/errors.ts +12 -2
- package/src/cli/help.ts +1 -1
- package/src/cli/history-export.ts +22 -2
- package/src/cli/history-rows.ts +154 -4
- package/src/cli/index.ts +18 -1
- package/src/cli/known-gaps.ts +40 -5
- package/src/cli/listing-arg.ts +46 -9
- package/src/cli/options.ts +1 -1
- package/src/cli/output-file.ts +60 -13
- package/src/cli/pane-functions/catalog.ts +14 -3
- package/src/cli/pane-functions/data.ts +1 -1
- package/src/cli/pane-functions/dom.ts +45 -0
- package/src/cli/pane-functions/freshness.ts +46 -9
- package/src/cli/pane-functions/function-help.ts +17 -9
- package/src/cli/pane-functions/headless.ts +22 -14
- package/src/cli/pane-functions/index.ts +6 -2
- package/src/cli/pane-functions/options.ts +6 -2
- package/src/cli/pane-functions/resolver.ts +27 -5
- package/src/cli/pane-functions/screenshot-assessment.ts +33 -0
- package/src/cli/pane-functions/screenshot-bridge.ts +94 -13
- package/src/cli/pane-functions/screenshot-http.ts +70 -0
- package/src/cli/pane-functions/screenshot-render.ts +19 -5
- package/src/cli/quote-answer.ts +20 -0
- package/src/cli/registry.ts +41 -13
- package/src/cli/result.ts +12 -6
- package/src/cli/ticker-resolution.ts +2 -1
- package/src/cli/types.ts +2 -0
- package/src/components/chart/composite/format.ts +3 -0
- package/src/components/chart-table/header.tsx +1 -1
- package/src/components/chart-table/layout.ts +28 -11
- package/src/components/command-bar/action-runtime.ts +4 -0
- package/src/components/command-bar/commands/direct/index.ts +18 -33
- package/src/components/command-bar/commands/direct/runtime.ts +5 -0
- package/src/components/command-bar/commands/registry.ts +10 -1
- package/src/components/command-bar/routes/root/command-items.ts +9 -4
- package/src/components/command-bar/routes/root/runtime.ts +24 -1
- package/src/components/command-bar/surface/index.tsx +9 -3
- package/src/components/company-logo.tsx +2 -1
- package/src/components/layout/header.tsx +8 -3
- package/src/components/layout/market-summary.ts +25 -7
- package/src/components/layout/pane/footer/index.tsx +7 -2
- package/src/components/layout/pane/footer/model.ts +40 -5
- package/src/components/market-board/index.tsx +3 -2
- package/src/components/onboarding/desks-step.tsx +12 -4
- package/src/components/onboarding/first-run-workspace.ts +81 -4
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-keyboard.ts +8 -5
- package/src/components/onboarding/onboarding-wizard.tsx +54 -23
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +7 -2
- package/src/components/onboarding/run-setup-again.ts +24 -0
- package/src/components/onboarding/use-onboarding-progress.ts +33 -15
- package/src/components/onboarding/use-onboarding-upgrade.ts +2 -6
- package/src/components/onboarding/wizard-model.ts +57 -3
- package/src/components/onboarding/wizard-positions.ts +1 -1
- package/src/components/ui/delayed-mark.tsx +56 -0
- package/src/components/ui/external-link.tsx +3 -6
- package/src/components/ui/icon.tsx +8 -1
- package/src/components/ui/logo-mark.tsx +8 -5
- package/src/components/ui/stat-grid.tsx +263 -19
- package/src/data/config/store/node.ts +11 -5
- package/src/data/config/store/normalize.ts +16 -3
- package/src/data/profile-permissions.ts +47 -0
- package/src/data/resource-store.ts +34 -31
- package/src/data/sqlite/access.ts +19 -8
- package/src/data/sqlite/checkpointer.ts +95 -8
- package/src/data/sqlite/database.ts +40 -7
- package/src/docs/snapshot.json +191 -78
- package/src/i18n/es.ts +18 -2
- package/src/i18n/ja.ts +18 -2
- package/src/i18n/ko.ts +18 -2
- package/src/i18n/zh-cn.ts +18 -2
- package/src/i18n/zh-tw.ts +18 -2
- package/src/layout/desk-keys.ts +4 -1
- package/src/layout/desks.ts +38 -5
- package/src/layout-marketplace/gallery-desktop.tsx +45 -16
- package/src/market-data/batched-reads.ts +16 -0
- package/src/market-data/coordinator/auxiliary.ts +4 -2
- package/src/market-data/coordinator/index.ts +11 -2
- package/src/market-data/futures-board.ts +1 -1
- package/src/market-data/futures-contract.ts +3 -1
- package/src/market-data/market/index-level.ts +6 -4
- package/src/market-data/published-apac-sessions.ts +6 -0
- package/src/news/aggregator.ts +7 -4
- package/src/news/news-model.ts +11 -0
- package/src/news/story-id.ts +22 -0
- package/src/news/ticker-symbols.ts +17 -1
- package/src/notifications/app-notifier.ts +35 -14
- package/src/plugins/builtin/account-management/form-components.tsx +1 -1
- package/src/plugins/builtin/account-management/index.tsx +2 -3
- package/src/plugins/builtin/account-management/model.ts +1 -1
- package/src/plugins/builtin/alerts/cli-forms.ts +15 -1
- package/src/plugins/builtin/alerts/events-pane.tsx +7 -5
- package/src/plugins/builtin/alerts/events.ts +10 -2
- package/src/plugins/builtin/alerts/index.tsx +5 -5
- package/src/plugins/builtin/alerts/research-builder.ts +5 -1
- package/src/plugins/builtin/alerts/research-rules.ts +18 -0
- package/src/plugins/builtin/alerts/targets.ts +4 -1
- package/src/plugins/builtin/attention/headless.ts +1 -1
- package/src/plugins/builtin/attention/index.tsx +2 -4
- package/src/plugins/builtin/attention/listing.ts +1 -1
- package/src/plugins/builtin/attention/series.ts +1 -1
- package/src/plugins/builtin/awards/index.tsx +1 -2
- package/src/plugins/builtin/backtest/client.ts +1 -0
- package/src/plugins/builtin/backtest/index.tsx +1 -2
- package/src/plugins/builtin/backtest/rules.ts +3 -1
- package/src/plugins/builtin/backtest/window.ts +8 -7
- package/src/plugins/builtin/broker-manager/detail.tsx +16 -3
- package/src/plugins/builtin/broker-manager/index.tsx +1 -1
- package/src/plugins/builtin/buildout/cells.tsx +5 -0
- package/src/plugins/builtin/buildout/company/flow.tsx +367 -0
- package/src/plugins/builtin/buildout/company/id.ts +2 -0
- package/src/plugins/builtin/buildout/company/model.ts +247 -0
- package/src/plugins/builtin/buildout/company/pane.tsx +317 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +31 -10
- package/src/plugins/builtin/buildout/facilities.ts +309 -0
- package/src/plugins/builtin/buildout/format.ts +9 -0
- package/src/plugins/builtin/buildout/index.tsx +51 -5
- package/src/plugins/builtin/buildout/model/index.ts +62 -10
- package/src/plugins/builtin/buildout/model/normalizers.ts +5 -0
- package/src/plugins/builtin/buildout/model/types.ts +11 -2
- package/src/plugins/builtin/buildout/pane/index.tsx +91 -20
- package/src/plugins/builtin/buildout/pane/status.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/themes.tsx +110 -0
- package/src/plugins/builtin/buildout/table-model.ts +26 -2
- package/src/plugins/builtin/builtin-plugin-meta.ts +16 -0
- package/src/plugins/builtin/cds/headless.ts +10 -2
- package/src/plugins/builtin/cds/model.ts +7 -3
- package/src/plugins/builtin/central-bank-rates/index.tsx +1 -2
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +3 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +7 -2
- package/src/plugins/builtin/chart-composer/headless.ts +88 -21
- package/src/plugins/builtin/chart-composer/index.tsx +2 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +26 -2
- package/src/plugins/builtin/chart-composer/series-expression.ts +3 -2
- package/src/plugins/builtin/chart-composer/studies.ts +1 -1
- package/src/plugins/builtin/chat/controller/index.ts +1 -1
- package/src/plugins/builtin/chat/controller/messages.ts +15 -4
- package/src/plugins/builtin/chat/ghost-user.ts +8 -0
- package/src/plugins/builtin/chat/index.tsx +5 -5
- package/src/plugins/builtin/clinical-trials/index.tsx +1 -2
- package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
- package/src/plugins/builtin/cloud/index.tsx +1 -2
- package/src/plugins/builtin/cloud/plugin.tsx +6 -4
- package/src/plugins/builtin/cloud/team/module.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/module.tsx +1 -1
- package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +24 -1
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +39 -15
- package/src/plugins/builtin/comment-letters/index.tsx +1 -2
- package/src/plugins/builtin/commodity-balances/index.tsx +3 -3
- package/src/plugins/builtin/commodity-balances/pane.tsx +2 -2
- package/src/plugins/builtin/congress-trades/aggregates.ts +13 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +20 -4
- package/src/plugins/builtin/congress-trades/headless.ts +2 -0
- package/src/plugins/builtin/congress-trades/model.ts +92 -64
- package/src/plugins/builtin/congress-trades/table.tsx +14 -2
- package/src/plugins/builtin/connections/index.ts +2 -3
- package/src/plugins/builtin/cot/index.tsx +1 -2
- package/src/plugins/builtin/cpi/index.tsx +1 -2
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/client.ts +7 -2
- package/src/plugins/builtin/credit-boards/headless.ts +25 -12
- package/src/plugins/builtin/credit-boards/model.ts +29 -0
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +9 -4
- package/src/plugins/builtin/crypto-board/bitcoin-network.ts +143 -0
- package/src/plugins/builtin/crypto-board/headless.ts +4 -3
- package/src/plugins/builtin/crypto-board/index.tsx +4 -2
- package/src/plugins/builtin/crypto-board/series.ts +2 -0
- package/src/plugins/builtin/crypto-etf-flows/client.ts +9 -2
- package/src/plugins/builtin/crypto-etf-flows/headless.ts +61 -6
- package/src/plugins/builtin/crypto-etf-flows/index.tsx +1 -2
- package/src/plugins/builtin/crypto-etf-flows/model.ts +14 -1
- package/src/plugins/builtin/crypto-etf-flows/pane.tsx +8 -5
- package/src/plugins/builtin/debt-maturities/headless.ts +4 -3
- package/src/plugins/builtin/debt-maturities/model.ts +17 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +18 -4
- package/src/plugins/builtin/debug/index.tsx +1 -1
- package/src/plugins/builtin/dividend-yield/client.ts +32 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +58 -33
- package/src/plugins/builtin/dividend-yield/index.tsx +37 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +32 -13
- package/src/plugins/builtin/dividend-yield/ranking-reads.ts +73 -0
- package/src/plugins/builtin/dividend-yield/ranking-view.tsx +163 -0
- package/src/plugins/builtin/dividend-yield/ranking.ts +1 -1
- package/src/plugins/builtin/dividend-yield/view.ts +21 -0
- package/src/plugins/builtin/docs/index.tsx +2 -3
- package/src/plugins/builtin/doe/headless.ts +16 -3
- package/src/plugins/builtin/doe/index.tsx +5 -4
- package/src/plugins/builtin/earnings/board.tsx +2 -1
- package/src/plugins/builtin/earnings/history.tsx +2 -1
- package/src/plugins/builtin/earnings-ripple/client.ts +32 -7
- package/src/plugins/builtin/earnings-ripple/index.tsx +1 -2
- package/src/plugins/builtin/econ/calendar-model.ts +29 -6
- package/src/plugins/builtin/econ-statistics/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +1 -0
- package/src/plugins/builtin/equity-screener/ideas.ts +30 -1
- package/src/plugins/builtin/equity-screener/index.tsx +1 -2
- package/src/plugins/builtin/equity-screener/model.ts +28 -4
- package/src/plugins/builtin/equity-screener/pane.tsx +5 -0
- package/src/plugins/builtin/equity-screener/screen-check.ts +23 -1
- package/src/plugins/builtin/estimate-revisions/index.tsx +1 -2
- package/src/plugins/builtin/exposure/holdings.ts +35 -13
- package/src/plugins/builtin/exposure/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/cli.ts +44 -0
- package/src/plugins/builtin/fear-greed/index.tsx +5 -53
- package/src/plugins/builtin/fear-greed/pane.tsx +2 -1
- package/src/plugins/builtin/fixed-income/index.tsx +1 -2
- package/src/plugins/builtin/futures/board-reads.ts +25 -0
- package/src/plugins/builtin/futures/coalesced-bump.ts +44 -0
- package/src/plugins/builtin/futures/index.tsx +15 -0
- package/src/plugins/builtin/futures/model.ts +14 -0
- package/src/plugins/builtin/futures/use-board-terms.ts +22 -15
- package/src/plugins/builtin/futures/use-front-returns.ts +20 -14
- package/src/plugins/builtin/futures-curve/headless.ts +6 -1
- package/src/plugins/builtin/futures-curve/index.tsx +1 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/gpu/board.tsx +51 -12
- package/src/plugins/builtin/gpu/client.ts +15 -2
- package/src/plugins/builtin/gpu/equities.tsx +5 -2
- package/src/plugins/builtin/gpu/headless.ts +14 -2
- package/src/plugins/builtin/gpu/index.tsx +1 -2
- package/src/plugins/builtin/gpu/model.ts +20 -2
- package/src/plugins/builtin/gpu/pane.tsx +1 -1
- package/src/plugins/builtin/help/index.tsx +1 -1
- package/src/plugins/builtin/holders/index-holders.ts +24 -12
- package/src/plugins/builtin/holders/index.tsx +2 -3
- package/src/plugins/builtin/ipo-calendar/index.tsx +1 -2
- package/src/plugins/builtin/iv-history/index.tsx +1 -2
- package/src/plugins/builtin/jodi/index.tsx +1 -2
- package/src/plugins/builtin/kelly-sizer/index.tsx +1 -2
- package/src/plugins/builtin/layout-manager/index.tsx +9 -8
- package/src/plugins/builtin/macro-day/pane.tsx +4 -2
- package/src/plugins/builtin/market-halts/index.tsx +1 -2
- package/src/plugins/builtin/market-heatmap/index.tsx +11 -2
- package/src/plugins/builtin/market-movers/index.tsx +7 -1
- package/src/plugins/builtin/market-movers/session-body.tsx +4 -1
- package/src/plugins/builtin/market-valuation/index.tsx +1 -2
- package/src/plugins/builtin/members/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/index.tsx +1 -2
- package/src/plugins/builtin/money-markets/pane.tsx +2 -1
- package/src/plugins/builtin/news/headless.ts +7 -0
- package/src/plugins/builtin/news/wire/index.tsx +1 -1
- package/src/plugins/builtin/news/wire/news/pop-out.tsx +9 -3
- package/src/plugins/builtin/notes/index.tsx +3 -6
- package/src/plugins/builtin/openfda/index.tsx +1 -2
- package/src/plugins/builtin/options/view.tsx +6 -5
- package/src/plugins/builtin/options-calculator/index.tsx +1 -2
- package/src/plugins/builtin/options-positioning/index.tsx +1 -2
- package/src/plugins/builtin/options-scenario/index.tsx +2 -4
- package/src/plugins/builtin/perps/index.tsx +1 -2
- package/src/plugins/builtin/plugin-marketplace/index.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +2 -1
- package/src/plugins/builtin/plugin-module.ts +16 -2
- package/src/plugins/builtin/portfolio-list/cli/render.ts +53 -3
- package/src/plugins/builtin/portfolio-list/index.tsx +1 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +11 -0
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -2
- package/src/plugins/builtin/power/index.tsx +1 -2
- package/src/plugins/builtin/rate-path/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/headless.ts +2 -0
- package/src/plugins/builtin/realized-vol/index.tsx +1 -2
- package/src/plugins/builtin/realized-vol/model.ts +16 -2
- package/src/plugins/builtin/realized-vol/pane.tsx +3 -3
- package/src/plugins/builtin/relative-rotation/index.tsx +1 -2
- package/src/plugins/builtin/research/analyst-headless.ts +33 -6
- package/src/plugins/builtin/research/analyst-model.ts +37 -2
- package/src/plugins/builtin/research/analyst-pane.tsx +25 -5
- package/src/plugins/builtin/research/index.tsx +5 -8
- package/src/plugins/builtin/research-search/index.tsx +1 -2
- package/src/plugins/builtin/revenue-breakdown/headless.ts +22 -8
- package/src/plugins/builtin/revenue-breakdown/model.ts +23 -0
- package/src/plugins/builtin/reverse-dcf/pane.tsx +4 -2
- package/src/plugins/builtin/roadmap/index.tsx +2 -3
- package/src/plugins/builtin/roadmap/pane.tsx +49 -2
- package/src/plugins/builtin/scanner/hilo-model.ts +18 -1
- package/src/plugins/builtin/scanner/hilo-pane.tsx +12 -2
- package/src/plugins/builtin/scanner/live-pane.tsx +7 -4
- package/src/plugins/builtin/sec/filing-content.ts +38 -6
- package/src/plugins/builtin/sec/headless.ts +5 -1
- package/src/plugins/builtin/sec/index.tsx +4 -2
- package/src/plugins/builtin/sec/model.ts +2 -0
- package/src/plugins/builtin/sectors/index.tsx +11 -5
- package/src/plugins/builtin/series-inventory.ts +2 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +60 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +24 -1
- package/src/plugins/builtin/shared/forming-bars.ts +29 -0
- package/src/plugins/builtin/shared/fx-rate-freshness.ts +17 -0
- package/src/plugins/builtin/shared/series-detail-chart.tsx +4 -2
- package/src/plugins/builtin/shared/use-quote-board.ts +5 -0
- package/src/plugins/builtin/short-interest/publications-pane.tsx +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +2 -1
- package/src/plugins/builtin/social-mentions/index.tsx +1 -2
- package/src/plugins/builtin/social-mentions/pane.tsx +2 -1
- package/src/plugins/builtin/supply-chain/client.ts +22 -1
- package/src/plugins/builtin/supply-chain/graph-client.ts +22 -1
- package/src/plugins/builtin/supply-chain/index.tsx +1 -2
- package/src/plugins/builtin/thirteenf/api.ts +116 -13
- package/src/plugins/builtin/thirteenf/data.ts +132 -30
- package/src/plugins/builtin/thirteenf/index.tsx +2 -4
- package/src/plugins/builtin/thirteenf/model.ts +2 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +20 -11
- package/src/plugins/builtin/thirteenf/types.ts +12 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +17 -4
- package/src/plugins/builtin/ticker-detail/data-panes/returns-history.ts +81 -11
- package/src/plugins/builtin/ticker-detail/headless.ts +11 -3
- package/src/plugins/builtin/ticker-detail/index.tsx +5 -10
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +16 -6
- package/src/plugins/builtin/time-sales/index.tsx +1 -2
- package/src/plugins/builtin/time-sales/pane.tsx +3 -1
- package/src/plugins/builtin/trace-bonds/headless.ts +11 -2
- package/src/plugins/builtin/trace-bonds/model.ts +44 -10
- package/src/plugins/builtin/trace-bonds/pane.tsx +12 -7
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +18 -3
- package/src/plugins/builtin/vol-surface/index.tsx +1 -2
- package/src/plugins/builtin/volatility/index.tsx +1 -2
- package/src/plugins/builtin/world-indices/footer.ts +6 -1
- package/src/plugins/builtin/world-venue-map/client.ts +10 -1
- package/src/plugins/builtin/world-venue-map/index.ts +6 -2
- package/src/plugins/builtin/world-venue-map/layered-pane.tsx +48 -7
- package/src/plugins/builtin/world-venue-map/layers.ts +17 -0
- package/src/plugins/builtin/world-venue-map/local-layers.ts +141 -0
- package/src/plugins/builtin/world-venue-map/map-symbols.ts +18 -0
- package/src/plugins/builtin/world-venue-map/map.tsx +4 -2
- package/src/plugins/builtin/world-venue-map/scope.ts +63 -0
- package/src/plugins/builtin/yield-curve/curves.ts +16 -2
- package/src/plugins/builtin/yield-curve/headless.ts +3 -3
- package/src/plugins/bundle.ts +52 -2
- package/src/plugins/catalog-browser.ts +2 -2
- package/src/plugins/catalog-cli.ts +20 -0
- package/src/plugins/host-link.ts +13 -1
- package/src/plugins/installer.ts +15 -4
- package/src/plugins/loader.ts +43 -11
- package/src/plugins/peer-imports.ts +201 -0
- package/src/plugins/peer-retry.ts +26 -29
- package/src/plugins/plugin-process.ts +128 -0
- package/src/plugins/redirection-guard.ts +45 -0
- package/src/remote/endpoint.ts +16 -3
- package/src/renderers/browser/main.tsx +3 -2
- package/src/renderers/cloudflare/http-proxy.ts +7 -123
- package/src/renderers/dom/desktop/icons.tsx +2 -0
- package/src/renderers/dom/host/image-surface.tsx +46 -21
- package/src/renderers/dom/styles.css +7 -0
- package/src/renderers/dom/toast-host.tsx +2 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/rpc-timing.ts +88 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +20 -4
- package/src/renderers/electrobun/bun/desktop/view-ids.ts +8 -4
- package/src/renderers/electrobun/bun/external-plugins.ts +4 -0
- package/src/renderers/electrobun/bun/window/create.ts +4 -2
- package/src/renderers/electrobun/bun/window/native-timing.ts +48 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +82 -4
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +13 -5
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +3 -0
- package/src/renderers/electrobun/view/main.tsx +7 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/renderers/opentui/browser-open.ts +10 -4
- package/src/renderers/opentui/image/surface.tsx +1 -1
- package/src/renderers/opentui/start.tsx +12 -6
- package/src/renderers/opentui/toast-host.tsx +2 -1
- package/src/series/registry.ts +18 -0
- package/src/series/standalone.ts +10 -1
- package/src/series/types.ts +7 -0
- package/src/series/validate.ts +11 -2
- package/src/sources/gloomberb-cloud/index.ts +28 -5
- package/src/sources/gloomberb-cloud/news.ts +24 -3
- package/src/sources/gloomberb-cloud/normalizers.ts +17 -2
- package/src/sources/provider-errors.ts +26 -3
- package/src/sources/provider-router/history.ts +5 -0
- package/src/sources/provider-router/index.ts +13 -0
- package/src/sync/core-contributors.ts +32 -0
- package/src/telemetry/attention-counts.ts +64 -6
- package/src/telemetry/attention-setting.ts +33 -0
- package/src/telemetry/crash-reports-node.ts +3 -1
- package/src/telemetry/stall-clocks.ts +93 -0
- package/src/telemetry/stall-reports-node.ts +12 -7
- package/src/telemetry/stall-reports.ts +34 -5
- package/src/test-support/preload.ts +9 -0
- package/src/tickers/company-name.ts +19 -0
- package/src/tickers/listing-fate.ts +14 -3
- package/src/tickers/saved-instrument.ts +17 -0
- package/src/tickers/search/home-listing.ts +13 -2
- package/src/tickers/search/index.ts +4 -1
- package/src/tickers/search/types.ts +6 -1
- package/src/time-series/bar-aggregation.ts +7 -1
- package/src/time-series/base-series.ts +3 -3
- package/src/time-series/chart-bounds.ts +1 -1
- package/src/time-series/chart-data.ts +5 -2
- package/src/time-series/chart-seed.ts +1 -1
- package/src/time-series/coin-session-close.ts +211 -0
- package/src/time-series/live-bars.ts +13 -2
- package/src/time-series/live-quotes.ts +1 -1
- package/src/time-series/official-close.ts +159 -0
- package/src/time-series/price-comparison.ts +55 -6
- package/src/time-series/resolve.ts +12 -3
- package/src/time-series/series-presentation.ts +1 -1
- package/src/time-series/studies/formula.ts +205 -0
- package/src/time-series/studies/indicators.ts +299 -0
- package/src/time-series/studies/leverage.ts +75 -0
- package/src/time-series/studies/pair.ts +138 -0
- package/src/time-series/studies/resolve.ts +333 -0
- package/src/time-series/studies/shared.ts +141 -0
- package/src/time-series/studies/units.ts +243 -0
- package/src/time-series/studies/vwap.ts +274 -0
- package/src/types/config.ts +18 -2
- package/src/types/data-provider.ts +23 -0
- package/src/types/financials.ts +22 -0
- package/src/types/headless.ts +42 -3
- package/src/types/plugin.ts +32 -3
- package/src/ui/host.tsx +11 -0
- package/src/ui/toast.tsx +26 -1
- package/src/utils/ascii-text.ts +140 -0
- package/src/utils/cli-output.ts +91 -18
- package/src/utils/concurrency.ts +24 -0
- package/src/utils/exchanges.ts +6 -0
- package/src/utils/external-url.ts +20 -7
- package/src/utils/futures-generic.ts +4 -4
- package/src/utils/http-proxy-upstream.ts +124 -0
- package/src/utils/main-thread-stall.ts +75 -3
- package/src/utils/perf-marks.ts +36 -2
- package/src/utils/private-files.ts +141 -0
- package/src/utils/public-address.ts +99 -0
- package/src/account/upgrade-personalization.ts +0 -57
- package/src/account/upgrade-tickers.ts +0 -124
- package/src/api-client/upgrade-experiment.ts +0 -91
- package/src/plugins/builtin/cloud/browser.tsx +0 -3
- package/src/renderers/electrobun/launcher/preload.ts +0 -13
- package/src/renderers/electrobun/launcher/windows-emulation.ts +0 -50
- package/src/time-series/studies.ts +0 -1628
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
RESEARCH_SEARCH_PANE_ID,
|
|
8
8
|
RESEARCH_SEARCH_TEMPLATE_ID,
|
|
9
9
|
} from "./model";
|
|
10
|
-
import { ResearchSearchPane } from "./pane";
|
|
11
10
|
|
|
12
11
|
const description =
|
|
13
12
|
"Full-text search across earnings call transcripts, news wires, and SEC filings, with saved searches and keyword alerts.";
|
|
@@ -40,7 +39,7 @@ export const researchSearchPlugin: GloomPlugin = {
|
|
|
40
39
|
reportFreshness: { status: "not-a-feed", basis: "search results" },
|
|
41
40
|
name: "Research Search",
|
|
42
41
|
icon: "RS",
|
|
43
|
-
component: ResearchSearchPane,
|
|
42
|
+
get component(): typeof import("./pane").ResearchSearchPane { const { ResearchSearchPane } = require("./pane"); return ResearchSearchPane; },
|
|
44
43
|
defaultPosition: "right",
|
|
45
44
|
defaultMode: "floating",
|
|
46
45
|
defaultFloatingSize: { width: 118, height: 32 },
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import type { RevenueBreakdownView } from "../../../api-client/revenue-breakdown";
|
|
2
2
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
3
3
|
import { fetchRevenueBreakdown } from "./client";
|
|
4
|
-
import { growthPercent, quarterLabel, reportedSpan,
|
|
4
|
+
import { growthPercent, quarterLabel, reportedSpan, revenueScale, sharePercent } from "./model";
|
|
5
5
|
import { SEC_FILINGS } from "../shared/report-freshness";
|
|
6
6
|
import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** Shares and growth in percent, as the pane prints them (83.7%, +10.8%), amounts on the table's scale; exports keep the whole number. */
|
|
9
9
|
const number = (format: (value: number | null) => string) => (value: unknown) => format(typeof value === "number" ? value : null);
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A table's amounts on its one scale (`revenueScale`), named once in the text
|
|
13
|
+
* report's heading ("Total revenue, USD millions"). Exports keep the section
|
|
14
|
+
* name and the whole number.
|
|
15
|
+
*/
|
|
16
|
+
function amountScale(values: ReadonlyArray<number | null>, currency: string | null) {
|
|
17
|
+
const scale = revenueScale(values, currency);
|
|
18
|
+
return {
|
|
19
|
+
section: (title: string) => ({ title, ...scale.unit ? { textTitle: `${title}, ${scale.unit}` } : {} }),
|
|
20
|
+
column: (key: string, header: string): HeadlessPaneColumn => ({ key, header, align: "right", textFormat: number(scale.format) }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
13
25
|
shape: "bundle",
|
|
@@ -46,16 +58,18 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
46
58
|
ctx.apiClient,
|
|
47
59
|
await resolveHeadlessIssuerListing(ctx, symbol),
|
|
48
60
|
));
|
|
61
|
+
const rowScale = amountScale(data.rows.flatMap((row) => [...row.values, row.ttm]), data.currency);
|
|
62
|
+
const totalScale = amountScale(data.total, data.currency);
|
|
49
63
|
return {
|
|
50
64
|
complete: data.access === "full",
|
|
51
65
|
errors: data.lockedRows > 0 ? [`${data.lockedRows} more rows need Gloom Pro`] : [],
|
|
52
66
|
sections: [
|
|
53
67
|
{
|
|
54
|
-
|
|
68
|
+
...rowScale.section(`Quarterly revenue by ${data.view}`),
|
|
55
69
|
columns: [
|
|
56
70
|
{ key: data.view, header: data.view[0]!.toUpperCase() + data.view.slice(1) },
|
|
57
|
-
...data.periods.map((period) =>
|
|
58
|
-
|
|
71
|
+
...data.periods.map((period) => rowScale.column(quarterLabel(period), quarterLabel(period))),
|
|
72
|
+
rowScale.column("ttm", "TTM"),
|
|
59
73
|
{ key: "share", header: "Share", align: "right", textFormat: number(sharePercent) },
|
|
60
74
|
{ key: "yoy", header: "YoY", align: "right", textFormat: number(growthPercent) },
|
|
61
75
|
],
|
|
@@ -68,8 +82,8 @@ export const revenueBreakdownHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
68
82
|
})),
|
|
69
83
|
},
|
|
70
84
|
{
|
|
71
|
-
|
|
72
|
-
columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" },
|
|
85
|
+
...totalScale.section("Total revenue"),
|
|
86
|
+
columns: [{ key: "quarter", header: "Quarter" }, { key: "end", header: "End" }, totalScale.column("revenue", "Revenue")],
|
|
73
87
|
rows: data.periods.map((period, index) => ({
|
|
74
88
|
quarter: quarterLabel(period),
|
|
75
89
|
end: period.end,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DataTableColumn } from "../../../components";
|
|
2
|
+
import { pickUnit } from "../../../utils/format";
|
|
2
3
|
import type {
|
|
3
4
|
RevenueBreakdownPayload,
|
|
4
5
|
RevenueBreakdownPeriod,
|
|
@@ -83,6 +84,28 @@ export function revenueAmount(value: number | null): string {
|
|
|
83
84
|
return `${value < 0 ? "-" : ""}${scaled.toFixed(digits)}${suffix}`;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
const SCALE_WORDS: Record<string, string> = { K: "thousands", M: "millions", B: "billions", T: "trillions" };
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* One scale for a report table's amounts, its largest one's, so a 0.0 never sits beside a 5.52M: `unit`
|
|
91
|
+
* names it once ("USD millions") and `format` prints each amount in it, with one count of decimals down the
|
|
92
|
+
* table (179.3 and 0.0, or 54.32 and 7.88). An amount too small for that many decimals keeps two significant
|
|
93
|
+
* digits (0.030), so only a true zero reads 0.0. The pane keeps each amount's own suffix (`revenueAmount`).
|
|
94
|
+
*/
|
|
95
|
+
export function revenueScale(values: ReadonlyArray<number | null>, currency: string | null): { unit: string; format: (value: number | null) => string } {
|
|
96
|
+
const amounts = values.filter((value): value is number => value !== null && Number.isFinite(value));
|
|
97
|
+
const { suffix, divisor } = pickUnit(amounts);
|
|
98
|
+
const digits = Math.max(0, ...amounts.map((value) => Math.abs(value) / divisor)) >= 100 ? 1 : 2;
|
|
99
|
+
const format = (value: number | null) => {
|
|
100
|
+
if (value === null || !Number.isFinite(value)) return revenueAmount(null);
|
|
101
|
+
const scaled = value / divisor;
|
|
102
|
+
if (scaled === 0 || Number(scaled.toFixed(digits)) !== 0) return scaled.toFixed(digits);
|
|
103
|
+
// Two significant digits: as many decimals as the first one's place, and one more.
|
|
104
|
+
return scaled.toFixed(Math.min(20, 1 - Math.floor(Math.log10(Math.abs(scaled)))));
|
|
105
|
+
};
|
|
106
|
+
return { unit: [currency?.trim(), SCALE_WORDS[suffix]].filter(Boolean).join(" "), format };
|
|
107
|
+
}
|
|
108
|
+
|
|
86
109
|
export const sharePercent = (value: number | null) =>
|
|
87
110
|
value === null ? "--" : `${(value * 100).toFixed(1)}%`;
|
|
88
111
|
|
|
@@ -110,7 +110,9 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
|
|
|
110
110
|
}, [model]);
|
|
111
111
|
|
|
112
112
|
const selectedId = String(discountRate);
|
|
113
|
-
|
|
113
|
+
// A figure wraps only into rows the table's least leaves, so the pane never spills.
|
|
114
|
+
const statMaxLines = height - 1 - 4;
|
|
115
|
+
const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width, undefined, { maxLines: statMaxLines }));
|
|
114
116
|
|
|
115
117
|
return <Box width={width} height={height} flexDirection="column" overflow="hidden">
|
|
116
118
|
<QueryBar width={width} filters={[
|
|
@@ -120,7 +122,7 @@ export function ReverseDcfPane({ width, height, focused }: PaneProps) {
|
|
|
120
122
|
{!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="reverse DCF" loading={inputs.loading && !model}
|
|
121
123
|
error={!model ? inputs.error ?? identityError ?? null : model.error} empty={false}>
|
|
122
124
|
{model && !model.error ? <>
|
|
123
|
-
<StatGrid items={stats} width={width} />
|
|
125
|
+
<StatGrid items={stats} width={width} maxLines={statMaxLines} />
|
|
124
126
|
<DataTableView<Row> focused={focused} columns={COLUMNS} items={model.sensitivity} rootWidth={width} rootHeight={contentHeight}
|
|
125
127
|
getItemKey={(row) => String(row.discountRate)} emptyStateTitle="No implied growth." sortColumnId={null} sortDirection="desc"
|
|
126
128
|
selection={{ kind: "id", selectedId, getId: (row) => String(row.discountRate),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
|
-
import { RoadmapPane } from "./pane";
|
|
3
2
|
|
|
4
3
|
export const roadmapModule: PluginModule = {
|
|
5
4
|
panes: [
|
|
@@ -7,8 +6,8 @@ export const roadmapModule: PluginModule = {
|
|
|
7
6
|
id: "roadmap",
|
|
8
7
|
name: "Roadmap",
|
|
9
8
|
icon: "R",
|
|
10
|
-
component: RoadmapPane,
|
|
11
|
-
reportFreshness: { status: "not-a-feed", basis: "curated list" },
|
|
9
|
+
get component(): typeof import("./pane").RoadmapPane { const { RoadmapPane } = require("./pane"); return RoadmapPane; },
|
|
10
|
+
reportFreshness: { status: "not-a-feed", basis: "curated list", local: true },
|
|
12
11
|
defaultPosition: "right",
|
|
13
12
|
defaultMode: "floating",
|
|
14
13
|
defaultFloatingSize: { width: 100, height: 30 },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
1
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, type RefObject } from "react";
|
|
2
2
|
import { apiClient } from "../../../api-client";
|
|
3
3
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
4
4
|
import type { RoadmapItem, RoadmapVote } from "../../../api-client/roadmap";
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
DataTableStackView,
|
|
8
8
|
EMPTY_TABLE_CELL,
|
|
9
9
|
isSectionedItemRow,
|
|
10
|
+
PaneFooterScope,
|
|
10
11
|
renderSectionedRowHeader,
|
|
12
|
+
usePaneTabs,
|
|
11
13
|
usePaneStatusFooter,
|
|
12
14
|
useRowNotes,
|
|
13
15
|
type DataTableCell,
|
|
@@ -23,6 +25,7 @@ import { useThemeColors } from "../../../theme/theme-context";
|
|
|
23
25
|
import type { PaneProps } from "../../../types/plugin";
|
|
24
26
|
import { Box, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
25
27
|
import { formatShortDate } from "../../../utils/datetime-format";
|
|
28
|
+
import { ChangelogPane } from "../changelog";
|
|
26
29
|
import {
|
|
27
30
|
isVotable,
|
|
28
31
|
optimisticVote,
|
|
@@ -97,7 +100,7 @@ function RoadmapDetail({ item, failure, width, scrollRef }: {
|
|
|
97
100
|
);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
function RoadmapView({ focused, width, height }: PaneProps) {
|
|
101
104
|
const colors = useThemeColors();
|
|
102
105
|
const client = useContext(RoadmapClientContext) ?? apiClient;
|
|
103
106
|
const access = usePlanAccess();
|
|
@@ -242,3 +245,47 @@ export function RoadmapPane({ focused, width, height }: PaneProps) {
|
|
|
242
245
|
/>
|
|
243
246
|
);
|
|
244
247
|
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* What is coming and what came: the roadmap's features, and the Changelog as
|
|
251
|
+
* CHG shows it. The first tab is not "Roadmap", which the title already says.
|
|
252
|
+
*/
|
|
253
|
+
const VIEWS = [
|
|
254
|
+
{ value: "roadmap", label: "Features" },
|
|
255
|
+
{ value: "changelog", label: "Changelog" },
|
|
256
|
+
] as const;
|
|
257
|
+
type ViewId = (typeof VIEWS)[number]["value"];
|
|
258
|
+
const VIEW_PANES: Record<ViewId, (props: PaneProps) => ReactNode> = {
|
|
259
|
+
roadmap: RoadmapView,
|
|
260
|
+
changelog: ChangelogPane,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export function RoadmapPane(props: PaneProps) {
|
|
264
|
+
const [storedView, setView] = usePluginPaneState<string>("view", "roadmap");
|
|
265
|
+
const view: ViewId = storedView === "changelog" ? "changelog" : "roadmap";
|
|
266
|
+
// A view stays mounted once shown, so coming back to it keeps its place.
|
|
267
|
+
const [mounted, setMounted] = useState<ReadonlySet<ViewId>>(() => new Set([view]));
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
setMounted((current) => (current.has(view) ? current : new Set([...current, view])));
|
|
270
|
+
}, [view]);
|
|
271
|
+
const { strip, rows } = usePaneTabs({ tabs: [...VIEWS], activeValue: view, onSelect: setView, focused: props.focused, dense: true });
|
|
272
|
+
const bodyHeight = Math.max(1, props.height - rows);
|
|
273
|
+
|
|
274
|
+
return (
|
|
275
|
+
<Box width={props.width} height={props.height} flexDirection="column" overflow="hidden">
|
|
276
|
+
{strip}
|
|
277
|
+
{VIEWS.map(({ value }) => {
|
|
278
|
+
if (!mounted.has(value) && value !== view) return null;
|
|
279
|
+
const View = VIEW_PANES[value];
|
|
280
|
+
const active = value === view;
|
|
281
|
+
return (
|
|
282
|
+
<Box key={value} visible={active} height={bodyHeight} flexGrow={1} flexBasis={0} overflow="hidden" data-gloom-role="nested-pane-content">
|
|
283
|
+
<PaneFooterScope active={active}>
|
|
284
|
+
<View {...props} height={bodyHeight} focused={props.focused && active} />
|
|
285
|
+
</PaneFooterScope>
|
|
286
|
+
</Box>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</Box>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ScannerHiloExtreme, ScannerHiloPayload } from "../../../api-client";
|
|
1
|
+
import type { ScannerHiloExtreme, ScannerHiloPayload, ScannerStatus } from "../../../api-client";
|
|
2
|
+
import { venueMarketLine } from "../../../cli/pane-functions/freshness";
|
|
2
3
|
|
|
3
4
|
type HiloWindowKey = keyof ScannerHiloPayload["windows"];
|
|
4
5
|
export type HiloMinPrice = "off" | "1" | "5";
|
|
@@ -94,3 +95,19 @@ export function filterHiloRows(
|
|
|
94
95
|
? [...filtered].sort((left, right) => right.count - left.count || right.at - left.at)
|
|
95
96
|
: [...filtered].sort((left, right) => right.at - left.at);
|
|
96
97
|
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* What an empty table says. The price filter when rows came in and it hid them all, or when the market is open and
|
|
101
|
+
* nothing has printed a new extreme yet. A closed market says so and when it reopens, in the status line's words
|
|
102
|
+
* (`Markets closed until Mon.`), as SCAN says its own close: its session has no new highs or lows to count.
|
|
103
|
+
*/
|
|
104
|
+
export function hiloEmptyState(
|
|
105
|
+
{ filtered, status, now }: { filtered: boolean; status: ScannerStatus | undefined; now: number },
|
|
106
|
+
): { title: string; hint?: string } {
|
|
107
|
+
if (filtered || status !== "closed") return { title: "Nothing above the price filter yet." };
|
|
108
|
+
const line = venueMarketLine("NYSE", now);
|
|
109
|
+
return {
|
|
110
|
+
title: line?.state === "closed" ? `${line.text[0]!.toUpperCase()}${line.text.slice(1)}.` : "Market closed.",
|
|
111
|
+
hint: "New highs and lows resume at the open.",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -18,7 +18,7 @@ import { usePluginTickerActions } from "../../runtime";
|
|
|
18
18
|
import { ScannerDeniedState } from "./denied";
|
|
19
19
|
import { useHiloFeed, useScannerStatusFooter } from "./feed";
|
|
20
20
|
import { HiloBars } from "./hilo-bars";
|
|
21
|
-
import { filterHiloRows, formatHiloPrice, type HiloMinPrice, type HiloSort } from "./hilo-model";
|
|
21
|
+
import { filterHiloRows, formatHiloPrice, hiloEmptyState, type HiloMinPrice, type HiloSort } from "./hilo-model";
|
|
22
22
|
|
|
23
23
|
type Side = "lows" | "highs";
|
|
24
24
|
|
|
@@ -118,6 +118,15 @@ function HiloPane({ focused, width, height }: PaneProps) {
|
|
|
118
118
|
);
|
|
119
119
|
|
|
120
120
|
useScannerStatusFooter("hilo", feed, focused);
|
|
121
|
+
// Read when the feed moves, not each render: the clock only matters once a closed session's text is shown.
|
|
122
|
+
const status = feed.payload?.status;
|
|
123
|
+
const emptyStates = useMemo<Record<Side, { title: string; hint?: string }>>(() => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
return {
|
|
126
|
+
lows: hiloEmptyState({ filtered: !!feed.payload?.lows.length, status, now }),
|
|
127
|
+
highs: hiloEmptyState({ filtered: !!feed.payload?.highs.length, status, now }),
|
|
128
|
+
};
|
|
129
|
+
}, [feed.payload?.lows.length, feed.payload?.highs.length, status]);
|
|
121
130
|
|
|
122
131
|
const split = width >= SPLIT_MIN_WIDTH;
|
|
123
132
|
// Zero counts before the first payload would read as a dead market, not a loading one.
|
|
@@ -168,7 +177,8 @@ function HiloPane({ focused, width, height }: PaneProps) {
|
|
|
168
177
|
renderCell={RENDER_ROW[side]}
|
|
169
178
|
selectedTextOverridesCellColor
|
|
170
179
|
emptyContent={feed.payload ? undefined : <PaneStatusBody loading loadingLabel="Waiting for the scanner..." />}
|
|
171
|
-
emptyStateTitle=
|
|
180
|
+
emptyStateTitle={emptyStates[side].title}
|
|
181
|
+
emptyStateHint={emptyStates[side].hint}
|
|
172
182
|
/>
|
|
173
183
|
);
|
|
174
184
|
|
|
@@ -100,9 +100,11 @@ const DISPLAY_ORDER = ["symbol", "name", "price", "change", "conditions", "rvol"
|
|
|
100
100
|
/**
|
|
101
101
|
* Symbol, name, price and change, then one column per condition with the
|
|
102
102
|
* figure it matched on, then context as the width allows: a column is added
|
|
103
|
-
* only when it fits whole, the row's status first
|
|
103
|
+
* only when it fits whole, the row's status first, and only when a row has
|
|
104
|
+
* one (after the close none does, and an empty column would take the room of
|
|
105
|
+
* the figures).
|
|
104
106
|
*/
|
|
105
|
-
function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]): DataTableColumn[] {
|
|
107
|
+
export function liveScanColumns(width: number, conditions: readonly LiveScanCondition[], withStatus: boolean): DataTableColumn[] {
|
|
106
108
|
const own = conditions.map((condition, index): DataTableColumn => {
|
|
107
109
|
const label = conditionColumnLabel(condition);
|
|
108
110
|
return { id: `c:${index}`, label, width: Math.max(8, label.length), align: "right" };
|
|
@@ -121,7 +123,7 @@ function liveScanColumns(width: number, conditions: readonly LiveScanCondition[]
|
|
|
121
123
|
));
|
|
122
124
|
let columns = order([...base, ...own]);
|
|
123
125
|
for (const column of OPTIONAL_COLUMNS) {
|
|
124
|
-
if (repeats.has(column.id)) continue;
|
|
126
|
+
if (repeats.has(column.id) || (column.id === "status" && !withStatus)) continue;
|
|
125
127
|
const next = order([...columns, column]);
|
|
126
128
|
if (getTableWidth(next) > width) break;
|
|
127
129
|
columns = next;
|
|
@@ -349,7 +351,8 @@ export default function LiveScanPane({ focused, width, height }: PaneProps) {
|
|
|
349
351
|
[payload, track],
|
|
350
352
|
);
|
|
351
353
|
const matches = useMemo(() => sortMatches(payload?.matches ?? [], sort, noteOf), [noteOf, payload?.matches, sort]);
|
|
352
|
-
const
|
|
354
|
+
const withStatus = useMemo(() => matches.some((match) => noteOf(match) != null), [matches, noteOf]);
|
|
355
|
+
const columns = useMemo(() => liveScanColumns(width, scan.conditions, withStatus), [scan.conditions, width, withStatus]);
|
|
353
356
|
|
|
354
357
|
const apply = (next: LiveScan) => {
|
|
355
358
|
setStoredScan(next);
|
|
@@ -3,6 +3,7 @@ import { useSecFilingContent } from "../../../market-data/hooks";
|
|
|
3
3
|
import { getSharedMarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
|
|
4
4
|
import type { QueryEntry } from "../../../market-data/result-types";
|
|
5
5
|
import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
|
|
6
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
6
7
|
import { documentContentTarget, isInlineExhibitDocument } from "./filing-documents";
|
|
7
8
|
|
|
8
9
|
interface ScopedFilingContentCache {
|
|
@@ -41,18 +42,32 @@ export function buildInlineFilingContentTargets(
|
|
|
41
42
|
.map((document) => documentContentTarget(filing, document));
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
/** Filings whose content is read at once: the queue's head and the next ones read ahead. */
|
|
46
|
+
const CONTENT_READS_AT_ONCE = 4;
|
|
47
|
+
|
|
48
|
+
/** The first `count` targets not cached yet, once each, in the caller's priority order. */
|
|
49
|
+
export function nextUncachedFilingContentTargets(
|
|
45
50
|
targets: readonly SecFilingItem[],
|
|
46
51
|
cache: ReadonlyMap<string, string | null>,
|
|
47
|
-
|
|
52
|
+
count: number,
|
|
53
|
+
): SecFilingItem[] {
|
|
48
54
|
const seen = new Set<string>();
|
|
55
|
+
const next: SecFilingItem[] = [];
|
|
49
56
|
for (const target of targets) {
|
|
57
|
+
if (next.length >= count) break;
|
|
50
58
|
const key = target.accessionNumber;
|
|
51
59
|
if (seen.has(key)) continue;
|
|
52
60
|
seen.add(key);
|
|
53
|
-
if (!cache.has(key))
|
|
61
|
+
if (!cache.has(key)) next.push(target);
|
|
54
62
|
}
|
|
55
|
-
return
|
|
63
|
+
return next;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function nextUncachedFilingContentTarget(
|
|
67
|
+
targets: readonly SecFilingItem[],
|
|
68
|
+
cache: ReadonlyMap<string, string | null>,
|
|
69
|
+
): SecFilingItem | null {
|
|
70
|
+
return nextUncachedFilingContentTargets(targets, cache, 1)[0] ?? null;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export function useSecFilingContentCache({
|
|
@@ -81,12 +96,29 @@ export function useSecFilingContentCache({
|
|
|
81
96
|
return () => { retryGeneration.current += 1; };
|
|
82
97
|
}, [scopeKey]);
|
|
83
98
|
|
|
84
|
-
const
|
|
85
|
-
() =>
|
|
99
|
+
const upcoming = useMemo(
|
|
100
|
+
() => nextUncachedFilingContentTargets(targets, contentCache, CONTENT_READS_AT_ONCE),
|
|
86
101
|
[contentCache, targets],
|
|
87
102
|
);
|
|
103
|
+
const nextTarget = upcoming[0] ?? null;
|
|
88
104
|
const contentEntry = useSecFilingContent(nextTarget);
|
|
89
105
|
|
|
106
|
+
// The queue takes one filing at a time; the next few are read beside it, and
|
|
107
|
+
// taken from the coordinator once the head settles (below). Each is asked
|
|
108
|
+
// once per scope: the coordinator shares a read in flight and keeps it.
|
|
109
|
+
const readAhead = useRef<{ scopeKey: string; keys: Set<string> }>({ scopeKey, keys: new Set() });
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
112
|
+
if (!coordinator) return;
|
|
113
|
+
if (readAhead.current.scopeKey !== scopeKey) readAhead.current = { scopeKey, keys: new Set() };
|
|
114
|
+
const asked = readAhead.current.keys;
|
|
115
|
+
for (const target of upcoming.slice(1)) {
|
|
116
|
+
if (asked.has(target.accessionNumber)) continue;
|
|
117
|
+
asked.add(target.accessionNumber);
|
|
118
|
+
void coordinator.loadSecFilingContent(target).catch(logFailure("sec.filing-content", "Reading filing content ahead failed"));
|
|
119
|
+
}
|
|
120
|
+
}, [scopeKey, upcoming]);
|
|
121
|
+
|
|
90
122
|
useEffect(() => {
|
|
91
123
|
if (!nextTarget) return;
|
|
92
124
|
const first = settledFilingContent(contentEntry);
|
|
@@ -39,8 +39,12 @@ const SEC_COLUMNS: HeadlessPaneColumn[] = [
|
|
|
39
39
|
/** Some row has 8-K item codes; a column every row leaves `-` takes no room in the text table (exports keep it). */
|
|
40
40
|
const hasItems = (rows: readonly HeadlessPaneRow[]) => rows.some((row) => typeof row.items === "string" && row.items !== "");
|
|
41
41
|
|
|
42
|
+
/** A current report's press release exhibit, printed only when some row has one; exports keep it with the filing's link. */
|
|
43
|
+
const PRESS_RELEASE_COLUMN: HeadlessPaneColumn = { key: "pressReleaseUrl", header: "Press release" };
|
|
44
|
+
|
|
42
45
|
function secTextColumns(rows: readonly HeadlessPaneRow[]): HeadlessPaneColumn[] {
|
|
43
|
-
|
|
46
|
+
const columns = hasItems(rows) ? SEC_COLUMNS : SEC_COLUMNS.filter((column) => column.key !== "items");
|
|
47
|
+
return rows.some((row) => typeof row.pressReleaseUrl === "string" && row.pressReleaseUrl) ? [...columns, PRESS_RELEASE_COLUMN] : columns;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -457,9 +457,11 @@ function SecView({ width, height, focused, paneType }: { width: number; height:
|
|
|
457
457
|
usePaneStatusLinkFooter({
|
|
458
458
|
registrationId: "sec",
|
|
459
459
|
focused,
|
|
460
|
-
|
|
460
|
+
// A current report links its press release, where its figures are, when its index names one.
|
|
461
|
+
url: openFiling ? openFiling.pressReleaseUrl ?? openFiling.filingUrl : factsUrl ?? abroadPage ?? announcements?.url,
|
|
461
462
|
source: openFiling ? openFiling.form : facts?.covered ? listNames(fundFactPublishers(facts)) : announcements?.name ?? null,
|
|
462
|
-
label: openFiling ?
|
|
463
|
+
label: openFiling ? openFiling.pressReleaseUrl ? "press release" : "filing"
|
|
464
|
+
: factsUrl ? "source" : abroadPage ? "issuer page" : announcements ? "announcements" : "filing",
|
|
463
465
|
// Rows read in the background for their insiders do not hold up the pane.
|
|
464
466
|
loading: loading || loadingDocuments || (!!loadingKey && viewedTargets.some((target) => target.accessionNumber === loadingKey)),
|
|
465
467
|
error: authWall ? null : error ?? detailError,
|
|
@@ -227,6 +227,8 @@ export function buildSecFilingRows(filings: readonly SecFilingItem[]) {
|
|
|
227
227
|
cik: filing.cik,
|
|
228
228
|
companyName: filing.companyName ?? null,
|
|
229
229
|
url: filing.filingUrl,
|
|
230
|
+
// A current report's press release exhibit, where its index names one: the page its figures are on.
|
|
231
|
+
...(filing.pressReleaseUrl ? { pressReleaseUrl: filing.pressReleaseUrl } : {}),
|
|
230
232
|
};
|
|
231
233
|
});
|
|
232
234
|
}
|
|
@@ -16,6 +16,7 @@ import type { ExtendedSession } from "../../../market-data/market/status";
|
|
|
16
16
|
import { useAutoRefresh, useUpdatedAgo } from "../../../react/auto-refresh";
|
|
17
17
|
import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
|
|
18
18
|
import { isStreamCarryingQuote, useVisibleBoardSymbols } from "../shared/use-quote-board";
|
|
19
|
+
import { useQuoteDelayFooter } from "../shared/cloud-upgrade";
|
|
19
20
|
import { SectorMoveBar } from "./move-bar";
|
|
20
21
|
import {
|
|
21
22
|
SECTOR_COLLECTIONS,
|
|
@@ -345,16 +346,21 @@ function EtfBoard({ collectionId, focused, width, height, rootBefore }: EtfBoard
|
|
|
345
346
|
.map((row) => `${row.etf}: ${sectorRowIssues(row).join(" · ")}`);
|
|
346
347
|
usePaneNoticeFooter({ registrationId: "sectors:row-issues", notices: rowIssueNotices, focused });
|
|
347
348
|
|
|
349
|
+
// Signed out or free, the ETFs' quotes are delayed ones.
|
|
350
|
+
const quoteDelay = useQuoteDelayFooter(rows.map((row) => (row.loading ? null : { dataSource: row.quoteDataSource })), "sectors-delay");
|
|
348
351
|
usePaneFooter("sectors", () => {
|
|
349
352
|
const info = loadingErrorFooterInfo(loading, loadError);
|
|
350
353
|
if (returnAsOfDate) info.push({ id: "return-as-of", parts: [{ text: `returns as of ${returnAsOfDate}`, tone: "muted" }] });
|
|
351
354
|
if (updatedAgo) info.push({ id: "updated", parts: [{ text: `checked ${updatedAgo}`, tone: "muted" }] });
|
|
352
355
|
// Keep the leading current failure readable when the pane is narrow; separate
|
|
353
356
|
// flex children would each shrink it to a few characters beside routine status.
|
|
354
|
-
return { info:
|
|
355
|
-
...(
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
return { info: [
|
|
358
|
+
...(info.length > 0 ? [{ id: "status", parts: info.flatMap((segment, index) => [
|
|
359
|
+
...(index > 0 ? [{ text: "·", tone: "muted" as const }] : []), ...segment.parts,
|
|
360
|
+
]) }] : []),
|
|
361
|
+
...(quoteDelay ? [quoteDelay] : []),
|
|
362
|
+
] };
|
|
363
|
+
}, [loadError, loading, quoteDelay, returnAsOfDate, updatedAgo]);
|
|
358
364
|
|
|
359
365
|
return (
|
|
360
366
|
<DataTableView<SectorRow, SectorColumn>
|
|
@@ -442,7 +448,7 @@ export const sectorsModule: PluginModule = {
|
|
|
442
448
|
paneId: "sectors",
|
|
443
449
|
label: "Thematic Baskets",
|
|
444
450
|
description: "Curated themes with equal-weight returns, breadth and member leaders and laggards.",
|
|
445
|
-
keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
|
|
451
|
+
keywords: ["themes", "thematic", "baskets", "AI compute", "datacenters", "electrification", "power grid", "nuclear", "uranium", "defense", "aerospace", "cybersecurity", "robotics", "automation", "critical minerals", "crypto", "bitcoin", "bitcoin miners", "miners", "bitcoin mining", "solar", "clean energy", "gold", "silver", "homebuilders", "memory", "chip equipment", "cloud software"],
|
|
446
452
|
shortcut: { prefix: "THEM", argKind: "text", argPlaceholder: "theme", argOptional: true },
|
|
447
453
|
headless: themesHeadless,
|
|
448
454
|
// The theme param opens the Themes tab, and the named theme's members.
|
|
@@ -61,6 +61,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
|
|
|
61
61
|
{ module: "futures", chart: "Front-month futures board, charted from the board", series: ["fut:*"] },
|
|
62
62
|
{ module: "futures-curve", chart: "Futures curve by contract month", notInCatalog: "not-a-time-series", reason: "Plotted by delivery month; each front month is in the catalog as FUT:code." },
|
|
63
63
|
{ module: "crypto-board", chart: "Price sparklines for the large coins", series: ["crypto:*"] },
|
|
64
|
+
{ module: "crypto-board", chart: "Bitcoin network hash rate, difficulty, next retarget estimate and hashprice", series: ["bitcoin:*"] },
|
|
64
65
|
{ module: "perps", chart: "Perpetual funding, open interest, premium and long/short account share", series: ["perps:*"] },
|
|
65
66
|
{ module: "crypto-etf-flows", chart: "The running net flow of the US spot bitcoin or ether ETFs since launch", notInCatalog: "not-yet", reason: "Daily and dated: the running total and each fund's daily flow could be crypto ETF series, and none is registered for the composer yet." },
|
|
66
67
|
{ module: "perps", chart: "Perpetual mark price", notInCatalog: "not-yet", reason: "The pair's price is in the catalog; the perpetual's own mark price could be a crypto series." },
|
|
@@ -96,6 +97,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
|
|
|
96
97
|
{ module: "analytics", chart: "Portfolio value, drawdown and factor exposure", notInCatalog: "account", reason: "Built from the user's own portfolio; the factor legs are tickers in the catalog." },
|
|
97
98
|
{ module: "holders", chart: "Holders treemap", notInCatalog: "not-a-time-series", reason: "A treemap of one filing date." },
|
|
98
99
|
{ module: "market-heatmap", chart: "Market heat map", notInCatalog: "not-a-time-series", reason: "A treemap of today's moves." },
|
|
100
|
+
{ module: "buildout", chart: "TheBuildout's theme board", notInCatalog: "not-a-time-series", reason: "A treemap of the themes' market caps, coloured by their median 1Y return today." },
|
|
99
101
|
{ module: "portfolio-list", chart: "Portfolio treemap", notInCatalog: "not-a-time-series", reason: "A treemap of current positions." },
|
|
100
102
|
{ module: "world-venue-map", chart: "Chokepoint transits and the map's place series", series: ["geo:*"] },
|
|
101
103
|
];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { logFailure } from "../../../utils/log-failure";
|
|
2
2
|
import { ApiRequestError } from "../../../api-client/errors";
|
|
3
|
+
import { createMissingRouteLatch } from "../../../api-client/coalesce";
|
|
3
4
|
import { savedCopyForTransientFailure, type PluginCacheResult } from "../../../data/plugin-cache";
|
|
4
5
|
|
|
5
6
|
interface CloudCache<T> {
|
|
@@ -81,3 +82,62 @@ export function unavailableOnServer(error: unknown, message: string, statuses: r
|
|
|
81
82
|
? new ApiRequestError(message, error.status, error.retryAfterMs, error.code, error.details)
|
|
82
83
|
: error;
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Reads ahead many items' payloads in batch requests (`read`, at most `max`
|
|
88
|
+
* ids each, sent side by side) into the cache their own loads read under
|
|
89
|
+
* `key`, skipping an item cached fresh unless `force`. Answers the ids stored, which a forced load can
|
|
90
|
+
* take as fresh; an item the batch failed or left out stays for its own load
|
|
91
|
+
* to ask, which says why. A batch route the server does not serve (404) is
|
|
92
|
+
* left alone until it is asked again (BATCH_ROUTE_REPROBE_MS).
|
|
93
|
+
*/
|
|
94
|
+
export function createCloudBatchAhead<T>(
|
|
95
|
+
cache: CloudCache<T>,
|
|
96
|
+
options: {
|
|
97
|
+
max: number;
|
|
98
|
+
validate: (payload: T) => T;
|
|
99
|
+
read: (ids: string[]) => Promise<Array<{ id: string; payload: T }>>;
|
|
100
|
+
},
|
|
101
|
+
): (items: ReadonlyArray<{ id: string; key: string }>, force?: boolean) => Promise<Set<string>> {
|
|
102
|
+
const route = createMissingRouteLatch();
|
|
103
|
+
return async (items, force = false) => {
|
|
104
|
+
const stored = new Set<string>();
|
|
105
|
+
const keys = new Map<string, string>();
|
|
106
|
+
for (const { id, key } of items) {
|
|
107
|
+
const cached = cache.get(key);
|
|
108
|
+
if (force || !cached || cached.stale) keys.set(id, key);
|
|
109
|
+
}
|
|
110
|
+
const ids = [...keys.keys()];
|
|
111
|
+
if (route.missing() || ids.length < 2) return stored;
|
|
112
|
+
const chunks = Array.from({ length: Math.ceil(ids.length / options.max) }, (_, index) => ids.slice(index * options.max, (index + 1) * options.max));
|
|
113
|
+
await Promise.all(chunks.map(async (chunk) => {
|
|
114
|
+
const read = await options.read(chunk).catch((error: unknown) => {
|
|
115
|
+
route.refused(error);
|
|
116
|
+
if (!(error instanceof ApiRequestError && error.status === 404)) logFailure("shared.cloud-resource", "Read a batch of cloud payloads ahead failed")(error);
|
|
117
|
+
return [];
|
|
118
|
+
});
|
|
119
|
+
await Promise.all(read.map(async ({ id, payload }) => {
|
|
120
|
+
const key = keys.get(id);
|
|
121
|
+
if (!key) return;
|
|
122
|
+
try {
|
|
123
|
+
const valid = options.validate(payload);
|
|
124
|
+
await cache.load(key, async () => valid, { force: true });
|
|
125
|
+
stored.add(id);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logFailure("shared.cloud-resource", "Store a batched cloud payload failed")(error);
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
}));
|
|
131
|
+
return stored;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A batch reply's items. A reply without them is a service that read the
|
|
137
|
+
* batch path as something else (an older one's `/:symbol` route, for a symbol
|
|
138
|
+
* named "batch"), which is a service without the route: a 404.
|
|
139
|
+
*/
|
|
140
|
+
export function batchReplyItems<T>(reply: { items?: T[] } | null | undefined): T[] {
|
|
141
|
+
if (!Array.isArray(reply?.items)) throw new ApiRequestError("This service does not read these in batches", 404);
|
|
142
|
+
return reply.items;
|
|
143
|
+
}
|
|
@@ -11,7 +11,7 @@ import { useRendererHost } from "../../../ui";
|
|
|
11
11
|
import type { RendererHost } from "../../../ui";
|
|
12
12
|
import { getSharedRegistry } from "../../registry";
|
|
13
13
|
import { requestAccountManagementTab } from "../account-management/navigation";
|
|
14
|
-
import { resolvePlanAccess, usePlanAccess, type PlanAccess } from "../../../api-client/plan-access";
|
|
14
|
+
import { freeQuotesDelayMinutes, resolvePlanAccess, usePlanAccess, type PlanAccess, type QuoteDelayFacts } from "../../../api-client/plan-access";
|
|
15
15
|
import { useViewport } from "../../../react/input";
|
|
16
16
|
import { type DialogApi, useOptionalDialog } from "../../../ui/dialog";
|
|
17
17
|
import { promptCloudUpgrade } from "../../../account/upgrade-dialog";
|
|
@@ -365,3 +365,26 @@ export function useCloudAccessFooter({
|
|
|
365
365
|
|
|
366
366
|
return { access, hint, openUpgrade, segment };
|
|
367
367
|
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* The footer's clock for a pane whose prices this account sees late: the free
|
|
371
|
+
* tier's delay (`freeQuotesDelayMinutes`) on any of `quotes`, pressed for the
|
|
372
|
+
* upgrade, as the quote footer's clock is. Null with Pro or a trial, and while
|
|
373
|
+
* every price shown is live, so those accounts see nothing new. A pane whose
|
|
374
|
+
* data comes as one board says so with one `{ dataSource }` for the board.
|
|
375
|
+
*/
|
|
376
|
+
export function useQuoteDelayFooter(
|
|
377
|
+
quotes: Iterable<QuoteDelayFacts | null | undefined>,
|
|
378
|
+
placement: string,
|
|
379
|
+
): PaneFooterSegment | null {
|
|
380
|
+
const language = useAppLanguage();
|
|
381
|
+
const minutes = freeQuotesDelayMinutes(usePlanAccess(), quotes);
|
|
382
|
+
const openUpgrade = useCloudUpgradeAction(placement);
|
|
383
|
+
return useMemo(() => (minutes == null ? null : delayedFooterSegment({
|
|
384
|
+
id: "quote-delay",
|
|
385
|
+
detail: tf("{delay} delayed · try Pro live", { delay: `${minutes}m` }),
|
|
386
|
+
delay: `${minutes}m`,
|
|
387
|
+
onPress: openUpgrade,
|
|
388
|
+
quoteStatus: true,
|
|
389
|
+
})), [language, minutes, openUpgrade]);
|
|
390
|
+
}
|