gloomberb 0.18.0 → 0.18.2
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/package.json +1 -1
- package/src/api-client/commodity-balances.ts +6 -0
- package/src/api-client/company-kpis.ts +9 -0
- package/src/api-client/crypto-markets.ts +9 -0
- package/src/api-client/data.ts +6 -5
- package/src/api-client/equity-screener.ts +2 -0
- package/src/api-client/market-movers.ts +7 -0
- package/src/api-client/types.ts +14 -0
- package/src/app/pane-runtime/plugin-bindings.ts +3 -1
- package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
- package/src/app/runtime/plugin-bindings.ts +6 -2
- package/src/brokers/logos.ts +66 -0
- package/src/brokers/profile-form.ts +3 -0
- package/src/brokers/remote-broker-adapter.ts +13 -3
- package/src/capabilities/index.ts +1 -1
- package/src/capabilities/service-capabilities.ts +7 -1
- package/src/cli/base-converter.ts +60 -18
- package/src/cli/commands/fred-series.ts +15 -5
- package/src/cli/commands/market.ts +290 -159
- package/src/cli/commands/news-search.ts +35 -8
- package/src/cli/commands/overview.ts +31 -6
- package/src/cli/commands/system.ts +35 -1
- package/src/cli/commands/ticker.ts +105 -52
- package/src/cli/currency-hints.ts +27 -0
- package/src/cli/fx-pair.ts +41 -4
- package/src/cli/glossary.ts +57 -4
- package/src/cli/helpers.ts +36 -3
- package/src/cli/index.ts +4 -1
- package/src/cli/known-gaps.ts +37 -1
- package/src/cli/listing-arg.ts +37 -2
- package/src/cli/pane-functions/catalog.ts +3 -1
- package/src/cli/pane-functions/discovery.ts +15 -1
- package/src/cli/pane-functions/dom.ts +9 -4
- package/src/cli/pane-functions/freshness.ts +4 -4
- package/src/cli/pane-functions/function-help.ts +31 -7
- package/src/cli/pane-functions/headless.ts +6 -5
- package/src/cli/pane-functions/index.ts +3 -2
- package/src/cli/pane-functions/lock-note.ts +7 -4
- package/src/cli/pane-functions/resolver.ts +2 -1
- package/src/cli/quote-answer.ts +179 -0
- package/src/cli/series-search.ts +2 -1
- package/src/cli/symbol-list.ts +51 -16
- package/src/cli/types.ts +4 -0
- package/src/components/chart/composite/composite-chart.tsx +22 -7
- package/src/components/chart/composite/format.ts +13 -1
- package/src/components/chart/composite/level-labels.ts +30 -0
- package/src/components/chart/composite/levels.ts +17 -6
- package/src/components/chart/composite/panel-surface.tsx +64 -16
- package/src/components/chart/composite/price-axis-labels.tsx +2 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
- package/src/components/command-bar/workflow/collection-actions.ts +4 -0
- package/src/components/index.ts +2 -0
- package/src/components/layout/pane/content.tsx +16 -14
- package/src/components/layout/pane/footer/index.tsx +22 -3
- package/src/components/layout/pane/footer/model.ts +99 -4
- package/src/components/layout/pane/header-tabs.tsx +15 -1
- package/src/components/onboarding/onboarding-frame.tsx +9 -11
- package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
- package/src/components/onboarding/wizard-model.ts +10 -3
- package/src/components/onboarding/wizard-positions.ts +8 -0
- package/src/components/ui/button.tsx +11 -4
- package/src/components/ui/choice-dialog.tsx +8 -5
- package/src/components/ui/display.tsx +11 -6
- package/src/components/ui/fields.tsx +100 -20
- package/src/components/ui/icon.tsx +6 -1
- package/src/components/ui/list-row-lines.tsx +87 -0
- package/src/components/ui/list-view.tsx +14 -0
- package/src/components/ui/logo-mark.tsx +64 -0
- package/src/components/ui/number-step.ts +54 -0
- package/src/components/ui/select-button.tsx +5 -4
- package/src/components/ui/select-field.tsx +3 -3
- package/src/components/ui/status.tsx +17 -3
- package/src/components/ui/tabs.tsx +9 -1
- package/src/components/ui/toggle.tsx +17 -3
- package/src/core/state/app/instrument.ts +8 -3
- package/src/data/config/store/normalize.ts +4 -1
- package/src/data/plugin-cache.ts +32 -6
- package/src/docs/snapshot.json +57 -22
- package/src/i18n/es.ts +9 -3
- package/src/i18n/ja.ts +9 -3
- package/src/i18n/ko.ts +9 -3
- package/src/i18n/zh-cn.ts +10 -4
- package/src/i18n/zh-tw.ts +10 -4
- package/src/market-data/commodity-names.ts +80 -0
- package/src/market-data/coordinator/auxiliary.ts +3 -1
- package/src/market-data/coordinator/chart.ts +3 -1
- package/src/market-data/futures-board.ts +38 -1
- package/src/market-data/futures-contract.ts +91 -0
- package/src/market-data/fx-closes.ts +78 -1
- package/src/market-data/market/change-basis.ts +219 -0
- package/src/market-data/market/format.ts +28 -12
- package/src/market-data/market/freshness.ts +38 -0
- package/src/market-data/market/status.ts +55 -9
- package/src/market-data/market/year-to-date.ts +46 -0
- package/src/market-data/quotes/contributions.ts +12 -0
- package/src/market-data/quotes/delisting.ts +34 -0
- package/src/market-data/quotes/live-valuation.ts +23 -0
- package/src/market-data/quotes/resolution.ts +8 -2
- package/src/market-data/tax-equivalent.ts +10 -2
- package/src/notifications/app-notifier.ts +2 -2
- package/src/notifications/notification-log.ts +8 -6
- package/src/plugins/builtin/alerts/cli.ts +26 -18
- package/src/plugins/builtin/alerts/headless.ts +1 -1
- package/src/plugins/builtin/alerts/index.tsx +13 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/analytics/risk-client.ts +44 -11
- package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
- package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
- package/src/plugins/builtin/analytics/risk-model.ts +79 -16
- package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
- package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
- package/src/plugins/builtin/attention/trending.tsx +8 -6
- package/src/plugins/builtin/backtest/client.ts +5 -2
- package/src/plugins/builtin/backtest/window.ts +16 -3
- package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
- package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
- package/src/plugins/builtin/broker-manager/index.tsx +20 -9
- package/src/plugins/builtin/broker-manager/model.ts +5 -0
- package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
- package/src/plugins/builtin/broker-manager/table.ts +5 -1
- package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
- package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
- package/src/plugins/builtin/broker-trading/command.ts +103 -0
- package/src/plugins/builtin/broker-trading/controller.ts +688 -0
- package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
- package/src/plugins/builtin/broker-trading/headless.ts +76 -0
- package/src/plugins/builtin/broker-trading/index.tsx +77 -0
- package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
- package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
- package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
- package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
- package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
- package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
- package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
- package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
- package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
- package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
- package/src/plugins/builtin/buildout/format.ts +1 -0
- package/src/plugins/builtin/buildout/model/index.ts +16 -7
- package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
- package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
- package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
- package/src/plugins/builtin/chart-composer/headless.ts +2 -1
- package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
- package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
- package/src/plugins/builtin/chart-composer/presets.ts +2 -2
- package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
- package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
- package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
- package/src/plugins/builtin/chat/index.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
- package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
- package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
- package/src/plugins/builtin/commodity-balances/client.ts +6 -4
- package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
- package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
- package/src/plugins/builtin/commodity-balances/model.ts +22 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
- package/src/plugins/builtin/company-kpis/client.ts +3 -2
- package/src/plugins/builtin/company-kpis/headless.ts +5 -7
- package/src/plugins/builtin/company-kpis/model.ts +85 -2
- package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
- package/src/plugins/builtin/composite-plugins.ts +3 -2
- package/src/plugins/builtin/cot/headless.ts +1 -1
- package/src/plugins/builtin/cot/index.tsx +1 -1
- package/src/plugins/builtin/cot/model.ts +35 -12
- package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
- package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
- package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +33 -2
- package/src/plugins/builtin/crypto-board/headless.ts +5 -2
- package/src/plugins/builtin/crypto-board/model.ts +28 -8
- package/src/plugins/builtin/custom-view/loader.ts +3 -2
- package/src/plugins/builtin/custom-view/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
- package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
- package/src/plugins/builtin/econ/calendar-model.ts +38 -2
- package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
- package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
- package/src/plugins/builtin/econ/index.tsx +31 -4
- package/src/plugins/builtin/econ/print-watch.ts +279 -0
- package/src/plugins/builtin/equity-screener/headless.ts +20 -5
- package/src/plugins/builtin/equity-screener/model.ts +40 -2
- package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
- package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
- package/src/plugins/builtin/executives/data.ts +24 -8
- package/src/plugins/builtin/executives/index.tsx +2 -18
- package/src/plugins/builtin/executives/pane.tsx +34 -10
- package/src/plugins/builtin/exposure/client.ts +4 -1
- package/src/plugins/builtin/exposure/editor.tsx +3 -3
- package/src/plugins/builtin/exposure/evidence.ts +4 -1
- package/src/plugins/builtin/exposure/holdings.ts +3 -1
- package/src/plugins/builtin/exposure/pane.tsx +37 -16
- package/src/plugins/builtin/filing-events/index.tsx +9 -3
- package/src/plugins/builtin/futures/table.tsx +6 -3
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/insider/base-values.ts +72 -0
- package/src/plugins/builtin/insider/headless.ts +29 -4
- package/src/plugins/builtin/jobs/headless.ts +2 -2
- package/src/plugins/builtin/jobs/lock-note.ts +14 -3
- package/src/plugins/builtin/market-halts/model.ts +31 -6
- package/src/plugins/builtin/market-halts/pane.tsx +3 -0
- package/src/plugins/builtin/market-movers/client.ts +2 -2
- package/src/plugins/builtin/market-movers/index.tsx +0 -3
- package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
- package/src/plugins/builtin/market-movers/session.ts +25 -10
- package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
- package/src/plugins/builtin/members/pane.tsx +8 -3
- package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
- package/src/plugins/builtin/news/wire/index.tsx +7 -2
- package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
- package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
- package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
- package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
- package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
- package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
- package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
- package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
- package/src/plugins/builtin/notes/cli.ts +33 -8
- package/src/plugins/builtin/notes/export.ts +49 -3
- package/src/plugins/builtin/notes/files.ts +14 -3
- package/src/plugins/builtin/notes/index.tsx +3 -3
- package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
- package/src/plugins/builtin/notes/store.ts +13 -3
- package/src/plugins/builtin/options/footer.ts +25 -20
- package/src/plugins/builtin/options/live-quotes.ts +33 -18
- package/src/plugins/builtin/options/view.tsx +77 -22
- package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
- package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
- package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
- package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
- package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
- package/src/plugins/builtin/primary-dealers/client.ts +137 -62
- package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
- package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
- package/src/plugins/builtin/primary-dealers/model.ts +179 -129
- package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
- package/src/plugins/builtin/research/analyst-model.ts +5 -1
- package/src/plugins/builtin/risk-factors/index.tsx +12 -3
- package/src/plugins/builtin/scanner/feed.ts +6 -1
- package/src/plugins/builtin/scanner/headless.ts +1 -1
- package/src/plugins/builtin/scanner/lock-note.ts +3 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
- package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
- package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
- package/src/plugins/builtin/shared/free-routes.ts +24 -0
- package/src/plugins/builtin/shared/headless-options.ts +11 -0
- package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
- package/src/plugins/builtin/shared/research-trading.tsx +62 -0
- package/src/plugins/builtin/social-mentions/headless.ts +1 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
- package/src/plugins/builtin/themes/headless.ts +8 -2
- package/src/plugins/builtin/themes/model.ts +43 -10
- package/src/plugins/builtin/themes/pane.tsx +8 -6
- package/src/plugins/builtin/thirteenf/api.ts +4 -1
- package/src/plugins/builtin/thirteenf/data.ts +5 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
- package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
- package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
- package/src/plugins/builtin/time-sales/pane.tsx +2 -2
- package/src/plugins/builtin/trace-bonds/client.ts +14 -3
- package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
- package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/compat.ts +3 -0
- package/src/plugins/external-runtime.ts +5 -0
- package/src/plugins/loader.ts +6 -0
- package/src/plugins/plugin-icon.ts +61 -0
- package/src/plugins/registry/host-actions.ts +2 -1
- package/src/plugins/registry/index.ts +9 -1
- package/src/plugins/runtime/context.tsx +5 -0
- package/src/plugins/runtime/index.tsx +3 -0
- package/src/portfolio/column-values.ts +13 -4
- package/src/portfolio/cost-fx.ts +61 -0
- package/src/portfolio/mutations.ts +16 -2
- package/src/portfolio/position-metrics.ts +45 -4
- package/src/portfolio/position-quantity.ts +43 -0
- package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
- package/src/public/broker.ts +8 -0
- package/src/public/market-data.ts +5 -0
- package/src/renderers/dom/desktop/control-styles.ts +7 -1
- package/src/renderers/dom/desktop/controls.tsx +39 -7
- package/src/renderers/dom/desktop/icons.tsx +14 -0
- package/src/renderers/dom/desktop/list-view.tsx +51 -19
- package/src/renderers/dom/desktop/menu.tsx +10 -1
- package/src/renderers/dom/desktop/select-field.tsx +8 -6
- package/src/renderers/dom/host/style.ts +6 -1
- package/src/renderers/dom/host/tabs.tsx +6 -0
- package/src/renderers/dom/styles.css +12 -5
- package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
- package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
- package/src/renderers/electrobun/view/external-plugins.ts +1 -0
- package/src/series/validate.ts +2 -2
- package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
- package/src/sources/gloomberb-cloud/index.ts +50 -17
- package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
- package/src/sources/history-result.ts +3 -0
- package/src/sources/provider-errors.ts +3 -1
- package/src/sources/provider-router/history.ts +49 -6
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/activity.ts +13 -0
- package/src/sync/controller.ts +4 -0
- package/src/sync/core-contributors.ts +63 -11
- package/src/sync/local-only.ts +121 -0
- package/src/sync/react.ts +6 -0
- package/src/sync/types.ts +3 -0
- package/src/team/collections.ts +2 -0
- package/src/tickers/listing-fate.ts +27 -8
- package/src/tickers/metadata.ts +10 -0
- package/src/tickers/search/index.ts +24 -3
- package/src/time-series/price-comparison.ts +35 -0
- package/src/time-series/resolve.ts +52 -11
- package/src/time-series/spec.ts +1 -1
- package/src/time-series/types.ts +5 -0
- package/src/types/broker.ts +56 -1
- package/src/types/config.ts +6 -0
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +23 -0
- package/src/types/headless.ts +12 -2
- package/src/types/instrument.ts +4 -0
- package/src/types/plugin.ts +24 -6
- package/src/types/price-history.ts +6 -1
- package/src/types/ticker.ts +9 -0
- package/src/types/trading.ts +67 -0
- package/src/ui/host.tsx +4 -0
- package/src/utils/base-currency.ts +108 -0
- package/src/utils/currency-names.ts +128 -0
- package/src/utils/currency-pair.ts +2 -1
- package/src/utils/currency-units.ts +41 -1
- package/src/utils/dom-screenshot.ts +22 -9
- package/src/utils/exchange-notation.generated.ts +360 -0
- package/src/utils/exchanges.ts +15 -322
- package/src/utils/format.ts +31 -1
- package/src/utils/instrument-identity.ts +38 -1
- package/src/utils/options.ts +46 -1
- package/src/utils/price-history.ts +4 -0
- package/src/utils/reported-money.ts +8 -4
|
@@ -298,6 +298,14 @@ export function formatChartLegendValue(
|
|
|
298
298
|
return trimmed && trimmed.length <= 6 ? `${compact}${trimmed.startsWith("/") ? "" : " "}${trimmed}` : compact;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/** compactNumber's scale, with the decimals a tick gap needs at that scale. */
|
|
302
|
+
function preciseCompactNumber(value: number, gap: number): string {
|
|
303
|
+
const absolute = Math.abs(value);
|
|
304
|
+
const [divisor, suffix] = ([[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]] as const).find(([scale]) => absolute >= scale) ?? [1, ""];
|
|
305
|
+
const digits = Math.min(12, Math.max(0, Math.ceil(-Math.log10(gap / divisor) - 1e-9)));
|
|
306
|
+
return `${(value / divisor).toFixed(digits)}${suffix}`;
|
|
307
|
+
}
|
|
308
|
+
|
|
301
309
|
const COMPACT_LABEL = /^(-?\d+)(?:\.(\d+))?([KMBT]?)$/;
|
|
302
310
|
|
|
303
311
|
function trimmedDecimals(label: string): number {
|
|
@@ -332,13 +340,17 @@ export function formatCompositeAxisValue(value: number, domain: CompositeAxisDom
|
|
|
332
340
|
// Where that happens the axis spends the digits needed to tell them apart,
|
|
333
341
|
// shared across every tick so the column reads as one scale.
|
|
334
342
|
const gap = narrowestAxisTickGap(domain);
|
|
343
|
+
const prefix = currencyPrefix(domain.unit);
|
|
335
344
|
if (gap !== null && gap < compactResolution(value)) {
|
|
336
345
|
const resolved = formatAxisPriceValue(value, domain, {
|
|
337
346
|
fixedFractionDigits: Math.max(0, Math.ceil(-Math.log10(gap))),
|
|
338
347
|
});
|
|
339
348
|
if (resolved) return resolved;
|
|
349
|
+
// No currency to format with, such as a price whose quote has not
|
|
350
|
+
// arrived: the same digits still keep the ticks apart.
|
|
351
|
+
const precise = preciseCompactNumber(value, gap);
|
|
352
|
+
return prefix && precise.startsWith("-") ? `-${prefix}${precise.slice(1)}` : `${prefix}${precise}`;
|
|
340
353
|
}
|
|
341
|
-
const prefix = currencyPrefix(domain.unit);
|
|
342
354
|
return prefix && compact.startsWith("-") ? `-${prefix}${compact.slice(1)}` : `${prefix}${compact}`;
|
|
343
355
|
}
|
|
344
356
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { blendForContrast, higherContrast } from "../../../theme/color-utils";
|
|
3
|
+
import { fitAxisLabels, type CompositeChartLevel, type ProjectedLevel } from "./levels";
|
|
4
|
+
|
|
5
|
+
/** Keep a precise price in the tag only when it has no readable axis marker. */
|
|
6
|
+
export function levelLabelText(level: CompositeChartLevel, axisVisible: boolean): string {
|
|
7
|
+
return `${level.label ?? ""}${!axisVisible && level.axisLabel ? ` ${level.axisLabel}` : ""}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Compact DOM tags straddle their line; crowded or clipped tags yield. */
|
|
11
|
+
export function layoutLevelLabels(levels: readonly ProjectedLevel[], plotHeight: number, textHeight: number) {
|
|
12
|
+
const candidates = levels.filter(({ level }) => level.label).map((entry) => ({
|
|
13
|
+
...entry, top: entry.yRatio * plotHeight - textHeight / 2,
|
|
14
|
+
})).filter(({ top }) => top >= 0 && top + textHeight <= plotHeight);
|
|
15
|
+
return fitAxisLabels(candidates, [], ({ top }) => top / textHeight);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function levelLabelStyle({ yRatio, level }: ProjectedLevel, background: string): CSSProperties {
|
|
19
|
+
return {
|
|
20
|
+
top: `${yRatio * 100}%`,
|
|
21
|
+
transform: "translateY(-50%)",
|
|
22
|
+
pointerEvents: "none",
|
|
23
|
+
paddingInline: 3,
|
|
24
|
+
height: "auto",
|
|
25
|
+
width: "max-content",
|
|
26
|
+
border: `1px solid ${level.color}`,
|
|
27
|
+
backgroundColor: background,
|
|
28
|
+
color: blendForContrast(level.color, background, higherContrast("#000000", "#ffffff", background), 4.5),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -14,6 +14,11 @@ export interface CompositeChartLevel {
|
|
|
14
14
|
editable: boolean;
|
|
15
15
|
/** Whether the chart's level action applies to it. */
|
|
16
16
|
actionable: boolean;
|
|
17
|
+
/** Optional read-only annotation, independent of drawing-tool selection. */
|
|
18
|
+
label?: string;
|
|
19
|
+
/** Exact instrument price, independent of the grid's compact tick formatter. */
|
|
20
|
+
axisLabel?: string;
|
|
21
|
+
dashed?: boolean;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export type CompositeLevelEdit =
|
|
@@ -111,13 +116,17 @@ export function fitAxisLabels<T>(
|
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
function levelSegments(dashed?: boolean): Array<[number, number]> {
|
|
120
|
+
return dashed ? Array.from({ length: 48 }, (_, i) => [i / 48, (i + 0.55) / 48]) : [[0, 1]];
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
export function levelVectors(levels: readonly ProjectedLevel[], selectedId: string | null): ChartVectorShape[] {
|
|
115
|
-
return levels.
|
|
116
|
-
id: `level:${level.id}`,
|
|
117
|
-
points: [{ x:
|
|
124
|
+
return levels.flatMap(({ level, yRatio }) => levelSegments(level.dashed).map(([start, end], index) => ({
|
|
125
|
+
id: `level:${level.id}:${index}`,
|
|
126
|
+
points: [{ x: start, y: yRatio }, { x: end, y: yRatio }],
|
|
118
127
|
color: level.color,
|
|
119
128
|
strokeWidth: level.id === selectedId ? 2.2 : 1.2,
|
|
120
|
-
}));
|
|
129
|
+
})));
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/** Levels painted on a copy of the panel raster, the one layer the terminal uploads. */
|
|
@@ -131,7 +140,9 @@ export function paintLevels(
|
|
|
131
140
|
const maxY = Math.max(base.height - 1, 0);
|
|
132
141
|
for (const { level, yRatio } of levels) {
|
|
133
142
|
const y = Math.round(yRatio * maxY);
|
|
134
|
-
|
|
143
|
+
for (const [start, end] of levelSegments(level.dashed)) {
|
|
144
|
+
drawLine(data, base.width, base.height, start * maxX, y, end * maxX, y, parseHex(level.color), level.id === selectedId ? 2.2 : 1.2);
|
|
145
|
+
}
|
|
135
146
|
}
|
|
136
147
|
return { width: base.width, height: base.height, pixels: data };
|
|
137
148
|
}
|
|
@@ -145,7 +156,7 @@ export function writeLevelText(lines: readonly string[], levels: readonly Projec
|
|
|
145
156
|
const rows = lines.map((line) => Array.from(line));
|
|
146
157
|
for (const { level, yRatio } of levels) {
|
|
147
158
|
const row = rows[clamp(Math.round(yRatio * (rows.length - 1)), 0, rows.length - 1)]!;
|
|
148
|
-
const glyph = level.id === selectedId ? "━" : level.editable ? "
|
|
159
|
+
const glyph = level.id === selectedId ? "━" : level.dashed || !level.editable ? "┄" : "─";
|
|
149
160
|
for (let x = 0; x < row.length; x += 1) {
|
|
150
161
|
if (row[x] === " " || row[x] === "·" || row[x] === "╌") row[x] = glyph;
|
|
151
162
|
}
|
|
@@ -12,7 +12,8 @@ import { colors as themeColors } from "../../../theme/colors";
|
|
|
12
12
|
import { consumeChartMouseEvent, getGlobalMouseX, getLocalPlotPointer, type ChartMouseEvent } from "../core/pointer";
|
|
13
13
|
import { useStaticChartBitmapSize } from "./bitmap";
|
|
14
14
|
import { useMeasuredCells } from "../../../ui/measured-cells";
|
|
15
|
-
import { PriceAxisLabels } from "./price-axis-labels";
|
|
15
|
+
import { PriceAxisLabels, placeAxisTicks } from "./price-axis-labels";
|
|
16
|
+
import { layoutLevelLabels, levelLabelStyle, levelLabelText } from "./level-labels";
|
|
16
17
|
import { formatCompositeCursorValue, type CompositeAxisValueFormatter } from "./format";
|
|
17
18
|
import {
|
|
18
19
|
compositeViewportPositions,
|
|
@@ -111,6 +112,8 @@ interface CompositePanelSurfaceProps {
|
|
|
111
112
|
onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
|
|
112
113
|
onSetViewport: (range: CompositeViewportRange) => void;
|
|
113
114
|
onToolSpanChange: (span: ChartToolSpan | null) => void;
|
|
115
|
+
/** A press on the plot started (true) or stopped (false) dragging: a pan, a tool, a drawing or a level. */
|
|
116
|
+
onDragActiveChange?: (dragging: boolean) => void;
|
|
114
117
|
/** The tool being placed from the keyboard, when it lives in this panel. */
|
|
115
118
|
keyboardToolDrag: KeyboardToolDrag | null;
|
|
116
119
|
/** A press on the plot: the pointer takes over from a keyboard placement. */
|
|
@@ -154,6 +157,7 @@ export function CompositePanelSurface({
|
|
|
154
157
|
onZoomViewport,
|
|
155
158
|
onSetViewport,
|
|
156
159
|
onToolSpanChange,
|
|
160
|
+
onDragActiveChange,
|
|
157
161
|
keyboardToolDrag,
|
|
158
162
|
onPointerPress,
|
|
159
163
|
showTextFallback,
|
|
@@ -207,14 +211,12 @@ export function CompositePanelSurface({
|
|
|
207
211
|
const plotSpanFactor = 1 + compositeRightOffsetRatio(scene.timeScale);
|
|
208
212
|
const bitmapSize = useStaticChartBitmapSize(plotSize.width, plotSize.height);
|
|
209
213
|
const bitmap = useCompositePanelBitmap({ panel, bitmapSize, colors, isDesktopWeb, revealed: plotSize.revealed === true });
|
|
210
|
-
// The
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
const readsPointerLevel = !isDesktopWeb || armedTool !== null || activeDrag !== null;
|
|
217
|
-
const pointerCursorYRatio = scene.cursorXRatio === null || !readsPointerLevel ? null : heldCursorYRatio;
|
|
214
|
+
// The chart under the pointer draws both lines: the level line follows the
|
|
215
|
+
// pointer, and its value sits on the axes, beside each series' own tag on
|
|
216
|
+
// the desktop and the web. A keyboard or shared cursor knows its column
|
|
217
|
+
// only, so a chart following another chart's cursor draws the time line
|
|
218
|
+
// alone. A tool placed from the keyboard is the exception: its end is a level.
|
|
219
|
+
const pointerCursorYRatio = scene.cursorXRatio === null ? null : heldCursorYRatio;
|
|
218
220
|
const crosshair = useMemo(
|
|
219
221
|
() => resolvePanelCrosshair(panel, bitmap, scene, pointerCursorYRatio, colors.crosshair),
|
|
220
222
|
[bitmap, colors.crosshair, panel, pointerCursorYRatio, scene],
|
|
@@ -479,14 +481,16 @@ export function CompositePanelSurface({
|
|
|
479
481
|
.sort((left, right) => Number(right.level.id === selectedLevelId) - Number(left.level.id === selectedLevelId)
|
|
480
482
|
|| Number(left.level.editable) - Number(right.level.editable))
|
|
481
483
|
.map(({ level, yRatio }) => ({
|
|
484
|
+
id: level.id,
|
|
482
485
|
side: levelDomain.side,
|
|
483
486
|
yRatio,
|
|
484
|
-
label: formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain),
|
|
487
|
+
label: level.axisLabel ?? (formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain)),
|
|
485
488
|
color: level.color,
|
|
486
489
|
})) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
|
|
487
490
|
const buildAxisMarkers = (side: "left" | "right") => {
|
|
488
491
|
if (!panel.axes[side]) return undefined;
|
|
489
|
-
const toAxisMarker = (marker: { yRatio: number; label: string; color: string }) => ({
|
|
492
|
+
const toAxisMarker = (marker: { id?: string; yRatio: number; label: string; color: string }) => ({
|
|
493
|
+
id: marker.id,
|
|
490
494
|
row: Math.round(marker.yRatio * Math.max(panel.height - 1, 0)),
|
|
491
495
|
pixelY: marker.yRatio * Math.max(panel.height * cellHeightPx - 1, 0),
|
|
492
496
|
label: marker.label,
|
|
@@ -501,8 +505,14 @@ export function CompositePanelSurface({
|
|
|
501
505
|
// A label is a row tall, so close levels would stack into an unreadable
|
|
502
506
|
// pile and hide the last price: each keeps its line, and only the labels
|
|
503
507
|
// with a row to themselves show. Desktop badges sit between rows.
|
|
508
|
+
const grid = fractionalViewport
|
|
509
|
+
? placeAxisTicks({ ticks: side === "left" ? leftAxisTicks : rightAxisTicks, height: panel.height, cellHeightPx }).map((tick) => ({
|
|
510
|
+
...toAxisMarker({ yRatio: tick.ratio, label: tick.label, color: colors.textDim }),
|
|
511
|
+
pixelY: tick.topPercent / 100 * Math.max(panel.height * cellHeightPx - 1, 1),
|
|
512
|
+
}))
|
|
513
|
+
: [...(side === "left" ? leftAxisLabels : rightAxisLabels)].map(([row, label]) => ({ id: undefined, row, pixelY: row * cellHeightPx, label, color: colors.textDim }));
|
|
504
514
|
const markers = [
|
|
505
|
-
...fitAxisLabels(levelLabels, fixed, (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
|
|
515
|
+
...fitAxisLabels(levelLabels, [...fixed, ...grid], (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
|
|
506
516
|
...fixed,
|
|
507
517
|
];
|
|
508
518
|
return markers.length > 0 ? markers : undefined;
|
|
@@ -738,12 +748,29 @@ export function CompositePanelSurface({
|
|
|
738
748
|
}
|
|
739
749
|
finishToolDrag();
|
|
740
750
|
}, [finishToolDrag]);
|
|
751
|
+
// Said once per change, so a pointer move costs the chart nothing.
|
|
752
|
+
const reportedDragRef = useRef(false);
|
|
753
|
+
const reportDrag = useCallback((dragging: boolean) => {
|
|
754
|
+
if (reportedDragRef.current === dragging) return;
|
|
755
|
+
reportedDragRef.current = dragging;
|
|
756
|
+
onDragActiveChange?.(dragging);
|
|
757
|
+
}, [onDragActiveChange]);
|
|
758
|
+
const pressPlot = useCallback((event: ChartMouseEvent) => {
|
|
759
|
+
const before = dragRef.current;
|
|
760
|
+
startDrag(event);
|
|
761
|
+
if (dragRef.current !== null && dragRef.current !== before) reportDrag(true);
|
|
762
|
+
}, [reportDrag, startDrag]);
|
|
763
|
+
const releasePlot = useCallback(() => {
|
|
764
|
+
resetDrag();
|
|
765
|
+
reportDrag(false);
|
|
766
|
+
}, [reportDrag, resetDrag]);
|
|
741
767
|
const handleMouseMove = useCallback((event: ChartMouseEvent) => {
|
|
742
768
|
// A release over a neighbouring pane never reaches this surface, so treat a
|
|
743
769
|
// plain move, which only fires with no button held, as the missing release.
|
|
744
770
|
finishToolDrag();
|
|
771
|
+
reportDrag(false);
|
|
745
772
|
updateCursor(event);
|
|
746
|
-
}, [finishToolDrag, updateCursor]);
|
|
773
|
+
}, [finishToolDrag, reportDrag, updateCursor]);
|
|
747
774
|
// Trackpads fire wheel events faster than the plot can paint, so one frame
|
|
748
775
|
// absorbs every event that arrived since the last one.
|
|
749
776
|
const pendingWheelRef = useRef<PendingWheel | null>(null);
|
|
@@ -878,10 +905,10 @@ export function CompositePanelSurface({
|
|
|
878
905
|
crosshair={crosshair}
|
|
879
906
|
vectors={vectors}
|
|
880
907
|
onMouseMove={interactive ? handleMouseMove : undefined}
|
|
881
|
-
onMouseDown={interactive ? navigable ?
|
|
908
|
+
onMouseDown={interactive ? navigable ? pressPlot : pressCursor : undefined}
|
|
882
909
|
onMouseDrag={interactive && navigable ? dragViewport : undefined}
|
|
883
|
-
onMouseUp={interactive && navigable ?
|
|
884
|
-
onMouseDragEnd={interactive && navigable ?
|
|
910
|
+
onMouseUp={interactive && navigable ? releasePlot : undefined}
|
|
911
|
+
onMouseDragEnd={interactive && navigable ? releasePlot : undefined}
|
|
885
912
|
onMouseScroll={interactive && navigable ? panFromWheel : undefined}
|
|
886
913
|
onMouseOut={interactive ? clearCursor : undefined}
|
|
887
914
|
cursor={interactive ? toolDrag || !navigable || onPickTime ? "crosshair" : "grab" : undefined}
|
|
@@ -892,6 +919,27 @@ export function CompositePanelSurface({
|
|
|
892
919
|
>
|
|
893
920
|
{textLines.map((line, index) => <Text key={index} fg={colors.text}>{line}</Text>)}
|
|
894
921
|
</ChartSurface>
|
|
922
|
+
{(isDesktopWeb
|
|
923
|
+
? layoutLevelLabels(projectedLevels, plotSize.height * cellHeightPx, cellHeightPx)
|
|
924
|
+
: fitAxisLabels(projectedLevels.filter((entry) => entry.level.label), [], (entry) => entry.yRatio * Math.max(1, panel.height - 1))
|
|
925
|
+
).map((entry) => {
|
|
926
|
+
const { level, yRatio } = entry;
|
|
927
|
+
const axisWidth = levelDomain?.side === "left" ? leftAxisWidth : rightAxisWidth;
|
|
928
|
+
const axisMarkers = levelDomain?.side === "left" ? leftAxisMarkers : rightAxisMarkers;
|
|
929
|
+
const cursorCovers = activeCursorYRatio !== null && Math.abs(activeCursorYRatio - yRatio) * panel.height < 1;
|
|
930
|
+
const axisVisible = !cursorCovers && (level.axisLabel?.length ?? 0) <= axisWidth && !!axisMarkers?.some((marker) => marker.id === level.id);
|
|
931
|
+
const label = levelLabelText(level, axisVisible);
|
|
932
|
+
const labelStyle = isDesktopWeb ? {
|
|
933
|
+
...levelLabelStyle(entry, colors.background), left: "auto", right: (rightAxisWidth + (rightAxisWidth ? axisGap : 0) + 1) * cellWidthPx,
|
|
934
|
+
maxWidth: Math.max(1, plotWidth - 2) * cellWidthPx,
|
|
935
|
+
} : undefined;
|
|
936
|
+
const labelWidth = Math.min(plotWidth - 2, label.length + 2);
|
|
937
|
+
return <Box key={`annotation:${level.id}`} position="absolute" left={leftAxisWidth + (leftAxisWidth ? axisGap : 0) + plotWidth - labelWidth - 1} top={yRatio * Math.max(0, panel.height - 1)} height={1}
|
|
938
|
+
width={labelWidth} flexDirection="row" justifyContent="flex-end" backgroundColor={colors.background} zIndex={20}
|
|
939
|
+
style={labelStyle} data-gloom-role="chart-level-label">
|
|
940
|
+
<Text fg={labelStyle?.color ?? level.color} wrapMode="none" style={isDesktopWeb ? { lineHeight: 1 } : undefined}>{label}</Text>
|
|
941
|
+
</Box>;
|
|
942
|
+
})}
|
|
895
943
|
{rightAxisWidth > 0 ? (
|
|
896
944
|
<>
|
|
897
945
|
<Box width={axisGap} />
|
|
@@ -281,7 +281,8 @@ export function PriceAxisLabels({
|
|
|
281
281
|
transform: "translateY(-50%)",
|
|
282
282
|
whiteSpace: "pre",
|
|
283
283
|
pointerEvents: "none",
|
|
284
|
-
|
|
284
|
+
// The value under the pointer reads whole, over a series' tag at that height.
|
|
285
|
+
zIndex: 3,
|
|
285
286
|
}}
|
|
286
287
|
>
|
|
287
288
|
<Box flexDirection="row" width={axisWidth} justifyContent={axisLabelJustify} overflow="hidden">
|
|
@@ -274,6 +274,10 @@ export interface CompositeChartProps {
|
|
|
274
274
|
remoteKind?: string;
|
|
275
275
|
/** Permit panning into an older unloaded window so the owner can backfill it. */
|
|
276
276
|
allowHistoricalBackfill?: boolean;
|
|
277
|
+
/**
|
|
278
|
+
* The owner's cursor. With `onCursorDateChange` the owner moves it; without,
|
|
279
|
+
* the cursor rests there while no pointer is on the plot.
|
|
280
|
+
*/
|
|
277
281
|
cursorDate?: Date | null;
|
|
278
282
|
/**
|
|
279
283
|
* A crosshair shared with other charts. By default an interactive chart
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type TickerSearchInstrumentClass,
|
|
18
18
|
} from "../../../../tickers/search";
|
|
19
19
|
import type { ResultItem } from "../../list/model";
|
|
20
|
+
import { delistingLabel, parseListingDelisting } from "../../../../market-data/quotes/delisting";
|
|
20
21
|
import {
|
|
21
22
|
buildTickerSearchCacheKey,
|
|
22
23
|
createQuickLookTickerCandidates,
|
|
@@ -74,6 +75,9 @@ export function useCommandBarTickerSearchActions({
|
|
|
74
75
|
// The candidate's detail leads with the name; the rest repeats the badge or venue.
|
|
75
76
|
const leadingName = candidate.detail.split(" | ")[0]?.trim();
|
|
76
77
|
const name = leadingName && leadingName.toUpperCase() !== candidate.label.toUpperCase() ? leadingName : undefined;
|
|
78
|
+
// A listing that no longer trades says so under its row, with its last session.
|
|
79
|
+
const delisting = parseListingDelisting(candidate.result?.delisting);
|
|
80
|
+
const lines = delisting ? [{ segments: [{ text: delistingLabel(delisting), emphasis: "muted" as const }] }] : undefined;
|
|
77
81
|
|
|
78
82
|
if (candidate.kind === "ticker" && candidate.ticker) {
|
|
79
83
|
return {
|
|
@@ -86,6 +90,7 @@ export function useCommandBarTickerSearchActions({
|
|
|
86
90
|
badge,
|
|
87
91
|
name,
|
|
88
92
|
right,
|
|
93
|
+
...(lines ? { lines } : {}),
|
|
89
94
|
category: candidate.category,
|
|
90
95
|
kind: "ticker",
|
|
91
96
|
resolveTicker: async () => candidate.ticker!,
|
|
@@ -112,6 +117,7 @@ export function useCommandBarTickerSearchActions({
|
|
|
112
117
|
badge,
|
|
113
118
|
name,
|
|
114
119
|
right,
|
|
120
|
+
...(lines ? { lines } : {}),
|
|
115
121
|
category: candidate.category,
|
|
116
122
|
kind: "search",
|
|
117
123
|
resolveTicker: () => resolveSearchTicker(candidate.result!, publicTickerKey(
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
validatePositionShares,
|
|
28
28
|
} from "../../../portfolio/mutations";
|
|
29
29
|
import { loadFuturesEntrySize } from "../../../portfolio/futures-terms";
|
|
30
|
+
import { wholeUnitQuantityProblem } from "../../../portfolio/position-quantity";
|
|
30
31
|
import type { CommandBarFieldValue } from "./types";
|
|
31
32
|
import type { WorkflowStringValues } from "./broker";
|
|
32
33
|
import { coerceFieldString } from "../helpers";
|
|
@@ -248,6 +249,9 @@ export function createCommandBarCollectionWorkflowActions(options: {
|
|
|
248
249
|
// Negative for a short; zero is no position. A future counts contracts, and the messages say so.
|
|
249
250
|
const quantityLabel = positionQuantityLabel(ticker);
|
|
250
251
|
const shares = readTypedNumber(coerceFieldString(values.shares), quantityLabel);
|
|
252
|
+
// 1.250 on a listing held in whole lots could be 1,250 as well, and asks the same way.
|
|
253
|
+
const lots = wholeUnitQuantityProblem(coerceFieldString(values.shares).trim(), ticker, { label: quantityLabel });
|
|
254
|
+
if (lots) throw new Error(lots);
|
|
251
255
|
const sharesError = validatePositionShares(shares, quantityLabel);
|
|
252
256
|
if (sharesError) {
|
|
253
257
|
throw new Error(t(sharesError));
|
package/src/components/index.ts
CHANGED
|
@@ -101,6 +101,8 @@ export { usePaneStatusFooter, usePaneStatusLinkFooter } from "./layout/pane/stat
|
|
|
101
101
|
export { usePaneNoticeFooter, type UsePaneNoticeFooterOptions } from "./use-pane-notice-footer";
|
|
102
102
|
export { loadingErrorFooterInfo } from "./data-table/table-pane";
|
|
103
103
|
export type { PaneFooterPressEvent, PaneFooterSegment, PaneHint } from "./layout/pane/footer";
|
|
104
|
+
// Data a source delivers late: the footer's yellow clock, its detail on hover.
|
|
105
|
+
export { DELAYED_SEGMENT, delayedFooterSegment, splitDelayedStatus } from "./layout/pane/footer";
|
|
104
106
|
export {
|
|
105
107
|
getPaneSidebarWidth,
|
|
106
108
|
getPaneSidebarWidthRange,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { memo, Profiler, useCallback, useMemo, type ReactNode } from "react";
|
|
2
2
|
import { useAppLanguage } from "../../../i18n/react";
|
|
3
3
|
import { isPerfTraceEnabled, recordPerfSample } from "../../../utils/perf-marks";
|
|
4
|
-
import { PaneInViewProvider } from "../../../state/app/activity";
|
|
4
|
+
import { PaneFocusedProvider, PaneInViewProvider } from "../../../state/app/activity";
|
|
5
5
|
import { PaneInstanceProvider } from "../../../state/app/context";
|
|
6
6
|
import { RemoteUiPaneScope } from "../../../remote/semantic-tree";
|
|
7
7
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
@@ -91,19 +91,21 @@ export const PaneContent = memo(function PaneContent({
|
|
|
91
91
|
<PaneInstanceProvider paneId={paneId}>
|
|
92
92
|
<RemoteUiPaneScope paneId={paneId}>
|
|
93
93
|
<PaneInViewProvider value={inView}>
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
<PaneFocusedProvider value={focused}>
|
|
95
|
+
<PaneKeyboardScrollController paneId={paneId} focused={focused} />
|
|
96
|
+
<Box
|
|
97
|
+
flexDirection="column"
|
|
98
|
+
flexGrow={1}
|
|
99
|
+
flexShrink={1}
|
|
100
|
+
flexBasis={0}
|
|
101
|
+
minWidth={0}
|
|
102
|
+
minHeight={0}
|
|
103
|
+
overflow="hidden"
|
|
104
|
+
data-gloom-role="pane-content"
|
|
105
|
+
>
|
|
106
|
+
{body}
|
|
107
|
+
</Box>
|
|
108
|
+
</PaneFocusedProvider>
|
|
107
109
|
</PaneInViewProvider>
|
|
108
110
|
</RemoteUiPaneScope>
|
|
109
111
|
</PaneInstanceProvider>
|
|
@@ -5,7 +5,7 @@ import { t } from "../../../../i18n";
|
|
|
5
5
|
import { ShortcutHint } from "../../../ui/shortcut-hint";
|
|
6
6
|
import { Button } from "../../../ui/button";
|
|
7
7
|
import { ChoiceDialog } from "../../../ui/choice-dialog";
|
|
8
|
-
import { IconButton } from "../../../ui/icon";
|
|
8
|
+
import { Icon, IconButton } from "../../../ui/icon";
|
|
9
9
|
import { useRemoteUiNode } from "../../../../remote/semantic-tree";
|
|
10
10
|
import { useOptionalDialog, type PromptContext } from "../../../../ui/dialog";
|
|
11
11
|
import { nativePaneFooterRows } from "../sizing";
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
|
|
24
24
|
export {
|
|
25
25
|
datesPaneData,
|
|
26
|
+
DELAYED_SEGMENT,
|
|
27
|
+
delayedFooterSegment,
|
|
26
28
|
hasPaneFooterContent,
|
|
29
|
+
splitDelayedStatus,
|
|
27
30
|
type CombinedPaneFooter,
|
|
28
31
|
type PaneFooterPressEvent,
|
|
29
32
|
type PaneFooterSegment,
|
|
@@ -105,6 +108,21 @@ function SegmentView({ segment, focused }: { segment: PaneFooterSegment; focused
|
|
|
105
108
|
else stopMouseEvent(event);
|
|
106
109
|
};
|
|
107
110
|
|
|
111
|
+
if (nativePaneChrome && segment.icon && !segment.onPress) {
|
|
112
|
+
// A status, not a control: the icon button's box, with its detail on hover.
|
|
113
|
+
return (
|
|
114
|
+
<Box
|
|
115
|
+
role="img"
|
|
116
|
+
aria-label={label}
|
|
117
|
+
title={segment.title ?? label}
|
|
118
|
+
data-gloom-role="pane-footer-icon"
|
|
119
|
+
data-icon={segment.icon}
|
|
120
|
+
>
|
|
121
|
+
<Icon name={segment.icon} size={14} color={segment.disabled ? colors.textMuted : footerToneColor(segment.parts[0] ?? { text: "" })} />
|
|
122
|
+
</Box>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
if (nativePaneChrome && segment.icon) {
|
|
109
127
|
return (
|
|
110
128
|
<IconButton
|
|
@@ -247,7 +265,7 @@ function FooterContent({
|
|
|
247
265
|
: undefined;
|
|
248
266
|
// A segment with shorter forms takes the longest that fits beside the hints, gaps and terminal keys included.
|
|
249
267
|
const keysWidth = nativePaneChrome ? 0 : footer.info.reduce((total, segment) => total + terminalSegmentKey(segment, focused).length, 0);
|
|
250
|
-
const info = infoWidth != null ? fitPaneFooterInfo(footer.info, infoWidth - keysWidth) : footer.info;
|
|
268
|
+
const info = infoWidth != null ? fitPaneFooterInfo(footer.info, infoWidth - keysWidth, { besideHints: hasHints }) : footer.info;
|
|
251
269
|
|
|
252
270
|
if (!hasInfo && !hasHints) {
|
|
253
271
|
return <Box flexGrow={1} height={1} />;
|
|
@@ -273,7 +291,8 @@ function FooterContent({
|
|
|
273
291
|
{...(infoWidth != null ? { width: infoWidth } : {})}
|
|
274
292
|
>
|
|
275
293
|
{info.map((segment, index) => (
|
|
276
|
-
|
|
294
|
+
// An icon is a few cells that mean nothing cut: the text beside it gives way first.
|
|
295
|
+
<Box key={segment.id} flexDirection="row" flexShrink={segment.icon ? 0 : undefined} marginRight={index === info.length - 1 ? 0 : 1}>
|
|
277
296
|
<SegmentView segment={segment} focused={focused} />
|
|
278
297
|
</Box>
|
|
279
298
|
))}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ContextMenuItem } from "../../../../types/context-menu";
|
|
2
2
|
import { t } from "../../../../i18n";
|
|
3
|
-
import { displayWidth } from "../../../../utils/format";
|
|
3
|
+
import { clipToDisplayWidth, displayWidth } from "../../../../utils/format";
|
|
4
4
|
import { getShortcutHintWidth } from "../../../ui/shortcut-hint";
|
|
5
5
|
|
|
6
6
|
export interface PaneFooterRegistration {
|
|
@@ -23,8 +23,11 @@ export interface PaneFooterRegistration {
|
|
|
23
23
|
export interface PaneFooterSegment {
|
|
24
24
|
id: string;
|
|
25
25
|
parts: PaneFooterPart[];
|
|
26
|
-
/**
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Desktop affordance; parts retain the terminal representation. `clock` is
|
|
28
|
+
* data a source delivers late: build it with {@link delayedFooterSegment}.
|
|
29
|
+
*/
|
|
30
|
+
icon?: "warning" | "clock";
|
|
28
31
|
/**
|
|
29
32
|
* The segment only dates the pane's data (`as of 2026-10-05`). A `gloomberb
|
|
30
33
|
* shot` whose status line already dates that data leaves it out, so the
|
|
@@ -52,6 +55,68 @@ export interface PaneFooterSegment {
|
|
|
52
55
|
disabled?: boolean;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/** The terminal's mark for data that arrives late, as quote ages carry it (`◷15s`). */
|
|
59
|
+
const DELAYED_FOOTER_GLYPH = "◷";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The footer's one form for data a source delivers late: a yellow clock with
|
|
63
|
+
* `detail` in its tooltip on the desktop, and the clock with the delay
|
|
64
|
+
* (`◷15m`) in the terminal, which has no tooltip, so it takes a few cells
|
|
65
|
+
* rather than a phrase. `onPress` makes it a button, such as an upgrade.
|
|
66
|
+
*/
|
|
67
|
+
export function delayedFooterSegment({
|
|
68
|
+
id,
|
|
69
|
+
detail,
|
|
70
|
+
delay,
|
|
71
|
+
onPress,
|
|
72
|
+
quoteStatus,
|
|
73
|
+
}: {
|
|
74
|
+
id: string;
|
|
75
|
+
/** What is late and by how much, in words: the tooltip and the accessible name. */
|
|
76
|
+
detail: string;
|
|
77
|
+
/** How late, when known (`15m`, `12h`); the terminal draws it after the clock. */
|
|
78
|
+
delay?: string;
|
|
79
|
+
onPress?: () => void;
|
|
80
|
+
quoteStatus?: boolean;
|
|
81
|
+
}): PaneFooterSegment {
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
icon: "clock",
|
|
85
|
+
label: detail,
|
|
86
|
+
title: detail,
|
|
87
|
+
parts: [{ text: `${DELAYED_FOOTER_GLYPH}${delay ?? ""}`, tone: "warning" }],
|
|
88
|
+
...(onPress ? { onPress } : {}),
|
|
89
|
+
...(quoteStatus ? { quoteStatus } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A pane whose market data is all delayed, by no one figure: the clock alone, its tooltip saying so. */
|
|
94
|
+
export const DELAYED_SEGMENT: PaneFooterSegment = delayedFooterSegment({ id: "delayed", detail: "Delayed market data" });
|
|
95
|
+
|
|
96
|
+
/** A status that opens with how late its data is: `15m delayed`, `mixed real-time and delayed`, `delayed`. */
|
|
97
|
+
const DELAYED_LEAD = /^(\d+[smhd] delayed|mixed real-time and delayed|delayed)(?: · |$)/;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A status segment whose text opens with how late its data is (`15m delayed ·
|
|
101
|
+
* as of 14:47 UTC`) as the footer's clock (see {@link delayedFooterSegment})
|
|
102
|
+
* and the rest of its text; any other segment as it is.
|
|
103
|
+
*/
|
|
104
|
+
export function splitDelayedStatus(segment: PaneFooterSegment): PaneFooterSegment[] {
|
|
105
|
+
const [first, ...more] = segment.parts;
|
|
106
|
+
const match = first ? DELAYED_LEAD.exec(first.text) : null;
|
|
107
|
+
if (!first || !match) return [segment];
|
|
108
|
+
const lead = match[1]!;
|
|
109
|
+
const clock = delayedFooterSegment({
|
|
110
|
+
id: `${segment.id}-delay`,
|
|
111
|
+
detail: lead,
|
|
112
|
+
delay: /^\d/.test(lead) ? lead.slice(0, lead.indexOf(" ")) : undefined,
|
|
113
|
+
quoteStatus: segment.quoteStatus,
|
|
114
|
+
});
|
|
115
|
+
const rest = first.text.slice(match[0].length);
|
|
116
|
+
const parts = rest ? [{ ...first, text: rest }, ...more] : more;
|
|
117
|
+
return parts.length ? [clock, { ...segment, parts }] : [clock];
|
|
118
|
+
}
|
|
119
|
+
|
|
55
120
|
/** Whether the segment only dates the pane's data: marked `asOf`, or reading `as of <date>`. */
|
|
56
121
|
export function datesPaneData(segment: Pick<PaneFooterSegment, "asOf" | "parts">): boolean {
|
|
57
122
|
return segment.asOf ?? /^as of\b/i.test(segment.parts[0]?.text ?? "");
|
|
@@ -127,7 +192,7 @@ function infoTextWidth(segments: readonly PaneFooterSegment[]): number {
|
|
|
127
192
|
* The info segments as they fit `width`: segments with `variants` step down
|
|
128
193
|
* together, one variant at a time, until the row fits or none is left.
|
|
129
194
|
*/
|
|
130
|
-
export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number): PaneFooterSegment[] {
|
|
195
|
+
export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
|
|
131
196
|
const stages = Math.max(0, ...info.map((segment) => segment.variants?.length ?? 0));
|
|
132
197
|
let fitted = info;
|
|
133
198
|
for (let stage = 0; stage < stages && infoTextWidth(fitted) > width; stage += 1) {
|
|
@@ -140,9 +205,39 @@ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number): Pan
|
|
|
140
205
|
} : segment;
|
|
141
206
|
});
|
|
142
207
|
}
|
|
208
|
+
// A trailing status cut mid-word reads as part of the hints beside it ("quote 12h t[x] put"): one that does not
|
|
209
|
+
// fit ends in an ellipsis a column short of them, or is left out when less than half of it would show. A
|
|
210
|
+
// warning, an icon or a pressable segment stays as it is, clipped by the row if it must be. With no hints beside it
|
|
211
|
+
// (a pane picture's footer), the row clips it, so a frame that fits its own status line sees it overflow.
|
|
212
|
+
while (besideHints && fitted.length > 1 && infoTextWidth(fitted) > width) {
|
|
213
|
+
const last = fitted.at(-1)!;
|
|
214
|
+
if (last.icon || last.onPress || last.parts.some((part) => part.tone === "warning" || part.tone === "negative")) break;
|
|
215
|
+
const room = width - infoTextWidth(fitted.slice(0, -1)) - 2;
|
|
216
|
+
if (room * 2 >= infoTextWidth([last])) return [...fitted.slice(0, -1), clipSegment(last, room)];
|
|
217
|
+
fitted = fitted.slice(0, -1);
|
|
218
|
+
}
|
|
143
219
|
return fitted;
|
|
144
220
|
}
|
|
145
221
|
|
|
222
|
+
/** A segment cut to `width` behind an ellipsis, each part keeping its tone; the full text stays as its title. */
|
|
223
|
+
function clipSegment(segment: PaneFooterSegment, width: number): PaneFooterSegment {
|
|
224
|
+
const full = segment.parts.map((part) => part.text).join(" ");
|
|
225
|
+
const clipped = clipToDisplayWidth(full, width);
|
|
226
|
+
if (clipped === full) return segment;
|
|
227
|
+
const body = clipped.slice(0, -1);
|
|
228
|
+
const parts: PaneFooterPart[] = [];
|
|
229
|
+
let offset = 0;
|
|
230
|
+
for (const part of segment.parts) {
|
|
231
|
+
if (offset >= body.length) break;
|
|
232
|
+
parts.push({ ...part, text: body.slice(offset, offset + part.text.length) });
|
|
233
|
+
offset += part.text.length + 1;
|
|
234
|
+
}
|
|
235
|
+
const last = parts.at(-1);
|
|
236
|
+
if (!last) return { ...segment, title: segment.title ?? full, parts: [{ ...segment.parts[0]!, text: clipped }] };
|
|
237
|
+
parts[parts.length - 1] = { ...last, text: `${last.text}…` };
|
|
238
|
+
return { ...segment, title: segment.title ?? full, parts };
|
|
239
|
+
}
|
|
240
|
+
|
|
146
241
|
function moreControl(budget: number): { label: string; width: number } {
|
|
147
242
|
const label = t("More");
|
|
148
243
|
const width = displayWidth(label) + 2;
|
|
@@ -63,10 +63,24 @@ export function NestedPaneTabs({ children }: { children: ReactNode }) {
|
|
|
63
63
|
return <PaneHeaderTabsContext.Provider value={null}>{children}</PaneHeaderTabsContext.Provider>;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** One id per artwork element, so a strip redraws when a tab's `leading` is a new element and not otherwise. */
|
|
67
|
+
const leadingIds = new WeakMap<object, number>();
|
|
68
|
+
let nextLeadingId = 1;
|
|
69
|
+
function leadingSignature(leading: ReactNode): string | number {
|
|
70
|
+
if (leading === undefined || leading === null) return "";
|
|
71
|
+
if (typeof leading !== "object") return String(leading);
|
|
72
|
+
let id = leadingIds.get(leading);
|
|
73
|
+
if (id === undefined) {
|
|
74
|
+
id = nextLeadingId++;
|
|
75
|
+
leadingIds.set(leading, id);
|
|
76
|
+
}
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
|
|
66
80
|
function registrationSignature(registration: PaneHeaderTabsRegistration | null): string {
|
|
67
81
|
if (!registration) return "";
|
|
68
82
|
return JSON.stringify([
|
|
69
|
-
registration.tabs.map((tab) => [tab.value, tab.label, tab.disabled === true, !!tab.onClose, tab.fg ?? "", tab.reorderable !== false]),
|
|
83
|
+
registration.tabs.map((tab) => [tab.value, tab.label, tab.disabled === true, !!tab.onClose, tab.fg ?? "", tab.reorderable !== false, leadingSignature(tab.leading)]),
|
|
70
84
|
registration.activeValue,
|
|
71
85
|
registration.focused === true,
|
|
72
86
|
registration.keyboardNavigation !== false,
|