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,624 @@
1
+ import type { PaneRuntimeState } from "../core/state/app/types";
2
+ import {
3
+ CURRENT_CONFIG_VERSION,
4
+ removePaneInstances,
5
+ type DockLayoutNode,
6
+ type LayoutConfig,
7
+ type PaneBinding,
8
+ } from "../types/config";
9
+ import type {
10
+ PaneDef,
11
+ PaneSharePrivateFields,
12
+ } from "../types/plugin";
13
+
14
+ const MAX_LAYOUT_BYTES = 128 * 1024;
15
+ const MAX_INSTANCES = 40;
16
+ const MAX_DOCK_DEPTH = 20;
17
+ const MAX_ID_LENGTH = 160;
18
+ const MAX_PANE_ID_LENGTH = 120;
19
+ const MAX_SYMBOL_LENGTH = 64;
20
+ const MAX_NAME_LENGTH = 80;
21
+ const MAX_TITLE_LENGTH = 200;
22
+ const MAX_AUTHOR_LENGTH = 100;
23
+ const MAX_COORDINATE = 100_000;
24
+ const MAX_JSON_DEPTH = 12;
25
+ const MAX_JSON_KEYS = 512;
26
+ const MAX_JSON_ARRAY_ITEMS = 5_000;
27
+ const MAX_JSON_STRING_LENGTH = 16_384;
28
+ const WIRE_ID = /^p[1-9][0-9]*$/;
29
+ const MARKETPLACE_ID = /^[a-f0-9]{32}$/i;
30
+
31
+ const PRIVATE_FIELD_NAMES = new Set([
32
+ "account",
33
+ "accountid",
34
+ "accounts",
35
+ "accesstoken",
36
+ "accessurl",
37
+ "alerts",
38
+ "apikey",
39
+ "apisecret",
40
+ "attachments",
41
+ "authorization",
42
+ "bankroll",
43
+ "brokeraccountid",
44
+ "brokeraccounts",
45
+ "brokerid",
46
+ "brokerinstanceid",
47
+ "channelid",
48
+ "clientid",
49
+ "collectionid",
50
+ "collectionsorts",
51
+ "cookie",
52
+ "costbasis",
53
+ "credential",
54
+ "credentials",
55
+ "currentvalue",
56
+ "datadir",
57
+ "directory",
58
+ "draft",
59
+ "email",
60
+ "filepath",
61
+ "holdings",
62
+ "localpath",
63
+ "messages",
64
+ "oauth",
65
+ "password",
66
+ "portfolio",
67
+ "portfolioanalytics",
68
+ "portfolioid",
69
+ "portfolios",
70
+ "positions",
71
+ "privatekey",
72
+ "prompt",
73
+ "publicemail",
74
+ "queryid",
75
+ "refreshtoken",
76
+ "secret",
77
+ "session",
78
+ "sessiontoken",
79
+ "setuptoken",
80
+ "shares",
81
+ "targetmessageid",
82
+ "token",
83
+ "transcript",
84
+ "userid",
85
+ "visiblecollectionids",
86
+ "watchlist",
87
+ "watchlists",
88
+ ]);
89
+
90
+ export type LayoutMarketplaceSchemaVersion = 1 | 2;
91
+ type JsonPrimitive = string | number | boolean | null;
92
+ type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };
93
+
94
+ export interface LayoutMarketplaceAuthor {
95
+ username: string | null;
96
+ displayName: string;
97
+ }
98
+
99
+ /** Internal normalized form. V1 responses are upgraded with an empty paneState map. */
100
+ export interface LayoutMarketplacePayload {
101
+ schemaVersion: LayoutMarketplaceSchemaVersion;
102
+ sourceConfigVersion: number;
103
+ layout: LayoutConfig;
104
+ paneState: Record<string, PaneRuntimeState>;
105
+ }
106
+
107
+ export interface LayoutMarketplaceEntry extends LayoutMarketplacePayload {
108
+ id: string;
109
+ name: string;
110
+ author: LayoutMarketplaceAuthor;
111
+ publishedAt: string;
112
+ }
113
+
114
+ function record(value: unknown): value is Record<string, unknown> {
115
+ return value !== null && typeof value === "object" && !Array.isArray(value);
116
+ }
117
+
118
+ function exactKeys(value: Record<string, unknown>, required: string[], optional: string[] = []): boolean {
119
+ const allowed = new Set([...required, ...optional]);
120
+ return required.every((key) => Object.prototype.hasOwnProperty.call(value, key))
121
+ && Object.keys(value).every((key) => allowed.has(key));
122
+ }
123
+
124
+ function boundedString(value: unknown, max: number): value is string {
125
+ return typeof value === "string" && value.length > 0 && value.length <= max;
126
+ }
127
+
128
+ function boundedNumber(value: unknown, min: number, max: number): value is number {
129
+ return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
130
+ }
131
+
132
+ function encodedSize(value: unknown): number {
133
+ try {
134
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
135
+ } catch {
136
+ return Number.POSITIVE_INFINITY;
137
+ }
138
+ }
139
+
140
+ function normalizedKey(key: string): string {
141
+ return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
142
+ }
143
+
144
+ export function isPrivateMarketplaceField(key: string): boolean {
145
+ const normalized = normalizedKey(key);
146
+ return PRIVATE_FIELD_NAMES.has(normalized)
147
+ || normalized.endsWith("password")
148
+ || normalized.endsWith("secret")
149
+ || normalized.endsWith("token")
150
+ || normalized.endsWith("credential")
151
+ || normalized.endsWith("cookie");
152
+ }
153
+
154
+ function parseJson(value: unknown, depth = 0): JsonValue | undefined {
155
+ if (value === null || typeof value === "boolean") return value;
156
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
157
+ if (typeof value === "string") return value.length <= MAX_JSON_STRING_LENGTH ? value : undefined;
158
+ if (depth >= MAX_JSON_DEPTH) return undefined;
159
+ if (Array.isArray(value)) {
160
+ if (value.length > MAX_JSON_ARRAY_ITEMS) return undefined;
161
+ const parsed = value.map((entry) => parseJson(entry, depth + 1));
162
+ return parsed.some((entry) => entry === undefined) ? undefined : parsed as JsonValue[];
163
+ }
164
+ if (!record(value)) return undefined;
165
+ const entries = Object.entries(value);
166
+ if (entries.length > MAX_JSON_KEYS) return undefined;
167
+ const parsed: Record<string, JsonValue> = {};
168
+ for (const [key, entry] of entries) {
169
+ if (isPrivateMarketplaceField(key)) return undefined;
170
+ const next = parseJson(entry, depth + 1);
171
+ if (next === undefined) return undefined;
172
+ parsed[key] = next;
173
+ }
174
+ return parsed;
175
+ }
176
+
177
+ function sanitizeJson(value: unknown, depth = 0): JsonValue | undefined {
178
+ if (value === null || typeof value === "boolean") return value;
179
+ if (typeof value === "number") return Number.isFinite(value) ? value : undefined;
180
+ if (typeof value === "string") return value.length <= MAX_JSON_STRING_LENGTH ? value : undefined;
181
+ if (depth >= MAX_JSON_DEPTH) return undefined;
182
+ if (Array.isArray(value)) {
183
+ if (value.length > MAX_JSON_ARRAY_ITEMS) return undefined;
184
+ const sanitized = value.map((entry) => sanitizeJson(entry, depth + 1));
185
+ return sanitized.some((entry) => entry === undefined) ? undefined : sanitized as JsonValue[];
186
+ }
187
+ if (!record(value)) return undefined;
188
+ const entries = Object.entries(value);
189
+ if (entries.length > MAX_JSON_KEYS) return undefined;
190
+ const sanitized: Record<string, JsonValue> = {};
191
+ for (const [key, entry] of entries) {
192
+ if (isPrivateMarketplaceField(key)) continue;
193
+ const next = sanitizeJson(entry, depth + 1);
194
+ if (next !== undefined) sanitized[key] = next;
195
+ }
196
+ return sanitized;
197
+ }
198
+
199
+ function parseRecord(value: unknown): Record<string, JsonValue> | null {
200
+ if (!record(value)) return null;
201
+ const parsed = parseJson(value);
202
+ return parsed && !Array.isArray(parsed) && typeof parsed === "object"
203
+ ? parsed as Record<string, JsonValue>
204
+ : null;
205
+ }
206
+
207
+ function sanitizeRecord(
208
+ value: Record<string, unknown> | undefined,
209
+ privateFields: PaneSharePrivateFields | undefined,
210
+ ): Record<string, JsonValue> | undefined {
211
+ if (!value || privateFields === true) return undefined;
212
+ const excluded = new Set(privateFields ?? []);
213
+ const sanitized: Record<string, JsonValue> = {};
214
+ for (const [key, entry] of Object.entries(value)) {
215
+ if (excluded.has(key) || isPrivateMarketplaceField(key)) continue;
216
+ const next = sanitizeJson(entry);
217
+ if (next !== undefined) sanitized[key] = next;
218
+ }
219
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
220
+ }
221
+
222
+ function parseBinding(value: unknown): PaneBinding | null {
223
+ if (!record(value) || !boundedString(value.kind, 16)) return null;
224
+ if (value.kind === "none" && exactKeys(value, ["kind"])) return { kind: "none" };
225
+ if (value.kind === "fixed" && exactKeys(value, ["kind", "symbol"]) && boundedString(value.symbol, MAX_SYMBOL_LENGTH)) {
226
+ const symbol = value.symbol.trim().toUpperCase();
227
+ return symbol ? { kind: "fixed", symbol } : null;
228
+ }
229
+ if (
230
+ value.kind === "follow"
231
+ && exactKeys(value, ["kind", "sourceInstanceId"])
232
+ && boundedString(value.sourceInstanceId, MAX_ID_LENGTH)
233
+ ) {
234
+ return { kind: "follow", sourceInstanceId: value.sourceInstanceId };
235
+ }
236
+ return null;
237
+ }
238
+
239
+ function parseDockNode(
240
+ value: unknown,
241
+ schemaVersion: LayoutMarketplaceSchemaVersion,
242
+ depth = 0,
243
+ ): DockLayoutNode | null | undefined {
244
+ if (value === null) return null;
245
+ if (depth > MAX_DOCK_DEPTH || !record(value) || !boundedString(value.kind, 16)) return undefined;
246
+ if (
247
+ value.kind === "pane"
248
+ && exactKeys(value, ["kind", "instanceId"])
249
+ && boundedString(value.instanceId, MAX_ID_LENGTH)
250
+ && (schemaVersion === 1 || WIRE_ID.test(value.instanceId))
251
+ ) {
252
+ return { kind: "pane", instanceId: value.instanceId };
253
+ }
254
+ if (
255
+ value.kind === "split"
256
+ && exactKeys(value, ["kind", "axis", "ratio", "first", "second"])
257
+ && (value.axis === "horizontal" || value.axis === "vertical")
258
+ && boundedNumber(value.ratio, 0, 1)
259
+ ) {
260
+ const first = parseDockNode(value.first, schemaVersion, depth + 1);
261
+ const second = parseDockNode(value.second, schemaVersion, depth + 1);
262
+ if (!first || !second) return undefined;
263
+ return {
264
+ kind: "split",
265
+ axis: value.axis,
266
+ ratio: value.ratio,
267
+ first,
268
+ second,
269
+ };
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ function collectDockedIds(node: DockLayoutNode | null, ids: string[]): boolean {
275
+ if (!node) return true;
276
+ if (node.kind === "pane") {
277
+ if (ids.includes(node.instanceId)) return false;
278
+ ids.push(node.instanceId);
279
+ return true;
280
+ }
281
+ return collectDockedIds(node.first, ids) && collectDockedIds(node.second, ids);
282
+ }
283
+
284
+ function hasFollowCycle(instances: LayoutConfig["instances"]): boolean {
285
+ const follows = new Map(instances.flatMap((instance) => (
286
+ instance.binding?.kind === "follow"
287
+ ? [[instance.instanceId, instance.binding.sourceInstanceId] as const]
288
+ : []
289
+ )));
290
+ for (const start of follows.keys()) {
291
+ const visited = new Set<string>();
292
+ let current: string | undefined = start;
293
+ while (current && follows.has(current)) {
294
+ if (visited.has(current)) return true;
295
+ visited.add(current);
296
+ current = follows.get(current);
297
+ }
298
+ }
299
+ return false;
300
+ }
301
+
302
+ function parseLayout(value: unknown, schemaVersion: LayoutMarketplaceSchemaVersion): LayoutConfig | null {
303
+ if (!record(value) || !exactKeys(value, ["dockRoot", "instances", "floating", "detached"])) return null;
304
+ if (!Array.isArray(value.instances) || value.instances.length === 0 || value.instances.length > MAX_INSTANCES) return null;
305
+ if (!Array.isArray(value.floating) || !Array.isArray(value.detached)) return null;
306
+
307
+ const dockRoot = parseDockNode(value.dockRoot, schemaVersion);
308
+ if (dockRoot === undefined) return null;
309
+
310
+ const instanceIds = new Set<string>();
311
+ const instances: LayoutConfig["instances"] = [];
312
+ for (const raw of value.instances) {
313
+ const optional = schemaVersion === 1
314
+ ? ["binding"]
315
+ : ["title", "binding", "params", "settings"];
316
+ if (
317
+ !record(raw)
318
+ || !exactKeys(raw, ["instanceId", "paneId"], optional)
319
+ || !boundedString(raw.instanceId, MAX_ID_LENGTH)
320
+ || (schemaVersion === 2 && !WIRE_ID.test(raw.instanceId))
321
+ || !boundedString(raw.paneId, MAX_PANE_ID_LENGTH)
322
+ || instanceIds.has(raw.instanceId)
323
+ ) return null;
324
+ const binding = raw.binding === undefined ? undefined : parseBinding(raw.binding);
325
+ if (raw.binding !== undefined && !binding) return null;
326
+ const title = raw.title === undefined ? undefined : raw.title;
327
+ if (title !== undefined && (!boundedString(title, MAX_TITLE_LENGTH) || title !== title.trim())) return null;
328
+ const params = raw.params === undefined ? undefined : parseRecord(raw.params);
329
+ if (raw.params !== undefined && (!params || Object.values(params).some((entry) => typeof entry !== "string"))) return null;
330
+ const settings = raw.settings === undefined ? undefined : parseRecord(raw.settings);
331
+ if (raw.settings !== undefined && !settings) return null;
332
+ instanceIds.add(raw.instanceId);
333
+ instances.push({
334
+ instanceId: raw.instanceId,
335
+ paneId: raw.paneId,
336
+ ...(typeof title === "string" ? { title } : {}),
337
+ ...(binding ? { binding } : {}),
338
+ ...(params ? { params: params as Record<string, string> } : {}),
339
+ ...(settings ? { settings } : {}),
340
+ });
341
+ }
342
+
343
+ const parsePlacement = (
344
+ raw: unknown,
345
+ detached: boolean,
346
+ ): LayoutConfig["floating"][number] | LayoutConfig["detached"][number] | null => {
347
+ if (!record(raw)) return null;
348
+ const optional = detached ? [] : ["zIndex"];
349
+ if (!exactKeys(raw, ["instanceId", "x", "y", "width", "height"], optional)) return null;
350
+ if (
351
+ !boundedString(raw.instanceId, MAX_ID_LENGTH)
352
+ || (schemaVersion === 2 && !WIRE_ID.test(raw.instanceId))
353
+ || !boundedNumber(raw.x, 0, MAX_COORDINATE)
354
+ || !boundedNumber(raw.y, 0, MAX_COORDINATE)
355
+ || !boundedNumber(raw.width, 1, MAX_COORDINATE)
356
+ || !boundedNumber(raw.height, 1, MAX_COORDINATE)
357
+ ) return null;
358
+ if (!detached && raw.zIndex !== undefined && !boundedNumber(raw.zIndex, -MAX_COORDINATE, MAX_COORDINATE)) return null;
359
+ return {
360
+ instanceId: raw.instanceId,
361
+ x: Math.round(raw.x),
362
+ y: Math.round(raw.y),
363
+ width: Math.round(raw.width),
364
+ height: Math.round(raw.height),
365
+ ...(!detached && typeof raw.zIndex === "number" ? { zIndex: Math.round(raw.zIndex) } : {}),
366
+ };
367
+ };
368
+
369
+ const floating: LayoutConfig["floating"] = [];
370
+ for (const raw of value.floating) {
371
+ const placement = parsePlacement(raw, false);
372
+ if (!placement) return null;
373
+ floating.push(placement);
374
+ }
375
+ const detached: LayoutConfig["detached"] = [];
376
+ for (const raw of value.detached) {
377
+ const placement = parsePlacement(raw, true);
378
+ if (!placement) return null;
379
+ detached.push(placement);
380
+ }
381
+
382
+ const dockedIds: string[] = [];
383
+ if (!collectDockedIds(dockRoot, dockedIds)) return null;
384
+ const placedIds = [...dockedIds, ...floating.map((entry) => entry.instanceId), ...detached.map((entry) => entry.instanceId)];
385
+ if (new Set(placedIds).size !== placedIds.length) return null;
386
+ if (placedIds.some((id) => !instanceIds.has(id)) || placedIds.length !== instanceIds.size) return null;
387
+ for (const instance of instances) {
388
+ if (instance.binding?.kind === "follow" && !instanceIds.has(instance.binding.sourceInstanceId)) return null;
389
+ }
390
+ if (hasFollowCycle(instances)) return null;
391
+
392
+ return { dockRoot, instances, floating, detached };
393
+ }
394
+
395
+ function parsePaneState(
396
+ value: unknown,
397
+ instanceIds: Set<string>,
398
+ ): Record<string, PaneRuntimeState> | null {
399
+ if (!record(value) || Object.keys(value).some((id) => !instanceIds.has(id))) return null;
400
+ const parsed: Record<string, PaneRuntimeState> = {};
401
+ for (const [id, state] of Object.entries(value)) {
402
+ const next = parseRecord(state);
403
+ if (!next) return null;
404
+ parsed[id] = next as PaneRuntimeState;
405
+ }
406
+ return parsed;
407
+ }
408
+
409
+ export function parseMarketplaceLayoutPayload(value: unknown): LayoutMarketplacePayload | null {
410
+ if (!record(value) || (value.schemaVersion !== 1 && value.schemaVersion !== 2)) return null;
411
+ const schemaVersion = value.schemaVersion;
412
+ const required = schemaVersion === 2
413
+ ? ["schemaVersion", "sourceConfigVersion", "layout", "paneState"]
414
+ : ["schemaVersion", "sourceConfigVersion", "layout"];
415
+ if (
416
+ !exactKeys(value, required)
417
+ || !Number.isSafeInteger(value.sourceConfigVersion)
418
+ || (value.sourceConfigVersion as number) < 1
419
+ || encodedSize(value) > MAX_LAYOUT_BYTES
420
+ ) return null;
421
+ const layout = parseLayout(value.layout, schemaVersion);
422
+ if (!layout) return null;
423
+ const paneState = schemaVersion === 2
424
+ ? parsePaneState(value.paneState, new Set(layout.instances.map((instance) => instance.instanceId)))
425
+ : {};
426
+ return paneState
427
+ ? {
428
+ schemaVersion,
429
+ sourceConfigVersion: value.sourceConfigVersion as number,
430
+ layout,
431
+ paneState,
432
+ }
433
+ : null;
434
+ }
435
+
436
+ function mapDockNode(node: DockLayoutNode | null, ids: ReadonlyMap<string, string>): DockLayoutNode | null {
437
+ if (!node) return null;
438
+ if (node.kind === "pane") return { kind: "pane", instanceId: ids.get(node.instanceId) ?? node.instanceId };
439
+ return {
440
+ ...node,
441
+ first: mapDockNode(node.first, ids)!,
442
+ second: mapDockNode(node.second, ids)!,
443
+ };
444
+ }
445
+
446
+ function mapBinding(binding: PaneBinding | undefined, ids: ReadonlyMap<string, string>): PaneBinding | undefined {
447
+ return binding?.kind === "follow"
448
+ ? { kind: "follow", sourceInstanceId: ids.get(binding.sourceInstanceId) ?? binding.sourceInstanceId }
449
+ : binding ? structuredClone(binding) : undefined;
450
+ }
451
+
452
+ function publishableLayout(
453
+ publicLayout: LayoutConfig,
454
+ paneState: Record<string, PaneRuntimeState>,
455
+ panes: ReadonlyMap<string, PaneDef>,
456
+ ): LayoutMarketplacePayload {
457
+ const ids = new Map(publicLayout.instances.map((instance, index) => [instance.instanceId, `p${index + 1}`]));
458
+ const projectedState: Record<string, PaneRuntimeState> = {};
459
+ const instances = publicLayout.instances.map((instance) => {
460
+ const instanceId = ids.get(instance.instanceId)!;
461
+ const def = panes.get(instance.paneId);
462
+ const privacy = def?.portableShare?.private;
463
+ const params = def ? sanitizeRecord(instance.params, privacy?.params) : undefined;
464
+ const settings = def ? sanitizeRecord(instance.settings, privacy?.settings) : undefined;
465
+ const state = def ? sanitizeRecord(paneState[instance.instanceId], privacy?.state) : undefined;
466
+ if (state) projectedState[instanceId] = state as PaneRuntimeState;
467
+ const title = def && !privacy?.title ? instance.title?.trim() : undefined;
468
+ return {
469
+ instanceId,
470
+ paneId: instance.paneId,
471
+ ...(title ? { title } : {}),
472
+ ...(instance.binding ? { binding: mapBinding(instance.binding, ids) } : {}),
473
+ ...(params ? { params: params as Record<string, string> } : {}),
474
+ ...(settings ? { settings } : {}),
475
+ };
476
+ });
477
+ const projected: LayoutConfig = {
478
+ dockRoot: mapDockNode(publicLayout.dockRoot, ids),
479
+ instances,
480
+ floating: publicLayout.floating.map(({ instanceId, x, y, width, height, zIndex }) => ({
481
+ instanceId: ids.get(instanceId) ?? instanceId,
482
+ x,
483
+ y,
484
+ width,
485
+ height,
486
+ ...(zIndex === undefined ? {} : { zIndex }),
487
+ })),
488
+ detached: publicLayout.detached.map(({ instanceId, x, y, width, height }) => ({
489
+ instanceId: ids.get(instanceId) ?? instanceId,
490
+ x,
491
+ y,
492
+ width,
493
+ height,
494
+ })),
495
+ };
496
+ const parsed = parseMarketplaceLayoutPayload({
497
+ schemaVersion: 2,
498
+ sourceConfigVersion: CURRENT_CONFIG_VERSION,
499
+ layout: projected,
500
+ paneState: projectedState,
501
+ });
502
+ if (!parsed) throw new Error("This layout cannot be published safely.");
503
+ return parsed;
504
+ }
505
+
506
+ export function publishableMarketplaceLayout(
507
+ layout: LayoutConfig,
508
+ paneState: Record<string, PaneRuntimeState>,
509
+ panes: ReadonlyMap<string, PaneDef>,
510
+ ): LayoutMarketplacePayload {
511
+ return publishableLayout(removePaneInstances(
512
+ layout,
513
+ layout.instances
514
+ .filter((instance) => instance.paneId === "layout-marketplace")
515
+ .map((instance) => instance.instanceId),
516
+ ), paneState, panes);
517
+ }
518
+
519
+ export function publishableMarketplacePane(
520
+ pane: LayoutConfig["instances"][number],
521
+ paneState: PaneRuntimeState,
522
+ panes: ReadonlyMap<string, PaneDef>,
523
+ resolvedTicker?: string | null,
524
+ ): LayoutMarketplacePayload {
525
+ const def = panes.get(pane.paneId);
526
+ if (!def) throw new Error("This pane is unavailable.");
527
+ const binding = pane.binding?.kind === "follow"
528
+ ? resolvedTicker?.trim()
529
+ ? { kind: "fixed" as const, symbol: resolvedTicker.trim() }
530
+ : { kind: "none" as const }
531
+ : pane.binding;
532
+ const size = def.defaultFloatingSize ?? { width: 80, height: 24 };
533
+ return publishableLayout({
534
+ dockRoot: null,
535
+ instances: [{ ...pane, ...(binding ? { binding } : {}) }],
536
+ floating: [{
537
+ instanceId: pane.instanceId,
538
+ x: 0,
539
+ y: 0,
540
+ width: size.width,
541
+ height: size.height,
542
+ }],
543
+ detached: [],
544
+ }, { [pane.instanceId]: paneState }, panes);
545
+ }
546
+
547
+ export function materializeMarketplaceLayout(
548
+ payload: Pick<LayoutMarketplacePayload, "layout" | "paneState">,
549
+ createId: (paneId: string, index: number) => string = (paneId) => (
550
+ `${paneId.slice(0, 80)}:shared-${crypto.randomUUID().replaceAll("-", "").slice(0, 20)}`
551
+ ),
552
+ ): { layout: LayoutConfig; paneState: Record<string, PaneRuntimeState> } {
553
+ const ids = new Map(payload.layout.instances.map((instance, index) => [
554
+ instance.instanceId,
555
+ createId(instance.paneId, index),
556
+ ]));
557
+ if (new Set(ids.values()).size !== ids.size) throw new Error("Could not create unique pane ids for this layout.");
558
+ const layout: LayoutConfig = {
559
+ dockRoot: mapDockNode(payload.layout.dockRoot, ids),
560
+ instances: payload.layout.instances.map((instance) => ({
561
+ ...structuredClone(instance),
562
+ instanceId: ids.get(instance.instanceId)!,
563
+ ...(instance.binding ? { binding: mapBinding(instance.binding, ids) } : {}),
564
+ })),
565
+ floating: payload.layout.floating.map((entry) => ({
566
+ ...entry,
567
+ instanceId: ids.get(entry.instanceId) ?? entry.instanceId,
568
+ })),
569
+ detached: payload.layout.detached.map((entry) => ({
570
+ ...entry,
571
+ instanceId: ids.get(entry.instanceId) ?? entry.instanceId,
572
+ })),
573
+ };
574
+ const paneState = Object.fromEntries(Object.entries(payload.paneState).flatMap(([id, state]) => {
575
+ const materializedId = ids.get(id);
576
+ return materializedId ? [[materializedId, structuredClone(state)]] : [];
577
+ }));
578
+ return { layout, paneState };
579
+ }
580
+
581
+ export function parseMarketplaceLayoutEntry(value: unknown): LayoutMarketplaceEntry | null {
582
+ if (!record(value) || (value.schemaVersion !== 1 && value.schemaVersion !== 2)) return null;
583
+ const required = ["id", "name", "schemaVersion", "sourceConfigVersion", "layout", "author", "publishedAt"];
584
+ const optional = value.schemaVersion === 2 ? ["paneState"] : [];
585
+ if (
586
+ !exactKeys(value, required, optional)
587
+ || typeof value.id !== "string"
588
+ || !MARKETPLACE_ID.test(value.id)
589
+ || !boundedString(value.name, MAX_NAME_LENGTH)
590
+ || !record(value.author)
591
+ || !exactKeys(value.author, ["username", "displayName"])
592
+ || !(value.author.username === null || boundedString(value.author.username, MAX_AUTHOR_LENGTH))
593
+ || !boundedString(value.author.displayName, MAX_AUTHOR_LENGTH)
594
+ || !boundedString(value.publishedAt, 64)
595
+ || Number.isNaN(Date.parse(value.publishedAt))
596
+ ) return null;
597
+ const payload = parseMarketplaceLayoutPayload({
598
+ schemaVersion: value.schemaVersion,
599
+ sourceConfigVersion: value.sourceConfigVersion,
600
+ layout: value.layout,
601
+ ...(value.schemaVersion === 2 ? { paneState: value.paneState } : {}),
602
+ });
603
+ if (!payload) return null;
604
+ return {
605
+ id: value.id,
606
+ name: value.name,
607
+ ...payload,
608
+ author: {
609
+ username: value.author.username as string | null,
610
+ displayName: value.author.displayName,
611
+ },
612
+ publishedAt: value.publishedAt,
613
+ };
614
+ }
615
+
616
+ export function parseMarketplaceLayoutList(value: unknown): LayoutMarketplaceEntry[] | null {
617
+ if (!record(value) || !exactKeys(value, ["items"]) || !Array.isArray(value.items) || value.items.length > 50) return null;
618
+ const items = value.items.map(parseMarketplaceLayoutEntry);
619
+ return items.every((item): item is LayoutMarketplaceEntry => item !== null) ? items : null;
620
+ }
621
+
622
+ export function isMarketplaceLayoutId(value: string): boolean {
623
+ return MARKETPLACE_ID.test(value);
624
+ }
@@ -0,0 +1,63 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { apiClient } from "../api-client";
3
+ import type {
4
+ LayoutMarketplaceEntry,
5
+ LayoutMarketplacePayload,
6
+ } from "./payload";
7
+
8
+ export type LayoutMarketplaceState =
9
+ | { status: "signed-out"; items: [] }
10
+ | { status: "idle"; items: LayoutMarketplaceEntry[] }
11
+ | { status: "loading"; items: LayoutMarketplaceEntry[] }
12
+ | { status: "ready"; items: LayoutMarketplaceEntry[] }
13
+ | { status: "error"; items: LayoutMarketplaceEntry[]; error: string };
14
+
15
+ export interface LayoutMarketplaceRuntime {
16
+ state: LayoutMarketplaceState;
17
+ refresh: () => void;
18
+ publish: (name: string, payload: LayoutMarketplacePayload) => Promise<LayoutMarketplaceEntry>;
19
+ }
20
+
21
+ export function useLayoutMarketplace(active: boolean, signedIn: boolean): LayoutMarketplaceRuntime {
22
+ const [state, setState] = useState<LayoutMarketplaceState>(() => (
23
+ signedIn ? { status: "idle", items: [] } : { status: "signed-out", items: [] }
24
+ ));
25
+ const [revision, setRevision] = useState(0);
26
+
27
+ useEffect(() => {
28
+ if (!signedIn) {
29
+ setState({ status: "signed-out", items: [] });
30
+ return;
31
+ }
32
+ if (!active) {
33
+ setState((current) => current.status === "signed-out" ? { status: "idle", items: [] } : current);
34
+ return;
35
+ }
36
+
37
+ const controller = new AbortController();
38
+ setState((current) => ({ status: "loading", items: current.items }));
39
+ void apiClient.listMarketplaceLayouts({ signal: controller.signal })
40
+ .then((items) => setState({ status: "ready", items }))
41
+ .catch((error) => {
42
+ if (controller.signal.aborted) return;
43
+ setState((current) => ({
44
+ status: "error",
45
+ items: current.items,
46
+ error: error instanceof Error ? error.message : "Could not load layouts.",
47
+ }));
48
+ });
49
+ return () => controller.abort();
50
+ }, [active, revision, signedIn]);
51
+
52
+ const refresh = useCallback(() => setRevision((current) => current + 1), []);
53
+ const publish = useCallback(async (name: string, payload: LayoutMarketplacePayload) => {
54
+ const item = await apiClient.publishMarketplaceLayout(name, payload);
55
+ setState((current) => ({
56
+ status: "ready",
57
+ items: [item, ...current.items.filter((candidate) => candidate.id !== item.id)].slice(0, 50),
58
+ }));
59
+ return item;
60
+ }, []);
61
+
62
+ return { state, refresh, publish };
63
+ }
@@ -810,8 +810,8 @@ describe("MarketDataCoordinator", () => {
810
810
  },
811
811
  );
812
812
 
813
- expect(coordinator.getQuoteEntry(instrument).data).toBeNull();
814
- expect(coordinator.getQuoteEntry(instrument).lastGoodData?.price).toBe(598);
813
+ expect(coordinator.getQuoteEntry(instrument).data?.price).toBe(598);
814
+ expect(coordinator.getQuoteEntry(instrument).data?.providerId).toBe("yahoo");
815
815
  expect(coordinator.getTickerFinancialsSync(instrument)?.quote?.price).toBe(598);
816
816
  expect(coordinator.getTickerFinancialsSync(instrument)?.quote?.providerId).toBe("yahoo");
817
817
  } finally {