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
@@ -7,6 +7,7 @@ import { useThemeColors } from "../../theme/theme-context";
7
7
  import { t } from "../../i18n";
8
8
  import { Button, ListView, type ListViewItem } from "../ui";
9
9
  import type { ButtonProps } from "../ui/button";
10
+ import { ListRowLines } from "../ui/list-row-lines";
10
11
  import { TITLEBAR_OVERLAY_HEIGHT_PX } from "../layout/titlebar-overlay";
11
12
 
12
13
  /**
@@ -525,17 +526,14 @@ export function OnboardingChoiceList({
525
526
  selectOnHover
526
527
  renderRow={(item, state) => (
527
528
  <Box flexDirection="row" alignItems="center" width="100%" minWidth={0}>
528
- <Box flexDirection="column" flexGrow={1} minWidth={0}>
529
- <Text
530
- fg={state.selected ? colors.textBright : colors.text}
531
- attributes={state.selected ? TextAttributes.BOLD : 0}
532
- wrapText
533
- >
534
- {item.label}
535
- </Text>
536
- {item.description ? (
537
- <Text fg={colors.textMuted} wrapText>{item.description}</Text>
538
- ) : null}
529
+ <Box flexDirection="row" flexGrow={1} minWidth={0}>
530
+ <ListRowLines
531
+ label={item.label}
532
+ description={item.description}
533
+ leading={item.leading}
534
+ labelColor={state.selected ? colors.textBright : colors.text}
535
+ labelBold={state.selected}
536
+ />
539
537
  </Box>
540
538
  {item.detail ? (
541
539
  <Text fg={colors.borderFocused} attributes={TextAttributes.BOLD} style={{ marginLeft: 10 }}>
@@ -1,4 +1,5 @@
1
1
  import {
2
+ createElement,
2
3
  useCallback,
3
4
  useEffect,
4
5
  useMemo,
@@ -16,6 +17,7 @@ import { tf } from "../../i18n";
16
17
  import type { useAppLanguage } from "../../i18n/react";
17
18
  import type { PluginRegistry } from "../../plugins/registry";
18
19
  import type { ListViewItem } from "../ui";
20
+ import { LogoMark } from "../ui/logo-mark";
19
21
  import type { PortfolioSub } from "./onboarding-steps";
20
22
  import { brokerSetupGuideUrl } from "./portfolio-step/broker-setup-panel";
21
23
  import type { useOnboardingBrokerSync } from "./wizard-broker-sync";
@@ -42,14 +44,15 @@ export function useOnboardingBrokerFields({
42
44
  (): BrokerOption[] => getConnectableBrokerOptions(buildBrokerDirectory({
43
45
  signedIn: signedInBrokers,
44
46
  adapters: pluginRegistry.brokers.values(),
45
- })),
46
- [pluginRegistry.brokers, signedInBrokers],
47
+ }), pluginRegistry.getBrokerLogo),
48
+ [pluginRegistry.brokers, pluginRegistry.getBrokerLogo, signedInBrokers],
47
49
  );
48
50
  const brokerChoices = useMemo<ListViewItem[]>(() => brokerOptions.map((broker) => ({
49
51
  id: broker.id,
50
52
  label: tf("Connect {broker}", { broker: broker.name }),
51
53
  // A broker offered two ways shows which way each choice connects.
52
54
  description: broker.methodLabel ?? tf("Import positions from {broker}", { broker: broker.name }),
55
+ leading: createElement(LogoMark, { src: broker.logo, name: broker.name, size: "lead" }),
53
56
  })), [brokerOptions, language]);
54
57
  const activeBrokerFields = useMemo((): BrokerConfigField[] => {
55
58
  if (!selectedBrokerId) return [];
@@ -1,4 +1,5 @@
1
1
  import { brokerMethodLabel, type BrokerDirectoryEntry } from "../../brokers/directory";
2
+ import { brokerDirectoryLogo, type BrokerPluginLogo } from "../../brokers/logos";
2
3
  import type { SignedInBroker } from "../../brokers/signed-in/client";
3
4
  import type { BrokerAdapter, BrokerPosition } from "../../types/broker";
4
5
  import type { AppConfig, OnboardingProgress, OnboardingStage } from "../../types/config";
@@ -47,19 +48,25 @@ export type BrokerOption = {
47
48
  name: string;
48
49
  /** How this choice connects, when the same broker is also offered another way. */
49
50
  methodLabel?: string;
