gloomberb 0.15.7 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (524) hide show
  1. package/README.md +11 -2
  2. package/package.json +1 -1
  3. package/src/api-client/app-attention.ts +147 -0
  4. package/src/api-client/attention.ts +26 -0
  5. package/src/api-client/awards.ts +215 -0
  6. package/src/api-client/catalysts.ts +160 -0
  7. package/src/api-client/chat.ts +137 -14
  8. package/src/api-client/company-kpis.ts +153 -0
  9. package/src/api-client/cpi.ts +52 -0
  10. package/src/api-client/credit-documents.ts +107 -0
  11. package/src/api-client/data.ts +192 -3
  12. package/src/api-client/distress.ts +164 -0
  13. package/src/api-client/doe.ts +75 -0
  14. package/src/api-client/equity-screener.ts +11 -0
  15. package/src/api-client/exposure.ts +202 -0
  16. package/src/api-client/gpu.ts +93 -0
  17. package/src/api-client/hiring.ts +110 -0
  18. package/src/api-client/index.ts +78 -3
  19. package/src/api-client/normalizers.ts +31 -1
  20. package/src/api-client/paths.ts +3 -0
  21. package/src/api-client/perps.ts +127 -0
  22. package/src/api-client/power.ts +148 -0
  23. package/src/api-client/request.ts +58 -1
  24. package/src/api-client/research-activity.ts +172 -13
  25. package/src/api-client/socket.ts +75 -27
  26. package/src/api-client/supply-chain-graph.ts +40 -0
  27. package/src/api-client/supply-chain.ts +98 -0
  28. package/src/api-client/telemetry.ts +29 -0
  29. package/src/api-client/themes.ts +28 -0
  30. package/src/api-client/types.ts +33 -1
  31. package/src/api-client/wall-experiments.ts +100 -0
  32. package/src/api-client/wall-summary.ts +49 -0
  33. package/src/api-client/web-experiments.ts +136 -0
  34. package/src/app/pane-runtime/plugin-bindings.ts +5 -0
  35. package/src/app/pane-runtime/ticker-open-runtime.ts +8 -2
  36. package/src/app/runtime/update.ts +13 -1
  37. package/src/cli/commands/system.ts +2 -0
  38. package/src/cli/desktop-pane-shot.ts +3 -4
  39. package/src/cli/pane-functions/data.ts +78 -0
  40. package/src/cli/pane-functions/financial-ratio-shot.ts +160 -0
  41. package/src/cli/pane-functions/function-help.ts +220 -8
  42. package/src/cli/pane-functions/headless.ts +4 -0
  43. package/src/cli/pane-functions/screenshot-evidence.ts +21 -0
  44. package/src/cli/pane-functions/screenshot.ts +49 -5
  45. package/src/components/chart/composite/composite-chart.tsx +15 -2
  46. package/src/components/chart/composite/format.ts +24 -11
  47. package/src/components/chart/composite/rasterizer.ts +67 -16
  48. package/src/components/chart/composite/scene.ts +53 -2
  49. package/src/components/chart/composite/session-shading.ts +3 -2
  50. package/src/components/chart/composite/text-renderer.ts +52 -1
  51. package/src/components/chart/composite/time-axis.ts +100 -30
  52. package/src/components/chart/composite/tools.ts +2 -1
  53. package/src/components/chart/composite/types.ts +4 -0
  54. package/src/components/chart-table/index.ts +1 -0
  55. package/src/components/chart-table/layout.ts +15 -0
  56. package/src/components/command-bar/action-runtime.ts +1 -0
  57. package/src/components/command-bar/commands/direct/index.ts +32 -0
  58. package/src/components/command-bar/commands/registry.ts +7 -0
  59. package/src/components/command-bar/pane-templates/items.ts +11 -0
  60. package/src/components/command-bar/pane-templates/workflow.ts +19 -4
  61. package/src/components/command-bar/routes/root/command-items.ts +6 -0
  62. package/src/components/command-bar/routes/root/provider-search.ts +2 -0
  63. package/src/components/command-bar/routes/root/results.ts +64 -0
  64. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  65. package/src/components/command-bar/routes/ticker-search/results.ts +89 -24
  66. package/src/components/command-bar/routes/ticker-search/route.ts +2 -0
  67. package/src/components/command-bar/surface/index.tsx +15 -0
  68. package/src/components/command-bar/view-model.ts +3 -0
  69. package/src/components/command-bar/workflow/collection-actions.ts +9 -7
  70. package/src/components/command-bar/workflow/ops.ts +19 -0
  71. package/src/components/command-bar/workflow/tickers.ts +5 -0
  72. package/src/components/company-logo.tsx +15 -2
  73. package/src/components/data-table/table-pane.ts +2 -1
  74. package/src/components/index.ts +1 -0
  75. package/src/components/layout/detached-pane-shell.tsx +12 -1
  76. package/src/components/layout/missing-pane.tsx +87 -23
  77. package/src/components/layout/pane/footer/index.tsx +64 -18
  78. package/src/components/layout/pane/footer/model.ts +85 -0
  79. package/src/components/layout/pane/sidebar.tsx +19 -1
  80. package/src/components/layout/shell/index.tsx +38 -28
  81. package/src/components/layout/shell/pane/management-shortcuts.ts +13 -1
  82. package/src/components/layout/status-bar.tsx +93 -28
  83. package/src/components/layout/status-widget-space.ts +72 -0
  84. package/src/components/onboarding/first-run-workspace.ts +8 -2
  85. package/src/components/onboarding/onboarding-wizard.tsx +35 -16
  86. package/src/components/onboarding/portfolio-step/positions-panel.tsx +14 -5
  87. package/src/components/onboarding/wizard-model.ts +13 -5
  88. package/src/components/onboarding/wizard-positions.ts +56 -7
  89. package/src/components/price-performance.tsx +31 -34
  90. package/src/components/ui/data-table/index.tsx +11 -8
  91. package/src/components/ui/display.tsx +15 -6
  92. package/src/components/ui/fields.tsx +5 -0
  93. package/src/components/ui/figure.tsx +94 -0
  94. package/src/components/ui/header-case.ts +25 -3
  95. package/src/components/ui/icon.tsx +3 -1
  96. package/src/components/ui/message-composer.tsx +8 -1
  97. package/src/components/ui/query-bar.tsx +8 -1
  98. package/src/components/ui/ratio-bar.tsx +84 -0
  99. package/src/components/ui/toggle.tsx +3 -1
  100. package/src/components/use-scrollbox-layout.ts +17 -0
  101. package/src/core/state/app/layout.ts +35 -1
  102. package/src/core/state/app/state.ts +15 -0
  103. package/src/core/state/app/types.ts +1 -0
  104. package/src/data/config/store/normalize.ts +24 -0
  105. package/src/i18n/es.ts +2 -0
  106. package/src/i18n/ja.ts +2 -0
  107. package/src/i18n/ko.ts +2 -0
  108. package/src/i18n/zh-cn.ts +2 -0
  109. package/src/i18n/zh-tw.ts +2 -0
  110. package/src/layout-marketplace/pane-imagery.ts +1 -0
  111. package/src/market-data/coordinator/entries.ts +13 -0
  112. package/src/market-data/coordinator/financials.ts +33 -8
  113. package/src/market-data/market/freshness.ts +16 -5
  114. package/src/market-data/market/trading-sessions.ts +65 -1
  115. package/src/market-data/published-cn-sessions.ts +25 -0
  116. package/src/market-data/published-nse-sessions.ts +20 -0
  117. package/src/news/aggregator.ts +4 -0
  118. package/src/plugins/builtin/account-management/model.ts +6 -1
  119. package/src/plugins/builtin/account-management/pane.tsx +36 -25
  120. package/src/plugins/builtin/alerts/catalysts.ts +77 -0
  121. package/src/plugins/builtin/alerts/events-pane.tsx +3 -0
  122. package/src/plugins/builtin/alerts/events.ts +10 -4
  123. package/src/plugins/builtin/alerts/history-pane.tsx +1 -1
  124. package/src/plugins/builtin/alerts/history.ts +1 -0
  125. package/src/plugins/builtin/alerts/index.tsx +6 -0
  126. package/src/plugins/builtin/alerts/research-builder.ts +10 -3
  127. package/src/plugins/builtin/alerts/research-rules.ts +15 -1
  128. package/src/plugins/builtin/analytics/risk-client.ts +15 -3
  129. package/src/plugins/builtin/analytics/risk-headless.ts +123 -18
  130. package/src/plugins/builtin/attention/client.ts +80 -0
  131. package/src/plugins/builtin/attention/evidence.ts +30 -0
  132. package/src/plugins/builtin/attention/headless.ts +33 -0
  133. package/src/plugins/builtin/attention/index.tsx +18 -0
  134. package/src/plugins/builtin/attention/model.ts +59 -0
  135. package/src/plugins/builtin/attention/pane.tsx +211 -0
  136. package/src/plugins/builtin/attention/trending.tsx +59 -0
  137. package/src/plugins/builtin/awards/alerts.ts +32 -0
  138. package/src/plugins/builtin/awards/client.ts +82 -0
  139. package/src/plugins/builtin/awards/detail.tsx +107 -0
  140. package/src/plugins/builtin/awards/evidence.ts +36 -0
  141. package/src/plugins/builtin/awards/headless.ts +55 -0
  142. package/src/plugins/builtin/awards/index.tsx +20 -0
  143. package/src/plugins/builtin/awards/model.ts +42 -0
  144. package/src/plugins/builtin/awards/pane.tsx +263 -0
  145. package/src/plugins/builtin/catalysts/client.ts +28 -0
  146. package/src/plugins/builtin/catalysts/detail.tsx +88 -0
  147. package/src/plugins/builtin/catalysts/evidence.ts +47 -0
  148. package/src/plugins/builtin/catalysts/headless.ts +42 -0
  149. package/src/plugins/builtin/catalysts/history.ts +26 -0
  150. package/src/plugins/builtin/catalysts/index.tsx +29 -0
  151. package/src/plugins/builtin/catalysts/model.ts +135 -0
  152. package/src/plugins/builtin/catalysts/pane.tsx +159 -0
  153. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -0
  154. package/src/plugins/builtin/chart-composer/headless-schema.ts +6 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +8 -1
  156. package/src/plugins/builtin/chart-composer/pane.tsx +28 -1
  157. package/src/plugins/builtin/chat/attachments/desktop.tsx +610 -0
  158. package/src/plugins/builtin/chat/attachments/files.ts +45 -0
  159. package/src/plugins/builtin/chat/attachments/model.ts +164 -0
  160. package/src/plugins/builtin/chat/attachments/terminal.tsx +72 -0
  161. package/src/plugins/builtin/chat/attachments/transfer.ts +165 -0
  162. package/src/plugins/builtin/chat/content/channel-navigation.ts +13 -12
  163. package/src/plugins/builtin/chat/content/composer-runtime.ts +25 -4
  164. package/src/plugins/builtin/chat/content/composer.tsx +46 -4
  165. package/src/plugins/builtin/chat/content/footer.tsx +46 -8
  166. package/src/plugins/builtin/chat/content/index.tsx +263 -62
  167. package/src/plugins/builtin/chat/content/layout-metrics.ts +10 -2
  168. package/src/plugins/builtin/chat/content/open-targets.ts +17 -3
  169. package/src/plugins/builtin/chat/content/scroll.ts +7 -1
  170. package/src/plugins/builtin/chat/content/shortcuts.ts +36 -11
  171. package/src/plugins/builtin/chat/content/snapshot.ts +3 -0
  172. package/src/plugins/builtin/chat/content/transcript.tsx +17 -1
  173. package/src/plugins/builtin/chat/content/types.ts +7 -0
  174. package/src/plugins/builtin/chat/controller/attachments.ts +213 -0
  175. package/src/plugins/builtin/chat/controller/connections.ts +3 -1
  176. package/src/plugins/builtin/chat/controller/index.ts +114 -2
  177. package/src/plugins/builtin/chat/controller/lifecycle.ts +9 -0
  178. package/src/plugins/builtin/chat/controller/messages.ts +35 -2
  179. package/src/plugins/builtin/chat/controller/send.ts +128 -25
  180. package/src/plugins/builtin/chat/controller/state.ts +12 -1
  181. package/src/plugins/builtin/chat/controller/utils.ts +7 -5
  182. package/src/plugins/builtin/chat/controller/view.ts +2 -0
  183. package/src/plugins/builtin/chat/index.tsx +21 -0
  184. package/src/plugins/builtin/chat/layout.ts +9 -4
  185. package/src/plugins/builtin/chat/message/desktop.tsx +55 -20
  186. package/src/plugins/builtin/chat/message/header.tsx +30 -5
  187. package/src/plugins/builtin/chat/message/inline-tokens.tsx +9 -0
  188. package/src/plugins/builtin/chat/message/pending-send.ts +35 -0
  189. package/src/plugins/builtin/chat/message/profile-popover.tsx +5 -1
  190. package/src/plugins/builtin/chat/message/render-state.ts +12 -2
  191. package/src/plugins/builtin/chat/message/terminal.tsx +51 -4
  192. package/src/plugins/builtin/chat/message/types.ts +6 -1
  193. package/src/plugins/builtin/chat/pane-state.ts +59 -1
  194. package/src/plugins/builtin/chat/profile-popover.ts +50 -1
  195. package/src/plugins/builtin/chat/sidebar.tsx +8 -3
  196. package/src/plugins/builtin/chat/status-widget.tsx +29 -12
  197. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +143 -0
  198. package/src/plugins/builtin/chat/unread-inbox.ts +91 -0
  199. package/src/plugins/builtin/clinical-trials/client.ts +237 -0
  200. package/src/plugins/builtin/clinical-trials/index.tsx +74 -0
  201. package/src/plugins/builtin/clinical-trials/pane.tsx +225 -0
  202. package/src/plugins/builtin/clinical-trials/types.ts +2 -0
  203. package/src/plugins/builtin/cloud/askg/controller.ts +37 -6
  204. package/src/plugins/builtin/cloud/askg/executor.ts +59 -44
  205. package/src/plugins/builtin/cloud/askg/host.ts +8 -1
  206. package/src/plugins/builtin/cloud/askg/manifest.ts +29 -2
  207. package/src/plugins/builtin/cloud/askg/model.ts +47 -13
  208. package/src/plugins/builtin/cloud/askg/notes.ts +100 -0
  209. package/src/plugins/builtin/cloud/askg/pane.tsx +50 -14
  210. package/src/plugins/builtin/cloud/askg/protocol.ts +39 -1
  211. package/src/plugins/builtin/cloud/askg/result-budget.ts +156 -0
  212. package/src/plugins/builtin/cloud/askg/user-data.ts +89 -0
  213. package/src/plugins/builtin/cloud/auth-actions.tsx +55 -31
  214. package/src/plugins/builtin/cloud/plugin.tsx +4 -1
  215. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -1
  216. package/src/plugins/builtin/cloud/team/pane.tsx +3 -2
  217. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +2 -2
  218. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  219. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +2 -2
  220. package/src/plugins/builtin/cloud/trial-offer-status-widget.tsx +202 -0
  221. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  222. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +29 -3
  223. package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +1 -1
  224. package/src/plugins/builtin/cloud-tweets/table.tsx +1 -1
  225. package/src/plugins/builtin/comment-letters/client.ts +342 -0
  226. package/src/plugins/builtin/comment-letters/index.tsx +74 -0
  227. package/src/plugins/builtin/comment-letters/pane.tsx +295 -0
  228. package/src/plugins/builtin/comment-letters/types.ts +43 -0
  229. package/src/plugins/builtin/company-attention/app-rank.tsx +79 -0
  230. package/src/plugins/builtin/company-attention/client.ts +69 -0
  231. package/src/plugins/builtin/company-attention/evidence.ts +45 -0
  232. package/src/plugins/builtin/company-attention/headless.ts +47 -0
  233. package/src/plugins/builtin/company-attention/index.tsx +30 -0
  234. package/src/plugins/builtin/company-attention/model.ts +129 -0
  235. package/src/plugins/builtin/company-attention/pane.tsx +183 -0
  236. package/src/plugins/builtin/company-kpis/client.ts +92 -0
  237. package/src/plugins/builtin/company-kpis/headless.ts +32 -0
  238. package/src/plugins/builtin/company-kpis/index.tsx +26 -0
  239. package/src/plugins/builtin/company-kpis/model.ts +124 -0
  240. package/src/plugins/builtin/company-kpis/pane.tsx +226 -0
  241. package/src/plugins/builtin/company-kpis/related.ts +11 -0
  242. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +44 -0
  243. package/src/plugins/builtin/composite-plugins.ts +34 -0
  244. package/src/plugins/builtin/correlation/history.ts +4 -1
  245. package/src/plugins/builtin/correlation/index.tsx +139 -80
  246. package/src/plugins/builtin/correlation/matrix/pair-chart.tsx +48 -0
  247. package/src/plugins/builtin/correlation/matrix/selection.ts +77 -0
  248. package/src/plugins/builtin/correlation/relationship/model.ts +2 -2
  249. package/src/plugins/builtin/cpi/client.ts +66 -0
  250. package/src/plugins/builtin/cpi/headless.ts +78 -0
  251. package/src/plugins/builtin/cpi/index.tsx +31 -0
  252. package/src/plugins/builtin/cpi/model.ts +198 -0
  253. package/src/plugins/builtin/cpi/pane.tsx +157 -0
  254. package/src/plugins/builtin/credit-documents/client.ts +99 -0
  255. package/src/plugins/builtin/credit-documents/detail.tsx +109 -0
  256. package/src/plugins/builtin/credit-documents/evidence.ts +64 -0
  257. package/src/plugins/builtin/credit-documents/gauge.tsx +55 -0
  258. package/src/plugins/builtin/credit-documents/headless.ts +40 -0
  259. package/src/plugins/builtin/credit-documents/index.tsx +19 -0
  260. package/src/plugins/builtin/credit-documents/model.ts +336 -0
  261. package/src/plugins/builtin/credit-documents/pane.tsx +185 -0
  262. package/src/plugins/builtin/custom-view/view-spec.ts +14 -2
  263. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -0
  264. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  265. package/src/plugins/builtin/doe/client.ts +73 -0
  266. package/src/plugins/builtin/doe/headless.ts +93 -0
  267. package/src/plugins/builtin/doe/index.tsx +41 -0
  268. package/src/plugins/builtin/doe/model.ts +215 -0
  269. package/src/plugins/builtin/doe/pane.tsx +148 -0
  270. package/src/plugins/builtin/doe/range-bar.tsx +45 -0
  271. package/src/plugins/builtin/earnings/board.tsx +3 -0
  272. package/src/plugins/builtin/earnings/index.tsx +2 -0
  273. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -1
  274. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +7 -7
  275. package/src/plugins/builtin/earnings-ripple/client.ts +60 -0
  276. package/src/plugins/builtin/earnings-ripple/headless.ts +35 -0
  277. package/src/plugins/builtin/earnings-ripple/index.tsx +35 -0
  278. package/src/plugins/builtin/earnings-ripple/model.ts +98 -0
  279. package/src/plugins/builtin/earnings-ripple/pane.tsx +103 -0
  280. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  281. package/src/plugins/builtin/equity-screener/criterion-editor.tsx +1 -1
  282. package/src/plugins/builtin/equity-screener/model.ts +74 -9
  283. package/src/plugins/builtin/equity-screener/pane.tsx +2 -0
  284. package/src/plugins/builtin/estimate-revisions/pane.tsx +3 -0
  285. package/src/plugins/builtin/executives/pane.tsx +7 -5
  286. package/src/plugins/builtin/exposure/charts.tsx +66 -0
  287. package/src/plugins/builtin/exposure/client.ts +53 -0
  288. package/src/plugins/builtin/exposure/drivers.ts +29 -0
  289. package/src/plugins/builtin/exposure/editor.tsx +61 -0
  290. package/src/plugins/builtin/exposure/evidence.ts +39 -0
  291. package/src/plugins/builtin/exposure/headless.ts +51 -0
  292. package/src/plugins/builtin/exposure/holdings.ts +56 -0
  293. package/src/plugins/builtin/exposure/index.tsx +13 -0
  294. package/src/plugins/builtin/exposure/model.ts +104 -0
  295. package/src/plugins/builtin/exposure/pane.tsx +247 -0
  296. package/src/plugins/builtin/fear-greed/charts.tsx +1 -1
  297. package/src/plugins/builtin/fear-greed/pane.tsx +2 -9
  298. package/src/plugins/builtin/filing-events/pane.tsx +2 -1
  299. package/src/plugins/builtin/gpu/board.tsx +163 -0
  300. package/src/plugins/builtin/gpu/changes.tsx +94 -0
  301. package/src/plugins/builtin/gpu/client.ts +130 -0
  302. package/src/plugins/builtin/gpu/equities.tsx +112 -0
  303. package/src/plugins/builtin/gpu/headless.ts +67 -0
  304. package/src/plugins/builtin/gpu/history.tsx +140 -0
  305. package/src/plugins/builtin/gpu/index.tsx +18 -0
  306. package/src/plugins/builtin/gpu/ladder.tsx +127 -0
  307. package/src/plugins/builtin/gpu/model.ts +317 -0
  308. package/src/plugins/builtin/gpu/pane.tsx +63 -0
  309. package/src/plugins/builtin/help/index.tsx +19 -2
  310. package/src/plugins/builtin/holders/pane.tsx +5 -9
  311. package/src/plugins/builtin/insider/detail.tsx +119 -0
  312. package/src/plugins/builtin/insider/display.ts +221 -0
  313. package/src/plugins/builtin/insider/headless.ts +24 -3
  314. package/src/plugins/builtin/insider/index.tsx +206 -108
  315. package/src/plugins/builtin/insider/insider-data.ts +31 -3
  316. package/src/plugins/builtin/insider/model.ts +53 -4
  317. package/src/plugins/builtin/insider/table-model.ts +226 -0
  318. package/src/plugins/builtin/insider/table.tsx +79 -0
  319. package/src/plugins/builtin/iv-history/screen-pane.tsx +1 -1
  320. package/src/plugins/builtin/jobs/pane.tsx +14 -7
  321. package/src/plugins/builtin/kelly-sizer/pane.tsx +1 -1
  322. package/src/plugins/builtin/macro-day/client.ts +50 -0
  323. package/src/plugins/builtin/macro-day/headless.ts +51 -0
  324. package/src/plugins/builtin/macro-day/index.tsx +21 -0
  325. package/src/plugins/builtin/macro-day/model.ts +137 -0
  326. package/src/plugins/builtin/macro-day/pane.tsx +118 -0
  327. package/src/plugins/builtin/macro-day/releases.ts +58 -0
  328. package/src/plugins/builtin/market-halts/model.ts +30 -17
  329. package/src/plugins/builtin/market-halts/pane.tsx +15 -21
  330. package/src/plugins/builtin/market-heatmap/data.ts +1 -1
  331. package/src/plugins/builtin/market-heatmap/index.tsx +198 -50
  332. package/src/plugins/builtin/market-heatmap/portfolio.ts +220 -0
  333. package/src/plugins/builtin/market-movers/index.tsx +1 -0
  334. package/src/plugins/builtin/market-movers/session-body.tsx +5 -2
  335. package/src/plugins/builtin/mna/distress/client.ts +326 -0
  336. package/src/plugins/builtin/mna/distress/detail.tsx +247 -0
  337. package/src/plugins/builtin/mna/distress/headless.ts +257 -0
  338. package/src/plugins/builtin/mna/distress/model.ts +427 -0
  339. package/src/plugins/builtin/mna/distress/pane.tsx +606 -0
  340. package/src/plugins/builtin/mna/index.tsx +15 -0
  341. package/src/plugins/builtin/mna/pane.tsx +42 -1
  342. package/src/plugins/builtin/news/index.tsx +62 -16
  343. package/src/plugins/builtin/news/wire/filter-articles.ts +54 -0
  344. package/src/plugins/builtin/news/wire/index.tsx +37 -3
  345. package/src/plugins/builtin/news/wire/industry-pane.tsx +29 -13
  346. package/src/plugins/builtin/news/wire/mutes.ts +253 -0
  347. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +117 -0
  348. package/src/plugins/builtin/news/wire/news/footer.ts +4 -0
  349. package/src/plugins/builtin/news/wire/news/list-search.ts +34 -0
  350. package/src/plugins/builtin/news/wire/news/pop-out.tsx +158 -0
  351. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +64 -19
  352. package/src/plugins/builtin/openfda/client.ts +300 -0
  353. package/src/plugins/builtin/openfda/index.tsx +71 -0
  354. package/src/plugins/builtin/openfda/pane.tsx +382 -0
  355. package/src/plugins/builtin/openfda/types.ts +76 -0
  356. package/src/plugins/builtin/options/footer.ts +1 -0
  357. package/src/plugins/builtin/options/view.tsx +1 -1
  358. package/src/plugins/builtin/options-calculator/pane.tsx +0 -1
  359. package/src/plugins/builtin/options-positioning/index.tsx +4 -2
  360. package/src/plugins/builtin/options-positioning/pane.tsx +1 -1
  361. package/src/plugins/builtin/pe-band/client.ts +61 -0
  362. package/src/plugins/builtin/pe-band/headless.ts +51 -0
  363. package/src/plugins/builtin/pe-band/index.tsx +20 -0
  364. package/src/plugins/builtin/pe-band/model.ts +267 -0
  365. package/src/plugins/builtin/pe-band/pane.tsx +159 -0
  366. package/src/plugins/builtin/perps/client.ts +66 -0
  367. package/src/plugins/builtin/perps/equity-row.tsx +28 -0
  368. package/src/plugins/builtin/perps/headless.ts +34 -0
  369. package/src/plugins/builtin/perps/index.tsx +14 -0
  370. package/src/plugins/builtin/perps/model.ts +58 -0
  371. package/src/plugins/builtin/perps/pane.tsx +185 -0
  372. package/src/plugins/builtin/plugin-marketplace/pane.tsx +0 -1
  373. package/src/plugins/builtin/portfolio-list/cli/render.ts +177 -87
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -20
  375. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -2
  376. package/src/plugins/builtin/portfolio-list/headless.ts +237 -0
  377. package/src/plugins/builtin/portfolio-list/index.tsx +2 -0
  378. package/src/plugins/builtin/portfolio-list/mutations.ts +70 -1
  379. package/src/plugins/builtin/portfolio-list/pane/data.ts +17 -0
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +88 -5
  381. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +85 -0
  382. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +18 -12
  383. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +6 -0
  384. package/src/plugins/builtin/power/client.ts +100 -0
  385. package/src/plugins/builtin/power/detail.tsx +82 -0
  386. package/src/plugins/builtin/power/evidence.ts +56 -0
  387. package/src/plugins/builtin/power/generation.tsx +37 -0
  388. package/src/plugins/builtin/power/headless.ts +43 -0
  389. package/src/plugins/builtin/power/index.tsx +16 -0
  390. package/src/plugins/builtin/power/model.ts +89 -0
  391. package/src/plugins/builtin/power/pane.tsx +209 -0
  392. package/src/plugins/builtin/power/query.ts +25 -0
  393. package/src/plugins/builtin/relative-rotation/pane.tsx +3 -3
  394. package/src/plugins/builtin/research/analyst-pane.tsx +1 -1
  395. package/src/plugins/builtin/research/corporate-actions-pane.tsx +13 -4
  396. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +11 -9
  397. package/src/plugins/builtin/research-search/pane.tsx +21 -7
  398. package/src/plugins/builtin/revenue-breakdown/pane.tsx +1 -1
  399. package/src/plugins/builtin/reverse-dcf/client.ts +35 -0
  400. package/src/plugins/builtin/reverse-dcf/headless.ts +44 -0
  401. package/src/plugins/builtin/reverse-dcf/index.tsx +20 -0
  402. package/src/plugins/builtin/reverse-dcf/model.ts +87 -0
  403. package/src/plugins/builtin/reverse-dcf/pane.tsx +107 -0
  404. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  405. package/src/plugins/builtin/scanner/denied.tsx +4 -2
  406. package/src/plugins/builtin/scanner/feed.ts +1 -0
  407. package/src/plugins/builtin/scanner/flow-pane.tsx +3 -2
  408. package/src/plugins/builtin/scanner/hilo-pane.tsx +1 -1
  409. package/src/plugins/builtin/seasonality/client.ts +4 -1
  410. package/src/plugins/builtin/sec/command-bar-search.ts +217 -0
  411. package/src/plugins/builtin/sec/forms.ts +54 -0
  412. package/src/plugins/builtin/sec/headless.ts +31 -5
  413. package/src/plugins/builtin/sec/index.tsx +67 -11
  414. package/src/plugins/builtin/sec/model.ts +35 -5
  415. package/src/plugins/builtin/sectors/headless.ts +1 -0
  416. package/src/plugins/builtin/sectors/index.tsx +72 -26
  417. package/src/plugins/builtin/shared/cloud-upgrade.ts +143 -39
  418. package/src/plugins/builtin/shared/pro-feature-wall.tsx +8 -4
  419. package/src/plugins/builtin/shared/research-cells.tsx +107 -0
  420. package/src/plugins/builtin/shared/series-list-detail.tsx +5 -1
  421. package/src/plugins/builtin/shared/ticker-request.ts +1 -1
  422. package/src/plugins/builtin/shared/wall-teaser-catalog.ts +80 -0
  423. package/src/plugins/builtin/shared/wall-teaser.tsx +192 -0
  424. package/src/plugins/builtin/shared/wall-view-lifecycle.ts +43 -0
  425. package/src/plugins/builtin/short-interest/index.tsx +31 -0
  426. package/src/plugins/builtin/short-interest/watch-client.ts +70 -0
  427. package/src/plugins/builtin/short-interest/watch-headless.ts +49 -0
  428. package/src/plugins/builtin/short-interest/watch-model.ts +185 -0
  429. package/src/plugins/builtin/short-interest/watch-pane.tsx +169 -0
  430. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  431. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  432. package/src/plugins/builtin/supply-chain/client.ts +104 -0
  433. package/src/plugins/builtin/supply-chain/disclosure-quote.tsx +17 -0
  434. package/src/plugins/builtin/supply-chain/evidence.ts +88 -0
  435. package/src/plugins/builtin/supply-chain/flow-layout.ts +133 -0
  436. package/src/plugins/builtin/supply-chain/flow.tsx +323 -0
  437. package/src/plugins/builtin/supply-chain/graph-client.ts +83 -0
  438. package/src/plugins/builtin/supply-chain/graph-evidence.ts +25 -0
  439. package/src/plugins/builtin/supply-chain/graph-model.ts +45 -0
  440. package/src/plugins/builtin/supply-chain/graph-pane.tsx +218 -0
  441. package/src/plugins/builtin/supply-chain/graph.tsx +163 -0
  442. package/src/plugins/builtin/supply-chain/headless.ts +105 -0
  443. package/src/plugins/builtin/supply-chain/index.tsx +17 -0
  444. package/src/plugins/builtin/supply-chain/model.ts +145 -0
  445. package/src/plugins/builtin/supply-chain/pane.tsx +276 -0
  446. package/src/plugins/builtin/supply-chain/row-evidence.tsx +96 -0
  447. package/src/plugins/builtin/supply-chain/trust.ts +34 -0
  448. package/src/plugins/builtin/themes/client.ts +39 -0
  449. package/src/plugins/builtin/themes/headless.ts +38 -0
  450. package/src/plugins/builtin/themes/model.ts +92 -0
  451. package/src/plugins/builtin/themes/pane.tsx +125 -0
  452. package/src/plugins/builtin/thirteenf/pane.tsx +6 -15
  453. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -6
  454. package/src/plugins/builtin/ticker-detail/data-panes/returns-model.ts +201 -0
  455. package/src/plugins/builtin/ticker-detail/data-panes/returns-pane.tsx +327 -0
  456. package/src/plugins/builtin/ticker-detail/financials/period-end-history.ts +5 -2
  457. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +60 -12
  458. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +2 -1
  459. package/src/plugins/builtin/ticker-detail/headless.ts +7 -2
  460. package/src/plugins/builtin/ticker-detail/index.tsx +22 -0
  461. package/src/plugins/builtin/ticker-detail/overview/components.tsx +53 -30
  462. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -0
  463. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +112 -47
  464. package/src/plugins/builtin/ticker-detail/pane.tsx +50 -6
  465. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +21 -42
  466. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +3 -0
  467. package/src/plugins/builtin/time-sales/pane.tsx +1 -1
  468. package/src/plugins/builtin/world-indices/indices.ts +1 -0
  469. package/src/plugins/catalog-browser.ts +40 -0
  470. package/src/plugins/catalog-manifest.ts +3 -0
  471. package/src/plugins/catalog-ui.ts +6 -0
  472. package/src/plugins/current-target.ts +0 -9
  473. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  474. package/src/renderers/browser/cloud-transport.ts +66 -1
  475. package/src/renderers/browser/main.tsx +2 -0
  476. package/src/renderers/dom/data-table/row.tsx +2 -0
  477. package/src/renderers/dom/desktop/controls.tsx +38 -25
  478. package/src/renderers/dom/desktop/icons.tsx +8 -0
  479. package/src/renderers/dom/host/chart-surface.tsx +36 -2
  480. package/src/renderers/dom/host/focus-scope.ts +11 -1
  481. package/src/renderers/dom/styles.css +28 -0
  482. package/src/renderers/electrobun/view/backend-rpc.ts +3 -2
  483. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  484. package/src/renderers/electrobun/view/http-fetch.ts +2 -0
  485. package/src/renderers/electrobun/view/main.tsx +3 -1
  486. package/src/renderers/electrobun/view/rpc-timeout.ts +59 -0
  487. package/src/renderers/opentui/start.tsx +3 -0
  488. package/src/shares/api.ts +3 -1
  489. package/src/sources/gloomberb-cloud/index.ts +47 -13
  490. package/src/sources/history-result.ts +1 -0
  491. package/src/sources/history-retention.ts +16 -0
  492. package/src/sources/provider-router/history.ts +4 -2
  493. package/src/sources/sec-edgar/identity.ts +36 -0
  494. package/src/sources/sec-edgar.ts +1 -34
  495. package/src/state/app/context/index.tsx +8 -0
  496. package/src/sync/core-contributors.ts +20 -2
  497. package/src/telemetry/attention-counts.ts +164 -0
  498. package/src/telemetry/live-config.ts +7 -0
  499. package/src/test-support/rpc-loopback.ts +7 -3
  500. package/src/test-support/shell-pane-keys.tsx +34 -0
  501. package/src/tickers/instrument-kind.ts +12 -6
  502. package/src/tickers/open-target.ts +1 -1
  503. package/src/tickers/search/asset-classes.ts +91 -0
  504. package/src/tickers/search/index.ts +62 -15
  505. package/src/tickers/search/ranking.ts +22 -3
  506. package/src/tickers/search/upsert.ts +4 -1
  507. package/src/time-series/live-bars.ts +8 -3
  508. package/src/time-series/resolve.ts +224 -17
  509. package/src/time-series/spec.ts +1 -0
  510. package/src/time-series/types.ts +9 -1
  511. package/src/types/config.ts +23 -1
  512. package/src/types/data-provider.ts +2 -0
  513. package/src/types/headless.ts +17 -0
  514. package/src/types/price-history.ts +2 -0
  515. package/src/ui/host.tsx +6 -0
  516. package/src/utils/double-escape-close.ts +20 -0
  517. package/src/utils/exchanges.ts +1 -0
  518. package/src/utils/html-entities.ts +15 -0
  519. package/src/utils/http-proxy-response.ts +44 -7
  520. package/src/utils/plugin-proxy-hosts.json +3 -0
  521. package/src/utils/price-history.ts +35 -1
  522. package/src/utils/sec.ts +21 -2
  523. package/src/utils/text-wrap.ts +13 -8
  524. package/src/utils/zoned-date-time.ts +67 -0
