gloomberb 0.16.6 → 0.17.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 (348) hide show
  1. package/package.json +2 -1
  2. package/src/api-client/chat-pane-link.ts +47 -0
  3. package/src/api-client/chat.ts +9 -1
  4. package/src/api-client/data.ts +37 -1
  5. package/src/api-client/equity-screener.ts +3 -0
  6. package/src/api-client/europe-energy.ts +91 -0
  7. package/src/api-client/index.ts +6 -0
  8. package/src/api-client/letters.ts +41 -0
  9. package/src/api-client/normalizers.ts +36 -6
  10. package/src/api-client/organization-tickers.ts +44 -0
  11. package/src/api-client/request.ts +3 -1
  12. package/src/api-client/types.ts +27 -1
  13. package/src/app/runtime/desktop-deeplink.ts +48 -15
  14. package/src/architecture/core-builtin-allowlist.json +1 -1
  15. package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
  16. package/src/ask-gloom/pane-context.ts +23 -19
  17. package/src/ask-gloom/pane-request.ts +2 -2
  18. package/src/cli/commands/fred-series.ts +77 -11
  19. package/src/cli/commands/market.ts +77 -20
  20. package/src/cli/commands/overview.ts +29 -20
  21. package/src/cli/commands/ticker.ts +64 -11
  22. package/src/cli/desktop-pane-shot-view.ts +114 -0
  23. package/src/cli/desktop-pane-shot.ts +30 -31
  24. package/src/cli/entry.ts +7 -0
  25. package/src/cli/export-source.ts +43 -0
  26. package/src/cli/freshness.ts +18 -3
  27. package/src/cli/glossary.ts +17 -0
  28. package/src/cli/history-rows.ts +174 -13
  29. package/src/cli/known-gaps.ts +14 -3
  30. package/src/cli/listing-arg.ts +20 -2
  31. package/src/cli/listing-suggestion.ts +159 -0
  32. package/src/cli/native-smoke.ts +13 -0
  33. package/src/cli/not-a-ticker.ts +13 -1
  34. package/src/cli/pane-functions/capabilities.ts +16 -8
  35. package/src/cli/pane-functions/dom.ts +28 -13
  36. package/src/cli/pane-functions/function-help.ts +51 -6
  37. package/src/cli/pane-functions/headless.ts +26 -7
  38. package/src/cli/pane-functions/index.ts +46 -3
  39. package/src/cli/pane-functions/report-notices.ts +6 -3
  40. package/src/cli/pane-functions/report.ts +16 -2
  41. package/src/cli/pane-functions/resolver.ts +7 -4
  42. package/src/cli/report-tables.ts +31 -7
  43. package/src/cli/result.ts +54 -11
  44. package/src/cli/symbol-rows.ts +18 -1
  45. package/src/components/chart/composite/axis-overlays.tsx +2 -0
  46. package/src/components/chart/composite/composite-chart.tsx +4 -21
  47. package/src/components/chart/composite/legend.tsx +10 -6
  48. package/src/components/chart/composite/price-axis-labels.tsx +1 -0
  49. package/src/components/command-bar/list/model.ts +2 -0
  50. package/src/components/command-bar/pane-templates/items.ts +1 -1
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
  52. package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
  53. package/src/components/command-bar/workflow/ops.ts +8 -97
  54. package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
  55. package/src/components/data-table/view.tsx +1 -0
  56. package/src/components/feed-data-table/stack-view.tsx +18 -42
  57. package/src/components/layout/missing-pane.tsx +2 -0
  58. package/src/components/layout/shell/index.tsx +1 -0
  59. package/src/components/layout/shell/pane/ask.ts +12 -7
  60. package/src/components/layout/shell/pane/capture.ts +8 -34
  61. package/src/components/markdown-text.tsx +37 -6
  62. package/src/components/reader/article-reader.tsx +91 -266
  63. package/src/components/reader/detail-reader.tsx +210 -0
  64. package/src/components/reader/reader-text.tsx +239 -0
  65. package/src/components/table-view-shared.tsx +8 -4
  66. package/src/components/ticker/badge/list.tsx +15 -4
  67. package/src/components/ui/choice-dialog.tsx +3 -0
  68. package/src/components/ui/data-table/types.ts +7 -0
  69. package/src/components/ui/detail-scroll-body.tsx +31 -4
  70. package/src/components/ui/display.tsx +46 -1
  71. package/src/components/ui/icon.tsx +11 -1
  72. package/src/components/ui/list-view.tsx +3 -1
  73. package/src/components/ui/query-bar-keys.tsx +2 -0
  74. package/src/components/ui/query-bar.tsx +9 -1
  75. package/src/components/ui/select-button.tsx +3 -0
  76. package/src/data/config/store/fold-panes.ts +202 -11
  77. package/src/data/config/store/migrations.ts +13 -1
  78. package/src/docs/snapshot.json +29 -0
  79. package/src/i18n/es.ts +14 -0
  80. package/src/i18n/ja.ts +14 -0
  81. package/src/i18n/ko.ts +14 -0
  82. package/src/i18n/zh-cn.ts +14 -0
  83. package/src/i18n/zh-tw.ts +14 -0
  84. package/src/layout/desk-keys.ts +7 -0
  85. package/src/layout/desks.ts +2 -2
  86. package/src/market-data/market/status.ts +67 -6
  87. package/src/market-data/quotes/contributions.ts +15 -0
  88. package/src/market-data/quotes/regular-session-valuation.ts +131 -0
  89. package/src/market-data/quotes/resolution.ts +4 -0
  90. package/src/market-data/snapshot-provider.ts +6 -3
  91. package/src/notifications/app-badge.ts +30 -0
  92. package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
  93. package/src/plugins/builtin/account-management/form-components.tsx +53 -1
  94. package/src/plugins/builtin/account-management/keyboard.ts +5 -0
  95. package/src/plugins/builtin/account-management/model.ts +27 -0
  96. package/src/plugins/builtin/account-management/pane.tsx +62 -2
  97. package/src/plugins/builtin/alerts/cli.ts +6 -4
  98. package/src/plugins/builtin/analytics/index.tsx +1 -1
  99. package/src/plugins/builtin/awards/pane.tsx +29 -18
  100. package/src/plugins/builtin/backtest/pane.tsx +2 -1
  101. package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
  102. package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
  103. package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
  104. package/src/plugins/builtin/catalysts/model.ts +92 -10
  105. package/src/plugins/builtin/catalysts/pane.tsx +6 -4
  106. package/src/plugins/builtin/cds/index.tsx +21 -30
  107. package/src/plugins/builtin/changelog/index.tsx +11 -12
  108. package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
  109. package/src/plugins/builtin/chart-composer/headless.ts +87 -5
  110. package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
  111. package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
  112. package/src/plugins/builtin/chat/command-bar.ts +6 -8
  113. package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
  114. package/src/plugins/builtin/chat/content/composer.tsx +13 -4
  115. package/src/plugins/builtin/chat/content/footer.tsx +15 -5
  116. package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
  117. package/src/plugins/builtin/chat/content/index.tsx +113 -9
  118. package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
  119. package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
  120. package/src/plugins/builtin/chat/content/scroll.ts +6 -1
  121. package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
  122. package/src/plugins/builtin/chat/ghost-user.ts +1 -1
  123. package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
  124. package/src/plugins/builtin/chat/index.tsx +44 -21
  125. package/src/plugins/builtin/chat/layout.ts +7 -14
  126. package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
  127. package/src/plugins/builtin/chat/message/header.tsx +69 -21
  128. package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
  129. package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
  130. package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
  131. package/src/plugins/builtin/chat/message/render-state.ts +15 -2
  132. package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
  133. package/src/plugins/builtin/chat/message/types.ts +3 -0
  134. package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
  135. package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
  136. package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
  137. package/src/plugins/builtin/chat/pane-state.ts +5 -8
  138. package/src/plugins/builtin/chat/people.ts +202 -0
  139. package/src/plugins/builtin/chat/profile-popover.ts +18 -2
  140. package/src/plugins/builtin/chat/status-widget.tsx +3 -8
  141. package/src/plugins/builtin/chat/unread-badge.ts +65 -0
  142. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
  143. package/src/plugins/builtin/clinical-trials/client.ts +26 -1
  144. package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
  145. package/src/plugins/builtin/clinical-trials/model.ts +269 -0
  146. package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
  147. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
  148. package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
  149. package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
  150. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
  151. package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
  152. package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
  153. package/src/plugins/builtin/commodity-balances/client.ts +208 -0
  154. package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
  155. package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
  156. package/src/plugins/builtin/commodity-balances/model.ts +261 -0
  157. package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
  158. package/src/plugins/builtin/composite-plugins.ts +26 -1
  159. package/src/plugins/builtin/credit-boards/index.tsx +21 -42
  160. package/src/plugins/builtin/credit-boards/model.ts +0 -2
  161. package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
  162. package/src/plugins/builtin/credit-documents/index.tsx +27 -10
  163. package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
  164. package/src/plugins/builtin/debug/index.tsx +2 -2
  165. package/src/plugins/builtin/dividend-yield/client.ts +3 -1
  166. package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
  167. package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
  168. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  169. package/src/plugins/builtin/docs/blocks.tsx +4 -3
  170. package/src/plugins/builtin/docs/pane.tsx +16 -8
  171. package/src/plugins/builtin/earnings/index.tsx +2 -0
  172. package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
  173. package/src/plugins/builtin/equity-screener/client.ts +1 -1
  174. package/src/plugins/builtin/equity-screener/headless.ts +36 -2
  175. package/src/plugins/builtin/equity-screener/model.ts +121 -68
  176. package/src/plugins/builtin/executives/pane.tsx +3 -1
  177. package/src/plugins/builtin/filing-events/index.tsx +20 -0
  178. package/src/plugins/builtin/fixed-income/index.tsx +46 -0
  179. package/src/plugins/builtin/fixed-income/model.ts +27 -0
  180. package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
  181. package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
  182. package/src/plugins/builtin/futures/index.tsx +24 -7
  183. package/src/plugins/builtin/futures/series.ts +23 -2
  184. package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
  185. package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
  186. package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
  187. package/src/plugins/builtin/insider/detail.tsx +20 -12
  188. package/src/plugins/builtin/insider/index.tsx +2 -2
  189. package/src/plugins/builtin/iv-history/index.tsx +3 -0
  190. package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
  191. package/src/plugins/builtin/jodi/client.ts +220 -0
  192. package/src/plugins/builtin/jodi/headless.ts +63 -0
  193. package/src/plugins/builtin/jodi/index.tsx +32 -0
  194. package/src/plugins/builtin/jodi/model.ts +75 -0
  195. package/src/plugins/builtin/jodi/pane.tsx +141 -0
  196. package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
  197. package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
  198. package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
  199. package/src/plugins/builtin/market-heatmap/live.ts +2 -1
  200. package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
  201. package/src/plugins/builtin/market-movers/table.ts +2 -2
  202. package/src/plugins/builtin/market-valuation/align.ts +10 -9
  203. package/src/plugins/builtin/mna/distress/model.ts +52 -1
  204. package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
  205. package/src/plugins/builtin/mna/index.tsx +2 -0
  206. package/src/plugins/builtin/news/wire/index.tsx +2 -2
  207. package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
  208. package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
  209. package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
  210. package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
  211. package/src/plugins/builtin/news/wire/mutes.ts +2 -1
  212. package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
  213. package/src/plugins/builtin/openfda/client.ts +123 -15
  214. package/src/plugins/builtin/openfda/model.ts +163 -0
  215. package/src/plugins/builtin/openfda/pane.tsx +109 -65
  216. package/src/plugins/builtin/openfda/types.ts +25 -0
  217. package/src/plugins/builtin/options/view.tsx +1 -1
  218. package/src/plugins/builtin/pe-band/model.ts +3 -2
  219. package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
  220. package/src/plugins/builtin/plugin-module.ts +3 -1
  221. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
  222. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
  223. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
  224. package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
  225. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
  226. package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
  227. package/src/plugins/builtin/power/evidence.ts +32 -3
  228. package/src/plugins/builtin/power/gas-client.ts +110 -0
  229. package/src/plugins/builtin/power/gas-model.ts +194 -0
  230. package/src/plugins/builtin/power/gas.tsx +145 -0
  231. package/src/plugins/builtin/power/headless.ts +4 -1
  232. package/src/plugins/builtin/power/index.tsx +19 -9
  233. package/src/plugins/builtin/power/pane.tsx +2 -3
  234. package/src/plugins/builtin/power/prices-headless.ts +75 -7
  235. package/src/plugins/builtin/power/prices.tsx +36 -13
  236. package/src/plugins/builtin/primary-dealers/client.ts +108 -0
  237. package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
  238. package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
  239. package/src/plugins/builtin/primary-dealers/model.ts +206 -0
  240. package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
  241. package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
  242. package/src/plugins/builtin/research/analyst-model.ts +9 -5
  243. package/src/plugins/builtin/research/headless-schema.ts +2 -1
  244. package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
  245. package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
  246. package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
  247. package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
  248. package/src/plugins/builtin/sec/filing-display.tsx +3 -36
  249. package/src/plugins/builtin/sec/index.tsx +131 -43
  250. package/src/plugins/builtin/sec/model.ts +148 -2
  251. package/src/plugins/builtin/sec/row-summaries.ts +105 -0
  252. package/src/plugins/builtin/sectors/index.tsx +2 -0
  253. package/src/plugins/builtin/sectors/sector-model.ts +2 -2
  254. package/src/plugins/builtin/series-inventory.ts +1 -0
  255. package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
  256. package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
  257. package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
  258. package/src/plugins/builtin/shared/zip-text.ts +46 -0
  259. package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
  260. package/src/plugins/builtin/themes/model.ts +2 -2
  261. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
  262. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
  263. package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
  264. package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
  265. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
  266. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
  267. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
  268. package/src/plugins/builtin/time-sales/headless.ts +22 -0
  269. package/src/plugins/builtin/trace-bonds/client.ts +196 -0
  270. package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
  271. package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
  272. package/src/plugins/builtin/trace-bonds/model.ts +115 -0
  273. package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
  274. package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
  275. package/src/plugins/builtin/treasury-daily/client.ts +96 -0
  276. package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
  277. package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
  278. package/src/plugins/builtin/treasury-daily/model.ts +69 -0
  279. package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
  280. package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
  281. package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
  282. package/src/plugins/builtin-editorial.ts +1 -1
  283. package/src/plugins/catalog-browser.ts +25 -2
  284. package/src/plugins/marketplace/model.ts +57 -21
  285. package/src/renderers/dom/build-assets.ts +14 -5
  286. package/src/renderers/dom/data-table/index.tsx +5 -3
  287. package/src/renderers/dom/desktop/controls.tsx +2 -1
  288. package/src/renderers/dom/desktop/icons.tsx +25 -0
  289. package/src/renderers/dom/desktop/list-view.tsx +1 -1
  290. package/src/renderers/dom/desktop/menu.tsx +1 -1
  291. package/src/renderers/dom/desktop/query-bar.tsx +3 -1
  292. package/src/renderers/dom/host/chart-surface.tsx +7 -2
  293. package/src/renderers/dom/host/resize-rows.ts +111 -0
  294. package/src/renderers/dom/host/sticky-scroll.ts +22 -3
  295. package/src/renderers/dom/host/style.ts +14 -0
  296. package/src/renderers/dom/host/surface-3d.tsx +2 -1
  297. package/src/renderers/dom/host/surface-fit.ts +10 -6
  298. package/src/renderers/dom/host/text.tsx +2 -2
  299. package/src/renderers/dom/styles.css +63 -6
  300. package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
  301. package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
  302. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  303. package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
  304. package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
  305. package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
  306. package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
  307. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  308. package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
  309. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
  310. package/src/renderers/electrobun/view/main.tsx +2 -0
  311. package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
  312. package/src/series/validate.ts +1 -1
  313. package/src/shares/api.ts +5 -1
  314. package/src/shares/location.ts +31 -0
  315. package/src/shares/open.ts +26 -0
  316. package/src/shares/pane-image.ts +18 -6
  317. package/src/shares/personal-pane.ts +42 -54
  318. package/src/sources/gloomberb-cloud/index.ts +7 -2
  319. package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
  320. package/src/sources/history-coverage.ts +22 -4
  321. package/src/sources/history-result.ts +3 -1
  322. package/src/sources/provider-errors.ts +2 -1
  323. package/src/sources/provider-router/batches.ts +18 -2
  324. package/src/sources/provider-router/index.ts +2 -1
  325. package/src/state/app/context/index.tsx +3 -2
  326. package/src/test-support/builtin-panes.ts +31 -0
  327. package/src/tickers/listing-venues.ts +22 -0
  328. package/src/tickers/search/index.ts +7 -2
  329. package/src/tickers/search/ranking.ts +19 -0
  330. package/src/time-series/chart-data.ts +2 -0
  331. package/src/time-series/parsed-history-cache.ts +41 -11
  332. package/src/time-series/resolve.ts +47 -15
  333. package/src/types/config.ts +1 -1
  334. package/src/types/data-provider.ts +13 -1
  335. package/src/types/financials.ts +13 -0
  336. package/src/types/headless.ts +22 -0
  337. package/src/types/plugin.ts +33 -6
  338. package/src/types/price-history.ts +2 -0
  339. package/src/ui/host.tsx +6 -0
  340. package/src/utils/crypto-pair.ts +1 -1
  341. package/src/utils/currency-pair.ts +66 -0
  342. package/src/utils/decimal-comma.ts +57 -0
  343. package/src/utils/guards.ts +25 -0
  344. package/src/utils/local-utc-offset.ts +37 -0
  345. package/src/utils/plugin-proxy-hosts.json +8 -1
  346. package/src/components/chart/composite/crosshair-readout.tsx +0 -150
  347. package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
  348. package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
