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