gloomberb 0.10.0 → 0.10.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 (353) hide show
  1. package/package.json +8 -1
  2. package/src/api-client/auth.ts +1 -0
  3. package/src/api-client/index.test.ts +359 -108
  4. package/src/api-client/index.ts +24 -13
  5. package/src/api-client/socket.ts +70 -35
  6. package/src/api-client/types.ts +1 -1
  7. package/src/app/language-observer.test.tsx +82 -0
  8. package/src/app/language-observer.tsx +13 -0
  9. package/src/app/pane-runtime/index.tsx +2 -2
  10. package/src/app/pane-runtime/pane-settings-runtime.tsx +2 -2
  11. package/src/app/pane-runtime/pane-template-runtime.ts +2 -2
  12. package/src/app/pane-runtime/plugin-bindings.ts +2 -2
  13. package/src/app/pane-runtime/ticker-open-runtime.ts +2 -2
  14. package/src/app/runtime/broker-import.ts +2 -2
  15. package/src/app/runtime/plugin-bindings.ts +2 -2
  16. package/src/app/runtime/startup.ts +2 -2
  17. package/src/app.tsx +16 -7
  18. package/src/brokers/account-cache.ts +6 -6
  19. package/src/brokers/broker-ticker-sync.ts +3 -3
  20. package/src/brokers/remote-broker-adapter.ts +2 -2
  21. package/src/brokers/sync-broker-instance.ts +5 -6
  22. package/src/cli/commands/api.ts +3 -6
  23. package/src/cli/commands/automation.ts +7 -15
  24. package/src/cli/commands/market.ts +26 -61
  25. package/src/cli/commands/overview.ts +34 -45
  26. package/src/cli/commands/search.ts +25 -27
  27. package/src/cli/commands/system.ts +27 -57
  28. package/src/cli/commands/ticker.ts +62 -63
  29. package/src/cli/context.test.ts +63 -0
  30. package/src/cli/context.ts +15 -1
  31. package/src/cli/index.ts +1 -1
  32. package/src/cli/pane-functions/data.ts +2 -19
  33. package/src/cli/pane-functions/index.ts +26 -25
  34. package/src/cli/pane-functions/report.ts +1 -1
  35. package/src/cli/pane-functions/screenshot.ts +3 -3
  36. package/src/cli/scoped-context.ts +41 -0
  37. package/src/components/chart/composite/composite-chart.test.tsx +615 -18
  38. package/src/components/chart/composite/composite-chart.tsx +179 -79
  39. package/src/components/chart/composite/format.test.ts +20 -7
  40. package/src/components/chart/composite/index.ts +0 -21
  41. package/src/components/chart/composite/interactions.test.ts +90 -3
  42. package/src/components/chart/composite/interactions.ts +189 -24
  43. package/src/components/chart/composite/price-series.ts +0 -2
  44. package/src/components/chart/composite/rasterizer.ts +10 -9
  45. package/src/components/chart/composite/renderers.test.ts +108 -2
  46. package/src/components/chart/composite/scene.test.ts +36 -0
  47. package/src/components/chart/composite/scene.ts +5 -2
  48. package/src/components/chart/composite/text-renderer.ts +37 -25
  49. package/src/components/chart/composite/time-axis.test.ts +243 -0
  50. package/src/components/chart/composite/time-axis.ts +608 -0
  51. package/src/components/chart/composite/types.ts +12 -1
  52. package/src/components/chart/core/data.ts +2 -159
  53. package/src/components/chart/core/date-window.ts +6 -225
  54. package/src/components/chart/core/palette.ts +1 -12
  55. package/src/components/chart/core/pointer.ts +0 -142
  56. package/src/components/chart/core/types.ts +1 -58
  57. package/src/components/chart/cursor-motion.ts +2 -4
  58. package/src/components/chart/indicators/types.ts +0 -9
  59. package/src/components/chart/market-session.ts +0 -11
  60. package/src/components/chart/native/kitty/protocol.ts +1 -1
  61. package/src/components/chart/native/raster/layout.ts +3 -10
  62. package/src/components/chart/static/chart/axis-overlays.test.tsx +64 -0
  63. package/src/components/chart/static/chart/axis-overlays.tsx +59 -14
  64. package/src/components/chart/static/chart/surface.tsx +1 -2
  65. package/src/components/command-bar/action-runtime.ts +2 -2
  66. package/src/components/command-bar/commands/direct/index.ts +1 -1
  67. package/src/components/command-bar/panel/body.tsx +2 -2
  68. package/src/components/command-bar/panel/index.tsx +11 -9
  69. package/src/components/command-bar/routes/ticker-search/actions.ts +2 -2
  70. package/src/components/command-bar/surface/index.tsx +2 -2
  71. package/src/components/command-bar/workflow/body.tsx +3 -3
  72. package/src/components/command-bar/workflow/collection-actions.ts +2 -2
  73. package/src/components/command-bar/workflow/coordinator.ts +2 -2
  74. package/src/components/command-bar/workflow/field-row.tsx +25 -11
  75. package/src/components/command-bar/workflow/tickers.ts +2 -2
  76. package/src/components/feed-data-table/stack-view.test.tsx +28 -36
  77. package/src/components/feed-data-table/stack-view.tsx +3 -1
  78. package/src/components/layout/detached-pane-shell.tsx +6 -6
  79. package/src/components/layout/header.tsx +13 -9
  80. package/src/components/layout/pane/content.test.tsx +42 -3
  81. package/src/components/layout/pane/content.tsx +3 -1
  82. package/src/components/layout/pane/footer/index.test.tsx +45 -2
  83. package/src/components/layout/pane/footer/registration.tsx +3 -1
  84. package/src/components/layout/shell/index.tsx +1 -2
  85. package/src/components/layout/shell/native/surfaces.ts +1 -1
  86. package/src/components/layout/shell/native/window-state.ts +1 -1
  87. package/src/components/layout/status-bar.tsx +12 -8
  88. package/src/components/layout/window-controls.tsx +8 -6
  89. package/src/components/onboarding/onboarding-wizard.tsx +3 -1
  90. package/src/components/pane-settings-dialog/desktop.tsx +0 -2
  91. package/src/components/pane-settings-dialog.tsx +0 -2
  92. package/src/components/pane-template-wizard.tsx +10 -9
  93. package/src/components/price-performance.tsx +2 -113
  94. package/src/components/price-sparkline/model.ts +15 -17
  95. package/src/components/price-sparkline/view.tsx +8 -7
  96. package/src/components/speedometer-gauge/desktop.tsx +2 -1
  97. package/src/components/speedometer-gauge/index.tsx +0 -2
  98. package/src/components/speedometer-gauge/terminal.tsx +24 -5
  99. package/src/components/ticker/badge/index.tsx +1 -18
  100. package/src/components/ui/data-table/opentui/index.tsx +3 -3
  101. package/src/components/ui/frame.tsx +1 -2
  102. package/src/components/ui/page-stack-view.test.tsx +126 -0
  103. package/src/components/ui/page-stack-view.tsx +24 -12
  104. package/src/components/ui/tabs.tsx +7 -3
  105. package/src/components/ui/ui.test.tsx +23 -2
  106. package/src/components/use-external-link-footer.ts +4 -2
  107. package/src/core/app-service-ports.ts +46 -0
  108. package/src/core/app-services.test.ts +66 -0
  109. package/src/core/app-services.ts +17 -15
  110. package/src/data/config/chart-settings.ts +3 -10
  111. package/src/data/config/store/index.test.ts +99 -2
  112. package/src/data/config/store/migrations.ts +278 -0
  113. package/src/data/config/store/normalize.ts +44 -186
  114. package/src/data/resource-store.ts +73 -12
  115. package/src/data/sqlite/cache.test.ts +104 -15
  116. package/src/i18n/index.test.ts +17 -0
  117. package/src/i18n/index.ts +23 -6
  118. package/src/i18n/ja.ts +13 -0
  119. package/src/i18n/ko.ts +13 -0
  120. package/src/i18n/react.ts +6 -0
  121. package/src/i18n/zh-cn.ts +13 -0
  122. package/src/i18n/zh-tw.ts +13 -0
  123. package/src/market-data/coordinator/auxiliary.ts +7 -3
  124. package/src/market-data/coordinator/chart.ts +1 -1
  125. package/src/market-data/coordinator/index.test.ts +26 -0
  126. package/src/market-data/coordinator/index.ts +11 -5
  127. package/src/market-data/coordinator/quotes.ts +4 -40
  128. package/src/market-data/coordinator/subscriptions.test.ts +89 -1
  129. package/src/market-data/hooks.tsx +47 -1
  130. package/src/market-data/market/status.ts +6 -6
  131. package/src/market-data/performance.ts +1 -1
  132. package/src/market-data/quote-subscription-target.test.ts +37 -0
  133. package/src/market-data/quote-subscription-target.ts +60 -0
  134. package/src/market-data/quotes/freshness.ts +1 -0
  135. package/src/market-data/quotes/resolution.ts +4 -0
  136. package/src/market-data/request-types.ts +3 -3
  137. package/src/news/aggregator.test.ts +97 -1
  138. package/src/news/aggregator.ts +100 -57
  139. package/src/news/hooks.test.tsx +64 -0
  140. package/src/news/hooks.ts +18 -25
  141. package/src/plugins/builtin/account-management/footer.ts +4 -2
  142. package/src/plugins/builtin/account-management/pane.tsx +9 -4
  143. package/src/plugins/builtin/ai/agent-history.ts +11 -7
  144. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +0 -5
  145. package/src/plugins/builtin/ai/ask-ai.test.tsx +3 -4
  146. package/src/plugins/builtin/ai/index.test.ts +2 -2
  147. package/src/plugins/builtin/ai/index.tsx +10 -11
  148. package/src/plugins/builtin/ai/pane-settings.ts +1 -1
  149. package/src/plugins/builtin/ai/pi/runtime.ts +6 -5
  150. package/src/plugins/builtin/ai/providers.ts +1 -9
  151. package/src/plugins/builtin/ai/runner.ts +0 -6
  152. package/src/plugins/builtin/ai/screener/footer.ts +4 -1
  153. package/src/plugins/builtin/ai/screener/pane.test.tsx +9 -9
  154. package/src/plugins/builtin/ai/workspace/model.ts +2 -27
  155. package/src/plugins/builtin/ai/workspace/pane.tsx +2 -2
  156. package/src/plugins/builtin/analytics/metrics.ts +1 -7
  157. package/src/plugins/builtin/broker-manager/detail.tsx +3 -2
  158. package/src/plugins/builtin/broker-manager/index.tsx +4 -2
  159. package/src/plugins/builtin/chart-composer/editor/controller.ts +643 -0
  160. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +134 -0
  161. package/src/plugins/builtin/chart-composer/editor/model.test.ts +85 -0
  162. package/src/plugins/builtin/chart-composer/editor/model.ts +292 -0
  163. package/src/plugins/builtin/chart-composer/editor/opentui-view.tsx +108 -0
  164. package/src/plugins/builtin/chart-composer/editor.tsx +65 -947
  165. package/src/plugins/builtin/chart-composer/index.tsx +0 -6
  166. package/src/plugins/builtin/chart-composer/pane.tsx +69 -36
  167. package/src/plugins/builtin/chart-composer/presets.ts +1 -19
  168. package/src/plugins/builtin/chart-composer/settings.ts +1 -1
  169. package/src/plugins/builtin/chat/content/composer-runtime.ts +1 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +0 -1
  171. package/src/plugins/builtin/chat/controller/fetch.ts +8 -2
  172. package/src/plugins/builtin/chat/controller/persistence.ts +2 -0
  173. package/src/plugins/builtin/chat/controller/session.test.ts +25 -0
  174. package/src/plugins/builtin/chat/controller/session.ts +2 -0
  175. package/src/plugins/builtin/chat/controller/state.ts +7 -0
  176. package/src/plugins/builtin/chat/controller.test.ts +73 -20
  177. package/src/plugins/builtin/chat/message/desktop.tsx +2 -0
  178. package/src/plugins/builtin/cloud-tweets/footer.ts +9 -28
  179. package/src/plugins/builtin/cloud-tweets/pane.tsx +0 -5
  180. package/src/plugins/builtin/cloud-tweets/table.tsx +12 -17
  181. package/src/plugins/builtin/correlation/compute.ts +4 -10
  182. package/src/plugins/builtin/econ/index.tsx +17 -19
  183. package/src/plugins/builtin/holders/types.ts +0 -1
  184. package/src/plugins/builtin/insider/index.tsx +6 -31
  185. package/src/plugins/builtin/news/index.tsx +5 -7
  186. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +6 -8
  187. package/src/plugins/builtin/news/wire/news/footer.ts +10 -11
  188. package/src/plugins/builtin/news/wire/read-state.test.ts +7 -0
  189. package/src/plugins/builtin/news/wire/read-state.ts +18 -28
  190. package/src/plugins/builtin/news/wire/rss/parser.ts +1 -11
  191. package/src/plugins/builtin/options/footer.test.ts +115 -0
  192. package/src/plugins/builtin/options/footer.ts +110 -0
  193. package/src/plugins/builtin/options/live-quotes.test.ts +238 -0
  194. package/src/plugins/builtin/options/live-quotes.ts +147 -0
  195. package/src/plugins/builtin/options/view.test.tsx +71 -1
  196. package/src/plugins/builtin/options/view.tsx +39 -24
  197. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +103 -117
  198. package/src/plugins/builtin/portfolio-list/cli/render.ts +11 -5
  199. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -135
  200. package/src/plugins/builtin/portfolio-list/header.tsx +9 -4
  201. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -1
  202. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +4 -2
  203. package/src/plugins/builtin/research/corporate-actions-pane.tsx +24 -52
  204. package/src/plugins/builtin/sec/filing-content.test.ts +41 -0
  205. package/src/plugins/builtin/sec/filing-content.ts +94 -0
  206. package/src/plugins/builtin/sec/index.tsx +21 -104
  207. package/src/plugins/builtin/sectors/sector-model.ts +1 -7
  208. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -0
  209. package/src/plugins/builtin/shared/pane-footer.ts +82 -0
  210. package/src/plugins/builtin/shared/read-state.ts +64 -3
  211. package/src/plugins/builtin/substack/read-state.ts +16 -41
  212. package/src/plugins/builtin/thirteenf/pane.tsx +24 -73
  213. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +10 -10
  214. package/src/plugins/builtin/yield-curve/index.tsx +11 -9
  215. package/src/plugins/ibkr/dialogs.tsx +3 -3
  216. package/src/plugins/ibkr/gateway/fundamentals.ts +1 -1
  217. package/src/plugins/ibkr/gateway/history-requests.ts +2 -2
  218. package/src/plugins/ibkr/gateway/history.ts +2 -2
  219. package/src/plugins/ibkr/gateway/selection.ts +2 -2
  220. package/src/plugins/ibkr/gateway/service/index.ts +2 -2
  221. package/src/plugins/ibkr/gateway/service/native.ts +2 -2
  222. package/src/plugins/plugin-persistence.ts +3 -4
  223. package/src/plugins/prediction-markets/controller/keyboard.ts +1 -1
  224. package/src/plugins/prediction-markets/controller/live-updates.ts +6 -5
  225. package/src/plugins/prediction-markets/detail/book.tsx +2 -2
  226. package/src/plugins/prediction-markets/detail/outcomes.tsx +3 -1
  227. package/src/plugins/prediction-markets/detail/pane.tsx +3 -1
  228. package/src/plugins/prediction-markets/detail/trades.tsx +3 -3
  229. package/src/plugins/registry/context.ts +3 -4
  230. package/src/plugins/registry/index.ts +5 -6
  231. package/src/react/input.test.ts +126 -3
  232. package/src/react/input.ts +120 -1
  233. package/src/remote/app-host.tsx +2 -13
  234. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +44 -60
  235. package/src/renderers/electrobun/bun/desktop/capability-bridge.ts +15 -13
  236. package/src/renderers/electrobun/bun/desktop/host-requests.ts +26 -22
  237. package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -1
  238. package/src/renderers/electrobun/bun/desktop/initialization.ts +11 -6
  239. package/src/renderers/electrobun/bun/desktop/media.ts +5 -3
  240. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +21 -22
  241. package/src/renderers/electrobun/bun/desktop/window-controls.ts +3 -1
  242. package/src/renderers/electrobun/bun/desktop/workspace/requests.ts +18 -18
  243. package/src/renderers/electrobun/bun/index.ts +123 -83
  244. package/src/renderers/electrobun/shared/protocol.ts +146 -7
  245. package/src/renderers/electrobun/view/app-services.ts +5 -8
  246. package/src/renderers/electrobun/view/backend-rpc.ts +18 -8
  247. package/src/renderers/electrobun/view/broker-remote-client.ts +2 -2
  248. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +5 -22
  249. package/src/renderers/electrobun/view/config-host.ts +1 -1
  250. package/src/renderers/electrobun/view/context-menu.ts +1 -17
  251. package/src/renderers/electrobun/view/data-table/index.tsx +0 -1
  252. package/src/renderers/electrobun/view/data-table/row.tsx +0 -4
  253. package/src/renderers/electrobun/view/desktop/control-styles.ts +37 -34
  254. package/src/renderers/electrobun/view/desktop/controls.tsx +36 -33
  255. package/src/renderers/electrobun/view/desktop/list-view.tsx +15 -12
  256. package/src/renderers/electrobun/view/desktop/popover.tsx +2 -3
  257. package/src/renderers/electrobun/view/dialog-host.tsx +2 -3
  258. package/src/renderers/electrobun/view/fatal-screen.tsx +0 -1
  259. package/src/renderers/electrobun/view/host/box.tsx +0 -1
  260. package/src/renderers/electrobun/view/host/chart-surface.tsx +0 -1
  261. package/src/renderers/electrobun/view/host/input.tsx +0 -1
  262. package/src/renderers/electrobun/view/host/media-surface.tsx +0 -1
  263. package/src/renderers/electrobun/view/host/native.ts +0 -1
  264. package/src/renderers/electrobun/view/host/scroll-box.tsx +0 -1
  265. package/src/renderers/electrobun/view/host/tabs.tsx +0 -1
  266. package/src/renderers/electrobun/view/host/text.tsx +0 -1
  267. package/src/renderers/electrobun/view/http-fetch.ts +3 -11
  268. package/src/renderers/electrobun/view/input-host.tsx +10 -53
  269. package/src/renderers/electrobun/view/key-event.ts +0 -1
  270. package/src/renderers/electrobun/view/main.tsx +3 -3
  271. package/src/renderers/electrobun/view/native-renderer.ts +0 -1
  272. package/src/renderers/electrobun/view/native-stubs/backend-rpc.ts +13 -1
  273. package/src/renderers/electrobun/view/native-stubs/chart/surface-manager.ts +11 -0
  274. package/src/renderers/electrobun/view/native-stubs/native-stubs.test.ts +47 -0
  275. package/src/renderers/electrobun/view/remote/asset-data-client.ts +6 -44
  276. package/src/renderers/electrobun/view/remote/persistence.ts +6 -2
  277. package/src/renderers/electrobun/view/remote/quote-subscription-registry.test.ts +86 -0
  278. package/src/renderers/electrobun/view/remote/quote-subscription-registry.ts +72 -0
  279. package/src/renderers/electrobun/view/remote/ticker-repository.ts +2 -2
  280. package/src/renderers/electrobun/view/toast-host.tsx +0 -1
  281. package/src/renderers/electrobun/view/ui-host.tsx +1 -2
  282. package/src/renderers/electrobun/view/update-host.ts +1 -1
  283. package/src/renderers/opentui/chart-surface.tsx +2 -1
  284. package/src/renderers/opentui/dialog-host.tsx +20 -4
  285. package/src/renderers/opentui/image/loader.ts +12 -2
  286. package/src/renderers/opentui/input-host.tsx +7 -49
  287. package/src/renderers/opentui/start.tsx +2 -0
  288. package/src/sources/gloomberb-cloud/history-quality.ts +44 -0
  289. package/src/sources/gloomberb-cloud/index.test.ts +173 -0
  290. package/src/sources/gloomberb-cloud/index.ts +42 -20
  291. package/src/sources/gloomberb-cloud/normalizers.ts +9 -3
  292. package/src/sources/provider-router/cache.ts +1 -1
  293. package/src/sources/provider-router/document-routes.ts +10 -5
  294. package/src/sources/provider-router/financials.test.ts +14 -0
  295. package/src/sources/provider-router/financials.ts +6 -1
  296. package/src/sources/provider-router/history.test.ts +163 -1
  297. package/src/sources/provider-router/history.ts +278 -347
  298. package/src/sources/provider-router/index.ts +2 -2
  299. package/src/sources/provider-router/routing.test.ts +39 -0
  300. package/src/sources/provider-router/routing.ts +24 -11
  301. package/src/sources/provider-router/supplemental.ts +37 -17
  302. package/src/sources/sec-edgar/content.ts +2 -12
  303. package/src/sources/sec-edgar.ts +2 -13
  304. package/src/sources/yahoo-finance/history.ts +2 -2
  305. package/src/sources/yahoo-finance/options.ts +5 -0
  306. package/src/sources/yahoo-finance.ts +2 -2
  307. package/src/state/app/bootstrap.ts +2 -2
  308. package/src/state/app/context/index.tsx +2 -2
  309. package/src/state/hooks/quote-streaming.ts +66 -1
  310. package/src/state/hooks/session-snapshot.ts +2 -2
  311. package/src/state/selectors.ts +0 -9
  312. package/src/sync/controller.test.ts +98 -0
  313. package/src/sync/controller.ts +29 -5
  314. package/src/sync/react.ts +2 -2
  315. package/src/sync/types.ts +2 -2
  316. package/src/theme/colors.test.ts +10 -0
  317. package/src/theme/colors.ts +75 -78
  318. package/src/theme/theme-context.test.tsx +67 -0
  319. package/src/theme/theme-context.tsx +9 -5
  320. package/src/tickers/open-target.ts +2 -2
  321. package/src/tickers/search/upsert.ts +2 -2
  322. package/src/time-series/alignment.ts +0 -7
  323. package/src/time-series/chart-data.ts +145 -0
  324. package/src/time-series/date-window.ts +44 -0
  325. package/src/time-series/history-window.ts +21 -0
  326. package/src/time-series/hooks.test.ts +59 -0
  327. package/src/time-series/hooks.ts +33 -12
  328. package/src/time-series/market-resolution.ts +2 -2
  329. package/src/time-series/market.ts +1 -1
  330. package/src/time-series/range.ts +9 -0
  331. package/src/{components/chart/core → time-series}/resolution.ts +3 -102
  332. package/src/time-series/resolve.test.ts +203 -1
  333. package/src/time-series/resolve.ts +96 -18
  334. package/src/time-series/spec-studies.test.ts +15 -0
  335. package/src/time-series/spec.ts +2 -2
  336. package/src/time-series/studies.ts +5 -2
  337. package/src/time-series/types.ts +3 -1
  338. package/src/time-series/use-chart-resolution.test.tsx +358 -0
  339. package/src/time-series/use-chart-resolution.ts +46 -4
  340. package/src/types/broker.ts +2 -2
  341. package/src/types/data-provider.ts +3 -3
  342. package/src/types/financials.ts +12 -0
  343. package/src/types/plugin.ts +2 -2
  344. package/src/ui/context-menu.tsx +3 -2
  345. package/src/ui/dialog.tsx +2 -2
  346. package/src/utils/html-entities.ts +3 -5
  347. package/src/utils/main-thread-monitor.ts +1 -1
  348. package/src/utils/price-history.ts +5 -5
  349. package/src/version.ts +3 -1
  350. package/src/components/chart/core/viewport.test.ts +0 -8
  351. package/src/components/chart/core/viewport.ts +0 -24
  352. package/src/time-series/index.ts +0 -14
  353. /package/src/{components/chart/core → time-series}/resolution.test.ts +0 -0
