gloomberb 0.16.2 → 0.16.4

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 (527) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/account-cache.ts +45 -0
  30. package/src/brokers/signed-in/adapter.ts +33 -2
  31. package/src/brokers/signed-in/client.ts +10 -0
  32. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  33. package/src/brokers/signed-in/sign-in.ts +62 -5
  34. package/src/capabilities/registry.ts +8 -1
  35. package/src/capabilities/types.ts +2 -0
  36. package/src/cli/command-options.ts +74 -0
  37. package/src/cli/commands/command-utils.ts +29 -0
  38. package/src/cli/commands/crypto-hints.ts +12 -4
  39. package/src/cli/commands/market.ts +524 -148
  40. package/src/cli/commands/options-chain.ts +172 -0
  41. package/src/cli/commands/options-leaps.ts +422 -0
  42. package/src/cli/commands/overview.ts +223 -42
  43. package/src/cli/commands/search.ts +29 -8
  44. package/src/cli/commands/system.ts +99 -19
  45. package/src/cli/commands/themes.ts +64 -0
  46. package/src/cli/commands/ticker.ts +200 -73
  47. package/src/cli/desktop-pane-shot.ts +100 -16
  48. package/src/cli/errors.ts +25 -2
  49. package/src/cli/freshness.ts +7 -4
  50. package/src/cli/fx-pair.ts +57 -0
  51. package/src/cli/help.ts +40 -7
  52. package/src/cli/history-rows.ts +187 -15
  53. package/src/cli/index-roots.ts +51 -0
  54. package/src/cli/index.ts +21 -13
  55. package/src/cli/listing-arg.ts +211 -3
  56. package/src/cli/not-a-ticker.ts +37 -0
  57. package/src/cli/options.ts +55 -7
  58. package/src/cli/pane-functions/capabilities.ts +13 -2
  59. package/src/cli/pane-functions/catalog.ts +13 -2
  60. package/src/cli/pane-functions/data.ts +36 -5
  61. package/src/cli/pane-functions/dom.ts +89 -28
  62. package/src/cli/pane-functions/freshness.ts +205 -26
  63. package/src/cli/pane-functions/function-help.ts +71 -24
  64. package/src/cli/pane-functions/headless.ts +168 -35
  65. package/src/cli/pane-functions/index.ts +12 -2
  66. package/src/cli/pane-functions/market-session.ts +133 -0
  67. package/src/cli/pane-functions/options.ts +15 -4
  68. package/src/cli/pane-functions/report-notices.ts +63 -0
  69. package/src/cli/pane-functions/report.ts +12 -1
  70. package/src/cli/pane-functions/resolver.ts +32 -8
  71. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  72. package/src/cli/pane-functions/screenshot.ts +109 -23
  73. package/src/cli/registry.ts +8 -4
  74. package/src/cli/report-tables.ts +460 -0
  75. package/src/cli/result.ts +63 -16
  76. package/src/cli/row-window.ts +76 -0
  77. package/src/cli/scroll-truncation.ts +34 -0
  78. package/src/components/chart/composite/composite-chart.tsx +43 -4
  79. package/src/components/chart/composite/legend.tsx +39 -6
  80. package/src/components/chart/composite/rasterizer.ts +64 -4
  81. package/src/components/chart/composite/series-cues.ts +71 -0
  82. package/src/components/chart/composite/text-renderer.ts +16 -8
  83. package/src/components/chart/composite/types.ts +3 -0
  84. package/src/components/command-bar/commands/direct/index.ts +9 -1
  85. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  86. package/src/components/command-bar/commands/registry.ts +7 -0
  87. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  88. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  89. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  90. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  91. package/src/components/command-bar/routes/root/results.ts +40 -10
  92. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  93. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  94. package/src/components/command-bar/surface/index.tsx +18 -4
  95. package/src/components/command-bar/theme-picker.tsx +48 -13
  96. package/src/components/command-bar/workflow/ops.ts +5 -3
  97. package/src/components/data-table/export.ts +6 -51
  98. package/src/components/data-table/view.tsx +31 -10
  99. package/src/components/layout/floating-pane.tsx +6 -2
  100. package/src/components/layout/market-summary.ts +23 -9
  101. package/src/components/layout/pane/content.tsx +46 -29
  102. package/src/components/layout/pane/cycle-order.ts +2 -11
  103. package/src/components/layout/pane/header.tsx +2 -1
  104. package/src/components/layout/pane/title.ts +1 -1
  105. package/src/components/layout/presentation.ts +22 -0
  106. package/src/components/layout/shell/active-drag.ts +67 -35
  107. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  108. package/src/components/layout/shell/drag/index.ts +16 -7
  109. package/src/components/layout/shell/drag/live.ts +193 -0
  110. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  111. package/src/components/layout/shell/drag/runtime.ts +62 -50
  112. package/src/components/layout/shell/drag/slide.ts +10 -0
  113. package/src/components/layout/shell/index.tsx +84 -57
  114. package/src/components/layout/shell/layout-state.ts +5 -11
  115. package/src/components/layout/shell/menu.ts +28 -1
  116. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  117. package/src/components/layout/shell/native/surfaces.ts +30 -14
  118. package/src/components/layout/shell/pane/actions.ts +2 -1
  119. package/src/components/layout/shell/pane/layers.tsx +461 -180
  120. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  121. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  122. package/src/components/layout/shell/shortcuts.ts +5 -2
  123. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  124. package/src/components/layout/shell/visible-layout.ts +44 -1
  125. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  126. package/src/components/layout/status-bar.tsx +4 -1
  127. package/src/components/markdown-text.tsx +9 -5
  128. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  129. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  130. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  131. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  132. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  133. package/src/components/onboarding/pro-step-layout.ts +86 -0
  134. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  135. package/src/components/ui/button.tsx +10 -0
  136. package/src/components/ui/data-table/index.tsx +38 -0
  137. package/src/components/ui/data-table/types.ts +8 -1
  138. package/src/components/ui/data-table/visible-range.ts +2 -0
  139. package/src/components/ui/icon.tsx +11 -2
  140. package/src/components/ui/index.ts +1 -1
  141. package/src/core/connection-health.ts +1 -1
  142. package/src/core/state/app/layout-reducer.ts +89 -3
  143. package/src/core/state/app/state.ts +8 -5
  144. package/src/core/state/app/types.ts +13 -2
  145. package/src/data/config/layout.ts +55 -21
  146. package/src/data/config/store/fold-panes.ts +149 -0
  147. package/src/data/config/store/migrations.ts +7 -0
  148. package/src/data/config/store/node.ts +27 -6
  149. package/src/data/config/store/normalize.ts +37 -3
  150. package/src/data/json-persistence.ts +17 -3
  151. package/src/data/json-storage.ts +2 -0
  152. package/src/data/memory-resource-store.ts +46 -0
  153. package/src/i18n/es.ts +14 -1
  154. package/src/i18n/ja.ts +14 -1
  155. package/src/i18n/ko.ts +14 -1
  156. package/src/i18n/zh-cn.ts +14 -1
  157. package/src/i18n/zh-tw.ts +14 -1
  158. package/src/layout/desks.ts +1 -2
  159. package/src/layout/pane-layout-move.ts +264 -0
  160. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  161. package/src/layout/pane-manager/index.ts +1 -0
  162. package/src/market-data/coordinator/entries.ts +7 -1
  163. package/src/market-data/coordinator/quotes.ts +2 -2
  164. package/src/market-data/market/freshness.ts +156 -11
  165. package/src/market-data/market/status.ts +40 -0
  166. package/src/market-data/options-alternatives.ts +69 -0
  167. package/src/market-data/quotes/contributions.ts +19 -2
  168. package/src/market-data/quotes/freshness.ts +5 -0
  169. package/src/market-data/quotes/metadata.ts +16 -0
  170. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  171. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  172. package/src/plugins/builtin/account-management/model.ts +1 -0
  173. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  174. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  175. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  176. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  177. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  178. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  179. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  180. package/src/plugins/builtin/attention/pane.tsx +1 -1
  181. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  182. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  183. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  184. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  185. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  186. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  187. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  188. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  189. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  190. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  191. package/src/plugins/builtin/chat/channels.ts +34 -36
  192. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  193. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  194. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  195. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  196. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  197. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  198. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  199. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  200. package/src/plugins/builtin/chat/conversations.ts +256 -0
  201. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  202. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  203. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  204. package/src/plugins/builtin/chat/index.tsx +40 -2
  205. package/src/plugins/builtin/chat/layout.ts +2 -0
  206. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  207. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  208. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  209. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  210. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  211. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  212. package/src/plugins/builtin/chat/pane.tsx +8 -0
  213. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  214. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  215. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  216. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  217. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  218. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  219. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  220. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  221. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  222. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  223. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  224. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  225. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  226. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  227. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  228. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  229. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  230. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  231. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  232. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  233. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  234. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  235. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  236. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  237. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  238. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  239. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  240. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  241. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  242. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  243. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  244. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  245. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  246. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  247. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  248. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  249. package/src/plugins/builtin/composite-plugins.ts +107 -64
  250. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  251. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  252. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  253. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  254. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  255. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  256. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  257. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  258. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  259. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  260. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  261. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  262. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  263. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  264. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  265. package/src/plugins/builtin/econ/index.tsx +68 -22
  266. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  267. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  268. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  269. package/src/plugins/builtin/executives/data.ts +17 -6
  270. package/src/plugins/builtin/executives/pane.tsx +16 -7
  271. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  272. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  273. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  274. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  275. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  276. package/src/plugins/builtin/gpu/headless.ts +9 -3
  277. package/src/plugins/builtin/gpu/model.ts +14 -5
  278. package/src/plugins/builtin/help/index.tsx +1 -0
  279. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  280. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  281. package/src/plugins/builtin/holders/client.ts +3 -6
  282. package/src/plugins/builtin/holders/format.ts +31 -1
  283. package/src/plugins/builtin/holders/headless.ts +32 -6
  284. package/src/plugins/builtin/holders/pane.tsx +25 -13
  285. package/src/plugins/builtin/holders/report-header.ts +46 -5
  286. package/src/plugins/builtin/insider/index.tsx +4 -4
  287. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  288. package/src/plugins/builtin/iv-history/client.ts +12 -2
  289. package/src/plugins/builtin/iv-history/format.ts +33 -2
  290. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  291. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  292. package/src/plugins/builtin/iv-history/model.ts +61 -13
  293. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  294. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  295. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  296. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  297. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  298. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  299. package/src/plugins/builtin/kelly-sizer/pane.tsx +121 -45
  300. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  301. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  302. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  303. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  304. package/src/plugins/builtin/macro-day/client.ts +24 -0
  305. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  306. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  307. package/src/plugins/builtin/macro-day/model.ts +11 -6
  308. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  309. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  310. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  311. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  312. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  313. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  314. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  315. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  316. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  317. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  318. package/src/plugins/builtin/market-movers/model.ts +71 -17
  319. package/src/plugins/builtin/market-movers/table.ts +50 -23
  320. package/src/plugins/builtin/members/pane.tsx +10 -0
  321. package/src/plugins/builtin/news/headless.ts +9 -2
  322. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  323. package/src/plugins/builtin/openfda/client.ts +1 -1
  324. package/src/plugins/builtin/options/carry.ts +95 -0
  325. package/src/plugins/builtin/options/index.tsx +58 -5
  326. package/src/plugins/builtin/options/strike-window.ts +183 -0
  327. package/src/plugins/builtin/options/table.ts +55 -3
  328. package/src/plugins/builtin/options/types.ts +8 -1
  329. package/src/plugins/builtin/options/view.tsx +161 -12
  330. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  331. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  332. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  333. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  334. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  335. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  336. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  337. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  338. package/src/plugins/builtin/pe-band/model.ts +2 -2
  339. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  340. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  341. package/src/plugins/builtin/perps/model.ts +16 -5
  342. package/src/plugins/builtin/perps/pane.tsx +1 -1
  343. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  344. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  345. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  346. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  347. package/src/plugins/builtin/plugin-module.ts +87 -4
  348. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  349. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  350. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  351. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  352. package/src/plugins/builtin/portfolio-list/cli/render.ts +89 -18
  353. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  354. package/src/plugins/builtin/portfolio-list/headless.ts +40 -7
  355. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  356. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  357. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  358. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  359. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  360. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  361. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  362. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  363. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  364. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  365. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  366. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  367. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  368. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  369. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  370. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  371. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  372. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  373. package/src/plugins/builtin/sec/index.tsx +5 -5
  374. package/src/plugins/builtin/sectors/client.ts +10 -2
  375. package/src/plugins/builtin/sectors/headless.ts +35 -6
  376. package/src/plugins/builtin/sectors/index.tsx +18 -1
  377. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  378. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  379. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  380. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  381. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  382. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  383. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  384. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  385. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  386. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  387. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  388. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  389. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  390. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  391. package/src/plugins/builtin/themes/pane.tsx +3 -0
  392. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  393. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  394. package/src/plugins/builtin/ticker-detail/headless.ts +63 -25
  395. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  396. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  397. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  398. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  399. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  400. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  401. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  402. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  403. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  404. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  405. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  406. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  407. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  408. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  409. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  410. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  411. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  412. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  413. package/src/plugins/builtin/world-indices/client.ts +22 -4
  414. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  415. package/src/plugins/builtin/world-indices/model.ts +4 -2
  416. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  417. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  418. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  419. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  420. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  421. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  422. package/src/plugins/builtin/world-venue-map/index.ts +6 -6
  423. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  424. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  425. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  426. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  427. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  428. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  429. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  430. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  431. package/src/plugins/builtin-editorial.ts +126 -0
  432. package/src/plugins/catalog-backend.ts +16 -9
  433. package/src/plugins/catalog-browser.ts +127 -63
  434. package/src/plugins/catalog-manifest.ts +73 -40
  435. package/src/plugins/catalog-ui.ts +26 -6
  436. package/src/plugins/extracted-plugins.ts +7 -1
  437. package/src/plugins/ownership.ts +150 -7
  438. package/src/plugins/plugin-functions.ts +54 -0
  439. package/src/plugins/plugin-off.ts +36 -0
  440. package/src/plugins/registry/host-actions.ts +16 -0
  441. package/src/plugins/registry/index.ts +84 -24
  442. package/src/plugins/registry/plugin-state.ts +25 -1
  443. package/src/plugins/runtime/context.tsx +29 -0
  444. package/src/react/input.ts +48 -1
  445. package/src/remote/app-host.tsx +5 -0
  446. package/src/remote/controller.ts +3 -53
  447. package/src/remote/matching.ts +69 -0
  448. package/src/remote/schema.ts +1 -1
  449. package/src/remote/semantic-tree.tsx +14 -0
  450. package/src/remote/types.ts +2 -0
  451. package/src/renderers/browser/app-services.ts +3 -1
  452. package/src/renderers/dom/data-table/dom.ts +31 -0
  453. package/src/renderers/dom/data-table/index.tsx +5 -0
  454. package/src/renderers/dom/desktop/icons.tsx +14 -0
  455. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  456. package/src/renderers/dom/host/box.tsx +113 -17
  457. package/src/renderers/dom/host/drag-shield.ts +35 -0
  458. package/src/renderers/dom/host/live-frame.ts +62 -0
  459. package/src/renderers/dom/host/mouse.ts +8 -1
  460. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  461. package/src/renderers/dom/host/tabs.tsx +3 -2
  462. package/src/renderers/dom/input-host.tsx +6 -1
  463. package/src/renderers/dom/styles.css +26 -5
  464. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  465. package/src/renderers/electrobun/bun/desktop/backend.ts +7 -1
  466. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  467. package/src/renderers/electrobun/bun/desktop/initialization.ts +15 -1
  468. package/src/renderers/electrobun/shared/protocol.ts +12 -1
  469. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +4 -1
  470. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  471. package/src/renderers/electrobun/view/host/native.ts +0 -10
  472. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  473. package/src/renderers/electrobun/view/main.tsx +4 -1
  474. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  475. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  476. package/src/renderers/opentui/input-host.tsx +6 -1
  477. package/src/renderers/opentui/start.tsx +9 -0
  478. package/src/renderers/opentui/ui-host.tsx +7 -1
  479. package/src/shares/portable-layout.ts +16 -6
  480. package/src/sources/gloomberb-cloud/index.ts +19 -10
  481. package/src/sources/gloomberb-cloud/normalizers.ts +20 -21
  482. package/src/sources/listing-symbols.ts +11 -5
  483. package/src/sources/provider-errors.ts +88 -3
  484. package/src/sources/provider-router/batches.ts +3 -3
  485. package/src/sources/provider-router/cached-routes.ts +8 -4
  486. package/src/sources/provider-router/financial-routes.ts +13 -7
  487. package/src/sources/provider-router/financials.ts +106 -16
  488. package/src/sources/provider-router/history.ts +27 -11
  489. package/src/sources/provider-router/index.ts +1 -1
  490. package/src/sources/provider-router/primary.ts +24 -6
  491. package/src/sources/sec-edgar.ts +68 -8
  492. package/src/state/pane-scroll-registry.tsx +67 -35
  493. package/src/sync/core-contributors.ts +4 -3
  494. package/src/test-support/cli-context.ts +2 -1
  495. package/src/test-support/data-provider.ts +34 -0
  496. package/src/test-support/margin-account.ts +108 -0
  497. package/src/test-support/shell-pane-keys.tsx +1 -0
  498. package/src/theme/series-colors.ts +46 -0
  499. package/src/theme/themes.ts +149 -0
  500. package/src/tickers/search/index.ts +22 -2
  501. package/src/tickers/search/ranking.ts +45 -1
  502. package/src/tickers/search/types.ts +3 -1
  503. package/src/time-series/resolution.ts +15 -0
  504. package/src/time-series/resolve.ts +1 -14
  505. package/src/types/config.ts +61 -1
  506. package/src/types/financials.ts +32 -3
  507. package/src/types/headless.ts +11 -0
  508. package/src/types/instrument.ts +8 -0
  509. package/src/types/plugin.ts +49 -3
  510. package/src/ui/host.tsx +16 -0
  511. package/src/ui/index.tsx +10 -3
  512. package/src/ui/scroll-box-axis.ts +26 -0
  513. package/src/utils/cli-output.ts +14 -1
  514. package/src/utils/currency-units.ts +8 -3
  515. package/src/utils/depositary-receipt.ts +54 -10
  516. package/src/utils/display-number.ts +63 -0
  517. package/src/utils/exchanges.ts +13 -0
  518. package/src/utils/format.ts +2 -1
  519. package/src/utils/fundamentals.ts +17 -1
  520. package/src/utils/fx-market-hours.ts +34 -0
  521. package/src/utils/option-expiry.ts +126 -0
  522. package/src/utils/price-history-integrity.ts +19 -6
  523. package/src/utils/price-history.ts +39 -6
  524. package/src/utils/rpc-timeout-error.ts +6 -2
  525. package/src/utils/sec-eps-basis.ts +159 -30
  526. package/src/utils/sec.ts +38 -34
  527. package/src/utils/throttled-fetch.ts +51 -22
