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
@@ -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
+ }
@@ -22,6 +22,12 @@ export interface ListViewItem {
22
22
  disabled?: boolean;
23
23
  /** Tints the label, e.g. a status in its tone; the words carry the meaning. */
24
24
  color?: string;
25
+ /**
26
+ * Artwork before the label on the desktop and the web, such as a broker's
27
+ * logo (`LogoMark`). It is decoration: the terminal does not draw it, and
28
+ * automation and screen readers read only `label`.
29
+ */
30
+ leading?: ReactNode;
25
31
  }
26
32
 
27
33
  export interface ListRowState {
@@ -39,6 +45,12 @@ export interface ListViewProps {
39
45
  renderRow?: (item: ListViewItem, state: ListRowState, index: number) => ReactNode;
40
46
  getRowBackgroundColor?: (item: ListViewItem, state: ListRowState, index: number) => string | undefined;
41
47
  showSelectedDescription?: boolean;
48
+ /**
49
+ * Desktop and web: every row sets its `description` on a second line under
50
+ * its label, and is two rows tall unless `rowHeight` says otherwise. The
51
+ * terminal keeps one line per row, with `detail` at its end.
52
+ */
53
+ rowDescriptions?: boolean;
42
54
  emptyMessage?: string;
43
55
  bgColor?: string;
44
56
  selectedBgColor?: string;
@@ -173,6 +185,7 @@ export function ListView({
173
185
  renderRow,
174
186
  getRowBackgroundColor,
175
187
  showSelectedDescription = false,
188
+ rowDescriptions,
176
189
  emptyMessage = "Nothing to show.",
177
190
  bgColor,
178
191
  selectedBgColor,
@@ -243,6 +256,7 @@ export function ListView({
243
256
  renderRow={renderRow}
244
257
  getRowBackgroundColor={getRowBackgroundColor}
245
258
  showSelectedDescription={showSelectedDescription}
259
+ rowDescriptions={rowDescriptions}
246
260
  emptyMessage={emptyMessage}
247
261
  bgColor={bgColor}
248
262
  selectedBgColor={selectedBgColor}
@@ -0,0 +1,64 @@
1
+ import type { CSSProperties } from "react";
2
+ import { Box, ImageSurface, Text, TextAttributes, useUiCapabilities } from "../../ui";
3
+ import { useThemeColors } from "../../theme/theme-context";
4
+
5
+ /**
6
+ * `row`: beside a label in one row (a table cell's `leading`, a list row).
7
+ * `lead`: the art of a two-line list row; it fills the square `ListRowLines`
8
+ * sizes to the two lines beside it.
9
+ */
10
+ type LogoMarkSize = "row" | "lead";
11
+
12
+ const TILE: Record<LogoMarkSize, { side: string; radius: string; type: string }> = {
13
+ // A pixel above and below in its row, as a chip.
14
+ row: { side: "calc(var(--cell-h) - 2px)", radius: "var(--gloom-radius-sm)", type: "var(--gloom-type-caption)" },
15
+ lead: { side: "100%", radius: "var(--gloom-radius-md)", type: "var(--gloom-type-ui)" },
16
+ };
17
+
18
+ // Sized by its capital and baseline, so flex centring puts the letter in the
19
+ // middle of the tile whatever the font's ascent and descent (as chrome text).
20
+ const LETTER_STYLE: Record<LogoMarkSize, CSSProperties> = {
21
+ row: { fontSize: TILE.row.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
22
+ lead: { fontSize: TILE.lead.type, lineHeight: 1, textBox: "trim-both cap alphabetic" } as CSSProperties,
23
+ };
24
+
25
+ export interface LogoMarkProps {
26
+ /** The logo, an image URL; without one, or when it fails to load, the tile shows `name`'s initial. */
27
+ src?: string | null;
28
+ /** What the logo stands for, such as a broker. Only its initial is drawn; the label beside it names it. */
29
+ name: string;
30
+ size?: LogoMarkSize;
31
+ }
32
+
33
+ function initialOf(name: string): string {
34
+ const letter = [...name.trim()].find((character) => /[\p{L}\p{N}]/u.test(character));
35
+ return letter ? letter.toLocaleUpperCase() : "?";
36
+ }
37
+
38
+ /**
39
+ * A logo in a square tile, for lists of brokers and the accounts they hold, on
40
+ * the desktop and the web. Decoration: hidden from assistive technology, so
41
+ * the label beside it stays the name. The terminal draws nothing.
42
+ */
43
+ export function LogoMark({ src, name, size = "row" }: LogoMarkProps) {
44
+ const desktop = useUiCapabilities().nativePaneChrome === true;
45
+ const colors = useThemeColors();
46
+ if (!desktop) return null;
47
+ const tile = TILE[size];
48
+ return (
49
+ <ImageSurface
50
+ src={src ?? undefined}
51
+ alt=""
52
+ objectFit="cover"
53
+ aria-hidden="true"
54
+ data-gloom-role="logo-mark"
55
+ style={{ width: tile.side, height: tile.side, flexShrink: 0, borderRadius: tile.radius }}
56
+ >
57
+ <Box width="100%" height="100%" alignItems="center" justifyContent="center" backgroundColor={colors.border}>
58
+ <Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={LETTER_STYLE[size]}>
59
+ {initialOf(name)}
60
+ </Text>
61
+ </Box>
62
+ </ImageSurface>
63
+ );
64
+ }
@@ -0,0 +1,54 @@
1
+ /** Decimal stepping on a zero-based increment grid, without binary rounding drift. */
2
+ function decimal(value: string | number): { units: bigint; places: number } | undefined {
3
+ const text = String(value).trim();
4
+ const match = /^([+-]?)(\d*)(?:\.(\d*))?(?:e([+-]?\d+))?$/i.exec(text);
5
+ if (!match || !`${match[2]}${match[3] ?? ""}` || !Number.isFinite(Number(text))) return;
6
+ const exponent = Number(match[4] ?? 0);
7
+ const places = (match[3]?.length ?? 0) - exponent;
8
+ if (Math.abs(places) > 18 || text.length > 100) return;
9
+ const units = BigInt(`${match[1] === "-" ? "-" : ""}${match[2] || "0"}${match[3] ?? ""}`);
10
+ return places < 0 ? { units: units * 10n ** BigInt(-places), places: 0 } : { units, places };
11
+ }
12
+ function floorDivide(a: bigint, b: bigint): bigint { return a / b - (a < 0n && a % b !== 0n ? 1n : 0n); }
13
+ function ceilDivide(a: bigint, b: bigint): bigint { return -floorDivide(-a, b); }
14
+ function format(units: bigint, places: number): string {
15
+ const digits = (units < 0n ? -units : units).toString().padStart(places + 1, "0");
16
+ const text = places ? `${digits.slice(0, -places)}.${digits.slice(-places)}`.replace(/\.?0+$/, "") : digits;
17
+ return `${units < 0n ? "-" : ""}${text}`;
18
+ }
19
+ export function stepNumber(value: string, direction: 1 | -1, step: number, min?: number, max?: number): string | undefined {
20
+ const current = decimal(value === "" ? 0 : value), increment = decimal(step);
21
+ const lower = min === undefined ? undefined : decimal(min), upper = max === undefined ? undefined : decimal(max);
22
+ if (!current || !increment || increment.units <= 0n || min !== undefined && !lower || max !== undefined && !upper) return;
23
+ const places = Math.max(current.places, increment.places, lower?.places ?? 0, upper?.places ?? 0);
24
+ const scaled = (n: { units: bigint; places: number }) => n.units * 10n ** BigInt(places - n.places);
25
+ const at = scaled(current), by = scaled(increment);
26
+ let ticks = direction > 0 ? floorDivide(at, by) + 1n : ceilDivide(at, by) - 1n;
27
+ const low = lower && ceilDivide(scaled(lower), by), high = upper && floorDivide(scaled(upper), by);
28
+ if (low !== undefined && high !== undefined && low > high) return;
29
+ if (low !== undefined && ticks < low) ticks = low;
30
+ if (high !== undefined && ticks > high) ticks = high;
31
+ const next = ticks * by;
32
+ // A bound must not make a plus go down or a minus go up, except an empty field.
33
+ if (value !== "" && (direction > 0 ? next <= at : next >= at)) return;
34
+ const text = format(next, places), roundTrip = decimal(Number(text));
35
+ if (!roundTrip || roundTrip.places > places || scaled(roundTrip) !== next) return;
36
+ return text;
37
+ }
38
+
39
+ /** Smallest whole-number multiple, for brokers combining a step with whole quantities. */
40
+ export function wholeNumberStep(step: number): number | undefined {
41
+ const value = decimal(step);
42
+ if (!value || value.units <= 0n) return;
43
+ let a = value.units, b = 10n ** BigInt(value.places);
44
+ while (b) { const remainder = a % b; a = b; b = remainder; }
45
+ const result = Number(value.units / a);
46
+ return Number.isSafeInteger(result) ? result : undefined;
47
+ }
48
+
49
+ export function matchesNumberStep(value: number, step: number): boolean {
50
+ const n = decimal(value), s = decimal(step);
51
+ if (!n || !s || s.units <= 0n) return false;
52
+ const places = Math.max(n.places, s.places);
53
+ return n.units * 10n ** BigInt(places - n.places) % (s.units * 10n ** BigInt(places - s.places)) === 0n;
54
+ }
@@ -48,9 +48,10 @@ export interface SelectButtonProps<T extends string = string> {
48
48
  * `inline`: the label and the value in a row, in a toolbar. `field`: a
49
49
  * bordered field. `form`: a field in a form of text fields; on the desktop
50
50
  * and the web its label sits above it, bold, as a TextField's does; the
51
- * terminal draws it inline.
51
+ * terminal draws it inline. `well`: a field without its border, on a
52
+ * recessed fill, for a dense panel such as the order ticket.
52
53
  */
53
- variant?: "inline" | "field" | "form";
54
+ variant?: "inline" | "field" | "form" | "well";
54
55
  width?: number;
55
56
  onFocus?: () => void;
56
57
  /**
@@ -189,8 +190,8 @@ export function SelectButton<T extends string = string>({
189
190
  }
190
191
 
191
192
  // The terminal draws a form select as it draws an inline one.
192
- if (variant === "field") {
193
- return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
193
+ if (variant === "field" || variant === "well") {
194
+ return <Button label={showLabel ? `${label}: ${current?.short ?? current?.label ?? ""}` : current?.short ?? current?.label ?? label} width={width} variant={variant === "well" ? "quiet" : undefined} active={emphasized} disabled={disabled} onPress={openDialog} stopPropagation />;
194
195
  }
195
196
 
196
197
  return (
@@ -22,10 +22,10 @@ export interface SelectFieldProps {
22
22
  width?: number | string;
23
23
  height?: number;
24
24
  /**
25
- * `field` is the form control used in dialogs. `inline` drops the box so a
26
- * dense row reads as text; both open the same Menu.
25
+ * `field` is the framed form control used in dialogs. `well` uses a quiet
26
+ * filled surface; `inline` reads as text. All open the same Menu.
27
27
  */
28
- variant?: "field" | "inline";
28
+ variant?: "field" | "inline" | "well";
29
29
  includeUnsetOption?: boolean;
30
30
  /**
31
31
  * Return focus to the trigger when the menu closes (inside a dialog). A list
@@ -5,6 +5,7 @@ import { Box, Text, useUiCapabilities } from "../../ui";
5
5
  import { formatRelativeAge } from "../../utils/datetime-format";
6
6
  import { Spinner } from "./loading";
7
7
  import { ButtonActionScope } from "./action-scope";
8
+ import { blendForContrast, higherContrast } from "../../theme/color-utils";
8
9
  import { Button } from "./button";
9
10
 
10
11
  /**
@@ -233,14 +234,27 @@ function EmptyStateBody({ title, message, hint, actions, retry, status = "empty"
233
234
  export interface NoticeProps {
234
235
  children: ReactNode;
235
236
  tone?: "muted" | "positive" | "warning" | "negative";
237
+ variant?: "inline" | "callout";
236
238
  }
237
239
 
238
240
  /** Inline feedback leaves existing content visible, including stale data after a refresh failure. */
239
- export function Notice({ children, tone = "warning" }: NoticeProps) {
241
+ export function Notice({ children, tone = "warning", variant = "inline" }: NoticeProps) {
240
242
  const colors = useThemeColors();
243
+ const native = useUiCapabilities().nativePaneChrome;
244
+ const color = tone === "muted" ? colors.textDim : colors[tone];
245
+ const status = tone === "negative" ? "error" : "notice";
246
+ // Inline feedback keeps its original single-text shape: a flex row here collapses inside content-sized parents.
247
+ if (variant !== "callout") {
248
+ return (
249
+ <Box data-gloom-status={status} data-gloom-ui="notice">
250
+ <Text fg={color} wrapText>{children}</Text>
251
+ </Box>
252
+ );
253
+ }
254
+ const foreground = blendForContrast(color, colors.panel, higherContrast("#000000", "#ffffff", colors.panel), 4.5);
241
255
  return (
242
- <Box data-gloom-status={tone === "negative" ? "error" : "notice"} data-gloom-ui="notice">
243
- <Text fg={tone === "muted" ? colors.textDim : colors[tone]} wrapText>{children}</Text>
256
+ <Box flexDirection="row" gap={1} paddingX={1} paddingY={native ? 0.35 : 0} backgroundColor={colors.panel} border={["left"]} borderColor={color} data-gloom-status={status} data-gloom-ui="notice" role={native ? tone === "negative" ? "alert" : "status" : undefined} aria-atomic={native ? true : undefined}>
257
+ <Text fg={foreground}>!</Text><Box flexGrow={1} minWidth={0}><Text fg={foreground} wrapText>{children}</Text></Box>
244
258
  </Box>
245
259
  );
246
260
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
2
2
  import { usePaneMenuItems, usePaneTabStripKeys } from "../layout/pane/footer/registration";
3
3
  import type { ContextMenuItem } from "../../types/context-menu";
4
4
  import { useShortcut } from "../../react/input";
@@ -22,6 +22,14 @@ type TabPointerEvent = {
22
22
  interface TabItem {
23
23
  label: string;
24
24
  value: string;
25
+ /**
26
+ * Artwork before the label on the desktop and the web, such as the logo of
27
+ * the broker a portfolio's account is at (`LogoMark`). It is decoration: the
28
+ * terminal does not draw it, and automation and screen readers read only
29
+ * `label`. Keep the same element between renders (useMemo): a title-bar
30
+ * strip redraws when it changes.
31
+ */
32
+ leading?: ReactNode;
25
33
  disabled?: boolean;
26
34
  /** Text color while inactive; used for group markers such as a team accent. */
27
35
  fg?: string;