gloomberb 0.5.0 → 0.6.0

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 (500) hide show
  1. package/README.md +91 -11
  2. package/package.json +12 -5
  3. package/src/app.tsx +414 -191
  4. package/src/architecture/import-boundaries.test.ts +118 -0
  5. package/src/assets/gloomberb-logo.png +0 -0
  6. package/src/assets/gloomberb-logo.svg +73 -0
  7. package/src/brokers/account-cache.ts +157 -0
  8. package/src/brokers/profile-form.test.ts +73 -0
  9. package/src/brokers/profile-form.ts +109 -0
  10. package/src/brokers/remote-broker-adapter.ts +91 -0
  11. package/src/brokers/sync-broker-instance.test.ts +124 -1
  12. package/src/brokers/sync-broker-instance.ts +113 -17
  13. package/src/capabilities/factories.ts +100 -0
  14. package/src/capabilities/index.ts +4 -0
  15. package/src/capabilities/registry.test.ts +137 -0
  16. package/src/capabilities/registry.ts +131 -0
  17. package/src/capabilities/service-capabilities.ts +27 -0
  18. package/src/capabilities/types.ts +84 -0
  19. package/src/cli/commands/ticker.ts +11 -3
  20. package/src/cli/context.ts +19 -13
  21. package/src/cli/helpers.ts +1 -0
  22. package/src/cli/types.ts +2 -2
  23. package/src/components/chart/chart-controller.ts +4 -24
  24. package/src/components/chart/chart-data.ts +3 -14
  25. package/src/components/chart/chart-indicator-selector.tsx +28 -6
  26. package/src/components/chart/chart-pane-settings.ts +15 -28
  27. package/src/components/chart/chart-renderer.ts +74 -99
  28. package/src/components/chart/chart-resolution.ts +10 -10
  29. package/src/components/chart/chart-scroll.test.ts +6 -5
  30. package/src/components/chart/chart-scroll.ts +2 -2
  31. package/src/components/chart/chart-types.ts +0 -1
  32. package/src/components/chart/chart-viewport.ts +2 -1
  33. package/src/components/chart/chart.test.ts +37 -0
  34. package/src/components/chart/comparison-chart-data.ts +3 -17
  35. package/src/components/chart/comparison-chart-renderer.test.ts +27 -4
  36. package/src/components/chart/comparison-chart-renderer.ts +1 -1
  37. package/src/components/chart/comparison-stock-chart.tsx +304 -108
  38. package/src/components/chart/cursor-motion.ts +3 -2
  39. package/src/components/chart/indicators/bands.test.ts +0 -14
  40. package/src/components/chart/indicators/moving-averages.test.ts +0 -15
  41. package/src/components/chart/indicators/options.ts +3 -3
  42. package/src/components/chart/indicators/oscillators.test.ts +0 -21
  43. package/src/components/chart/indicators/types.ts +1 -2
  44. package/src/components/chart/native/chart-rasterizer.ts +37 -44
  45. package/src/components/chart/native/kitty-adapter.ts +2 -5
  46. package/src/components/chart/native/kitty-manager.ts +1 -1
  47. package/src/components/chart/native/kitty-protocol.ts +0 -1
  48. package/src/components/chart/native/kitty-support.ts +1 -1
  49. package/src/components/chart/native/kitty.test.ts +30 -13
  50. package/src/components/chart/native/renderer-selection.ts +1 -1
  51. package/src/components/chart/native/surface-manager.ts +1 -1
  52. package/src/components/chart/static-chart-surface.test.tsx +51 -0
  53. package/src/components/chart/static-chart-surface.tsx +191 -0
  54. package/src/components/chart/stock-chart-renderer-switch.test.tsx +242 -35
  55. package/src/components/chart/stock-chart.tsx +438 -141
  56. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  57. package/src/components/command-bar/command-bar.test.tsx +269 -20
  58. package/src/components/command-bar/command-bar.tsx +830 -353
  59. package/src/components/command-bar/command-registry.ts +10 -20
  60. package/src/components/command-bar/helpers.test.ts +1 -1
  61. package/src/components/command-bar/helpers.ts +9 -11
  62. package/src/components/command-bar/pane-template-display.ts +4 -8
  63. package/src/components/command-bar/root-shortcuts.ts +12 -11
  64. package/src/components/command-bar/view-model.test.ts +8 -42
  65. package/src/components/command-bar/view-model.ts +8 -36
  66. package/src/components/command-bar/workflow-ops.test.ts +76 -2
  67. package/src/components/command-bar/workflow-ops.ts +40 -9
  68. package/src/components/command-bar/workflow-types.ts +2 -1
  69. package/src/components/data-table-stack-view.test.tsx +206 -0
  70. package/src/components/data-table-stack-view.tsx +152 -0
  71. package/src/components/data-table-view.test.tsx +188 -0
  72. package/src/components/data-table-view.tsx +279 -0
  73. package/src/components/feed-data-table-stack-view.test.tsx +225 -0
  74. package/src/components/feed-data-table-stack-view.tsx +365 -0
  75. package/src/components/index.ts +26 -31
  76. package/src/components/layout/detached-pane-shell.tsx +185 -0
  77. package/src/components/layout/floating-pane.tsx +87 -13
  78. package/src/components/layout/header.tsx +139 -49
  79. package/src/components/layout/pane-content.tsx +40 -0
  80. package/src/components/layout/pane-footer.test.tsx +195 -0
  81. package/src/components/layout/pane-footer.tsx +410 -0
  82. package/src/components/layout/pane-header.test.tsx +39 -0
  83. package/src/components/layout/pane-header.tsx +192 -14
  84. package/src/components/layout/pane-sizing.ts +13 -4
  85. package/src/components/layout/pane-title.ts +35 -0
  86. package/src/components/layout/pane.tsx +56 -8
  87. package/src/components/layout/shell.test.tsx +322 -19
  88. package/src/components/layout/shell.tsx +1010 -366
  89. package/src/components/layout/status-bar.test.tsx +17 -10
  90. package/src/components/layout/status-bar.tsx +367 -100
  91. package/src/components/layout/titlebar-overlay.ts +2 -0
  92. package/src/components/markdown-editor.tsx +29 -14
  93. package/src/components/markdown-text.tsx +25 -24
  94. package/src/components/onboarding/onboarding-wizard.test.tsx +8 -3
  95. package/src/components/onboarding/onboarding-wizard.tsx +347 -473
  96. package/src/components/pane-settings-dialog.tsx +521 -89
  97. package/src/components/pane-template-wizard.test.tsx +1 -1
  98. package/src/components/pane-template-wizard.tsx +38 -37
  99. package/src/components/price-selector-dialog.tsx +22 -21
  100. package/src/components/ticker-badge-list.tsx +53 -0
  101. package/src/components/ticker-badge-text.test.tsx +44 -1
  102. package/src/components/ticker-badge-text.tsx +78 -38
  103. package/src/components/ticker-badge.tsx +28 -7
  104. package/src/components/ticker-list-table-view.tsx +248 -0
  105. package/src/components/ticker-list-table.test.tsx +47 -1
  106. package/src/components/ticker-list-table.tsx +212 -41
  107. package/src/components/toggle-list.tsx +114 -8
  108. package/src/components/ui/button.tsx +24 -16
  109. package/src/components/ui/choice-dialog.tsx +89 -0
  110. package/src/components/ui/data-table.tsx +313 -114
  111. package/src/components/ui/external-link.tsx +52 -13
  112. package/src/components/ui/fields.tsx +44 -30
  113. package/src/components/ui/frame.tsx +24 -57
  114. package/src/components/ui/index.ts +15 -50
  115. package/src/components/ui/list-view.tsx +65 -25
  116. package/src/components/ui/loading.tsx +5 -60
  117. package/src/components/ui/message-composer.test.tsx +81 -0
  118. package/src/components/ui/message-composer.tsx +139 -0
  119. package/src/components/ui/multi-select-dialog.tsx +100 -32
  120. package/src/components/ui/multi-select.ts +112 -0
  121. package/src/components/ui/native-select.tsx +120 -0
  122. package/src/components/ui/page-stack-view.tsx +49 -19
  123. package/src/components/ui/remote-image.tsx +75 -0
  124. package/src/components/ui/row-value-cache.test.ts +17 -0
  125. package/src/components/ui/row-value-cache.ts +41 -0
  126. package/src/components/ui/shortcut-hint.tsx +55 -0
  127. package/src/components/ui/status.tsx +12 -67
  128. package/src/components/ui/table-layout.ts +59 -0
  129. package/src/components/ui/tabs.tsx +249 -27
  130. package/src/components/ui/toggle.tsx +19 -127
  131. package/src/components/ui/ui.test.tsx +266 -53
  132. package/src/components/use-double-click-activation.ts +7 -2
  133. package/src/components/use-external-link-footer.ts +79 -0
  134. package/src/core/app-services.ts +95 -0
  135. package/src/core/state/app-state.ts +886 -0
  136. package/src/{state → core/state}/session-persistence.ts +7 -6
  137. package/src/data/config-layout.ts +225 -0
  138. package/src/data/config-store-node.ts +390 -0
  139. package/src/data/config-store.test.ts +72 -0
  140. package/src/data/config-store.ts +35 -478
  141. package/src/data/sqlite-json.ts +2 -5
  142. package/src/data/sqlite-retry.ts +1 -0
  143. package/src/index.tsx +2 -68
  144. package/src/market-data/coordinator-subscriptions.test.ts +125 -0
  145. package/src/market-data/coordinator.test.ts +73 -4
  146. package/src/market-data/coordinator.ts +190 -24
  147. package/src/market-data/hooks.test.tsx +1 -1
  148. package/src/market-data/hooks.tsx +109 -87
  149. package/src/market-data/query-store.ts +3 -3
  150. package/src/market-data/request-types.test.ts +0 -14
  151. package/src/market-data/result-types.ts +1 -0
  152. package/src/news/aggregator.test.ts +267 -0
  153. package/src/news/aggregator.ts +452 -0
  154. package/src/news/hooks.ts +39 -0
  155. package/src/news/ticker-symbols.ts +21 -0
  156. package/src/news/types.ts +77 -0
  157. package/src/notifications/app-notifier.ts +21 -10
  158. package/src/pane-settings.test.ts +0 -2
  159. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +124 -134
  160. package/src/plugins/builtin/ai/gloomberb-cli.ts +14 -13
  161. package/src/plugins/builtin/ai/providers.ts +1 -9
  162. package/src/plugins/builtin/ai/runner.ts +41 -5
  163. package/src/plugins/builtin/ai/screener-pane.test.tsx +37 -55
  164. package/src/plugins/builtin/ai/screener-pane.tsx +231 -276
  165. package/src/plugins/builtin/ai/settings.ts +0 -1
  166. package/src/plugins/builtin/ai/utils.ts +1 -71
  167. package/src/plugins/builtin/alerts/alert-engine.test.ts +1 -38
  168. package/src/plugins/builtin/alerts/index.test.tsx +61 -82
  169. package/src/plugins/builtin/alerts/index.tsx +263 -155
  170. package/src/plugins/builtin/alerts/types.ts +7 -0
  171. package/src/plugins/builtin/analytics/index.test.tsx +13 -8
  172. package/src/plugins/builtin/analytics/index.tsx +78 -120
  173. package/src/plugins/builtin/analytics/metrics.test.ts +0 -4
  174. package/src/plugins/builtin/ask-ai.test.tsx +55 -54
  175. package/src/plugins/builtin/broker-manager/index.test.tsx +168 -0
  176. package/src/plugins/builtin/broker-manager/index.tsx +759 -0
  177. package/src/plugins/builtin/broker-manager/model.test.ts +107 -0
  178. package/src/plugins/builtin/broker-manager/model.ts +142 -0
  179. package/src/plugins/builtin/chat-controller.test.ts +150 -0
  180. package/src/plugins/builtin/chat-controller.ts +88 -16
  181. package/src/plugins/builtin/chat.test.tsx +283 -36
  182. package/src/plugins/builtin/chat.tsx +373 -172
  183. package/src/plugins/builtin/cloud-auth-actions.tsx +72 -0
  184. package/src/plugins/builtin/cloud-tweets.tsx +896 -0
  185. package/src/plugins/builtin/comparison-chart.test.tsx +46 -57
  186. package/src/plugins/builtin/comparison-chart.tsx +11 -11
  187. package/src/plugins/builtin/correlation/compute.test.ts +0 -17
  188. package/src/plugins/builtin/correlation/index.tsx +51 -46
  189. package/src/plugins/builtin/correlation/settings.ts +1 -2
  190. package/src/plugins/builtin/debug.tsx +99 -70
  191. package/src/plugins/builtin/earnings/earnings-cache.test.ts +2 -2
  192. package/src/plugins/builtin/earnings/earnings-cache.ts +12 -3
  193. package/src/plugins/builtin/earnings/earnings-data.test.ts +42 -11
  194. package/src/plugins/builtin/earnings/earnings-data.ts +70 -5
  195. package/src/plugins/builtin/earnings/index.tsx +187 -94
  196. package/src/plugins/builtin/econ/calendar-source.ts +9 -116
  197. package/src/plugins/builtin/econ/fred-client.test.ts +1 -14
  198. package/src/plugins/builtin/econ/fred-series-map.ts +1 -0
  199. package/src/plugins/builtin/econ/index.tsx +236 -421
  200. package/src/plugins/builtin/econ/types.ts +1 -0
  201. package/src/plugins/builtin/fx-matrix/index.tsx +43 -45
  202. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -0
  203. package/src/plugins/builtin/help.test.tsx +120 -60
  204. package/src/plugins/builtin/help.tsx +266 -82
  205. package/src/plugins/builtin/holders/index.test.ts +45 -0
  206. package/src/plugins/builtin/holders/index.tsx +949 -0
  207. package/src/plugins/builtin/insider/index.tsx +207 -253
  208. package/src/plugins/builtin/insider/insider-data.test.ts +1 -11
  209. package/src/plugins/builtin/insider/insider-data.ts +1 -0
  210. package/src/plugins/builtin/layout-manager.tsx +12 -40
  211. package/src/plugins/builtin/market-movers/index.tsx +70 -144
  212. package/src/plugins/builtin/market-movers/screener.test.ts +0 -18
  213. package/src/plugins/builtin/news-wire/breaking-notifications.test.ts +98 -0
  214. package/src/plugins/builtin/news-wire/breaking-notifications.ts +110 -0
  215. package/src/plugins/builtin/news-wire/breaking-pane.tsx +157 -0
  216. package/src/plugins/builtin/news-wire/categories.ts +114 -0
  217. package/src/plugins/builtin/news-wire/default-feeds.ts +3 -0
  218. package/src/plugins/builtin/news-wire/digest-store.ts +44 -0
  219. package/src/plugins/builtin/news-wire/feed-config.test.ts +90 -0
  220. package/src/plugins/builtin/news-wire/feed-config.ts +216 -0
  221. package/src/plugins/builtin/news-wire/feed-pane.tsx +21 -0
  222. package/src/plugins/builtin/news-wire/hash.ts +9 -0
  223. package/src/plugins/builtin/news-wire/index.tsx +92 -0
  224. package/src/plugins/builtin/news-wire/industry-pane.tsx +140 -0
  225. package/src/plugins/builtin/news-wire/news-detail-view.tsx +256 -0
  226. package/src/plugins/builtin/news-wire/news-footer.ts +28 -0
  227. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +94 -0
  228. package/src/plugins/builtin/news-wire/news-query-presets.ts +40 -0
  229. package/src/plugins/builtin/news-wire/news-table.test.tsx +206 -0
  230. package/src/plugins/builtin/news-wire/news-table.tsx +296 -0
  231. package/src/plugins/builtin/news-wire/persisted-articles.ts +92 -0
  232. package/src/plugins/builtin/news-wire/read-state.test.ts +30 -0
  233. package/src/plugins/builtin/news-wire/read-state.ts +76 -0
  234. package/src/plugins/builtin/news-wire/rss-parser.test.ts +184 -0
  235. package/src/plugins/builtin/news-wire/rss-parser.ts +213 -0
  236. package/src/plugins/builtin/news-wire/rss-source.test.ts +159 -0
  237. package/src/plugins/builtin/news-wire/rss-source.ts +181 -0
  238. package/src/plugins/builtin/news-wire/top-pane.tsx +21 -0
  239. package/src/plugins/builtin/news.tsx +98 -25
  240. package/src/plugins/builtin/notes-files.ts +51 -10
  241. package/src/plugins/builtin/notes.tsx +332 -131
  242. package/src/plugins/builtin/options.test.tsx +185 -0
  243. package/src/plugins/builtin/options.tsx +346 -206
  244. package/src/plugins/builtin/plugin-groups.ts +92 -0
  245. package/src/plugins/builtin/portfolio-list/header.tsx +72 -37
  246. package/src/plugins/builtin/portfolio-list/index.tsx +25 -42
  247. package/src/plugins/builtin/portfolio-list/metrics.ts +1 -1
  248. package/src/plugins/builtin/portfolio-list/pane.tsx +246 -164
  249. package/src/plugins/builtin/portfolio-list/settings.ts +19 -51
  250. package/src/plugins/builtin/portfolio-list/summary.tsx +21 -16
  251. package/src/plugins/builtin/portfolio-list/table.tsx +66 -25
  252. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +1 -1
  253. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +1 -1
  254. package/src/plugins/builtin/portfolio-list.test.ts +60 -1
  255. package/src/plugins/builtin/portfolio-list.test.tsx +122 -55
  256. package/src/plugins/builtin/quote-monitor.test.tsx +2 -27
  257. package/src/plugins/builtin/research/index.tsx +724 -0
  258. package/src/plugins/builtin/sec.tsx +67 -19
  259. package/src/plugins/builtin/sectors/index.tsx +42 -80
  260. package/src/plugins/builtin/sectors/sector-data.ts +1 -0
  261. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +25 -39
  262. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +108 -58
  263. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +383 -271
  264. package/src/plugins/builtin/ticker-detail/pane.tsx +135 -49
  265. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +43 -27
  266. package/src/plugins/builtin/ticker-detail/settings.ts +3 -6
  267. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +16 -10
  268. package/src/plugins/builtin/ticker-detail.test.tsx +156 -33
  269. package/src/plugins/builtin/ticker-surface.ts +68 -0
  270. package/src/plugins/builtin/world-indices/index.tsx +44 -82
  271. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  272. package/src/plugins/builtin/yahoo.ts +55 -1
  273. package/src/plugins/builtin/yield-curve/index.tsx +87 -145
  274. package/src/plugins/builtin/yield-curve/treasury-data.ts +6 -77
  275. package/src/plugins/catalog-ui.test.ts +24 -0
  276. package/src/plugins/catalog-ui.ts +44 -0
  277. package/src/plugins/catalog.ts +2 -50
  278. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  279. package/src/plugins/ibkr/account-cache.test.ts +15 -14
  280. package/src/plugins/ibkr/account-cache.ts +15 -120
  281. package/src/plugins/ibkr/broker-adapter.ts +67 -0
  282. package/src/plugins/ibkr/config.test.ts +0 -18
  283. package/src/plugins/ibkr/dialogs.tsx +14 -56
  284. package/src/plugins/ibkr/flex.test.ts +57 -2
  285. package/src/plugins/ibkr/flex.ts +116 -12
  286. package/src/plugins/ibkr/gateway-helpers.ts +8 -3
  287. package/src/plugins/ibkr/gateway-service-native.ts +2381 -0
  288. package/src/plugins/ibkr/gateway-service.test.ts +47 -9
  289. package/src/plugins/ibkr/gateway-service.ts +264 -2235
  290. package/src/plugins/ibkr/gateway-types.ts +27 -0
  291. package/src/plugins/ibkr/index.test.tsx +3 -11
  292. package/src/plugins/ibkr/index.tsx +1 -0
  293. package/src/plugins/ibkr/price-normalization.test.ts +1 -6
  294. package/src/plugins/ibkr/price-normalization.ts +1 -0
  295. package/src/plugins/ibkr/trade-badge.tsx +6 -5
  296. package/src/plugins/ibkr/trade-tab.test.tsx +2 -2
  297. package/src/plugins/ibkr/trade-tab.tsx +164 -117
  298. package/src/plugins/ibkr/trading-pane.tsx +117 -83
  299. package/src/plugins/pane-manager.test.ts +3 -1
  300. package/src/plugins/pane-manager.ts +93 -19
  301. package/src/plugins/plugin-runtime.test.tsx +176 -74
  302. package/src/plugins/plugin-runtime.tsx +228 -10
  303. package/src/plugins/prediction-markets/cache.ts +49 -0
  304. package/src/plugins/prediction-markets/categories.ts +0 -10
  305. package/src/plugins/prediction-markets/chart.test.tsx +1 -1
  306. package/src/plugins/prediction-markets/chart.tsx +21 -20
  307. package/src/plugins/prediction-markets/cli.ts +1 -0
  308. package/src/plugins/prediction-markets/controller-data.ts +135 -89
  309. package/src/plugins/prediction-markets/controller.ts +37 -108
  310. package/src/plugins/prediction-markets/detail/book.tsx +23 -22
  311. package/src/plugins/prediction-markets/detail/outcomes.tsx +33 -32
  312. package/src/plugins/prediction-markets/detail/overview.tsx +17 -12
  313. package/src/plugins/prediction-markets/detail/pane.tsx +59 -45
  314. package/src/plugins/prediction-markets/detail/rules.tsx +22 -7
  315. package/src/plugins/prediction-markets/detail/shared.tsx +3 -2
  316. package/src/plugins/prediction-markets/detail/trades.tsx +12 -11
  317. package/src/plugins/prediction-markets/detail.test.tsx +1 -1
  318. package/src/plugins/prediction-markets/keyboard.ts +1 -0
  319. package/src/plugins/prediction-markets/launch.test.ts +1 -1
  320. package/src/plugins/prediction-markets/launch.ts +1 -1
  321. package/src/plugins/prediction-markets/metrics.ts +25 -12
  322. package/src/plugins/prediction-markets/pane.test.tsx +16 -7
  323. package/src/plugins/prediction-markets/pane.tsx +180 -82
  324. package/src/plugins/prediction-markets/plugin.test.tsx +37 -45
  325. package/src/plugins/prediction-markets/services/fetch.ts +27 -10
  326. package/src/plugins/prediction-markets/services/kalshi-adapter.ts +20 -3
  327. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  328. package/src/plugins/prediction-markets/settings.ts +0 -5
  329. package/src/plugins/prediction-markets/test-helpers.tsx +19 -44
  330. package/src/plugins/registry.test.ts +307 -0
  331. package/src/plugins/registry.ts +293 -82
  332. package/src/react/input.ts +53 -0
  333. package/src/react/plugins/plugin-slot.tsx +21 -0
  334. package/src/react/use-raf-callback.ts +44 -0
  335. package/src/renderers/electrobun/bun/application-menu-click.test.ts +41 -0
  336. package/src/renderers/electrobun/bun/application-menu-click.ts +55 -0
  337. package/src/renderers/electrobun/bun/application-menu.ts +118 -0
  338. package/src/renderers/electrobun/bun/context-menu-click.test.ts +67 -0
  339. package/src/renderers/electrobun/bun/context-menu-click.ts +61 -0
  340. package/src/renderers/electrobun/bun/context-menu-normalize.test.ts +52 -0
  341. package/src/renderers/electrobun/bun/context-menu-normalize.ts +67 -0
  342. package/src/renderers/electrobun/bun/core-capabilities.ts +328 -0
  343. package/src/renderers/electrobun/bun/desktop-workspace.test.ts +63 -0
  344. package/src/renderers/electrobun/bun/desktop-workspace.ts +162 -0
  345. package/src/renderers/electrobun/bun/index.ts +1149 -0
  346. package/src/renderers/electrobun/bun/window-frame.test.ts +40 -0
  347. package/src/renderers/electrobun/bun/window-frame.ts +46 -0
  348. package/src/renderers/electrobun/shared/protocol.ts +72 -0
  349. package/src/renderers/electrobun/view/ai-host.ts +93 -0
  350. package/src/renderers/electrobun/view/app-services.ts +348 -0
  351. package/src/renderers/electrobun/view/application-menu-bridge.ts +12 -0
  352. package/src/renderers/electrobun/view/backend-rpc.ts +173 -0
  353. package/src/renderers/electrobun/view/broker-remote-client.ts +154 -0
  354. package/src/renderers/electrobun/view/config-host.ts +35 -0
  355. package/src/renderers/electrobun/view/context-menu.test.ts +140 -0
  356. package/src/renderers/electrobun/view/context-menu.ts +164 -0
  357. package/src/renderers/electrobun/view/data-table.tsx +773 -0
  358. package/src/renderers/electrobun/view/desktop-controls.tsx +651 -0
  359. package/src/renderers/electrobun/view/desktop-window-bridge.ts +59 -0
  360. package/src/renderers/electrobun/view/desktop-window-snapshot.test.ts +57 -0
  361. package/src/renderers/electrobun/view/desktop-window-snapshot.ts +41 -0
  362. package/src/renderers/electrobun/view/dialog-host.tsx +62 -0
  363. package/src/renderers/electrobun/view/http-fetch.ts +113 -0
  364. package/src/renderers/electrobun/view/input-host.tsx +65 -0
  365. package/src/renderers/electrobun/view/key-event.test.ts +34 -0
  366. package/src/renderers/electrobun/view/key-event.ts +134 -0
  367. package/src/renderers/electrobun/view/main.tsx +104 -0
  368. package/src/renderers/electrobun/view/native-renderer.ts +110 -0
  369. package/src/renderers/electrobun/view/native-stubs/chart-kitty-support.ts +9 -0
  370. package/src/renderers/electrobun/view/native-stubs/chart-surface-manager.ts +25 -0
  371. package/src/renderers/electrobun/view/native-stubs/chart-surface-sync.ts +1 -0
  372. package/src/renderers/electrobun/view/notes-files.ts +56 -0
  373. package/src/renderers/electrobun/view/resource-store.test.ts +44 -0
  374. package/src/renderers/electrobun/view/resource-store.ts +106 -0
  375. package/src/renderers/electrobun/view/rpc-codec.ts +42 -0
  376. package/src/renderers/electrobun/view/scrollbar-activity.ts +23 -0
  377. package/src/renderers/electrobun/view/toast-host.tsx +65 -0
  378. package/src/renderers/electrobun/view/ui-host.tsx +1490 -0
  379. package/src/renderers/electrobun/view/update-host.ts +38 -0
  380. package/src/renderers/opentui/dialog-host.test.tsx +44 -0
  381. package/src/renderers/opentui/dialog-host.tsx +56 -0
  382. package/src/renderers/opentui/host.tsx +134 -0
  383. package/src/renderers/opentui/input-host.tsx +24 -0
  384. package/src/renderers/opentui/start.tsx +71 -0
  385. package/src/renderers/opentui/test-utils.tsx +76 -0
  386. package/src/renderers/opentui/toast-host.tsx +10 -0
  387. package/src/renderers/opentui/ui-host.tsx +97 -0
  388. package/src/sources/gloomberb-cloud.test.ts +288 -17
  389. package/src/sources/gloomberb-cloud.ts +176 -9
  390. package/src/sources/provider-errors.ts +1 -0
  391. package/src/sources/provider-router.test.ts +220 -81
  392. package/src/sources/provider-router.ts +294 -111
  393. package/src/sources/sec-edgar.ts +19 -6
  394. package/src/sources/yahoo-finance.test.ts +0 -5
  395. package/src/sources/yahoo-finance.ts +420 -11
  396. package/src/state/app-activity.ts +7 -7
  397. package/src/state/app-bootstrap.ts +114 -40
  398. package/src/state/app-context-selector.test.tsx +70 -1
  399. package/src/state/app-context.test.ts +71 -3
  400. package/src/state/app-context.test.tsx +14 -7
  401. package/src/state/app-context.tsx +205 -869
  402. package/src/state/config-save-scheduler.ts +35 -0
  403. package/src/state/persist-scheduler.test.ts +67 -0
  404. package/src/state/persist-scheduler.ts +69 -0
  405. package/src/state/selectors-ui.ts +55 -0
  406. package/src/state/selectors.ts +15 -71
  407. package/src/state/session-persistence.test.ts +1 -1
  408. package/src/state/ticker-refresh-queue.ts +52 -3
  409. package/src/state/use-inline-tickers.ts +18 -16
  410. package/src/state/use-quote-streaming.test.ts +0 -6
  411. package/src/state/use-quote-streaming.test.tsx +1 -1
  412. package/src/state/use-quote-streaming.ts +3 -2
  413. package/src/state/use-session-snapshot.ts +32 -17
  414. package/src/test-support/plugin-runtime.ts +149 -0
  415. package/src/theme/color-utils.ts +1 -0
  416. package/src/theme/colors.ts +16 -0
  417. package/src/types/broker.ts +16 -1
  418. package/src/types/capability-route-source.ts +23 -0
  419. package/src/types/config.test.ts +23 -0
  420. package/src/types/config.ts +145 -10
  421. package/src/types/context-menu.ts +95 -0
  422. package/src/types/data-provider.ts +34 -3
  423. package/src/types/desktop-menu.ts +13 -0
  424. package/src/types/desktop-window.ts +29 -0
  425. package/src/types/financials.ts +123 -2
  426. package/src/types/instrument.ts +1 -0
  427. package/src/types/news-source.ts +9 -0
  428. package/src/types/persistence.ts +1 -0
  429. package/src/types/plugin.ts +32 -8
  430. package/src/ui/ascii-font.ts +85 -0
  431. package/src/ui/context-menu.test.ts +94 -0
  432. package/src/ui/context-menu.tsx +433 -0
  433. package/src/ui/dialog.tsx +55 -0
  434. package/src/ui/host.tsx +361 -0
  435. package/src/ui/index.tsx +126 -0
  436. package/src/ui/toast.tsx +40 -0
  437. package/src/updater.test.ts +69 -0
  438. package/src/updater.ts +61 -18
  439. package/src/utils/api-client.test.ts +186 -6
  440. package/src/utils/api-client.ts +433 -33
  441. package/src/utils/back-navigation.ts +6 -0
  442. package/src/utils/cli-output.ts +1 -0
  443. package/src/utils/currency-units.test.ts +1 -6
  444. package/src/utils/currency-units.ts +1 -0
  445. package/src/utils/debug-log.ts +68 -0
  446. package/src/utils/dom-screenshot.ts +326 -0
  447. package/src/utils/exchange-rate-map.ts +1 -0
  448. package/src/utils/exchanges.ts +214 -0
  449. package/src/utils/format.test.ts +7 -0
  450. package/src/utils/format.ts +1 -0
  451. package/src/utils/fuzzy-search.ts +1 -0
  452. package/src/utils/html-entities.test.ts +8 -0
  453. package/src/utils/html-entities.ts +32 -0
  454. package/src/utils/http-transport.ts +11 -0
  455. package/src/utils/main-thread-monitor.ts +127 -0
  456. package/src/utils/perf-marks.ts +54 -0
  457. package/src/utils/quote-freshness.ts +27 -50
  458. package/src/utils/quote-resolution.test.ts +5 -5
  459. package/src/utils/quote-resolution.ts +0 -1
  460. package/src/utils/quote-time.ts +1 -5
  461. package/src/utils/sec.test.ts +0 -12
  462. package/src/utils/selection-clipboard.ts +14 -6
  463. package/src/utils/shortcut-labels.test.ts +20 -0
  464. package/src/utils/shortcut-labels.ts +67 -0
  465. package/src/utils/slugify.ts +1 -0
  466. package/src/utils/terminal-input-reset.ts +1 -0
  467. package/src/utils/text-wrap.ts +82 -0
  468. package/src/utils/throttled-fetch.test.ts +19 -0
  469. package/src/utils/throttled-fetch.ts +11 -9
  470. package/src/utils/ticker-list.test.ts +1 -7
  471. package/src/utils/ticker-list.ts +1 -0
  472. package/src/utils/timestamp.ts +29 -0
  473. package/src/utils/tweet-text.test.ts +13 -0
  474. package/src/utils/tweet-text.ts +18 -0
  475. package/src/utils/url.ts +11 -0
  476. package/src/version.ts +1 -1
  477. package/src/components/chart/chart-control-hint.tsx +0 -39
  478. package/src/components/detail-feed-view.test.tsx +0 -180
  479. package/src/components/detail-feed-view.tsx +0 -531
  480. package/src/components/layout/pixel-mouse-mode.test.ts +0 -122
  481. package/src/components/layout/pixel-mouse.test.ts +0 -40
  482. package/src/components/spinner.tsx +0 -2
  483. package/src/components/tab-bar.tsx +0 -17
  484. package/src/components/ui/multi-select-chips.tsx +0 -69
  485. package/src/plugins/builtin/ai/index.test.tsx +0 -139
  486. package/src/plugins/builtin/ask-ai.tsx +0 -9
  487. package/src/plugins/builtin/correlation/plugin.test.ts +0 -44
  488. package/src/plugins/builtin/correlation/settings.test.ts +0 -34
  489. package/src/plugins/builtin/econ/calendar-source.test.ts +0 -219
  490. package/src/plugins/builtin/econ/fred-client.ts +0 -79
  491. package/src/plugins/builtin/fred-settings.ts +0 -10
  492. package/src/plugins/builtin/options-availability.test.tsx +0 -179
  493. package/src/plugins/builtin/options-availability.ts +0 -158
  494. package/src/plugins/builtin/portfolio-list/metrics.test.ts +0 -61
  495. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +0 -212
  496. package/src/plugins/prediction-markets/status.ts +0 -24
  497. package/src/plugins/prediction-markets/table.tsx +0 -87
  498. package/src/utils/quote-time.test.ts +0 -22
  499. package/src/utils/selection-clipboard.test.ts +0 -43
  500. package/src/utils/terminal-input-reset.test.ts +0 -50
