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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { fxLegForCurrency } from "../market-data/coordinator/fx-legs";
|
|
2
|
+
import { fxCloseOnDate, type FxHistoryClient } from "../market-data/fx-closes";
|
|
3
|
+
import { heldCurrencyUnit } from "../utils/currency-units";
|
|
4
|
+
|
|
5
|
+
/** Reads a pair's daily bars; `apiClient` in the app, a fake in tests. */
|
|
6
|
+
export type CostFxHistoryClient = FxHistoryClient;
|
|
7
|
+
|
|
8
|
+
/** The rate a lot's cost was bought at: one unit of its currency in the portfolio's, at a dated daily close. */
|
|
9
|
+
export interface CostFxReading {
|
|
10
|
+
rate: number;
|
|
11
|
+
/** The UTC day of the FX close it was read from: the purchase date, or the last trading day before it. */
|
|
12
|
+
date: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Why no rate could be read for a purchase date; the command says to give one with --cost-fx instead. */
|
|
16
|
+
export class CostFxError extends Error {
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "CostFxError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* USD per unit of an ISO currency at its last completed daily close on or
|
|
25
|
+
* before `date`, looking back a week at most (`fxCloseOnDate`, as `fx --date`
|
|
26
|
+
* reads it). Null when the pair has no close in that week.
|
|
27
|
+
*/
|
|
28
|
+
async function usdPerUnitOn(code: string, date: string, client: CostFxHistoryClient, now: Date): Promise<CostFxReading | null> {
|
|
29
|
+
if (code === "USD") return { rate: 1, date };
|
|
30
|
+
const leg = fxLegForCurrency(code);
|
|
31
|
+
if (!leg) throw new CostFxError(`No daily FX history is served for ${code}`);
|
|
32
|
+
return fxCloseOnDate(leg, date, client, now);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What one unit of `from` (a lot's currency, GBp counting pence and USX cents) cost in
|
|
37
|
+
* `into` (the portfolio's) at the daily FX closes of `date`, each pair's last
|
|
38
|
+
* close on or before it, crossed through USD as every converted value is.
|
|
39
|
+
* Throws `CostFxError` when a pair is not served or has no close in the week
|
|
40
|
+
* before the date.
|
|
41
|
+
*/
|
|
42
|
+
export async function loadCostFxRate(
|
|
43
|
+
from: string,
|
|
44
|
+
into: string,
|
|
45
|
+
date: string,
|
|
46
|
+
client: CostFxHistoryClient,
|
|
47
|
+
now = new Date(),
|
|
48
|
+
): Promise<CostFxReading> {
|
|
49
|
+
const fromUnit = heldCurrencyUnit(from);
|
|
50
|
+
const intoUnit = heldCurrencyUnit(into);
|
|
51
|
+
const [fromUsd, intoUsd] = await Promise.all([
|
|
52
|
+
usdPerUnitOn(fromUnit.currency, date, client, now),
|
|
53
|
+
usdPerUnitOn(intoUnit.currency, date, client, now),
|
|
54
|
+
]);
|
|
55
|
+
const missing = [fromUsd ? null : fromUnit.currency, intoUsd ? null : intoUnit.currency].filter(Boolean);
|
|
56
|
+
if (!fromUsd || !intoUsd) throw new CostFxError(`No ${missing.join(" or ")} FX close in the week up to ${date}`);
|
|
57
|
+
const rate = (fromUsd.rate / fromUnit.divisor) / (intoUsd.rate / intoUnit.divisor);
|
|
58
|
+
if (!Number.isFinite(rate) || rate <= 0) throw new CostFxError(`No usable ${fromUnit.currency}/${intoUnit.currency} rate for ${date}`);
|
|
59
|
+
// A pair that did not trade that day (its holiday) dates the cross by its own last close.
|
|
60
|
+
return { rate, date: fromUsd.date < intoUsd.date ? fromUsd.date : intoUsd.date };
|
|
61
|
+
}
|
|
@@ -39,6 +39,13 @@ export interface SetManualPortfolioPositionInput {
|
|
|
39
39
|
* read does not unsize it.
|
|
40
40
|
*/
|
|
41
41
|
multiplier?: number;
|
|
42
|
+
/** The purchase date, YYYY-MM-DD. Left out, a single manual lot keeps the one it had. */
|
|
43
|
+
dateAcquired?: string;
|
|
44
|
+
/**
|
|
45
|
+
* The rate the cost was bought at: one unit of `currency` in `costFx.currency`.
|
|
46
|
+
* Null clears it; left out, a single manual lot in the same currency keeps the one it had.
|
|
47
|
+
*/
|
|
48
|
+
costFx?: { rate: number; currency: string } | null;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
/** What a position's size counts, as its field and messages name it. */
|
|
@@ -406,8 +413,14 @@ export function setManualPortfolioPosition(
|
|
|
406
413
|
const replacedPositionCount = replacedPositions.length;
|
|
407
414
|
// Editing a single snapshot (for example after a split) does not change its
|
|
408
415
|
// acquisition date. A consolidated replacement of several lots has no one date.
|
|
409
|
-
const
|
|
410
|
-
|
|
416
|
+
const single = replacedPositions.length === 1 && replacedPositions[0]?.broker === "manual" ? replacedPositions[0] : null;
|
|
417
|
+
const dateAcquired = input.dateAcquired ?? single?.dateAcquired;
|
|
418
|
+
// The rate the cost was bought at goes with its cost: a lot kept in its currency keeps it unless it is replaced or cleared.
|
|
419
|
+
const keptCostFx = single && single.currency?.trim().toUpperCase() === nextCurrency
|
|
420
|
+
&& typeof single.costFxRate === "number" && single.costFxCurrency
|
|
421
|
+
? { rate: single.costFxRate, currency: single.costFxCurrency } : null;
|
|
422
|
+
const costFx = input.costFx === undefined ? keptCostFx : input.costFx;
|
|
423
|
+
if (costFx && !(Number.isFinite(costFx.rate) && costFx.rate > 0)) throw new Error("The cost FX rate must be a positive number.");
|
|
411
424
|
// A contract's size does not change with the count held; one the lots agree on carries over.
|
|
412
425
|
const keptMultipliers = new Set(replacedPositions.map((position) => position.multiplier));
|
|
413
426
|
const keptMultiplier = keptMultipliers.size === 1 ? [...keptMultipliers][0] : undefined;
|
|
@@ -422,6 +435,7 @@ export function setManualPortfolioPosition(
|
|
|
422
435
|
currency: nextCurrency,
|
|
423
436
|
broker: "manual",
|
|
424
437
|
...(dateAcquired ? { dateAcquired } : {}),
|
|
438
|
+
...(costFx ? { costFxRate: costFx.rate, costFxCurrency: costFx.currency.trim().toUpperCase() } : {}),
|
|
425
439
|
...(multiplier !== undefined ? { multiplier } : {}),
|
|
426
440
|
});
|
|
427
441
|
|
|
@@ -3,7 +3,7 @@ import type { Quote } from "../types/financials";
|
|
|
3
3
|
import type { PriceBasis } from "../types/instrument";
|
|
4
4
|
import { resolvePriceBasis } from "../market-data/market/price-basis";
|
|
5
5
|
import { getActiveQuoteDisplay, type ActiveQuoteDisplay } from "../market-data/market/status";
|
|
6
|
-
import { resolveCurrencyUnit } from "../utils/currency-units";
|
|
6
|
+
import { heldCurrencyUnit, resolveCurrencyUnit } from "../utils/currency-units";
|
|
7
7
|
import { isFuturesTicker } from "./futures-positions";
|
|
8
8
|
|
|
9
9
|
export interface PortfolioPositionMetrics {
|
|
@@ -103,6 +103,42 @@ function resolvePositionCostMultiplier(position: TickerRecord["metadata"]["posit
|
|
|
103
103
|
return withoutMultiplierError < withMultiplierError ? 1 : priceMultiplier;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* The rate a lot's cost converts at when valued in `valuationCurrency`: the
|
|
108
|
+
* one it was bought at (`costFxRate`), when that rate converts into this very
|
|
109
|
+
* currency and the lot is held in another. Null otherwise, and the cost
|
|
110
|
+
* converts at today's rate as its value does.
|
|
111
|
+
*/
|
|
112
|
+
export function lotCostFxRate(
|
|
113
|
+
position: Pick<TickerPosition, "costFxRate" | "costFxCurrency">,
|
|
114
|
+
positionCurrency: string,
|
|
115
|
+
valuationCurrency: string,
|
|
116
|
+
): number | null {
|
|
117
|
+
const rate = position.costFxRate;
|
|
118
|
+
const into = position.costFxCurrency?.trim();
|
|
119
|
+
if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0 || !into) return null;
|
|
120
|
+
return into === valuationCurrency.trim() && !sameCurrency(positionCurrency, into) ? rate : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Two currency codes in one currency, its minor units included: GBp is GBP, USX is USD. */
|
|
124
|
+
export function sameCurrency(left: string, right: string): boolean {
|
|
125
|
+
return heldCurrencyUnit(left).currency === heldCurrencyUnit(right).currency;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Whether a lot's cost, valued in `valuationCurrency`, converts at today's
|
|
130
|
+
* rate: it is held in another currency and no rate from when it was bought
|
|
131
|
+
* applies, so its P&L leaves out the currency's move since.
|
|
132
|
+
*/
|
|
133
|
+
export function lotCostAtTodaysRate(
|
|
134
|
+
position: Pick<TickerPosition, "costFxRate" | "costFxCurrency">,
|
|
135
|
+
positionCurrency: string,
|
|
136
|
+
valuationCurrency: string,
|
|
137
|
+
): boolean {
|
|
138
|
+
return !!positionCurrency.trim() && !sameCurrency(positionCurrency, valuationCurrency)
|
|
139
|
+
&& lotCostFxRate(position, positionCurrency, valuationCurrency) === null;
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
export function getPortfolioPositionMetrics(
|
|
107
143
|
ticker: TickerRecord,
|
|
108
144
|
activeTab: string | undefined,
|
|
@@ -128,6 +164,11 @@ export function getPortfolioPositionMetrics(
|
|
|
128
164
|
if (!currency) return Number.NaN;
|
|
129
165
|
return valuation ? valuation.convert(value, currency) : currencies.size > 1 ? Number.NaN : value;
|
|
130
166
|
};
|
|
167
|
+
// A cost converts at the rate the lot was bought at, when it has one into the valuation's currency.
|
|
168
|
+
const convertCost = (value: number, position: typeof positions[number]) => {
|
|
169
|
+
const bought = valuation ? lotCostFxRate(position, currencyFor(position), valuation.currency) : null;
|
|
170
|
+
return bought !== null ? value * bought : convert(value, position);
|
|
171
|
+
};
|
|
131
172
|
const metrics: PortfolioPositionMetrics = {
|
|
132
173
|
positionCurrency, positionCount: positions.length, hasShorts: false,
|
|
133
174
|
totalShares: 0, totalCost: 0, hasCostBasis: positions.length > 0, priceBasis, signedCost: 0, totalCostUnits: 0,
|
|
@@ -169,8 +210,8 @@ export function getPortfolioPositionMetrics(
|
|
|
169
210
|
metrics.unknownContractSize ||= unknownSize && !Number.isFinite(position.marketValue);
|
|
170
211
|
if (Number.isFinite(contractMultiplier)) metrics.multiplierHint = Math.max(metrics.multiplierHint, contractMultiplier);
|
|
171
212
|
metrics.totalShares += magnitude * direction;
|
|
172
|
-
metrics.totalCost +=
|
|
173
|
-
metrics.signedCost += direction *
|
|
213
|
+
metrics.totalCost += convertCost(cost, position);
|
|
214
|
+
metrics.signedCost += direction * convertCost(cost, position);
|
|
174
215
|
metrics.totalCostUnits += magnitude * costMultiplier;
|
|
175
216
|
metrics.totalPriceUnits += magnitude * quoteMultiplier * direction;
|
|
176
217
|
metrics.grossPriceUnits += magnitude * quoteMultiplier;
|
|
@@ -191,7 +232,7 @@ export function getPortfolioPositionMetrics(
|
|
|
191
232
|
if (pnl == null || !Number.isFinite(pnl)) metrics.hasBrokerPnl = false;
|
|
192
233
|
else metrics.brokerPnl += convert(pnl, position);
|
|
193
234
|
metrics.pnlLots.push({
|
|
194
|
-
signedCost: direction *
|
|
235
|
+
signedCost: direction * convertCost(cost, position),
|
|
195
236
|
priceUnits: magnitude * quoteMultiplier * direction,
|
|
196
237
|
direction,
|
|
197
238
|
brokerMarketValue: marketValue != null && Number.isFinite(marketValue) ? convert(marketValue, position) : null,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { resolveAssetDisplayKind } from "../market-data/market/format";
|
|
2
|
+
import type { TickerRecord } from "../types/ticker";
|
|
3
|
+
import { ambiguousNumberMessage, thousandsDotReadings } from "../utils/decimal-comma";
|
|
4
|
+
import { isKnownNonUsListing } from "../utils/sec";
|
|
5
|
+
import { isFuturesTicker } from "./futures-positions";
|
|
6
|
+
import { positionQuantityLabel, type PositionQuantityLabel } from "./mutations";
|
|
7
|
+
|
|
8
|
+
/** Fund units are issued in fractions wherever the fund is listed. */
|
|
9
|
+
const FRACTIONAL_FUND_TYPES = new Set(["FUND", "MUTUALFUND"]);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Whether a listing is held in whole units: a future's or an option's
|
|
13
|
+
* contracts, and shares listed outside the US, which trade in whole lots
|
|
14
|
+
* (THYAO.IS, SAP:XETRA). US shares, which brokers split into fractions,
|
|
15
|
+
* mutual funds, coins, currencies and bonds may be held in fractions.
|
|
16
|
+
*/
|
|
17
|
+
function tradesInWholeUnits(ticker: TickerRecord): boolean {
|
|
18
|
+
const kind = resolveAssetDisplayKind({ assetCategory: ticker.metadata.assetCategory });
|
|
19
|
+
if (isFuturesTicker(ticker) || kind === "contract") return true;
|
|
20
|
+
const category = ticker.metadata.assetCategory?.trim().toUpperCase().replace(/[\s_-]/g, "") ?? "";
|
|
21
|
+
const shares = kind === "equity" ? !FRACTIONAL_FUND_TYPES.has(category) : kind === "other" && !category;
|
|
22
|
+
return shares && isKnownNonUsListing(ticker);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a typed size cannot be stored as read: a dot and three digits (`1.250`)
|
|
27
|
+
* on a listing held in whole units, where it is far likelier the thousands
|
|
28
|
+
* grouping many locales write (1,250 shares) than a quarter of a share, so the
|
|
29
|
+
* command asks which, as it does for `1,250`. Null for any other size, and
|
|
30
|
+
* for a listing that may be held in fractions, where `1.250` is 1.25.
|
|
31
|
+
*/
|
|
32
|
+
export function wholeUnitQuantityProblem(
|
|
33
|
+
typed: string,
|
|
34
|
+
ticker: TickerRecord,
|
|
35
|
+
options: { label?: PositionQuantityLabel; listing?: string } = {},
|
|
36
|
+
): string | null {
|
|
37
|
+
const reading = thousandsDotReadings(typed);
|
|
38
|
+
if (!reading || !tradesInWholeUnits(ticker)) return null;
|
|
39
|
+
const label = options.label ?? positionQuantityLabel(ticker);
|
|
40
|
+
const listing = options.listing ?? ticker.metadata.ticker;
|
|
41
|
+
const because = `${listing} trades in whole ${label === "Contracts" ? "contracts" : "shares"}`;
|
|
42
|
+
return `${label} ${ambiguousNumberMessage(typed, reading, { because })}.`;
|
|
43
|
+
}
|
|
@@ -4,6 +4,7 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
|
|
|
4
4
|
{
|
|
5
5
|
".": [],
|
|
6
6
|
"./broker": [
|
|
7
|
+
"LOCAL_BROKER_MARKET_DATA",
|
|
7
8
|
"PRESERVED_PASSWORD_HINT",
|
|
8
9
|
"cloudBrokerLink",
|
|
9
10
|
"getBrokerRemoteClient",
|
|
@@ -53,6 +54,7 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
|
|
|
53
54
|
"CompositeChart",
|
|
54
55
|
"ConfirmDialog",
|
|
55
56
|
"CurveSurface",
|
|
57
|
+
"DELAYED_SEGMENT",
|
|
56
58
|
"DataTable",
|
|
57
59
|
"DataTableStackView",
|
|
58
60
|
"DataTableView",
|
|
@@ -132,6 +134,7 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
|
|
|
132
134
|
"curveSlope",
|
|
133
135
|
"curveStrip",
|
|
134
136
|
"curveSurfaceMinRows",
|
|
137
|
+
"delayedFooterSegment",
|
|
135
138
|
"fieldFrameStyle",
|
|
136
139
|
"fieldGridColumns",
|
|
137
140
|
"fieldGridRows",
|
|
@@ -159,6 +162,7 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
|
|
|
159
162
|
"sortStackItems",
|
|
160
163
|
"spanAxisFormatter",
|
|
161
164
|
"spanDigits",
|
|
165
|
+
"splitDelayedStatus",
|
|
162
166
|
"stackedSeries",
|
|
163
167
|
"statGridColumns",
|
|
164
168
|
"statGridRows",
|
|
@@ -201,6 +205,7 @@ exports[`the runtime exports of every public subpath are the reviewed ones 1`] =
|
|
|
201
205
|
],
|
|
202
206
|
"./market-data": [
|
|
203
207
|
"computeTickerPriceReturns",
|
|
208
|
+
"createProviderMiss",
|
|
204
209
|
"currencyMinorDigits",
|
|
205
210
|
"formatCompactMarketPriceWithCurrency",
|
|
206
211
|
"formatMarketChangeWithCurrency",
|
package/src/public/broker.ts
CHANGED
|
@@ -20,6 +20,14 @@ export type { BrokerRemoteClient } from "../brokers/remote-broker-adapter";
|
|
|
20
20
|
export type { ResourceStore } from "../data/resource-store";
|
|
21
21
|
export { resolveTickerFinancialsForInstrument } from "../market-data/coordinator";
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* This host's Cloud sync honors `BrokerAdapter.localMarketData`. A broker whose market data must
|
|
25
|
+
* not reach Gloom Cloud reads it to tell an older host, which would sync its quotes and where it
|
|
26
|
+
* still has to ask for Cloud to be off: `import * as broker from "gloomberb/broker"`, then
|
|
27
|
+
* `broker.LOCAL_BROKER_MARKET_DATA === true`.
|
|
28
|
+
*/
|
|
29
|
+
export const LOCAL_BROKER_MARKET_DATA = true;
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* The app's cached-resource store. Set by the host at startup and cleared on
|
|
25
33
|
* teardown; null before services are constructed.
|
|
@@ -25,6 +25,11 @@ export type {
|
|
|
25
25
|
AssetDisplayContext, AssetDisplayKind, MarketFormatOptions, StablePriceContext,
|
|
26
26
|
} from "../market-data/market/format";
|
|
27
27
|
|
|
28
|
+
// How a provider says it has nothing for a symbol, as built-in sources do: the
|
|
29
|
+
// router tries the next provider, does not log it as a failure, and shows the
|
|
30
|
+
// reason where the data would be.
|
|
31
|
+
export { createProviderMiss } from "../sources/provider-errors";
|
|
32
|
+
|
|
28
33
|
// The coordinator the app loads snapshots through, so a plugin pane that shows
|
|
29
34
|
// its own list of tickers warms the same cache the rest of the app reads.
|
|
30
35
|
export { getSharedMarketDataCoordinator } from "../market-data/coordinator";
|
|
@@ -35,9 +35,14 @@ export function controlBorderColor(focused = false, active = false, palette: The
|
|
|
35
35
|
* casts a shadow. The icon button is `.gloom-icon-button` in styles.css.
|
|
36
36
|
*/
|
|
37
37
|
export function buttonPalette(
|
|
38
|
-
props: Pick<ButtonProps, "variant" | "active" | "disabled">,
|
|
38
|
+
props: Pick<ButtonProps, "variant" | "active" | "disabled" | "tone">,
|
|
39
39
|
palette: ThemeColors = colors,
|
|
40
40
|
) {
|
|
41
|
+
if (props.variant === "quiet") return {
|
|
42
|
+
fg: props.disabled ? palette.textMuted : props.active ? palette.textBright : palette.textDim,
|
|
43
|
+
bg: props.active ? selectedPanelFill(palette) : "transparent",
|
|
44
|
+
border: "transparent",
|
|
45
|
+
};
|
|
41
46
|
if (props.variant === "plain" || props.variant === "ghost") {
|
|
42
47
|
return {
|
|
43
48
|
fg: props.disabled ? palette.textMuted : props.active ? palette.textBright : palette.textDim,
|
|
@@ -52,6 +57,7 @@ export function buttonPalette(
|
|
|
52
57
|
border: controlBorderColour(palette),
|
|
53
58
|
};
|
|
54
59
|
}
|
|
60
|
+
if (props.tone && props.variant === "primary") return { fg: palette.bg, bg: palette[props.tone], border: props.active ? palette.textBright : palette[props.tone] };
|
|
55
61
|
if (props.active) {
|
|
56
62
|
return {
|
|
57
63
|
fg: palette.selectedText,
|
|
@@ -37,6 +37,7 @@ export function WebButton({
|
|
|
37
37
|
expanded,
|
|
38
38
|
onPress,
|
|
39
39
|
variant = "secondary",
|
|
40
|
+
tone,
|
|
40
41
|
disabled = false,
|
|
41
42
|
pending = false,
|
|
42
43
|
active = false,
|
|
@@ -49,7 +50,7 @@ export function WebButton({
|
|
|
49
50
|
title,
|
|
50
51
|
}: ButtonProps) {
|
|
51
52
|
const colors = useThemeColors();
|
|
52
|
-
const palette = buttonPalette({ variant, active, disabled }, colors);
|
|
53
|
+
const palette = buttonPalette({ variant, active, disabled, tone }, colors);
|
|
53
54
|
const filled = active || variant === "primary" || variant === "danger";
|
|
54
55
|
|
|
55
56
|
return (
|
|
@@ -261,10 +262,12 @@ const COMFORTABLE_FIELD_HEIGHT = 26;
|
|
|
261
262
|
|
|
262
263
|
export function WebTextField({
|
|
263
264
|
label,
|
|
265
|
+
accessibleLabel,
|
|
264
266
|
value,
|
|
265
267
|
placeholder,
|
|
266
268
|
maxLength,
|
|
267
269
|
focused,
|
|
270
|
+
disabled = false,
|
|
268
271
|
width,
|
|
269
272
|
inputRef,
|
|
270
273
|
onChange,
|
|
@@ -290,6 +293,7 @@ export function WebTextField({
|
|
|
290
293
|
const renderer = useRendererHost();
|
|
291
294
|
const { nativeContextMenu } = useUiCapabilities();
|
|
292
295
|
const plain = variant === "plain";
|
|
296
|
+
const well = variant === "well";
|
|
293
297
|
const comfortable = size === "comfortable";
|
|
294
298
|
|
|
295
299
|
return (
|
|
@@ -311,6 +315,7 @@ export function WebTextField({
|
|
|
311
315
|
alignItems="center"
|
|
312
316
|
backgroundColor={plain ? "transparent" : resolvedBackgroundColor}
|
|
313
317
|
onMouseDown={() => {
|
|
318
|
+
if (disabled) return;
|
|
314
319
|
onMouseDown?.();
|
|
315
320
|
resolvedInputRef.current?.focus?.();
|
|
316
321
|
}}
|
|
@@ -324,12 +329,16 @@ export function WebTextField({
|
|
|
324
329
|
data-gloom-role="desktop-text-field"
|
|
325
330
|
style={{
|
|
326
331
|
height: comfortable ? COMFORTABLE_FIELD_HEIGHT : undefined,
|
|
327
|
-
border: plain ? "none" : `1px solid ${controlBorderColor(focused, false, colors)}`,
|
|
328
|
-
borderRadius: plain ? 0 : CONTROL_RADIUS,
|
|
332
|
+
border: plain || well ? "none" : `1px solid ${controlBorderColor(focused, false, colors)}`,
|
|
333
|
+
borderRadius: plain ? 0 : well ? "var(--gloom-radius-sm)" : CONTROL_RADIUS,
|
|
334
|
+
// A well has no border: focus is the accent along its foot.
|
|
335
|
+
boxShadow: well && focused ? "inset 0 -2px var(--gloom-accent)" : undefined,
|
|
329
336
|
overflow: "hidden",
|
|
330
337
|
}}
|
|
331
338
|
>
|
|
332
339
|
<Input
|
|
340
|
+
aria-label={accessibleLabel ?? label}
|
|
341
|
+
disabled={disabled}
|
|
333
342
|
ref={resolvedInputRef as RefObject<InputRenderable | null>}
|
|
334
343
|
width="100%"
|
|
335
344
|
value={value}
|
|
@@ -459,15 +468,18 @@ export function WebMessageComposer({
|
|
|
459
468
|
}
|
|
460
469
|
|
|
461
470
|
export function WebSegmentedControl({
|
|
471
|
+
accessibleLabel,
|
|
462
472
|
options,
|
|
463
473
|
value,
|
|
464
474
|
onChange,
|
|
465
475
|
focused,
|
|
466
476
|
width,
|
|
467
477
|
wrap = false,
|
|
478
|
+
size = "default",
|
|
468
479
|
}: SegmentedControlProps) {
|
|
469
480
|
const colors = useThemeColors();
|
|
470
481
|
const enabled = options.filter((option) => !option.disabled);
|
|
482
|
+
const large = size === "large";
|
|
471
483
|
const selectAdjacent = (direction: -1 | 1) => {
|
|
472
484
|
const index = enabled.findIndex((option) => option.value === value);
|
|
473
485
|
const next = enabled[index < 0 ? 0 : (index + direction + enabled.length) % enabled.length];
|
|
@@ -476,26 +488,37 @@ export function WebSegmentedControl({
|
|
|
476
488
|
|
|
477
489
|
// A segmented choice is a tab strip (the tab rule in styles.css): text, the
|
|
478
490
|
// chosen one bright and bold on the accent underline. One row tall, so it
|
|
479
|
-
// never bleeds over the pane title or the table header around it.
|
|
491
|
+
// never bleeds over the pane title or the table header around it. A large
|
|
492
|
+
// one, such as an order's side, is two rows of blocks that share the width,
|
|
493
|
+
// the chosen one filled in its tone.
|
|
480
494
|
return (
|
|
481
495
|
<Box
|
|
482
496
|
flexDirection="row"
|
|
483
497
|
flexWrap={wrap ? "wrap" : "nowrap"}
|
|
484
498
|
width={width}
|
|
485
|
-
height={wrap ? undefined : 1}
|
|
499
|
+
height={wrap ? undefined : large ? 2 : 1}
|
|
486
500
|
alignItems="stretch"
|
|
487
501
|
role="radiogroup"
|
|
502
|
+
aria-label={accessibleLabel}
|
|
488
503
|
data-gloom-role="segmented-control"
|
|
504
|
+
data-size={large ? "large" : undefined}
|
|
489
505
|
data-focused={focused ? "true" : undefined}
|
|
506
|
+
style={large ? {
|
|
507
|
+
borderRadius: "var(--gloom-radius-md)",
|
|
508
|
+
overflow: "hidden",
|
|
509
|
+
boxShadow: focused ? "0 0 0 2px var(--gloom-accent)" : undefined,
|
|
510
|
+
} : undefined}
|
|
490
511
|
>
|
|
491
512
|
{options.map((option) => {
|
|
492
513
|
const active = option.value === value;
|
|
514
|
+
const tone = option.tone ? colors[option.tone] : undefined;
|
|
493
515
|
return (
|
|
494
516
|
<Box
|
|
495
517
|
key={option.value}
|
|
496
518
|
flexDirection="row"
|
|
497
519
|
alignItems="center"
|
|
498
520
|
justifyContent="center"
|
|
521
|
+
flexGrow={large ? 1 : undefined}
|
|
499
522
|
onMouseDown={() => {
|
|
500
523
|
if (!option.disabled) onChange?.(option.value);
|
|
501
524
|
}}
|
|
@@ -503,7 +526,7 @@ export function WebSegmentedControl({
|
|
|
503
526
|
// after the keyboard has moved on to another pane.
|
|
504
527
|
onClick={(event: { currentTarget: unknown; detail: number }) => releasePointerFocus(event.currentTarget, event.detail)}
|
|
505
528
|
data-gloom-interactive={option.disabled ? undefined : "true"}
|
|
506
|
-
data-gloom-role="tab-button"
|
|
529
|
+
data-gloom-role={large ? "segment-block" : "tab-button"}
|
|
507
530
|
data-active={active ? "true" : undefined}
|
|
508
531
|
role="radio"
|
|
509
532
|
aria-checked={active}
|
|
@@ -521,9 +544,18 @@ export function WebSegmentedControl({
|
|
|
521
544
|
onChange?.(option.value);
|
|
522
545
|
}
|
|
523
546
|
}}
|
|
524
|
-
style={{
|
|
547
|
+
style={large ? {
|
|
548
|
+
height: "100%",
|
|
549
|
+
cursor: option.disabled ? "default" : "pointer",
|
|
550
|
+
outline: "none",
|
|
551
|
+
fontSize: "var(--gloom-type-heading)",
|
|
552
|
+
fontWeight: "var(--gloom-weight-bold)",
|
|
553
|
+
background: option.disabled ? "var(--gloom-panel)" : active ? tone ?? colors.selected : "var(--gloom-panel)",
|
|
554
|
+
color: option.disabled ? colors.textMuted : active ? tone ? colors.bg : colors.selectedText : tone ?? colors.textDim,
|
|
555
|
+
} : {
|
|
525
556
|
height: wrap ? undefined : "100%",
|
|
526
557
|
cursor: option.disabled ? "default" : "pointer",
|
|
558
|
+
...(tone ? { "--tab-fg": tone, "--tab-active-fg": tone } as CSSProperties : {}),
|
|
527
559
|
...(option.disabled ? { color: colors.textMuted } : {}),
|
|
528
560
|
}}
|
|
529
561
|
>
|
|
@@ -151,6 +151,20 @@ const ICONS: Record<IconName, ReactNode> = {
|
|
|
151
151
|
<path d="M1.5 5.3h9M4.8 2.5 4.3 5.3 6 10.1l1.7-4.8-.5-2.8" stroke="currentColor" strokeWidth=".9" strokeLinejoin="round" />
|
|
152
152
|
</>
|
|
153
153
|
),
|
|
154
|
+
// A clock face at ten past: data that arrives late.
|
|
155
|
+
clock: (
|
|
156
|
+
<>
|
|
157
|
+
<circle cx="6" cy="6" r="4.4" stroke="currentColor" strokeWidth="1.2" />
|
|
158
|
+
<path d="M6 3.4V6l1.9 1.2" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
159
|
+
</>
|
|
160
|
+
),
|
|
161
|
+
// A pencil, for changing something in place.
|
|
162
|
+
edit: (
|
|
163
|
+
<>
|
|
164
|
+
<path d="M2.2 9.8l.5-2.3 5.4-5.4a1.1 1.1 0 0 1 1.6 0l.2.2a1.1 1.1 0 0 1 0 1.6L4.5 9.3l-2.3.5Z" stroke="currentColor" strokeWidth="1.1" strokeLinejoin="round" />
|
|
165
|
+
<path d="M7.3 2.9l1.8 1.8" stroke="currentColor" strokeWidth="1.1" />
|
|
166
|
+
</>
|
|
167
|
+
),
|
|
154
168
|
restore: (
|
|
155
169
|
<>
|
|
156
170
|
<rect x="2" y="4" width="6" height="6" rx="0.8" stroke="currentColor" strokeWidth="1.1" />
|
|
@@ -3,6 +3,7 @@ import { useEffect, useId, useRef, useState, type CSSProperties } from "react";
|
|
|
3
3
|
import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../../ui";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
5
|
import type { ListRowState, ListViewItem, ListViewProps } from "../../../components/ui/list-view";
|
|
6
|
+
import { ListRowLines } from "../../../components/ui/list-row-lines";
|
|
6
7
|
import { hoverBg, type ThemeColors } from "../../../theme/colors";
|
|
7
8
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
8
9
|
import { WEB_CELL_HEIGHT } from "../../../theme/font-scale";
|
|
@@ -18,9 +19,12 @@ import {
|
|
|
18
19
|
function DefaultDesktopRow({
|
|
19
20
|
item,
|
|
20
21
|
selected,
|
|
22
|
+
stacked,
|
|
21
23
|
}: {
|
|
22
24
|
item: ListViewItem;
|
|
23
25
|
selected: boolean;
|
|
26
|
+
/** The description on a second line under the label (`rowDescriptions`). */
|
|
27
|
+
stacked: boolean;
|
|
24
28
|
}) {
|
|
25
29
|
const colors = useThemeColors();
|
|
26
30
|
return (
|
|
@@ -28,28 +32,52 @@ function DefaultDesktopRow({
|
|
|
28
32
|
flexDirection="row"
|
|
29
33
|
justifyContent="space-between"
|
|
30
34
|
width="100%"
|
|
35
|
+
// A two-line row is taller than its lines: they sit in its middle.
|
|
36
|
+
height={stacked ? "100%" : undefined}
|
|
31
37
|
minWidth={0}
|
|
32
38
|
alignItems="center"
|
|
33
39
|
style={{ boxSizing: "border-box" }}
|
|
34
40
|
>
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
41
|
+
{stacked ? (
|
|
42
|
+
<ListRowLines
|
|
43
|
+
label={item.label}
|
|
44
|
+
description={item.description}
|
|
45
|
+
leading={item.leading}
|
|
46
|
+
labelColor={item.color}
|
|
47
|
+
labelBold={selected}
|
|
48
|
+
/>
|
|
49
|
+
) : (
|
|
50
|
+
<Box
|
|
51
|
+
flexDirection="row"
|
|
52
|
+
alignItems="center"
|
|
53
|
+
minWidth={0}
|
|
54
|
+
flexShrink={1}
|
|
55
|
+
overflow="hidden"
|
|
56
|
+
style={item.leading !== undefined ? { gap: "var(--gloom-space-sm)" } : undefined}
|
|
49
57
|
>
|
|
50
|
-
{item.
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
{item.leading !== undefined && (
|
|
59
|
+
<Box flexShrink={0} alignItems="center" aria-hidden="true" data-gloom-role="list-row-leading">
|
|
60
|
+
{item.leading}
|
|
61
|
+
</Box>
|
|
62
|
+
)}
|
|
63
|
+
<Text
|
|
64
|
+
fg={item.color ?? (selected ? colors.text : colors.textDim)}
|
|
65
|
+
attributes={selected ? TextAttributes.BOLD : 0}
|
|
66
|
+
style={{
|
|
67
|
+
// The base text style pins flexShrink to 0, which lets a long
|
|
68
|
+
// label run under the detail instead of shrinking to an ellipsis.
|
|
69
|
+
flexShrink: 1,
|
|
70
|
+
minWidth: 0,
|
|
71
|
+
maxWidth: "100%",
|
|
72
|
+
overflow: "hidden",
|
|
73
|
+
textOverflow: "ellipsis",
|
|
74
|
+
whiteSpace: "nowrap",
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{item.label}
|
|
78
|
+
</Text>
|
|
79
|
+
</Box>
|
|
80
|
+
)}
|
|
53
81
|
{item.detail && (
|
|
54
82
|
<Text fg={colors.textMuted} style={{ flexShrink: 0, marginLeft: 12 }}>
|
|
55
83
|
{item.detail}
|
|
@@ -59,6 +87,9 @@ function DefaultDesktopRow({
|
|
|
59
87
|
);
|
|
60
88
|
}
|
|
61
89
|
|
|
90
|
+
/** A row with its description under its label: the two lines and a quarter row above and below them. */
|
|
91
|
+
const STACKED_ROW_HEIGHT = 2.5;
|
|
92
|
+
|
|
62
93
|
const FRAMED_LIST_PADDING_PX = 4;
|
|
63
94
|
/** What a framed list adds above and below its rows: padding and a 1px border on each side. */
|
|
64
95
|
const FRAMED_LIST_CHROME_PX = 2 * (FRAMED_LIST_PADDING_PX + 1);
|
|
@@ -84,12 +115,13 @@ export function WebListView({
|
|
|
84
115
|
renderRow,
|
|
85
116
|
getRowBackgroundColor,
|
|
86
117
|
showSelectedDescription = false,
|
|
118
|
+
rowDescriptions = false,
|
|
87
119
|
emptyMessage = "Nothing to show.",
|
|
88
120
|
bgColor,
|
|
89
121
|
selectedBgColor,
|
|
90
122
|
hoverBgColor,
|
|
91
123
|
rowGap = 1,
|
|
92
|
-
rowHeight = 1,
|
|
124
|
+
rowHeight = rowDescriptions ? STACKED_ROW_HEIGHT : 1,
|
|
93
125
|
surface,
|
|
94
126
|
selection = "frame",
|
|
95
127
|
height,
|
|
@@ -242,7 +274,7 @@ export function WebListView({
|
|
|
242
274
|
>
|
|
243
275
|
{renderRow
|
|
244
276
|
? renderRow(item, state, index)
|
|
245
|
-
: <DefaultDesktopRow item={item} selected={selected} />}
|
|
277
|
+
: <DefaultDesktopRow item={item} selected={selected} stacked={rowDescriptions} />}
|
|
246
278
|
</Box>
|
|
247
279
|
);
|
|
248
280
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource react */
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
3
3
|
import type { HostMenuItem, HostMenuProps } from "../../../ui/host";
|
|
4
4
|
import { CheckboxBox } from "./controls";
|
|
5
5
|
import { WebIcon } from "./icons";
|
|
@@ -10,9 +10,12 @@ function selectable(item: HostMenuItem): boolean {
|
|
|
10
10
|
return item.kind !== "divider" && item.kind !== "heading" && !item.disabled;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const LEADING_STYLE: CSSProperties = { display: "inline-flex", flex: "none", alignItems: "center" };
|
|
14
|
+
|
|
13
15
|
/** Desktop Menu: the one list every popover menu in the app renders. */
|
|
14
16
|
export function WebMenu({ items, onSelect, selection = "none", title, label, onClose, highlightedId, onHighlight }: HostMenuProps) {
|
|
15
17
|
const enabled = useMemo(() => items.filter(selectable), [items]);
|
|
18
|
+
const alignLeading = items.find((item) => item.leading !== undefined)?.leading;
|
|
16
19
|
const controlled = highlightedId !== undefined;
|
|
17
20
|
const [ownHighlighted, setOwnHighlighted] = useState<string | null>(
|
|
18
21
|
() => items.find((item) => item.selected && selectable(item))?.id ?? enabled[0]?.id ?? null,
|
|
@@ -92,6 +95,12 @@ export function WebMenu({ items, onSelect, selection = "none", title, label, onC
|
|
|
92
95
|
{(selection === "multi" || toggle) && (
|
|
93
96
|
<CheckboxBox checked={!!item.checked} />
|
|
94
97
|
)}
|
|
98
|
+
{item.leading !== undefined ? (
|
|
99
|
+
<span aria-hidden="true" style={LEADING_STYLE}>{item.leading}</span>
|
|
100
|
+
) : alignLeading !== undefined ? (
|
|
101
|
+
// The room another item's artwork takes, so every label starts in one column.
|
|
102
|
+
<span aria-hidden="true" style={{ ...LEADING_STYLE, visibility: "hidden" }}>{alignLeading}</span>
|
|
103
|
+
) : null}
|
|
95
104
|
<span className="gloom-menu-label">
|
|
96
105
|
<span className="gloom-menu-text" style={item.color ? { color: item.color } : undefined}>{item.label}</span>
|
|
97
106
|
{item.description ? <span className="gloom-menu-description">{item.description}</span> : null}
|