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,133 @@
1
+ /** @jsxImportSource react */
2
+ import type { CSSProperties } from "react";
3
+ import type { DockLayoutNode, PaneInstanceConfig } from "../../types/config";
4
+ import type { LayoutMarketplaceEntry } from "../../layout-marketplace/payload";
5
+
6
+ const PANE_NAMES: Record<string, string> = {
7
+ "chart-composer": "Chart",
8
+ "credit-conditions": "Credit Spreads",
9
+ "earnings-calendar": "Earnings Calendar",
10
+ "fx-matrix": "FX Cross Rates",
11
+ "kelly-sizer": "Position Sizer",
12
+ "market-halts": "Market Halts",
13
+ "market-heatmap": "Market Heatmap",
14
+ "news-breaking": "Breaking News",
15
+ "news-feed": "News Feed",
16
+ "news-industry": "Sector News",
17
+ "news-top": "Top News",
18
+ "options-calculator": "Options Calculator",
19
+ "portfolio-list": "Portfolio",
20
+ "prediction-markets": "Prediction Markets",
21
+ "quick-notes": "Notes",
22
+ sec: "SEC",
23
+ "thirteenf-funds": "13F Funds",
24
+ "ticker-news": "Ticker News",
25
+ "ticker-research": "Ticker Research",
26
+ "twitter-feed": "X Feed",
27
+ "volatility-term-structure": "VIX Term Structure",
28
+ "world-indices": "World Indices",
29
+ };
30
+
31
+ export function marketplacePaneName(paneId: string): string {
32
+ return PANE_NAMES[paneId] ?? paneId
33
+ .split("-")
34
+ .filter(Boolean)
35
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
36
+ .join(" ");
37
+ }
38
+
39
+ function paneSubtitle(pane: PaneInstanceConfig): string | null {
40
+ if (pane.binding?.kind === "fixed") return pane.binding.symbol;
41
+ const query = pane.params?.query;
42
+ return query?.trim() ? query.trim().slice(0, 80) : null;
43
+ }
44
+
45
+ function PaneCard({ pane }: { pane: PaneInstanceConfig | undefined }) {
46
+ if (!pane) return <div className="layout-pane missing">Unavailable pane</div>;
47
+ const subtitle = paneSubtitle(pane);
48
+ return (
49
+ <div className="layout-pane">
50
+ <strong>{pane.title?.trim() || marketplacePaneName(pane.paneId)}</strong>
51
+ {subtitle ? <span>{subtitle}</span> : null}
52
+ </div>
53
+ );
54
+ }
55
+
56
+ function DockPreview({
57
+ node,
58
+ panes,
59
+ }: {
60
+ node: DockLayoutNode;
61
+ panes: ReadonlyMap<string, PaneInstanceConfig>;
62
+ }) {
63
+ if (node.kind === "pane") return <PaneCard pane={panes.get(node.instanceId)} />;
64
+ const firstStyle = { "--share-ratio": node.ratio } as CSSProperties;
65
+ const secondStyle = { "--share-ratio": 1 - node.ratio } as CSSProperties;
66
+ return (
67
+ <div className={`layout-split ${node.axis}`}>
68
+ <div className="layout-split-child" style={firstStyle}>
69
+ <DockPreview node={node.first} panes={panes} />
70
+ </div>
71
+ <div className="layout-split-child" style={secondStyle}>
72
+ <DockPreview node={node.second} panes={panes} />
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function formatDate(value: string): string {
79
+ const date = new Date(value);
80
+ return Number.isNaN(date.getTime())
81
+ ? ""
82
+ : date.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
83
+ }
84
+
85
+ export function LayoutShareView({
86
+ entry,
87
+ openLiveUrl,
88
+ }: {
89
+ entry: LayoutMarketplaceEntry;
90
+ openLiveUrl: string;
91
+ }) {
92
+ const panes = new Map(entry.layout.instances.map((pane) => [pane.instanceId, pane]));
93
+ const docked = new Set<string>();
94
+ const collect = (node: DockLayoutNode | null) => {
95
+ if (!node) return;
96
+ if (node.kind === "pane") {
97
+ docked.add(node.instanceId);
98
+ return;
99
+ }
100
+ collect(node.first);
101
+ collect(node.second);
102
+ };
103
+ collect(entry.layout.dockRoot);
104
+ const extra = entry.layout.instances.filter((pane) => !docked.has(pane.instanceId));
105
+ const author = entry.author.username ? `@${entry.author.username}` : entry.author.displayName;
106
+ const date = formatDate(entry.publishedAt);
107
+
108
+ return (
109
+ <main className="layout-share wide">
110
+ <header>
111
+ <p className="eyebrow">Shared via Gloomberb</p>
112
+ <h1>{entry.name}</h1>
113
+ <p className="layout-meta">{[author, date, `${entry.layout.instances.length} panes`].filter(Boolean).join(" · ")}</p>
114
+ </header>
115
+
116
+ <section className="layout-workspace" aria-label={`${entry.name} layout preview`}>
117
+ {entry.layout.dockRoot
118
+ ? <DockPreview node={entry.layout.dockRoot} panes={panes} />
119
+ : <p>No docked panes</p>}
120
+ {extra.length > 0 ? (
121
+ <div className="layout-extra-panes">
122
+ {extra.map((pane) => <PaneCard key={pane.instanceId} pane={pane} />)}
123
+ </div>
124
+ ) : null}
125
+ </section>
126
+
127
+ <div className="handoff">
128
+ <a className="cta" href={openLiveUrl}>Use this layout</a>
129
+ <p className="descriptor">Opens in Gloomberb as an independent, editable copy.</p>
130
+ </div>
131
+ </main>
132
+ );
133
+ }
@@ -0,0 +1,102 @@
1
+ /** @jsxImportSource react */
2
+ import "./styles.css";
3
+ import { useEffect, useState } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import {
6
+ getPublicMarketplaceLayout,
7
+ openLiveMarketplaceLayoutUrl,
8
+ parseMarketplaceLayoutId,
9
+ } from "../../layout-marketplace/api";
10
+ import type { LayoutMarketplaceEntry } from "../../layout-marketplace/payload";
11
+ import {
12
+ deleteShare,
13
+ getShare,
14
+ openLiveShareUrl,
15
+ parseShareId,
16
+ type ShareRecord,
17
+ } from "../../shares/api";
18
+ import { LayoutShareView } from "./layout-view";
19
+ import { ShareView } from "./view";
20
+
21
+ function LayoutApp({ id }: { id: string }) {
22
+ const [state, setState] = useState<{
23
+ entry?: LayoutMarketplaceEntry;
24
+ error?: string;
25
+ }>({});
26
+ useEffect(() => {
27
+ const controller = new AbortController();
28
+ getPublicMarketplaceLayout(
29
+ id,
30
+ (url, init) => fetch(url, { ...init, signal: controller.signal }),
31
+ )
32
+ .then((entry) => setState(entry
33
+ ? { entry }
34
+ : { error: "This shared layout is unavailable." }))
35
+ .catch(() => {
36
+ if (!controller.signal.aborted) setState({ error: "This shared layout could not be loaded." });
37
+ });
38
+ return () => controller.abort();
39
+ }, [id]);
40
+
41
+ return state.entry
42
+ ? <LayoutShareView entry={state.entry} openLiveUrl={openLiveMarketplaceLayoutUrl(id)} />
43
+ : <main><h1>Gloomberb</h1><p>{state.error ?? "Loading shared layout..."}</p></main>;
44
+ }
45
+
46
+ function ContentShareApp({ id }: { id: string }) {
47
+ const [state, setState] = useState<{
48
+ share?: ShareRecord;
49
+ error?: string;
50
+ deleting?: boolean;
51
+ deleted?: boolean;
52
+ }>({});
53
+ useEffect(() => {
54
+ const controller = new AbortController();
55
+ getShare(id, (url, init) => fetch(url, { ...init, signal: controller.signal }))
56
+ .then((share) => setState(share ? { share } : { error: "This share is unavailable or has expired." }))
57
+ .catch(() => { if (!controller.signal.aborted) setState({ error: "This share could not be loaded." }); });
58
+ return () => controller.abort();
59
+ }, [id]);
60
+ const remove = async () => {
61
+ if (!state.share?.ownedByViewer || state.deleting) return;
62
+ setState((current) => ({ ...current, deleting: true, error: undefined }));
63
+ try {
64
+ await deleteShare(id);
65
+ setState({ deleted: true });
66
+ } catch (error) {
67
+ setState((current) => ({
68
+ ...current,
69
+ deleting: false,
70
+ error: error instanceof Error ? error.message : "Could not delete share.",
71
+ }));
72
+ }
73
+ };
74
+ if (state.deleted) return <main><h1>Share deleted</h1><p>This link is no longer available.</p></main>;
75
+ if (state.share) {
76
+ return (
77
+ <ShareView
78
+ share={state.share}
79
+ openLiveUrl={openLiveShareUrl(id)}
80
+ deleting={state.deleting === true}
81
+ deleteError={state.error}
82
+ onDelete={state.share.ownedByViewer ? remove : undefined}
83
+ />
84
+ );
85
+ }
86
+ return <main><h1>Gloomberb</h1><p>{state.error ?? "Loading shared view..."}</p></main>;
87
+ }
88
+
89
+ function SocialShareApp() {
90
+ const pathname = window.location.pathname;
91
+ const layoutId = parseMarketplaceLayoutId(pathname);
92
+ if (layoutId) return <LayoutApp id={layoutId} />;
93
+ if (pathname.startsWith("/l/")) return <main><h1>Gloomberb</h1><p>Invalid layout link.</p></main>;
94
+ const shareId = parseShareId(pathname);
95
+ return shareId
96
+ ? <ContentShareApp id={shareId} />
97
+ : <main><h1>Gloomberb</h1><p>Invalid share link.</p></main>;
98
+ }
99
+
100
+ const root = document.getElementById("root");
101
+ if (!root) throw new Error("Missing root element");
102
+ createRoot(root).render(<SocialShareApp />);
@@ -0,0 +1,54 @@
1
+ html { color-scheme: dark; background: #272a34; color: #f2f2f2; font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
2
+ body { margin: 0; }
3
+ main { width: min(760px, calc(100% - 40px)); margin: 0 auto; padding: 64px 0; }
4
+ main.wide { width: min(1180px, calc(100% - 40px)); }
5
+ h1 { margin: 0 0 28px; color: #f1b94a; font-size: 24px; }
6
+ a { display: inline-block; margin-top: 24px; color: #f1b94a; }
7
+ .article-text { white-space: pre-wrap; overflow-wrap: anywhere; }
8
+ .table-wrap { overflow: auto; border: 1px solid #4a4e5c; }
9
+ table { width: 100%; border-collapse: collapse; }
10
+ th, td { padding: 8px 10px; border-bottom: 1px solid #3a3e49; text-align: left; white-space: nowrap; }
11
+ th { color: #f1b94a; background: #30333d; }
12
+ .chart { width: 100%; height: auto; border: 1px solid #4a4e5c; background: #20232b; }
13
+ .series { fill: none; stroke: #f1b94a; stroke-width: 2; vector-effect: non-scaling-stroke; }
14
+ .series-1 { stroke: #62b8f6; } .series-2 { stroke: #7fd18a; } .series-3 { stroke: #ef7f7f; } .series-4 { stroke: #b697ef; } .series-5 { stroke: #e29b63; }
15
+ .legend { display: flex; flex-wrap: wrap; gap: 10px 24px; padding: 0; list-style: none; color: #b8bbc5; }
16
+ .owner-actions { display: flex; align-items: center; gap: 12px; margin-top: 32px; }
17
+ .owner-actions button { border: 1px solid #6d7180; border-radius: 4px; padding: 6px 10px; background: transparent; color: #f2f2f2; font: inherit; cursor: pointer; }
18
+ .owner-actions button:disabled { cursor: default; opacity: 0.55; }
19
+ .owner-actions [role="alert"] { color: #ef7f7f; }
20
+ :focus-visible { outline: 2px solid #f1b94a; outline-offset: 2px; }
21
+ .eyebrow { margin: 0 0 10px; color: #b8bbc5; font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase; }
22
+ main.pane h1 { margin-bottom: 14px; overflow-wrap: anywhere; }
23
+ .pane-description { margin: 0; max-width: 62ch; color: #d5d7de; overflow-wrap: anywhere; }
24
+ .pane-facts { margin: 28px 0 0; border: 1px solid #4a4e5c; background: #20232b; }
25
+ .pane-facts > div { display: flex; gap: 16px; padding: 8px 12px; border-bottom: 1px solid #3a3e49; }
26
+ .pane-facts > div:last-child { border-bottom: none; }
27
+ .pane-facts dt { flex: 0 0 clamp(110px, 26%, 200px); color: #b8bbc5; }
28
+ .pane-facts dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
29
+ .handoff { margin-top: 32px; }
30
+ .cta { margin: 0; border: 1px solid #f1b94a; border-radius: 4px; padding: 10px 16px; color: #f1b94a; text-decoration: none; }
31
+ .cta:hover { background: #f1b94a; color: #20232b; }
32
+ .descriptor { margin: 14px 0 0; max-width: 58ch; color: #b8bbc5; }
33
+ .layout-share h1 { margin-bottom: 10px; overflow-wrap: anywhere; }
34
+ .layout-meta { margin: 0; color: #b8bbc5; }
35
+ .layout-workspace { position: relative; display: flex; height: 440px; margin-top: 28px; padding: 10px; border: 1px solid #4a4e5c; background: #181b22; overflow: hidden; }
36
+ .layout-split { display: flex; flex: 1 1 0; min-width: 0; min-height: 0; gap: 8px; }
37
+ .layout-split.horizontal { flex-direction: row; }
38
+ .layout-split.vertical { flex-direction: column; }
39
+ .layout-split-child { display: flex; flex-grow: var(--share-ratio); flex-basis: 0; min-width: 0; min-height: 0; }
40
+ .layout-pane { display: flex; flex: 1 1 0; min-width: 0; min-height: 42px; flex-direction: column; justify-content: center; gap: 4px; padding: 10px 12px; border: 1px solid #3a4759; background: #20232b; overflow: hidden; }
41
+ .layout-pane strong { color: #f1b94a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
42
+ .layout-pane span { color: #b8bbc5; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
43
+ .layout-pane.missing { color: #b8bbc5; }
44
+ .layout-extra-panes { position: absolute; right: 20px; bottom: 20px; display: grid; width: min(360px, 42%); gap: 8px; padding: 8px; border: 1px solid #52657d; background: #181b22; }
45
+ @media (max-width: 640px) {
46
+ main { padding: 40px 0; }
47
+ .pane-facts > div { flex-direction: column; gap: 2px; }
48
+ .pane-facts dt { flex: none; }
49
+ .cta { display: block; text-align: center; }
50
+ .layout-workspace { height: 360px; padding: 6px; }
51
+ .layout-split { gap: 4px; }
52
+ .layout-pane { padding: 7px; font-size: 12px; }
53
+ .layout-extra-panes { right: 10px; bottom: 10px; width: 52%; }
54
+ }
@@ -0,0 +1,100 @@
1
+ import { expect, test } from "bun:test";
2
+ import { renderToStaticMarkup } from "react-dom/server";
3
+ import { ShareView } from "./view";
4
+
5
+ test("share rendering escapes content and protects external links", () => {
6
+ const html = renderToStaticMarkup(<ShareView share={{
7
+ kind: "article",
8
+ data: {
9
+ title: "<img src=x onerror=alert(1)>",
10
+ text: "<script>alert(1)</script>",
11
+ sourceUrl: "https://example.com/story",
12
+ },
13
+ createdAt: "2026-08-21T00:00:00Z",
14
+ expiresAt: "2026-09-20T00:00:00Z",
15
+ ownedByViewer: true,
16
+ }} onDelete={() => {}} />);
17
+ expect(html).not.toContain("<script>");
18
+ expect(html).toContain("&lt;script&gt;");
19
+ expect(html).toContain('target="_blank"');
20
+ expect(html).toContain('rel="noopener noreferrer"');
21
+ expect(html).toContain("Delete share");
22
+ });
23
+
24
+ test("portable pane share renders sanitized pane configuration", () => {
25
+ const html = renderToStaticMarkup(<ShareView
26
+ share={{
27
+ kind: "pane",
28
+ data: {
29
+ version: 2,
30
+ title: "Fed Markets",
31
+ description: "Prediction markets filtered to the Fed.",
32
+ layout: {
33
+ schemaVersion: 2,
34
+ sourceConfigVersion: 13,
35
+ layout: {
36
+ dockRoot: null,
37
+ instances: [{
38
+ instanceId: "p1",
39
+ paneId: "prediction-markets",
40
+ binding: { kind: "none" },
41
+ params: { query: "fed", scope: "polymarket" },
42
+ }],
43
+ floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
44
+ detached: [],
45
+ },
46
+ paneState: {},
47
+ },
48
+ },
49
+ createdAt: "2026-08-25T00:00:00Z",
50
+ expiresAt: "2026-09-24T00:00:00Z",
51
+ ownedByViewer: false,
52
+ }}
53
+ openLiveUrl="https://api.gloom.sh/shares/abc/open"
54
+ />);
55
+ expect(html).toContain("Fed Markets");
56
+ expect(html).toContain("Prediction Markets");
57
+ expect(html).toContain("Query");
58
+ expect(html).toContain("fed");
59
+ expect(html).toContain("polymarket");
60
+ expect(html).toContain("Explore this pane live");
61
+ expect(html).not.toContain("p1");
62
+ });
63
+
64
+ test("pane share renders the handoff copy, tracked CTA and printable facts only", () => {
65
+ const html = renderToStaticMarkup(<ShareView
66
+ share={{
67
+ kind: "pane",
68
+ data: {
69
+ version: 1,
70
+ templateId: "watchlist",
71
+ title: "Momentum <script>alert(1)</script>",
72
+ description: "Movers I watch daily.",
73
+ data: {
74
+ symbols: ["AAPL", "MSFT"],
75
+ lookbackDays: 90,
76
+ blank: " ",
77
+ nestedConfig: { hidden: true },
78
+ },
79
+ },
80
+ createdAt: "2026-08-25T00:00:00Z",
81
+ expiresAt: "2026-09-24T00:00:00Z",
82
+ ownedByViewer: true,
83
+ }}
84
+ openLiveUrl="https://api.gloom.sh/shares/abc/open"
85
+ onDelete={() => {}}
86
+ />);
87
+ expect(html).toContain("Shared via Gloomberb");
88
+ expect(html).toContain("A free, open-source finance terminal for market data, charts, and research.");
89
+ expect(html).toContain('href="https://api.gloom.sh/shares/abc/open"');
90
+ expect(html).toContain("Explore this pane live");
91
+ expect(html).toContain("Movers I watch daily.");
92
+ expect(html).toContain("Symbols");
93
+ expect(html).toContain("AAPL, MSFT");
94
+ expect(html).toContain("Lookback Days");
95
+ expect(html).not.toContain("[object Object]");
96
+ expect(html).not.toContain("Nested Config");
97
+ expect(html).not.toContain("Blank");
98
+ expect(html).not.toContain("<script>");
99
+ expect(html).toContain("Delete share");
100
+ });
@@ -0,0 +1,150 @@
1
+ /** @jsxImportSource react */
2
+ import type { ShareRecord } from "../../shares/api";
3
+
4
+ const FACT_LIMIT = 8;
5
+ const FACT_MAX_LENGTH = 120;
6
+
7
+ function factValue(value: unknown): string | null {
8
+ if (typeof value === "string") return value.trim() ? value.trim().slice(0, FACT_MAX_LENGTH) : null;
9
+ if (typeof value === "number") return Number.isFinite(value) ? String(value) : null;
10
+ if (typeof value === "boolean") return value ? "Yes" : "No";
11
+ if (Array.isArray(value)) {
12
+ const items = value.filter((item) => typeof item === "string" || typeof item === "number");
13
+ return items.length ? items.join(", ").slice(0, FACT_MAX_LENGTH) : null;
14
+ }
15
+ return null;
16
+ }
17
+
18
+ function factLabel(key: string): string {
19
+ const spaced = key.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
20
+ return spaced ? spaced.charAt(0).toUpperCase() + spaced.slice(1) : key;
21
+ }
22
+
23
+ function paneFacts(data: Record<string, unknown>): Array<{ key: string; label: string; value: string }> {
24
+ const facts: Array<{ key: string; label: string; value: string }> = [];
25
+ for (const [key, raw] of Object.entries(data)) {
26
+ const value = factValue(raw);
27
+ if (value !== null) facts.push({ key, label: factLabel(key), value });
28
+ if (facts.length === FACT_LIMIT) break;
29
+ }
30
+ return facts;
31
+ }
32
+
33
+ function SourceLink({ url }: { url?: string }) {
34
+ return url ? <a href={url} target="_blank" rel="noopener noreferrer">View source</a> : null;
35
+ }
36
+
37
+ function OwnerActions({
38
+ deleting,
39
+ error,
40
+ onDelete,
41
+ }: {
42
+ deleting: boolean;
43
+ error?: string;
44
+ onDelete?: () => void;
45
+ }) {
46
+ if (!onDelete) return null;
47
+ return (
48
+ <div className="owner-actions">
49
+ <button type="button" disabled={deleting} onClick={onDelete}>
50
+ {deleting ? "Deleting..." : "Delete share"}
51
+ </button>
52
+ {error ? <span role="alert">{error}</span> : null}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function ShareView({
58
+ share,
59
+ openLiveUrl,
60
+ deleting = false,
61
+ deleteError,
62
+ onDelete,
63
+ }: {
64
+ share: ShareRecord;
65
+ /** Platform-tracked `/shares/:id/open` URL. */
66
+ openLiveUrl?: string;
67
+ deleting?: boolean;
68
+ deleteError?: string;
69
+ onDelete?: () => void;
70
+ }) {
71
+ const ownerActions = (
72
+ <OwnerActions deleting={deleting} error={deleteError} onDelete={onDelete} />
73
+ );
74
+ if (share.kind === "pane") {
75
+ const instance = share.data.version === 2
76
+ ? share.data.layout.layout.instances[0]
77
+ : null;
78
+ const facts = share.data.version === 1
79
+ ? paneFacts(share.data.data)
80
+ : paneFacts({
81
+ paneType: instance
82
+ ? factLabel(instance.paneId).replace(/\b\w/g, (character) => character.toUpperCase())
83
+ : undefined,
84
+ ...(instance?.binding?.kind === "fixed" ? { symbol: instance.binding.symbol } : {}),
85
+ ...(instance?.params ?? {}),
86
+ ...(instance?.settings ?? {}),
87
+ });
88
+ return (
89
+ <main className="pane">
90
+ <header>
91
+ <p className="eyebrow">Shared via Gloomberb</p>
92
+ <h1>{share.data.title}</h1>
93
+ {share.data.description ? <p className="pane-description">{share.data.description}</p> : null}
94
+ </header>
95
+ {facts.length ? (
96
+ <dl className="pane-facts">
97
+ {facts.map((fact) => (
98
+ <div key={fact.key}>
99
+ <dt>{fact.label}</dt>
100
+ <dd>{fact.value}</dd>
101
+ </div>
102
+ ))}
103
+ </dl>
104
+ ) : null}
105
+ <div className="handoff">
106
+ {openLiveUrl ? <a className="cta" href={openLiveUrl}>Explore this pane live</a> : null}
107
+ <p className="descriptor">A free, open-source finance terminal for market data, charts, and research.</p>
108
+ </div>
109
+ {ownerActions}
110
+ </main>
111
+ );
112
+ }
113
+ if (share.kind === "article") {
114
+ return <main><h1>{share.data.title}</h1><p className="article-text">{share.data.text}</p><SourceLink url={share.data.sourceUrl} />{ownerActions}</main>;
115
+ }
116
+ if (share.kind === "table") {
117
+ return (
118
+ <main className="wide">
119
+ <h1>{share.data.title}</h1>
120
+ <div className="table-wrap"><table><thead><tr>{share.data.columns.map((column) => <th key={column.key}>{column.label}</th>)}</tr></thead>
121
+ <tbody>{share.data.rows.map((row, index) => <tr key={index}>{share.data.columns.map((column) => <td key={column.key}>{String(row[column.key] ?? "")}</td>)}</tr>)}</tbody>
122
+ </table></div>
123
+ <SourceLink url={share.data.sourceUrl} />
124
+ {ownerActions}
125
+ </main>
126
+ );
127
+ }
128
+ const values = share.data.series.flatMap((series) => series.points.map((point) => point.y));
129
+ const min = Math.min(...values);
130
+ const max = Math.max(...values);
131
+ const span = max - min || 1;
132
+ return (
133
+ <main className="wide">
134
+ <h1>{share.data.title}</h1>
135
+ <svg className="chart" viewBox="0 0 1000 420" role="img" aria-label={share.data.title}>
136
+ {share.data.series.map((series, seriesIndex) => {
137
+ const points = series.points.map((point, index) => {
138
+ const x = series.points.length <= 1 ? 500 : 20 + (index / (series.points.length - 1)) * 960;
139
+ const y = 400 - ((point.y - min) / span) * 380;
140
+ return `${x},${y}`;
141
+ }).join(" ");
142
+ return <polyline key={series.name} points={points} className={`series series-${seriesIndex % 6}`} />;
143
+ })}
144
+ </svg>
145
+ <ul className="legend">{share.data.series.map((series) => <li key={series.name}>{series.name}</li>)}</ul>
146
+ <SourceLink url={share.data.sourceUrl} />
147
+ {ownerActions}
148
+ </main>
149
+ );
150
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createShare, deleteShare, getShare, openLiveShareUrl, publicShareUrl, SHARE_API_ORIGIN } from "./api";
3
+ import { parseSharePayload } from "./payload";
4
+
5
+ const article = { kind: "article", data: { title: "AAPL", text: "Research", sourceUrl: "https://example.com/a" } } as const;
6
+ const portablePane = {
7
+ kind: "pane",
8
+ data: {
9
+ version: 2,
10
+ title: "AAPL Price",
11
+ layout: {
12
+ schemaVersion: 2,
13
+ sourceConfigVersion: 13,
14
+ layout: {
15
+ dockRoot: null,
16
+ instances: [{ instanceId: "p1", paneId: "graph-price", binding: { kind: "fixed", symbol: "AAPL" } }],
17
+ floating: [{ instanceId: "p1", x: 0, y: 0, width: 100, height: 30 }],
18
+ detached: [],
19
+ },
20
+ paneState: {},
21
+ },
22
+ },
23
+ } as const;
24
+ const shareId = "0123456789abcdef0123456789abcdef";
25
+
26
+ describe("share API client", () => {
27
+ test("validates strict payloads and http(s)-only source URLs", () => {
28
+ expect(parseSharePayload(article)).toEqual(article);
29
+ expect(parseSharePayload({ ...article, data: { ...article.data, sourceUrl: "javascript:alert(1)" } })).toBeNull();
30
+ expect(parseSharePayload({ kind: "table", data: { title: "x", columns: [], rows: [] } })).toBeNull();
31
+ expect(parseSharePayload({
32
+ kind: "pane",
33
+ data: { version: 1, templateId: "graph-price-pane", title: "AAPL Price", data: { symbol: "AAPL" } },
34
+ })?.kind).toBe("pane");
35
+ expect(parseSharePayload({
36
+ kind: "pane",
37
+ data: { version: 1, templateId: "../private", title: "Private", data: {} },
38
+ })).toBeNull();
39
+ expect(parseSharePayload(portablePane)).toEqual(portablePane);
40
+ expect(parseSharePayload({
41
+ ...portablePane,
42
+ data: {
43
+ ...portablePane.data,
44
+ layout: {
45
+ ...portablePane.data.layout,
46
+ layout: {
47
+ ...portablePane.data.layout.layout,
48
+ instances: [],
49
+ floating: [],
50
+ },
51
+ },
52
+ },
53
+ })).toBeNull();
54
+ });
55
+
56
+ test("creates and deletes through api.gloom.sh with credentials", async () => {
57
+ const calls: Array<[string, RequestInit | undefined]> = [];
58
+ const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => {
59
+ calls.push([String(url), init]);
60
+ return init?.method === "DELETE"
61
+ ? new Response(null, { status: 204 })
62
+ : Response.json({ id: shareId, expiresAt: "2026-09-20T00:00:00.000Z" });
63
+ }) as typeof fetch;
64
+ const created = await createShare(article, fetchImpl);
65
+ await deleteShare(created.id, fetchImpl);
66
+ expect(calls[0]).toEqual([`${SHARE_API_ORIGIN}/shares`, expect.objectContaining({ method: "POST", credentials: "include" })]);
67
+ expect(calls[1]).toEqual([`${SHARE_API_ORIGIN}/shares/${shareId}`, expect.objectContaining({ method: "DELETE", credentials: "include" })]);
68
+ });
69
+
70
+ test("explains when a Cloud account still needs email verification", async () => {
71
+ const fetchImpl = (async () => Response.json(
72
+ { error: "Email verification required" },
73
+ { status: 403 },
74
+ )) as typeof fetch;
75
+ await expect(createShare(article, fetchImpl)).rejects.toThrow(
76
+ "Verify your Gloom Cloud email to share.",
77
+ );
78
+ });
79
+
80
+ test("loads public shares with optional owner credentials and builds current-origin URLs", async () => {
81
+ let init: RequestInit | undefined;
82
+ const urls: string[] = [];
83
+ const fetchImpl = (async (url: string | URL | Request, options?: RequestInit) => {
84
+ urls.push(String(url));
85
+ init = options;
86
+ return Response.json({
87
+ ...article,
88
+ createdAt: "2026-08-21T00:00:00Z",
89
+ expiresAt: "2026-09-20T00:00:00Z",
90
+ ownedByViewer: true,
91
+ });
92
+ }) as typeof fetch;
93
+ const share = await getShare(shareId, fetchImpl);
94
+ await getShare(shareId, fetchImpl, { trackView: false });
95
+ expect(share?.kind).toBe("article");
96
+ expect(share?.ownedByViewer).toBe(true);
97
+ expect(init?.credentials).toBe("include");
98
+ expect(urls).toEqual([
99
+ `${SHARE_API_ORIGIN}/shares/${shareId}`,
100
+ `${SHARE_API_ORIGIN}/shares/${shareId}?purpose=open`,
101
+ ]);
102
+ expect(publicShareUrl(shareId)).toBe(`https://term.gloom.sh/s/${shareId}`);
103
+ expect(openLiveShareUrl(shareId)).toBe(`https://term.gloom.sh/api/shares/${shareId}/open`);
104
+ });
105
+ });