@@ -0,0 +1,35 @@
1
+ import type { PluginModule } from "../plugin-module";
2
+ import { formatTickerListInput, parseTickerListInput } from "../../../tickers/list";
3
+ import { earningsRippleHeadless } from "./headless";
4
+ import { EarningsRipplePane } from "./pane";
5
+
6
+ function namedSymbols(arg: string | undefined): string[] {
7
+ try { return arg?.trim() ? parseTickerListInput(arg) : []; } catch { return []; }
8
+ }
9
+
10
+ export const earningsRippleModule: PluginModule = {
11
+ panes: [{
12
+ id: "earnings-ripple", name: "Earnings Ripple", icon: "E", component: EarningsRipplePane,
13
+ defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 20 },
14
+ tableExport: true,
15
+ settings: { title: "Earnings Ripple Settings", fields: [
16
+ { key: "symbols", label: "Holdings", type: "text", placeholder: "Blank for your portfolios and watchlists" },
17
+ ] },
18
+ }],
19
+ paneTemplates: [{
20
+ id: "earnings-ripple-pane", paneId: "earnings-ripple", label: "Earnings Ripple",
21
+ description: "Customers of your holdings that report soon, with the share of each holding's revenue they make up.",
22
+ keywords: ["ripl", "ripple", "earnings", "customers", "supply chain", "exposure", "read-through"],
23
+ shortcut: { prefix: "RIPL", argPlaceholder: "tickers", argKind: "ticker-list", argOptional: true, openWithoutArg: true },
24
+ headless: earningsRippleHeadless,
25
+ canCreate: () => true,
26
+ createInstance: (_context, options) => {
27
+ const symbols = options?.symbols?.length ? options.symbols : namedSymbols(options?.arg);
28
+ return {
29
+ title: symbols.length ? `RIPL ${formatTickerListInput(symbols)}` : "Earnings Ripple",
30
+ placement: "floating",
31
+ settings: symbols.length ? { symbols: formatTickerListInput(symbols) } : undefined,
32
+ };
33
+ },
34
+ }],
35
+ };
@@ -0,0 +1,98 @@
1
+ import type { EarningsCalendarReport, EarningsTiming } from "../../../api-client/earnings";
2
+ import type { SupplyChainPayload, SupplyRow } from "../../../api-client/supply-chain";
3
+ import { isUsListingExchange } from "../../../utils/exchanges";
4
+ import { MIN_AVERAGE_REPORTS } from "../earnings/board-model";
5
+
6
+ /** Days ahead the calendar is read. */
7
+ export const RIPPLE_DAYS = 30;
8
+
9
+ export interface RippleRow {
10
+ id: string;
11
+ /** The company that reports. */
12
+ company: string;
13
+ companyName: string;
14
+ /** `customer`: the company buys from the holding. `supplier`: it sells to the holding. */
15
+ link: "customer" | "supplier";
16
+ date: string;
17
+ timing: EarningsTiming | null;
18
+ /** Mean absolute move over the company's last reports, once it has as many as ERN requires. */
19
+ averageMove: number | null;
20
+ holding: string;
21
+ /**
22
+ * Percent of the seller's revenue, as the seller's filing discloses it: the
23
+ * holding's revenue for a customer, the supplier's revenue for a supplier.
24
+ */
25
+ pctOfRevenue: number;
26
+ pctScope: string | null;
27
+ period: string;
28
+ /** The holding's own next report in the window, when it has one. */
29
+ holdingDate: string | null;
30
+ }
31
+
32
+ /** Whose revenue the share is of. */
33
+ export const revenueOwner = (row: RippleRow) => row.link === "customer" ? row.holding : row.company;
34
+
35
+ /**
36
+ * Listed counterparties in one role with a share of the seller's revenue: one
37
+ * per company, the latest period, then the larger share. Receivables and
38
+ * purchase shares measure something else and are left out.
39
+ */
40
+ function revenueLinks(rows: readonly SupplyRow[], role: "customer" | "supplier"): SupplyRow[] {
41
+ const best = new Map<string, SupplyRow>();
42
+ for (const row of rows) {
43
+ const ticker = row.counterparty.ticker;
44
+ if (row.role !== role || !ticker || row.counterparty.aggregate || row.pctBasis !== "revenue" || row.pctOfRevenue == null) continue;
45
+ // The calendar covers US listings; a foreign code such as Foxconn's 2317 could collide with one.
46
+ if (row.counterparty.exchange && !isUsListingExchange(row.counterparty.exchange)) continue;
47
+ const key = ticker.toUpperCase();
48
+ const current = best.get(key);
49
+ if (!current || row.period > current.period || (row.period === current.period && row.pctOfRevenue > current.pctOfRevenue!)) best.set(key, row);
50
+ }
51
+ return [...best.values()];
52
+ }
53
+
54
+ /** Customers a holding names (`says`) and suppliers that name it (`names`), each with its link. */
55
+ function rippleLinks(chain: SupplyChainPayload) {
56
+ return [
57
+ ...revenueLinks(chain.says, "customer").map((row) => ({ row, link: "customer" as const })),
58
+ ...revenueLinks(chain.names, "supplier").map((row) => ({ row, link: "supplier" as const })),
59
+ ];
60
+ }
61
+
62
+ /** Calendar symbols use the same eligibility as the displayed rows, in both directions. */
63
+ export function rippleCompanyTickers(chains: ReadonlyMap<string, SupplyChainPayload>): string[] {
64
+ const companies = new Set<string>();
65
+ for (const [holding, chain] of chains) {
66
+ for (const { row } of rippleLinks(chain)) {
67
+ const company = row.counterparty.ticker!.toUpperCase();
68
+ if (company !== holding.toUpperCase()) companies.add(company);
69
+ }
70
+ }
71
+ return [...companies];
72
+ }
73
+
74
+ /**
75
+ * Customers a holding names in its own filings (`says`), and suppliers whose
76
+ * filings name the holding as a customer (`names`), that report in the
77
+ * calendar: soonest first, then by the share.
78
+ */
79
+ export function projectRipple(chains: ReadonlyMap<string, SupplyChainPayload>, reports: readonly EarningsCalendarReport[]): RippleRow[] {
80
+ const next = new Map<string, EarningsCalendarReport>();
81
+ for (const report of reports) {
82
+ const key = report.symbol.toUpperCase();
83
+ const current = next.get(key);
84
+ if (!current || report.date < current.date) next.set(key, report);
85
+ }
86
+ const rows: RippleRow[] = [];
87
+ for (const [holding, chain] of chains) {
88
+ for (const { row, link } of rippleLinks(chain)) {
89
+ const company = row.counterparty.ticker!.toUpperCase();
90
+ const report = next.get(company);
91
+ if (!report || company === holding.toUpperCase()) continue;
92
+ rows.push({ id: `${holding}:${link}:${company}`, company, companyName: report.name ?? row.counterparty.name, link, date: report.date,
93
+ timing: report.timing, averageMove: report.averageReports >= MIN_AVERAGE_REPORTS ? report.averageMove : null, holding, pctOfRevenue: row.pctOfRevenue!, pctScope: row.pctScope,
94
+ period: row.period, holdingDate: next.get(holding.toUpperCase())?.date ?? null });
95
+ }
96
+ }
97
+ return rows.sort((a, b) => a.date.localeCompare(b.date) || b.pctOfRevenue - a.pctOfRevenue || a.holding.localeCompare(b.holding));
98
+ }
@@ -0,0 +1,103 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { DataTableView, EmptyState, PaneStatusBody, usePaneFooter, usePaneNoticeFooter,
3
+ type DataTableCell, type DataTableColumn, type PaneHint } from "../../../components";
4
+ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
+ import { usePlanAccess } from "../../../api-client/plan-access";
6
+ import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginAppActions, usePluginPaneState, useTickers } from "../../../public/react";
7
+ import type { PaneProps } from "../../../types/plugin";
8
+ import { Box } from "../../../ui";
9
+ import { isUsListingExchange } from "../../../utils/exchanges";
10
+ import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
11
+ import { useResearchCloudSession } from "../shared/research-cloud-session";
12
+ import { cachedRippleSources, loadRipple } from "./client";
13
+ import { revenueOwner, RIPPLE_DAYS, type RippleRow } from "./model";
14
+
15
+ /** Calls per refresh stay bounded: one disclosure request per holding. */
16
+ const RIPPLE_HOLDINGS_LIMIT = 60;
17
+
18
+ const COLUMNS: DataTableColumn[] = [
19
+ { id: "date", label: "Date", width: 11, align: "left" },
20
+ { id: "timing", label: "Time", width: 5, align: "left" },
21
+ { id: "company", label: "Reports", width: 24, align: "left" },
22
+ { id: "link", label: "Link", width: 13, align: "left" },
23
+ { id: "holding", label: "Holding", width: 9, align: "left" },
24
+ { id: "pct", label: "Revenue share", width: 15, align: "right" },
25
+ { id: "move", label: "Avg move", width: 9, align: "right" },
26
+ { id: "holdingDate", label: "Holding reports", width: 16, align: "right" },
27
+ ];
28
+
29
+ function renderCell(row: RippleRow, column: DataTableColumn): DataTableCell {
30
+ switch (column.id) {
31
+ case "date": return { text: row.date };
32
+ case "timing": return { text: row.timing?.toUpperCase() ?? "--" };
33
+ case "company": return { text: `${row.company} ${row.companyName}` };
34
+ case "link": return { text: row.link === "customer" ? "customer of" : "supplier to" };
35
+ case "holding": return { text: row.holding };
36
+ case "pct": return { text: `${row.pctOfRevenue}%${row.pctScope ? "*" : ""} of ${revenueOwner(row)}`, value: row.pctOfRevenue };
37
+ case "move": return { text: row.averageMove == null ? "--" : `±${(row.averageMove * 100).toFixed(1)}%`, value: row.averageMove };
38
+ default: return { text: row.holdingDate ?? "--" };
39
+ }
40
+ }
41
+
42
+ export function EarningsRipplePane({ width, height, focused }: PaneProps) {
43
+ const [symbolsText] = usePaneSettingValue("symbols", "");
44
+ const tickers = useTickers();
45
+ const { createPaneFromTemplate } = usePluginAppActions();
46
+ const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedRow", null);
47
+ const session = useResearchCloudSession();
48
+ const access = usePlanAccess();
49
+ const accessKey = `${session.requestKey}:${access.hasProAccess ? "full" : "preview"}`;
50
+ // Named tickers, or every US listing in the user's portfolios and watchlists, positions first.
51
+ const scope = useMemo(() => {
52
+ const named = symbolsText.split(/[\s,]+/).map((symbol) => symbol.trim().toUpperCase()).filter(Boolean);
53
+ const all = named.length ? [...new Set(named)] : [...new Set([...tickers.values()]
54
+ .filter((ticker) => ticker.metadata.portfolios.length + ticker.metadata.watchlists.length > 0 && isUsListingExchange(ticker.metadata.exchange))
55
+ .sort((a, b) => Number(b.metadata.portfolios.length > 0) - Number(a.metadata.portfolios.length > 0) || a.metadata.ticker.localeCompare(b.metadata.ticker))
56
+ .map((ticker) => ticker.metadata.ticker.toUpperCase()))];
57
+ return { holdings: all.slice(0, RIPPLE_HOLDINGS_LIMIT), total: all.length, named: named.length > 0 };
58
+ }, [symbolsText, tickers]);
59
+ const holdings = scope.holdings;
60
+ const holdingsKey = holdings.join(",");
61
+ const loader = useCallback((force: boolean) => loadRipple(holdings, cachedRippleSources(accessKey, force)), [holdingsKey, accessKey]);
62
+ const ripple = useAsyncResource(holdings.length ? loader : null);
63
+ useAutoRefresh(ripple.updatedAt, ripple.load);
64
+ usePaneRefreshKey(() => { void ripple.reload(); }, { focused });
65
+
66
+ const rows = ripple.data?.rows ?? [];
67
+ const selected = rows.find((row) => row.id === selectedId) ?? rows[0] ?? null;
68
+ const failures = ripple.data?.failures ?? [];
69
+ const truncated = ripple.data?.truncated ?? [];
70
+ const openUpgrade = useCloudUpgradeAction("ripl");
71
+ usePaneNoticeFooter({ registrationId: "earnings-ripple-notices", focused, notices: [
72
+ ...(scope.total > holdings.length ? [`Checking ${holdings.length} of ${scope.total} holdings${scope.named ? "" : ", positions first. Name tickers in pane settings to pick others"}`] : []),
73
+ ...(failures.length ? [`No disclosures for ${failures.map((failure) => failure.symbol).join(", ")}`] : []),
74
+ ...(truncated.length ? [`Free preview: top three customers and suppliers for ${truncated.join(", ")}`] : []),
75
+ ] });
76
+ const openSupply = useCallback((row: RippleRow) => createPaneFromTemplate("supply-chain-pane", { symbol: row.holding }), [createPaneFromTemplate]);
77
+ usePaneFooter("earnings-ripple", () => {
78
+ const hints: PaneHint[] = [
79
+ ...(selected ? [
80
+ { id: "supply", key: "s", label: "upply chain", onPress: () => openSupply(selected) },
81
+ { id: "earnings", key: "e", label: "arnings", onPress: () => createPaneFromTemplate("earnings-calendar-pane", { arg: selected.company }) },
82
+ ] : []),
83
+ ...(truncated.length ? [{ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: openUpgrade }] : []),
84
+ ];
85
+ return { info: [
86
+ ...(ripple.loading ? [{ id: "loading", parts: [{ text: "loading disclosures", tone: "muted" as const }] }] : []),
87
+ ...(ripple.data?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
88
+ ], hints };
89
+ }, [ripple.loading, ripple.data?.stale, selected, openSupply, createPaneFromTemplate, truncated.length, openUpgrade]);
90
+
91
+ return <Box width={width} height={height} flexDirection="column" overflow="hidden">
92
+ {!holdings.length ? <EmptyState title="Add tickers to a portfolio or watchlist, or name them in pane settings." />
93
+ : <PaneStatusBody subject="earnings ripple" loading={ripple.loading && !ripple.data} error={!ripple.data ? ripple.error : null}
94
+ empty={!!ripple.data && !rows.length} emptyTitle={`No disclosed customer or supplier of these holdings reports in the next ${RIPPLE_DAYS} days.`}>
95
+ <DataTableView<RippleRow> focused={focused} columns={COLUMNS} items={rows} rootWidth={width} rootHeight={height}
96
+ getItemKey={(row) => row.id} emptyStateTitle="No reports." sortColumnId={null} sortDirection="asc"
97
+ selection={{ kind: "id", selectedId: selected?.id ?? null, getId: (row) => row.id, onChange: setSelectedId }}
98
+ onActivate={openSupply}
99
+ getExportMetadata={() => [["window", `${ripple.data?.from} to ${ripple.data?.to}`], ["revenue share", "percent of the seller's revenue, as its filing discloses"]]}
100
+ renderCell={renderCell} />
101
+ </PaneStatusBody>}
102
+ </Box>;
103
+ }
@@ -19,7 +19,7 @@ const savedPath = (id: string) => `saved/${encodeURIComponent(id)}`;
19
19
  export const screenerApi = {
20
20
  query: (query: ScreenQuery, signal?: AbortSignal, client: ScreenerClient = apiClient) =>
21
21
  client.equityScreener<ScreenPayload>("query", json("POST", query, { signal })),
22
- fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research", { signal }),
22
+ fields: (signal?: AbortSignal) => apiClient.equityScreener<ScreenFieldsResponse>("fields?include=social,research,attention,hiring-apps,kpis", { signal }),
23
23
  export: (definition: ScreenDefinition, snapshotId: string) =>
24
24
  apiClient.equityScreener<ScreenExportResponse>("export", json("POST", { ...definition, snapshotId }, { headers: { Accept: "application/json" } })),
25
25
  saved: async () => (await apiClient.equityScreener<{ screens: SavedScreen[] }>("saved")).screens,
@@ -100,7 +100,7 @@ export function CriterionEditor({
100
100
  label="Field"
101
101
  emphasized={focused && active === "field"}
102
102
  value={fieldId}
103
- options={fields.map((row) => ({ value: row.id, label: row.label }))}
103
+ options={fields.map((row) => ({ value: row.id, label: row.access === "pro" ? `${row.label} · Pro` : row.label }))}
104
104
  variant="field"
105
105
  width={Math.min(40, width - 4)}
106
106
  onFocus={() => setActive("field")}
@@ -1,7 +1,10 @@
1
1
  import {
2
+ ATTENTION_FIELDS,
3
+ HIRING_APP_FIELDS,
2
4
  CATEGORY_FIELDS,
3
5
  NUMERIC_FIELDS,
4
6
  RESEARCH_FIELDS,
7
+ KPI_FIELDS,
5
8
  SOCIAL_FIELDS,
6
9
  type NumericField,
7
10
  type ScreenMetric,
@@ -20,6 +23,27 @@ import { formatCompact, formatNumber } from "../../../utils/format";
20
23
  * Growth and margins come from the latest fiscal-year statement, so they say FY.
21
24
  */
22
25
  export const SHORT_LABELS: Record<NumericField, string> = {
26
+ kpiArr: "ARR",
27
+ kpiNrrPercent: "NRR%",
28
+ kpiRpo: "RPO",
29
+ kpiCrpo: "CRPO",
30
+ kpiSubscribers: "SUBSCRIBERS",
31
+ kpiDau: "DAU",
32
+ kpiMau: "MAU",
33
+ kpiGmv: "GMV",
34
+ kpiTakeRatePercent: "TAKE RATE%",
35
+ kpiSameStoreSalesPercent: "SAME STORE%",
36
+ kpiStores: "STORES",
37
+ kpiBacklog: "BACKLOG",
38
+ kpiBookToBill: "BOOK/BILL",
39
+ kpiNimPercent: "NIM%",
40
+ kpiCet1Percent: "CET1%",
41
+ kpiCombinedRatioPercent: "COMBINED%",
42
+ kpiOccupancyPercent: "OCCUPANCY%",
43
+ kpiLoadFactorPercent: "LOAD FACTOR%",
44
+ guideRevenueChangePercent: "REV GUIDE CHG%",
45
+ guideEpsChangePercent: "EPS GUIDE CHG%",
46
+
23
47
  price: "PRICE",
24
48
  changePercent: "CHG%",
25
49
  volume: "VOLUME",
@@ -47,6 +71,8 @@ export const SHORT_LABELS: Record<NumericField, string> = {
47
71
  xPostsVsMedian: "X VS MED",
48
72
  wikiViewsPerDay: "WIKI/D",
49
73
  wikiViewsVsMedian: "WIKI VS MED",
74
+ attentionResearchUnits: "ATTN HRS",
75
+ attentionZScore: "ATTN Z",
50
76
  return1WPercent: "1W%",
51
77
  return1MPercent: "1M%",
52
78
  return3MPercent: "3M%",
@@ -61,20 +87,29 @@ export const SHORT_LABELS: Record<NumericField, string> = {
61
87
  analystUpsidePercent: "UPSIDE%",
62
88
  ivRank: "IV RANK",
63
89
  ivToHv: "IV/HV",
90
+ hiringOpenRoles: "OPEN ROLES",
91
+ hiringNetAddsWeek: "NET ROLES/WK",
92
+ hiringRemotePercent: "REMOTE%",
93
+ hiringZScore: "HIRING Z",
94
+ appAttentionScore: "APP SCORE",
95
+ appRankVelocity7d: "APP RANK/D",
96
+ appRatingDrift7d: "RATING 7D",
97
+ appRatingCountGrowth7d: "RATINGS 7D",
64
98
  };
65
- const COMPACT = new Set<NumericField>(["marketCap", "volume", "averageVolume20d", "shortInterestShares"]);
99
+ const COMPACT = new Set<NumericField>(["kpiArr", "kpiRpo", "kpiCrpo", "kpiSubscribers", "kpiDau", "kpiMau", "kpiGmv", "kpiStores", "kpiBacklog", "marketCap", "volume", "averageVolume20d", "shortInterestShares"]);
66
100
  /** Daily counts: whole numbers below a thousand, compact above. */
67
101
  const DAILY_COUNTS = new Set<NumericField>(["xPostsPerDay", "wikiViewsPerDay"]);
68
- const RATIOS = new Set<NumericField>(["xPostsVsMedian", "wikiViewsVsMedian", "priceToBook", "evToEbitda", "ivToHv"]);
102
+ const RATIOS = new Set<NumericField>(["kpiBookToBill", "xPostsVsMedian", "wikiViewsVsMedian", "priceToBook", "evToEbitda", "ivToHv"]);
69
103
  const RETURNS: NumericField[] = ["return1WPercent", "return1MPercent", "return3MPercent", "returnYtdPercent", "return1YPercent"];
70
- const SIGNED = new Set<NumericField>([
104
+ const SIGNED = new Set<NumericField>(["hiringNetAddsWeek", "hiringZScore", "appRankVelocity7d", "appRatingDrift7d", "appRatingCountGrowth7d",
105
+ "guideRevenueChangePercent", "guideEpsChangePercent", "kpiSameStoreSalesPercent",
71
106
  "changePercent", "revenueGrowthPercent", "earningsGrowthPercent", "shortInterestChangePercent",
72
107
  ...RETURNS, "fromHigh52WPercent", "epsRevision30dPercent", "analystUpsidePercent",
73
108
  ]);
74
109
  /** Moves whose sign is the news: they take the sign colour. */
75
110
  export const SIGN_COLORED = new Set<NumericField>(["changePercent", ...RETURNS, "epsRevision30dPercent", "analystUpsidePercent"]);
76
- const COUNTS = new Set<NumericField>(["insiderPurchases90d", "insiderSales90d", "institutionalHolders", "institutionalNewHolders", "institutionalExits", "ivRank"]);
77
- const MONETARY = new Set<NumericField>(["price", "marketCap"]);
111
+ const COUNTS = new Set<NumericField>(["appRatingCountGrowth7d", "hiringNetAddsWeek", "hiringOpenRoles", "attentionResearchUnits", "insiderPurchases90d", "insiderSales90d", "institutionalHolders", "institutionalNewHolders", "institutionalExits", "ivRank"]);
112
+ const MONETARY = new Set<NumericField>(["price", "marketCap", "kpiArr", "kpiRpo", "kpiCrpo", "kpiGmv", "kpiBacklog"]);
78
113
  /** Shown after the screen's own criteria when the pane has room. */
79
114
  const CONTEXT_FIELDS: NumericField[] = ["marketCap", "price", "changePercent", "trailingPE", "revenueGrowthPercent", "operatingMarginPercent", "dividendYieldPercent"];
80
115
 
@@ -117,6 +152,26 @@ export const screenRowId = (row: Pick<ScreenRow, "symbol" | "exchange">) =>
117
152
  `${canonicalExchange(row.exchange)}:${row.symbol}`;
118
153
  export const screenLabel = (field: string) =>
119
154
  ({
155
+ kpiArr: "Annual recurring revenue",
156
+ kpiNrrPercent: "Net revenue retention %",
157
+ kpiRpo: "Remaining performance obligations",
158
+ kpiCrpo: "Current RPO",
159
+ kpiSubscribers: "Subscribers",
160
+ kpiDau: "Daily active users",
161
+ kpiMau: "Monthly active users",
162
+ kpiGmv: "Gross merchandise value",
163
+ kpiTakeRatePercent: "Take rate %",
164
+ kpiSameStoreSalesPercent: "Same-store sales growth %",
165
+ kpiStores: "Stores",
166
+ kpiBacklog: "Backlog",
167
+ kpiBookToBill: "Book-to-bill",
168
+ kpiNimPercent: "Net interest margin %",
169
+ kpiCet1Percent: "CET1 ratio %",
170
+ kpiCombinedRatioPercent: "Combined ratio %",
171
+ kpiOccupancyPercent: "Occupancy %",
172
+ kpiLoadFactorPercent: "Load factor %",
173
+ guideRevenueChangePercent: "Revenue guide change %",
174
+ guideEpsChangePercent: "EPS guide change %",
120
175
  trailingPE: "Trailing P/E",
121
176
  forwardPE: "Forward P/E",
122
177
  enterpriseToRevenue: "EV / revenue",
@@ -142,6 +197,8 @@ export const screenLabel = (field: string) =>
142
197
  xPostsVsMedian: "X posts vs median",
143
198
  wikiViewsPerDay: "Wikipedia views per day",
144
199
  wikiViewsVsMedian: "Wikipedia views vs median",
200
+ attentionResearchUnits: "Research attention hours (Pro)",
201
+ attentionZScore: "Abnormal research attention (Pro)",
145
202
  return1WPercent: "1W return %",
146
203
  return1MPercent: "1M return %",
147
204
  return3MPercent: "3M return %",
@@ -156,6 +213,14 @@ export const screenLabel = (field: string) =>
156
213
  analystUpsidePercent: "Analyst upside %",
157
214
  ivRank: "IV rank",
158
215
  ivToHv: "IV / HV",
216
+ hiringOpenRoles: "Hiring open roles",
217
+ hiringNetAddsWeek: "Net open roles per week",
218
+ hiringRemotePercent: "Remote roles %",
219
+ hiringZScore: "Hiring z-score",
220
+ appAttentionScore: "App attention score / 100",
221
+ appRankVelocity7d: "App rank places per day, 7D",
222
+ appRatingDrift7d: "App rating drift, 7D",
223
+ appRatingCountGrowth7d: "App ratings added, 7D",
159
224
  })[field] ??
160
225
  field
161
226
  .replace(/([A-Z])/g, " $1")
@@ -300,14 +365,14 @@ export function parseScreenDefinition(value: unknown): ScreenDefinition {
300
365
  });
301
366
  if (
302
367
  !item.currency &&
303
- (["price", "marketCap"].includes(item.sort.field) ||
368
+ (MONETARY.has(item.sort.field as NumericField) ||
304
369
  item.criteria.some(
305
370
  (criterion) =>
306
- ["price", "marketCap"].includes(criterion.field) &&
371
+ MONETARY.has(criterion.field as NumericField) &&
307
372
  !["present", "missing"].includes(criterion.op),
308
373
  ))
309
374
  )
310
- throw new Error("Select one currency for price or market-cap comparisons.");
375
+ throw new Error("Select one currency for monetary comparisons.");
311
376
  return { version: 1, currency: item.currency, criteria,
312
377
  sort: { field: item.sort.field, direction: item.sort.direction } };
313
378
  }
@@ -363,7 +428,7 @@ export function validateScreenPayload(value: unknown): ScreenPayload {
363
428
  ids.add(screenRowId(row));
364
429
  for (const field of NUMERIC_FIELDS) {
365
430
  // A server from before social or research fields omits them; they read as unavailable.
366
- if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS] as readonly string[]).includes(field))
431
+ if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS] as readonly string[]).includes(field))
367
432
  row.metrics[field] = unavailableMetric();
368
433
  const metric = row.metrics[field];
369
434
  if (
@@ -634,6 +634,7 @@ function EquityScreenView({
634
634
  const bodyHeight = Math.max(5, height - 1 - tabRows);
635
635
  const saveContent = !access.emailVerified ? (
636
636
  <SignInWall
637
+ placement="eqs-save-signin"
637
638
  action="save and open your screens"
638
639
  needsVerification={session.needsVerification}
639
640
  />
@@ -787,6 +788,7 @@ function EquityScreenView({
787
788
  ) : mode === "saved" ? (
788
789
  !access.emailVerified ? (
789
790
  <SignInWall
791
+ placement="eqs-signin"
790
792
  action="save and open your screens"
791
793
  needsVerification={session.needsVerification}
792
794
  />
@@ -1,3 +1,4 @@
1
+ import { useCompanyDisclosureLinks } from "../company-kpis/related";
1
2
  import { useCallback, useEffect, useMemo, useRef } from "react";
2
3
  import { Box, ScrollBox, type ScrollBoxRenderable } from "../../../ui";
3
4
  import {
@@ -242,6 +243,7 @@ export function EstimateRevisionsPane({ width, height, focused }: PaneProps) {
242
243
  colors = useThemeColors();
243
244
  const identity = listingIdentity(boundSymbol, ticker?.metadata.exchange ?? "");
244
245
  const symbol = identity?.symbol ?? null;
246
+ useCompanyDisclosureLinks(boundSymbol);
245
247
  const exchange = canonicalExchange(identity?.exchange ?? "");
246
248
  const loader = useCallback(
247
249
  (force: boolean) => loadEstimates(symbol!, exchange, force),
@@ -386,6 +388,7 @@ export function EstimateRevisionsPane({ width, height, focused }: PaneProps) {
386
388
  if (signInWall)
387
389
  return (
388
390
  <SignInWall
391
+ placement="estimate-revisions-signin"
389
392
  action="view estimate revisions"
390
393
  needsVerification={session.needsVerification}
391
394
  />
@@ -230,6 +230,7 @@ function figuresOf(statement: CloudProxyStatementPayload) {
230
230
  }
231
231
 
232
232
  const EXEC_WALL: ProFeatureWallCopy = {
233
+ placement: "exec-wall",
233
234
  action: "see executive pay",
234
235
  title: "Executive pay is part of Gloom Cloud Pro.",
235
236
  message: "Named executive officers and how they were paid, read from each proxy statement and checked against the filing.",
@@ -238,6 +239,7 @@ const EXEC_WALL: ProFeatureWallCopy = {
238
239
  export function ExecutivesPane({
239
240
  focused,
240
241
  width,
242
+ height,
241
243
  nested = false,
242
244
  }: {
243
245
  focused: boolean;
@@ -246,7 +248,7 @@ export function ExecutivesPane({
246
248
  /** Inside Ticker Research, whose own tab strip keeps h/l and the arrows. */
247
249
  nested?: boolean;
248
250
  }) {
249
- const { wall, guard } = useProFeatureWall(EXEC_WALL);
251
+ const { wall, guard } = useProFeatureWall(EXEC_WALL, { width, height });
250
252
  const { symbol } = useBoundTicker();
251
253
  const ticker = symbol ? symbol.toUpperCase() : null;
252
254
  if (wall) return wall;
@@ -373,21 +375,21 @@ function ExecutiveResearch({ ticker, focused, width, nested, guard }: { ticker:
373
375
  >
374
376
  {figures.length > 0 && (
375
377
  <Box flexDirection="column">
376
- <SectionHeading title="KEY FIGURES" />
378
+ <SectionHeading title="Key Figures" />
377
379
  <FigureList figures={figures} width={proseWidth} minValueWidth={4} maxValueWidth={14} />
378
380
  </Box>
379
381
  )}
380
382
  {statement.ceo && (
381
383
  <Box flexDirection="column">
382
384
  <SectionHeading marginTop={figures.length > 0 ? 1 : 0}
383
- title={`HOW ${statement.ceo.name.split(" ").pop()?.toUpperCase() ?? "THE CEO"} WAS PAID`}
385
+ title={`How ${statement.ceo.name.trim().split(/\s+/).pop() || "the CEO"} Was Paid`}
384
386
  />
385
387
  <PayMixBar row={statement.ceo} width={proseWidth} />
386
388
  </Box>
387
389
  )}
388
390
  {statement.namedExecutives.length > 0 && (
389
391
  <Box flexDirection="column">
390
- <SectionHeading marginTop={1} title="NAMED EXECUTIVE OFFICERS" />
392
+ <SectionHeading marginTop={1} title="Named Executive Officers" />
391
393
  <ExecutiveRows
392
394
  rows={statement.namedExecutives}
393
395
  width={proseWidth}
@@ -396,7 +398,7 @@ function ExecutiveResearch({ ticker, focused, width, nested, guard }: { ticker:
396
398
  )}
397
399
  {statement.highlights && (
398
400
  <Box flexDirection="column">
399
- <SectionHeading marginTop={1} title="WHAT CHANGED" />
401
+ <SectionHeading marginTop={1} title="What Changed" />
400
402
  <BulletList items={statement.highlights.split("\n")} width={proseWidth} color={colors.text} />
401
403
  </Box>
402
404
  )}
@@ -0,0 +1,66 @@
1
+ import { ActionRow } from "../../../components";
2
+ import { Box, ChartSurface, Text, useUiCapabilities } from "../../../ui";
3
+ import { useThemeColors } from "../../../theme/theme-context";
4
+ import type { ChartVectorShape } from "../../../ui/host";
5
+ import type { ExposureComponent } from "../../../api-client/exposure";
6
+ import { basisLabel, rangeText, type ExposureRow } from "./model";
7
+ import { truncateToDisplayWidth } from "../../../utils/format";
8
+
9
+ /** A disclosure-bound interval, not a probability band. Same numerical domain on both renderers. */
10
+ export function ExposureRanges({ rows, selectedId, width, height, onSelect, onOpen, showTargets = false }: {
11
+ rows: ExposureRow[]; selectedId: string | null; width: number; height: number; onSelect: (id: string) => void; onOpen: (id: string) => void; showTargets?: boolean;
12
+ }) {
13
+ const colors = useThemeColors();
14
+ const desktop = !!useUiCapabilities().nativePaneChrome;
15
+ const selected = rows.find(r => r.id === selectedId) ?? rows[0];
16
+ const eligible = rows.filter(r => r.shockId === selected?.shockId && r.basis === selected?.basis && r.period === selected?.period && r.exposure);
17
+ const shown = eligible.slice(0, Math.max(1, height - 1));
18
+ const lower = Math.min(0, ...eligible.map(r => r.exposure!.low));
19
+ // A fifth of headroom past the largest bound, so no interval or point sits on the edge of its track.
20
+ const upper = Math.max(1, ...eligible.map(r => r.exposure!.high)) * 1.2;
21
+ const span = upper - lower;
22
+ const labelWidth = Math.max(10, Math.min(showTargets ? 42 : 22, Math.floor(width * .32)));
23
+ const plotWidth = Math.max(8, width - labelWidth - 24);
24
+ return <Box flexDirection="column" flexGrow={1} minHeight={0} data-gloom-role="exposure-ranges">
25
+ {shown.map(row => {
26
+ const low = (row.exposure!.low - lower) / span, high = (row.exposure!.high - lower) / span;
27
+ const color = row.classification === "disclosed" ? colors.borderFocused : colors.warning;
28
+ // A disclosed point is a tick; an estimated interval is a band between two ticks.
29
+ const vectors: ChartVectorShape[] = [
30
+ { id: "zero", points: [{ x: (0 - lower) / span, y: .2 }, { x: (0 - lower) / span, y: .8 }], color: colors.border, strokeWidth: 1 },
31
+ { id: "track", points: [{ x: 0, y: .5 }, { x: 1, y: .5 }], color: colors.border, strokeWidth: 1 },
32
+ ...(high - low > 0.004 ? [{ id: "range", points: [{ x: low, y: .5 }, { x: high, y: .5 }], color, strokeWidth: 5 }] : []),
33
+ { id: "low", points: [{ x: low, y: .15 }, { x: low, y: .85 }], color, strokeWidth: 3 },
34
+ { id: "high", points: [{ x: high, y: .15 }, { x: high, y: .85 }], color, strokeWidth: 3 },
35
+ ];
36
+ const start = Math.floor(low * (plotWidth - 1)), end = Math.floor(high * (plotWidth - 1));
37
+ return <Box key={row.id} flexDirection="row" height={1} minHeight={1} paddingX={1}>
38
+ <Box width={labelWidth}><ActionRow label={truncateToDisplayWidth(showTargets ? `${row.label} · ${row.symbol}` : row.symbol || row.label, labelWidth - 1)} active={selectedId === row.id} onPress={() => { onSelect(row.id); onOpen(row.id); }} /></Box>
39
+ {desktop ? <ChartSurface width={plotWidth} height={1} vectors={vectors} /> : <Text fg={color}>{" ".repeat(start)}{"─".repeat(Math.max(0, end - start))}│{" ".repeat(Math.max(0, plotWidth - end - 1))}</Text>}
40
+ <Box width={20} alignItems="flex-end"><Text fg={color}>{rangeText(row.exposure)}</Text></Box>
41
+ </Box>;
42
+ })}
43
+ </Box>;
44
+ }
45
+
46
+ export function ExposurePathDiagram({ component, width, height }: { component: ExposureComponent | null; width: number; height: number }) {
47
+ const colors = useThemeColors();
48
+ const desktop = !!useUiCapabilities().nativePaneChrome;
49
+ if (!component?.path.length) return <Box paddingX={1} flexGrow={1}><Text fg={colors.textDim}>{component ? `${component.label} · ${basisLabel(component.basis)}` : "Select an evidence path."}</Text></Box>;
50
+ const hops = component.path;
51
+ const nodes = [hops[0]!.from, ...hops.map(h => h.to)];
52
+ const nodeWidth = Math.max(8, Math.floor((width - 2) / nodes.length));
53
+ const vectors: ChartVectorShape[] = hops.map((hop, i) => ({ id: `${i}`, points: [{ x: (i + .5) / nodes.length, y: .5 }, { x: (i + 1.5) / nodes.length, y: .5 }], color: hop.pct === null ? colors.textDim : colors.warning, strokeWidth: 2, handles: true }));
54
+ return <Box flexDirection="column" height={height} overflow="hidden">
55
+ <Box paddingX={1} height={1} flexShrink={0}><Text fg={colors.textDim}>{hops.length} hop{hops.length === 1 ? "" : "s"} · {component.classification}{component.exposurePct ? ` · ${rangeText(component.exposurePct)}` : ""}</Text></Box>
56
+ <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((node, i) => <Box key={`${node.id}:${i}`} width={nodeWidth} alignItems="center"><Text fg={colors.textBright}>{truncateToDisplayWidth(node.symbol ?? node.name, nodeWidth - 2)}</Text></Box>)}</Box>
57
+ <Box height={1} flexShrink={0} paddingX={1}>
58
+ {desktop ? <ChartSurface width={width - 2} height={1} vectors={vectors} /> : <Text fg={colors.textDim}>{nodes.map((_, i) => i < hops.length ? "─".repeat(Math.max(1, nodeWidth - 2)) + "→ " : "").join("")}</Text>}
59
+ </Box>
60
+ {height >= 4 ? <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((_, i) => <Box key={i} width={nodeWidth} alignItems="center"><Text fg={colors.warning}>{i === 0 ? "" : truncateToDisplayWidth(hops[i - 1]!.pct === null ? "Undisclosed share" : `${hops[i - 1]!.pct}% ${basisLabel(hops[i - 1]!.basis)}`, nodeWidth - 1)}</Text></Box>)}</Box> : null}
61
+ {height >= 5 ? <Box flexDirection="row" height={1} flexShrink={0} paddingX={1}>{nodes.map((_, i) => {
62
+ const evidence = i > 0 ? hops[i - 1]!.evidence[0] : null;
63
+ return <Box key={i} width={nodeWidth} alignItems="center"><Text fg={colors.textDim}>{evidence ? truncateToDisplayWidth(`${evidence.tier} · ${evidence.asOf ?? "undated"}`, nodeWidth - 1) : ""}</Text></Box>;
64
+ })}</Box> : null}
65
+ </Box>;
66
+ }
@@ -0,0 +1,53 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import type { ExposurePayload, ExposureRequest, ExposureScenario } from "../../../api-client/exposure";
3
+ import { isRecord } from "../../../utils/guards";
4
+ import { validRange, parseCustomScenario } from "./model";
5
+
6
+ const finite = (v: unknown) => typeof v === "number" && Number.isFinite(v);
7
+ const nullableRange = (v: unknown) => v === null || validRange(v);
8
+ const strings = (v: unknown) => Array.isArray(v) && v.every(x => typeof x === "string");
9
+ function evidence(v: unknown): boolean {
10
+ if (!isRecord(v)) return false;
11
+ const safeUrl = v.url === null || (typeof v.url === "string" && /^https?:\/\//.test(v.url));
12
+ return typeof v.id === "string" && safeUrl && (v.quote === null || typeof v.quote === "string")
13
+ && ["structured", "primary", "secondary", "imported", "profile", "user"].includes(String(v.tier))
14
+ && (v.confidence === null || (finite(v.confidence) && Number(v.confidence) >= 0 && Number(v.confidence) <= 1));
15
+ }
16
+ /** Reject malformed percentages and untrusted source links before displaying any result. */
17
+ export function validateExposure(value: ExposurePayload): ExposurePayload {
18
+ const data = value as ExposurePayload;
19
+ let valid = !!data && data.version === 1 && ["full", "preview"].includes(data.access)
20
+ && Number.isInteger(data.depth) && data.depth >= 1 && data.depth <= 4
21
+ && typeof data.evaluatedAt === "string" && Number.isFinite(Date.parse(data.evaluatedAt))
22
+ && Number.isInteger(data.requestedHoldings) && Number.isInteger(data.lockedHoldings) && data.lockedHoldings >= 0
23
+ && Array.isArray(data.holdings) && strings(data.unknowns) && strings(data.methodology);
24
+ try { parseCustomScenario(JSON.stringify(data.scenario)); } catch { valid = false; }
25
+ valid &&= data.holdings.every(h => typeof h.symbol === "string" && finite(h.weight) && Array.isArray(h.unknowns)
26
+ && ["quantified", "partial", "unknown"].includes(h.status) && !!h.coverage
27
+ && Array.isArray(h.measures) && h.measures.every(m => validRange(m.exposurePct) && nullableRange(m.impactPct) && strings(m.componentIds))
28
+ && Array.isArray(h.extensions) && h.extensions.every(row => isRecord(row) && typeof row.id === "string" && typeof row.label === "string"
29
+ && ["kpi", "guidance", "credit"].includes(row.kind) && (row.value === null || finite(row.value)) && typeof row.units === "string"
30
+ && (row.range === undefined || isRecord(row.range) && (row.range.low === null || finite(row.range.low)) && (row.range.high === null || finite(row.range.high))
31
+ && (row.range.low === null || row.range.high === null || row.range.low <= row.range.high))
32
+ && [row.currency, row.period, row.asOf, row.basis, row.qualifier, row.status].every(v => v == null || typeof v === "string")
33
+ && (row.valueText === undefined || typeof row.valueText === "string")
34
+ && (!row.dimensions || isRecord(row.dimensions) && Object.values(row.dimensions).every(v => typeof v === "string"))
35
+ && (!row.notes || strings(row.notes)) && Array.isArray(row.evidence) && row.evidence.every(evidence))
36
+ && Array.isArray(h.components) && h.components.every(c => nullableRange(c.exposurePct) && nullableRange(c.impactPct)
37
+ && (c.proportionalEstimatePct == null || finite(c.proportionalEstimatePct))
38
+ && ["disclosed", "estimated", "unknown"].includes(c.classification) && Array.isArray(c.evidence) && c.evidence.every(evidence)
39
+ && Array.isArray(c.path) && c.path.every(hop => !!hop.from && !!hop.to && Array.isArray(hop.evidence) && hop.evidence.every(evidence)) && strings(c.unknowns)));
40
+ valid &&= !!data.portfolio && finite(data.portfolio.netWeight) && finite(data.portfolio.grossWeight) && finite(data.portfolio.residualWeight)
41
+ && Array.isArray(data.portfolio.measures) && data.portfolio.measures.every(m => validRange(m.signedExposurePct) && validRange(m.grossExposurePct) && nullableRange(m.impactPct))
42
+ && Array.isArray(data.portfolio.concentrations) && data.portfolio.concentrations.every(c => nullableRange(c.grossExposurePct) && nullableRange(c.signedExposurePct) && Array.isArray(c.evidence) && c.evidence.every(evidence));
43
+ if (!valid) throw new Error("The server returned unreadable exposure data.");
44
+ return data;
45
+ }
46
+ export async function fetchExposure(request: ExposureRequest, client: Pick<typeof apiClient, "analyzeCloudExposure"> = apiClient) {
47
+ return validateExposure(await client.analyzeCloudExposure(request));
48
+ }
49
+ export async function fetchScenarios(client: Pick<typeof apiClient, "getCloudExposureScenarios"> = apiClient): Promise<ExposureScenario[]> {
50
+ const data = await client.getCloudExposureScenarios();
51
+ if (!Array.isArray(data.scenarios)) throw new Error("Scenario library is unavailable.");
52
+ return data.scenarios.map(s => parseCustomScenario(JSON.stringify(s)));
53
+ }