@@ -1,27 +1,33 @@
1
- import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
1
+ import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
2
  import { useShortcut } from "../../react/input";
3
3
  import { scrollByLines } from "../../state/pane-scroll-registry";
4
4
  import { useThemeColors } from "../../theme/theme-context";
5
5
  import {
6
6
  Box,
7
7
  ScrollBox,
8
- StyledText,
9
8
  Text,
10
- TextAttributes,
11
9
  useNativeRenderer,
12
10
  useUiCapabilities,
13
11
  type ScrollBoxRenderable,
14
12
  } from "../../ui";
15
13
  import { t } from "../../i18n";
16
14
  import { isPlainKey } from "../../utils/keyboard";
17
- import { displayWidth, truncateToDisplayWidth } from "../../utils/format";
15
+ import { displayWidth } from "../../utils/format";
18
16
  import { wrapTextLines } from "../../utils/text-wrap";
19
17
  import { usePaneFooter, usePaneMenuItems, type PaneHint } from "../layout/pane/footer";
20
18
  import { TickerBadgeList } from "../ticker/badge/list";
21
19
  import { ActionRow } from "../ui/action-row";
22
20
  import { titleCase } from "../ui/header-case";
23
- import { splitFigures } from "../ui/prose";
24
21
  import { blockWords, markTerms, type ArticleBlock } from "./article-text";
