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