@@ -75,6 +75,33 @@ function capLayerSelection(ordered: readonly GeoLayerInfo[]): GeoLayerInfo[] {
75
75
  return kept;
76
76
  }
77
77
 
78
+ /** Layers a plain map opens with: the server's default layers that can serve, two at most. */
79
+ const MAX_DEFAULT_LAYERS = 2;
80
+
81
+ function defaultLayerIds(layers: readonly GeoLayerInfo[] | null | undefined): string[] {
82
+ if (!layers?.length) return [];
83
+ const defaults = layers.filter((layer) => layer.defaultVisible && layer.status !== "unavailable");
84
+ return capLayerSelection(defaults.slice(0, MAX_DEFAULT_LAYERS)).map((layer) => layer.id);
85
+ }
86
+
87
+ /**
88
+ * A pane that never chose its layers or venues: a plain `MAP`, or a map
89
+ * saved before layers existed. It shows venues and the server's default
90
+ * layers. A preset or an edit in the layer picker writes both settings, and
91
+ * from then on the pane keeps exactly what was chosen.
92
+ */
93
+ export function isPlainMap(settings: Record<string, unknown> | undefined): boolean {
94
+ return settings?.[LAYERS_SETTING_KEY] == null && typeof settings?.[VENUES_SETTING_KEY] !== "boolean";
95
+ }
96
+
97
+ /** The layer ids or groups a pane shows: what it chose, or the defaults on a plain map. */
98
+ export function activeLayerTokens(
99
+ settings: Record<string, unknown> | undefined,
100
+ layers: readonly GeoLayerInfo[] | null | undefined,
101
+ ): string[] {
102
+ return isPlainMap(settings) ? defaultLayerIds(layers) : parseLayerTokens(settings?.[LAYERS_SETTING_KEY]);
103
+ }
104
+
78
105
  /** Expands groups to their layers, drops what the server does not list, and applies the caps. */
