gloomberb 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContextMenuItem } from "../../../../types/context-menu";
|
|
2
|
-
import { t } from "../../../../i18n";
|
|
2
|
+
import { t, tf } from "../../../../i18n";
|
|
3
|
+
import { type CompanyFrom, companyFromLabel } from "../../../../types/company-from";
|
|
3
4
|
import { clipToDisplayWidth, displayWidth } from "../../../../utils/format";
|
|
4
5
|
import { formatShortDate } from "../../../../utils/datetime-format";
|
|
5
6
|
import { getShortcutHintWidth } from "../../../ui/shortcut-hint";
|
|
@@ -44,11 +45,19 @@ export interface PaneFooterSegment {
|
|
|
44
45
|
*/
|
|
45
46
|
quoteStatus?: boolean;
|
|
46
47
|
/**
|
|
47
|
-
* The segment says what the rows leave out (releases a filter hides),
|
|
48
|
-
*
|
|
48
|
+
* The segment says what the rows leave out (releases a filter hides), or
|
|
49
|
+
* whose they are (a receipt's company data from its home line), which a
|
|
50
|
+
* picture of the rows cannot: a `gloom shot` keeps it, as it keeps a
|
|
49
51
|
* warning.
|
|
50
52
|
*/
|
|
51
53
|
capture?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* When the copy the pane shows was saved, on the saved-copy segment
|
|
56
|
+
* ({@link savedCopyFooterSegment}). The footer draws that segment first,
|
|
57
|
+
* wherever a pane registers it, and once: the oldest when several parts of
|
|
58
|
+
* the pane show a saved copy.
|
|
59
|
+
*/
|
|
60
|
+
savedCopyAt?: number;
|
|
52
61
|
/**
|
|
53
62
|
* Shorter forms of `parts`, longest first. When the row is too narrow for
|
|
54
63
|
* `parts` beside the hints, it shows the first that fits, or the last, and
|
|
@@ -111,12 +120,43 @@ export function savedCopyFooterSegment(id: string, savedAt: number, now = Date.n
|
|
|
111
120
|
return {
|
|
112
121
|
id,
|
|
113
122
|
quoteStatus: true,
|
|
123
|
+
savedCopyAt: savedAt,
|
|
114
124
|
title: `${t("Couldn't refresh")}: ${text}`,
|
|
115
125
|
parts: [{ text, tone: "warning" }],
|
|
116
126
|
variants: [[{ text: t("saved copy"), tone: "warning" }]],
|
|
117
127
|
};
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The saved-copy segment of a pane showing several answers (a list and the
|
|
132
|
+
* report it opens): the oldest of the copies among `savedAts` (ms), or no
|
|
133
|
+
* segment when none of them is one.
|
|
134
|
+
*/
|
|
135
|
+
export function savedCopyFooterInfo(savedAts: readonly (number | null | undefined)[], id = "saved-copy"): PaneFooterSegment[] {
|
|
136
|
+
const times = savedAts.filter((at): at is number => typeof at === "number" && Number.isFinite(at));
|
|
137
|
+
return times.length > 0 ? [savedCopyFooterSegment(id, Math.min(...times))] : [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The footer's one form for company data a pane shows from another listing
|
|
142
|
+
* of the company: a receipt or a foreign line (SKHY, HY9H in Frankfurt) reads
|
|
143
|
+
* its home line's news, filings or holders, `company data from 000660 KRX`.
|
|
144
|
+
* Null when the data is the listing's own.
|
|
145
|
+
*/
|
|
146
|
+
export function companyDataFooterSegment(from: CompanyFrom | null | undefined): PaneFooterSegment | null {
|
|
147
|
+
if (!from) return null;
|
|
148
|
+
const listing = companyFromLabel(from);
|
|
149
|
+
const text = tf("company data from {listing}", { listing });
|
|
150
|
+
return {
|
|
151
|
+
id: "company-data",
|
|
152
|
+
title: from.name ? `${text} (${from.name})` : text,
|
|
153
|
+
// A capture of the rows keeps it: the picture cannot say whose they are.
|
|
154
|
+
capture: true,
|
|
155
|
+
parts: [{ text, tone: "muted" }],
|
|
156
|
+
variants: [[{ text: tf("from {listing}", { listing }), tone: "muted" }]],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
120
160
|
/** A pane whose market data is all delayed, by no one figure: the clock alone, its tooltip saying so. */
|
|
121
161
|
export const DELAYED_SEGMENT: PaneFooterSegment = delayedFooterSegment({ id: "delayed", detail: "Delayed market data" });
|
|
122
162
|
|
|
@@ -202,6 +242,25 @@ const INFO_FLOOR_CHARS = 10;
|
|
|
202
242
|
const WARNING_FLOOR_CHARS = 24;
|
|
203
243
|
const HINT_GAP = 1;
|
|
204
244
|
|
|
245
|
+
/**
|
|
246
|
+
* What the footer draws between two info segments, muted, as a status draws
|
|
247
|
+
* between its own parts ("Newest story Mon 14:23 · 12h delayed"): two
|
|
248
|
+
* segments side by side never read as one sentence ("company data from PBR ·
|
|
249
|
+
* as of 2026-06-30"). An icon (`⚠[!]`, `◷15m`) is a mark, not a phrase: a
|
|
250
|
+
* column sets it apart.
|
|
251
|
+
*/
|
|
252
|
+
export const PANE_FOOTER_SEPARATOR = " · ";
|
|
253
|
+
const SEPARATOR_WIDTH = displayWidth(PANE_FOOTER_SEPARATOR);
|
|
254
|
+
|
|
255
|
+
/** Whether the footer draws {@link PANE_FOOTER_SEPARATOR} between two adjacent segments, else a one-column gap. */
|
|
256
|
+
export function separatesPaneFooterSegments(before: Pick<PaneFooterSegment, "icon">, after: Pick<PaneFooterSegment, "icon">): boolean {
|
|
257
|
+
return !before.icon && !after.icon;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function segmentGapWidth(before: PaneFooterSegment, after: PaneFooterSegment): number {
|
|
261
|
+
return separatesPaneFooterSegments(before, after) ? SEPARATOR_WIDTH : 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
205
264
|
function paneHintWidth(hint: Pick<PaneHint, "key" | "label">, prefix = ""): number {
|
|
206
265
|
return getShortcutHintWidth(hint.key, hint.label, prefix);
|
|
207
266
|
}
|
|
@@ -216,15 +275,12 @@ function infoTextWidth(segments: readonly PaneFooterSegment[]): number {
|
|
|
216
275
|
const text = segment.parts.reduce((sum, part, partIndex) => (
|
|
217
276
|
sum + (partIndex > 0 ? 1 : 0) + displayWidth(part.text)
|
|
218
277
|
), 0);
|
|
219
|
-
return total + (index > 0 ? 1 : 0) + text;
|
|
278
|
+
return total + (index > 0 ? segmentGapWidth(segments[index - 1]!, segment) : 0) + text;
|
|
220
279
|
}, 0);
|
|
221
280
|
}
|
|
222
281
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
* together, one variant at a time, until the row fits or none is left.
|
|
226
|
-
*/
|
|
227
|
-
export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
|
|
282
|
+
/** Segments with `variants` step down together, one variant at a time, until they fit or none is left. */
|
|
283
|
+
function stepDownTogether(info: PaneFooterSegment[], width: number): PaneFooterSegment[] {
|
|
228
284
|
const stages = Math.max(0, ...info.map((segment) => segment.variants?.length ?? 0));
|
|
229
285
|
let fitted = info;
|
|
230
286
|
for (let stage = 0; stage < stages && infoTextWidth(fitted) > width; stage += 1) {
|
|
@@ -237,20 +293,50 @@ export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { be
|
|
|
237
293
|
} : segment;
|
|
238
294
|
});
|
|
239
295
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
296
|
+
return fitted;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* A trailing status cut mid-word reads as part of the hints beside it ("quote
|
|
301
|
+
* 12h t[x] put"): one that does not fit ends in an ellipsis a column short of
|
|
302
|
+
* them, or is left out when less than half of it would show, until the row
|
|
303
|
+
* fits or `keep` segments are left. A warning, an icon or a pressable segment
|
|
304
|
+
* stays as it is, clipped by the row if it must be.
|
|
305
|
+
*/
|
|
306
|
+
function trimTrailing(info: PaneFooterSegment[], width: number, keep: number): PaneFooterSegment[] {
|
|
307
|
+
let fitted = info;
|
|
308
|
+
while (fitted.length > keep && infoTextWidth(fitted) > width) {
|
|
245
309
|
const last = fitted.at(-1)!;
|
|
246
310
|
if (last.icon || last.onPress || last.parts.some((part) => part.tone === "warning" || part.tone === "negative")) break;
|
|
247
|
-
|
|
311
|
+
// After the separator, a column short of the hints.
|
|
312
|
+
const room = width - infoTextWidth(fitted.slice(0, -1)) - segmentGapWidth(fitted.at(-2)!, last) - 1;
|
|
248
313
|
if (room * 2 >= infoTextWidth([last])) return [...fitted.slice(0, -1), clipSegment(last, room)];
|
|
249
314
|
fitted = fitted.slice(0, -1);
|
|
250
315
|
}
|
|
251
316
|
return fitted;
|
|
252
317
|
}
|
|
253
318
|
|
|
319
|
+
/**
|
|
320
|
+
* The info segments as they fit `width`: segments with `variants` step down
|
|
321
|
+
* together, one variant at a time, until the row fits or none is left; beside
|
|
322
|
+
* the hints, the trailing status that still does not fit is then cut or left
|
|
323
|
+
* out (trimTrailing). A status after every segment with shorter forms gives
|
|
324
|
+
* way first, before any of them shortens: a pane puts what goes first last
|
|
325
|
+
* (an update time after a note that explains the row in hand).
|
|
326
|
+
*
|
|
327
|
+
* With no hints beside it (a pane picture's footer), the row clips the rest,
|
|
328
|
+
* so a frame that fits its own status line sees it overflow.
|
|
329
|
+
*/
|
|
330
|
+
export function fitPaneFooterInfo(info: PaneFooterSegment[], width: number, { besideHints = true }: { besideHints?: boolean } = {}): PaneFooterSegment[] {
|
|
331
|
+
if (!besideHints) return stepDownTogether(info, width);
|
|
332
|
+
const lastWithVariants = info.findLastIndex((segment) => (segment.variants?.length ?? 0) > 0);
|
|
333
|
+
if (lastWithVariants >= 0 && lastWithVariants < info.length - 1 && infoTextWidth(info) > width) {
|
|
334
|
+
const trimmed = trimTrailing(info, width, lastWithVariants + 1);
|
|
335
|
+
if (infoTextWidth(trimmed) <= width) return trimmed;
|
|
336
|
+
}
|
|
337
|
+
return trimTrailing(stepDownTogether(info, width), width, 1);
|
|
338
|
+
}
|
|
339
|
+
|
|
254
340
|
/** A segment cut to `width` behind an ellipsis, each part keeping its tone; the full text stays as its title. */
|
|
255
341
|
function clipSegment(segment: PaneFooterSegment, width: number): PaneFooterSegment {
|
|
256
342
|
const full = segment.parts.map((part) => part.text).join(" ");
|
|
@@ -382,7 +468,20 @@ export function combinePaneFooterRegistrations(registrations: Map<string, PaneFo
|
|
|
382
468
|
const shown = yielded.length > 0 ? hints.filter((hint) => !yielded.includes(hint)) : hints;
|
|
383
469
|
|
|
384
470
|
if (info.length === 0 && shown.length === 0 && menu.length === 0 && keys.length === 0) return EMPTY_FOOTER;
|
|
385
|
-
return { info, hints: shown, menu, keys };
|
|
471
|
+
return { info: savedCopyFirst(info), hints: shown, menu, keys };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The saved-copy segment in its one place: first, so it reads the same in
|
|
476
|
+
* every pane and is the last a narrow row cuts, and once, the oldest copy,
|
|
477
|
+
* when a list and the report it opens each register one.
|
|
478
|
+
*/
|
|
479
|
+
function savedCopyFirst(info: PaneFooterSegment[]): PaneFooterSegment[] {
|
|
480
|
+
let oldest: PaneFooterSegment | undefined;
|
|
481
|
+
for (const segment of info) {
|
|
482
|
+
if (segment.savedCopyAt != null && (oldest?.savedCopyAt == null || segment.savedCopyAt < oldest.savedCopyAt)) oldest = segment;
|
|
483
|
+
}
|
|
484
|
+
return oldest ? [oldest, ...info.filter((segment) => segment.savedCopyAt == null)] : info;
|
|
386
485
|
}
|
|
387
486
|
|
|
388
487
|
function sameFooterParts(left: PaneFooterPart[], right: PaneFooterPart[]): boolean {
|
|
@@ -439,6 +538,7 @@ export function samePaneFooterRegistration(
|
|
|
439
538
|
&& segment.shortcut === other.shortcut
|
|
440
539
|
&& !!segment.onPress === !!other.onPress
|
|
441
540
|
&& segment.disabled === other.disabled
|
|
541
|
+
&& segment.savedCopyAt === other.savedCopyAt
|
|
442
542
|
&& sameFooterParts(segment.parts, other.parts)
|
|
443
543
|
&& (segment.variants?.length ?? 0) === (other.variants?.length ?? 0)
|
|
444
544
|
&& (segment.variants ?? []).every((variant, variantIndex) => sameFooterParts(variant, other.variants![variantIndex]!));
|
|
@@ -178,13 +178,13 @@ export function PaneHeader({
|
|
|
178
178
|
event.stopPropagation?.();
|
|
179
179
|
onHeaderMouseDown?.(event);
|
|
180
180
|
};
|
|
181
|
-
const beginWindowDrag = (event?: { stopPropagation?: () => void }) => {
|
|
181
|
+
const beginWindowDrag = (event?: { stopPropagation?: () => void; detail?: number }) => {
|
|
182
182
|
event?.stopPropagation?.();
|
|
183
183
|
if (titleBar) {
|
|
184
184
|
onHeaderMouseDown?.(event);
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
|
-
void rendererHost.startWindowDrag?.();
|
|
187
|
+
void rendererHost.startWindowDrag?.(event);
|
|
188
188
|
};
|
|
189
189
|
const barHandlers = windowDrag
|
|
190
190
|
? { onMouseDown: beginWindowDrag }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { usePaneFooter, type PaneFooterSegment, type PaneHint } from "./footer";
|
|
1
|
+
import { useId, useMemo } from "react";
|
|
2
|
+
import { companyDataFooterSegment, usePaneFooter, type PaneFooterSegment, type PaneHint } from "./footer";
|
|
3
|
+
import type { CompanyFrom } from "../../../types/company-from";
|
|
4
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
3
5
|
import { useExternalLinkFooter } from "../../use-external-link-footer";
|
|
4
6
|
import { loadingErrorFooterInfo } from "../../data-table/table-pane";
|
|
5
7
|
|
|
@@ -9,7 +11,8 @@ const EMPTY_STATUS_INFO: PaneFooterSegment[] = [];
|
|
|
9
11
|
// has no per-pane footer hint. Do not reintroduce one. See PR #589. Panes bind
|
|
10
12
|
// it with `usePaneRefreshKey`.
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
/** The shared warning for data on screen older than it should be, for a pane composing its own footer. */
|
|
15
|
+
export const STALE_SEGMENT: PaneFooterSegment = { id: "stale", parts: [{ text: "stale", tone: "warning" }] };
|
|
13
16
|
|
|
14
17
|
interface PaneStatusInfoOptions {
|
|
15
18
|
loading?: boolean;
|
|
@@ -28,6 +31,37 @@ function buildPaneStatusInfo({
|
|
|
28
31
|
return [...info, ...(stale ? [STALE_SEGMENT] : []), ...loadingErrorFooterInfo(loading, error)];
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Where the company data note sits: after the warning indicator (-10) and
|
|
36
|
+
* before the pane's own status (0 and the ticker quote's -1), so it leads what
|
|
37
|
+
* the pane says about its data and a narrow footer cuts that status first.
|
|
38
|
+
*/
|
|
39
|
+
const COMPANY_DATA_ORDER = -5;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A company pane on a receipt or a foreign line that shows its company's data
|
|
43
|
+
* from the home line says which: `company data from 000660 KRX · as of
|
|
44
|
+
* 2026-06-30`, before the pane's own status (after a warning). Nothing when
|
|
45
|
+
* the data is the listing's own. Every company pane registers it here, so it
|
|
46
|
+
* reads, looks and sits the same in each.
|
|
47
|
+
*/
|
|
48
|
+
export function useCompanyDataFooter(companyFrom: CompanyFrom | null | undefined, enabled = true) {
|
|
49
|
+
const language = useAppLanguage();
|
|
50
|
+
const segment = useMemo(() => companyDataFooterSegment(companyFrom), [
|
|
51
|
+
language,
|
|
52
|
+
companyFrom?.symbol,
|
|
53
|
+
companyFrom?.exchange,
|
|
54
|
+
companyFrom?.name,
|
|
55
|
+
]);
|
|
56
|
+
// Its own registration, so a view nested in a pane (13D/G under holders) never clears the pane's.
|
|
57
|
+
const id = useId();
|
|
58
|
+
usePaneFooter(
|
|
59
|
+
`company-data:${id}`,
|
|
60
|
+
() => enabled && segment ? { order: COMPANY_DATA_ORDER, info: [segment] } : null,
|
|
61
|
+
[enabled, segment],
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
export function usePaneStatusFooter({
|
|
32
66
|
registrationId,
|
|
33
67
|
loading = false,
|
|
@@ -53,6 +53,15 @@ export function usePublishStatusWidgetRoom(columns: number): void {
|
|
|
53
53
|
}, []);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Extra columns the other widgets claim, for a widget that sizes itself to
|
|
58
|
+
* what they leave rather than run into them.
|
|
59
|
+
*/
|
|
60
|
+
export function useOtherStatusWidgetClaims(id: string): number {
|
|
61
|
+
const others = () => claimedColumns() - (claims.get(id) ?? 0);
|
|
62
|
+
return useSyncExternalStore(subscribe, others, others);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
/** The columns the widgets can use, as the status bar last published them. */
|
|
57
66
|
export function useStatusWidgetRoom(): number {
|
|
58
67
|
return useSyncExternalStore(subscribe, () => room, () => room);
|
|
@@ -219,7 +219,8 @@ export function OnboardingCoach({
|
|
|
219
219
|
</Box>
|
|
220
220
|
<Box height={1} />
|
|
221
221
|
<Box flexGrow={1} minHeight={0}>{children}</Box>
|
|
222
|
-
|
|
222
|
+
{/* As OnboardingActions: a button that does not fit beside the one before it wraps under it. */}
|
|
223
|
+
<Box flexDirection="row" flexWrap="wrap" alignItems="center" columnGap={1} flexShrink={0}>{actions}</Box>
|
|
223
224
|
</Box>
|
|
224
225
|
);
|
|
225
226
|
}
|
|
@@ -322,12 +322,15 @@ export function OnboardingWizard(props: OnboardingWizardProps) {
|
|
|
322
322
|
return <OnboardingCoach step={t("YOUR WORKSPACE")}
|
|
323
323
|
title={tf("Built around {ticker}", { ticker })}
|
|
324
324
|
actions={<>
|
|
325
|
-
{/*
|
|
326
|
-
|
|
325
|
+
{/* Each button shows its key, as Connect shows Alt+Enter; in the
|
|
326
|
+
terminal the second wraps under the first. The desktop card has no
|
|
327
|
+
room for both: like a toast, its dismiss key is in the tooltip.
|
|
328
|
+
F10 also dismisses it. */}
|
|
327
329
|
<OnboardingButton
|
|
328
330
|
label="Keep exploring"
|
|
329
331
|
variant="ghost"
|
|
330
|
-
|
|
332
|
+
shortcut={!desktop && notificationDismissShortcut ? notificationDismissShortcut : undefined}
|
|
333
|
+
title={desktop && notificationDismissShortcut ? `${t("Keep exploring")} (${notificationDismissShortcut})` : undefined}
|
|
331
334
|
onPress={() => { void finish(); }}
|
|
332
335
|
/>
|
|
333
336
|
<OnboardingButton
|
|
@@ -29,8 +29,6 @@ import type { QuoteFlashDirection } from "../quote-flash";
|
|
|
29
29
|
import { objectVersion } from "../../utils/object-version";
|
|
30
30
|
import { getCompletedRegularSessionDisplay } from "../../market-data/market/status";
|
|
31
31
|
import { usePaneFooter } from "../layout/pane/footer";
|
|
32
|
-
import { Button } from "../ui/button";
|
|
33
|
-
import { PaneStatusBody } from "../ui/status";
|
|
34
32
|
|
|
35
33
|
export interface TickerTableCell {
|
|
36
34
|
text: string;
|
|
@@ -99,11 +97,10 @@ export interface TickerListTableViewProps {
|
|
|
99
97
|
emptyTitle?: string;
|
|
100
98
|
emptyHint?: string;
|
|
101
99
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
100
|
+
* The pane's own key for adding a ticker, which the empty table names. The
|
|
101
|
+
* command bar's key when absent. The pane shows its add control once, in its
|
|
102
|
+
* own row, so the empty table points at the key and offers no second one.
|
|
104
103
|
*/
|
|
105
|
-
onAddTicker?: () => void;
|
|
106
|
-
/** The pane's key for the same flow, named after the click. The command bar's key when absent. */
|
|
107
104
|
addTickerShortcut?: string;
|
|
108
105
|
virtualize?: boolean;
|
|
109
106
|
overscan?: number;
|
|
@@ -237,24 +234,12 @@ export function TickerListTableView({
|
|
|
237
234
|
rowMenuAccelerators,
|
|
238
235
|
emptyTitle = t("No tickers."),
|
|
239
236
|
emptyHint,
|
|
240
|
-
onAddTicker,
|
|
241
237
|
addTickerShortcut,
|
|
242
238
|
virtualize = true,
|
|
243
239
|
overscan = 4,
|
|
244
240
|
}: TickerListTableViewProps) {
|
|
245
241
|
const commandBarShortcut = useCommandBarShortcut();
|
|
246
|
-
const
|
|
247
|
-
const resolvedEmptyHint = emptyHint ?? (onAddTicker
|
|
248
|
-
? tf("Click {button} to add one, or press {shortcut}.", { button: addLabel, shortcut: addTickerShortcut ?? commandBarShortcut })
|
|
249
|
-
: tf("Press {shortcut} to add one.", { shortcut: commandBarShortcut }));
|
|
250
|
-
const emptyContent = onAddTicker ? (
|
|
251
|
-
<PaneStatusBody
|
|
252
|
-
empty
|
|
253
|
-
emptyTitle={emptyTitle}
|
|
254
|
-
emptyMessage={resolvedEmptyHint}
|
|
255
|
-
actions={<Button label="Add ticker" displayLabel={addLabel} compact onPress={onAddTicker} />}
|
|
256
|
-
/>
|
|
257
|
-
) : undefined;
|
|
242
|
+
const resolvedEmptyHint = emptyHint ?? tf("Press {shortcut} to add one.", { shortcut: addTickerShortcut ?? commandBarShortcut });
|
|
258
243
|
const renderer = useRendererHost();
|
|
259
244
|
const { showContextMenu } = useContextMenu();
|
|
260
245
|
const { nativeContextMenu } = useUiCapabilities();
|
|
@@ -398,7 +383,6 @@ export function TickerListTableView({
|
|
|
398
383
|
getRowVersion={getRowVersion}
|
|
399
384
|
emptyStateTitle={emptyTitle}
|
|
400
385
|
emptyStateHint={resolvedEmptyHint}
|
|
401
|
-
emptyContent={emptyContent}
|
|
402
386
|
virtualize={virtualize}
|
|
403
387
|
overscan={overscan}
|
|
404
388
|
rootBefore={rootBefore}
|
|
@@ -7,6 +7,7 @@ import { Box, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
|
7
7
|
import { truncateToDisplayWidth } from "../../utils/format";
|
|
8
8
|
import { CountryFlagLead } from "./country-flag";
|
|
9
9
|
import { usePaneLinkMenuEntry } from "./external-link";
|
|
10
|
+
import { ICON_GLYPHS, Icon, type IconName } from "./icon";
|
|
10
11
|
import { titleCase } from "./header-case";
|
|
11
12
|
import { statToneColor, type StatTone } from "./stat-grid";
|
|
12
13
|
|
|
@@ -199,6 +200,10 @@ export interface BadgeProps {
|
|
|
199
200
|
/** For a domain scale, such as a sentiment score or chart series. */
|
|
200
201
|
color?: string;
|
|
201
202
|
variant?: "subtle" | "solid";
|
|
203
|
+
/** A mark before the label, in its colour: an SVG on the desktop, its glyph in the terminal. */
|
|
204
|
+
icon?: IconName;
|
|
205
|
+
/** False for a label that is not English to translate, such as a username. */
|
|
206
|
+
translate?: boolean;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
/**
|
|
@@ -209,9 +214,20 @@ export interface BadgeProps {
|
|
|
209
214
|
export const NATIVE_BADGE_STYLE = { height: "calc(100% - 2px)", borderRadius: "var(--gloom-radius-sm)" };
|
|
210
215
|
/** How far a subtle chip's fill is tinted toward its tone. */
|
|
211
216
|
export const BADGE_TINT = 0.28;
|
|
217
|
+
const NATIVE_ICON_BADGE_STYLE = { ...NATIVE_BADGE_STYLE, gap: "var(--gloom-space-xs)" };
|
|
212
218
|
const NATIVE_BADGE_TEXT_STYLE = { fontWeight: "var(--gloom-weight-regular)" };
|
|
219
|
+
/**
|
|
220
|
+
* A label with lowercase letters has them in the middle of the chip,
|
|
221
|
+
* wherever the font puts them in its line: the line is cut to x-height and
|
|
222
|
+
* baseline, and that is what the chip centres. Centred whole, SF Mono's
|
|
223
|
+
* lowercase sit a pixel and a half low. An icon centres in the chip, so it
|
|
224
|
+
* reads on the letters' line. A label in capitals and digits keeps the
|
|
225
|
+
* whole line, as ticker chips do; a browser without `text-box` keeps it too.
|
|
226
|
+
*/
|
|
227
|
+
const NATIVE_LOWERCASE_BADGE_TEXT_STYLE = { ...NATIVE_BADGE_TEXT_STYLE, textBox: "trim-both ex alphabetic" };
|
|
228
|
+
const LOWERCASE = /\p{Ll}/u;
|
|
213
229
|
|
|
214
|
-
export function Badge({ label, tone = "neutral", color, variant = "subtle" }: BadgeProps) {
|
|
230
|
+
export function Badge({ label, tone = "neutral", color, variant = "subtle", icon, translate = true }: BadgeProps) {
|
|
215
231
|
const colors = useThemeColors();
|
|
216
232
|
const { nativePaneChrome } = useUiCapabilities();
|
|
217
233
|
const accent = color ?? (tone === "neutral" ? colors.textDim : tone === "accent" ? colors.borderFocused : colors[tone]);
|
|
@@ -220,10 +236,20 @@ export function Badge({ label, tone = "neutral", color, variant = "subtle" }: Ba
|
|
|
220
236
|
const background = neutral ? colors.selected : solid ? accent : blendHex(colors.bg, accent, BADGE_TINT);
|
|
221
237
|
const baseForeground = neutral ? colors.selectedText : solid ? colors.bg : accent;
|
|
222
238
|
const foreground = blendForContrast(baseForeground, background, higherContrast("#000000", "#ffffff", background), 4.5);
|
|
239
|
+
const text = translate ? t(label) : label;
|
|
240
|
+
const nativeIcon = nativePaneChrome && icon;
|
|
241
|
+
// The terminal draws the icon's glyph as text in front of the label.
|
|
242
|
+
const shown = icon && !nativePaneChrome ? [ICON_GLYPHS[icon], text].filter(Boolean).join(" ") : text;
|
|
223
243
|
const chip = (
|
|
224
244
|
<Box height={nativePaneChrome ? undefined : 1} paddingX={1} backgroundColor={background}
|
|
225
|
-
data-gloom-ui="badge"
|
|
226
|
-
|
|
245
|
+
data-gloom-ui="badge" {...(nativeIcon ? { flexDirection: "row", alignItems: "center" } as const : {})}
|
|
246
|
+
justifyContent={nativePaneChrome && !nativeIcon ? "center" : undefined}
|
|
247
|
+
style={nativePaneChrome ? nativeIcon ? NATIVE_ICON_BADGE_STYLE : NATIVE_BADGE_STYLE : undefined}>
|
|
248
|
+
{nativeIcon ? <Icon name={nativeIcon} color={foreground} fullHeight /> : null}
|
|
249
|
+
{shown || !nativeIcon ? (
|
|
250
|
+
<Text fg={foreground} attributes={TextAttributes.BOLD}
|
|
251
|
+
style={nativePaneChrome ? LOWERCASE.test(text) ? NATIVE_LOWERCASE_BADGE_TEXT_STYLE : NATIVE_BADGE_TEXT_STYLE : undefined}>{shown}</Text>
|
|
252
|
+
) : null}
|
|
227
253
|
</Box>
|
|
228
254
|
);
|
|
229
255
|
return nativePaneChrome ? <Box height={1} flexShrink={0} justifyContent="center">{chip}</Box> : chip;
|
|
@@ -38,7 +38,8 @@ export type IconName =
|
|
|
38
38
|
| "team"
|
|
39
39
|
| "pro"
|
|
40
40
|
| "clock"
|
|
41
|
-
| "edit"
|
|
41
|
+
| "edit"
|
|
42
|
+
| "mail";
|
|
42
43
|
|
|
43
44
|
/** Terminal glyph for each icon; the desktop host draws SVG. */
|
|
44
45
|
export const ICON_GLYPHS: Record<IconName, string> = {
|
|
@@ -84,6 +85,8 @@ export const ICON_GLYPHS: Record<IconName, string> = {
|
|
|
84
85
|
// Time, for data that arrives late (a delayed quote).
|
|
85
86
|
clock: "◷",
|
|
86
87
|
edit: "✎",
|
|
88
|
+
// A message written to you (a DM): text presentation, one cell.
|
|
89
|
+
mail: "✉",
|
|
87
90
|
};
|
|
88
91
|
|
|
89
92
|
export interface IconProps {
|
|
@@ -91,13 +94,19 @@ export interface IconProps {
|
|
|
91
94
|
/** Pixel size on the desktop; the terminal draws one glyph. */
|
|
92
95
|
size?: number;
|
|
93
96
|
color?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Desktop: takes its row's full height and draws in the middle of it. A
|
|
99
|
+
* box centred in a row an odd number of pixels taller lands on a whole
|
|
100
|
+
* pixel, half a pixel off; the drawing inside the box does not.
|
|
101
|
+
*/
|
|
102
|
+
fullHeight?: boolean;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
/** One glyph from the app's icon set. */
|
|
97
|
-
export function Icon({ name, size = 12, color }: IconProps) {
|
|
106
|
+
export function Icon({ name, size = 12, color, fullHeight }: IconProps) {
|
|
98
107
|
const colors = useThemeColors();
|
|
99
108
|
const HostIcon = useUiHost().Icon as ComponentType<IconProps> | undefined;
|
|
100
|
-
if (HostIcon) return <HostIcon name={name} size={size} color={color} />;
|
|
109
|
+
if (HostIcon) return <HostIcon name={name} size={size} color={color} fullHeight={fullHeight} />;
|
|
101
110
|
return <Text fg={color ?? colors.textDim} selectable={false}>{ICON_GLYPHS[name]}</Text>;
|
|
102
111
|
}
|
|
103
112
|
|
package/src/core/app-services.ts
CHANGED
|
@@ -10,8 +10,17 @@ import { createAppRuntime } from "./app-runtime";
|
|
|
10
10
|
|
|
11
11
|
export type AppServices = ReturnType<typeof createAppServices>;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
interface AppServicesOptions extends AppServicesFactoryOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Runs where the user waits on it (the terminal, the desktop app), so no
|
|
16
|
+
* database statement waits on another program's lock (data/sqlite/access.ts).
|
|
17
|
+
* A command sets it to false and waits instead.
|
|
18
|
+
*/
|
|
19
|
+
interactive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createAppServices({ config, plugins, externalPlugins, interactive = true }: AppServicesOptions) {
|
|
23
|
+
const persistence = new AppPersistence(join(config.dataDir, ".gloomberb-cache.db"), { interactive });
|
|
15
24
|
const tickerRepository = new TickerRepository(persistence.tickers);
|
|
16
25
|
const providerRouter = new AssetDataRouter(null, [], persistence.resources, connectionHealth);
|
|
17
26
|
const runtime = createAppRuntime({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { PluginStateStore } from "./plugin-state-store";
|
|
2
2
|
import { ResourceStore } from "./resource-store";
|
|
3
3
|
import { SessionStore } from "./session-store";
|
|
4
|
-
import { SqliteDatabase } from "./sqlite/database";
|
|
4
|
+
import { SqliteDatabase, type SqliteDatabaseOptions } from "./sqlite/database";
|
|
5
5
|
import { TickerStore } from "./ticker-store";
|
|
6
6
|
|
|
7
7
|
export class AppPersistence {
|
|
@@ -11,15 +11,17 @@ export class AppPersistence {
|
|
|
11
11
|
readonly pluginState: PluginStateStore;
|
|
12
12
|
readonly sessions: SessionStore;
|
|
13
13
|
|
|
14
|
-
constructor(dbPath: string) {
|
|
15
|
-
this.database = new SqliteDatabase(dbPath);
|
|
16
|
-
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
19
|
-
this.
|
|
14
|
+
constructor(dbPath: string, options: SqliteDatabaseOptions = {}) {
|
|
15
|
+
this.database = new SqliteDatabase(dbPath, options);
|
|
16
|
+
const { connection, access } = this.database;
|
|
17
|
+
this.tickers = new TickerStore(connection, access);
|
|
18
|
+
this.resources = new ResourceStore(connection, access);
|
|
19
|
+
this.pluginState = new PluginStateStore(connection, access);
|
|
20
|
+
this.sessions = new SessionStore(connection, access);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
close(): void {
|
|
24
|
+
this.resources.close();
|
|
23
25
|
this.database.close();
|
|
24
26
|
}
|
|
25
27
|
}
|
package/src/data/plugin-cache.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { logFailure } from "../utils/log-failure";
|
|
|
2
2
|
import type { CachePolicy } from "../types/persistence";
|
|
3
3
|
import type { PluginPersistence } from "../types/plugin";
|
|
4
4
|
import { CachedQuery, type CachedValue } from "./cached-query";
|
|
5
|
+
import { isTransientFailure } from "../api-client/errors";
|
|
5
6
|
|
|
6
7
|
export interface PluginCacheResult<T> {
|
|
7
8
|
data: T;
|
|
@@ -12,6 +13,20 @@ export interface PluginCacheResult<T> {
|
|
|
12
13
|
source: "cache" | "network" | "stale-fallback";
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
/**
|
|
17
|
+
* `result`, or the failure of the refresh it stands in for when a saved copy
|
|
18
|
+
* may not answer for it. A saved copy covers only a failure asking again may
|
|
19
|
+
* get past (`isTransientFailure`: no connection, a timeout, a rate-limited or
|
|
20
|
+
* overloaded server, a reply whose source did not answer this time); a
|
|
21
|
+
* refusal (another 4xx, such as a series no longer served or a signed-out
|
|
22
|
+
* session) and an answer that failed validation are thrown, so they show as
|
|
23
|
+
* the errors they are instead of as old data.
|
|
24
|
+
*/
|
|
25
|
+
export function savedCopyForTransientFailure<R extends { source?: PluginCacheResult<unknown>["source"]; error?: unknown }>(result: R): R {
|
|
26
|
+
if (result.source === "stale-fallback" && !isTransientFailure(result.error)) throw result.error;
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
/** Shared persistence/request lifecycle; keys, codecs, validation and policies stay with the plugin. */
|
|
16
31
|
export function createPluginCache<T, Stored = T>(options: {
|
|
17
32
|
kind: string;
|
|
@@ -108,11 +123,13 @@ export function createPluginCache<T, Stored = T>(options: {
|
|
|
108
123
|
}
|
|
109
124
|
return { value, fetchedAt, staleAt: fetchedAt + options.policy.staleMs, expiresAt: fetchedAt + options.policy.expireMs, source: options.source };
|
|
110
125
|
} });
|
|
126
|
+
// A retained copy carries its refresh failure, whatever was thrown (an Error without a message included).
|
|
127
|
+
const failed = "refreshError" in result;
|
|
111
128
|
const error = result.refreshError;
|
|
112
129
|
return {
|
|
113
|
-
data: result.value, fetchedAt: result.fetchedAt, stale:
|
|
114
|
-
source:
|
|
115
|
-
...(
|
|
130
|
+
data: result.value, fetchedAt: result.fetchedAt, stale: failed || result.staleAt <= Date.now(),
|
|
131
|
+
source: failed ? "stale-fallback" : result === before ? "cache" : "network",
|
|
132
|
+
...(failed ? { error, refreshError: error instanceof Error ? error.message : String(error) } : {}),
|
|
116
133
|
};
|
|
117
134
|
},
|
|
118
135
|
};
|