22
+ import {
23
+ READER_APP_FONT,
24
+ READER_COLUMN_FONT,
25
+ DESKTOP_LINE_HEIGHT,
26
+ ReaderBlock,
27
+ ReaderText,
28
+ figureSegments,
29
+ type ReaderGeometry,
30
+ } from "./reader-text";
25
31
 
26
32
  /**
27
33
  * The longest line the reader sets, in characters. Past about 70 the eye
@@ -29,14 +35,6 @@ import { blockWords, markTerms, type ArticleBlock } from "./article-text";
29
35
  */
30
36
  const TERMINAL_MEASURE = 72;
31
37
  const DESKTOP_MEASURE_CH = 66;
32
- /**
33
- * The reading size on the desktop against the app's own (12px by default):
34
- * 17.4px, or 16.2px in a pane under 480px, where a phone sets its body.
35
- */
36
- const DESKTOP_FONT_EM = 1.45;
37
- const DESKTOP_NARROW_FONT_EM = 1.35;
38
- const DESKTOP_NARROW_CELLS = 60;
39
- const DESKTOP_LINE_HEIGHT = 1.65;
40
38
  /** Fewer hidden words than this are shown rather than tucked behind Read more. */
41
39
  const COLLAPSE_MIN_HIDDEN_WORDS = 80;
