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.
Files changed (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. 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 dateAcquired = replacedPositions.length === 1 && replacedPositions[0]?.broker === "manual"
410
- ? replacedPositions[0].dateAcquired : undefined;
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 += convert(cost, position);
173
- metrics.signedCost += direction * convert(cost, position);
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 * convert(cost, position),
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",
@@ -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
- <Box flexDirection="row" alignItems="center" minWidth={0} flexShrink={1} overflow="hidden">
36
- <Text
37
- fg={item.color ?? (selected ? colors.text : colors.textDim)}
38
- attributes={selected ? TextAttributes.BOLD : 0}
39
- style={{
40
- // The base text style pins flexShrink to 0, which lets a long
41
- // label run under the detail instead of shrinking to an ellipsis.
42
- flexShrink: 1,
43
- minWidth: 0,
44
- maxWidth: "100%",
45
- overflow: "hidden",
46
- textOverflow: "ellipsis",
47
- whiteSpace: "nowrap",
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.label}
51
- </Text>
52
- </Box>
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}