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
@@ -0,0 +1,481 @@
1
+ import { createElement, useCallback, useEffect, useMemo, useRef, useState, type PointerEvent as ReactPointerEvent } from "react";
2
+ import type { CloudWorldVenuePayload } from "../../../api-client";
3
+ import { Box, ChartSurface, Text, useNativeRenderer, useUiHost } from "../../../ui";
4
+ import { useThemeColors } from "../../../theme/theme-context";
5
+ import { resolveNativeBitmapSize, shouldRenderNativeBitmap } from "../../../components/chart/native/bitmap-support";
6
+ import { drawCircle, drawLine, fillOpaque, parseHex } from "../../../components/chart/native/raster/primitives";
7
+ import type { NativeChartBitmap } from "../../../components/chart/native/chart-rasterizer";
8
+ import { getLocalPlotPointer, type ChartMouseEvent } from "../../../components/chart/core/pointer";
9
+ import {
10
+ closestWorldVenueCluster,
11
+ clusterWorldVenues,
12
+ DEFAULT_WORLD_MAP_VIEWPORT,
13
+ panWorldMapViewport,
14
+ projectWorldPoint,
15
+ zoomWorldMapViewport,
16
+ type WorldMapPoint,
17
+ type WorldMapViewport,
18
+ type WorldVenueCluster,
19
+ } from "./model";
20
+ import { WORLD_OUTLINES } from "./world-outlines";
21
+
22
+ interface WorldVenueMapProps {
23
+ venues: readonly CloudWorldVenuePayload[];
24
+ selectedMic: string | null;
25
+ width: number;
26
+ height: number;
27
+ onSelect: (venue: CloudWorldVenuePayload) => void;
28
+ }
29
+
30
+ function clusterVenue(cluster: WorldVenueCluster, selectedMic: string | null): CloudWorldVenuePayload {
31
+ return cluster.venues.find((venue) => venue.mic === selectedMic)
32
+ ?? cluster.venues.find((venue) => venue.isOpen)
33
+ ?? cluster.venues[0]!;
34
+ }
35
+
36
+ function isSelectedCluster(cluster: WorldVenueCluster, selectedMic: string | null): boolean {
37
+ return cluster.venues.some((venue) => venue.mic === selectedMic);
38
+ }
39
+
40
+ function drawWorldOutlines(
41
+ pixels: Uint8Array,
42
+ width: number,
43
+ height: number,
44
+ color: ReturnType<typeof parseHex>,
45
+ thickness: number,
46
+ ) {
47
+ for (const outline of WORLD_OUTLINES) {
48
+ for (let index = 1; index < outline.length; index += 1) {
49
+ const previous = outline[index - 1]!;
50
+ const current = outline[index]!;
51
+ if (previous[1] < -60 || current[1] < -60) continue;
52
+ const from = projectWorldPoint(previous[0], previous[1], width, height);
53
+ const to = projectWorldPoint(current[0], current[1], width, height);
54
+ if (Math.abs(to.x - from.x) > width / 2) continue;
55
+ drawLine(pixels, width, height, from.x, from.y, to.x, to.y, color, thickness);
56
+ }
57
+ }
58
+ }
59
+
60
+ const DIGITS: Record<string, readonly string[]> = {
61
+ "0": ["111", "101", "101", "101", "111"],
62
+ "1": ["010", "110", "010", "010", "111"],
63
+ "2": ["111", "001", "111", "100", "111"],
64
+ "3": ["111", "001", "111", "001", "111"],
65
+ "4": ["101", "101", "111", "001", "001"],
66
+ "5": ["111", "100", "111", "001", "111"],
67
+ "6": ["111", "100", "111", "101", "111"],
68
+ "7": ["111", "001", "010", "010", "010"],
69
+ "8": ["111", "101", "111", "101", "111"],
70
+ "9": ["111", "101", "111", "001", "111"],
71
+ };
72
+
73
+ function drawClusterCount(
74
+ bitmap: NativeChartBitmap,
75
+ x: number,
76
+ y: number,
77
+ count: number,
78
+ color: ReturnType<typeof parseHex>,
79
+ scale: number,
80
+ ) {
81
+ const value = String(count);
82
+ const glyphWidth = 3 * scale;
83
+ const gap = scale;
84
+ const totalWidth = value.length * glyphWidth + (value.length - 1) * gap;
85
+ const left = Math.round(x - (totalWidth - 1) / 2);
86
+ const top = Math.round(y - (5 * scale - 1) / 2);
87
+ for (let digitIndex = 0; digitIndex < value.length; digitIndex += 1) {
88
+ const glyph = DIGITS[value[digitIndex]!]!;
89
+ for (let row = 0; row < glyph.length; row += 1) {
90
+ for (let column = 0; column < 3; column += 1) {
91
+ if (glyph[row]![column] !== "1") continue;
92
+ const x0 = left + digitIndex * (glyphWidth + gap) + column * scale;
93
+ const y0 = top + row * scale;
94
+ for (let dy = 0; dy < scale; dy += 1) {
95
+ for (let dx = 0; dx < scale; dx += 1) {
96
+ const pixelX = x0 + dx;
97
+ const pixelY = y0 + dy;
98
+ if (pixelX < 0 || pixelY < 0 || pixelX >= bitmap.width || pixelY >= bitmap.height) continue;
99
+ const offset = (pixelY * bitmap.width + pixelX) * 4;
100
+ bitmap.pixels[offset] = color.r;
101
+ bitmap.pixels[offset + 1] = color.g;
102
+ bitmap.pixels[offset + 2] = color.b;
103
+ bitmap.pixels[offset + 3] = color.a;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ function renderWorldBitmap(
112
+ venues: readonly CloudWorldVenuePayload[],
113
+ selectedMic: string | null,
114
+ width: number,
115
+ height: number,
116
+ colors: ReturnType<typeof useThemeColors>,
117
+ ): NativeChartBitmap {
118
+ const bitmap = { width, height, pixels: new Uint8Array(width * height * 4) };
119
+ fillOpaque(bitmap.pixels, parseHex(colors.bg));
120
+ drawWorldOutlines(bitmap.pixels, width, height, parseHex(colors.textDim, 0.62), Math.max(1, width / 900));
121
+
122
+ const clusters = clusterWorldVenues(venues, width, height);
123
+ for (const cluster of clusters) {
124
+ const selected = isSelectedCluster(cluster, selectedMic);
125
+ const radius = Math.max(3, Math.min(14, 3 + Math.sqrt(cluster.venues.length) * 1.7));
126
+ if (selected) {
127
+ drawCircle(bitmap.pixels, width, height, cluster.x, cluster.y, radius + 2.5, parseHex(colors.selectedText));
128
+ }
129
+ drawCircle(
130
+ bitmap.pixels,
131
+ width,
132
+ height,
133
+ cluster.x,
134
+ cluster.y,
135
+ radius,
136
+ parseHex(cluster.isOpen ? colors.positive : colors.textMuted),
137
+ );
138
+ if (cluster.venues.length > 1) {
139
+ drawClusterCount(bitmap, cluster.x, cluster.y, cluster.venues.length, parseHex(colors.bg), width >= 700 ? 2 : 1);
140
+ }
141
+ }
142
+ return bitmap;
143
+ }
144
+
145
+ function renderAsciiMap(
146
+ venues: readonly CloudWorldVenuePayload[],
147
+ selectedMic: string | null,
148
+ width: number,
149
+ height: number,
150
+ cellAspect: number,
151
+ ): string[] {
152
+ const grid = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
153
+ for (const outline of WORLD_OUTLINES) {
154
+ for (const coordinate of outline) {
155
+ const point = projectWorldPoint(coordinate[0], coordinate[1], width, height, cellAspect);
156
+ const x = Math.round(point.x);
157
+ const y = Math.round(point.y);
158
+ if (grid[y]?.[x] === " ") grid[y]![x] = ".";
159
+ }
160
+ }
161
+ for (const cluster of clusterWorldVenues(venues, width, height, cellAspect)) {
162
+ const x = Math.round(cluster.x);
163
+ const y = Math.round(cluster.y);
164
+ grid[y]![x] = isSelectedCluster(cluster, selectedMic)
165
+ ? "@"
166
+ : cluster.venues.length > 1
167
+ ? String(Math.min(cluster.venues.length, 9))
168
+ : cluster.isOpen ? "O" : "o";
169
+ }
170
+ return grid.map((row) => row.join(""));
171
+ }
172
+
173
+ function TerminalWorldVenueMap(props: WorldVenueMapProps) {
174
+ const colors = useThemeColors();
175
+ const renderer = useNativeRenderer();
176
+ const surfaceRef = useRef<any>(null);
177
+ const { nativeCharts, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiHost().capabilities ?? {};
178
+ const rendererCapabilities = renderer.capabilities;
179
+ const rendererResolution = renderer.resolution;
180
+ const rendererTerminalWidth = renderer.terminalWidth;
181
+ const rendererTerminalHeight = renderer.terminalHeight;
182
+ const bitmap = useMemo(() => {
183
+ if (!shouldRenderNativeBitmap(renderer, nativeCharts === true)) return null;
184
+ const size = resolveNativeBitmapSize({
185
+ width: props.width,
186
+ height: props.height,
187
+ resolution: rendererResolution,
188
+ terminalWidth: rendererTerminalWidth,
189
+ terminalHeight: rendererTerminalHeight,
190
+ cellWidthPx,
191
+ cellHeightPx,
192
+ pixelRatio,
193
+ });
194
+ return renderWorldBitmap(props.venues, props.selectedMic, size.pixelWidth, size.pixelHeight, colors);
195
+ }, [
196
+ cellHeightPx,
197
+ cellWidthPx,
198
+ colors,
199
+ nativeCharts,
200
+ pixelRatio,
201
+ props.height,
202
+ props.selectedMic,
203
+ props.venues,
204
+ props.width,
205
+ renderer,
206
+ rendererCapabilities,
207
+ rendererResolution,
208
+ rendererTerminalHeight,
209
+ rendererTerminalWidth,
210
+ ]);
211
+ const cellAspect = cellHeightPx / Math.max(cellWidthPx, 1);
212
+ const clusters = useMemo(
213
+ () => clusterWorldVenues(props.venues, props.width, props.height, cellAspect),
214
+ [cellAspect, props.height, props.venues, props.width],
215
+ );
216
+ const ascii = useMemo(
217
+ () => renderAsciiMap(props.venues, props.selectedMic, props.width, props.height, cellAspect),
218
+ [cellAspect, props.height, props.selectedMic, props.venues, props.width],
219
+ );
220
+
221
+ const selectAt = (event: ChartMouseEvent) => {
222
+ const pointer = getLocalPlotPointer(event, surfaceRef.current, renderer);
223
+ if (!pointer) return;
224
+ const cluster = closestWorldVenueCluster(clusters, pointer.cellX, pointer.cellY, 4);
225
+ if (cluster) props.onSelect(clusterVenue(cluster, props.selectedMic));
226
+ };
227
+
228
+ return (
229
+ <ChartSurface
230
+ ref={surfaceRef}
231
+ width={props.width}
232
+ height={props.height}
233
+ flexDirection="column"
234
+ bitmaps={bitmap ? [bitmap] : null}
235
+ onMouseDown={selectAt}
236
+ data-gloom-role="world-venue-map"
237
+ aria-label="World venue map"
238
+ >
239
+ {ascii.map((line, index) => <Text key={index} fg={colors.textDim}>{line}</Text>)}
240
+ </ChartSurface>
241
+ );
242
+ }
243
+
244
+ const DESKTOP_MAP_ASPECT = 2.12;
245
+ const MAP_PAN_THRESHOLD_PX = 4;
246
+ const MAP_CLICK_HIT_PX = 14;
247
+ const MAP_DOUBLE_CLICK_ZOOM = 1.8;
248
+
249
+ function wheelZoomFactor(event: WheelEvent): number {
250
+ const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
251
+ return Math.exp(-delta * 0.002);
252
+ }
253
+
254
+ function clientToMapPoint(
255
+ event: { clientX: number; clientY: number },
256
+ element: HTMLElement,
257
+ width: number,
258
+ height: number,
259
+ ): WorldMapPoint {
260
+ const rect = element.getBoundingClientRect();
261
+ return {
262
+ x: rect.width <= 0 ? 0 : ((event.clientX - rect.left) / rect.width) * width,
263
+ y: rect.height <= 0 ? 0 : ((event.clientY - rect.top) / rect.height) * height,
264
+ };
265
+ }
266
+
267
+ function clientDeltaToMapDelta(
268
+ deltaX: number,
269
+ deltaY: number,
270
+ element: HTMLElement,
271
+ width: number,
272
+ height: number,
273
+ ): WorldMapPoint {
274
+ const rect = element.getBoundingClientRect();
275
+ return {
276
+ x: rect.width <= 0 ? 0 : (deltaX / rect.width) * width,
277
+ y: rect.height <= 0 ? 0 : (deltaY / rect.height) * height,
278
+ };
279
+ }
280
+
281
+ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
282
+ const colors = useThemeColors();
283
+ const surfaceRef = useRef<HTMLDivElement | null>(null);
284
+ const [viewport, setViewport] = useState<WorldMapViewport>(DEFAULT_WORLD_MAP_VIEWPORT);
285
+ const viewportRef = useRef(viewport);
286
+ viewportRef.current = viewport;
287
+ const [dragging, setDragging] = useState(false);
288
+ const dragRef = useRef<{
289
+ pointerId: number;
290
+ lastX: number;
291
+ lastY: number;
292
+ originX: number;
293
+ originY: number;
294
+ moved: boolean;
295
+ } | null>(null);
296
+ // Desktop rows are roughly twice as tall as they are wide. Matching the SVG
297
+ // view box to those pixels lets the contained world projection keep its shape.
298
+ const plotHeight = props.height * DESKTOP_MAP_ASPECT;
299
+ const zoomed = viewport.zoom > 1;
300
+ const clusters = useMemo(
301
+ () => clusterWorldVenues(props.venues, props.width, plotHeight, 1, viewport),
302
+ [plotHeight, props.venues, props.width, viewport],
303
+ );
304
+ const outlinePaths = useMemo(() => WORLD_OUTLINES.map((outline) => {
305
+ let drawing = false;
306
+ let previousLongitude: number | null = null;
307
+ return outline.map(([longitude, latitude]) => {
308
+ if (latitude < -60 || (previousLongitude != null && Math.abs(longitude - previousLongitude) > 180)) {
309
+ drawing = false;
310
+ previousLongitude = longitude;
311
+ return "";
312
+ }
313
+ const point = projectWorldPoint(longitude, latitude, props.width, plotHeight, 1, viewport);
314
+ previousLongitude = longitude;
315
+ const command = drawing ? "L" : "M";
316
+ drawing = true;
317
+ return `${command}${point.x.toFixed(2)} ${point.y.toFixed(2)}`;
318
+ }).join(" ");
319
+ }), [plotHeight, props.width, viewport]);
320
+
321
+ const selectAt = useCallback((point: WorldMapPoint, element: HTMLElement) => {
322
+ const rect = element.getBoundingClientRect();
323
+ const hit = rect.height <= 0 ? 2.4 : (MAP_CLICK_HIT_PX / rect.height) * plotHeight;
324
+ const cluster = closestWorldVenueCluster(clusters, point.x, point.y, Math.max(1.6, hit));
325
+ if (cluster) props.onSelect(clusterVenue(cluster, props.selectedMic));
326
+ }, [clusters, plotHeight, props]);
327
+
328
+ useEffect(() => {
329
+ const element = surfaceRef.current;
330
+ if (!element) return;
331
+ const onWheel = (event: WheelEvent) => {
332
+ event.preventDefault();
333
+ event.stopPropagation();
334
+ const point = clientToMapPoint(event, element, props.width, plotHeight);
335
+ const factor = wheelZoomFactor(event);
336
+ setViewport((current) => zoomWorldMapViewport(current, props.width, plotHeight, point, factor));
337
+ };
338
+ element.addEventListener("wheel", onWheel, { passive: false });
339
+ return () => element.removeEventListener("wheel", onWheel);
340
+ }, [plotHeight, props.width]);
341
+
342
+ const endDrag = (event: ReactPointerEvent<HTMLDivElement>) => {
343
+ const drag = dragRef.current;
344
+ if (!drag || drag.pointerId !== event.pointerId) return;
345
+ dragRef.current = null;
346
+ setDragging(false);
347
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
348
+ event.currentTarget.releasePointerCapture(event.pointerId);
349
+ }
350
+ if (!drag.moved) selectAt(clientToMapPoint(event, event.currentTarget, props.width, plotHeight), event.currentTarget);
351
+ };
352
+
353
+ return (
354
+ <Box width={props.width} height={props.height} overflow="hidden">
355
+ <div
356
+ ref={surfaceRef}
357
+ data-gloom-role="world-venue-map"
358
+ data-zoomed={zoomed ? "true" : "false"}
359
+ role="application"
360
+ aria-label="World venue map"
361
+ onPointerDown={(event) => {
362
+ if (event.button !== 0) return;
363
+ event.stopPropagation();
364
+ dragRef.current = {
365
+ pointerId: event.pointerId,
366
+ lastX: event.clientX,
367
+ lastY: event.clientY,
368
+ originX: event.clientX,
369
+ originY: event.clientY,
370
+ moved: false,
371
+ };
372
+ event.currentTarget.setPointerCapture(event.pointerId);
373
+ }}
374
+ onPointerMove={(event) => {
375
+ const drag = dragRef.current;
376
+ if (!drag || drag.pointerId !== event.pointerId) return;
377
+ const travel = Math.hypot(event.clientX - drag.originX, event.clientY - drag.originY);
378
+ if (!drag.moved && travel < MAP_PAN_THRESHOLD_PX) return;
379
+ if (viewportRef.current.zoom <= 1) return;
380
+ drag.moved = true;
381
+ const delta = clientDeltaToMapDelta(
382
+ event.clientX - drag.lastX,
383
+ event.clientY - drag.lastY,
384
+ event.currentTarget,
385
+ props.width,
386
+ plotHeight,
387
+ );
388
+ drag.lastX = event.clientX;
389
+ drag.lastY = event.clientY;
390
+ setDragging(true);
391
+ setViewport((current) => panWorldMapViewport(current, props.width, plotHeight, delta.x, delta.y));
392
+ }}
393
+ onPointerUp={endDrag}
394
+ onPointerCancel={endDrag}
395
+ onDoubleClick={(event) => {
396
+ const point = clientToMapPoint(event, event.currentTarget, props.width, plotHeight);
397
+ setViewport((current) => zoomWorldMapViewport(current, props.width, plotHeight, point, MAP_DOUBLE_CLICK_ZOOM));
398
+ }}
399
+ style={{
400
+ position: "relative",
401
+ width: "100%",
402
+ height: "100%",
403
+ flex: 1,
404
+ minHeight: 0,
405
+ overflow: "hidden",
406
+ touchAction: "none",
407
+ userSelect: "none",
408
+ cursor: dragging ? "grabbing" : zoomed ? "grab" : "pointer",
409
+ background: colors.bg,
410
+ }}
411
+ >
412
+ <svg
413
+ viewBox={`0 0 ${props.width} ${plotHeight}`}
414
+ width="100%"
415
+ height="100%"
416
+ aria-hidden="true"
417
+ style={{ display: "block", background: colors.bg, pointerEvents: "none" }}
418
+ >
419
+ {outlinePaths.map((path, index) => (
420
+ <path
421
+ key={index}
422
+ d={path}
423
+ fill="none"
424
+ stroke={colors.textDim}
425
+ strokeOpacity="0.64"
426
+ strokeWidth="0.55"
427
+ vectorEffect="non-scaling-stroke"
428
+ />
429
+ ))}
430
+ {clusters.map((cluster) => {
431
+ const selected = isSelectedCluster(cluster, props.selectedMic);
432
+ const venue = clusterVenue(cluster, props.selectedMic);
433
+ const radius = Math.max(0.55, Math.min(1.5, 0.45 + Math.sqrt(cluster.venues.length) * 0.22));
434
+ return (
435
+ <g
436
+ key={cluster.id}
437
+ role="button"
438
+ tabIndex={0}
439
+ aria-label={`${cluster.venues.length} venues near ${venue.city}`}
440
+ onKeyDown={(event) => {
441
+ if (event.key === "Enter" || event.key === " ") {
442
+ event.preventDefault();
443
+ props.onSelect(venue);
444
+ }
445
+ }}
446
+ >
447
+ <title>{`${cluster.venues.length} venue${cluster.venues.length === 1 ? "" : "s"} near ${venue.city}: ${cluster.venues.map((item) => `${item.mic} ${item.name}`).join(", ")}`}</title>
448
+ <circle
449
+ cx={cluster.x}
450
+ cy={cluster.y}
451
+ r={radius + (selected ? 0.24 : 0)}
452
+ fill={cluster.isOpen ? colors.positive : colors.textMuted}
453
+ stroke={selected ? colors.selectedText : colors.bg}
454
+ strokeWidth={selected ? 0.35 : 0.16}
455
+ vectorEffect="non-scaling-stroke"
456
+ />
457
+ {cluster.venues.length > 1 ? createElement("text", {
458
+ x: cluster.x,
459
+ y: cluster.y,
460
+ fill: colors.bg,
461
+ dy: "0.34em",
462
+ fontSize: Math.max(0.62, Math.min(0.95, radius * 0.78)),
463
+ fontWeight: "700",
464
+ fontFamily: "inherit",
465
+ textAnchor: "middle",
466
+ pointerEvents: "none",
467
+ }, cluster.venues.length) : null}
468
+ </g>
469
+ );
470
+ })}
471
+ </svg>
472
+ </div>
473
+ </Box>
474
+ );
475
+ }
476
+
477
+ export function WorldVenueMap(props: WorldVenueMapProps) {
478
+ return useUiHost().kind === "desktop-web"
479
+ ? <DesktopWorldVenueMap {...props} />
480
+ : <TerminalWorldVenueMap {...props} />;
481
+ }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { CloudWorldVenuePayload } from "../../../api-client";
3
+ import {
4
+ clusterWorldVenues,
5
+ DEFAULT_WORLD_MAP_VIEWPORT,
6
+ filterWorldVenues,
7
+ panWorldMapViewport,
8
+ projectWorldPoint,
9
+ unprojectWorldPoint,
10
+ venueRemainingSeconds,
11
+ zoomWorldMapViewport,
12
+ } from "./model";
13
+
14
+ function venue(overrides: Partial<CloudWorldVenuePayload>): CloudWorldVenuePayload {
15
+ return {
16
+ mic: "XNYS",
17
+ name: "NYSE",
18
+ title: "New York Stock Exchange",
19
+ country: "United States",
20
+ countryCode: "US",
21
+ city: "New York",
22
+ timezone: "America/New_York",
23
+ latitude: 40.7127,
24
+ longitude: -74.006,
25
+ isOpen: false,
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ describe("world venue map model", () => {
31
+ test("projects, clusters, filters, and advances server countdowns", () => {
32
+ expect(projectWorldPoint(-180, 85, 361, 146)).toEqual({ x: 0, y: 0 });
33
+ expect(projectWorldPoint(180, -60, 361, 146)).toEqual({ x: 360, y: 145 });
34
+
35
+ const tallTopLeft = projectWorldPoint(-180, 85, 101, 101);
36
+ const tallBottomRight = projectWorldPoint(180, -60, 101, 101);
37
+ expect(tallTopLeft.x).toBe(0);
38
+ expect(tallBottomRight.x).toBe(100);
39
+ expect(100 / (tallBottomRight.y - tallTopLeft.y)).toBeCloseTo(360 / 145);
40
+
41
+ const terminalTopLeft = projectWorldPoint(-180, 85, 101, 51, 2);
42
+ const terminalBottomRight = projectWorldPoint(180, -60, 101, 51, 2);
43
+ expect(100 / ((terminalBottomRight.y - terminalTopLeft.y) * 2)).toBeCloseTo(360 / 145);
44
+
45
+ const rows = [
46
+ venue({ mic: "XNYS", timeToOpenSeconds: 3_600 }),
47
+ venue({ mic: "XASE", name: "NYSE American", isOpen: true, timeToCloseSeconds: 7_200 }),
48
+ venue({ mic: "XLON", name: "London Stock Exchange", title: "London Stock Exchange", city: "London", country: "United Kingdom", countryCode: "GB", longitude: -0.1278, latitude: 51.5074 }),
49
+ ];
50
+
51
+ const clusters = clusterWorldVenues(rows, 80, 24);
52
+ expect(clusters.find((cluster) => cluster.venues.some((item) => item.mic === "XNYS"))?.venues).toHaveLength(2);
53
+ expect(filterWorldVenues(rows, "london").map((item) => item.mic)).toEqual(["XLON"]);
54
+ expect(filterWorldVenues(rows, "")[0]?.mic).toBe("XASE");
55
+ expect(venueRemainingSeconds(rows[0]!, 1_000, 61_000)).toBe(3_540);
56
+ });
57
+
58
+ test("keeps a cursor point anchored while zooming and splits nearby clusters", () => {
59
+ const width = 361;
60
+ const height = 146;
61
+ const cursor = { x: 90, y: 40 };
62
+ const geographic = unprojectWorldPoint(cursor.x, cursor.y, width, height);
63
+ const zoomed = zoomWorldMapViewport(DEFAULT_WORLD_MAP_VIEWPORT, width, height, cursor, 4);
64
+ expect(zoomed.zoom).toBe(4);
65
+ const stillUnderCursor = projectWorldPoint(geographic.longitude, geographic.latitude, width, height, 1, zoomed);
66
+ expect(stillUnderCursor.x).toBeCloseTo(cursor.x, 6);
67
+ expect(stillUnderCursor.y).toBeCloseTo(cursor.y, 6);
68
+
69
+ const nearby = [
70
+ venue({ mic: "WEST", longitude: -20, latitude: 20 }),
71
+ venue({ mic: "EAST", longitude: -18, latitude: 20 }),
72
+ ];
73
+ expect(clusterWorldVenues(nearby, 80, 24)).toHaveLength(1);
74
+ expect(clusterWorldVenues(nearby, 80, 24, 1, {
75
+ zoom: 10,
76
+ centerLongitude: 0,
77
+ centerLatitude: DEFAULT_WORLD_MAP_VIEWPORT.centerLatitude,
78
+ })).toHaveLength(2);
79
+
80
+ const panned = panWorldMapViewport(zoomed, width, height, 40, 0);
81
+ expect(panned.centerLongitude).toBeLessThan(zoomed.centerLongitude);
82
+ expect(panWorldMapViewport(DEFAULT_WORLD_MAP_VIEWPORT, width, height, 40, 0)).toEqual(DEFAULT_WORLD_MAP_VIEWPORT);
83
+ expect(zoomWorldMapViewport(zoomed, width, height, cursor, 0.01)).toEqual(DEFAULT_WORLD_MAP_VIEWPORT);
84
+ });
85
+ });