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.
- package/package.json +2 -1
- package/src/api-client/chat-pane-link.ts +47 -0
- package/src/api-client/chat.ts +9 -1
- package/src/api-client/data.ts +37 -1
- package/src/api-client/equity-screener.ts +3 -0
- package/src/api-client/europe-energy.ts +91 -0
- package/src/api-client/index.ts +6 -0
- package/src/api-client/letters.ts +41 -0
- package/src/api-client/normalizers.ts +36 -6
- package/src/api-client/organization-tickers.ts +44 -0
- package/src/api-client/request.ts +3 -1
- package/src/api-client/types.ts +27 -1
- package/src/app/runtime/desktop-deeplink.ts +48 -15
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
- package/src/ask-gloom/pane-context.ts +23 -19
- package/src/ask-gloom/pane-request.ts +2 -2
- package/src/cli/commands/fred-series.ts +77 -11
- package/src/cli/commands/market.ts +77 -20
- package/src/cli/commands/overview.ts +29 -20
- package/src/cli/commands/ticker.ts +64 -11
- package/src/cli/desktop-pane-shot-view.ts +114 -0
- package/src/cli/desktop-pane-shot.ts +30 -31
- package/src/cli/entry.ts +7 -0
- package/src/cli/export-source.ts +43 -0
- package/src/cli/freshness.ts +18 -3
- package/src/cli/glossary.ts +17 -0
- package/src/cli/history-rows.ts +174 -13
- package/src/cli/known-gaps.ts +14 -3
- package/src/cli/listing-arg.ts +20 -2
- package/src/cli/listing-suggestion.ts +159 -0
- package/src/cli/native-smoke.ts +13 -0
- package/src/cli/not-a-ticker.ts +13 -1
- package/src/cli/pane-functions/capabilities.ts +16 -8
- package/src/cli/pane-functions/dom.ts +28 -13
- package/src/cli/pane-functions/function-help.ts +51 -6
- package/src/cli/pane-functions/headless.ts +26 -7
- package/src/cli/pane-functions/index.ts +46 -3
- package/src/cli/pane-functions/report-notices.ts +6 -3
- package/src/cli/pane-functions/report.ts +16 -2
- package/src/cli/pane-functions/resolver.ts +7 -4
- package/src/cli/report-tables.ts +31 -7
- package/src/cli/result.ts +54 -11
- package/src/cli/symbol-rows.ts +18 -1
- package/src/components/chart/composite/axis-overlays.tsx +2 -0
- package/src/components/chart/composite/composite-chart.tsx +4 -21
- package/src/components/chart/composite/legend.tsx +10 -6
- package/src/components/chart/composite/price-axis-labels.tsx +1 -0
- package/src/components/command-bar/list/model.ts +2 -0
- package/src/components/command-bar/pane-templates/items.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
- package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
- package/src/components/command-bar/workflow/ops.ts +8 -97
- package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
- package/src/components/data-table/view.tsx +1 -0
- package/src/components/feed-data-table/stack-view.tsx +18 -42
- package/src/components/layout/missing-pane.tsx +2 -0
- package/src/components/layout/shell/index.tsx +1 -0
- package/src/components/layout/shell/pane/ask.ts +12 -7
- package/src/components/layout/shell/pane/capture.ts +8 -34
- package/src/components/markdown-text.tsx +37 -6
- package/src/components/reader/article-reader.tsx +91 -266
- package/src/components/reader/detail-reader.tsx +210 -0
- package/src/components/reader/reader-text.tsx +239 -0
- package/src/components/table-view-shared.tsx +8 -4
- package/src/components/ticker/badge/list.tsx +15 -4
- package/src/components/ui/choice-dialog.tsx +3 -0
- package/src/components/ui/data-table/types.ts +7 -0
- package/src/components/ui/detail-scroll-body.tsx +31 -4
- package/src/components/ui/display.tsx +46 -1
- package/src/components/ui/icon.tsx +11 -1
- package/src/components/ui/list-view.tsx +3 -1
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +9 -1
- package/src/components/ui/select-button.tsx +3 -0
- package/src/data/config/store/fold-panes.ts +202 -11
- package/src/data/config/store/migrations.ts +13 -1
- package/src/docs/snapshot.json +29 -0
- package/src/i18n/es.ts +14 -0
- package/src/i18n/ja.ts +14 -0
- package/src/i18n/ko.ts +14 -0
- package/src/i18n/zh-cn.ts +14 -0
- package/src/i18n/zh-tw.ts +14 -0
- package/src/layout/desk-keys.ts +7 -0
- package/src/layout/desks.ts +2 -2
- package/src/market-data/market/status.ts +67 -6
- package/src/market-data/quotes/contributions.ts +15 -0
- package/src/market-data/quotes/regular-session-valuation.ts +131 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/snapshot-provider.ts +6 -3
- package/src/notifications/app-badge.ts +30 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/form-components.tsx +53 -1
- package/src/plugins/builtin/account-management/keyboard.ts +5 -0
- package/src/plugins/builtin/account-management/model.ts +27 -0
- package/src/plugins/builtin/account-management/pane.tsx +62 -2
- package/src/plugins/builtin/alerts/cli.ts +6 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/awards/pane.tsx +29 -18
- package/src/plugins/builtin/backtest/pane.tsx +2 -1
- package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
- package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
- package/src/plugins/builtin/catalysts/model.ts +92 -10
- package/src/plugins/builtin/catalysts/pane.tsx +6 -4
- package/src/plugins/builtin/cds/index.tsx +21 -30
- package/src/plugins/builtin/changelog/index.tsx +11 -12
- package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
- package/src/plugins/builtin/chart-composer/headless.ts +87 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
- package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
- package/src/plugins/builtin/chat/command-bar.ts +6 -8
- package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
- package/src/plugins/builtin/chat/content/composer.tsx +13 -4
- package/src/plugins/builtin/chat/content/footer.tsx +15 -5
- package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
- package/src/plugins/builtin/chat/content/index.tsx +113 -9
- package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
- package/src/plugins/builtin/chat/content/scroll.ts +6 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
- package/src/plugins/builtin/chat/ghost-user.ts +1 -1
- package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
- package/src/plugins/builtin/chat/index.tsx +44 -21
- package/src/plugins/builtin/chat/layout.ts +7 -14
- package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
- package/src/plugins/builtin/chat/message/header.tsx +69 -21
- package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
- package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
- package/src/plugins/builtin/chat/message/render-state.ts +15 -2
- package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
- package/src/plugins/builtin/chat/message/types.ts +3 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
- package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
- package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
- package/src/plugins/builtin/chat/pane-state.ts +5 -8
- package/src/plugins/builtin/chat/people.ts +202 -0
- package/src/plugins/builtin/chat/profile-popover.ts +18 -2
- package/src/plugins/builtin/chat/status-widget.tsx +3 -8
- package/src/plugins/builtin/chat/unread-badge.ts +65 -0
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
- package/src/plugins/builtin/clinical-trials/client.ts +26 -1
- package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
- package/src/plugins/builtin/clinical-trials/model.ts +269 -0
- package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
- package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
- package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
- package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
- package/src/plugins/builtin/commodity-balances/client.ts +208 -0
- package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
- package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
- package/src/plugins/builtin/commodity-balances/model.ts +261 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
- package/src/plugins/builtin/composite-plugins.ts +26 -1
- package/src/plugins/builtin/credit-boards/index.tsx +21 -42
- package/src/plugins/builtin/credit-boards/model.ts +0 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
- package/src/plugins/builtin/credit-documents/index.tsx +27 -10
- package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
- package/src/plugins/builtin/debug/index.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/client.ts +3 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/docs/blocks.tsx +4 -3
- package/src/plugins/builtin/docs/pane.tsx +16 -8
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +36 -2
- package/src/plugins/builtin/equity-screener/model.ts +121 -68
- package/src/plugins/builtin/executives/pane.tsx +3 -1
- package/src/plugins/builtin/filing-events/index.tsx +20 -0
- package/src/plugins/builtin/fixed-income/index.tsx +46 -0
- package/src/plugins/builtin/fixed-income/model.ts +27 -0
- package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
- package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
- package/src/plugins/builtin/futures/index.tsx +24 -7
- package/src/plugins/builtin/futures/series.ts +23 -2
- package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
- package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
- package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
- package/src/plugins/builtin/insider/detail.tsx +20 -12
- package/src/plugins/builtin/insider/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/index.tsx +3 -0
- package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
- package/src/plugins/builtin/jodi/client.ts +220 -0
- package/src/plugins/builtin/jodi/headless.ts +63 -0
- package/src/plugins/builtin/jodi/index.tsx +32 -0
- package/src/plugins/builtin/jodi/model.ts +75 -0
- package/src/plugins/builtin/jodi/pane.tsx +141 -0
- package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
- package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
- package/src/plugins/builtin/market-heatmap/live.ts +2 -1
- package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/align.ts +10 -9
- package/src/plugins/builtin/mna/distress/model.ts +52 -1
- package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
- package/src/plugins/builtin/mna/index.tsx +2 -0
- package/src/plugins/builtin/news/wire/index.tsx +2 -2
- package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
- package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
- package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
- package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
- package/src/plugins/builtin/news/wire/mutes.ts +2 -1
- package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
- package/src/plugins/builtin/openfda/client.ts +123 -15
- package/src/plugins/builtin/openfda/model.ts +163 -0
- package/src/plugins/builtin/openfda/pane.tsx +109 -65
- package/src/plugins/builtin/openfda/types.ts +25 -0
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/pe-band/model.ts +3 -2
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/plugin-module.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
- package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
- package/src/plugins/builtin/power/evidence.ts +32 -3
- package/src/plugins/builtin/power/gas-client.ts +110 -0
- package/src/plugins/builtin/power/gas-model.ts +194 -0
- package/src/plugins/builtin/power/gas.tsx +145 -0
- package/src/plugins/builtin/power/headless.ts +4 -1
- package/src/plugins/builtin/power/index.tsx +19 -9
- package/src/plugins/builtin/power/pane.tsx +2 -3
- package/src/plugins/builtin/power/prices-headless.ts +75 -7
- package/src/plugins/builtin/power/prices.tsx +36 -13
- package/src/plugins/builtin/primary-dealers/client.ts +108 -0
- package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
- package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
- package/src/plugins/builtin/primary-dealers/model.ts +206 -0
- package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
- package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
- package/src/plugins/builtin/research/analyst-model.ts +9 -5
- package/src/plugins/builtin/research/headless-schema.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
- package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
- package/src/plugins/builtin/sec/filing-display.tsx +3 -36
- package/src/plugins/builtin/sec/index.tsx +131 -43
- package/src/plugins/builtin/sec/model.ts +148 -2
- package/src/plugins/builtin/sec/row-summaries.ts +105 -0
- package/src/plugins/builtin/sectors/index.tsx +2 -0
- package/src/plugins/builtin/sectors/sector-model.ts +2 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
- package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
- package/src/plugins/builtin/shared/zip-text.ts +46 -0
- package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
- package/src/plugins/builtin/themes/model.ts +2 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
- package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
- package/src/plugins/builtin/time-sales/headless.ts +22 -0
- package/src/plugins/builtin/trace-bonds/client.ts +196 -0
- package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
- package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
- package/src/plugins/builtin/trace-bonds/model.ts +115 -0
- package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
- package/src/plugins/builtin/treasury-daily/client.ts +96 -0
- package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
- package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
- package/src/plugins/builtin/treasury-daily/model.ts +69 -0
- package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
- package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
- package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/catalog-browser.ts +25 -2
- package/src/plugins/marketplace/model.ts +57 -21
- package/src/renderers/dom/build-assets.ts +14 -5
- package/src/renderers/dom/data-table/index.tsx +5 -3
- package/src/renderers/dom/desktop/controls.tsx +2 -1
- package/src/renderers/dom/desktop/icons.tsx +25 -0
- package/src/renderers/dom/desktop/list-view.tsx +1 -1
- package/src/renderers/dom/desktop/menu.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +3 -1
- package/src/renderers/dom/host/chart-surface.tsx +7 -2
- package/src/renderers/dom/host/resize-rows.ts +111 -0
- package/src/renderers/dom/host/sticky-scroll.ts +22 -3
- package/src/renderers/dom/host/style.ts +14 -0
- package/src/renderers/dom/host/surface-3d.tsx +2 -1
- package/src/renderers/dom/host/surface-fit.ts +10 -6
- package/src/renderers/dom/host/text.tsx +2 -2
- package/src/renderers/dom/styles.css +63 -6
- package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
- package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
- package/src/series/validate.ts +1 -1
- package/src/shares/api.ts +5 -1
- package/src/shares/location.ts +31 -0
- package/src/shares/open.ts +26 -0
- package/src/shares/pane-image.ts +18 -6
- package/src/shares/personal-pane.ts +42 -54
- package/src/sources/gloomberb-cloud/index.ts +7 -2
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
- package/src/sources/history-coverage.ts +22 -4
- package/src/sources/history-result.ts +3 -1
- package/src/sources/provider-errors.ts +2 -1
- package/src/sources/provider-router/batches.ts +18 -2
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/context/index.tsx +3 -2
- package/src/test-support/builtin-panes.ts +31 -0
- package/src/tickers/listing-venues.ts +22 -0
- package/src/tickers/search/index.ts +7 -2
- package/src/tickers/search/ranking.ts +19 -0
- package/src/time-series/chart-data.ts +2 -0
- package/src/time-series/parsed-history-cache.ts +41 -11
- package/src/time-series/resolve.ts +47 -15
- package/src/types/config.ts +1 -1
- package/src/types/data-provider.ts +13 -1
- package/src/types/financials.ts +13 -0
- package/src/types/headless.ts +22 -0
- package/src/types/plugin.ts +33 -6
- package/src/types/price-history.ts +2 -0
- package/src/ui/host.tsx +6 -0
- package/src/utils/crypto-pair.ts +1 -1
- package/src/utils/currency-pair.ts +66 -0
- package/src/utils/decimal-comma.ts +57 -0
- package/src/utils/guards.ts +25 -0
- package/src/utils/local-utc-offset.ts +37 -0
- package/src/utils/plugin-proxy-hosts.json +8 -1
- package/src/components/chart/composite/crosshair-readout.tsx +0 -150
- package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
124
|
-
return wrapTextLines(text, measure).length
|
|
97
|
+
function textRows(text: string, measure: number): number {
|
|
98
|
+
return wrapTextLines(text, measure).length;
|
|
125
99
|
}
|
|
126
100
|
|
|
127
|
-
function blockRows(block: ArticleBlock,
|
|
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
|
|
105
|
+
return textRows(block.text, measure) + 1;
|
|
133
106
|
case "list":
|
|
134
|
-
return block.items.reduce((sum, item) => sum + textRows(item, measure - 2
|
|
107
|
+
return block.items.reduce((sum, item) => sum + textRows(item, measure - 2), 0) + 1;
|
|
135
108
|
case "table":
|
|
136
|
-
return block.rows.length
|
|
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
|
-
*
|
|
142
|
-
* facts, or null when the whole article should show
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(() =>
|
|
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
|
-
|
|
222
|
-
const
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|