gloomberb 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -0,0 +1,323 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { ConfirmDialog } from "../components/ui/confirm-dialog";
3
+ import { useShortcut } from "../react/input";
4
+ import { useAppDispatch, useAppSelector } from "../state/app/context";
5
+ import { selectActiveLayoutIndex, selectSavedLayouts } from "../state/selectors-ui";
6
+ import { useDialog, useDialogState, type PromptContext } from "../ui/dialog";
7
+ import { useRendererHost, useUiHost } from "../ui";
8
+ import { requestAuthDialog } from "../plugins/builtin/cloud/auth-dialog";
9
+ import { usePlanAccess } from "../plugins/builtin/shared/plan-access";
10
+ import type { PluginRegistry } from "../plugins/registry";
11
+ import type { LayoutConfig } from "../types/config";
12
+ import { LayoutGalleryDesktop } from "./gallery-desktop";
13
+ import { LayoutGalleryTerminal } from "./gallery-terminal";
14
+ import { LayoutNameDialog } from "./name-dialog";
15
+ import {
16
+ buildCommunityEntries,
17
+ buildOwnedEntries,
18
+ filterGalleryEntries,
19
+ missingPaneIds,
20
+ type GalleryEntry,
21
+ } from "./model";
22
+ import { publicMarketplaceLayoutUrl } from "./api";
23
+ import {
24
+ materializeMarketplaceLayout,
25
+ publishableMarketplaceLayout,
26
+ } from "./payload";
27
+ import { useLayoutMarketplace } from "./use-marketplace";
28
+
29
+ export interface LayoutGalleryController {
30
+ query: string;
31
+ setQuery: (query: string) => void;
32
+ owned: GalleryEntry[];
33
+ community: GalleryEntry[];
34
+ /** Owned entries first, then community, in the order the destination renders them. */
35
+ entries: GalleryEntry[];
36
+ selectedId: string | null;
37
+ select: (id: string | null) => void;
38
+ detail: GalleryEntry | null;
39
+ openDetail: (entry: GalleryEntry) => void;
40
+ closeDetail: () => void;
41
+ activate: (entry: GalleryEntry) => void;
42
+ install: (entry: GalleryEntry) => void;
43
+ discover: ReturnType<typeof useLayoutMarketplace>;
44
+ signedIn: boolean;
45
+ requestSignIn: () => void;
46
+ publishCurrent: () => void;
47
+ copyLink: (entry: GalleryEntry) => void;
48
+ publishing: boolean;
49
+ newLayout: () => void;
50
+ renameLayout: (entry: GalleryEntry) => void;
51
+ duplicateLayout: (entry: GalleryEntry) => void;
52
+ deleteLayout: (entry: GalleryEntry) => void;
53
+ canDelete: boolean;
54
+ close: () => void;
55
+ panes: PluginRegistry["panes"];
56
+ missingPaneIds: (layout: LayoutConfig) => string[];
57
+ }
58
+
59
+ export function LayoutMarketplaceGallery({
60
+ pluginRegistry,
61
+ focused = true,
62
+ width,
63
+ height,
64
+ onClose,
65
+ }: {
66
+ pluginRegistry: PluginRegistry;
67
+ focused?: boolean;
68
+ width?: number;
69
+ height?: number;
70
+ onClose?: () => void;
71
+ }) {
72
+ const dispatch = useAppDispatch();
73
+ const dialog = useDialog();
74
+ const renderer = useRendererHost();
75
+ const dialogOpen = useDialogState((state) => state.isOpen);
76
+ const layouts = useAppSelector(selectSavedLayouts);
77
+ const activeIndex = useAppSelector(selectActiveLayoutIndex);
78
+ const currentLayout = useAppSelector((state) => state.config.layout);
79
+ const currentPaneState = useAppSelector((state) => state.paneState);
80
+ const { signedIn } = usePlanAccess();
81
+ const discover = useLayoutMarketplace(true, signedIn);
82
+ const [query, setQuery] = useState("");
83
+ const [selectedId, setSelectedId] = useState<string | null>(null);
84
+ const [detailId, setDetailId] = useState<string | null>(null);
85
+ const [publishing, setPublishing] = useState(false);
86
+
87
+ const panes = pluginRegistry.panes;
88
+ const owned = useMemo(
89
+ () => filterGalleryEntries(buildOwnedEntries(layouts, activeIndex), query, panes),
90
+ [activeIndex, layouts, panes, query],
91
+ );
92
+ const community = useMemo(
93
+ () => filterGalleryEntries(buildCommunityEntries(discover.state.items), query, panes),
94
+ [discover.state.items, panes, query],
95
+ );
96
+ const entries = useMemo(() => [...owned, ...community], [community, owned]);
97
+ const detail = useMemo(
98
+ () => entries.find((entry) => entry.id === detailId) ?? null,
99
+ [detailId, entries],
100
+ );
101
+
102
+ const close = useCallback(() => {
103
+ if (onClose) onClose();
104
+ else pluginRegistry.hidePane("layout-marketplace");
105
+ }, [onClose, pluginRegistry]);
106
+
107
+ const activate = useCallback((entry: GalleryEntry) => {
108
+ if (entry.kind === "community") {
109
+ setDetailId(entry.id);
110
+ setSelectedId(entry.id);
111
+ return;
112
+ }
113
+ close();
114
+ if (entry.index !== null && entry.index !== activeIndex) {
115
+ dispatch({ type: "SWITCH_LAYOUT", index: entry.index });
116
+ }
117
+ }, [activeIndex, close, dispatch]);
118
+
119
+ const install = useCallback((entry: GalleryEntry) => {
120
+ const installed = materializeMarketplaceLayout(entry);
121
+ close();
122
+ dispatch({
123
+ type: "INSTALL_LAYOUT_COPY",
124
+ name: entry.name,
125
+ layout: installed.layout,
126
+ paneState: installed.paneState,
127
+ });
128
+ pluginRegistry.notify({ body: `Layout "${entry.name}" added`, type: "success" });
129
+ }, [close, dispatch, pluginRegistry]);
130
+
131
+ const requestSignIn = useCallback(() => {
132
+ if (!requestAuthDialog({ mode: "login" })) {
133
+ pluginRegistry.notify({ body: "Open Account Management to log in.", type: "info" });
134
+ }
135
+ }, [pluginRegistry]);
136
+
137
+ const promptName = useCallback(async (options: {
138
+ title: string;
139
+ label: string;
140
+ confirmLabel: string;
141
+ initialValue?: string;
142
+ }) => {
143
+ const name = await dialog.prompt<string | undefined>({
144
+ closeOnClickOutside: true,
145
+ content: (context: unknown) => (
146
+ <LayoutNameDialog
147
+ {...(context as PromptContext<string | undefined>)}
148
+ title={options.title}
149
+ label={options.label}
150
+ confirmLabel={options.confirmLabel}
151
+ initialValue={options.initialValue ?? ""}
152
+ />
153
+ ),
154
+ }).catch(() => undefined);
155
+ return typeof name === "string" ? name.trim() : "";
156
+ }, [dialog]);
157
+
158
+ const newLayout = useCallback(async () => {
159
+ const name = await promptName({
160
+ title: "New Layout",
161
+ label: "Layout name",
162
+ confirmLabel: "Create Layout",
163
+ });
164
+ if (!name) return;
165
+ close();
166
+ dispatch({ type: "NEW_LAYOUT", name });
167
+ pluginRegistry.notify({ body: `Layout "${name}" created`, type: "success" });
168
+ }, [close, dispatch, pluginRegistry, promptName]);
169
+
170
+ const renameLayout = useCallback(async (entry: GalleryEntry) => {
171
+ if (entry.index === null) return;
172
+ const name = await promptName({
173
+ title: "Rename Layout",
174
+ label: "New name",
175
+ confirmLabel: "Rename Layout",
176
+ initialValue: entry.name,
177
+ });
178
+ if (!name || name === entry.name) return;
179
+ dispatch({ type: "RENAME_LAYOUT", index: entry.index, name });
180
+ }, [dispatch, promptName]);
181
+
182
+ const duplicateLayout = useCallback((entry: GalleryEntry) => {
183
+ if (entry.index === null) return;
184
+ close();
185
+ dispatch({ type: "DUPLICATE_LAYOUT", index: entry.index });
186
+ pluginRegistry.notify({ body: `Layout "${entry.name}" duplicated`, type: "success" });
187
+ }, [close, dispatch, pluginRegistry]);
188
+
189
+ const deleteLayout = useCallback(async (entry: GalleryEntry) => {
190
+ if (entry.index === null || layouts.length <= 1) return;
191
+ const confirmed = await dialog.prompt<boolean>({
192
+ closeOnClickOutside: true,
193
+ content: (context: unknown) => (
194
+ <ConfirmDialog
195
+ {...(context as PromptContext<boolean>)}
196
+ title="Delete Layout"
197
+ body={[`Delete layout "${entry.name}"? This cannot be undone.`]}
198
+ confirmLabel="Delete Layout"
199
+ cancelLabel="Cancel"
200
+ width={48}
201
+ />
202
+ ),
203
+ }).catch(() => false);
204
+ if (confirmed !== true) return;
205
+ dispatch({ type: "DELETE_LAYOUT", index: entry.index });
206
+ pluginRegistry.notify({ body: `Layout "${entry.name}" deleted`, type: "success" });
207
+ }, [dialog, dispatch, layouts.length, pluginRegistry]);
208
+
209
+ const copyLink = useCallback((entry: GalleryEntry) => {
210
+ if (!entry.marketplaceId) return;
211
+ void renderer.copyText(publicMarketplaceLayoutUrl(entry.marketplaceId)).then(() => {
212
+ pluginRegistry.notify({ body: "Layout link copied", type: "success" });
213
+ }).catch(() => {
214
+ pluginRegistry.notify({ body: "Could not copy the layout link.", type: "error" });
215
+ });
216
+ }, [pluginRegistry, renderer]);
217
+
218
+ const publishCurrent = useCallback(async () => {
219
+ if (!signedIn) {
220
+ requestSignIn();
221
+ return;
222
+ }
223
+ const name = layouts[activeIndex]?.name || "Community Layout";
224
+ const confirmed = await dialog.prompt<boolean>({
225
+ closeOnClickOutside: true,
226
+ content: (context: unknown) => (
227
+ <ConfirmDialog
228
+ {...(context as PromptContext<boolean>)}
229
+ title="Publish Current Layout"
230
+ body={[
231
+ `Publish "${name}" to Discover?`,
232
+ "Pane setup, queries, chart views, drawings, and portable pane state will be public.",
233
+ "Credentials, accounts, portfolios, and fields marked private are excluded.",
234
+ ]}
235
+ confirmLabel="Publish Layout"
236
+ cancelLabel="Cancel"
237
+ confirmVariant="primary"
238
+ width={52}
239
+ />
240
+ ),
241
+ }).catch(() => false);
242
+ if (confirmed !== true) return;
243
+ setPublishing(true);
244
+ try {
245
+ const item = await discover.publish(
246
+ name,
247
+ publishableMarketplaceLayout(currentLayout, currentPaneState, panes),
248
+ );
249
+ try {
250
+ await renderer.copyText(publicMarketplaceLayoutUrl(item.id));
251
+ pluginRegistry.notify({ body: `Layout "${name}" published · link copied`, type: "success" });
252
+ } catch {
253
+ pluginRegistry.notify({ body: `Layout "${name}" published`, type: "success" });
254
+ }
255
+ } catch (error) {
256
+ pluginRegistry.notify({
257
+ body: error instanceof Error ? error.message : "Could not publish this layout.",
258
+ type: "error",
259
+ });
260
+ } finally {
261
+ setPublishing(false);
262
+ }
263
+ }, [activeIndex, currentLayout, currentPaneState, dialog, discover, layouts, panes, pluginRegistry, renderer, requestSignIn, signedIn]);
264
+
265
+ useShortcut((event) => {
266
+ if (event.name !== "escape") return;
267
+ event.preventDefault();
268
+ event.stopPropagation();
269
+ if (detailId) setDetailId(null);
270
+ else close();
271
+ }, { enabled: focused && !dialogOpen, phase: "before", allowEditable: true, scope: "layout-gallery" });
272
+
273
+ const controller: LayoutGalleryController = {
274
+ query,
275
+ setQuery,
276
+ owned,
277
+ community,
278
+ entries,
279
+ selectedId,
280
+ select: setSelectedId,
281
+ detail,
282
+ openDetail: (entry) => {
283
+ setSelectedId(entry.id);
284
+ setDetailId(entry.id);
285
+ },
286
+ closeDetail: () => setDetailId(null),
287
+ activate,
288
+ install,
289
+ discover,
290
+ signedIn,
291
+ requestSignIn,
292
+ publishCurrent,
293
+ copyLink,
294
+ publishing,
295
+ newLayout,
296
+ renameLayout,
297
+ duplicateLayout,
298
+ deleteLayout,
299
+ canDelete: layouts.length > 1,
300
+ close,
301
+ panes,
302
+ missingPaneIds: (layout) => missingPaneIds(layout, panes),
303
+ };
304
+
305
+ return useUiHost().kind === "desktop-web"
306
+ ? (
307
+ <LayoutGalleryDesktop
308
+ controller={controller}
309
+ focused={focused}
310
+ width={width}
311
+ height={height}
312
+ />
313
+ )
314
+ : (
315
+ <LayoutGalleryTerminal
316
+ controller={controller}
317
+ dialogOpen={dialogOpen}
318
+ focused={focused}
319
+ width={width}
320
+ height={height}
321
+ />
322
+ );
323
+ }
@@ -0,0 +1,47 @@
1
+ import { expect, test } from "bun:test";
2
+ import { cloneLayout, createDefaultConfig } from "../types/config";
3
+ import { buildLayoutPreviewRects } from "./geometry";
4
+
5
+ const layout = (() => {
6
+ const next = cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout);
7
+ next.floating = [{ instanceId: "chat:main", x: 40, y: 6, width: 30, height: 10 }];
8
+ next.detached = [{ instanceId: "quote-monitor:main", x: 70, y: 2, width: 24, height: 8 }];
9
+ return next;
10
+ })();
11
+
12
+ test("maps docked, floating, and detached panes into in-bounds preview rects", () => {
13
+ const bounds = { width: 40, height: 7 };
14
+ const rects = buildLayoutPreviewRects(layout, bounds, { width: 120, height: 40 });
15
+
16
+ expect(rects.filter((entry) => entry.kind === "docked").length).toBe(3);
17
+ expect(rects.filter((entry) => entry.kind === "floating").length).toBe(1);
18
+ expect(rects.filter((entry) => entry.kind === "detached").length).toBe(1);
19
+ for (const { rect } of rects) {
20
+ expect(rect.width).toBeGreaterThanOrEqual(2);
21
+ expect(rect.height).toBeGreaterThanOrEqual(2);
22
+ expect(rect.x + rect.width).toBeLessThanOrEqual(bounds.width);
23
+ expect(rect.y + rect.height).toBeLessThanOrEqual(bounds.height);
24
+ }
25
+ });
26
+
27
+ test("keeps pixel previews fractional and layered above docked panes", () => {
28
+ const bounds = { width: 320, height: 180 };
29
+ const rects = buildLayoutPreviewRects(layout, bounds, { width: 120, height: 40 }, {
30
+ minSize: 10,
31
+ fractional: true,
32
+ });
33
+
34
+ const docked = rects.filter((entry) => entry.kind === "docked");
35
+ const floating = rects.find((entry) => entry.kind === "floating")!;
36
+ const detached = rects.find((entry) => entry.kind === "detached")!;
37
+ expect(floating.depth).toBeGreaterThan(docked[0]!.depth);
38
+ expect(detached.depth).toBeGreaterThan(floating.depth);
39
+ expect(floating.instanceId).toBe("chat:main");
40
+ // 40/120 of a 320px card, kept fractional instead of snapped to whole cells.
41
+ expect(floating.rect.x).toBeCloseTo(106.67, 1);
42
+ for (const { rect } of rects) {
43
+ expect(rect.width).toBeGreaterThanOrEqual(10);
44
+ expect(rect.x + rect.width).toBeLessThanOrEqual(bounds.width + 0.001);
45
+ expect(rect.y + rect.height).toBeLessThanOrEqual(bounds.height + 0.001);
46
+ }
47
+ });
@@ -0,0 +1,80 @@
1
+ import { getDockLeafLayouts, type LayoutBounds } from "../plugins/pane-manager";
2
+ import type { LayoutConfig } from "../types/config";
3
+
4
+ export type LayoutPreviewKind = "docked" | "floating" | "detached";
5
+
6
+ export interface LayoutPreviewRect {
7
+ key: string;
8
+ instanceId: string;
9
+ kind: LayoutPreviewKind;
10
+ rect: LayoutBounds;
11
+ /** Paint order: floating sits over docked, detached over both. */
12
+ depth: number;
13
+ }
14
+
15
+ /** Below this a rect is too small to hold a readable pane header. */
16
+ const DEFAULT_MIN_SIZE = 2;
17
+
18
+ function snapRect(
19
+ rect: LayoutBounds,
20
+ bounds: { width: number; height: number },
21
+ minSize: number,
22
+ round: (value: number) => number,
23
+ ): LayoutBounds {
24
+ const maxX = Math.max(0, bounds.width - minSize);
25
+ const maxY = Math.max(0, bounds.height - minSize);
26
+ const x = Math.max(0, Math.min(maxX, round(rect.x)));
27
+ const y = Math.max(0, Math.min(maxY, round(rect.y)));
28
+ return {
29
+ x,
30
+ y,
31
+ width: Math.max(minSize, Math.min(bounds.width - x, round(rect.x + rect.width) - x)),
32
+ height: Math.max(minSize, Math.min(bounds.height - y, round(rect.y + rect.height) - y)),
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Docked panes come from the real dock geometry at preview scale; floating and
38
+ * detached panes are stored in screen cells, so they are scaled by the screen
39
+ * the layout is being previewed against. Units are whatever the caller passes:
40
+ * terminal cells or CSS pixels.
41
+ */
42
+ export function buildLayoutPreviewRects(
43
+ layout: LayoutConfig,
44
+ bounds: { width: number; height: number },
45
+ screen: { width: number; height: number },
46
+ options?: { minSize?: number; fractional?: boolean },
47
+ ): LayoutPreviewRect[] {
48
+ const minSize = options?.minSize ?? DEFAULT_MIN_SIZE;
49
+ const round = options?.fractional ? (value: number) => value : Math.round;
50
+ const scaleX = bounds.width / Math.max(1, screen.width);
51
+ const scaleY = bounds.height / Math.max(1, screen.height);
52
+ const scaled = (
53
+ entries: readonly { instanceId: string; x: number; y: number; width: number; height: number }[],
54
+ kind: LayoutPreviewKind,
55
+ depth: number,
56
+ ): LayoutPreviewRect[] => entries.map((entry) => ({
57
+ key: `${kind}:${entry.instanceId}`,
58
+ instanceId: entry.instanceId,
59
+ kind,
60
+ depth,
61
+ rect: snapRect({
62
+ x: entry.x * scaleX,
63
+ y: entry.y * scaleY,
64
+ width: entry.width * scaleX,
65
+ height: entry.height * scaleY,
66
+ }, bounds, minSize, round),
67
+ }));
68
+
69
+ return [
70
+ ...getDockLeafLayouts(layout, { x: 0, y: 0, ...bounds }, { precise: true }).map((leaf) => ({
71
+ key: `docked:${leaf.instanceId}`,
72
+ instanceId: leaf.instanceId,
73
+ kind: "docked" as const,
74
+ depth: 1,
75
+ rect: snapRect(leaf.rect, bounds, minSize, round),
76
+ })),
77
+ ...scaled(layout.floating, "floating", 2),
78
+ ...scaled(layout.detached ?? [], "detached", 3),
79
+ ];
80
+ }
@@ -0,0 +1,122 @@
1
+ /** @jsxImportSource react */
2
+ import { Window } from "happy-dom";
3
+
4
+ const testWindow = new Window({ url: "http://localhost" });
5
+ const domGlobals = {
6
+ IS_REACT_ACT_ENVIRONMENT: true,
7
+ window: testWindow,
8
+ document: testWindow.document,
9
+ navigator: testWindow.navigator,
10
+ HTMLElement: testWindow.HTMLElement,
11
+ Node: testWindow.Node,
12
+ };
13
+
14
+ /** Bun shares one process across test files, so the DOM globals must not leak. */
15
+ const priorGlobals = Object.fromEntries(
16
+ Object.keys(domGlobals).map((key) => [key, (globalThis as Record<string, unknown>)[key]]),
17
+ );
18
+ Object.assign(globalThis, domGlobals);
19
+
20
+ import { afterAll, expect, test } from "bun:test";
21
+ import { act } from "react";
22
+ import { createRoot } from "react-dom/client";
23
+ import { UiHostProvider, type RendererHost, type UiHost } from "../ui";
24
+ import { WebBox } from "../renderers/electrobun/view/host/box";
25
+ import { WebText } from "../renderers/electrobun/view/host/text";
26
+ import { cloneLayout, createDefaultConfig } from "../types/config";
27
+ import type { PaneDef } from "../types/plugin";
28
+ import { MiniWorkspace } from "./mini-workspace";
29
+
30
+ const renderer: RendererHost = {
31
+ requestExit() {},
32
+ async openExternal() {},
33
+ async copyText() {},
34
+ async readText() { return ""; },
35
+ notify() {},
36
+ };
37
+
38
+ const ui = {
39
+ kind: "desktop-web",
40
+ capabilities: { cellWidthPx: 8, fractionalViewport: true },
41
+ Box: WebBox,
42
+ Text: WebText,
43
+ } as unknown as UiHost;
44
+
45
+ afterAll(() => {
46
+ for (const [key, value] of Object.entries(priorGlobals)) {
47
+ if (value === undefined) delete (globalThis as Record<string, unknown>)[key];
48
+ else (globalThis as Record<string, unknown>)[key] = value;
49
+ }
50
+ });
51
+
52
+ function paneDef(id: string, name: string, icon: string): PaneDef {
53
+ return { id, name, icon, component: () => null, defaultPosition: "left" };
54
+ }
55
+
56
+ const panes = new Map<string, PaneDef>([
57
+ ["portfolio-list", paneDef("portfolio-list", "Portfolio", "P")],
58
+ ["ticker-research", paneDef("ticker-research", "Ticker Research", "T")],
59
+ ["chat", paneDef("chat", "Chat", "M")],
60
+ ["ticker-chart", paneDef("ticker-chart", "Chart", "C")],
61
+ ]);
62
+
63
+ function testLayout() {
64
+ const layout = cloneLayout(createDefaultConfig("/tmp/gloomberb-mini-workspace-test").layout);
65
+ layout.instances = [
66
+ ...layout.instances,
67
+ { instanceId: "ticker-chart:1", paneId: "ticker-chart", binding: { kind: "fixed", symbol: "NVDA" } },
68
+ { instanceId: "mystery:1", paneId: "mystery-pane" },
69
+ ];
70
+ layout.floating = [{ instanceId: "ticker-chart:1", x: 20, y: 6, width: 60, height: 20 }];
71
+ layout.detached = [{ instanceId: "mystery:1", x: 120, y: 4, width: 50, height: 18 }];
72
+ return layout;
73
+ }
74
+
75
+ async function renderPreview() {
76
+ const container = testWindow.document.createElement("div");
77
+ testWindow.document.body.appendChild(container);
78
+ const root = createRoot(container as unknown as HTMLElement);
79
+ await act(async () => {
80
+ root.render(
81
+ <UiHostProvider ui={ui} renderer={renderer}>
82
+ <MiniWorkspace layout={testLayout()} panes={panes} width={320} height={160} />
83
+ </UiHostProvider>,
84
+ );
85
+ });
86
+ return { container, root };
87
+ }
88
+
89
+ test("draws every pane with a readable header instead of an empty rectangle", async () => {
90
+ const { container, root } = await renderPreview();
91
+
92
+ const labels = [...container.querySelectorAll("text")].map((node) => node.textContent ?? "");
93
+ expect(labels.length).toBe(5);
94
+ expect(labels.some((label) => label.includes("Portfolio"))).toBe(true);
95
+ expect(labels.some((label) => label.includes("Chat"))).toBe(true);
96
+ // A published fixed ticker is public metadata and belongs on the header.
97
+ expect(labels.some((label) => label.includes("Chart") && label.includes("NVDA"))).toBe(true);
98
+ // An uninstalled pane type keeps its id as the label rather than going blank.
99
+ expect(labels.some((label) => label.includes("mystery-pane"))).toBe(true);
100
+ expect(labels.every((label) => label.trim().length > 0)).toBe(true);
101
+
102
+ await act(async () => root.unmount());
103
+ });
104
+
105
+ test("layers floating and detached panes and hatches missing pane types", async () => {
106
+ const { container, root } = await renderPreview();
107
+
108
+ const svg = container.querySelector("svg")!;
109
+ expect(svg.getAttribute("aria-label")).toContain("Chart NVDA");
110
+ // Missing pane types get the hatch fill; installed panes keep real imagery.
111
+ expect(container.querySelector("rect[fill^='url(#gloom-missing-pane-']")).not.toBeNull();
112
+ expect(container.querySelectorAll("path[stroke-linejoin='round']").length).toBeGreaterThan(0);
113
+
114
+ const groups = [...container.querySelectorAll("g")];
115
+ const detachedIndex = groups.findIndex((group) => group.textContent?.includes("mystery-pane"));
116
+ const floatingIndex = groups.findIndex((group) => group.textContent?.includes("NVDA"));
117
+ const dockedIndex = groups.findIndex((group) => group.textContent?.includes("Portfolio"));
118
+ expect(dockedIndex).toBeLessThan(floatingIndex);
119
+ expect(floatingIndex).toBeLessThan(detachedIndex);
120
+
121
+ await act(async () => root.unmount());
122
+ });