@@ -6,6 +6,7 @@ import type { WizardStep } from "../types/plugin";
6
6
  import { colors } from "../theme/colors";
7
7
  import { isPlainKey } from "../utils/keyboard";
8
8
  import { DialogFrame, ListView, TextField } from "./ui";
9
+ import { t } from "../i18n";
9
10
 
10
11
  export function PaneTemplateInfoStep({
11
12
  dismiss,
@@ -24,7 +25,7 @@ export function PaneTemplateInfoStep({
24
25
  <Box flexDirection="column">
25
26
  {step.body?.map((line, index) => (
26
27
  <Box key={`${step.key}:${index}`} height={1}>
27
- <Text fg={colors.textDim}>{line || " "}</Text>
28
+ <Text fg={colors.textDim}>{line ? t(line) : " "}</Text>
28
29
  </Box>
29
30
  ))}
30
31
  </Box>
@@ -55,14 +56,14 @@ export function PaneTemplateInputStep({
55
56
  <Box flexDirection="column">
56
57
  {step.body?.map((line, index) => (
57
58
  <Box key={`${step.key}:${index}`} height={1}>
58
- <Text fg={colors.textDim}>{line || " "}</Text>
59
+ <Text fg={colors.textDim}>{line ? t(line) : " "}</Text>
59
60
  </Box>
60
61
  ))}
61
62
  <Box height={1} />
62
63
  <TextField
63
64
  inputRef={inputRef}
64
65
  value={value}
65
- placeholder={step.placeholder || ""}
66
+ placeholder={step.placeholder ? t(step.placeholder) : ""}
66
67
  type={step.type === "password" ? "password" : "text"}
67
68
  focused
68
69
  onChange={setValue}
@@ -112,7 +113,7 @@ export function PaneTemplateTextareaStep({
112
113
  <Box flexDirection="column" flexGrow={1}>
113
114
  {step.body?.map((line, index) => (
114
115
  <Box key={`${step.key}:${index}`} height={1}>
115
- <Text fg={colors.textDim}>{line || " "}</Text>
116
+ <Text fg={colors.textDim}>{line ? t(line) : " "}</Text>
116
117
  </Box>
117
118
  ))}
118
119
  <Box height={1} />
@@ -126,7 +127,7 @@ export function PaneTemplateTextareaStep({
126
127
  <Textarea
127
128
  ref={textareaRef}
128
129
  initialValue={step.defaultValue ?? ""}
129
- placeholder={step.placeholder || ""}
130
+ placeholder={step.placeholder ? t(step.placeholder) : ""}
130
131
  focused
131
132
  textColor={colors.text}
132
133
  placeholderColor={colors.textDim}
@@ -141,13 +142,13 @@ export function PaneTemplateTextareaStep({
141
142
  backgroundColor={colors.selected}
142
143
  onMouseDown={commit}
143
144
  >
144
- <Text fg={colors.selectedText}>{` Save `}</Text>
145
+ <Text fg={colors.selectedText}>{` ${t("Save")} `}</Text>
145
146
  </Box>
146
147
  <Box
147
148
  backgroundColor={colors.panel}
148
149
  onMouseDown={() => resolve("")}
149
150
  >
150
- <Text fg={colors.text}>{` Cancel `}</Text>
151
+ <Text fg={colors.text}>{` ${t("Cancel")} `}</Text>
151
152
  </Box>
152
153
  </Box>
153
154
  </Box>
@@ -186,14 +187,14 @@ export function PaneTemplateSelectStep({
186
187
  <Box flexDirection="column">
187
188
  {step.body?.map((line, index) => (
188
189
  <Box key={`${step.key}:${index}`} height={1}>
189
- <Text fg={colors.textDim}>{line || " "}</Text>
190
+ <Text fg={colors.textDim}>{line ? t(line) : " "}</Text>
190
191
  </Box>
191
192
  ))}
192
193
  <Box height={1} />
193
194
  <ListView
194
195
  items={options.map((option) => ({
195
196
  id: option.value,
196
- label: option.label,
197
+ label: t(option.label),
197
198
  }))}
198
199
  selectedIndex={selectedIndex}
199
200
  bgColor={colors.commandBg}
@@ -1,24 +1,10 @@
1
- import { Box, ScrollBox, Text } from "../ui";
2
- import { blendHex, colors, priceColor } from "../theme/colors";
1
+ import { Box, Text } from "../ui";
2
+ import { colors, priceColor } from "../theme/colors";
3
3
  import { displayWidth, formatPercent, padTo } from "../utils/format";
4
4
  import type { PriceReturnField } from "../market-data/performance";
5
5
 
6
- type MouseInteractionEvent = {
7
- stopPropagation?: () => void;
8
- preventDefault?: () => void;
9
- };
10
-
11
6
  const STRIP_COLUMN_GAP = 1;
12
7
  const RETURN_CELL_MIN_WIDTH = 7;
13
- const SYMBOL_COLUMN_MIN_WIDTH = 6;
14
- const SYMBOL_COLUMN_MAX_WIDTH = 12;
15
-
16
- export interface PriceReturnTableRow {
17
- symbol: string;
18
- color: string;
19
- fields: PriceReturnField[];
20
- selected: boolean;
21
- }
22
8
 
23
9
  function formatReturnValue(value: number | null): string {
24
10
  return value == null ? "-" : formatPercent(value);
@@ -84,100 +70,3 @@ export function PriceReturnStrip({
84
70
  </Box>
85
71
  );
86
72
  }
87
-
88
- function getFieldById(fields: readonly PriceReturnField[], id: string): PriceReturnField | null {
89
- return fields.find((field) => field.id === id) ?? null;
90
- }
91
-
92
- function chooseComparisonFieldIds(width: number, availableIds: readonly string[]): string[] {
93
- const preferred = width >= 86
94
- ? ["RNG", "1M", "3M", "6M", "1Y", "3Y", "5Y"]
95
- : width >= 72
96
- ? ["RNG", "1M", "3M", "1Y", "3Y", "5Y"]
97
- : width >= 58
98
- ? ["RNG", "1M", "1Y", "3Y", "5Y"]
99
- : width >= 44
100
- ? ["RNG", "1M", "1Y", "5Y"]
101
- : ["1Y", "5Y"];
102
- const available = new Set(availableIds);
103
- return preferred.filter((id) => available.has(id));
104
- }
105
-
106
- export function PriceReturnTable({
107
- height,
108
- onFocusInteraction,
109
- onOpenSymbol,
110
- onSelectSymbol,
111
- rows,
112
- width,
113
- }: {
114
- height: number;
115
- onFocusInteraction?: (event: MouseInteractionEvent | null | undefined) => void;
116
- onOpenSymbol: (symbol: string) => void;
117
- onSelectSymbol: (symbol: string) => void;
118
- rows: PriceReturnTableRow[];
119
- width: number;
120
- }) {
121
- if (height <= 0 || rows.length === 0) return null;
122
-
123
- const availableIds = rows[0]?.fields.map((field) => field.id) ?? [];
124
- const fieldIds = chooseComparisonFieldIds(width, availableIds);
125
- if (fieldIds.length === 0) return null;
126
-
127
- const valueWidth = Math.max(
128
- RETURN_CELL_MIN_WIDTH,
129
- Math.floor((width - SYMBOL_COLUMN_MIN_WIDTH - fieldIds.length) / Math.max(fieldIds.length, 1)),
130
- );
131
- const symbolWidth = Math.min(
132
- SYMBOL_COLUMN_MAX_WIDTH,
133
- Math.max(SYMBOL_COLUMN_MIN_WIDTH, width - fieldIds.length * (valueWidth + 1)),
134
- );
135
- const fieldsById = rows[0]?.fields ?? [];
136
-
137
- return (
138
- <ScrollBox height={height} scrollY>
139
- <Box flexDirection="column" width={width}>
140
- <Box flexDirection="row" height={1}>
141
- <Text fg={colors.textDim}>{padTo("Sym", symbolWidth)}</Text>
142
- {fieldIds.map((fieldId) => {
143
- const field = getFieldById(fieldsById, fieldId);
144
- return (
145
- <Box key={fieldId} flexDirection="row">
146
- <Text fg={colors.textDim}>{" "}</Text>
147
- <Text fg={colors.textDim}>{padTo(field?.label ?? fieldId, valueWidth, "right")}</Text>
148
- </Box>
149
- );
150
- })}
151
- </Box>
152
-
153
- {rows.map((row) => (
154
- <Box
155
- key={row.symbol}
156
- flexDirection="row"
157
- height={1}
158
- width={width}
159
- backgroundColor={row.selected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
160
- onMouseOver={() => onSelectSymbol(row.symbol)}
161
- onMouseDown={(event: any) => {
162
- onFocusInteraction?.(event);
163
- onSelectSymbol(row.symbol);
164
- onOpenSymbol(row.symbol);
165
- }}
166
- >
167
- <Text fg={row.color}>{padTo(`${row.selected ? ">" : " "} ${row.symbol}`, symbolWidth)}</Text>
168
- {fieldIds.map((fieldId) => {
169
- const field = getFieldById(row.fields, fieldId);
170
- const value = field?.value ?? null;
171
- return (
172
- <Box key={fieldId} flexDirection="row">
173
- <Text fg={colors.textDim}>{" "}</Text>
174
- <Text fg={returnValueColor(value)}>{padTo(formatReturnValue(value), valueWidth, "right")}</Text>
175
- </Box>
176
- );
177
- })}
178
- </Box>
179
- ))}
180
- </Box>
181
- </ScrollBox>
182
- );
183
- }
@@ -1,5 +1,6 @@
1
- import { colors } from "../../theme/colors";
1
+ import { colors, type ThemeColors } from "../../theme/colors";
2
2
  import type { PricePoint } from "../../types/financials";
3
+ import { getPricePointTimestamp } from "../../utils/price-history";
3
4
 
4
5
  const SPARKLINE_FALLBACK_POINTS = 22;
5
6
 
@@ -29,22 +30,15 @@ function closeValue(point: PricePoint): number | null {
29
30
  return Number.isFinite(point.close) ? point.close : null;
30
31
  }
31
32
 
32
- function getPointTime(point: PricePoint): number {
33
- const value = point.date as Date | string | number | null | undefined;
34
- if (value instanceof Date) return value.getTime();
35
- if (value == null) return Number.NaN;
36
- return new Date(value).getTime();
37
- }
38
-
39
33
  export function resolveSparklineHistory(priceHistory: PricePoint[], period: PriceSparklinePeriod = "1M"): PricePoint[] {
40
- const validHistory = priceHistory.filter((point) => Number.isFinite(getPointTime(point)));
34
+ const validHistory = priceHistory.filter((point) => Number.isFinite(getPricePointTimestamp(point)));
41
35
  const latest = validHistory.at(-1);
42
36
  if (!latest) return [];
43
37
 
44
- const latestTime = getPointTime(latest);
38
+ const latestTime = getPricePointTimestamp(latest);
45
39
  if (Number.isFinite(latestTime)) {
46
40
  const cutoffTime = latestTime - PERIOD_WINDOW_DAYS[period] * 86_400_000;
47
- const windowHistory = validHistory.filter((point) => getPointTime(point) >= cutoffTime);
41
+ const windowHistory = validHistory.filter((point) => getPricePointTimestamp(point) >= cutoffTime);
48
42
  if (windowHistory.length >= 2) return windowHistory;
49
43
  }
50
44
 
@@ -57,15 +51,19 @@ export function sparklineValues(priceHistory: PricePoint[]): number[] {
57
51
  .filter((value): value is number => value != null);
58
52
  }
59
53
 
60
- export function sparklineColor(values: number[], trend?: PriceSparklineTrend): string {
61
- if (trend === "positive") return colors.positive;
62
- if (trend === "negative") return colors.negative;
63
- if (trend === "neutral") return colors.textMuted;
54
+ export function sparklineColor(
55
+ values: number[],
56
+ trend?: PriceSparklineTrend,
57
+ palette: ThemeColors = colors,
58
+ ): string {
59
+ if (trend === "positive") return palette.positive;
60
+ if (trend === "negative") return palette.negative;
61
+ if (trend === "neutral") return palette.textMuted;
64
62
 
65
63
  const first = values[0];
66
64
  const last = values.at(-1);
67
- if (first == null || last == null) return colors.textMuted;
68
- return last >= first ? colors.positive : colors.negative;
65
+ if (first == null || last == null) return palette.textMuted;
66
+ return last >= first ? palette.positive : palette.negative;
69
67
  }
70
68
 
71
69
  export function buildSamples(values: number[], width: number, height: number, padding: number): SparklineSample[] {
@@ -7,7 +7,6 @@ import {
7
7
  useUiCapabilities,
8
8
  useUiHost,
9
9
  } from "../../ui";
10
- import { colors } from "../../theme/colors";
11
10
  import { useThemeColors } from "../../theme/theme-context";
12
11
  import type { PricePoint } from "../../types/financials";
13
12
  import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
@@ -37,16 +36,18 @@ function DesktopPriceSparkline({
37
36
  width,
38
37
  height,
39
38
  color,
39
+ emptyColor,
40
40
  }: {
41
41
  values: number[];
42
42
  width: number;
43
43
  height: number;
44
44
  color: string;
45
+ emptyColor: string;
45
46
  }) {
46
47
  const svgWidth = Math.max(24, width * 8);
47
48
  const svgHeight = Math.max(18, height * 18);
48
49
  const path = svgPath(buildSamples(values, svgWidth, svgHeight, 2));
49
- if (!path) return <Text fg={colors.textMuted}>{" "}</Text>;
50
+ if (!path) return <Text fg={emptyColor}>{" "}</Text>;
50
51
 
51
52
  return (
52
53
  <Box width={width} height={height} justifyContent="center" overflow="hidden">
@@ -149,7 +150,7 @@ export function PriceSparkline({
149
150
  height?: number;
150
151
  area?: boolean;
151
152
  }) {
152
- useThemeColors();
153
+ const colors = useThemeColors();
153
154
  const uiHost = useUiHost();
154
155
  const sparklineHistory = useMemo(() => resolveSparklineHistory(priceHistory ?? [], period), [period, priceHistory]);
155
156
  const values = useMemo(() => sparklineValues(sparklineHistory), [sparklineHistory]);
@@ -157,9 +158,9 @@ export function PriceSparkline({
157
158
  return <Text fg={colors.textMuted}>{" "}</Text>;
158
159
  }
159
160
 
160
- const color = sparklineColor(values, trend);
161
+ const color = sparklineColor(values, trend, colors);
161
162
  return uiHost.kind === "desktop-web"
162
- ? <DesktopPriceSparkline values={values} width={width} height={height} color={color} />
163
+ ? <DesktopPriceSparkline values={values} width={width} height={height} color={color} emptyColor={colors.textMuted} />
163
164
  : <TerminalPriceSparkline priceHistory={sparklineHistory} values={values} width={width} height={height} color={color} area={area} />;
164
165
  }
165
166
 
@@ -172,13 +173,13 @@ export function PriceAreaSparklineBackground({
172
173
  trend?: PriceSparklineTrend;
173
174
  period?: PriceSparklinePeriod;
174
175
  }) {
175
- useThemeColors();
176
+ const colors = useThemeColors();
176
177
  const uiHost = useUiHost();
177
178
  const sparklineHistory = useMemo(() => resolveSparklineHistory(priceHistory ?? [], period), [period, priceHistory]);
178
179
  const values = useMemo(() => sparklineValues(sparklineHistory), [sparklineHistory]);
179
180
  if (uiHost.kind !== "desktop-web" || values.length < 2) return null;
180
181
 
181
- const color = sparklineColor(values, trend);
182
+ const color = sparklineColor(values, trend, colors);
182
183
  const baseline = 100;
183
184
  const samples = buildSamples(values, 100, baseline, 0);
184
185
  const linePath = svgPath(samples);
@@ -1,6 +1,6 @@
1
1
  import { createElement, type SVGProps } from "react";
2
2
  import { Box } from "../../ui";
3
- import { colors } from "../../theme/colors";
3
+ import { useThemeColors } from "../../theme/theme-context";
4
4
  import {
5
5
  compactSegmentLabel,
6
6
  formatGaugeValue,
@@ -54,6 +54,7 @@ export function DesktopSpeedometerGauge({
54
54
  maxWidth,
55
55
  compact,
56
56
  }: Required<SpeedometerGaugeProps>) {
57
+ const colors = useThemeColors();
57
58
  const gaugeWidth = Math.min(Math.max(width - 2, minWidth), maxWidth);
58
59
  const gaugeHeight = compact ? 9 : 12;
59
60
  const viewBoxHeight = compact ? DESKTOP_COMPACT_VIEWBOX_HEIGHT : DESKTOP_VIEWBOX_HEIGHT;
@@ -1,5 +1,4 @@
1
1
  import { useUiHost } from "../../ui";
2
- import { useThemeColors } from "../../theme/theme-context";
3
2
  import { DesktopSpeedometerGauge } from "./desktop";
4
3
  import {
5
4
  DEFAULT_MAX_WIDTH,
@@ -25,7 +24,6 @@ export function SpeedometerGauge({
25
24
  maxWidth = DEFAULT_MAX_WIDTH,
26
25
  compact = false,
27
26
  }: SpeedometerGaugeProps) {
28
- useThemeColors();
29
27
  const props = {
30
28
  value,
31
29
  valueLabel,
@@ -3,7 +3,8 @@ import { Box, ChartSurface, Span, Text, TextAttributes, useNativeRenderer, useUi
3
3
  import type { NativeChartBitmap } from "../chart/native/chart-rasterizer";
4
4
  import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../chart/native/bitmap-support";
5
5
  import { drawCircle, drawLine, parseHex } from "../chart/native/raster/primitives";
6
- import { colors } from "../../theme/colors";
6
+ import { useThemeColors } from "../../theme/theme-context";
7
+ import type { ThemeColors } from "../../theme/colors";
7
8
  import {
8
9
  compactSegmentLabel,
9
10
  formatGaugeValue,
@@ -73,7 +74,7 @@ function renderLabelRow(width: number, min: number, max: number, segments: Speed
73
74
  return cellsToChunks(cells);
74
75
  }
75
76
 
76
- function renderTickRow(width: number, min: number, max: number): GaugeChunk[] {
77
+ function renderTickRow(width: number, min: number, max: number, colors: Pick<ThemeColors, "textDim">): GaugeChunk[] {
77
78
  const cells = blankGaugeLine(width);
78
79
  for (const tick of [min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max]) {
79
80
  placeText(cells, formatGaugeValue(tick), normalizeValue(tick, min, max) * (width - 1), colors.textDim);
@@ -81,7 +82,14 @@ function renderTickRow(width: number, min: number, max: number): GaugeChunk[] {
81
82
  return cellsToChunks(cells);
82
83
  }
83
84
 
84
- function renderArcRows(value: number, min: number, max: number, width: number, segments: SpeedometerSegment[]): GaugeChunk[][] {
85
+ function renderArcRows(
86
+ value: number,
87
+ min: number,
88
+ max: number,
89
+ width: number,
90
+ segments: SpeedometerSegment[],
91
+ colors: Pick<ThemeColors, "textDim" | "textBright">,
92
+ ): GaugeChunk[][] {
85
93
  const dialHeight = 7;
86
94
  const dial = Array.from({ length: dialHeight }, () => blankGaugeLine(width));
87
95
  const centerX = Math.floor((width - 1) / 2);
@@ -144,6 +152,7 @@ function renderGaugeBitmap(
144
152
  segments: SpeedometerSegment[],
145
153
  pixelWidth: number,
146
154
  pixelHeight: number,
155
+ colors: Pick<ThemeColors, "textDim" | "textBright">,
147
156
  ): NativeChartBitmap {
148
157
  const pixels = new Uint8Array(pixelWidth * pixelHeight * 4);
149
158
  const centerX = pixelWidth / 2;
@@ -193,6 +202,7 @@ export function TerminalSpeedometerGauge({
193
202
  minWidth,
194
203
  maxWidth,
195
204
  }: Required<SpeedometerGaugeProps>) {
205
+ const colors = useThemeColors();
196
206
  const {
197
207
  nativeCharts,
198
208
  cellWidthPx = 8,
@@ -202,8 +212,14 @@ export function TerminalSpeedometerGauge({
202
212
  const nativeRenderer = useNativeRenderer();
203
213
  const gaugeWidth = Math.max(minWidth, Math.min(maxWidth, Math.floor(width - 4)));
204
214
  const labelRow = useMemo(() => renderLabelRow(gaugeWidth, min, max, segments), [gaugeWidth, max, min, segments]);
205
- const tickRow = useMemo(() => renderTickRow(gaugeWidth, min, max), [gaugeWidth, max, min]);
206
- const arcRows = useMemo(() => renderArcRows(value, min, max, gaugeWidth, segments), [gaugeWidth, max, min, segments, value]);
215
+ const tickRow = useMemo(
216
+ () => renderTickRow(gaugeWidth, min, max, colors),
217
+ [colors.textDim, gaugeWidth, max, min],
218
+ );
219
+ const arcRows = useMemo(
220
+ () => renderArcRows(value, min, max, gaugeWidth, segments, colors),
221
+ [colors.textBright, colors.textDim, gaugeWidth, max, min, segments, value],
222
+ );
207
223
  const rendererCapabilities = nativeRenderer.capabilities;
208
224
  const rendererResolution = nativeRenderer.resolution;
209
225
  const rendererTerminalWidth = nativeRenderer.terminalWidth;
@@ -227,11 +243,14 @@ export function TerminalSpeedometerGauge({
227
243
  segments,
228
244
  bitmapSize.pixelWidth,
229
245
  bitmapSize.pixelHeight,
246
+ colors,
230
247
  );
231
248
  }, [
232
249
  arcRows.length,
233
250
  cellHeightPx,
234
251
  cellWidthPx,
252
+ colors.textBright,
253
+ colors.textDim,
235
254
  gaugeWidth,
236
255
  max,
237
256
  min,
@@ -1,6 +1,6 @@
1
1
  import { Box, Text, useTickerContextMenu, useUiCapabilities } from "../../../ui";
2
2
  import { TextAttributes } from "../../../ui";
3
- import { colors, priceColor } from "../../../theme/colors";
3
+ import { blendHex, colors, priceColor } from "../../../theme/colors";
4
4
  import { getSharedRegistry } from "../../../plugins/registry";
5
5
  import type { Quote } from "../../../types/financials";
6
6
  import { getTickerBadgeText } from "./format";
@@ -16,23 +16,6 @@ export interface TickerBadgeProps {
16
16
  onOpen: (symbol: string) => void;
17
17
  }
18
18
 
19
- function blendHex(base: string, accent: string, ratio: number): string {
20
- const parse = (hex: string) => {
21
- const value = hex.replace("#", "");
22
- return [
23
- parseInt(value.slice(0, 2), 16),
24
- parseInt(value.slice(2, 4), 16),
25
- parseInt(value.slice(4, 6), 16),
26
- ] as const;
27
- };
28
- const [br, bg, bb] = parse(base);
29
- const [ar, ag, ab] = parse(accent);
30
- const mix = (left: number, right: number) => Math.round(left + (right - left) * ratio)
31
- .toString(16)
32
- .padStart(2, "0");
33
- return `#${mix(br, ar)}${mix(bg, ag)}${mix(bb, ab)}`;
34
- }
35
-
36
19
  export function TickerBadge({
37
20
  symbol,
38
21
  status,
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, ScrollBox, Text, TextAttributes, useNativeRenderer } from "../../../../ui";
3
- import { colors, hoverBg } from "../../../../theme/colors";
3
+ import { hoverBg } from "../../../../theme/colors";
4
4
  import { useThemeColors } from "../../../../theme/theme-context";
5
5
  import { useAppDispatch, usePaneInstance } from "../../../../state/app/context";
6
6
  import { useViewport } from "../../../../react/input";
@@ -67,7 +67,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
67
67
  scrollToIndexAlign = "nearest",
68
68
  scrollToIndexVersion = 0,
69
69
  }: DataTableProps<T, C>) {
70
- useThemeColors();
70
+ const colors = useThemeColors();
71
71
  const dispatch = useAppDispatch();
72
72
  const paneInstanceId = usePaneInstance()?.instanceId ?? null;
73
73
  const appViewport = useViewport();
@@ -376,7 +376,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
376
376
  const rowBg = selected
377
377
  ? colors.selected
378
378
  : rowBackgroundColor ?? colors.bg;
379
- const rowHoverBg = selected ? undefined : hoverBg();
379
+ const rowHoverBg = selected ? undefined : hoverBg(colors);
380
380
 
381
381
  return (
382
382
  <Box
@@ -1,7 +1,6 @@
1
1
  import { Box, Text, useUiHost } from "../../ui";
2
2
  import { TextAttributes } from "../../ui";
3
3
  import { type ComponentType, type ReactNode } from "react";
4
- import { colors } from "../../theme/colors";
5
4
  import { t } from "../../i18n";
6
5
  import { useThemeColors } from "../../theme/theme-context";
7
6
 
@@ -15,7 +14,7 @@ export interface DialogFrameProps {
15
14
  export function DialogFrame({ title: rawTitle, children, footer: rawFooter, showTitleDivider = false }: DialogFrameProps) {
16
15
  const title = t(rawTitle);
17
16
  const footer = rawFooter === undefined ? undefined : t(rawFooter);
18
- useThemeColors();
17
+ const colors = useThemeColors();
19
18
  const HostDialogFrame = useUiHost().DialogFrame as ComponentType<DialogFrameProps> | undefined;
20
19
  if (HostDialogFrame) {
21
20
  return (
@@ -0,0 +1,126 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { act, useMemo, useState } from "react";
3
+ import { testRender } from "../../renderers/opentui/test-utils";
4
+ import { openTuiUiHost } from "../../renderers/opentui/ui-host";
5
+ import {
6
+ Box,
7
+ Text,
8
+ UiHostProvider,
9
+ type RendererHost,
10
+ type UiHost,
11
+ } from "../../ui";
12
+ import { PageStackView, type PageStackViewProps } from "./page-stack-view";
13
+
14
+ let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
15
+ let setUseHost: ((useHost: boolean) => void) | undefined;
16
+ let backCalls = 0;
17
+
18
+ const rendererHost: RendererHost = {
19
+ requestExit() {},
20
+ openExternal: async () => {},
21
+ copyText: async () => {},
22
+ readText: async () => "",
23
+ notify() {},
24
+ };
25
+
26
+ afterEach(async () => {
27
+ setUseHost = undefined;
28
+ backCalls = 0;
29
+ if (!testSetup) return;
30
+ await act(async () => {
31
+ testSetup!.renderer.destroy();
32
+ });
33
+ testSetup = undefined;
34
+ });
35
+
36
+ function HostPageStackView({ detailContent }: PageStackViewProps) {
37
+ return (
38
+ <Box flexDirection="column">
39
+ <Text>Host page stack</Text>
40
+ {detailContent}
41
+ </Box>
42
+ );
43
+ }
44
+
45
+ function HostSwitchHarness() {
46
+ const [useHost, updateUseHost] = useState(false);
47
+ setUseHost = updateUseHost;
48
+ const ui = useMemo<UiHost>(() => ({
49
+ ...openTuiUiHost,
50
+ PageStackView: useHost ? HostPageStackView : undefined,
51
+ }), [useHost]);
52
+
53
+ return (
54
+ <UiHostProvider ui={ui} renderer={rendererHost}>
55
+ <PageStackView
56
+ focused
57
+ detailOpen
58
+ onBack={() => {}}
59
+ rootContent={<Text>Root content</Text>}
60
+ detailContent={<Text>Detail content</Text>}
61
+ detailTitle="Detail title"
62
+ />
63
+ </UiHostProvider>
64
+ );
65
+ }
66
+
67
+ function MouseBackHarness() {
68
+ const [detailOpen, setDetailOpen] = useState(true);
69
+ return (
70
+ <PageStackView
71
+ focused
72
+ detailOpen={detailOpen}
73
+ onBack={() => {
74
+ backCalls += 1;
75
+ setDetailOpen(false);
76
+ }}
77
+ rootContent={<Text>Root content</Text>}
78
+ detailContent={<Text>Detail content</Text>}
79
+ detailTitle="Detail title"
80
+ />
81
+ );
82
+ }
83
+
84
+ describe("PageStackView", () => {
85
+ test("can switch between the fallback and host implementations without changing hook topology", async () => {
86
+ testSetup = await testRender(<HostSwitchHarness />, { width: 48, height: 8 });
87
+ await testSetup.renderOnce();
88
+ expect(testSetup.captureCharFrame()).toContain("← Back Detail title");
89
+
90
+ await act(async () => {
91
+ setUseHost?.(true);
92
+ await Promise.resolve();
93
+ });
94
+ await act(async () => {
95
+ await testSetup!.renderOnce();
96
+ });
97
+ expect(testSetup.captureCharFrame()).toContain("Host page stack");
98
+
99
+ await act(async () => {
100
+ setUseHost?.(false);
101
+ await Promise.resolve();
102
+ });
103
+ await act(async () => {
104
+ await testSetup!.renderOnce();
105
+ });
106
+ expect(testSetup.captureCharFrame()).toContain("← Back Detail title");
107
+ });
108
+
109
+ test("keeps the fallback back action mouse-accessible", async () => {
110
+ testSetup = await testRender(<MouseBackHarness />, { width: 48, height: 8 });
111
+ await testSetup.renderOnce();
112
+ expect(testSetup.captureCharFrame()).toContain("Detail content");
113
+
114
+ await act(async () => {
115
+ await testSetup!.mockMouse.click(1, 0);
116
+ await Promise.resolve();
117
+ });
118
+ await act(async () => {
119
+ await testSetup!.renderOnce();
120
+ });
121
+
122
+ expect(backCalls).toBe(1);
123
+ expect(testSetup.captureCharFrame()).toContain("Root content");
124
+ expect(testSetup.captureCharFrame()).not.toContain("Detail content");
125
+ });
126
+ });