79
106
  export function resolveActiveLayers(tokens: readonly string[], layers: readonly GeoLayerInfo[] | null | undefined): GeoLayerInfo[] {
80
107
  if (!layers?.length || !tokens.length) return [];
@@ -98,18 +125,23 @@ export interface MapPreset {
98
125
  venues: boolean;
99
126
  }
100
127
 
101
- /** `MAP ships` and friends: the typed words become layer tokens, resolved once the catalog is known. */
128
+ /**
129
+ * `MAP ships` and friends: the typed words become layer tokens, resolved once
130
+ * the catalog is known. `MAP venues` alone is the venue map without layers.
131
+ */
102
132
  export function parseMapPreset(arg: string | null | undefined): MapPreset | null {
103
133
  const tokens = parseLayerTokens(arg ?? "");
104
- if (!tokens.length) return null;
105
- return { layers: tokens, venues: /\bvenues\b/i.test(arg ?? "") };
134
+ const venues = /\bvenues\b/i.test(arg ?? "");
135
+ if (!tokens.length) return venues ? { layers: [], venues: true } : null;
136
+ return { layers: tokens, venues };
106
137
  }
107
138
 
108
139
  export const MAP_PRESET_OPTIONS = [
140
+ { value: "venues", label: "Trading venues only" },
109
141
  { value: "ships", label: "Ships and chokepoints" },
110
142
  { value: "ports", label: "Ports" },
111
143
  { value: "energy", label: "Pipelines, fields and terminals" },
112
- { value: "air", label: "Airports" },
144
+ { value: "air", label: "Airports and flights" },
113
145
  ] as const;
114
146
 
115
147
  function shortDate(iso: string): string | null {
@@ -152,8 +184,8 @@ function groupsOf(layers: readonly GeoLayerInfo[]): string[] {
152
184
  export function readVenuesSetting(settings: Record<string, unknown> | undefined, activeLayers: number): boolean {
153
185
  const value = settings?.[VENUES_SETTING_KEY];
154
186
  if (typeof value === "boolean") return value;
155
- // A preset opens on its layers; a plain MAP stays the venue map.
156
- return activeLayers === 0;
187
+ // A plain MAP shows venues under its default layers; a preset opens on its layers alone.
188
+ return settings?.[LAYERS_SETTING_KEY] == null || activeLayers === 0;
157
189
  }
158
190
 
159
191
  /**
@@ -166,7 +198,7 @@ export function buildMapSettingsDef(
166
198
  layers: readonly GeoLayerInfo[] | null | undefined,
167
199
  ): PaneSettingsDef | undefined {
168
200
  if (!layers?.length) return undefined;
169
- const active = resolveActiveLayers(parseLayerTokens(settings[LAYERS_SETTING_KEY]), layers);
201
+ const active = resolveActiveLayers(activeLayerTokens(settings, layers), layers);
170
202
  const values: Record<string, unknown> = { [VENUES_SETTING_KEY]: readVenuesSetting(settings, active.length) };
171
203
  const fields: PaneSettingField[] = [{
172
204
  key: VENUES_SETTING_KEY,
@@ -199,12 +231,16 @@ export function applyMapSetting(
199
231
  value: unknown,
200
232
  layers: readonly GeoLayerInfo[],
201
233
  ): Record<string, unknown> {
202
- if (key === VENUES_SETTING_KEY) return { ...settings, [VENUES_SETTING_KEY]: value === true };
203
- if (!key.startsWith(GROUP_PREFIX)) return { ...settings, [key]: value };
234
+ const before = resolveActiveLayers(activeLayerTokens(settings, layers), layers);
235
+ // The first edit of a plain map writes down what it showed, so only the edited part changes.
236
+ const chosen = isPlainMap(settings)
237
+ ? { ...settings, [LAYERS_SETTING_KEY]: before.map((layer) => layer.id), [VENUES_SETTING_KEY]: readVenuesSetting(settings, before.length) }
238
+ : settings;
239
+ if (key === VENUES_SETTING_KEY) return { ...chosen, [VENUES_SETTING_KEY]: value === true };
240
+ if (!key.startsWith(GROUP_PREFIX)) return { ...chosen, [key]: value };
204
241
  const group = key.slice(GROUP_PREFIX.length);
205
242
  const members = new Set(layers.filter((layer) => String(layer.group || "other") === group).map((layer) => layer.id));
206
243
  const picked = Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string" && members.has(entry)) : [];
207
- const before = resolveActiveLayers(parseLayerTokens(settings[LAYERS_SETTING_KEY]), layers);
208
244
  // Other groups keep their order; this group's picks follow, newest last, so the caps drop the oldest.
209
245
  const kept = before.filter((layer) => !members.has(layer.id) || picked.includes(layer.id));
210
246
  const added = picked.filter((id) => !kept.some((layer) => layer.id === id))
@@ -253,6 +289,28 @@ export function geoViewForViewport(
253
289
  };
254
290
  }
255
291
 
292
+ /**
293
+ * Zoom an opened entity is shown at: 16 times the world (the server's zoom 4,
294
+ * where dense point layers stop clustering) for a ship or a port, wider for a
295
+ * pipeline or a field, and never short of the zoom its layer draws from
296
+ * (flights from the server's zoom 5, 32 times the world).
297
+ */
298
+ export function entityOpenZoom(layer: Pick<GeoLayerInfo, "geometry" | "minZoom">): number {
299
+ const base = layer.geometry === "point" ? 16 : 4;
300
+ return layer.minZoom === undefined ? base : Math.max(base, 2 ** layer.minZoom);
301
+ }
302
+
303
+ /**
304
+ * The area a layer's table reads. A layer drawn only from a zoom (live
305
+ * flights) lists what the map shows once it is that close; further out, and
306
+ * for every other layer, the table asks without one and the server answers
307
+ * for the whole layer or, for flights, one busy area.
308
+ */
309
+ export function entityTableBbox(layer: Pick<GeoLayerInfo, "minZoom">, view: GeoView): GeoBbox | undefined {
310
+ if (layer.minZoom === undefined || view.zoom < layer.minZoom) return undefined;
311
+ return view.bbox;
312
+ }
313
+
256
314
  export function geoViewKey(view: GeoView): string {
257
315
  return `${view.bbox.join(",")}@${Math.floor(view.zoom)}`;
258
316
  }
@@ -16,6 +16,7 @@ import {
16
16
  MAX_WORLD_MAP_ZOOM,
17
17
  panWorldMapViewport,
18
18
  projectWorldPoint,
19
+ savedWorldMapViewport,
19
20
  unprojectWorldPoint,
20
21
  zoomWorldMapViewport,
21
22
  type WorldMapPoint,
@@ -26,7 +27,8 @@ import { WORLD_OUTLINES } from "./world-outlines";
26
27
  import { drawGeoBitmap, hitTestGeo, rasterGeoCells, type GeoHit, type GeoMapOverlay } from "./geo-draw";
27
28
  import { GeoSvgLayers, MapIconSwatch, toneColor, type MapMatrix, type SymbolTones } from "./geo-svg";
28
29
  import { BasemapLayer, type BasemapPalette } from "./basemap-layer";
29
- import type { GeoBox } from "./basemap";
30
+ import { loadCountryLabels, type CountryLabel, type GeoBox } from "./basemap";
31
+ import { COUNTRY_LABEL_FONT_PX, layoutCountryLabels, type LabelObstacle, type PlacedCountryLabel } from "./country-labels";
30
32
  import { layerSymbol, shipClass, usesClassTones, type MapSymbolId, type SymbolTone } from "./map-symbols";
31
33
  import { geoViewForViewport, sameGeoView, WORLD_GEO_VIEW, type GeoView } from "./layers";
32
34
 
@@ -52,6 +54,13 @@ interface WorldVenueMapProps {
52
54
  focus?: WorldMapFocus | null;
53
55
  /** How far the map may zoom; geo layers go closer than venues. */
54
56
  maxZoom?: number;
57
+ /**
58
+ * Where the map was when this pane last drew it (saved pane state). A pane
59
+ * docked, undocked, popped out or reloaded mounts a new map; it opens here.
60
+ */
61
+ savedViewport?: unknown;
62
+ /** The view once it has rested for a moment, for the pane to keep. */
63
+ onViewportSettled?: (viewport: WorldMapViewport) => void;
55
64
  }
56
65
 
57
66
  function clusterVenue(cluster: WorldVenueCluster, selectedMic: string | null): CloudWorldVenuePayload {
@@ -306,6 +315,75 @@ const MAP_CLICK_HIT_PX = 14;
306
315
  const MAP_DOUBLE_CLICK_ZOOM = 1.8;
307
316
  /** Share of the plot a selected venue may sit inside before the map pans to it. */
308
317
  const MAP_FOLLOW_MARGIN = 0.06;
318
+ /** How long the view rests before the pane keeps it. */
319
+ const VIEWPORT_SETTLE_MS = 400;
320
+
321
+ /**
322
+ * Whether a map of this many cells can be drawn. A pane mid-layout (a new
323
+ * window, a dock move) can hand the map a width of one cell for a frame; a
324
+ * projection from that has no scale, and a focus applied to it loses its
325
+ * centre.
326
+ */
327
+ function mapHasArea(width: number, height: number): boolean {
328
+ return width > 2 && height > 1;
329
+ }
330
+
331
+ /** A venue dot's radius in view units, larger for a cluster of venues. */
332
+ function venueDotRadius(count: number): number {
333
+ return Math.max(0.55, Math.min(1.5, 0.45 + Math.sqrt(count) * 0.22));
334
+ }
335
+
336
+ /** Country names are laid out again once the view has rested this long, not on every drag frame. */
337
+ const LABEL_SETTLE_MS = 150;
338
+ /** Room a selected or hovered marker keeps clear of country names, in pixels. */
339
+ const LABEL_MARKER_CLEARANCE_PX = 14;
340
+
341
+ /** The country names, from their lazy chunk; none until it arrives, none in a build without map data. */
342
+ function useCountryLabels(): readonly CountryLabel[] | null {
343
+ const [labels, setLabels] = useState<readonly CountryLabel[] | null>(null);
344
+ useEffect(() => {
345
+ let current = true;
346
+ loadCountryLabels().then((loaded) => {
347
+ if (current) setLabels(loaded);
348
+ }, () => {});
349
+ return () => {
350
+ current = false;
351
+ };
352
+ }, []);
353
+ return labels;
354
+ }
355
+
356
+ /** Country names under the markers: small, quiet, never in the way of a click. */
357
+ function CountryNames({ labels, project, unitPx, color, halo }: {
358
+ labels: readonly PlacedCountryLabel[];
359
+ project: (longitude: number, latitude: number) => WorldMapPoint;
360
+ unitPx: number;
361
+ color: string;
362
+ halo: string;
363
+ }) {
364
+ if (!labels.length) return null;
365
+ const fontSize = COUNTRY_LABEL_FONT_PX / unitPx;
366
+ return createElement(
367
+ "g",
368
+ {
369
+ "data-gloom-role": "country-labels",
370
+ fill: color,
371
+ stroke: halo,
372
+ strokeWidth: 3 / unitPx,
373
+ strokeOpacity: 0.8,
374
+ strokeLinejoin: "round",
375
+ paintOrder: "stroke",
376
+ fontSize,
377
+ fontFamily: "inherit",
378
+ textAnchor: "middle",
379
+ pointerEvents: "none",
380
+ },
381
+ ...labels.map((label) => {
382
+ const at = project(label.longitude, label.latitude);
383
+ return createElement("text", { key: label.name, x: at.x, y: at.y, dy: "0.35em" }, label.name);
384
+ }),
385
+ );
386
+ }
309
387
 
310
388
  function wheelZoomFactor(event: WheelEvent): number {
311
389
  const delta = event.deltaMode === 1 ? event.deltaY * 16 : event.deltaY;
@@ -386,7 +464,10 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
386
464
  const maxZoom = props.maxZoom ?? MAX_WORLD_MAP_ZOOM;
387
465
  const surfaceRef = useRef<HTMLDivElement | null>(null);
388
466
  const surfaceSize = useElementSize(surfaceRef);
389
- const [viewport, setViewport] = useState<WorldMapViewport>(DEFAULT_WORLD_MAP_VIEWPORT);
467
+ const [viewport, setViewport] = useState<WorldMapViewport>(
468
+ () => savedWorldMapViewport(props.savedViewport, maxZoom) ?? DEFAULT_WORLD_MAP_VIEWPORT,
469
+ );
470
+ const sized = mapHasArea(props.width, props.height);
390
471
  const viewportRef = useRef(viewport);
391
472
  viewportRef.current = viewport;
392
473
  const [dragging, setDragging] = useState(false);
@@ -431,6 +512,7 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
431
512
  land: blendHex(colors.bg, colors.text, dark ? 0.1 : 0.07),
432
513
  coast: blendHex(colors.bg, colors.textDim, dark ? 0.85 : 0.75),
433
514
  border: blendHex(colors.bg, colors.textDim, dark ? 0.42 : 0.35),
515
+ label: blendHex(colors.bg, colors.textDim, dark ? 0.7 : 0.8),
434
516
  }), [colors.bg, colors.text, colors.textDim, dark]);
435
517
  // Five colours in all: the layer's own for cargo, amber tankers, bright
436
518
  // passenger ships, green fishing boats, and one quiet grey for the rest.
@@ -439,6 +521,42 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
439
521
  return { tanker: colors.warning, passenger: colors.textBright, fishing: colors.positive, service: quiet, other: quiet };
440
522
  }, [colors.positive, colors.text, colors.textBright, colors.textDim, colors.warning]);
441
523
 
524
+ // Names are placed for the view once it rests; a drag moves the ones already placed.
525
+ const countryLabels = useCountryLabels();
526
+ const [labelViewport, setLabelViewport] = useState(viewport);
527
+ useEffect(() => {
528
+ if (labelViewport === viewport) return;
529
+ const timer = setTimeout(() => setLabelViewport(viewport), LABEL_SETTLE_MS);
530
+ return () => clearTimeout(timer);
531
+ }, [labelViewport, viewport]);
532
+ const selectedPoint = props.overlay?.selected ?? null;
533
+ const selectedVenuePoint = props.venues.find((venue) => venue.mic === props.selectedMic) ?? null;
534
+ const hoverAnchor = hover?.hit.kind === "feature" && hover.hit.feature.geometry.type === "Point" ? hover.hit.feature.geometry.coordinates : null;
535
+ const placedLabels = useMemo(() => {
536
+ if (!countryLabels?.length) return [];
537
+ const at = (longitude: number, latitude: number) => {
538
+ const point = projectWorldPoint(longitude, latitude, props.width, plotHeight, 1, labelViewport);
539
+ return { x: point.x * unitPx, y: point.y * unitPx };
540
+ };
541
+ const markers = [
542
+ selectedPoint ? [selectedPoint.longitude, selectedPoint.latitude] : null,
543
+ selectedVenuePoint ? [selectedVenuePoint.longitude, selectedVenuePoint.latitude] : null,
544
+ hoverAnchor,
545
+ ].filter((point): point is [number, number] => point !== null);
546
+ const obstacles: LabelObstacle[] = markers.map(([longitude, latitude]) => ({ ...at(longitude, latitude), radius: LABEL_MARKER_CLEARANCE_PX }));
547
+ // Venue dots are few and sit on land, where the names are: a name never runs under one.
548
+ for (const cluster of clusterWorldVenues(props.venues, props.width, plotHeight, 1, labelViewport)) {
549
+ obstacles.push({ x: cluster.x * unitPx, y: cluster.y * unitPx, radius: (venueDotRadius(cluster.venues.length) + 0.2) * unitPx });
550
+ }
551
+ return layoutCountryLabels(countryLabels, {
552
+ pxPerDegree: (at(1, 0).x - at(0, 0).x),
553
+ toPixels: at,
554
+ widthPx: props.width * unitPx,
555
+ heightPx: plotHeight * unitPx,
556
+ obstacles,
557
+ });
558
+ }, [countryLabels, hoverAnchor?.[0], hoverAnchor?.[1], labelViewport, plotHeight, props.venues, props.width, selectedPoint?.latitude, selectedPoint?.longitude, selectedVenuePoint, unitPx]);
559
+
442
560
  const hitRadius = useCallback((element: HTMLElement) => {
443
561
  const rect = element.getBoundingClientRect();
444
562
  return rect.height <= 0 ? 2.4 : (MAP_CLICK_HIT_PX / rect.height) * plotHeight;
@@ -464,24 +582,39 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
464
582
  const onViewChange = props.onViewChange;
465
583
  const reportedViewRef = useRef<GeoView | null>(null);
466
584
  useEffect(() => {
467
- if (!onViewChange) return;
585
+ if (!onViewChange || !sized) return;
468
586
  const next = geoViewForViewport(viewport, props.width, plotHeight);
469
587
  if (reportedViewRef.current && sameGeoView(reportedViewRef.current, next)) return;
470
588
  reportedViewRef.current = next;
471
589
  onViewChange(next);
472
- }, [onViewChange, plotHeight, props.width, viewport]);
590
+ }, [onViewChange, plotHeight, props.width, sized, viewport]);
473
591
 
474
- // A new focus (an entity picked in the table) centres the map on it.
592
+ // The pane keeps the view once it rests, so a remount opens where this left off.
593
+ const onViewportSettled = props.onViewportSettled;
594
+ const settledViewportRef = useRef(viewport);
595
+ useEffect(() => {
596
+ if (!onViewportSettled || settledViewportRef.current === viewport) return;
597
+ const timer = setTimeout(() => {
598
+ settledViewportRef.current = viewport;
599
+ onViewportSettled(viewport);
600
+ }, VIEWPORT_SETTLE_MS);
601
+ return () => clearTimeout(timer);
602
+ }, [onViewportSettled, viewport]);
603
+
604
+ // A new focus (an entity picked in the table) centres the map on it, once
605
+ // the map has an area to centre it in.
475
606
  const focusKey = props.focus?.key ?? null;
607
+ const appliedFocusRef = useRef<string | null>(null);
476
608
  useEffect(() => {
477
609
  const focus = props.focus;
478
- if (!focus) return;
610
+ if (!focus || !sized || appliedFocusRef.current === focus.key) return;
611
+ appliedFocusRef.current = focus.key;
479
612
  setViewport((current) => {
480
613
  const zoom = Math.max(current.zoom, focus.zoom ?? 1);
481
614
  if (zoom <= 1) return current;
482
615
  return clampWorldMapViewport({ zoom, centerLongitude: focus.longitude, centerLatitude: focus.latitude }, props.width, plotHeight, 1, maxZoom);
483
616
  });
484
- }, [focusKey]);
617
+ }, [focusKey, sized]);
485
618
 