42
40
  const MAX_TICKERS = 6;
@@ -86,81 +84,84 @@ export function ArticleReader(props: ArticleReaderProps) {
86
84
  return <ArticleReaderView key={props.articleKey} {...props} />;
87
85
  }
88
86
 
89
- type Segment = { text: string; marked?: boolean; figure?: boolean };
90
-
91
- function figureSegments(text: string): Segment[] {
92
- return splitFigures(text).map((run) => ({ text: run.text, figure: run.figure }));
93
- }
94
-
95
- interface ReaderGeometry {
96
- desktop: boolean;
97
- /** Desktop: the reading size, in em of the app's size. */
98
- fontEm: number;
99
- /** Terminal: characters per line. Desktop: an estimate, for deciding what Read more hides. */
100
- measure: number;
101
- /** Terminal only: columns left of the text column, which is centred in the pane. */
102
- margin: number;
103
- }
87
+ /** The desktop sets its type and wraps in CSS; the browser lays the column out. */
88
+ const DESKTOP_GEOMETRY: ReaderGeometry = { desktop: true, measure: 0, margin: 0 };
104
89
 
105
- function readerGeometry(width: number, desktop: boolean): ReaderGeometry {
106
- if (desktop) {
107
- const fontEm = width < DESKTOP_NARROW_CELLS ? DESKTOP_NARROW_FONT_EM : DESKTOP_FONT_EM;
108
- // A monospace glyph is about 0.6em wide; the column is inset 24px a side.
109
- const columnPx = Math.max(160, width * 8 - 48);
110
- return { desktop, fontEm, measure: Math.max(24, Math.min(DESKTOP_MEASURE_CH, Math.floor(columnPx / (fontEm * 12 * 0.6)))), margin: 0 };
111
- }
90
+ function terminalGeometry(width: number): ReaderGeometry {
112
91
  // The scrollbar holds the last column.
113
92
  const usable = Math.max(12, Math.floor(width) - 1);
114
93
  const measure = Math.min(TERMINAL_MEASURE, Math.max(10, usable - 4));
115
- return { desktop, fontEm: 1, measure, margin: Math.max(2, Math.floor((usable - measure) / 2)) };
116
- }
117
-
118
- /** Rows of the app's 18px grid a desktop reading line takes, at the default size. */
119
- function rowsPerLine(geometry: ReaderGeometry): number {
120
- return geometry.desktop ? (geometry.fontEm * DESKTOP_LINE_HEIGHT * 12) / 18 : 1;
94
+ return { desktop: false, measure, margin: Math.max(2, Math.floor((usable - measure) / 2)) };
121
95
  }
122
96
 
123
- function textRows(text: string, measure: number, geometry: ReaderGeometry): number {
124
- return wrapTextLines(text, measure).length * rowsPerLine(geometry);
97
+ function textRows(text: string, measure: number): number {
98
+ return wrapTextLines(text, measure).length;
125
99
  }
126
100
 
127
- function blockRows(block: ArticleBlock, geometry: ReaderGeometry): number {
128
- const { measure } = geometry;
101
+ function blockRows(block: ArticleBlock, measure: number): number {
129
102
  switch (block.kind) {
130
103
  case "paragraph":
131
104
  case "heading":
132
- return textRows(block.text, measure, geometry) + 1;
105
+ return textRows(block.text, measure) + 1;
133
106
  case "list":
134
- return block.items.reduce((sum, item) => sum + textRows(item, measure - 2, geometry), 0) + 1;
107
+ return block.items.reduce((sum, item) => sum + textRows(item, measure - 2), 0) + 1;
135
108
  case "table":
136
- return block.rows.length * (geometry.desktop ? 1.4 : 1) + 1;
109
+ return block.rows.length + 1;
137
110
  }
138
111
  }
139
112
 
113
+ /** Null when what would be hidden from `cut` on is too short to be worth a click. */
114
+ function collapseWorthIt(body: readonly ArticleBlock[], cut: number | null): number | null {
115
+ if (cut === null) return null;
116
+ const hiddenWords = body.slice(cut).reduce((sum, block) => sum + blockWords(block), 0);
117
+ return hiddenWords >= COLLAPSE_MIN_HIDDEN_WORDS ? cut : null;
118
+ }
119
+
140
120
  /**
141
- * How many body blocks fit in about a screen under the header, lead and
142
- * facts, or null when the whole article should show: it fits, or what would
143
- * be hidden is too short to be worth a click.
121
+ * Terminal: how many body blocks fit in about a screen under the header, lead
122
+ * and facts, counted in rows, or null when the whole article should show.
144
123
  */
145
124
  function visibleBlockCount(
146
125
  body: readonly ArticleBlock[],
147
126
  aboveRows: number,
148
127
  height: number,
149
- geometry: ReaderGeometry,
128
+ measure: number,
150
129
  ): number | null {
151
130
  let used = aboveRows;
152
131
  let cut: number | null = null;
153
132
  for (const [index, block] of body.entries()) {
154
- used += blockRows(block, geometry);
133
+ used += blockRows(block, measure);
155
134
  // At least one block of the body shows, whatever the header took.
156
135
  if (used > height && index >= 1) {
157
136
  cut = index;
158
137
  break;
159
138
  }
160
139
  }
161
- if (cut === null) return null;
162
- const hiddenWords = body.slice(cut).reduce((sum, block) => sum + blockWords(block), 0);
163
- return hiddenWords >= COLLAPSE_MIN_HIDDEN_WORDS ? cut : null;
140
+ return collapseWorthIt(body, cut);
141
+ }
142
+
143
+ type DomNode = { getBoundingClientRect(): { bottom: number } };
144
+
145
+ /**
146
+ * Desktop: the first body block that ends below the reader's viewport, read
147
+ * from the laid-out page in pixels, or null when the whole article shows.
148
+ */
149
+ function measuredBlockCount(
150
+ body: readonly ArticleBlock[],
151
+ viewport: { y: number; height: number } | undefined,
152
+ blockNode: (index: number) => DomNode | null,
153
+ ): number | null {
154
+ if (!viewport || viewport.height <= 0) return null;
155
+ let cut: number | null = null;
156
+ for (let index = 1; index < body.length; index += 1) {
157
+ const node = blockNode(index);
158
+ if (!node) return null;
159
+ if (node.getBoundingClientRect().bottom > viewport.y + viewport.height) {
160
+ cut = index;
161
+ break;
162
+ }
163
+ }
164
+ return collapseWorthIt(body, cut);
164
165
  }
165
166
 
166
167
  /** A section label in the kit's casing: title case in English, a translation as it is written. */
@@ -174,21 +175,6 @@ function readingMinutes(blocks: readonly ArticleBlock[]): number {
174
175
  return Math.max(1, Math.round(blocks.reduce((sum, block) => sum + blockWords(block), 0) / 220));
175
176
  }
176
177
 
177
- /** Columns padded to their widest cell; figures right-aligned, the first column left. */
178
- function tableLines(rows: readonly string[][]): string[] {
179
- const columns = Math.max(...rows.map((row) => row.length));
180
- const widths = Array.from({ length: columns }, (_, column) => Math.max(...rows.map((row) => displayWidth(row[column] ?? ""))));
181
- return rows.map((row) => widths.map((width, column) => {
182
- const cell = row[column] ?? "";
183
- const pad = " ".repeat(Math.max(0, width - displayWidth(cell)));
184
- return column === 0 ? `${cell}${pad}` : `${pad}${cell}`;
185
- }).join(" ").trimEnd());
186
- }
187
-
188
- /** A first row without a figure in it is the table's header. */
189
- function hasHeaderRow(rows: readonly string[][]): boolean {
190
- return rows.length > 1 && rows[0]!.every((cell) => !/\d[\d,.]*\s*%?$/.test(cell) || /^(?:Q[1-4]|FY|H[12])\s?'?\d{2,4}$/i.test(cell));
191
- }
192
178
 
193
179
  function ArticleReaderView({
194
180
  title,
@@ -213,31 +199,50 @@ function ArticleReaderView({
213
199
  const nativeRenderer = useNativeRenderer();
214
200
  const scrollRef = useRef<ScrollBoxRenderable | null>(null);
215
201
  const idPrefix = `reader-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
216
- const geometry = useMemo(() => readerGeometry(width, desktop), [desktop, width]);
202
+ const geometry = useMemo(() => (desktop ? DESKTOP_GEOMETRY : terminalGeometry(width)), [desktop, width]);
217
203
  const shownTickers = useMemo(() => [...new Set(tickers)].slice(0, MAX_TICKERS), [tickers]);
218
204
  const metaText = meta.map((part) => part?.trim()).filter((part): part is string => !!part).join(" \u00b7 ");
219
205
 
206
+ // The terminal counts rows at its measure; the desktop measures the laid-out page below.
220
207
  const aboveRows = useMemo(() => {
221
- const headline = title ? textRows(title, geometry.measure, geometry) * (geometry.desktop ? 1.3 : 1) : 0;
222
- const leadRows = lead ? textRows(lead, geometry.measure, geometry) * (geometry.desktop ? 1.1 : 1) + 1 : 0;
208
+ if (desktop) return 0;
209
+ const headline = title ? textRows(title, geometry.measure) : 0;
210
+ const leadRows = lead ? textRows(lead, geometry.measure) + 1 : 0;
223
211
  const factRows = facts.length > 0
224
- ? facts.reduce((sum, fact) => sum + textRows(fact, geometry.measure - 2, geometry), 0) + 2
212
+ ? facts.reduce((sum, fact) => sum + textRows(fact, geometry.measure - 2), 0) + 2
225
213
  : 0;
226
214
  return 2 + headline + 1 + (shownTickers.length > 0 ? 1 : 0) + 1 + leadRows + factRows;
227
- }, [facts, geometry, lead, shownTickers.length, title]);
228
- const collapseAt = useMemo(
229
- () => visibleBlockCount(body, aboveRows, Math.max(12, height), geometry),
230
- [aboveRows, body, geometry, height],
215
+ }, [desktop, facts, geometry, lead, shownTickers.length, title]);
216
+ const terminalCollapseAt = useMemo(
217
+ () => (desktop ? null : visibleBlockCount(body, aboveRows, Math.max(12, height), geometry.measure)),
218
+ [aboveRows, body, desktop, geometry, height],
231
219
  );
220
+ // Undefined until the desktop page is laid out and measured, before it paints.
221
+ const [measuredCollapseAt, setMeasuredCollapseAt] = useState<number | null | undefined>(undefined);
222
+ const collapseAt = desktop ? measuredCollapseAt ?? null : terminalCollapseAt;
232
223
  const revealIndex = revealSection == null ? -1 : body.findIndex((block) => block.section === revealSection);
233
224
  // A search that matched further down lands on the match, so that part is shown from the start.
234
- const [expanded, setExpanded] = useState(() => collapseAt === null || (revealIndex > 0 && revealIndex >= collapseAt));
225
+ const [expanded, setExpanded] = useState(() => !desktop && (collapseAt === null || (revealIndex > 0 && revealIndex >= collapseAt)));
235
226
  const [raw, setRaw] = useState(false);
236
227
  const visibleBody = expanded || collapseAt === null ? body : body.slice(0, collapseAt);
237
228
  const hiddenCount = body.length - visibleBody.length;
238
229
 
239
230
  const blockId = useCallback((index: number) => `${idPrefix}-b${index}`, [idPrefix]);
240
231
 
232
+ // Desktop: lay the whole article out once, then keep about a screen of it
233
+ // by where its blocks actually end, before the first paint.
234
+ useLayoutEffect(() => {
235
+ if (!desktop || measuredCollapseAt !== undefined) return;
236
+ const documentRef = (globalThis as { document?: { getElementById(id: string): DomNode | null } }).document;
237
+ const cut = measuredBlockCount(
238
+ body,
239
+ scrollRef.current?.getBoundingClientRect?.(),
240
+ (index) => documentRef?.getElementById(blockId(index)) ?? null,
241
+ );
242
+ setMeasuredCollapseAt(cut);
243
+ if (cut === null || (revealIndex > 0 && revealIndex >= cut)) setExpanded(true);
244
+ }, [blockId, body, desktop, measuredCollapseAt, revealIndex]);
245
+
241
246
  /** Where block `index` starts, as an offset into the scrolled content, in the host's own units. */
242
247
  const blockTop = useCallback((index: number): { top: number; pixels: boolean } | null => {
243
248
  const scrollBox = scrollRef.current;
@@ -356,7 +361,7 @@ function ArticleReaderView({
356
361
  </Box>
357
362
  ) : null}
358
363
  {shownTickers.length > 0 ? (
359
- <Box style={desktop ? { marginTop: "0.6em", fontSize: `${1 / geometry.fontEm}em`, lineHeight: "normal" } : undefined}>
364
+ <Box style={desktop ? { marginTop: "0.6em", fontSize: READER_APP_FONT, lineHeight: "normal" } : undefined}>
360
365
  <TickerBadgeList symbols={shownTickers} width={desktop ? Math.max(20, width - 8) : geometry.measure} />
361
366
  </Box>
362
367
  ) : null}
@@ -388,7 +393,7 @@ function ArticleReaderView({
388
393
  </Box>
389
394
  ))}
390
395
  {!raw && hiddenCount > 0 ? (
391
- <Box style={desktop ? { fontSize: `${1 / geometry.fontEm}em`, lineHeight: "normal", marginTop: "0.4em" } : undefined}>
396
+ <Box style={desktop ? { fontSize: READER_APP_FONT, lineHeight: "normal", marginTop: "0.4em" } : undefined}>
392
397
  <ActionRow label="Read more" expanded={false} onPress={() => setExpanded(true)}>
393
398
  <Text fg={colors.textDim}>{`${readingMinutes(body.slice(visibleBody.length))} min`}</Text>
394
399
  </ActionRow>
@@ -397,7 +402,7 @@ function ArticleReaderView({
397
402
  {children ? (
398
403
  <Box flexDirection="column" style={desktop ? { marginTop: lead || blocks.length > 0 ? "0.8em" : 0 } : undefined}>
399
404
  {childrenLabel ? <ReaderText text={readerLabel(childrenLabel)} geometry={geometry} color={colors.textDim} variant="label" /> : null}
400
- <Box flexDirection="column" style={desktop ? { fontSize: `${1 / geometry.fontEm}em`, lineHeight: "normal" } : undefined}>
405
+ <Box flexDirection="column" style={desktop ? { fontSize: READER_APP_FONT, lineHeight: "normal" } : undefined}>
401
406
  {children}
402
407
  </Box>
403
408
  </Box>
@@ -412,10 +417,11 @@ function ArticleReaderView({
412
417
  <Box
413
418
  flexDirection="column"
414
419
  width="100%"
420
+ data-gloom-ui="reader-column"
415
421
  style={{
416
422
  // The measure plus the column's own inset on both sides.
417
423
  maxWidth: `calc(${DESKTOP_MEASURE_CH}ch + 48px)`,
418
- fontSize: `${geometry.fontEm}em`,
424
+ fontSize: READER_COLUMN_FONT,
419
425
  lineHeight: DESKTOP_LINE_HEIGHT,
420
426
  padding: "1.2em 24px 3em",
421
427
  boxSizing: "border-box",
@@ -441,7 +447,7 @@ function RawText({ text, geometry }: { text: string; geometry: ReaderGeometry })
441
447
  const colors = useThemeColors();
442
448
  if (geometry.desktop) {
443
449
  return (
444
- <Text fg={colors.text} wrapText width="100%" style={{ display: "block", whiteSpace: "pre-wrap", fontSize: "0.85em", lineHeight: 1.6 }}>
450
+ <Text fg={colors.text} wrapText selectable width="100%" style={{ display: "block", whiteSpace: "pre-wrap", fontSize: "0.85em", lineHeight: 1.6 }}>
445
451
  {text}
446
452
  </Text>
447
453
  );
@@ -459,184 +465,3 @@ function RawText({ text, geometry }: { text: string; geometry: ReaderGeometry })
459
465
  </Box>
460
466
  );
461
467
  }
462
-
463
- type TextVariant = "headline" | "meta" | "lead" | "body" | "heading" | "label" | "fact" | "item";
464
-
465
- const DESKTOP_VARIANT_STYLE: Record<TextVariant, Record<string, string | number>> = {
466
- headline: { fontSize: "1.45em", lineHeight: 1.22, letterSpacing: "-0.01em" },
467
- meta: { fontSize: "0.78em", lineHeight: 1.5 },
468
- lead: { fontSize: "1.1em", lineHeight: 1.55, marginBottom: "1.1em" },
469
- body: { lineHeight: DESKTOP_LINE_HEIGHT, marginBottom: "0.9em" },
470
- heading: { fontSize: "1.08em", lineHeight: 1.35, margin: "0.6em 0 0.35em" },
471
- label: { fontSize: "0.8em", lineHeight: 1.6, marginBottom: "0.3em" },
472
- fact: { fontSize: "0.95em", lineHeight: 1.55, marginBottom: "0.35em" },
473
- item: { lineHeight: 1.6, marginBottom: "0.3em" },
474
- };
475
-
476
- function styled(segments: readonly Segment[], color: string, markColor: string, figureColor: string | undefined, attributes: number): StyledText {
477
- return new StyledText(segments.map((segment) => ({
478
- text: segment.text,
479
- fg: segment.marked ? markColor : segment.figure && figureColor ? figureColor : color,
480
- // A search match is marked by colour alone; a fact's figures are also set bold.
481
- attributes: segment.figure && figureColor ? attributes | TextAttributes.BOLD : attributes,
482
- })));
483
- }
484
-
485
- /**
486
- * Text at the reader's measure. The terminal draws it pre-wrapped, a row per
487
- * line; the desktop lets the browser wrap it at the reading size of its variant.
488
- * Facts and list items hang under a bullet.
489
- */
490
- function ReaderText({
491
- text,
492
- geometry,
493
- color,
494
- bold = false,
495
- variant,
496
- segments,
497
- figureColor,
498
- }: {
499
- text: string;
500
- geometry: ReaderGeometry;
501
- color: string;
502
- bold?: boolean;
503
- variant: TextVariant;
504
- segments?: readonly Segment[];
505
- figureColor?: string;
506
- }) {
507
- const colors = useThemeColors();
508
- const attributes = bold || variant === "heading" ? TextAttributes.BOLD : TextAttributes.NONE;
509
- const bullet = variant === "fact" || variant === "item";
510
- if (geometry.desktop) {
511
- const content = segments
512
- ? styled(segments, color, colors.warning, figureColor, attributes)
513
- : undefined;
514
- const textNode = (
515
- <Text
516
- fg={color}
517
- attributes={attributes}
518
- wrapText
519
- width="100%"
520
- style={{ display: "block", ...DESKTOP_VARIANT_STYLE[variant], ...(bullet ? { marginBottom: 0 } : {}) }}
521
- content={content}
522
- >
523
- {content ? undefined : text}
524
- </Text>
525
- );
526
- if (!bullet) return textNode;
527
- return (
528
- <Box flexDirection="row" width="100%" style={{ marginBottom: DESKTOP_VARIANT_STYLE[variant].marginBottom }}>
529
- <Text fg={colors.textDim} style={{ fontSize: DESKTOP_VARIANT_STYLE[variant].fontSize, lineHeight: DESKTOP_VARIANT_STYLE[variant].lineHeight, width: "1.3em", flexShrink: 0 }}>{"\u2022"}</Text>
530
- <Box flexGrow={1} minWidth={0}>{textNode}</Box>
531
- </Box>
532
- );
533
- }
534
- const prefix = bullet ? "\u2022 " : "";
535
- const lines = wrapTextLines(text, Math.max(8, geometry.measure - prefix.length));
536
- return (
537
- <Box flexDirection="column">
538
- {lines.map((line, index) => (
539
- <Box key={index} height={1} flexDirection="row">
540
- {prefix ? <Text fg={colors.textDim}>{index === 0 ? prefix : " "}</Text> : null}
541
- {/* Always styled content: a terminal text node cannot switch from content to children in place. */}
542
- <Text content={styled(segments ? lineSegments(segments, line, lines, index, text) : [{ text: line }], color, colors.warning, figureColor, attributes)} />
543
- </Box>
544
- ))}
545
- </Box>
546
- );
547
- }
548
-
549
- /**
550
- * The segments that fall on one pre-wrapped line. Wrapping drops the spaces
551
- * it breaks at, so each line is found in the text by its position rather than
552
- * by counting characters.
553
- */
554
- function lineSegments(segments: readonly Segment[], line: string, lines: readonly string[], lineIndex: number, text: string): Segment[] {
555
- let offset = 0;
556
- for (let index = 0; index < lineIndex; index += 1) {
557
- offset = text.indexOf(lines[index]!, offset) + lines[index]!.length;
558
- }
559
- const start = text.indexOf(line, offset);
560
- const end = start + line.length;
561
- const result: Segment[] = [];
562
- let position = 0;
563
- for (const segment of segments) {
564
- const segmentEnd = position + segment.text.length;
565
- const from = Math.max(start, position);
566
- const to = Math.min(end, segmentEnd);
567
- if (to > from) result.push({ ...segment, text: text.slice(from, to) });
568
- position = segmentEnd;
569
- }
570
- return result.length > 0 ? result : [{ text: line }];
571
- }
572
-
573
- function ReaderBlock({
574
- block,
575
- geometry,
576
- segments,
577
- }: {
578
- block: ArticleBlock;
579
- geometry: ReaderGeometry;
580
- segments?: Segment[];
581
- }) {
582
- const colors = useThemeColors();
583
- switch (block.kind) {
584
- case "heading":
585
- return <ReaderText text={block.text} geometry={geometry} color={colors.textBright} variant="heading" />;
586
- case "paragraph":
587
- return <ReaderText text={block.text} geometry={geometry} color={colors.text} variant="body" segments={segments} />;
588
- case "list":
589
- return (
590
- <Box flexDirection="column" style={geometry.desktop ? { marginBottom: "0.9em" } : undefined}>
591
- {block.items.map((item, index) => (
592
- <ReaderText key={index} text={item} geometry={geometry} color={colors.text} variant="item" />
593
- ))}
594
- </Box>
595
- );
596
- case "table":
597
- return <ReaderTable rows={block.rows} geometry={geometry} />;
598
- }
599
- }
600
-
601
- /**
602
- * A table set in columns. The app's type is monospace on every host, so
603
- * padding the cells lines them up; a table wider than the column scrolls
604
- * sideways on the desktop and is cut at the edge in the terminal.
605
- */
606
- function ReaderTable({ rows, geometry }: { rows: readonly string[][]; geometry: ReaderGeometry }) {
607
- const colors = useThemeColors();
608
- const lines = tableLines(rows);
609
- const header = hasHeaderRow(rows);
610
- if (geometry.desktop) {
611
- return (
612
- // At the app's own size, as its tables are: a reading-size table runs out of a narrow pane.
613
- <Box flexDirection="column" width="100%" style={{ overflowX: "auto", margin: "0.2em 0 1.3em", fontSize: `${1 / geometry.fontEm}em`, lineHeight: 1.9 }}>
614
- {lines.map((line, index) => (
615
- <Text
616
- key={index}
617
- fg={index === 0 && header ? colors.textDim : colors.text}
618
- attributes={index === 0 && header ? TextAttributes.BOLD : TextAttributes.NONE}
619
- style={{ display: "block", lineHeight: "inherit", whiteSpace: "pre", borderBottom: index === 0 && header ? `1px solid ${colors.border}` : undefined }}
620
- >
621
- {line}
622
- </Text>
623
- ))}
624
- </Box>
625
- );
626
- }
627
- return (
628
- <Box flexDirection="column">
629
- {lines.map((line, index) => (
630
- <Box key={index} height={1}>
631
- <Text
632
- fg={index === 0 && header ? colors.textDim : colors.text}
633
- attributes={index === 0 && header ? TextAttributes.BOLD : TextAttributes.NONE}
634
- >
635
- {truncateToDisplayWidth(line, geometry.measure)}
636
- </Text>
637
- </Box>
638
- ))}
639
- </Box>
640
- );
641
- }
642
-