gloomberb 0.11.2 → 0.12.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 (531) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +4 -2
  3. package/package.json +28 -1
  4. package/src/api-client/auth.ts +10 -0
  5. package/src/api-client/data.ts +108 -1
  6. package/src/api-client/index.test.ts +127 -0
  7. package/src/api-client/index.ts +134 -1
  8. package/src/api-client/normalizers.ts +39 -0
  9. package/src/api-client/paths.ts +85 -1
  10. package/src/api-client/types.ts +220 -0
  11. package/src/app/pane-runtime/index.tsx +15 -12
  12. package/src/app/pane-runtime/layout-placement.test.ts +27 -0
  13. package/src/app/pane-runtime/layout-placement.ts +13 -0
  14. package/src/app/pane-runtime/plugin-bindings.ts +62 -0
  15. package/src/app.tsx +60 -18
  16. package/src/architecture/browser-entry-graph.test.ts +96 -0
  17. package/src/brokers/profile-form.test.ts +25 -27
  18. package/src/brokers/test-broker.ts +68 -0
  19. package/src/capabilities/chart-series.ts +3 -2
  20. package/src/cli/commands/market.ts +2 -2
  21. package/src/cli/commands/plugins.ts +39 -11
  22. package/src/cli/desktop-pane-shot.ts +285 -16
  23. package/src/cli/index.ts +1 -1
  24. package/src/cli/pane-functions/capabilities.ts +61 -31
  25. package/src/cli/pane-functions/catalog.ts +16 -4
  26. package/src/cli/pane-functions/discovery.ts +1 -0
  27. package/src/cli/pane-functions/headless.test.ts +247 -0
  28. package/src/cli/pane-functions/headless.ts +423 -0
  29. package/src/cli/pane-functions/index.test.ts +9 -0
  30. package/src/cli/pane-functions/index.ts +6 -2
  31. package/src/cli/pane-functions/options.ts +27 -2
  32. package/src/cli/pane-functions/report.ts +4 -0
  33. package/src/cli/pane-functions/resolver.ts +26 -8
  34. package/src/cli/pane-functions/screenshot.test.ts +76 -0
  35. package/src/cli/pane-functions/screenshot.ts +236 -20
  36. package/src/cli/registry.ts +0 -22
  37. package/src/cli/restore-plugins.ts +34 -0
  38. package/src/cli/result.ts +7 -10
  39. package/src/components/chart/{static/chart → composite}/axis-overlays.test.tsx +3 -3
  40. package/src/components/chart/{static/chart → composite}/axis-overlays.tsx +6 -11
  41. package/src/components/chart/{static/chart → composite}/bitmap.ts +17 -3
  42. package/src/components/chart/composite/composite-chart.test.tsx +68 -20
  43. package/src/components/chart/composite/composite-chart.tsx +301 -204
  44. package/src/components/chart/composite/format.ts +8 -2
  45. package/src/components/chart/composite/interactions.test.ts +68 -102
  46. package/src/components/chart/composite/interactions.ts +214 -264
  47. package/src/components/chart/composite/price-axis-labels.test.ts +21 -0
  48. package/src/components/chart/{price-axis-labels.tsx → composite/price-axis-labels.tsx} +9 -4
  49. package/src/components/chart/composite/scene.ts +57 -3
  50. package/src/components/chart/composite/text-renderer.ts +3 -2
  51. package/src/components/chart/composite/time-scale.ts +138 -57
  52. package/src/components/chart/composite/types.ts +25 -1
  53. package/src/components/chart/core/data.ts +9 -13
  54. package/src/components/chart/core/date-window.ts +7 -1
  55. package/src/components/chart/core/pointer.ts +3 -0
  56. package/src/components/chart/core/types.ts +0 -45
  57. package/src/components/chart/native/chart-rasterizer.ts +0 -1
  58. package/src/components/chart/native/kitty/index.test.ts +0 -188
  59. package/src/components/chart/native/raster/crosshair.ts +6 -5
  60. package/src/components/chart/native/raster/primitives.ts +0 -23
  61. package/src/components/chart/native/renderer-selection.ts +81 -31
  62. package/src/components/chart/static/{chart/surface.test.tsx → chart-surface.test.tsx} +28 -80
  63. package/src/components/chart/static/chart-surface.tsx +150 -0
  64. package/src/components/chart/static/index.ts +8 -0
  65. package/src/components/chart/static/multi-line-chart-surface.tsx +51 -166
  66. package/src/components/chart/static/scatter-chart-surface.test.ts +30 -0
  67. package/src/components/chart/static/scatter-chart-surface.tsx +135 -34
  68. package/src/components/chart/static/series.ts +49 -0
  69. package/src/components/command-bar/action-runtime.ts +1 -3
  70. package/src/components/command-bar/assist/model.test.ts +17 -2
  71. package/src/components/command-bar/assist/model.ts +40 -9
  72. package/src/components/command-bar/commands/direct/index.ts +1 -1
  73. package/src/components/command-bar/commands/direct/runtime.ts +1 -1
  74. package/src/components/command-bar/commands/registry.ts +0 -11
  75. package/src/components/command-bar/helpers.test.ts +2 -2
  76. package/src/components/command-bar/helpers.ts +2 -6
  77. package/src/components/command-bar/keyboard-shortcuts.ts +0 -11
  78. package/src/components/command-bar/list/badge.test.ts +54 -0
  79. package/src/components/command-bar/list/badge.ts +62 -0
  80. package/src/components/command-bar/list/model.test.ts +103 -0
  81. package/src/components/command-bar/list/model.ts +61 -4
  82. package/src/components/command-bar/list/view.test.tsx +113 -0
  83. package/src/components/command-bar/list/view.tsx +147 -131
  84. package/src/components/command-bar/pane-templates/workflow.ts +1 -1
  85. package/src/components/command-bar/panel/body.tsx +51 -65
  86. package/src/components/command-bar/panel/index.tsx +48 -13
  87. package/src/components/command-bar/panel/layout.test.ts +139 -0
  88. package/src/components/command-bar/panel/layout.ts +65 -28
  89. package/src/components/command-bar/panel/native-surface.ts +24 -0
  90. package/src/components/command-bar/panel/prompt-binding.ts +54 -0
  91. package/src/components/command-bar/panel/runtime.ts +6 -3
  92. package/src/components/command-bar/panel/state.ts +41 -7
  93. package/src/components/command-bar/panel/types.ts +7 -1
  94. package/src/components/command-bar/routes/root/provider-search.ts +34 -15
  95. package/src/components/command-bar/routes/root/results.test.ts +161 -0
  96. package/src/components/command-bar/routes/root/results.ts +30 -28
  97. package/src/components/command-bar/routes/root/runtime.ts +37 -20
  98. package/src/components/command-bar/routes/root/search-providers.test.tsx +224 -0
  99. package/src/components/command-bar/routes/root/search-providers.ts +205 -0
  100. package/src/components/command-bar/routes/root/selection.ts +1 -12
  101. package/src/components/command-bar/routes/root/shortcuts.test.ts +8 -8
  102. package/src/components/command-bar/routes/ticker-search/actions.ts +10 -9
  103. package/src/components/command-bar/routes/ticker-search/results.test.ts +54 -1
  104. package/src/components/command-bar/routes/ticker-search/results.ts +60 -19
  105. package/src/components/command-bar/routes/ticker-search/route.ts +1 -0
  106. package/src/components/command-bar/routing/actions.ts +1 -81
  107. package/src/components/command-bar/routing/launch-request.ts +1 -1
  108. package/src/components/command-bar/routing/list-state.ts +6 -21
  109. package/src/components/command-bar/selection-runtime.ts +1 -1
  110. package/src/components/command-bar/surface/assist.test.tsx +14 -10
  111. package/src/components/command-bar/surface/environment.ts +8 -1
  112. package/src/components/command-bar/surface/index.test.tsx +84 -68
  113. package/src/components/command-bar/surface/index.tsx +33 -36
  114. package/src/components/command-bar/surface/test-harness.tsx +15 -3
  115. package/src/components/command-bar/theme-picker.tsx +43 -15
  116. package/src/components/command-bar/view-model.test.ts +53 -8
  117. package/src/components/command-bar/view-model.ts +43 -14
  118. package/src/components/command-bar/workflow/tickers.ts +1 -0
  119. package/src/components/command-bar/workflow/types.ts +1 -1
  120. package/src/components/data-table/export.test.ts +21 -0
  121. package/src/components/data-table/export.ts +21 -0
  122. package/src/components/data-table/view.test.tsx +45 -6
  123. package/src/components/data-table/view.tsx +55 -9
  124. package/src/components/index.ts +10 -2
  125. package/src/components/input-search-bar.test.tsx +36 -2
  126. package/src/components/input-search-bar.tsx +18 -1
  127. package/src/components/layout/header.test.tsx +57 -10
  128. package/src/components/layout/header.tsx +292 -149
  129. package/src/components/layout/market-summary.test.ts +81 -0
  130. package/src/components/layout/market-summary.ts +109 -0
  131. package/src/components/layout/shell/chrome.ts +96 -1
  132. package/src/components/layout/shell/index.tsx +11 -3
  133. package/src/components/layout/shell/menu.ts +9 -0
  134. package/src/components/layout/shell/pane/actions.ts +29 -0
  135. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  136. package/src/components/layout/shell/shortcuts.ts +3 -0
  137. package/src/components/layout/status-bar.test.tsx +9 -8
  138. package/src/components/layout/status-bar.tsx +85 -54
  139. package/src/components/onboarding/onboarding-frame.tsx +2 -26
  140. package/src/components/price-sparkline/index.ts +61 -34
  141. package/src/components/price-sparkline/view.tsx +1 -1
  142. package/src/components/sign-in-gate.test.tsx +128 -0
  143. package/src/components/sign-in-gate.tsx +149 -0
  144. package/src/components/speedometer-gauge/desktop.tsx +2 -12
  145. package/src/components/ticker/badge/list.tsx +17 -3
  146. package/src/components/ticker/list-table-view.tsx +6 -3
  147. package/src/components/ui/data-table/index.tsx +22 -4
  148. package/src/components/ui/data-table/opentui/index.tsx +165 -120
  149. package/src/components/ui/frame.tsx +28 -0
  150. package/src/components/ui/index.ts +3 -0
  151. package/src/components/ui/native-select.tsx +20 -10
  152. package/src/components/ui/select-button.tsx +138 -0
  153. package/src/components/ui/status.tsx +2 -2
  154. package/src/components/ui/table-layout.ts +1 -1
  155. package/src/components/use-double-click-activation.ts +7 -3
  156. package/src/core/app-services.ts +3 -3
  157. package/src/data/config/chart-settings.ts +4 -4
  158. package/src/data/config/layout.ts +12 -6
  159. package/src/data/config/store/index.test.ts +97 -0
  160. package/src/data/config/store/migrations.ts +41 -0
  161. package/src/data/config/store/normalize.ts +5 -3
  162. package/src/i18n/es.ts +6 -2
  163. package/src/i18n/ja.ts +6 -2
  164. package/src/i18n/ko.ts +6 -2
  165. package/src/i18n/zh-cn.ts +6 -2
  166. package/src/i18n/zh-tw.ts +6 -2
  167. package/src/layout-marketplace/gallery-terminal.tsx +126 -96
  168. package/src/layout-marketplace/gallery.test.tsx +65 -1
  169. package/src/layout-marketplace/pane-imagery.ts +1 -0
  170. package/src/layout-marketplace/payload.test.ts +17 -0
  171. package/src/layout-marketplace/payload.ts +40 -5
  172. package/src/market-data/coordinator/index.ts +3 -2
  173. package/src/market-data/coordinator/quotes.ts +173 -66
  174. package/src/market-data/coordinator/subscriptions.test.ts +215 -37
  175. package/src/pane-settings.ts +1 -1
  176. package/src/plugins/builtin/analytics/pane-model.ts +1 -1
  177. package/src/plugins/builtin/analytics/view.tsx +1 -1
  178. package/src/plugins/builtin/broker-manager/index.test.tsx +3 -2
  179. package/src/plugins/builtin/broker-manager/model.test.ts +2 -2
  180. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +16 -2
  181. package/src/plugins/builtin/chart-composer/cli-options.ts +12 -9
  182. package/src/plugins/builtin/chart-composer/pane.tsx +22 -8
  183. package/src/plugins/builtin/chart-composer/settings.ts +2 -13
  184. package/src/plugins/builtin/chat/content/composer.tsx +8 -4
  185. package/src/plugins/builtin/cloud/auth-dialog.tsx +14 -228
  186. package/src/plugins/builtin/cloud/auth-form.tsx +277 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +1 -0
  188. package/src/plugins/builtin/composite-plugins.ts +16 -1
  189. package/src/plugins/builtin/correlation/relationship/model.ts +1 -1
  190. package/src/plugins/builtin/correlation/relationship/pane.tsx +2 -6
  191. package/src/plugins/builtin/correlation/relationship/view-model.ts +2 -2
  192. package/src/plugins/builtin/credit-conditions/index.test.tsx +12 -6
  193. package/src/plugins/builtin/credit-conditions/index.tsx +2 -21
  194. package/src/plugins/builtin/dividend-yield/index.tsx +1 -0
  195. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  196. package/src/plugins/builtin/earnings/index.tsx +1 -0
  197. package/src/plugins/builtin/earnings-calls/data.ts +223 -0
  198. package/src/plugins/builtin/earnings-calls/format.ts +49 -0
  199. package/src/plugins/builtin/earnings-calls/index.tsx +69 -0
  200. package/src/plugins/builtin/earnings-calls/pane.tsx +756 -0
  201. package/src/plugins/builtin/earnings-calls/prose.test.ts +36 -0
  202. package/src/plugins/builtin/earnings-calls/prose.ts +59 -0
  203. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +334 -0
  204. package/src/plugins/builtin/econ/detail-view.tsx +1 -2
  205. package/src/plugins/builtin/econ/fred-series-map.ts +16 -1
  206. package/src/plugins/builtin/econ/index.tsx +3 -1
  207. package/src/plugins/builtin/econ-statistics/cache.ts +4 -0
  208. package/src/plugins/builtin/econ-statistics/client.ts +92 -0
  209. package/src/plugins/builtin/econ-statistics/defs.ts +66 -0
  210. package/src/plugins/builtin/econ-statistics/detail.tsx +154 -0
  211. package/src/plugins/builtin/econ-statistics/headless.test.ts +54 -0
  212. package/src/plugins/builtin/econ-statistics/headless.ts +171 -0
  213. package/src/plugins/builtin/econ-statistics/index.tsx +54 -0
  214. package/src/plugins/builtin/econ-statistics/pane.tsx +356 -0
  215. package/src/plugins/builtin/econ-statistics/stats.ts +522 -0
  216. package/src/plugins/builtin/econ-statistics/transform.test.ts +86 -0
  217. package/src/plugins/builtin/econ-statistics/transform.ts +85 -0
  218. package/src/plugins/builtin/econ-statistics/trend.ts +57 -0
  219. package/src/plugins/builtin/econ-statistics/view.ts +90 -0
  220. package/src/plugins/builtin/fear-greed/charts.tsx +7 -18
  221. package/src/plugins/builtin/fear-greed/data.ts +6 -1
  222. package/src/plugins/builtin/futures/index.tsx +1 -0
  223. package/src/plugins/builtin/fx-matrix/index.tsx +1 -0
  224. package/src/plugins/builtin/help/index.tsx +4 -0
  225. package/src/plugins/builtin/holders/index.tsx +1 -0
  226. package/src/plugins/builtin/insider/index.tsx +1 -0
  227. package/src/plugins/builtin/ipo-calendar/index.tsx +1 -0
  228. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  229. package/src/plugins/builtin/kelly-sizer/sections.tsx +2 -3
  230. package/src/plugins/builtin/market-halts/index.tsx +1 -0
  231. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  232. package/src/plugins/builtin/market-valuation/align.ts +122 -0
  233. package/src/plugins/builtin/market-valuation/cache.ts +32 -0
  234. package/src/plugins/builtin/market-valuation/chart-projection.ts +217 -0
  235. package/src/plugins/builtin/market-valuation/chart-series.ts +89 -0
  236. package/src/plugins/builtin/market-valuation/client.test.ts +141 -0
  237. package/src/plugins/builtin/market-valuation/client.ts +116 -0
  238. package/src/plugins/builtin/market-valuation/defs.ts +251 -0
  239. package/src/plugins/builtin/market-valuation/detail.tsx +159 -0
  240. package/src/plugins/builtin/market-valuation/headless.test.ts +58 -0
  241. package/src/plugins/builtin/market-valuation/headless.ts +170 -0
  242. package/src/plugins/builtin/market-valuation/index.tsx +64 -0
  243. package/src/plugins/builtin/market-valuation/indicators.ts +422 -0
  244. package/src/plugins/builtin/market-valuation/model.test.ts +350 -0
  245. package/src/plugins/builtin/market-valuation/model.ts +8 -0
  246. package/src/plugins/builtin/market-valuation/pane.test.tsx +247 -0
  247. package/src/plugins/builtin/market-valuation/pane.tsx +344 -0
  248. package/src/plugins/builtin/market-valuation/series.ts +12 -0
  249. package/src/plugins/builtin/market-valuation/settings.ts +38 -0
  250. package/src/plugins/builtin/market-valuation/sources.ts +130 -0
  251. package/src/plugins/builtin/market-valuation/trend.ts +89 -0
  252. package/src/plugins/builtin/market-valuation/view.ts +137 -0
  253. package/src/plugins/builtin/market-valuation/zone-scale/desktop.tsx +123 -0
  254. package/src/plugins/builtin/market-valuation/zone-scale/index.tsx +12 -0
  255. package/src/plugins/builtin/market-valuation/zone-scale/model.ts +60 -0
  256. package/src/plugins/builtin/market-valuation/zone-scale/terminal.tsx +94 -0
  257. package/src/plugins/builtin/news/wire/breaking/filters.test.ts +99 -0
  258. package/src/plugins/builtin/news/wire/breaking/filters.ts +110 -0
  259. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +157 -53
  260. package/src/plugins/builtin/news/wire/breaking/notifications.ts +91 -25
  261. package/src/plugins/builtin/news/wire/index.tsx +31 -44
  262. package/src/plugins/builtin/options/analytics.test.ts +83 -0
  263. package/src/plugins/builtin/options/analytics.ts +110 -0
  264. package/src/plugins/builtin/options/index.tsx +18 -2
  265. package/src/plugins/builtin/options/table.ts +121 -51
  266. package/src/plugins/builtin/options/types.ts +22 -15
  267. package/src/plugins/builtin/options/view.test.tsx +53 -1
  268. package/src/plugins/builtin/options/view.tsx +92 -41
  269. package/src/plugins/builtin/plugin-marketplace/feed.ts +80 -0
  270. package/src/plugins/builtin/plugin-marketplace/index.tsx +31 -0
  271. package/src/plugins/builtin/plugin-marketplace/model.test.ts +200 -0
  272. package/src/plugins/builtin/plugin-marketplace/model.ts +204 -0
  273. package/src/plugins/builtin/plugin-marketplace/pane.tsx +385 -0
  274. package/src/plugins/builtin/plugin-marketplace/store.ts +46 -0
  275. package/src/plugins/builtin/plugin-module.test.ts +2 -4
  276. package/src/plugins/builtin/plugin-module.ts +9 -8
  277. package/src/plugins/builtin/portfolio-list/index.tsx +1 -0
  278. package/src/plugins/builtin/research/index.tsx +4 -0
  279. package/src/plugins/builtin/research-search/command-bar-search.test.ts +81 -0
  280. package/src/plugins/builtin/research-search/command-bar-search.ts +175 -0
  281. package/src/plugins/builtin/research-search/data.ts +109 -0
  282. package/src/plugins/builtin/research-search/document-view.test.tsx +75 -0
  283. package/src/plugins/builtin/research-search/document-view.tsx +254 -0
  284. package/src/plugins/builtin/research-search/filter-bar.tsx +62 -0
  285. package/src/plugins/builtin/research-search/focus-handoff.ts +47 -0
  286. package/src/plugins/builtin/research-search/index.tsx +85 -0
  287. package/src/plugins/builtin/research-search/model.test.ts +40 -0
  288. package/src/plugins/builtin/research-search/model.ts +291 -0
  289. package/src/plugins/builtin/research-search/pane.test.tsx +241 -0
  290. package/src/plugins/builtin/research-search/pane.tsx +678 -0
  291. package/src/plugins/builtin/research-search/saved-view.tsx +171 -0
  292. package/src/plugins/builtin/research-search/snippet-text.tsx +35 -0
  293. package/src/plugins/builtin/research-search/snippet.test.ts +78 -0
  294. package/src/plugins/builtin/research-search/snippet.ts +134 -0
  295. package/src/plugins/builtin/scanner/flow-model.test.ts +11 -1
  296. package/src/plugins/builtin/scanner/flow-model.ts +29 -1
  297. package/src/plugins/builtin/scanner/flow-pane.tsx +78 -22
  298. package/src/plugins/builtin/scanner/index.ts +2 -0
  299. package/src/plugins/builtin/sec/index.tsx +1 -0
  300. package/src/plugins/builtin/sectors/index.tsx +1 -0
  301. package/src/plugins/builtin/shared/series-cache.ts +93 -0
  302. package/src/plugins/builtin/short-interest/index.tsx +1 -0
  303. package/src/plugins/builtin/short-interest/pane.tsx +1 -1
  304. package/src/plugins/builtin/thirteenf/index.tsx +1 -0
  305. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +3 -5
  306. package/src/plugins/builtin/ticker-detail/index.tsx +2 -14
  307. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +1 -1
  308. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  309. package/src/plugins/builtin/volatility/index.tsx +1 -1
  310. package/src/plugins/builtin/world-indices/index.tsx +1 -0
  311. package/src/plugins/builtin/world-venue-map/index.ts +27 -0
  312. package/src/plugins/builtin/world-venue-map/map.desktop.test.tsx +106 -0
  313. package/src/plugins/builtin/world-venue-map/map.tsx +481 -0
  314. package/src/plugins/builtin/world-venue-map/model.test.ts +85 -0
  315. package/src/plugins/builtin/world-venue-map/model.ts +268 -0
  316. package/src/plugins/builtin/world-venue-map/pane.tsx +300 -0
  317. package/src/plugins/builtin/world-venue-map/world-outlines.ts +2 -0
  318. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  319. package/src/plugins/bundle.test.ts +109 -0
  320. package/src/plugins/bundle.ts +139 -0
  321. package/src/plugins/catalog-backend.ts +6 -44
  322. package/src/plugins/catalog-browser.test.ts +1 -0
  323. package/src/plugins/catalog-browser.ts +12 -0
  324. package/src/plugins/catalog-manifest.test.ts +43 -0
  325. package/src/plugins/catalog-manifest.ts +92 -0
  326. package/src/plugins/catalog-ui.ts +20 -4
  327. package/src/plugins/catalog.ts +9 -2
  328. package/src/plugins/current-target.ts +28 -0
  329. package/src/plugins/host-contract.ts +37 -0
  330. package/src/plugins/host-link.ts +158 -0
  331. package/src/plugins/host-modules.ts +61 -0
  332. package/src/plugins/loader.ts +41 -22
  333. package/src/plugins/ownership.ts +1 -0
  334. package/src/plugins/prediction-markets/index.tsx +1 -0
  335. package/src/plugins/registry/context.ts +3 -0
  336. package/src/plugins/registry/contributions.ts +27 -0
  337. package/src/plugins/registry/index.test.ts +25 -0
  338. package/src/plugins/registry/index.ts +8 -0
  339. package/src/plugins/registry/pane-settings.test.ts +53 -0
  340. package/src/plugins/registry/pane-settings.ts +29 -3
  341. package/src/plugins/seed.test.ts +83 -0
  342. package/src/plugins/seed.ts +80 -0
  343. package/src/public/broker.ts +31 -0
  344. package/src/public/market-data.ts +9 -0
  345. package/src/public/pane-hooks.ts +57 -0
  346. package/src/public/public-api.test.ts +285 -0
  347. package/src/public/react.ts +62 -0
  348. package/src/public/test-support.ts +23 -0
  349. package/src/public/time-series.ts +8 -0
  350. package/src/public/utils.ts +77 -0
  351. package/src/remote/layout-helpers.ts +6 -8
  352. package/src/remote/resources.ts +2 -0
  353. package/src/remote/schema.ts +1 -0
  354. package/src/remote/semantic-tree.test.tsx +105 -0
  355. package/src/remote/semantic-tree.tsx +53 -35
  356. package/src/renderers/browser/main.tsx +6 -0
  357. package/src/renderers/browser/ui-host.tsx +11 -0
  358. package/src/renderers/cloudflare/worker.test.ts +19 -0
  359. package/src/renderers/cloudflare/worker.ts +1 -1
  360. package/src/renderers/electrobun/bun/desktop/host-requests.ts +9 -2
  361. package/src/renderers/electrobun/bun/external-plugins.ts +140 -0
  362. package/src/renderers/electrobun/bun/index.ts +6 -2
  363. package/src/renderers/electrobun/shared/protocol.ts +25 -0
  364. package/src/renderers/electrobun/view/build-assets.ts +0 -1
  365. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +51 -1
  366. package/src/renderers/electrobun/view/external-plugins.test.ts +45 -0
  367. package/src/renderers/electrobun/view/external-plugins.ts +63 -0
  368. package/src/renderers/electrobun/view/host/ascii-text.test.ts +48 -0
  369. package/src/renderers/electrobun/view/host/ascii-text.ts +11 -1
  370. package/src/renderers/electrobun/view/host/chart-surface.tsx +38 -32
  371. package/src/renderers/electrobun/view/host/mouse.ts +16 -2
  372. package/src/renderers/electrobun/view/main.tsx +27 -2
  373. package/src/renderers/electrobun/view/styles.css +5 -0
  374. package/src/renderers/electrobun/view/toast-host.tsx +18 -0
  375. package/src/renderers/electrobun/view/ui-host.tsx +3 -0
  376. package/src/renderers/opentui/chart-surface.tsx +5 -1
  377. package/src/renderers/opentui/host.tsx +20 -2
  378. package/src/renderers/opentui/interaction-performance.test.ts +79 -0
  379. package/src/renderers/opentui/interaction-performance.ts +188 -0
  380. package/src/renderers/opentui/start.tsx +26 -1
  381. package/src/renderers/opentui/test-utils.tsx +13 -0
  382. package/src/renderers/opentui/toast-host.tsx +26 -2
  383. package/src/renderers/opentui/ui-host.tsx +1 -0
  384. package/src/sources/provider-router/search.test.ts +191 -0
  385. package/src/sources/provider-router/search.ts +92 -39
  386. package/src/state/app/context/index.tsx +2 -0
  387. package/src/state/app/context/selector.test.tsx +33 -1
  388. package/src/state/hooks/quote-streaming.test.tsx +93 -0
  389. package/src/state/hooks/quote-streaming.ts +106 -30
  390. package/src/state/pane-table-export-registry.ts +50 -0
  391. package/src/theme/color-utils.ts +1 -1
  392. package/src/theme/colors.ts +50 -0
  393. package/src/theme/themes.test.ts +15 -2
  394. package/src/theme/themes.ts +19 -1
  395. package/src/tickers/search/index.test.ts +26 -0
  396. package/src/tickers/search/index.ts +42 -17
  397. package/src/tickers/search/ranking.ts +14 -0
  398. package/src/time-series/chart-data.test.ts +216 -0
  399. package/src/time-series/range.ts +12 -0
  400. package/src/time-series/resolution.test.ts +25 -26
  401. package/src/time-series/resolution.ts +44 -25
  402. package/src/time-series/resolve.ts +7 -0
  403. package/src/time-series/spec.ts +5 -5
  404. package/src/time-series/types.ts +3 -1
  405. package/src/time-series/use-chart-resolution.ts +5 -0
  406. package/src/types/config.ts +123 -3
  407. package/src/types/data-provider.ts +11 -0
  408. package/src/types/headless.ts +161 -0
  409. package/src/types/plugin.ts +104 -20
  410. package/src/ui/host.tsx +19 -1
  411. package/src/ui/index.tsx +1 -0
  412. package/src/ui/toast.tsx +8 -4
  413. package/src/utils/csv.test.ts +13 -0
  414. package/src/utils/csv.ts +37 -0
  415. package/src/utils/format.ts +35 -0
  416. package/src/utils/fuzzy-search.test.ts +94 -0
  417. package/src/utils/fuzzy-search.ts +81 -22
  418. package/src/utils/save-text-file.test.ts +24 -0
  419. package/src/utils/save-text-file.ts +36 -0
  420. package/src/utils/shortcut-labels.ts +14 -0
  421. package/src/components/chart/core/axis-format.ts +0 -157
  422. package/src/components/chart/core/index.test.ts +0 -1075
  423. package/src/components/chart/core/projection.ts +0 -357
  424. package/src/components/chart/core/render-utils.ts +0 -4
  425. package/src/components/chart/core/renderer.ts +0 -244
  426. package/src/components/chart/core/scene.ts +0 -180
  427. package/src/components/chart/core/time-axis.ts +0 -330
  428. package/src/components/chart/cursor-motion.ts +0 -47
  429. package/src/components/chart/indicators/types.ts +0 -26
  430. package/src/components/chart/market-session.ts +0 -176
  431. package/src/components/chart/multi-line/renderer.test.ts +0 -83
  432. package/src/components/chart/multi-line/renderer.ts +0 -161
  433. package/src/components/chart/multi-line/scene.ts +0 -188
  434. package/src/components/chart/native/raster/layout.ts +0 -129
  435. package/src/components/chart/native/raster/price-chart.ts +0 -244
  436. package/src/components/chart/rendering/chart-draw.ts +0 -270
  437. package/src/components/chart/rendering/geometry.ts +0 -95
  438. package/src/components/chart/rendering/pixel-buffer.ts +0 -267
  439. package/src/components/chart/scatter-chart-renderer.test.ts +0 -31
  440. package/src/components/chart/scatter-chart-renderer.ts +0 -223
  441. package/src/components/chart/static/chart/surface.tsx +0 -358
  442. package/src/components/command-bar/routes/root/article-results.test.ts +0 -83
  443. package/src/components/command-bar/routes/root/article-results.ts +0 -55
  444. package/src/plugins/builtin/news/wire/article-search.test.ts +0 -72
  445. package/src/plugins/builtin/news/wire/article-search.ts +0 -140
  446. package/src/plugins/builtin/substack/api/auth.ts +0 -185
  447. package/src/plugins/builtin/substack/api/cache.ts +0 -82
  448. package/src/plugins/builtin/substack/api/loaders.ts +0 -170
  449. package/src/plugins/builtin/substack/api/store.ts +0 -186
  450. package/src/plugins/builtin/substack/api/types.ts +0 -42
  451. package/src/plugins/builtin/substack/api.test.ts +0 -298
  452. package/src/plugins/builtin/substack/article-detail.tsx +0 -397
  453. package/src/plugins/builtin/substack/article-stack.tsx +0 -133
  454. package/src/plugins/builtin/substack/content.ts +0 -478
  455. package/src/plugins/builtin/substack/feed-tabs.tsx +0 -44
  456. package/src/plugins/builtin/substack/index.tsx +0 -49
  457. package/src/plugins/builtin/substack/login-view.tsx +0 -126
  458. package/src/plugins/builtin/substack/model.test.ts +0 -155
  459. package/src/plugins/builtin/substack/normalize.ts +0 -343
  460. package/src/plugins/builtin/substack/pane-footer.ts +0 -52
  461. package/src/plugins/builtin/substack/pane-state.ts +0 -170
  462. package/src/plugins/builtin/substack/pane.tsx +0 -547
  463. package/src/plugins/builtin/substack/read-state.ts +0 -30
  464. package/src/plugins/builtin/substack/table.ts +0 -111
  465. package/src/plugins/builtin/substack/types.ts +0 -71
  466. package/src/plugins/builtin/substack/utils.ts +0 -131
  467. package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +0 -229
  468. package/src/plugins/ibkr/account-cache.test.ts +0 -102
  469. package/src/plugins/ibkr/account-cache.ts +0 -24
  470. package/src/plugins/ibkr/broker-adapter.ts +0 -286
  471. package/src/plugins/ibkr/config.test.ts +0 -79
  472. package/src/plugins/ibkr/config.ts +0 -250
  473. package/src/plugins/ibkr/dialogs.tsx +0 -40
  474. package/src/plugins/ibkr/flex/index.test.ts +0 -264
  475. package/src/plugins/ibkr/flex/index.ts +0 -367
  476. package/src/plugins/ibkr/flex/statement-client.ts +0 -235
  477. package/src/plugins/ibkr/fundamental-parser.ts +0 -149
  478. package/src/plugins/ibkr/gateway/account-loaders.ts +0 -266
  479. package/src/plugins/ibkr/gateway/account-summary.ts +0 -164
  480. package/src/plugins/ibkr/gateway/client-lock.ts +0 -154
  481. package/src/plugins/ibkr/gateway/connection-events.ts +0 -114
  482. package/src/plugins/ibkr/gateway/connection.ts +0 -152
  483. package/src/plugins/ibkr/gateway/contracts.ts +0 -132
  484. package/src/plugins/ibkr/gateway/fundamentals.ts +0 -83
  485. package/src/plugins/ibkr/gateway/helpers.ts +0 -33
  486. package/src/plugins/ibkr/gateway/history-requests.ts +0 -158
  487. package/src/plugins/ibkr/gateway/history.ts +0 -120
  488. package/src/plugins/ibkr/gateway/instrument-lookup.ts +0 -117
  489. package/src/plugins/ibkr/gateway/market-data.ts +0 -345
  490. package/src/plugins/ibkr/gateway/order-execution.ts +0 -218
  491. package/src/plugins/ibkr/gateway/orders.ts +0 -63
  492. package/src/plugins/ibkr/gateway/price-normalization.test.ts +0 -44
  493. package/src/plugins/ibkr/gateway/price-normalization.ts +0 -50
  494. package/src/plugins/ibkr/gateway/quote-stream-controller.ts +0 -203
  495. package/src/plugins/ibkr/gateway/quote-stream.ts +0 -171
  496. package/src/plugins/ibkr/gateway/selection.ts +0 -51
  497. package/src/plugins/ibkr/gateway/service/index.test.ts +0 -675
  498. package/src/plugins/ibkr/gateway/service/index.ts +0 -429
  499. package/src/plugins/ibkr/gateway/service/lifecycle.ts +0 -191
  500. package/src/plugins/ibkr/gateway/service/manager.ts +0 -52
  501. package/src/plugins/ibkr/gateway/service/market-data.ts +0 -48
  502. package/src/plugins/ibkr/gateway/service/native-loader.ts +0 -12
  503. package/src/plugins/ibkr/gateway/service/native.ts +0 -543
  504. package/src/plugins/ibkr/gateway/timeouts.ts +0 -13
  505. package/src/plugins/ibkr/gateway/types.ts +0 -27
  506. package/src/plugins/ibkr/index.test.tsx +0 -162
  507. package/src/plugins/ibkr/index.tsx +0 -159
  508. package/src/plugins/ibkr/instance-selection.test.ts +0 -120
  509. package/src/plugins/ibkr/instance-selection.ts +0 -36
  510. package/src/plugins/ibkr/portfolio-performance.test.ts +0 -32
  511. package/src/plugins/ibkr/portfolio-performance.ts +0 -129
  512. package/src/plugins/ibkr/trade/badge.tsx +0 -28
  513. package/src/plugins/ibkr/trade/dialogs.tsx +0 -44
  514. package/src/plugins/ibkr/trade/preview-panel.tsx +0 -130
  515. package/src/plugins/ibkr/trade/tab/actions.tsx +0 -443
  516. package/src/plugins/ibkr/trade/tab/field-editors.tsx +0 -79
  517. package/src/plugins/ibkr/trade/tab/footer.ts +0 -53
  518. package/src/plugins/ibkr/trade/tab/header.tsx +0 -167
  519. package/src/plugins/ibkr/trade/tab/index.test.tsx +0 -178
  520. package/src/plugins/ibkr/trade/tab/index.tsx +0 -298
  521. package/src/plugins/ibkr/trade/tab/model.ts +0 -159
  522. package/src/plugins/ibkr/trade/tab/order-request.ts +0 -64
  523. package/src/plugins/ibkr/trade/tab/shortcuts.ts +0 -89
  524. package/src/plugins/ibkr/trade/tab/state-sync.ts +0 -86
  525. package/src/plugins/ibkr/trade/ticket-panel.tsx +0 -301
  526. package/src/plugins/ibkr/trade/utils.test.ts +0 -175
  527. package/src/plugins/ibkr/trade/utils.ts +0 -144
  528. package/src/plugins/ibkr/trading/pane-view.tsx +0 -237
  529. package/src/plugins/ibkr/trading/pane.tsx +0 -332
  530. package/src/plugins/ibkr/trading/state.test.ts +0 -109
  531. package/src/plugins/ibkr/trading/state.ts +0 -335