51
+ /** The broker's logo for the desktop and the web (brokers/logos.ts); null draws its initial. */
52
+ logo?: string | null;
50
53
  } & (
51
54
  | { adapter: BrokerAdapter; signedIn?: undefined }
52
55
  | { signedIn: SignedInBroker; adapter?: undefined }
53
56
  );
54
57
 
55
- export function getConnectableBrokerOptions(directory: readonly BrokerDirectoryEntry[]): BrokerOption[] {
58
+ export function getConnectableBrokerOptions(
59
+ directory: readonly BrokerDirectoryEntry[],
60
+ pluginLogo: BrokerPluginLogo = () => null,
61
+ ): BrokerOption[] {
56
62
  const options: BrokerOption[] = [];
57
63
  for (const entry of directory) {
64
+ const logo = brokerDirectoryLogo(entry, pluginLogo);
58
65
  for (const method of entry.methods) {
59
66
  const methodLabel = entry.methods.length > 1 ? brokerMethodLabel(entry, method) : undefined;
60
67
  options.push(method.kind === "signed-in"
61
- ? { id: `signed-in:${method.broker.id}`, name: entry.name, methodLabel, signedIn: method.broker }
62
- : { id: method.adapter.id, name: method.adapter.name, methodLabel, adapter: method.adapter });
68
+ ? { id: `signed-in:${method.broker.id}`, name: entry.name, methodLabel, logo, signedIn: method.broker }
69
+ : { id: method.adapter.id, name: method.adapter.name, methodLabel, logo, adapter: method.adapter });
63
70
  }
64
71
  }
65
72
  return options;
@@ -12,6 +12,7 @@ import {
12
12
  } from "../../portfolio/mutations";
13
13
  import { isFuturesTicker } from "../../portfolio/futures-positions";
14
14
  import { loadFuturesEntrySize } from "../../portfolio/futures-terms";
15
+ import { wholeUnitQuantityProblem } from "../../portfolio/position-quantity";
15
16
  import { useAppDispatch, useAppSelector, useAppStateRef } from "../../state/app/context";
16
17
  import { saveConfigImmediately } from "../../state/config-save-scheduler";
17
18
  import { resolveTickerSearch, upsertTickerFromSearchResult, type ResolvedTickerSearch } from "../../tickers/search";
@@ -291,6 +292,13 @@ export function useOnboardingPositions({
291
292
  if (shares === null) {
292
293
  nextTicker = addTickerToPortfolio(ticker, portfolioId).ticker;
293
294
  } else {
295
+ // 1.250 on a listing held in whole lots could be 1,250 as well, and asks as 1,250 does.
296
+ const lots = wholeUnitQuantityProblem(current.shares.trim(), ticker, { label: quantityLabel });
297
+ if (lots) {
298
+ setFieldIdx(1);
299
+ setError(lots);
300
+ return false;
301
+ }
294
302
  const costBasis = avgCost ?? result.quote?.price;
295
303
  if (costBasis === undefined || costBasis === null || !Number.isFinite(costBasis)) {
296
304
  setFieldIdx(2);
@@ -13,9 +13,10 @@ import { useScopedButtonAction } from "./action-scope";
13
13
  * still reads as a control next to a primary one. `plain` is text that happens
14
14
  * to be clickable: no border, no fill. Anything shaped like a row, a table
15
15
  * cell, a status bar chip or a link is `plain` — a box drawn around it reads as
16
- * a control nested in the surface instead of part of it.
16
+ * a control nested in the surface instead of part of it. Use `quiet` for text
17
+ * actions that also need a filled selection/focus state without a frame.
17
18
  */
18
- export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "plain";
19
+ export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger" | "plain" | "quiet";
19
20
 
20
21
  export interface ButtonProps {
21
22
  label: string;
@@ -25,6 +26,8 @@ export interface ButtonProps {
25
26
  expanded?: boolean;
26
27
  onPress?: () => void;
27
28
  variant?: ButtonVariant;
29
+ /** Semantic fill for a primary action, retained while keyboard focused. */
30
+ tone?: "positive" | "negative" | "neutral" | "warning";
28
31
  disabled?: boolean;
29
32
  /**
30
33
  * The action it started is still running (a retry waiting for its answer).
@@ -56,13 +59,15 @@ export interface ButtonProps {
56
59
  stopPropagation?: boolean;
57
60
  }
58
61
 
59
- function resolveButtonColors(variant: ButtonVariant, active: boolean, disabled: boolean, colors: ThemeColors): { bg?: string; fg: string } {
62
+ function resolveButtonColors(variant: ButtonVariant, active: boolean, disabled: boolean, colors: ThemeColors, tone?: ButtonProps["tone"]): { bg?: string; fg: string } {
63
+ if (variant === "quiet") return { bg: active ? colors.selected : undefined, fg: disabled ? colors.textMuted : active ? colors.textBright : colors.textDim };
60
64
  if (variant === "plain") {
61
65
  return { fg: disabled ? colors.textMuted : active ? colors.textBright : colors.textDim };
62
66
  }
63
67
  if (disabled) {
64
68
  return { bg: colors.panel, fg: colors.textMuted };
65
69
  }
70
+ if (tone && variant === "primary") return { bg: colors[tone], fg: colors.bg };
66
71
  if (active) {
67
72
  return { bg: colors.selected, fg: colors.selectedText };
68
73
  }
@@ -96,6 +101,7 @@ export function Button({
96
101
  expanded,
97
102
  onPress: pressAction,
98
103
  variant = "secondary",
104
+ tone,
99
105
  disabled = false,
100
106
  pending = false,
101
107
  pendingLabel,
@@ -138,6 +144,7 @@ export function Button({
138
144
  expanded={expanded}
139
145
  onPress={onPress}
140
146
  variant={variant}
147
+ tone={tone}
141
148
  disabled={inert}
142
149
  pending={pending}
143
150
  active={active}
@@ -152,7 +159,7 @@ export function Button({
152
159
  );
153
160
  }
154
161
 
155
- const palette = resolveButtonColors(variant, active, inert, colors);
162
+ const palette = resolveButtonColors(variant, active, inert, colors, tone);
156
163
 
157
164
  return (
158
165
  <Box
@@ -1,4 +1,4 @@
1
- import { useEffect, useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useUiHost } from "../../ui";
3
3
  import { type PromptContext, useDialogKeyboard } from "../../ui/dialog";
4
4
  import { useThemeColors } from "../../theme/theme-context";
@@ -61,6 +61,9 @@ export function ChoiceDialog({
61
61
  );
62
62
  const selectedIndex = clampIndex(index, choices.length, -1);
63
63
  const selectedChoice = selectedIndex >= 0 ? choices[selectedIndex] : undefined;
64
+ // Terminal input can deliver navigation and Enter before React renders.
65
+ const cursor = useRef(selectedIndex);
66
+ const selectIndex = (next: number) => { cursor.current = next; setIndex(next); };
64
67
  const items = useMemo<ListViewItem[]>(() => choices.map((choice) => ({
65
68
  id: choice.id,
66
69
  label: choice.label,
@@ -72,7 +75,7 @@ export function ChoiceDialog({
72
75
  const width = useMemo(() => choiceDialogWidth(title, choices), [choices, title]);
73
76
 
74
77
  useEffect(() => {
75
- setIndex((current) => clampIndex(current, choices.length, -1));
78
+ selectIndex(clampIndex(cursor.current, choices.length, -1));
76
79
  }, [choices.length]);
77
80
 
78
81
  const activateChoice = (choice: ChoiceDialogChoice | undefined) => {
@@ -84,9 +87,9 @@ export function ChoiceDialog({
84
87
  event.stopPropagation();
85
88
  const move = listCursorMove(event, MAX_VISIBLE_CHOICE_ROWS - 1);
86
89
  if (move) {
87
- setIndex((current) => move(choices, current));
90
+ selectIndex(move(choices, cursor.current));
88
91
  } else if (event.name === "enter" || event.name === "return") {
89
- activateChoice(selectedChoice);
92
+ activateChoice(choices[clampIndex(cursor.current, choices.length, -1)]);
90
93
  } else if (event.name === "escape") {
91
94
  resolve("");
92
95
  }
@@ -106,7 +109,7 @@ export function ChoiceDialog({
106
109
  height={Math.min(Math.max(items.length, 1), MAX_VISIBLE_CHOICE_ROWS)}
107
110
  scrollable={items.length > MAX_VISIBLE_CHOICE_ROWS}
108
111
  selectOnHover
109
- onSelect={setIndex}
112
+ onSelect={selectIndex}
110
113
  onActivate={(_, nextIndex) => activateChoice(choices[nextIndex])}
111
114
  />
112
115
  <Box height={1} />
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { t } from "../../i18n";
3
3
  import { blendHex, hoverBg } from "../../theme/colors";
4
+ import { blendForContrast, higherContrast } from "../../theme/color-utils";
4
5
  import { useThemeColors } from "../../theme/theme-context";
5
6
  import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
6
7
  import { truncateToDisplayWidth } from "../../utils/format";
@@ -62,6 +63,7 @@ export interface KeyValueRowProps {
62
63
  width?: number;
63
64
  labelWidth?: number;
64
65
  emphasis?: boolean;
66
+ align?: "left" | "right";
65
67
  /**
66
68
  * Opens what the value names (a security's DES, a function): the whole row is the target, and inside a
67
69
  * `PaneLinkMenu` the pane menu lists it as `link.label`, so the keyboard reaches what a click opens.
@@ -118,7 +120,7 @@ export function KeyValueRow(props: KeyValueRowProps) {
118
120
  * width in characters, the value and detail taking the rest of the row and
119
121
  * clipped in CSS where they run out of it.
120
122
  */
121
- function NativeKeyValueRow({ label, value, detail, tone, color, width, labelWidth, emphasis = true, link }: KeyValueRowProps) {
123
+ function NativeKeyValueRow({ label, value, detail, tone, color, width, labelWidth, emphasis = true, align = "left", link }: KeyValueRowProps) {
122
124
  const colors = useThemeColors();
123
125
  usePaneLinkMenuEntry(link ? `key-value:${label}:${value}` : null, link?.label ?? "", () => link?.onOpen());
124
126
  const target = link ? {
@@ -137,7 +139,7 @@ function NativeKeyValueRow({ label, value, detail, tone, color, width, labelWidt
137
139
  <Text
138
140
  fg={color ?? statToneColor(tone, colors) ?? colors.text}
139
141
  attributes={emphasis ? TextAttributes.BOLD : undefined}
140
- style={{ ...NATIVE_CLIP_STYLE, flex: detail ? "0 1 auto" : "1 1 auto" }}
142
+ style={{ ...NATIVE_CLIP_STYLE, flex: detail ? "0 1 auto" : "1 1 auto", textAlign: align }}
141
143
  >
142
144
  {value}
143
145
  </Text>
@@ -146,7 +148,7 @@ function NativeKeyValueRow({ label, value, detail, tone, color, width, labelWidt
146
148
  );
147
149
  }
148
150
 
149
- function TerminalKeyValueRow({ label, value, detail, tone, color, width, labelWidth, emphasis = true, link }: KeyValueRowProps) {
151
+ function TerminalKeyValueRow({ label, value, detail, tone, color, width, labelWidth, emphasis = true, align = "left", link }: KeyValueRowProps) {
150
152
  const colors = useThemeColors();
151
153
  usePaneLinkMenuEntry(link ? `key-value:${label}:${value}` : null, link?.label ?? "", () => link?.onOpen());
152
154
  const target = link ? {
@@ -169,7 +171,7 @@ function TerminalKeyValueRow({ label, value, detail, tone, color, width, labelWi
169
171
  <Box width={labelColumns} flexShrink={0} overflow="hidden">
170
172
  <Text fg={colors.textDim}>{t(label)}</Text>
171
173
  </Box>
172
- <Box width={valueWidth} flexShrink={0} overflow="hidden">
174
+ <Box width={valueWidth} flexShrink={0} overflow="hidden" alignItems={align === "right" ? "flex-end" : undefined}>
173
175
  <Text fg={color ?? statToneColor(tone, colors) ?? colors.text} attributes={emphasis ? TextAttributes.BOLD : undefined}>
174
176
  {valueWidth === undefined ? value : truncateToDisplayWidth(value, valueWidth)}
175
177
  </Text>
@@ -207,10 +209,13 @@ export function Badge({ label, tone = "neutral", color, variant = "subtle" }: Ba
207
209
  const accent = color ?? (tone === "neutral" ? colors.textDim : tone === "accent" ? colors.borderFocused : colors[tone]);
208
210
  const solid = variant === "solid";
209
211
  const neutral = solid && tone === "neutral" && !color;
212
+ const background = neutral ? colors.selected : solid ? accent : blendHex(colors.bg, accent, BADGE_TINT);
213
+ const baseForeground = neutral ? colors.selectedText : solid ? colors.bg : accent;
214
+ const foreground = blendForContrast(baseForeground, background, higherContrast("#000000", "#ffffff", background), 4.5);
210
215
  const chip = (
211
- <Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={neutral ? colors.selected : solid ? accent : blendHex(colors.bg, accent, BADGE_TINT)}
216
+ <Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={background}
212
217
  data-gloom-ui="badge" justifyContent={nativePaneChrome ? "center" : undefined} style={nativePaneChrome ? NATIVE_BADGE_STYLE : undefined}>
213
- <Text fg={neutral ? colors.selectedText : solid ? colors.bg : accent} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{t(label)}</Text>
218
+ <Text fg={foreground} attributes={TextAttributes.BOLD} style={nativePaneChrome ? NATIVE_BADGE_TEXT_STYLE : undefined}>{t(label)}</Text>
214
219
  </Box>
215
220
  );
216
221
  return nativePaneChrome ? <Box height={1} flexShrink={0} justifyContent="center">{chip}</Box> : chip;
@@ -1,10 +1,12 @@
1
1
  import { Box, Input, Span, Text, TextAttributes, useUiCapabilities, useUiHost } from "../../ui";
2
- import { useEffect, useRef, useState, type ComponentType, type RefObject } from "react";
2
+ import { useEffect, useLayoutEffect, useRef, useState, type ComponentType, type RefObject } from "react";
3
3
  import { type InputRenderable } from "../../ui";
4
4
  import { useThemeColors } from "../../theme/theme-context";
5
5
  import { useRemoteUiNode } from "../../remote/semantic-tree";
6
6
  import { remoteStringValue } from "../../remote/semantic-helpers";
7
7
  import { truncateWithEllipsis } from "../../utils/text-wrap";
8
+ import { Button } from "./button";
9
+ import { stepNumber } from "./number-step";
8
10
 
9
11
  export interface FieldLabelProps {
10
12
  label: string;
@@ -35,6 +37,8 @@ export function FieldLabel({ label, active = false, width, maxWidth }: FieldLabe
35
37
 
36
38
  export interface TextFieldProps {
37
39
  label?: string;
40
+ /** Accessible name when the visible label is rendered separately. */
41
+ accessibleLabel?: string;
38
42
  /**
39
43
  * The form's current field, in a form that walks its fields with the
40
44
  * keyboard. The terminal marks its label with "> " and indents the other
@@ -51,6 +55,7 @@ export interface TextFieldProps {
51
55
  /** The most characters it takes; typing and pasting stop there. */
52
56
  maxLength?: number;
53
57
  focused?: boolean;
58
+ disabled?: boolean;
54
59
  width?: number;
55
60
  inputRef?: RefObject<InputRenderable | null>;
56
61
  onChange?: (value: string) => void;
@@ -60,7 +65,8 @@ export interface TextFieldProps {
60
65
  type?: "text" | "password" | "date" | "email";
61
66
  /** DOM autofill hint, e.g. "email" or "current-password"; terminal hosts ignore it. */
62
67
  autoComplete?: string;
63
- variant?: "default" | "plain";
68
+ /** Well is a quiet filled field with an underline only while focused. */
69
+ variant?: "default" | "plain" | "well";
64
70
  /**
65
71
  * `comfortable` is the form-page size on the DOM renderer: a taller field
66
72
  * with a tight label gap, for dialogs a user fills in rather than dense pane
@@ -117,7 +123,7 @@ function InlineTextField({ label = "", labelWidth = 0, active, width, hint, onMo
117
123
  <Box flexDirection="column" width={width}>
118
124
  <Box height={1} flexDirection="row" alignItems="center" gap={1} onMouseDown={onMouseDown}>
119
125
  <FieldLabel label={label} active={active} width={labelWidth} />
120
- <TextFieldControl {...props} width={inputWidth} onMouseDown={onMouseDown} />
126
+ <TextFieldControl {...props} accessibleLabel={props.accessibleLabel ?? label} width={inputWidth} onMouseDown={onMouseDown} />
121
127
  </Box>
122
128
  {hint ? (
123
129
  <Box paddingLeft={labelWidth + 1}>
@@ -130,11 +136,13 @@ function InlineTextField({ label = "", labelWidth = 0, active, width, hint, onMo
130
136
 
131
137
  function TextFieldControl({
132
138
  label,
139
+ accessibleLabel,
133
140
  active,
134
141
  value,
135
142
  placeholder,
136
143
  maxLength,
137
144
  focused,
145
+ disabled = false,
138
146
  width,
139
147
  inputRef,
140
148
  onChange,
@@ -152,20 +160,20 @@ function TextFieldControl({
152
160
  onKeyDown,
153
161
  }: TextFieldProps) {
154
162
  const colors = useThemeColors();
155
- backgroundColor ??= colors.bg;
163
+ backgroundColor ??= variant === "well" ? colors.panel : colors.bg;
156
164
  textColor ??= colors.text;
157
165
  placeholderColor ??= colors.textDim;
158
166
  useRemoteUiNode({
159
167
  role: "text-field",
160
- label: label ?? placeholder,
168
+ label: accessibleLabel ?? label ?? placeholder,
161
169
  actions: {
162
170
  setValue: (input) => {
163
171
  const nextValue = remoteStringValue(input);
164
- onChange?.(nextValue);
172
+ if (!disabled) onChange?.(nextValue);
165
173
  },
166
174
  submit: (input) => {
167
175
  const nextValue = remoteStringValue(input, value ?? "");
168
- onSubmit?.(nextValue);
176
+ if (!disabled) onSubmit?.(nextValue);
169
177
  },
170
178
  blur: () => onBlur?.(value ?? ""),
171
179
  },
@@ -176,10 +184,12 @@ function TextFieldControl({
176
184
  return (
177
185
  <HostTextField
178
186
  label={label}
187
+ accessibleLabel={accessibleLabel}
179
188
  value={value}
180
189
  placeholder={placeholder}
181
190
  maxLength={maxLength}
182
- focused={focused}
191
+ focused={focused && !disabled}
192
+ disabled={disabled}
183
193
  width={width}
184
194
  inputRef={inputRef}
185
195
  onChange={onChange}
@@ -233,6 +243,7 @@ function TextFieldControl({
233
243
  </Box>
234
244
  )}
235
245
  <Box height={1} onMouseDown={() => {
246
+ if (disabled) return;
236
247
  onMouseDown?.();
237
248
  resolvedInputRef.current?.focus?.();
238
249
  }}>
@@ -243,16 +254,20 @@ function TextFieldControl({
243
254
  selectable={!isPassword}
244
255
  placeholder={isPassword ? "" : placeholder}
245
256
  {...(maxLength === undefined ? {} : { maxLength })}
246
- focused={focused}
257
+ focused={focused && !disabled}
247
258
  textColor={isPassword ? backgroundColor : textColor}
248
259
  placeholderColor={placeholderColor}
249
260
  backgroundColor={backgroundColor}
250
261
  selectionBg={isPassword ? backgroundColor : undefined}
251
262
  selectionFg={isPassword ? backgroundColor : undefined}
252
263
  showCursor={!isPassword}
253
- onKeyDown={onKeyDown}
264
+ onKeyDown={(event: Parameters<NonNullable<TextFieldProps["onKeyDown"]>>[0]) => {
265
+ if (disabled) { event.preventDefault(); event.stopPropagation(); return; }
266
+ onKeyDown?.(event);
267
+ }}
254
268
  onCursorChange={() => syncCursorOffset()}
255
269
  onInput={(nextValue: string) => {
270
+ if (disabled) return;
256
271
  currentValueRef.current = nextValue;
257
272
  syncCursorOffset(nextValue);
258
273
  onChange?.(nextValue);
@@ -262,12 +277,12 @@ function TextFieldControl({
262
277
  // the form has since cleared and swapped out still blurs when the
263
278
  // next one takes focus, and passing its old text on would put it
264
279
  // back into the cleared draft.
265
- if (nextValue === currentValueRef.current) return;
280
+ if (disabled || nextValue === currentValueRef.current) return;
266
281
  currentValueRef.current = nextValue;
267
282
  syncCursorOffset(nextValue);
268
283
  onChange?.(nextValue);
269
284
  }}
270
- onSubmit={() => onSubmit?.(currentValueRef.current)}
285
+ onSubmit={() => { if (!disabled) onSubmit?.(currentValueRef.current); }}
271
286
  onBlur={() => onBlur?.(currentValueRef.current)}
272
287
  />
273
288
  {isPassword && (
@@ -321,6 +336,10 @@ function sanitizeNumberInput(value: string, allowDecimal: boolean, allowNegative
321
336
  export interface NumberFieldProps extends Omit<TextFieldProps, "onChange" | "onSubmit" | "onBlur"> {
322
337
  allowDecimal?: boolean;
323
338
  allowNegative?: boolean;
339
+ /** Opt-in shared minus/plus, pointer wheel and Up/Down stepping. Not an exchange tick unless the caller supplies one. */
340
+ step?: number;
341
+ min?: number;
342
+ max?: number;
324
343
  onChange?: (value: string) => void;
325
344
  onSubmit?: (value: string) => void;
326
345
  onBlur?: (value: string) => void;
@@ -332,14 +351,75 @@ export function NumberField({
332
351
  onChange,
333
352
  onSubmit,
334
353
  onBlur,
354
+ step,
355
+ min,
356
+ max,
335
357
  ...props
336
358
  }: NumberFieldProps) {
337
- return (
338
- <TextField
339
- {...props}
340
- onChange={(nextValue) => onChange?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
341
- onSubmit={(nextValue) => onSubmit?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
342
- onBlur={(nextValue) => onBlur?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
343
- />
344
- );
359
+ const colors = useThemeColors();
360
+ const native = useUiCapabilities().nativePaneChrome;
361
+ const well = props.variant === "well";
362
+ const valueRef = useRef(props.value ?? "");
363
+ valueRef.current = props.value ?? "";
364
+ const localInput = useRef<InputRenderable | null>(null);
365
+ const inputRef = props.inputRef ?? localInput;
366
+ const stepping = step !== undefined;
367
+ const pendingCursor = useRef<number | undefined>(undefined);
368
+ useLayoutEffect(() => {
369
+ const cursor = pendingCursor.current, input = inputRef.current;
370
+ if (cursor === undefined || !input) return;
371
+ pendingCursor.current = undefined;
372
+ const offset = Math.min(cursor, (props.value ?? "").length);
373
+ if (input.setCursorOffset) input.setCursorOffset(offset);
374
+ else input.cursorOffset = offset;
375
+ }, [props.value, inputRef]);
376
+ const change = (value: string) => {
377
+ if (props.disabled) return;
378
+ valueRef.current = value;
379
+ onChange?.(value);
380
+ };
381
+ const next = (direction: 1 | -1) => step === undefined ? undefined : stepNumber(valueRef.current, direction, step, min ?? (allowNegative ? undefined : 0), max);
382
+ const advance = (direction: 1 | -1) => {
383
+ if (props.disabled) return;
384
+ const value = next(direction);
385
+ if (value === undefined) return;
386
+ // Restore after the controlled value commits, not before the DOM input updates.
387
+ pendingCursor.current = inputRef.current?.cursorOffset;
388
+ change(value);
389
+ };
390
+ const field = <TextField
391
+ {...props}
392
+ variant={stepping && well ? "plain" : props.variant}
393
+ backgroundColor={well ? colors.panel : props.backgroundColor}
394
+ label={stepping ? undefined : props.label}
395
+ accessibleLabel={props.accessibleLabel ?? props.label}
396
+ inputRef={inputRef}
397
+ width={stepping && props.width !== undefined ? Math.max(6, props.width - 8) : props.width}
398
+ onChange={(value) => change(sanitizeNumberInput(value, allowDecimal, allowNegative))}
399
+ onSubmit={(value) => { if (!props.disabled) onSubmit?.(sanitizeNumberInput(value, allowDecimal, allowNegative)); }}
400
+ onBlur={(value) => { if (!props.disabled) onBlur?.(sanitizeNumberInput(value, allowDecimal, allowNegative)); }}
401
+ onKeyDown={(event) => {
402
+ if (stepping && (event.name === "up" || event.name === "down")) {
403
+ event.preventDefault(); event.stopPropagation(); advance(event.name === "up" ? 1 : -1); return;
404
+ }
405
+ props.onKeyDown?.(event);
406
+ }}
407
+ />;
408
+ if (!stepping) return field;
409
+ const name = props.accessibleLabel ?? props.label ?? "value";
410
+ return <Box flexDirection="column" width={props.width}>
411
+ {props.label ? <FieldLabel label={props.label} active={props.active} /> : null}
412
+ <Box flexDirection="row" alignItems="center" gap={1} backgroundColor={well ? colors.panel : undefined}
413
+ // An outline says the well takes typing even before it has focus.
414
+ style={native && well ? { borderRadius: "var(--gloom-radius-sm)", paddingLeft: 6, boxShadow: props.focused ? `inset 0 0 0 1px ${colors.borderFocused}, inset 0 -2px ${colors.borderFocused}` : `inset 0 0 0 1px ${colors.border}` } : undefined}
415
+ onMouseScroll={(event: { scroll?: { direction?: string }; preventDefault(): void; stopPropagation(): void }) => {
416
+ if (props.disabled || !["up", "down"].includes(event.scroll?.direction ?? "")) return;
417
+ event.preventDefault(); event.stopPropagation();
418
+ advance(event.scroll?.direction === "up" ? 1 : -1);
419
+ }}>
420
+ {field}
421
+ <Button label={`Decrease ${name}`} displayLabel="-" variant={well ? "plain" : undefined} width={3} compact disabled={props.disabled || next(-1) === undefined} onPress={() => { inputRef.current?.focus?.(); advance(-1); }} />
422
+ <Button label={`Increase ${name}`} displayLabel="+" variant={well ? "plain" : undefined} width={3} compact disabled={props.disabled || next(1) === undefined} onPress={() => { inputRef.current?.focus?.(); advance(1); }} />
423
+ </Box>
424
+ </Box>;
345
425
  }
@@ -36,7 +36,9 @@ export type IconName =
36
36
  | "ask"
37
37
  | "bot"
38
38
  | "team"
39
- | "pro";
39
+ | "pro"
40
+ | "clock"
41
+ | "edit";
40
42
 
41
43
  /** Terminal glyph for each icon; the desktop host draws SVG. */
42
44
  export const ICON_GLYPHS: Record<IconName, string> = {
@@ -79,6 +81,9 @@ export const ICON_GLYPHS: Record<IconName, string> = {
79
81
  bot: "⍠",
80
82
  team: "◉",
81
83
  pro: "◊",
84
+ // Time, for data that arrives late (a delayed quote).
85
+ clock: "◷",
86
+ edit: "✎",
82
87
  };
83
88
 
84
89
  export interface IconProps {
@@ -0,0 +1,87 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { Box, Text, TextAttributes } from "../../ui";
3
+ import { useThemeColors } from "../../theme/theme-context";
4
+
5
+ /**
6
+ * Each line's height, in its own type: room for the font's ascent and descent,
7
+ * so a line's letters sit inside its box in every engine, without relying on
8
+ * `text-box` trimming (WebKit before 18.2 ignores it). Half of the space
9
+ * above and below the letters also separates the name from the line under it.
10
+ */
11
+ const LINE_HEIGHT = 1.2;
12
+
13
+ /** Room above and below a line for accents and descenders that reach past it. */
14
+ const INK_ROOM = "0.3em";
15
+
16
+ /**
17
+ * The line clips for the ellipsis, and WebKit clips both ways whatever the
18
+ * other axis says, so the clip box reaches past the line by padding, and an
19
+ * equal negative margin keeps the line where it was.
20
+ */
21
+ const LINE_STYLE = {
22
+ display: "block",
23
+ minWidth: 0,
24
+ maxWidth: "100%",
25
+ lineHeight: LINE_HEIGHT,
26
+ overflow: "hidden",
27
+ paddingBlock: INK_ROOM,
28
+ marginBlock: `calc(-1 * ${INK_ROOM})`,
29
+ textOverflow: "ellipsis",
30
+ whiteSpace: "nowrap",
31
+ } as CSSProperties;
32
+
33
+ /**
34
+ * The art beside the two lines (a `LogoMark` of size `lead`) is a square
35
+ * exactly as tall as the two lines, in their type, so the name and the line
36
+ * under it always sit within its height, the block centred on it.
37
+ */
38
+ const LEADING_STYLE: CSSProperties = {
39
+ flexShrink: 0,
40
+ fontSize: "var(--gloom-type-ui)",
41
+ width: `${2 * LINE_HEIGHT}em`,
42
+ height: `${2 * LINE_HEIGHT}em`,
43
+ };
44
+
45
+ export interface ListRowLinesProps {
46
+ label: string;
47
+ /** The muted line under the label, such as how a broker connects. */
48
+ description?: string;
49
+ /** Art before the lines, such as a broker's logo. Decoration: hidden from assistive technology. */
50
+ leading?: ReactNode;
51
+ /** The label's colour; the theme's text colour by default. */
52
+ labelColor?: string;
53
+ /** The label in bold, as a selected row's. */
54
+ labelBold?: boolean;
55
+ }
56
+
57
+ /**
58
+ * A desktop and web list row's name with one muted line under it, and its
59
+ * logo before them, the block centred in the row. Every two-line row with
60
+ * art uses it (`ListView`'s `rowDescriptions`, the first-run guide's list),
61
+ * so the lines sit on the logo the same way everywhere.
62
+ */
63
+ export function ListRowLines({ label, description, leading, labelColor, labelBold = false }: ListRowLinesProps) {
64
+ const colors = useThemeColors();
65
+ return (
66
+ <Box
67
+ flexDirection="row"
68
+ alignItems="center"
69
+ minWidth={0}
70
+ flexShrink={1}
71
+ data-gloom-role="list-row-lines"
72
+ style={{ gap: "var(--gloom-space-sm)", fontSize: "var(--gloom-type-ui)" }}
73
+ >
74
+ {leading !== undefined && (
75
+ <Box aria-hidden="true" data-gloom-role="list-row-leading" style={LEADING_STYLE}>
76
+ {leading}
77
+ </Box>
78
+ )}
79
+ <Box flexDirection="column" minWidth={0} flexShrink={1}>
80
+ <Text fg={labelColor ?? colors.text} attributes={labelBold ? TextAttributes.BOLD : 0} style={LINE_STYLE}>
81
+ {label}
82
+ </Text>
83
+ {description ? <Text fg={colors.textMuted} style={LINE_STYLE}>{description}</Text> : null}
84
+ </Box>
85
+ </Box>
86
+ );
87
+ }