@@ -1,9 +1,13 @@
1
+ import { Box, ChartSurface, Text } from "../../ui";
1
2
  import { memo, type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { TextAttributes, type BoxRenderable, type CliRenderer } from "@opentui/core";
3
- import { useKeyboard, useRenderer } from "@opentui/react";
3
+ import { TextAttributes, type BoxRenderable, type NativeRendererHost as CliRenderer } from "../../ui";
4
+ import { useNativeRenderer, useUiCapabilities } from "../../ui";
5
+ import { useShortcut } from "../../react/input";
4
6
  import { useAppDispatch, useAppSelector, usePaneInstance, usePaneInstanceId, usePaneSettingValue, usePaneTicker } from "../../state/app-context";
7
+ import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "../layout/pane-footer";
8
+ import { ShortcutHint } from "../ui";
5
9
  import { saveConfig } from "../../data/config-store";
6
- import { getSharedDataProvider } from "../../plugins/registry";
10
+ import { getSharedMarketData } from "../../plugins/registry";
7
11
  import { colors } from "../../theme/colors";
8
12
  import { formatCompact } from "../../utils/format";
9
13
  import { formatMarketPriceWithCurrency } from "../../utils/market-format";
@@ -79,7 +83,6 @@ import {
79
83
  import {
80
84
  buildChartScene,
81
85
  formatAxisCell,
82
- formatDateShort,
83
86
  formatCursorAxisValue,
84
87
  getActivePointIndex,
85
88
  getPointTerminalColumn,
@@ -105,7 +108,6 @@ import {
105
108
  type ChartViewState,
106
109
  type ResolvedChartRenderer,
107
110
  } from "./chart-types";
108
- import { ChartControlHint } from "./chart-control-hint";
109
111
  import {
110
112
  computeBitmapSize,
111
113
  intersectCellRects,
@@ -155,6 +157,7 @@ interface StockChartProps {
155
157
  height: number;
156
158
  focused: boolean;
157
159
  interactive?: boolean;
160
+ onActivate?: () => void;
158
161
  compact?: boolean;
159
162
  axisMode?: ChartAxisMode;
160
163
  historyOverride?: PricePoint[] | null;
@@ -162,6 +165,7 @@ interface StockChartProps {
162
165
  indicatorConfig?: IndicatorConfig;
163
166
  showVolume?: boolean;
164
167
  footerControls?: ReactNode;
168
+ footerHints?: PaneHint[];
165
169
  }
166
170
 
167
171
  interface ResolvedStockChartProps extends StockChartProps {
@@ -1204,7 +1208,7 @@ const INDICATOR_COLORS = [
1204
1208
  "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4", "#FFEAA7", "#DDA0DD", "#98D8C8",
1205
1209
  ];
1206
1210
 
1207
- export function getIndicatorWarmupPeriod(config: IndicatorConfig): number {
1211
+ function getIndicatorWarmupPeriod(config: IndicatorConfig): number {
1208
1212
  const periods = [
1209
1213
  ...(config.sma ?? []),
1210
1214
  ...(config.ema ?? []),
@@ -1354,7 +1358,7 @@ export function computeProjectedIndicatorOverlays(
1354
1358
  return reindexIndicatorOverlaysForProjection(overlays, sourcePoints, projectedPoints);
1355
1359
  }
1356
1360
 
1357
- export function reindexIndicatorOverlaysForProjection(
1361
+ function reindexIndicatorOverlaysForProjection(
1358
1362
  overlays: ChartIndicatorOverlays,
1359
1363
  sourcePoints: readonly PricePoint[],
1360
1364
  projectedPoints: readonly ProjectedChartPoint[],
@@ -1474,6 +1478,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1474
1478
  height,
1475
1479
  focused,
1476
1480
  interactive,
1481
+ onActivate,
1477
1482
  compact,
1478
1483
  axisMode = "price",
1479
1484
  historyOverride = null,
@@ -1481,11 +1486,13 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1481
1486
  indicatorConfig: indicatorConfigOverride,
1482
1487
  showVolume: showVolumeOverride,
1483
1488
  footerControls,
1489
+ footerHints,
1484
1490
  symbol,
1485
1491
  ticker,
1486
1492
  financials,
1487
1493
  }: ResolvedStockChartProps) {
1488
- const renderer = useRenderer();
1494
+ const renderer = useNativeRenderer();
1495
+ const { canvasCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
1489
1496
  const dispatch = useAppDispatch();
1490
1497
  const config = useAppSelector((state) => state.config);
1491
1498
  const paneId = usePaneInstanceId();
@@ -1523,7 +1530,12 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1523
1530
  const [lastReadyRenderView, setLastReadyRenderView] = useState<CachedRenderedView | null>(null);
1524
1531
  const showVolume = showVolumeOverride ?? !compact;
1525
1532
  const [kittySupport, setKittySupport] = useState<boolean | null>(() => getCachedKittySupport(renderer));
1533
+ const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
1534
+ const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
1535
+ const useCanvasChart = canvasCharts && effectiveRenderer !== "kitty";
1536
+ const showNativeUnavailable = rendererState.nativeUnavailable && !useCanvasChart;
1526
1537
  const [displayCursor, setDisplayCursor] = useState<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1538
+ const [canvasBaseBitmapState, setCanvasBaseBitmapState] = useState<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1527
1539
  const plotRef = useRef<BoxRenderable | null>(null);
1528
1540
  const nativeSurfaceScope = compact ? "compact" : "full";
1529
1541
  const nativeBaseSurfaceId = useMemo(
@@ -1538,6 +1550,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1538
1550
  const lastNativeGeometryRef = useRef<{ rect: CellRect; visibleRect: CellRect | null } | null>(null);
1539
1551
  const lastNativeBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1540
1552
  const lastNativeCrosshairBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1553
+ const lastCanvasBaseBitmapRef = useRef<{ key: string; bitmap: NativeChartBitmap } | null>(null);
1541
1554
  const displayCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1542
1555
  const targetCursorRef = useRef<DisplayCursorState>(EMPTY_DISPLAY_CURSOR);
1543
1556
  const cursorMotionKindRef = useRef<ChartCursorMotionKind>("discrete");
@@ -1633,7 +1646,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1633
1646
  () => instrumentFromTicker(ticker, ticker?.metadata.ticker ?? null),
1634
1647
  [ticker],
1635
1648
  );
1636
- const dataProvider = getSharedDataProvider();
1649
+ const dataProvider = getSharedMarketData();
1637
1650
  const capabilityKey = instrumentRef ? [
1638
1651
  instrumentRef.symbol,
1639
1652
  instrumentRef.exchange ?? "",
@@ -1748,11 +1761,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1748
1761
  (value) => Array.isArray(value) && value.length > 0,
1749
1762
  "No price history available.",
1750
1763
  );
1751
- const boundsBodyState = !compact && !boundsChartEntry && fallbackPriceHistory.length > 0
1764
+ const shouldUseBoundsFallback = !compact
1765
+ && fallbackPriceHistory.length > 0
1766
+ && (!boundsChartEntry || (rawBoundsBodyState.blocking && !rawBoundsBodyState.data?.length));
1767
+ const boundsBodyState = shouldUseBoundsFallback
1752
1768
  ? {
1753
1769
  data: fallbackPriceHistory,
1754
1770
  blocking: false,
1755
- updating: false,
1771
+ updating: !!boundsChartEntry && rawBoundsBodyState.blocking,
1756
1772
  emptyMessage: null,
1757
1773
  errorMessage: null,
1758
1774
  }
@@ -1765,11 +1781,14 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
1765
1781
  const axisGap = axisSectionWidthBudget > 0 ? 1 : 0;
1766
1782
  const minChartWidth = compact ? 12 : 20;
1767
1783
  const measurementChartWidth = Math.max(width - axisSectionWidthBudget - axisGap, minChartWidth);
1768
- const headerRows = compact ? 0 : 4;
1769
- const helpRow = compact ? 0 : 1;
1784
+ const headerRows = compact ? 0 : 2;
1785
+ const helpRow = compact ? 1 : 0;
1770
1786
  const timeAxisRow = 1;
1787
+ // Native/canvas surfaces can cover the last physical row after pixel rounding;
1788
+ // keep the text x-axis one row clear of the pane footer.
1789
+ const nativeSurfaceGuardRow = !compact && (useCanvasChart || effectiveRenderer === "kitty") ? 1 : 0;
1771
1790
  const volumeHeight = showVolume && !compact ? 3 : 0;
1772
- const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow, 4);
1791
+ const chartHeight = Math.max(height - headerRows - helpRow - timeAxisRow - nativeSurfaceGuardRow, 4);
1773
1792
  const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
1774
1793
  const chartAssetCategory = ticker?.metadata.assetCategory;
1775
1794
  const boundsHistory = compact
@@ -2803,7 +2822,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
2803
2822
  return buildIndicatorRenderKey(indicators);
2804
2823
  }, [indicators]);
2805
2824
 
2806
- useKeyboard((event) => {
2825
+ useShortcut((event) => {
2807
2826
  if (!focused || compact) return;
2808
2827
  const key = resolveChartKeyboardKey(event);
2809
2828
 
@@ -3125,9 +3144,6 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3125
3144
  indicators,
3126
3145
  }), [axisMode, chartColors, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3127
3146
 
3128
- const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
3129
- const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
3130
-
3131
3147
  const staticResult = useMemo(() => renderChart(projection.points, {
3132
3148
  width: chartWidth,
3133
3149
  height: chartHeight,
@@ -3145,7 +3161,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3145
3161
  }), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3146
3162
 
3147
3163
  const interactiveResult = useMemo(() => (
3148
- effectiveRenderer === "kitty"
3164
+ effectiveRenderer === "kitty" || useCanvasChart
3149
3165
  ? null
3150
3166
  : renderChart(projection.points, {
3151
3167
  width: chartWidth,
@@ -3162,20 +3178,20 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3162
3178
  timeAxisDates,
3163
3179
  indicators,
3164
3180
  })
3165
- ), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
3181
+ ), [axisMode, chartAssetCategory, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, indicators, projection.effectiveMode, projection.points, showVolume, timeAxisDates, useCanvasChart, volumeHeight]);
3166
3182
 
3167
- const result = effectiveRenderer === "kitty" ? staticResult : interactiveResult!;
3183
+ const result = effectiveRenderer === "kitty" || useCanvasChart ? staticResult : interactiveResult!;
3168
3184
 
3169
- const kittyCursorRow = effectiveRenderer === "kitty" && displayCursorY !== null && nativeBaseScene
3185
+ const rasterCursorRow = (effectiveRenderer === "kitty" || useCanvasChart) && displayCursorY !== null && nativeBaseScene
3170
3186
  ? Math.round(clamp(displayCursorY, 0, Math.max(nativeBaseScene.chartRows - 1, 0)))
3171
3187
  : null;
3172
- const kittyCrosshairPrice = effectiveRenderer === "kitty" && displayCursorY !== null && nativeBaseScene
3188
+ const rasterCrosshairPrice = (effectiveRenderer === "kitty" || useCanvasChart) && displayCursorY !== null && nativeBaseScene
3173
3189
  ? nativeBaseScene.max
3174
3190
  - (clamp(displayCursorY, 0, Math.max(nativeBaseScene.chartRows - 1, 0)) / Math.max(nativeBaseScene.chartRows - 1, 1))
3175
3191
  * (nativeBaseScene.max - nativeBaseScene.min)
3176
3192
  : null;
3177
- const cursorRow = effectiveRenderer === "kitty" ? kittyCursorRow : result.cursorRow;
3178
- const crosshairPrice = effectiveRenderer === "kitty" ? kittyCrosshairPrice : result.crosshairPrice;
3193
+ const cursorRow = effectiveRenderer === "kitty" || useCanvasChart ? rasterCursorRow : result.cursorRow;
3194
+ const crosshairPrice = effectiveRenderer === "kitty" || useCanvasChart ? rasterCrosshairPrice : result.crosshairPrice;
3179
3195
 
3180
3196
  const nativeCrosshair = useMemo<NativeCrosshairOverlay | null>(() => {
3181
3197
  if (!interactive || displayCursor.cellX === null || displayCursor.cellY === null) return null;
@@ -3417,11 +3433,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3417
3433
  const hasHistory = chartWindow.points.length > 0;
3418
3434
  const requestedMode = projection.requestedMode;
3419
3435
  const showOhlcSummary = projection.effectiveMode === "candles" || projection.effectiveMode === "ohlc";
3420
- const hasSelectionCursor = selectionCursorX !== null;
3421
3436
  const hasDisplayCursor = displayCursorX !== null && displayCursorY !== null;
3422
- const displayDate = hasSelectionCursor || showOhlcSummary
3423
- ? (selectionScene?.dateAtCursor ? formatDateShort(selectionScene.dateAtCursor) : null)
3424
- : null;
3425
3437
  const activePoint = showOhlcSummary ? (selectionScene?.activePoint ?? null) : null;
3426
3438
  const visiblePriceRange = selectionScene
3427
3439
  ? Math.max(selectionScene.max - selectionScene.min, 0)
@@ -3442,6 +3454,147 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3442
3454
  ? (renderBodyState.errorMessage ?? renderBodyState.emptyMessage)
3443
3455
  : null;
3444
3456
  const isUpdating = !compact && (renderBodyState.updating || boundsBodyState.updating);
3457
+ const timeAxisLabel = selectionScene?.timeLabels ?? staticResult.timeLabels;
3458
+ const timeAxisBox = (
3459
+ <Box
3460
+ height={1}
3461
+ width={chartWidth}
3462
+ overflow="hidden"
3463
+ data-gloom-role="chart-time-axis"
3464
+ >
3465
+ <Text fg={colors.textDim} style={{ whiteSpace: "pre" }}>{timeAxisLabel}</Text>
3466
+ </Box>
3467
+ );
3468
+ const chartFooterHints = useMemo<PaneHint[]>(() => {
3469
+ if (compact) return [];
3470
+
3471
+ const cycleMode = () => {
3472
+ setViewState((current) => {
3473
+ const activeMode = current.renderMode ?? "area";
3474
+ const index = CHART_RENDER_MODES.indexOf(activeMode);
3475
+ const nextMode = CHART_RENDER_MODES[(index + 1) % CHART_RENDER_MODES.length]!;
3476
+ persistDefaultRenderMode(nextMode);
3477
+ return { ...current, renderMode: nextMode };
3478
+ });
3479
+ };
3480
+ const cycleResolution = () => {
3481
+ const currentIndex = resolutionChips.indexOf(selectedResolution);
3482
+ const nextResolution = resolutionChips[(currentIndex + 1) % resolutionChips.length] ?? "auto";
3483
+ setResolution(nextResolution);
3484
+ };
3485
+ const zoomIn = () => {
3486
+ if (effectiveResolution === "auto") {
3487
+ requestAutoWindow(resolveAutoZoomWindow({
3488
+ historyPoints: history,
3489
+ boundsDates: boundsHistoryDates,
3490
+ currentWindow: navigableDateWindow,
3491
+ direction: "in",
3492
+ anchorRatio: RIGHT_EDGE_ANCHOR_RATIO,
3493
+ }));
3494
+ return;
3495
+ }
3496
+ setViewState((current) => applyZoomAroundAnchor(current, current.zoomLevel * 1.5, RIGHT_EDGE_ANCHOR_RATIO, boundsHistory));
3497
+ };
3498
+ const resetView = () => {
3499
+ if (effectiveResolution === "auto") {
3500
+ pendingAutoWindowRef.current = null;
3501
+ setPendingAutoWindowOverride(null);
3502
+ setRenderedAutoView(null);
3503
+ updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
3504
+ setViewState((current) => clearAutoViewportState(current));
3505
+ return;
3506
+ }
3507
+ pendingCanonicalResetRef.current += 1;
3508
+ updateDisplayCursorTarget(EMPTY_DISPLAY_CURSOR, "discrete");
3509
+ setViewState((current) => {
3510
+ const nextState = resolveViewportResolutionSelection(
3511
+ current,
3512
+ effectiveResolution,
3513
+ selectionSupportMap,
3514
+ visibleDateWindow,
3515
+ ) ?? current;
3516
+ return {
3517
+ ...nextState,
3518
+ panOffset: 0,
3519
+ zoomLevel: 1,
3520
+ cursorX: null,
3521
+ cursorY: null,
3522
+ };
3523
+ });
3524
+ };
3525
+ const cycleRange = () => {
3526
+ const currentIndex = activePreset ? TIME_RANGES.indexOf(activePreset) : -1;
3527
+ const nextRange = TIME_RANGES[(currentIndex + 1) % TIME_RANGES.length] ?? TIME_RANGES[0]!;
3528
+ setRange(nextRange);
3529
+ };
3530
+
3531
+ return [
3532
+ { id: "mode", key: "m", label: "ode", onPress: cycleMode },
3533
+ ...(footerHints ?? []),
3534
+ { id: "resolution", key: "r", label: "es", onPress: cycleResolution },
3535
+ { id: "zoom", key: "+/-", label: "zoom", onPress: zoomIn },
3536
+ { id: "reset", key: "0", label: "reset", onPress: resetView },
3537
+ ...(width >= 72 ? [{ id: "range", key: "1-7", label: "range", onPress: cycleRange }] : []),
3538
+ ];
3539
+ }, [
3540
+ activePreset,
3541
+ boundsHistory,
3542
+ boundsHistoryDates,
3543
+ compact,
3544
+ effectiveResolution,
3545
+ footerHints,
3546
+ history,
3547
+ navigableDateWindow,
3548
+ resolutionChips,
3549
+ selectedResolution,
3550
+ selectionSupportMap,
3551
+ visibleDateWindow,
3552
+ width,
3553
+ ]);
3554
+
3555
+ const chartFooterInfo = useMemo<PaneFooterSegment[]>(() => {
3556
+ if (compact || !showOhlcSummary || !activePoint) return [];
3557
+
3558
+ const formatPrice = (value: number) => formatMarketPriceWithCurrency(value, chartCurrency, {
3559
+ assetCategory: chartAssetCategory,
3560
+ minimumFractionDigits: 2,
3561
+ precisionOffset: 1,
3562
+ priceRange: visiblePriceRange,
3563
+ });
3564
+
3565
+ return [{
3566
+ id: "ohlc",
3567
+ parts: [
3568
+ { text: "O", tone: "label" },
3569
+ { text: formatPrice(activePoint.open), tone: "value" },
3570
+ { text: "H", tone: "label" },
3571
+ { text: formatPrice(activePoint.high), tone: "value" },
3572
+ { text: "L", tone: "label" },
3573
+ { text: formatPrice(activePoint.low), tone: "value" },
3574
+ { text: "C", tone: "label" },
3575
+ { text: formatPrice(activePoint.close), tone: "value" },
3576
+ { text: "V", tone: "label" },
3577
+ { text: formatCompact(activePoint.volume), tone: "value" },
3578
+ ],
3579
+ }];
3580
+ }, [
3581
+ activePoint,
3582
+ chartAssetCategory,
3583
+ chartCurrency,
3584
+ compact,
3585
+ showOhlcSummary,
3586
+ visiblePriceRange,
3587
+ ]);
3588
+
3589
+ usePaneFooter("stock-chart", () => (
3590
+ compact
3591
+ ? null
3592
+ : {
3593
+ order: 10,
3594
+ info: chartFooterInfo,
3595
+ hints: chartFooterHints,
3596
+ }
3597
+ ), [chartFooterHints, chartFooterInfo, compact]);
3445
3598
 
3446
3599
  useEffect(() => {
3447
3600
  queueMicrotask(() => renderer.requestRender());
@@ -3474,7 +3627,8 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3474
3627
  };
3475
3628
 
3476
3629
  const handlePlotDown = (event: ChartMouseEvent) => {
3477
- if (!interactive || compact) return;
3630
+ if (compact) return;
3631
+ if (!interactive) onActivate?.();
3478
3632
  focusPaneForMouseInteraction(event);
3479
3633
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3480
3634
  if (!localPointer) return;
@@ -3499,7 +3653,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3499
3653
  };
3500
3654
 
3501
3655
  const handlePlotDrag = (event: ChartMouseEvent) => {
3502
- if (!interactive || compact) return;
3656
+ if (compact || (!interactive && !dragRef.current)) return;
3503
3657
  const localPointer = getLocalPlotPointer(event, plotRef.current, renderer);
3504
3658
  if (localPointer) {
3505
3659
  const selectionCursor = resolveSelectionCursor(localPointer, projection.points.length, chartWidth, projection.effectiveMode);
@@ -3522,7 +3676,7 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3522
3676
  const deltaCells = getGlobalMouseX(event, renderer) - dragRef.current.startGlobalX;
3523
3677
  requestAutoWindow(shiftDateWindow(
3524
3678
  dragRef.current.startWindow,
3525
- -getDragPanWindowRatio(deltaCells, chartWidth),
3679
+ getDragPanWindowRatio(deltaCells, chartWidth),
3526
3680
  ));
3527
3681
  return;
3528
3682
  }
@@ -3601,122 +3755,260 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3601
3755
  }));
3602
3756
  };
3603
3757
 
3758
+ const canvasBitmapSize = useMemo(() => {
3759
+ if (!canvasCharts) return null;
3760
+ const resolutionScale = Math.max(1, pixelRatio);
3761
+ return {
3762
+ pixelWidth: Math.max(1, Math.round(chartWidth * cellWidthPx * resolutionScale)),
3763
+ pixelHeight: Math.max(1, Math.round(chartHeight * cellHeightPx * resolutionScale)),
3764
+ };
3765
+ }, [canvasCharts, cellHeightPx, cellWidthPx, chartHeight, chartWidth, pixelRatio]);
3766
+
3767
+ const canvasProjection = useMemo(() => {
3768
+ if (!canvasBitmapSize) return null;
3769
+ const canvasWidth = Math.max(chartWidth, canvasBitmapSize.pixelWidth);
3770
+ const canvasOhlcOptions = resolveStableOhlcProjectionOptions({
3771
+ pointCount: chartWindow.points.length,
3772
+ sourceIndexOffset: chartWindow.startIdx,
3773
+ bucketWidth: canvasWidth,
3774
+ navigationPointCount: navigationOhlcPointCount,
3775
+ });
3776
+ return projectChartData(
3777
+ chartWindow.points,
3778
+ canvasWidth,
3779
+ viewState.renderMode,
3780
+ !!compact,
3781
+ canvasOhlcOptions,
3782
+ );
3783
+ }, [
3784
+ canvasBitmapSize,
3785
+ chartWidth,
3786
+ chartWindow.points,
3787
+ chartWindow.startIdx,
3788
+ compact,
3789
+ navigationOhlcPointCount,
3790
+ viewState.renderMode,
3791
+ ]);
3792
+
3793
+ const canvasIndicators = useMemo(() => (
3794
+ sourceIndicatorOverlays && chartWindow.points.length && canvasProjection?.points.length
3795
+ ? reindexIndicatorOverlaysForProjection(
3796
+ sourceIndicatorOverlays,
3797
+ chartWindow.points,
3798
+ canvasProjection.points,
3799
+ chartWindow.startIdx,
3800
+ )
3801
+ : null
3802
+ ), [canvasProjection?.points, chartWindow.points, chartWindow.startIdx, sourceIndicatorOverlays]);
3803
+
3804
+ const canvasIndicatorRenderKey = useMemo(() => buildIndicatorRenderKey(canvasIndicators), [canvasIndicators]);
3805
+
3806
+ const canvasBaseScene = useMemo(() => (
3807
+ canvasProjection
3808
+ ? buildChartScene(canvasProjection.points, {
3809
+ width: canvasProjection.points.length,
3810
+ height: chartHeight,
3811
+ showVolume: showVolume && !compact,
3812
+ volumeHeight,
3813
+ cursorX: null,
3814
+ cursorY: null,
3815
+ mode: canvasProjection.effectiveMode,
3816
+ axisMode,
3817
+ colors: chartColors,
3818
+ timeAxisDates,
3819
+ indicators: canvasIndicators,
3820
+ })
3821
+ : null
3822
+ ), [axisMode, canvasIndicators, canvasProjection, chartColors, chartHeight, compact, showVolume, timeAxisDates, volumeHeight]);
3823
+
3824
+ const canvasBaseBitmapKey = useMemo(() => {
3825
+ if (!canvasBitmapSize || !canvasProjection || !hasHistory || isBlockingBody || bodyMessage) return null;
3826
+ return buildNativeBitmapKey(
3827
+ canvasProjection.points.length,
3828
+ canvasProjection.points,
3829
+ canvasBitmapSize.pixelWidth,
3830
+ canvasBitmapSize.pixelHeight,
3831
+ canvasProjection.effectiveMode,
3832
+ showVolume && !compact,
3833
+ [
3834
+ chartColors.lineColor,
3835
+ chartColors.fillColor,
3836
+ chartColors.gridColor,
3837
+ chartColors.volumeUp,
3838
+ chartColors.volumeDown,
3839
+ chartColors.candleUp,
3840
+ chartColors.candleDown,
3841
+ ].join(","),
3842
+ canvasIndicatorRenderKey,
3843
+ );
3844
+ }, [
3845
+ bodyMessage,
3846
+ canvasBitmapSize,
3847
+ canvasIndicatorRenderKey,
3848
+ canvasProjection,
3849
+ chartColors.candleDown,
3850
+ chartColors.candleUp,
3851
+ chartColors.fillColor,
3852
+ chartColors.gridColor,
3853
+ chartColors.lineColor,
3854
+ chartColors.volumeDown,
3855
+ chartColors.volumeUp,
3856
+ compact,
3857
+ hasHistory,
3858
+ isBlockingBody,
3859
+ showVolume,
3860
+ ]);
3861
+
3862
+ const canvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
3863
+ return canvasBaseBitmapKey && canvasBaseBitmapState?.key === canvasBaseBitmapKey
3864
+ ? canvasBaseBitmapState.bitmap
3865
+ : null;
3866
+ }, [canvasBaseBitmapKey, canvasBaseBitmapState]);
3867
+
3868
+ const visibleCanvasBaseBitmap = useMemo<NativeChartBitmap | null>(() => {
3869
+ if (canvasBaseBitmap) return canvasBaseBitmap;
3870
+ if (!canvasBaseBitmapKey || !canvasBitmapSize || !canvasBaseBitmapState) return null;
3871
+ return canvasBaseBitmapState.bitmap.width === canvasBitmapSize.pixelWidth
3872
+ && canvasBaseBitmapState.bitmap.height === canvasBitmapSize.pixelHeight
3873
+ ? canvasBaseBitmapState.bitmap
3874
+ : null;
3875
+ }, [canvasBaseBitmap, canvasBaseBitmapKey, canvasBaseBitmapState, canvasBitmapSize]);
3876
+
3877
+ useEffect(() => {
3878
+ if (!canvasBitmapSize || !canvasBaseBitmapKey || !canvasBaseScene) {
3879
+ lastCanvasBaseBitmapRef.current = null;
3880
+ setCanvasBaseBitmapState((current) => (current === null ? current : null));
3881
+ return;
3882
+ }
3883
+
3884
+ const cachedBitmap = lastCanvasBaseBitmapRef.current?.key === canvasBaseBitmapKey
3885
+ ? lastCanvasBaseBitmapRef.current.bitmap
3886
+ : null;
3887
+ if (cachedBitmap) {
3888
+ setCanvasBaseBitmapState((current) => (
3889
+ current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap: cachedBitmap }
3890
+ ));
3891
+ return;
3892
+ }
3893
+
3894
+ let cancelled = false;
3895
+ const frame = requestAnimationFrameSafe(() => {
3896
+ const bitmap = renderNativeChartBase(canvasBaseScene, canvasBitmapSize.pixelWidth, canvasBitmapSize.pixelHeight);
3897
+ if (cancelled) return;
3898
+ lastCanvasBaseBitmapRef.current = { key: canvasBaseBitmapKey, bitmap };
3899
+ setCanvasBaseBitmapState((current) => (
3900
+ current?.key === canvasBaseBitmapKey ? current : { key: canvasBaseBitmapKey, bitmap }
3901
+ ));
3902
+ });
3903
+
3904
+ return () => {
3905
+ cancelled = true;
3906
+ cancelAnimationFrameSafe(frame);
3907
+ };
3908
+ }, [canvasBaseBitmapKey, canvasBaseScene, canvasBitmapSize]);
3909
+
3910
+ const canvasCrosshair = useMemo(() => {
3911
+ if (!canvasBitmapSize || !visibleCanvasBaseBitmap || !nativeCrosshair) return null;
3912
+ const renderablePixelSize = getRenderablePixelSize(plotRef.current, renderer);
3913
+ const overlayPixelX = scaleLocalPixelCoordinate(
3914
+ nativeCrosshair.pixelX,
3915
+ renderablePixelSize?.pixelWidth ?? canvasBitmapSize.pixelWidth,
3916
+ canvasBitmapSize.pixelWidth,
3917
+ );
3918
+ const overlayPixelY = scaleLocalPixelCoordinate(
3919
+ nativeCrosshair.pixelY,
3920
+ renderablePixelSize?.pixelHeight ?? canvasBitmapSize.pixelHeight,
3921
+ canvasBitmapSize.pixelHeight,
3922
+ );
3923
+ if (overlayPixelX === null || overlayPixelY === null) return null;
3924
+ return {
3925
+ pixelX: overlayPixelX,
3926
+ pixelY: overlayPixelY,
3927
+ color: nativeCrosshair.colors.crosshairColor,
3928
+ };
3929
+ }, [canvasBitmapSize, nativeCrosshair, renderer, visibleCanvasBaseBitmap]);
3930
+
3931
+ const plotBitmaps = useMemo(() => {
3932
+ if (!visibleCanvasBaseBitmap) return null;
3933
+ return [visibleCanvasBaseBitmap];
3934
+ }, [visibleCanvasBaseBitmap]);
3935
+
3604
3936
  const plotLines: Array<string | StyledContent> = effectiveRenderer === "kitty"
3605
3937
  ? blankPlotLines
3606
3938
  : result.lines;
3607
- const plotContent = plotLines.map((line, index) => (
3608
- <text key={index} content={line as unknown as string} />
3609
- ));
3939
+ const plotContent = plotBitmaps || (useCanvasChart && canvasBaseBitmapKey)
3940
+ ? null
3941
+ : plotLines.map((line, index) => (
3942
+ <Text key={index} content={line as unknown as string} />
3943
+ ));
3610
3944
 
3611
3945
  const plotBox = (
3612
- <box
3946
+ <ChartSurface
3613
3947
  ref={plotRef}
3614
3948
  width={chartWidth}
3615
3949
  height={chartHeight}
3616
3950
  flexDirection="column"
3617
- backgroundColor={chartColors.bgColor}
3951
+ bitmaps={plotBitmaps}
3952
+ crosshair={canvasCrosshair}
3618
3953
  onMouseMove={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotMove}
3619
3954
  onMouseDown={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDown}
3620
3955
  onMouseUp={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
3621
3956
  onMouseDrag={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotDrag}
3622
3957
  onMouseDragEnd={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => { dragRef.current = null; }}
3623
- onMouseOut={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : () => {
3624
- dragRef.current = null;
3625
- }}
3958
+ onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
3626
3959
  >
3627
3960
  {isBlockingBody
3628
3961
  ? (
3629
- <box flexGrow={1} alignItems="center" justifyContent="center">
3630
- <text fg={colors.textDim}>Loading chart...</text>
3631
- </box>
3962
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
3963
+ <Text fg={colors.textDim}>Loading chart...</Text>
3964
+ </Box>
3632
3965
  )
3633
3966
  : bodyMessage
3634
3967
  ? (
3635
- <box flexGrow={1} alignItems="center" justifyContent="center">
3636
- <text fg={colors.textDim}>{bodyMessage}</text>
3637
- </box>
3968
+ <Box flexGrow={1} alignItems="center" justifyContent="center">
3969
+ <Text fg={colors.textDim}>{bodyMessage}</Text>
3970
+ </Box>
3638
3971
  )
3639
3972
  : plotContent}
3640
- </box>
3973
+ </ChartSurface>
3641
3974
  );
3642
3975
 
3643
3976
  const axisBox = (
3644
- <box width={axisSectionWidth} height={chartHeight} flexDirection="column">
3977
+ <Box width={axisSectionWidth} height={chartHeight} flexDirection="column">
3645
3978
  {Array.from({ length: chartHeight }, (_, row) => {
3646
3979
  const isCursorRow = cursorAxisLabel !== null && cursorRow === row;
3647
3980
  const label = isCursorRow ? cursorAxisLabel : (axisLabels.get(row) ?? null);
3648
3981
  return (
3649
- <text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
3982
+ <Text key={row} fg={isCursorRow ? chartColors.crosshairColor : colors.textDim}>
3650
3983
  {formatAxisCell(label, axisWidth).padEnd(axisSectionWidth)}
3651
- </text>
3984
+ </Text>
3652
3985
  );
3653
3986
  })}
3654
- </box>
3987
+ </Box>
3655
3988
  );
3656
3989
 
3657
3990
  if (compact) {
3658
3991
  return (
3659
- <box flexDirection="column">
3660
- <box flexDirection="row" height={chartHeight} gap={axisGap}>
3992
+ <Box flexDirection="column">
3993
+ <Box flexDirection="row" height={chartHeight} gap={axisGap}>
3661
3994
  {plotBox}
3662
3995
  {axisBox}
3663
- </box>
3664
- <box height={1}>
3665
- <text fg={colors.textDim}>{selectionScene?.timeLabels ?? staticResult.timeLabels}</text>
3666
- </box>
3667
- </box>
3996
+ </Box>
3997
+ {timeAxisBox}
3998
+ </Box>
3668
3999
  );
3669
4000
  }
3670
4001
 
3671
4002
  return (
3672
- <box
4003
+ <Box
3673
4004
  flexDirection="column"
3674
4005
  flexGrow={1}
3675
4006
  onMouseScroll={compact || !hasHistory || isBlockingBody || !!bodyMessage ? undefined : handlePlotScroll}
3676
4007
  >
3677
- <box flexDirection="row" gap={2} height={1}>
3678
- <text attributes={TextAttributes.BOLD} fg={colors.textBright}>
3679
- {ticker?.metadata.ticker ?? ""} - {getChartResolutionLabel(selectedResolution)}
3680
- </text>
3681
- {fallbackResolutionLabel && (
3682
- <text fg={colors.textDim}>{fallbackResolutionLabel}</text>
3683
- )}
3684
- {displayDate && <text fg={colors.textDim}>{displayDate}</text>}
3685
- {showOhlcSummary && activePoint && (
3686
- <>
3687
- <text fg={colors.textDim}>O {formatMarketPriceWithCurrency(activePoint.open, chartCurrency, {
3688
- assetCategory: chartAssetCategory,
3689
- minimumFractionDigits: 2,
3690
- precisionOffset: 1,
3691
- priceRange: visiblePriceRange,
3692
- })}</text>
3693
- <text fg={colors.textDim}>H {formatMarketPriceWithCurrency(activePoint.high, chartCurrency, {
3694
- assetCategory: chartAssetCategory,
3695
- minimumFractionDigits: 2,
3696
- precisionOffset: 1,
3697
- priceRange: visiblePriceRange,
3698
- })}</text>
3699
- <text fg={colors.textDim}>L {formatMarketPriceWithCurrency(activePoint.low, chartCurrency, {
3700
- assetCategory: chartAssetCategory,
3701
- minimumFractionDigits: 2,
3702
- precisionOffset: 1,
3703
- priceRange: visiblePriceRange,
3704
- })}</text>
3705
- <text fg={colors.textDim}>C {formatMarketPriceWithCurrency(activePoint.close, chartCurrency, {
3706
- assetCategory: chartAssetCategory,
3707
- minimumFractionDigits: 2,
3708
- precisionOffset: 1,
3709
- priceRange: visiblePriceRange,
3710
- })}</text>
3711
- <text fg={colors.textDim}>V {formatCompact(activePoint.volume)}</text>
3712
- </>
3713
- )}
3714
- </box>
3715
-
3716
- <box flexDirection="row" height={1}>
3717
- <box flexDirection="row" gap={1}>
4008
+ <Box flexDirection="row" height={1}>
4009
+ <Box flexDirection="row" gap={1}>
3718
4010
  {TIME_RANGES.map((range, index) => (
3719
- <text
4011
+ <Text
3720
4012
  key={range}
3721
4013
  fg={activePreset === range ? chartColors.activeRangeColor : (isRangePresetSupported(range, availableManualResolutions) ? chartColors.inactiveRangeColor : colors.textMuted)}
3722
4014
  attributes={activePreset === range ? TextAttributes.BOLD : 0}
@@ -3726,15 +4018,15 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3726
4018
  }}
3727
4019
  >
3728
4020
  {`${index + 1}:${range}`}
3729
- </text>
4021
+ </Text>
3730
4022
  ))}
3731
- </box>
3732
- </box>
4023
+ </Box>
4024
+ </Box>
3733
4025
 
3734
- <box flexDirection="row" height={1}>
3735
- <box flexDirection="row" gap={1}>
4026
+ <Box flexDirection="row" height={1}>
4027
+ <Box flexDirection="row" gap={1}>
3736
4028
  {resolutionChips.map((resolution) => (
3737
- <text
4029
+ <Text
3738
4030
  key={resolution}
3739
4031
  fg={selectedResolution === resolution ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
3740
4032
  attributes={selectedResolution === resolution ? TextAttributes.BOLD : 0}
@@ -3744,17 +4036,20 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3744
4036
  }}
3745
4037
  >
3746
4038
  {getChartResolutionLabel(resolution)}
3747
- </text>
4039
+ </Text>
3748
4040
  ))}
3749
4041
  {isUpdating && (
3750
- <text fg={colors.textDim}>updating</text>
4042
+ <Text fg={colors.textDim}>updating</Text>
4043
+ )}
4044
+ {fallbackResolutionLabel && (
4045
+ <Text fg={colors.textDim}>{fallbackResolutionLabel}</Text>
3751
4046
  )}
3752
- </box>
3753
- <box flexGrow={1} />
4047
+ </Box>
4048
+ <Box flexGrow={1} />
3754
4049
  {chartWidth >= 72 ? (
3755
- <box flexDirection="row" gap={1}>
4050
+ <Box flexDirection="row" gap={1}>
3756
4051
  {CHART_RENDER_MODES.map((mode) => (
3757
- <text
4052
+ <Text
3758
4053
  key={mode}
3759
4054
  fg={requestedMode === mode ? chartColors.activeRangeColor : chartColors.inactiveRangeColor}
3760
4055
  attributes={requestedMode === mode ? TextAttributes.BOLD : 0}
@@ -3764,29 +4059,29 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3764
4059
  }}
3765
4060
  >
3766
4061
  {MODE_CHIPS[mode]}
3767
- </text>
4062
+ </Text>
3768
4063
  ))}
3769
4064
  {projection.fallbackMode && (
3770
- <text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</text>
4065
+ <Text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</Text>
3771
4066
  )}
3772
- {rendererState.nativeUnavailable && (
3773
- <text fg={colors.textDim}>native unavailable</text>
4067
+ {showNativeUnavailable && (
4068
+ <Text fg={colors.textDim}>native unavailable</Text>
3774
4069
  )}
3775
- </box>
4070
+ </Box>
3776
4071
  ) : (
3777
- <box flexDirection="row" gap={1}>
3778
- <text fg={colors.textDim}>mode:{MODE_LABELS[requestedMode]}</text>
4072
+ <Box flexDirection="row" gap={1}>
4073
+ <Text fg={colors.textDim}>mode:{MODE_LABELS[requestedMode]}</Text>
3779
4074
  {projection.fallbackMode && (
3780
- <text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</text>
4075
+ <Text fg={colors.textDim}>auto:{MODE_LABELS[projection.fallbackMode]}</Text>
3781
4076
  )}
3782
- {rendererState.nativeUnavailable && (
3783
- <text fg={colors.textDim}>native unavailable</text>
4077
+ {showNativeUnavailable && (
4078
+ <Text fg={colors.textDim}>native unavailable</Text>
3784
4079
  )}
3785
- </box>
4080
+ </Box>
3786
4081
  )}
3787
- </box>
4082
+ </Box>
3788
4083
 
3789
- <box
4084
+ <Box
3790
4085
  flexDirection="row"
3791
4086
  height={chartHeight}
3792
4087
  gap={axisGap}
@@ -3794,20 +4089,22 @@ export const ResolvedStockChart = memo(function ResolvedStockChart({
3794
4089
  >
3795
4090
  {plotBox}
3796
4091
  {axisBox}
3797
- </box>
3798
-
3799
- <box height={1}>
3800
- <text fg={colors.textDim}>{selectionScene?.timeLabels ?? staticResult.timeLabels}</text>
3801
- </box>
3802
-
3803
- <box height={1} flexDirection="row" gap={1}>
3804
- <ChartControlHint hotkey="m" label="ode" />
3805
- {footerControls}
3806
- <ChartControlHint hotkey="r" label="es" />
3807
- <ChartControlHint hotkey="+/-" label="zoom" />
3808
- <ChartControlHint hotkey="0" label="reset" />
3809
- {width >= 72 && <ChartControlHint hotkey="1-7" label="range" />}
3810
- </box>
3811
- </box>
4092
+ </Box>
4093
+
4094
+ {timeAxisBox}
4095
+
4096
+ {compact && (
4097
+ <>
4098
+ <Box height={1} flexDirection="row" gap={1}>
4099
+ <ShortcutHint hotkey="m" label="ode" />
4100
+ {footerControls}
4101
+ <ShortcutHint hotkey="r" label="es" />
4102
+ <ShortcutHint hotkey="+/-" label="zoom" />
4103
+ <ShortcutHint hotkey="0" label="reset" />
4104
+ {width >= 72 && <ShortcutHint hotkey="1-7" label="range" />}
4105
+ </Box>
4106
+ </>
4107
+ )}
4108
+ </Box>
3812
4109
  );
3813
4110
  });