@@ -1,8 +1,9 @@
1
1
  import type { ResolvedSeries } from "../../../time-series/types";
2
+ import type { CompositeTimeScale } from "./types";
2
3
  import {
3
4
  buildCompositeTimeScale,
4
- projectCompositeTimestamp,
5
- unprojectCompositeTimestamp,
5
+ compositeTimeAtPosition,
6
+ compositeTimePosition,
6
7
  } from "./time-scale";
7
8
 
8
9
  export interface CompositeViewportRange {
@@ -28,31 +29,48 @@ export type CompositeChartInteraction =
28
29
 
29
30
  export const COMPOSITE_ZOOM_STEP_FACTOR = 1.2;
30
31
  export const COMPOSITE_KEYBOARD_PAN_RATIO = 0.02;
32
+ /** Pixels of trackpad or wheel travel that zoom the viewport by a factor of e. */
33
+ const WHEEL_ZOOM_PIXELS_PER_E = 160;
34
+ /** Bound on one wheel event so a coarse mouse notch cannot zoom by more than this. */
35
+ const WHEEL_ZOOM_STEP_LIMIT = 1.5;
31
36
 
32
- const FALLBACK_SINGLE_POINT_SPAN_MS = 24 * 60 * 60 * 1000;
37
+ const DAY_MS = 24 * 60 * 60 * 1000;
38
+ const FALLBACK_SINGLE_POINT_SPAN_MS = DAY_MS;
39
+ /** Bars that stay in view at the deepest zoom. */
40
+ const MINIMUM_VISIBLE_BARS = 2;
33
41
  const WHEEL_PAN_RATIO_PER_DELTA = 0.005;
34
42
  const MAX_WHEEL_DELTA_MAGNITUDE = 8;
43
+ const WHEEL_ZOOM_RATIO_PER_DELTA = 0.04;
35
44
  const SOURCE_VIEWPORT_RESET_RATIO = 0.01;
36
45
  const SOURCE_VIEWPORT_RESET_FLOOR_MS = 60_000;
37
46
 
47
+ /**
48
+ * The space every navigation gesture works in. Market charts collapse closed
49
+ * sessions, so a position is a bar slot there; calendar charts use wall-clock
50
+ * milliseconds. Data refreshes build a new frame; an in-flight gesture keeps
51
+ * the one it started with, so a refresh can never move the view under the
52
+ * pointer.
53
+ */
54
+ export interface CompositeNavigationFrame {
55
+ scale: CompositeTimeScale;
56
+ dataStart: number;
57
+ dataEnd: number;
58
+ dataStartPosition: number;
59
+ dataEndPosition: number;
60
+ minimumSpanPositions: number;
61
+ /** Wall-clock equivalent of `minimumSpanPositions`, for tools that select time ranges. */
62
+ minimumSpanMs: number;
63
+ /** Fraction of the visible span the viewport may extend before the first observation. */
64
+ historicalPaddingRatio: number;
65
+ }
66
+
38
67
  function finiteTime(value: Date | undefined): number | null {
39
68
  const time = value?.getTime();
40
69
  return typeof time === "number" && Number.isFinite(time) ? time : null;
41
70
  }
42
71
 
43
- function normalizeViewport(
44
- viewport: CompositeViewportRange | null | undefined,
45
- ): CompositeViewportRange | null {
46
- const start = finiteTime(viewport?.start);
47
- const end = finiteTime(viewport?.end);
48
- if (start === null || end === null || start > end) return null;
49
- if (start < end) {
50
- return { start: new Date(start), end: new Date(end) };
51
- }
52
- return {
53
- start: new Date(start - FALLBACK_SINGLE_POINT_SPAN_MS / 2),
54
- end: new Date(end + FALLBACK_SINGLE_POINT_SPAN_MS / 2),
55
- };
72
+ function clamp(value: number, minimum: number, maximum: number): number {
73
+ return Math.max(minimum, Math.min(maximum, value));
56
74
  }
57
75
 
58
76
  function hasRenderableValue(point: ResolvedSeries["points"][number]): boolean {
@@ -62,170 +80,140 @@ function hasRenderableValue(point: ResolvedSeries["points"][number]): boolean {
62
80
  );
63
81
  }
64
82
 
65
- function pointTimestamps(series: ResolvedSeries[]): number[] {
66
- const timestamps = new Set<number>();
83
+ function dataExtent(series: readonly ResolvedSeries[]): { start: number; end: number } | null {
84
+ let start = Number.POSITIVE_INFINITY;
85
+ let end = Number.NEGATIVE_INFINITY;
67
86
  for (const entry of series) {
68
87
  for (const point of entry.points) {
69
88
  if (!hasRenderableValue(point)) continue;
70
- const timestamp = point.date instanceof Date
71
- ? point.date.getTime()
72
- : new Date(point.date).getTime();
73
- if (Number.isFinite(timestamp)) timestamps.add(timestamp);
89
+ const timestamp = point.date.getTime();
90
+ if (!Number.isFinite(timestamp)) continue;
91
+ if (timestamp < start) start = timestamp;
92
+ if (timestamp > end) end = timestamp;
74
93
  }
75
94
  }
76
- return [...timestamps].sort((left, right) => left - right);
95
+ return start <= end ? { start, end } : null;
77
96
  }
78
97
 
79
- function viewportHasTimestamps(
80
- timestamps: readonly number[],
81
- viewport: CompositeViewportRange,
82
- minimumCount: number,
83
- ): boolean {
84
- const start = finiteTime(viewport.start);
85
- const end = finiteTime(viewport.end);
86
- if (start === null || end === null || start > end) return false;
87
- const required = Math.max(1, Math.floor(minimumCount));
88
- let count = 0;
89
- for (const timestamp of timestamps) {
90
- if (timestamp < start) continue;
91
- if (timestamp > end) break;
92
- count += 1;
93
- if (count >= required) return true;
98
+ function medianStepMs(series: readonly ResolvedSeries[]): number | null {
99
+ const timestamps = new Set<number>();
100
+ for (const entry of series) {
101
+ for (const point of entry.points) {
102
+ if (!hasRenderableValue(point)) continue;
103
+ const timestamp = point.date.getTime();
104
+ if (Number.isFinite(timestamp)) timestamps.add(timestamp);
105
+ }
94
106
  }
95
- return false;
96
- }
97
-
98
- export function compositeViewportHasObservations(
99
- series: ResolvedSeries[],
100
- viewport: CompositeViewportRange,
101
- minimumCount = 2,
102
- ): boolean {
103
- return viewportHasTimestamps(pointTimestamps(series), viewport, minimumCount);
107
+ const sorted = [...timestamps].sort((left, right) => left - right);
108
+ const steps: number[] = [];
109
+ for (let index = 1; index < sorted.length; index += 1) {
110
+ const step = sorted[index]! - sorted[index - 1]!;
111
+ if (step > 0) steps.push(step);
112
+ }
113
+ if (steps.length === 0) return null;
114
+ steps.sort((left, right) => left - right);
115
+ // An upper median keeps one near-duplicate live observation from lowering
116
+ // a daily chart's zoom floor.
117
+ return steps[Math.floor(steps.length / 2)]!;
104
118
  }
105
119
 
106
- export function resolveCompositeNavigationBounds(
107
- series: ResolvedSeries[],
108
- requestedViewport?: CompositeViewportRange | null,
120
+ export function buildCompositeNavigationFrame(
121
+ series: readonly ResolvedSeries[],
122
+ timelineSeries: readonly ResolvedSeries[],
109
123
  options: { historicalPaddingRatio?: number } = {},
110
- ): CompositeViewportRange | null {
111
- const requested = normalizeViewport(requestedViewport);
112
- const timestamps = pointTimestamps(series);
113
- if (timestamps.length === 0) return requested;
114
- const data = normalizeViewport({
115
- start: new Date(timestamps[0]!),
116
- end: new Date(timestamps.at(-1)!),
117
- });
118
- if (!requested) return data;
119
- if (!data) return requested;
120
- const overlapsData = requested.end.getTime() >= data.start.getTime()
121
- && requested.start.getTime() <= data.end.getTime();
122
- // Loaded series can include a buffer outside the authored viewport. Use the
123
- // complete real-data extent when the two ranges overlap. A disjoint request
124
- // keeps its own window, unioned with the data so navigation can always get
125
- // back: bounds equal to an observation-free window strand pan and zoom.
126
- if (!overlapsData) {
127
- return {
128
- start: new Date(Math.min(data.start.getTime(), requested.start.getTime())),
129
- end: new Date(Math.max(data.end.getTime(), requested.end.getTime())),
130
- };
131
- }
132
- const paddingRatio = Math.max(options.historicalPaddingRatio ?? 0, 0);
133
- if (paddingRatio === 0) return data;
134
- const requestedSpan = Math.max(requested.end.getTime() - requested.start.getTime(), 1);
124
+ ): CompositeNavigationFrame | null {
125
+ const extent = dataExtent(series);
126
+ if (!extent) return null;
127
+ const scale = buildCompositeTimeScale(timelineSeries, extent.start, extent.end);
128
+ const dataStartPosition = compositeTimePosition(scale, extent.start);
129
+ const dataEndPosition = compositeTimePosition(scale, extent.end);
130
+ const minimumSpanMs = scale.kind === "market"
131
+ ? scale.cadenceMs * MINIMUM_VISIBLE_BARS
132
+ : (medianStepMs(series) ?? FALLBACK_SINGLE_POINT_SPAN_MS) * MINIMUM_VISIBLE_BARS;
133
+ const minimumSpanPositions = scale.kind === "market" ? MINIMUM_VISIBLE_BARS : minimumSpanMs;
135
134
  return {
136
- start: new Date(Math.min(data.start.getTime(), requested.start.getTime()) - requestedSpan * paddingRatio),
137
- end: data.end,
135
+ scale,
136
+ dataStart: extent.start,
137
+ dataEnd: extent.end,
138
+ dataStartPosition,
139
+ dataEndPosition,
140
+ minimumSpanPositions: Math.max(minimumSpanPositions, Number.EPSILON),
141
+ minimumSpanMs: Math.max(minimumSpanMs, 1),
142
+ historicalPaddingRatio: Math.max(options.historicalPaddingRatio ?? 0, 0),
138
143
  };
139
144
  }
140
145
 
141
- export function resolveCompositeMinimumSpanMs(
142
- series: ResolvedSeries[],
143
- bounds: CompositeViewportRange,
144
- ): number {
145
- const start = bounds.start.getTime();
146
- const end = bounds.end.getTime();
147
- const timestamps = pointTimestamps(series).filter((timestamp) => timestamp >= start && timestamp <= end);
148
- const steps: number[] = [];
149
- for (let index = 1; index < timestamps.length; index += 1) {
150
- const step = timestamps[index]! - timestamps[index - 1]!;
151
- if (step > 0) steps.push(step);
146
+ export function compositeNavigationDataViewport(frame: CompositeNavigationFrame): CompositeViewportRange {
147
+ if (frame.dataStart < frame.dataEnd) {
148
+ return { start: new Date(frame.dataStart), end: new Date(frame.dataEnd) };
152
149
  }
153
- const boundsSpan = Math.max(end - start, 1);
154
- steps.sort((left, right) => left - right);
155
- // Use an observed upper median rather than the absolute minimum. This keeps
156
- // one near-duplicate live observation from lowering a daily chart's floor.
157
- const representativeStep = steps[Math.floor(steps.length / 2)];
158
- return representativeStep !== undefined
159
- ? Math.min(Math.max(representativeStep, 1), boundsSpan)
160
- : Math.max(Math.min(boundsSpan, FALLBACK_SINGLE_POINT_SPAN_MS), 1);
150
+ return {
151
+ start: new Date(frame.dataStart - FALLBACK_SINGLE_POINT_SPAN_MS / 2),
152
+ end: new Date(frame.dataEnd + FALLBACK_SINGLE_POINT_SPAN_MS / 2),
153
+ };
161
154
  }
162
155
 
163
- function clamp(value: number, minimum: number, maximum: number): number {
164
- return Math.max(minimum, Math.min(maximum, value));
156
+ interface PositionRange {
157
+ start: number;
158
+ end: number;
165
159
  }
166
160
 
167
- function marketViewportProjection(
161
+ export function compositeViewportPositions(
162
+ frame: CompositeNavigationFrame,
168
163
  viewport: CompositeViewportRange,
169
- bounds: CompositeViewportRange,
170
- series: ResolvedSeries[] | undefined,
171
- ): {
172
- scale: Extract<ReturnType<typeof buildCompositeTimeScale>, { kind: "market" }>;
173
- startRatio: number;
174
- endRatio: number;
175
- } | null {
176
- if (!series?.some((entry) => entry.timeBasis?.kind === "market")) return null;
177
- const scale = buildCompositeTimeScale(
178
- series,
179
- bounds.start.getTime(),
180
- bounds.end.getTime(),
181
- );
182
- if (scale.kind !== "market") return null;
183
- const startRatio = projectCompositeTimestamp(scale, viewport.start.getTime())?.ratio;
184
- const endRatio = projectCompositeTimestamp(scale, viewport.end.getTime())?.ratio;
185
- if (
186
- typeof startRatio !== "number"
187
- || !Number.isFinite(startRatio)
188
- || typeof endRatio !== "number"
189
- || !Number.isFinite(endRatio)
190
- || startRatio >= endRatio
191
- ) {
192
- return null;
193
- }
194
- return { scale, startRatio, endRatio };
164
+ ): PositionRange | null {
165
+ const startTime = finiteTime(viewport.start);
166
+ const endTime = finiteTime(viewport.end);
167
+ if (startTime === null || endTime === null || startTime > endTime) return null;
168
+ const start = compositeTimePosition(frame.scale, startTime);
169
+ const end = compositeTimePosition(frame.scale, endTime);
170
+ return { start, end: Math.max(end, start) };
195
171
  }
196
172
 
197
- function viewportFromMarketRatios(
198
- scale: Extract<ReturnType<typeof buildCompositeTimeScale>, { kind: "market" }>,
199
- startRatio: number,
200
- endRatio: number,
201
- ): CompositeViewportRange {
173
+ function viewportFromPositions(frame: CompositeNavigationFrame, range: PositionRange): CompositeViewportRange {
202
174
  return {
203
- start: new Date(unprojectCompositeTimestamp(scale, startRatio)),
204
- end: new Date(unprojectCompositeTimestamp(scale, endRatio)),
175
+ start: new Date(Math.round(compositeTimeAtPosition(frame.scale, range.start))),
176
+ end: new Date(Math.round(compositeTimeAtPosition(frame.scale, range.end))),
205
177
  };
206
178
  }
207
179
 
180
+ function clampPositions(
181
+ frame: CompositeNavigationFrame,
182
+ range: PositionRange,
183
+ paddingRatio: number,
184
+ ): PositionRange {
185
+ const dataSpan = Math.max(frame.dataEndPosition - frame.dataStartPosition, 0);
186
+ const maximumSpan = Math.max(dataSpan * (1 + paddingRatio), frame.minimumSpanPositions);
187
+ const span = clamp(range.end - range.start, frame.minimumSpanPositions, maximumSpan);
188
+ let start = Math.max(range.start, frame.dataStartPosition - span * paddingRatio);
189
+ // The newest observation is the hard edge: nothing lies beyond it yet.
190
+ start = Math.min(start, frame.dataEndPosition - span);
191
+ return { start, end: start + span };
192
+ }
193
+
194
+ /**
195
+ * Fits an authored viewport to the loaded data, keeping its wall-clock span:
196
+ * a one-day window that ends after the close still shows one session. One
197
+ * that misses the data entirely is left alone so the time axis still says
198
+ * where the chart is and navigation can bring it back.
199
+ */
208
200
  export function clampCompositeViewport(
201
+ frame: CompositeNavigationFrame,
209
202
  viewport: CompositeViewportRange,
210
- bounds: CompositeViewportRange,
211
203
  ): CompositeViewportRange {
212
- const boundsStart = bounds.start.getTime();
213
- const boundsEnd = bounds.end.getTime();
214
- const boundsSpan = Math.max(boundsEnd - boundsStart, 1);
215
- const rawRequestedStart = viewport.start.getTime();
216
- const rawRequestedEnd = viewport.end.getTime();
217
- const hasValidViewport = Number.isFinite(rawRequestedStart)
218
- && Number.isFinite(rawRequestedEnd)
219
- && rawRequestedStart <= rawRequestedEnd;
220
- const requestedStart = hasValidViewport ? rawRequestedStart : boundsStart;
221
- const requestedEnd = hasValidViewport ? rawRequestedEnd : boundsEnd;
222
- const requestedSpan = Math.max(requestedEnd - requestedStart, 1);
223
- const span = Math.min(requestedSpan, boundsSpan);
224
- const start = clamp(requestedStart, boundsStart, boundsEnd - span);
225
- return {
226
- start: new Date(start),
227
- end: new Date(start + span),
228
- };
204
+ const data = compositeNavigationDataViewport(frame);
205
+ const startTime = finiteTime(viewport.start);
206
+ const endTime = finiteTime(viewport.end);
207
+ if (startTime === null || endTime === null || startTime > endTime) return data;
208
+ const overlapsData = endTime >= frame.dataStart && startTime <= frame.dataEnd;
209
+ if (!overlapsData) return viewport;
210
+ const dataStart = data.start.getTime();
211
+ const dataEnd = data.end.getTime();
212
+ const span = Math.min(endTime - startTime, dataEnd - dataStart);
213
+ const start = clamp(startTime, dataStart, dataEnd - span);
214
+ return start === startTime && start + span === endTime
215
+ ? viewport
216
+ : { start: new Date(start), end: new Date(start + span) };
229
217
  }
230
218
 
231
219
  export function sameCompositeViewport(
@@ -237,138 +225,100 @@ export function sameCompositeViewport(
237
225
  && left.end.getTime() === right.end.getTime();
238
226
  }
239
227
 
228
+ /**
229
+ * Positive deltas move toward older observations, matching the legacy chart:
230
+ * dragging right or scrolling up/left reveals earlier dates.
231
+ */
232
+ export function panCompositeViewport(
233
+ frame: CompositeNavigationFrame,
234
+ viewport: CompositeViewportRange,
235
+ deltaPositions: number,
236
+ ): CompositeViewportRange {
237
+ const positions = compositeViewportPositions(frame, viewport);
238
+ if (!positions) return compositeNavigationDataViewport(frame);
239
+ const shift = Number.isFinite(deltaPositions) ? deltaPositions : 0;
240
+ const clamped = clampPositions(
241
+ frame,
242
+ { start: positions.start - shift, end: positions.end - shift },
243
+ frame.historicalPaddingRatio,
244
+ );
245
+ return viewportFromPositions(frame, clamped);
246
+ }
247
+
240
248
  export function zoomCompositeViewport(
249
+ frame: CompositeNavigationFrame,
241
250
  viewport: CompositeViewportRange,
242
- bounds: CompositeViewportRange,
243
251
  zoomFactor: number,
244
252
  anchorRatio: number,
245
- minimumSpanMs: number,
246
- series?: ResolvedSeries[],
247
253
  ): CompositeViewportRange {
248
- const current = clampCompositeViewport(viewport, bounds);
249
- if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return current;
250
-
251
- const start = current.start.getTime();
252
- const end = current.end.getTime();
253
- const boundsSpan = Math.max(bounds.end.getTime() - bounds.start.getTime(), 1);
254
- const currentSpan = Math.max(end - start, 1);
254
+ const positions = compositeViewportPositions(frame, viewport);
255
+ if (!positions) return compositeNavigationDataViewport(frame);
256
+ if (!Number.isFinite(zoomFactor) || zoomFactor <= 0) return viewport;
257
+ const span = Math.max(positions.end - positions.start, frame.minimumSpanPositions);
255
258
  const ratio = clamp(anchorRatio, 0, 1);
256
- const marketProjection = marketViewportProjection(current, bounds, series);
257
- const candidate = marketProjection
258
- ? (() => {
259
- const {
260
- scale,
261
- startRatio,
262
- endRatio,
263
- } = marketProjection;
264
- const currentMarketSpan = endRatio - startRatio;
265
- const totalMarketPositions = Math.max(
266
- scale.endPosition - scale.startPosition,
267
- Number.EPSILON,
268
- );
269
- const minimumMarketSpan = clamp(
270
- Math.max(minimumSpanMs, 1) / scale.cadenceMs / totalMarketPositions,
271
- Number.EPSILON,
272
- 1,
273
- );
274
- const nextMarketSpan = clamp(
275
- currentMarketSpan / zoomFactor,
276
- minimumMarketSpan,
277
- 1,
278
- );
279
- const anchor = startRatio + currentMarketSpan * ratio;
280
- const candidateStart = clamp(
281
- anchor - nextMarketSpan * ratio,
282
- 0,
283
- 1 - nextMarketSpan,
284
- );
285
- return viewportFromMarketRatios(
286
- scale,
287
- candidateStart,
288
- candidateStart + nextMarketSpan,
289
- );
290
- })()
291
- : (() => {
292
- const nextSpan = clamp(
293
- currentSpan / zoomFactor,
294
- Math.min(Math.max(minimumSpanMs, 1), boundsSpan),
295
- boundsSpan,
296
- );
297
- const anchor = start + currentSpan * ratio;
298
- return clampCompositeViewport({
299
- start: new Date(anchor - nextSpan * ratio),
300
- end: new Date(anchor + nextSpan * (1 - ratio)),
301
- }, bounds);
302
- })();
303
- if (!series) return candidate;
304
-
305
- const timestamps = pointTimestamps(series)
306
- .filter((timestamp) => timestamp >= bounds.start.getTime() && timestamp <= bounds.end.getTime());
307
- if (timestamps.length < 2 || viewportHasTimestamps(timestamps, candidate, 2)) {
308
- return candidate;
309
- }
310
-
311
- const currentHasObservations = viewportHasTimestamps(timestamps, current, 2);
312
- const candidateSpan = candidate.end.getTime() - candidate.start.getTime();
313
- // An adaptive response can invalidate an existing viewport. Let zoom-out
314
- // keep expanding from that state until observations re-enter the window.
315
- if (!currentHasObservations && candidateSpan > currentSpan) return candidate;
316
- return current;
259
+ const anchor = positions.start + span * ratio;
260
+ const nextSpan = span / zoomFactor;
261
+ const clamped = clampPositions(
262
+ frame,
263
+ { start: anchor - nextSpan * ratio, end: anchor + nextSpan * (1 - ratio) },
264
+ frame.historicalPaddingRatio,
265
+ );
266
+ return viewportFromPositions(frame, clamped);
317
267
  }
318
268
 
319
- /**
320
- * Positive ratios move toward older observations, matching the legacy chart:
321
- * dragging right or scrolling up/left reveals earlier dates.
322
- */
323
- export function panCompositeViewport(
269
+ export function fitCompositeViewport(
270
+ frame: CompositeNavigationFrame,
324
271
  viewport: CompositeViewportRange,
325
- bounds: CompositeViewportRange,
326
- shiftRatio: number,
327
- series?: ResolvedSeries[],
328
- allowEmpty = false,
329
272
  ): CompositeViewportRange {
330
- const current = clampCompositeViewport(viewport, bounds);
331
- if (!Number.isFinite(shiftRatio) || shiftRatio === 0) return current;
332
- const marketProjection = marketViewportProjection(current, bounds, series);
333
- // Market scales collapse overnight and weekend gaps, so a wall-clock shift
334
- // changes how many slots stay visible and the plot accordions instead of
335
- // translating. Shift in slot space so the visible span is preserved.
336
- const candidate = marketProjection
337
- ? (() => {
338
- const { scale, startRatio, endRatio } = marketProjection;
339
- const marketSpan = endRatio - startRatio;
340
- const nextStart = clamp(startRatio - marketSpan * shiftRatio, 0, 1 - marketSpan);
341
- return clampCompositeViewport(
342
- viewportFromMarketRatios(scale, nextStart, nextStart + marketSpan),
343
- bounds,
344
- );
345
- })()
346
- : (() => {
347
- const shift = Math.max(current.end.getTime() - current.start.getTime(), 1) * shiftRatio;
348
- return clampCompositeViewport({
349
- start: new Date(current.start.getTime() - shift),
350
- end: new Date(current.end.getTime() - shift),
351
- }, bounds);
352
- })();
353
- if (!series || allowEmpty) return candidate;
273
+ const positions = compositeViewportPositions(frame, viewport);
274
+ if (!positions) return compositeNavigationDataViewport(frame);
275
+ return viewportFromPositions(frame, clampPositions(frame, positions, frame.historicalPaddingRatio));
276
+ }
354
277
 
355
- const timestamps = pointTimestamps(series)
356
- .filter((timestamp) => timestamp >= bounds.start.getTime() && timestamp <= bounds.end.getTime());
357
- return timestamps.length < 2 || viewportHasTimestamps(timestamps, candidate, 2)
358
- ? candidate
359
- : current;
278
+ export interface CompositeWheelGesture {
279
+ direction: "up" | "down" | "left" | "right";
280
+ delta?: number;
281
+ /** Signed pixel deltas when the host reports them; terminals report notches only. */
282
+ deltaX?: number;
283
+ deltaY?: number;
360
284
  }
361
285
 
362
- export function resolveCompositeWheelPanRatio(
363
- direction: "up" | "down" | "left" | "right",
364
- delta: number | undefined,
286
+ /**
287
+ * Pan distance for one wheel event as a fraction of the visible span. Pixel
288
+ * hosts move the plot exactly as far as the fingers travelled, so the sum of
289
+ * both axes pans and a diagonal swipe never flickers between them. Notch hosts
290
+ * step a bounded fraction per event.
291
+ */
292
+ export function resolveCompositeWheelPan(
293
+ gesture: CompositeWheelGesture,
294
+ plotPixelWidth: number,
365
295
  ): number {
366
- const rawMagnitude = Math.abs(delta ?? 1);
296
+ if (typeof gesture.deltaX === "number" && typeof gesture.deltaY === "number") {
297
+ const travel = gesture.deltaX + gesture.deltaY;
298
+ if (!Number.isFinite(travel) || plotPixelWidth <= 0) return 0;
299
+ return -travel / plotPixelWidth;
300
+ }
301
+ const rawMagnitude = Math.abs(gesture.delta ?? 1);
367
302
  const magnitude = clamp(rawMagnitude > 0 ? rawMagnitude : 1, 1, MAX_WHEEL_DELTA_MAGNITUDE);
368
- const directionSign = direction === "up" || direction === "left" ? 1 : -1;
303
+ const directionSign = gesture.direction === "up" || gesture.direction === "left" ? 1 : -1;
369
304
  return directionSign * magnitude * WHEEL_PAN_RATIO_PER_DELTA;
370
305
  }
371
306
 
307
+ /** Zoom factor for one modifier-wheel or pinch event; greater than one zooms in. */
308
+ export function resolveCompositeWheelZoom(gesture: CompositeWheelGesture): number {
309
+ if (typeof gesture.deltaY === "number") {
310
+ if (!Number.isFinite(gesture.deltaY) || gesture.deltaY === 0) return 1;
311
+ return clamp(
312
+ Math.exp(-gesture.deltaY / WHEEL_ZOOM_PIXELS_PER_E),
313
+ 1 / WHEEL_ZOOM_STEP_LIMIT,
314
+ WHEEL_ZOOM_STEP_LIMIT,
315
+ );
316
+ }
317
+ const magnitude = clamp(Math.abs(gesture.delta ?? 1), 1, MAX_WHEEL_DELTA_MAGNITUDE);
318
+ const step = 1 + magnitude * WHEEL_ZOOM_RATIO_PER_DELTA;
319
+ return gesture.direction === "up" ? step : 1 / step;
320
+ }
321
+
372
322
  export function shouldResetCompositeViewport(
373
323
  previous: CompositeViewportRange | null,
374
324
  next: CompositeViewportRange | null,
@@ -0,0 +1,21 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { buildCursorPriceAxisOverlay } from "./price-axis-labels";
3
+
4
+ describe("buildCursorPriceAxisOverlay", () => {
5
+ test("positions desktop cursor y-axis overlay from exact pixels", () => {
6
+ const height = 8;
7
+ const cellHeightPx = 18;
8
+ const cursorPixelY = 45.25;
9
+ const overlay = buildCursorPriceAxisOverlay({
10
+ axisWidth: 7,
11
+ axisSectionWidth: 8,
12
+ height,
13
+ cursorPixelY,
14
+ cursorLabel: "$214.03",
15
+ cellHeightPx,
16
+ });
17
+
18
+ expect(overlay.labelText).toBe("$214.03");
19
+ expect(overlay.topPercent).toBeCloseTo((cursorPixelY / (height * cellHeightPx - 1)) * 100, 5);
20
+ });
21
+ });
@@ -1,9 +1,14 @@
1
1
  import { useMemo } from "react";
2
- import { Box, Text, useUiCapabilities } from "../../ui";
3
- import { colors } from "../../theme/colors";
4
- import { formatAxisCell } from "./core/renderer";
2
+ import { Box, Text, useUiCapabilities } from "../../../ui";
3
+ import { colors } from "../../../theme/colors";
5
4
 
6
- export interface PriceAxisMarker {
5
+ function formatAxisCell(label: string | null, width: number): string {
6
+ if (width <= 0) return "";
7
+ if (!label) return " ".repeat(width);
8
+ return label.length >= width ? label.slice(0, width) : label.padStart(width);
9
+ }
10
+
11
+ interface PriceAxisMarker {
7
12
  row: number;
8
13
  pixelY: number;
9
14
  label: string;