486
619
  const selectedVenue = props.venues.find((venue) => venue.mic === props.selectedMic) ?? null;
487
620
  const selectedVenueRef = useRef(selectedVenue);
@@ -637,60 +770,63 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
637
770
  background: colors.bg,
638
771
  }}
639
772
  >
640
- <svg
641
- viewBox={`0 0 ${props.width} ${plotHeight}`}
642
- width="100%"
643
- height="100%"
644
- aria-hidden="true"
645
- style={{ display: "block", background: colors.bg, pointerEvents: "none" }}
646
- >
647
- <BasemapLayer matrix={matrix} view={view} pxPerDegree={pxPerDegree} palette={palette} />
648
- {props.overlay ? (
649
- <GeoSvgLayers
650
- overlay={props.overlay}
651
- matrix={matrix}
652
- project={project}
653
- view={view}
654
- unitPx={unitPx}
655
- mapWidthPx={surfaceSize?.width ?? props.width * 8}
656
- background={colors.bg}
657
- selectedColor={colors.selectedText}
658
- textColor={colors.textBright}
659
- tones={tones}
660
- hovered={hovered?.hit ?? null}
661
- />
662
- ) : null}
663
- {clusters.map((cluster) => {
664
- const selected = isSelectedCluster(cluster, props.selectedMic);
665
- const venue = clusterVenue(cluster, props.selectedMic);
666
- const radius = Math.max(0.55, Math.min(1.5, 0.45 + Math.sqrt(cluster.venues.length) * 0.22));
667
- return (
668
- <g key={cluster.id}>
669
- <title>{`${cluster.venues.length} venue${cluster.venues.length === 1 ? "" : "s"} near ${venue.city}: ${cluster.venues.map((item) => `${item.mic} ${item.name}`).join(", ")}`}</title>
670
- <circle
671
- cx={cluster.x}
672
- cy={cluster.y}
673
- r={radius + (selected ? 0.24 : 0)}
674
- fill={cluster.isOpen ? colors.positive : colors.textMuted}
675
- stroke={selected ? colors.selectedText : colors.bg}
676
- strokeWidth={selected ? 0.35 : 0.16}
677
- vectorEffect="non-scaling-stroke"
678
- />
679
- {cluster.venues.length > 1 ? createElement("text", {
680
- x: cluster.x,
681
- y: cluster.y,
682
- fill: colors.bg,
683
- dy: "0.34em",
684
- fontSize: Math.max(0.62, Math.min(0.95, radius * 0.78)),
685
- fontWeight: "700",
686
- fontFamily: "inherit",
687
- textAnchor: "middle",
688
- pointerEvents: "none",
689
- }, cluster.venues.length) : null}
690
- </g>
691
- );
692
- })}
693
- </svg>
773
+ {sized ? (
774
+ <svg
775
+ viewBox={`0 0 ${props.width} ${plotHeight}`}
776
+ width="100%"
777
+ height="100%"
778
+ aria-hidden="true"
779
+ style={{ display: "block", background: colors.bg, pointerEvents: "none" }}
780
+ >
781
+ <BasemapLayer matrix={matrix} view={view} pxPerDegree={pxPerDegree} palette={palette} />
782
+ <CountryNames labels={placedLabels} project={project} unitPx={unitPx} color={palette.label} halo={colors.bg} />
783
+ {props.overlay ? (
784
+ <GeoSvgLayers
785
+ overlay={props.overlay}
786
+ matrix={matrix}
787
+ project={project}
788
+ view={view}
789
+ unitPx={unitPx}
790
+ mapWidthPx={surfaceSize?.width ?? props.width * 8}
791
+ background={colors.bg}
792
+ selectedColor={colors.selectedText}
793
+ textColor={colors.textBright}
794
+ tones={tones}
795
+ hovered={hovered?.hit ?? null}
796
+ />
797
+ ) : null}
798
+ {clusters.map((cluster) => {
799
+ const selected = isSelectedCluster(cluster, props.selectedMic);
800
+ const venue = clusterVenue(cluster, props.selectedMic);
801
+ const radius = venueDotRadius(cluster.venues.length);
802
+ return (
803
+ <g key={cluster.id}>
804
+ <title>{`${cluster.venues.length} venue${cluster.venues.length === 1 ? "" : "s"} near ${venue.city}: ${cluster.venues.map((item) => `${item.mic} ${item.name}`).join(", ")}`}</title>
805
+ <circle
806
+ cx={cluster.x}
807
+ cy={cluster.y}
808
+ r={radius + (selected ? 0.24 : 0)}
809
+ fill={cluster.isOpen ? colors.positive : colors.textMuted}
810
+ stroke={selected ? colors.selectedText : colors.bg}
811
+ strokeWidth={selected ? 0.35 : 0.16}
812
+ vectorEffect="non-scaling-stroke"
813
+ />
814
+ {cluster.venues.length > 1 ? createElement("text", {
815
+ x: cluster.x,
816
+ y: cluster.y,
817
+ fill: colors.bg,
818
+ dy: "0.34em",
819
+ fontSize: Math.max(0.62, Math.min(0.95, radius * 0.78)),
820
+ fontWeight: "700",
821
+ fontFamily: "inherit",
822
+ textAnchor: "middle",
823
+ pointerEvents: "none",
824
+ }, cluster.venues.length) : null}
825
+ </g>
826
+ );
827
+ })}
828
+ </svg>
829
+ ) : null}
694
830
  {tooltip && hovered ? (
695
831
  <MapTooltip
696
832
  title={tooltip.title}
@@ -700,7 +836,7 @@ function DesktopWorldVenueMap(props: WorldVenueMapProps) {
700
836
  width={surfaceSize?.width ?? 0}
701
837
  />
702
838
  ) : null}
703
- {props.overlay ? <GeoLegend overlay={props.overlay} venues={props.venues.length > 0} tones={tones} /> : null}
839
+ {props.overlay && sized ? <GeoLegend overlay={props.overlay} venues={props.venues.length > 0} tones={tones} /> : null}
704
840
  </div>
705
841
  </Box>
706
842
  );
@@ -97,6 +97,9 @@ export function clampWorldMapViewport(
97
97
  if (zoom <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
98
98
 
99
99
  const { availableWidth, effectiveHeight, latitudeSpan } = mapExtent(width, height, yUnitAspect);
100
+ // A plot with no area yet (a pane mid-layout) says nothing about what fits:
101
+ // keep the centre rather than resetting it to the middle of the world.
102
+ if (availableWidth <= 0 || effectiveHeight <= 0) return { ...viewport, zoom };
100
103
  const scale = fittedWorldMapScale(width, height, yUnitAspect) * zoom;
101
104
  const halfLongitude = availableWidth / (2 * scale);
102
105
  const halfLatitude = effectiveHeight / (2 * scale);
@@ -116,6 +119,19 @@ export function clampWorldMapViewport(
116
119
  };
117
120
  }
118
121
 
122
+ /** A viewport read back from saved pane state, or null when it is not one. */
123
+ export function savedWorldMapViewport(value: unknown, maxZoom = MAX_WORLD_MAP_ZOOM): WorldMapViewport | null {
124
+ if (!value || typeof value !== "object") return null;
125
+ const { zoom, centerLongitude, centerLatitude } = value as Partial<WorldMapViewport>;
126
+ if (![zoom, centerLongitude, centerLatitude].every((part) => typeof part === "number" && Number.isFinite(part))) return null;
127
+ if (zoom! <= 1) return DEFAULT_WORLD_MAP_VIEWPORT;
128
+ return {
129
+ zoom: Math.min(zoom!, maxZoom),
130
+ centerLongitude: clamp(centerLongitude!, -180, 180),
131
+ centerLatitude: clamp(centerLatitude!, MIN_LATITUDE, MAX_LATITUDE),
132
+ };
133
+ }
134
+
119
135
  export function zoomWorldMapViewport(
120
136
  viewport: WorldMapViewport,
121
137
  width: number,
@@ -4,6 +4,9 @@ import type { DetailTile } from "../basemap";
4
4
  /** @knipignore Loaded through the map's data specifier, which only view builds define (see ../basemap.ts). */
5
5
  export const loadWorld = () => import("./world-50m.chunk.js");
6
6
 
7
+ /** @knipignore Loaded through the map's data specifier, which only view builds define (see ../basemap.ts). */
8
+ export const loadLabels = () => import("./labels.chunk.js");
9
+
7
10
  export const DETAIL_TILES: readonly DetailTile[] = [
8
11
  { id: "w090s60", box: [-90.005, -60, -74.995, -44.995], load: () => import("./tiles/w090s60.chunk.js") },
9
12
  { id: "w075s60", box: [-75.005, -60, -59.995, -44.995], load: () => import("./tiles/w075s60.chunk.js") },