gloomberb 0.8.1 → 0.9.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/README.md +106 -54
- package/package.json +14 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/src/api-client/chat.ts +8 -0
- package/src/api-client/index.test.ts +30 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/normalizers.ts +1 -0
- package/src/api-client/types.ts +1 -0
- package/src/app/global-shortcuts.test.tsx +54 -4
- package/src/app/global-shortcuts.ts +11 -5
- package/src/capabilities/factories.ts +4 -0
- package/src/components/chart/comparison/data.test.ts +54 -0
- package/src/components/chart/comparison/data.ts +47 -24
- package/src/components/chart/core/projection.ts +51 -27
- package/src/components/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/data-table/stack-view.test.tsx +14 -11
- package/src/components/data-table/stack-view.tsx +1 -2
- package/src/components/data-table/view.test.tsx +24 -18
- package/src/components/data-table/view.tsx +313 -83
- package/src/components/feed-data-table/stack-view.tsx +6 -8
- package/src/components/layout/pane/footer/index.test.tsx +6 -0
- package/src/components/layout/pane/footer/index.tsx +14 -2
- package/src/components/layout/pane/title.ts +5 -1
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/pane-settings-dialog/field-dialogs.tsx +0 -2
- package/src/components/pane-settings-dialog/tui.tsx +1 -1
- package/src/components/ticker/list-table-view.tsx +17 -25
- package/src/components/ui/choice-dialog.tsx +12 -2
- package/src/components/ui/multi-select/dialog.tsx +17 -5
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/shortcut-hint.tsx +14 -2
- package/src/components/ui/table-layout.test.ts +2 -2
- package/src/components/ui/table-layout.ts +8 -2
- package/src/components/ui/tabs.tsx +115 -86
- package/src/components/ui/ui.test.tsx +83 -4
- package/src/core/state/app/types.ts +12 -4
- package/src/market-data/coordinator/auxiliary.ts +29 -1
- package/src/market-data/coordinator/index.ts +17 -1
- package/src/market-data/hooks.tsx +17 -1
- package/src/market-data/selectors.ts +4 -0
- package/src/plugins/builtin/account-management/form-components.tsx +0 -4
- package/src/plugins/builtin/account-management/model.test.ts +54 -0
- package/src/plugins/builtin/account-management/model.ts +13 -3
- package/src/plugins/builtin/account-management/pane.tsx +5 -11
- package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
- package/src/plugins/builtin/alerts/pane.tsx +5 -3
- package/src/plugins/builtin/analytics/index.tsx +0 -3
- package/src/plugins/builtin/analytics/view.tsx +6 -5
- package/src/plugins/builtin/broker-manager/index.tsx +6 -6
- package/src/plugins/builtin/buildout/pane/index.tsx +6 -6
- package/src/plugins/builtin/changelog/index.tsx +40 -27
- package/src/plugins/builtin/changelog/model.test.ts +77 -0
- package/src/plugins/builtin/changelog/model.ts +98 -0
- package/src/plugins/builtin/chat/channel-labels.ts +53 -0
- package/src/plugins/builtin/chat/channels.test.ts +28 -0
- package/src/plugins/builtin/chat/channels.ts +36 -19
- package/src/plugins/builtin/chat/chat.test.tsx +264 -5
- package/src/plugins/builtin/chat/content/channel-navigation.ts +50 -11
- package/src/plugins/builtin/chat/content/composer-runtime.test.tsx +144 -0
- package/src/plugins/builtin/chat/content/composer-runtime.ts +151 -20
- package/src/plugins/builtin/chat/content/composer.tsx +23 -2
- package/src/plugins/builtin/chat/content/index.tsx +66 -4
- package/src/plugins/builtin/chat/content/layout-metrics.ts +3 -1
- package/src/plugins/builtin/chat/content/shortcuts.ts +27 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +8 -0
- package/src/plugins/builtin/chat/content/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/index.ts +41 -0
- package/src/plugins/builtin/chat/controller/messages.ts +18 -0
- package/src/plugins/builtin/chat/controller/notifications.ts +10 -4
- package/src/plugins/builtin/chat/controller/utils.ts +6 -3
- package/src/plugins/builtin/chat/controller/view.ts +1 -1
- package/src/plugins/builtin/chat/controller.test.ts +189 -0
- package/src/plugins/builtin/chat/edit-window.ts +24 -0
- package/src/plugins/builtin/chat/layout.ts +94 -31
- package/src/plugins/builtin/chat/message/action-chip.tsx +1 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +53 -20
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +16 -10
- package/src/plugins/builtin/chat/message/render-state.ts +5 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +49 -20
- package/src/plugins/builtin/chat/message/types.ts +2 -0
- package/src/plugins/builtin/chat/pane.tsx +35 -7
- package/src/plugins/builtin/chat/sidebar.test.tsx +72 -0
- package/src/plugins/builtin/chat/status-widget.tsx +29 -11
- package/src/plugins/builtin/chat/test-harness.tsx +2 -0
- package/src/plugins/builtin/cloud/plugin.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +7 -11
- package/src/plugins/builtin/congress-trades/pane.tsx +14 -22
- package/src/plugins/builtin/earnings/index.tsx +8 -7
- package/src/plugins/builtin/econ/index.tsx +6 -8
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +326 -164
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/holders/pane.tsx +7 -6
- package/src/plugins/builtin/market-movers/index.tsx +6 -3
- package/src/plugins/builtin/news/wire/news/table.tsx +7 -6
- package/src/plugins/builtin/options/view.tsx +16 -8
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/research/analyst-pane.tsx +1 -2
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +358 -7
- package/src/plugins/builtin/research/index.test.ts +30 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +6 -8
- package/src/plugins/builtin/sec/filing-documents.ts +66 -0
- package/src/plugins/builtin/sec/index.tsx +132 -20
- package/src/plugins/builtin/sectors/index.tsx +13 -23
- package/src/plugins/builtin/shared/ticker-request.ts +1 -0
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +26 -25
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes/provider-search.tsx +6 -5
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +10 -8
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/builtin/world-indices/index.tsx +9 -6
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/plugins/prediction-markets/pane.tsx +10 -9
- package/src/plugins/registry/index.ts +3 -0
- package/src/plugins/runtime/context.tsx +1 -0
- package/src/plugins/runtime/index.tsx +4 -0
- package/src/react/use-throttled-commit-value.ts +116 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +3 -2
- package/src/renderers/electrobun/view/data-table/row.tsx +2 -2
- package/src/renderers/electrobun/view/host/input.tsx +23 -2
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +1 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -0
- package/src/sources/provider-router/cache.ts +1 -0
- package/src/sources/provider-router/document-routes.ts +45 -0
- package/src/sources/provider-router/index.ts +4 -0
- package/src/sources/sec-edgar/content.ts +9 -3
- package/src/sources/sec-edgar.test.ts +108 -1
- package/src/sources/sec-edgar.ts +105 -1
- package/src/sources/yahoo-finance.ts +4 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/test-support/plugin-runtime.ts +1 -0
- package/src/tickers/search/index.ts +1 -0
- package/src/types/data-provider.ts +11 -0
- package/src/ui/host.tsx +1 -0
- package/src/version.ts +1 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
-
import { TextAttributes } from "../../../ui";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, ScrollBox, Text, TextAttributes, type ScrollBoxRenderable } from "../../../ui";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
DataTableStackView,
|
|
5
5
|
usePaneFooter,
|
|
6
6
|
type DataTableCell,
|
|
7
7
|
type DataTableColumn,
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
9
|
} from "../../../components";
|
|
10
|
+
import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
|
|
10
11
|
import type {
|
|
11
12
|
AnalystEstimateRecord,
|
|
12
13
|
AnalystResearchData,
|
|
@@ -16,9 +17,23 @@ import type {
|
|
|
16
17
|
} from "../../../types/financials";
|
|
17
18
|
import { blendHex, colors } from "../../../theme/colors";
|
|
18
19
|
import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
20
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
21
|
+
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
22
|
+
import { useResolvedEntryValue, useSecFilingContent, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
|
|
23
|
+
import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
24
|
+
import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
25
|
+
import { isUsEquityTicker } from "../../../utils/sec";
|
|
19
26
|
import { useAssetData } from "../../runtime";
|
|
20
27
|
import { handleRefreshKey, loadingErrorFooterInfo, refreshFooterHint } from "../shared/table-pane";
|
|
21
28
|
import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
|
|
29
|
+
import {
|
|
30
|
+
documentContentKey,
|
|
31
|
+
documentContentTarget,
|
|
32
|
+
documentHeading,
|
|
33
|
+
formatCompactDocumentLabel,
|
|
34
|
+
isDefaultVisibleFilingDocument,
|
|
35
|
+
isInlineExhibitDocument,
|
|
36
|
+
} from "../sec/filing-documents";
|
|
22
37
|
|
|
23
38
|
type EventStatus = "Earnings" | "Q Est" | "FY Est" | "TTM" | "Dividend" | "Split";
|
|
24
39
|
|
|
@@ -46,6 +61,9 @@ type EstimatePair = {
|
|
|
46
61
|
revenue?: AnalystEstimateRecord;
|
|
47
62
|
};
|
|
48
63
|
|
|
64
|
+
const SEC_EVENT_FILING_LIMIT = 50;
|
|
65
|
+
const SEC_EVENT_MATCH_WINDOW_DAYS = 7;
|
|
66
|
+
|
|
49
67
|
function todayDateKey(): string {
|
|
50
68
|
const now = new Date();
|
|
51
69
|
const year = now.getFullYear();
|
|
@@ -84,6 +102,75 @@ function daysBetween(leftDate: string, rightDate: string): number {
|
|
|
84
102
|
return Math.abs(left - right) / 86_400_000;
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
function dateKeyToEpochDay(dateKey: string): number | null {
|
|
106
|
+
const timestamp = new Date(`${dateKey}T00:00:00Z`).getTime();
|
|
107
|
+
if (!Number.isFinite(timestamp)) return null;
|
|
108
|
+
return Math.floor(timestamp / 86_400_000);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function signedDaysBetween(leftDate: string, rightDate: string): number {
|
|
112
|
+
const left = dateKeyToEpochDay(leftDate);
|
|
113
|
+
const right = dateKeyToEpochDay(rightDate);
|
|
114
|
+
if (left == null || right == null) return Number.POSITIVE_INFINITY;
|
|
115
|
+
return right - left;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function filingDateKey(filing: SecFilingItem): string {
|
|
119
|
+
const value = filing.filingDate as Date | string | number;
|
|
120
|
+
if (value instanceof Date) return value.toISOString().slice(0, 10);
|
|
121
|
+
const date = new Date(value);
|
|
122
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString().slice(0, 10);
|
|
123
|
+
return String(value).slice(0, 10);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function isSecEarningsFilingCandidate(filing: SecFilingItem): boolean {
|
|
127
|
+
const form = filing.form.trim().toUpperCase();
|
|
128
|
+
return form === "8-K"
|
|
129
|
+
|| form === "8-K/A"
|
|
130
|
+
|| form === "10-Q"
|
|
131
|
+
|| form === "10-Q/A"
|
|
132
|
+
|| form === "10-K"
|
|
133
|
+
|| form === "10-K/A";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function filingSearchText(filing: SecFilingItem): string {
|
|
137
|
+
return [
|
|
138
|
+
filing.form,
|
|
139
|
+
filing.items,
|
|
140
|
+
filing.primaryDocument,
|
|
141
|
+
filing.primaryDocDescription,
|
|
142
|
+
].filter(Boolean).join(" ").toUpperCase();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scoreFilingForEarnings(filing: SecFilingItem, earningsDate: string): number | null {
|
|
146
|
+
if (!isSecEarningsFilingCandidate(filing)) return null;
|
|
147
|
+
const delta = signedDaysBetween(earningsDate, filingDateKey(filing));
|
|
148
|
+
if (!Number.isFinite(delta) || delta < -1 || delta > SEC_EVENT_MATCH_WINDOW_DAYS) return null;
|
|
149
|
+
|
|
150
|
+
const form = filing.form.trim().toUpperCase();
|
|
151
|
+
const text = filingSearchText(filing);
|
|
152
|
+
let score = Math.abs(delta) * 10;
|
|
153
|
+
if (delta < 0) score += 12;
|
|
154
|
+
if (form.startsWith("10-")) score += 30;
|
|
155
|
+
if (text.includes("2.02")) score -= 18;
|
|
156
|
+
if (text.includes("9.01")) score -= 5;
|
|
157
|
+
if (/RESULTS OF OPERATIONS|FINANCIAL CONDITION|EARNINGS/i.test(text)) score -= 6;
|
|
158
|
+
return score;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function matchEarningsSecFiling(row: { status: string; date: string } | null | undefined, filings: readonly SecFilingItem[]): SecFilingItem | null {
|
|
162
|
+
if (!row || row.status !== "Earnings") return null;
|
|
163
|
+
let best: { filing: SecFilingItem; score: number } | null = null;
|
|
164
|
+
for (const filing of filings) {
|
|
165
|
+
const score = scoreFilingForEarnings(filing, row.date);
|
|
166
|
+
if (score == null) continue;
|
|
167
|
+
if (!best || score < best.score) {
|
|
168
|
+
best = { filing, score };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return best?.filing ?? null;
|
|
172
|
+
}
|
|
173
|
+
|
|
87
174
|
function statementForEarningsDate(
|
|
88
175
|
quarterlyStatements: readonly FinancialStatement[],
|
|
89
176
|
earningsDate: string,
|
|
@@ -293,6 +380,98 @@ function toneColor(tone: EventRow["tone"]): string {
|
|
|
293
380
|
return colors.text;
|
|
294
381
|
}
|
|
295
382
|
|
|
383
|
+
function eventDetailTitle(row: EventRow): string {
|
|
384
|
+
return `${row.status} | ${row.date}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function eventSummaryLine(row: EventRow): string {
|
|
388
|
+
return [
|
|
389
|
+
row.date,
|
|
390
|
+
row.period,
|
|
391
|
+
row.qEps != null ? `EPS ${formatNumber(row.qEps, 2)}` : null,
|
|
392
|
+
row.qRevenue != null ? `Rev ${formatCompact(row.qRevenue)}` : null,
|
|
393
|
+
row.annualEps != null ? `Ann EPS ${formatNumber(row.annualEps, 2)}` : null,
|
|
394
|
+
row.annualRevenue != null ? `Ann Rev ${formatCompact(row.annualRevenue)}` : null,
|
|
395
|
+
row.value !== "-" ? row.value : null,
|
|
396
|
+
row.detail || null,
|
|
397
|
+
].filter((line): line is string => !!line).join(" | ");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function buildEventDetailBody({
|
|
401
|
+
row,
|
|
402
|
+
secFilingsLoading,
|
|
403
|
+
filing,
|
|
404
|
+
documents,
|
|
405
|
+
documentsLoading,
|
|
406
|
+
inlineContent,
|
|
407
|
+
primaryContent,
|
|
408
|
+
primaryContentLoading,
|
|
409
|
+
}: {
|
|
410
|
+
row: EventRow;
|
|
411
|
+
secFilingsLoading: boolean;
|
|
412
|
+
filing: SecFilingItem | null;
|
|
413
|
+
documents: SecFilingDocument[];
|
|
414
|
+
documentsLoading: boolean;
|
|
415
|
+
inlineContent: Map<string, string | null>;
|
|
416
|
+
primaryContent: string | null | undefined;
|
|
417
|
+
primaryContentLoading: boolean;
|
|
418
|
+
}): string {
|
|
419
|
+
const lines: string[] = ["Summary", eventSummaryLine(row)];
|
|
420
|
+
if (row.status !== "Earnings") {
|
|
421
|
+
return lines.join("\n");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
lines.push("", "SEC Filing");
|
|
425
|
+
if (secFilingsLoading && !filing) {
|
|
426
|
+
lines.push("Loading recent SEC filings...");
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
if (!filing) {
|
|
430
|
+
lines.push("No related SEC filing found in recent filings.");
|
|
431
|
+
return lines.join("\n");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
lines.push([
|
|
435
|
+
`${filing.form} filed ${filingDateKey(filing)}`,
|
|
436
|
+
filing.items ? `Items ${filing.items}` : null,
|
|
437
|
+
`Accession ${filing.accessionNumber}`,
|
|
438
|
+
].filter(Boolean).join(" | "));
|
|
439
|
+
|
|
440
|
+
lines.push("", "Documents");
|
|
441
|
+
if (documentsLoading && documents.length === 0) {
|
|
442
|
+
lines.push("Loading filing documents...");
|
|
443
|
+
} else if (documents.length === 0) {
|
|
444
|
+
lines.push("No filing documents were listed for this filing.");
|
|
445
|
+
} else {
|
|
446
|
+
const visibleDocuments = documents.filter(isDefaultVisibleFilingDocument);
|
|
447
|
+
lines.push(...visibleDocuments.map(formatCompactDocumentLabel));
|
|
448
|
+
const hiddenCount = documents.length - visibleDocuments.length;
|
|
449
|
+
if (hiddenCount > 0) lines.push(`+ ${hiddenCount} support documents hidden`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const exhibits = documents.filter(isInlineExhibitDocument);
|
|
453
|
+
if (exhibits.length > 0) {
|
|
454
|
+
lines.push("", "Inline Exhibits");
|
|
455
|
+
for (const document of exhibits) {
|
|
456
|
+
const key = documentContentKey(filing, document);
|
|
457
|
+
const hasContent = inlineContent.has(key);
|
|
458
|
+
const content = inlineContent.get(key);
|
|
459
|
+
lines.push("", documentHeading(document));
|
|
460
|
+
lines.push(hasContent
|
|
461
|
+
? content || "Readable document content was not available for this exhibit."
|
|
462
|
+
: "Loading exhibit content...");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!documentsLoading && exhibits.length === 0) {
|
|
467
|
+
lines.push("", "Primary Filing Content");
|
|
468
|
+
lines.push(primaryContentLoading
|
|
469
|
+
? "Loading filing content..."
|
|
470
|
+
: primaryContent || "Readable filing content was not available.");
|
|
471
|
+
}
|
|
472
|
+
return lines.join("\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
296
475
|
export function CorporateActionsView({
|
|
297
476
|
focused,
|
|
298
477
|
width,
|
|
@@ -305,7 +484,7 @@ export function CorporateActionsView({
|
|
|
305
484
|
footerPaneId?: string;
|
|
306
485
|
}) {
|
|
307
486
|
const dataProvider = useAssetData();
|
|
308
|
-
const { symbol, exchange, currency } = useSymbolBinding();
|
|
487
|
+
const { symbol, ticker, exchange, currency } = useSymbolBinding();
|
|
309
488
|
const actionsLoader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
|
|
310
489
|
if (!dataProvider?.getCorporateActions) throw new Error("Corporate actions source unavailable");
|
|
311
490
|
return dataProvider.getCorporateActions(nextSymbol, nextExchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
|
|
@@ -341,6 +520,12 @@ export function CorporateActionsView({
|
|
|
341
520
|
buildEventRows(actionsData, analystData, financialsData, displayCurrency)
|
|
342
521
|
), [actionsData, analystData, displayCurrency, financialsData]);
|
|
343
522
|
const columns = useMemo(() => buildEventColumns(), []);
|
|
523
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
524
|
+
const [openRowId, setOpenRowId] = useState<string | null>(null);
|
|
525
|
+
const detailScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
526
|
+
const [inlineContent, setInlineContent] = useState<Map<string, string | null>>(new Map());
|
|
527
|
+
const documentFetchStartedRef = useRef(new Set<string>());
|
|
528
|
+
const documentFetchGenRef = useRef(0);
|
|
344
529
|
const todayKey = todayDateKey();
|
|
345
530
|
const futureRowBackground = blendHex(colors.bg, colors.positive, 0.16);
|
|
346
531
|
const loading = actionsLoading || analystLoading || financialsLoading;
|
|
@@ -350,6 +535,163 @@ export function CorporateActionsView({
|
|
|
350
535
|
reloadAnalyst();
|
|
351
536
|
reloadFinancials();
|
|
352
537
|
}, [reloadActions, reloadAnalyst, reloadFinancials]);
|
|
538
|
+
const openRow = openRowId
|
|
539
|
+
? rows.find((row) => row.id === openRowId) ?? null
|
|
540
|
+
: null;
|
|
541
|
+
const instrument = useMemo(() => instrumentFromTicker(ticker, symbol), [symbol, ticker]);
|
|
542
|
+
const secFilingsEntry = useSecFilingsQuery(
|
|
543
|
+
openRow?.status === "Earnings" && instrument && isUsEquityTicker(ticker)
|
|
544
|
+
? { instrument, count: SEC_EVENT_FILING_LIMIT }
|
|
545
|
+
: null,
|
|
546
|
+
);
|
|
547
|
+
const secFilings = useResolvedEntryValue(secFilingsEntry) ?? [];
|
|
548
|
+
const secFilingsLoading = openRow?.status === "Earnings" && (
|
|
549
|
+
secFilingsEntry?.phase === "idle"
|
|
550
|
+
|| secFilingsEntry?.phase === "loading"
|
|
551
|
+
|| secFilingsEntry?.phase === "refreshing"
|
|
552
|
+
);
|
|
553
|
+
const matchedFiling = useMemo(
|
|
554
|
+
() => matchEarningsSecFiling(openRow, secFilings),
|
|
555
|
+
[openRow, secFilings],
|
|
556
|
+
);
|
|
557
|
+
const documentsEntry = useSecFilingDocuments(matchedFiling);
|
|
558
|
+
const documents = useResolvedEntryValue(documentsEntry) ?? [];
|
|
559
|
+
const documentsLoading = !!matchedFiling && (
|
|
560
|
+
documentsEntry?.phase === "idle"
|
|
561
|
+
|| documentsEntry?.phase === "loading"
|
|
562
|
+
|| documentsEntry?.phase === "refreshing"
|
|
563
|
+
);
|
|
564
|
+
const hasInlineExhibits = documents.some(isInlineExhibitDocument);
|
|
565
|
+
const filingContentEntry = useSecFilingContent(
|
|
566
|
+
matchedFiling && !documentsLoading && !hasInlineExhibits ? matchedFiling : null,
|
|
567
|
+
);
|
|
568
|
+
const primaryContent = useResolvedEntryValue(filingContentEntry);
|
|
569
|
+
const primaryContentLoading = !!matchedFiling && (
|
|
570
|
+
filingContentEntry?.phase === "idle"
|
|
571
|
+
|| filingContentEntry?.phase === "loading"
|
|
572
|
+
|| filingContentEntry?.phase === "refreshing"
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
if (rows.length > 0 && selectedIdx >= rows.length) {
|
|
577
|
+
setSelectedIdx(Math.max(0, rows.length - 1));
|
|
578
|
+
}
|
|
579
|
+
}, [rows.length, selectedIdx]);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (openRowId && !rows.some((row) => row.id === openRowId)) {
|
|
583
|
+
setOpenRowId(null);
|
|
584
|
+
}
|
|
585
|
+
}, [openRowId, rows]);
|
|
586
|
+
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (!openRowId) return;
|
|
589
|
+
const scrollBox = detailScrollRef.current;
|
|
590
|
+
if (scrollBox) scrollBox.scrollTop = 0;
|
|
591
|
+
}, [openRowId]);
|
|
592
|
+
|
|
593
|
+
useEffect(() => {
|
|
594
|
+
setInlineContent(new Map());
|
|
595
|
+
documentFetchStartedRef.current.clear();
|
|
596
|
+
documentFetchGenRef.current += 1;
|
|
597
|
+
}, [exchange, symbol]);
|
|
598
|
+
|
|
599
|
+
useEffect(() => {
|
|
600
|
+
if (!matchedFiling || documents.length === 0) return;
|
|
601
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
602
|
+
if (!coordinator) return;
|
|
603
|
+
const gen = documentFetchGenRef.current;
|
|
604
|
+
const documentsToFetch = documents.filter((document) => {
|
|
605
|
+
if (!isInlineExhibitDocument(document)) return false;
|
|
606
|
+
const key = documentContentKey(matchedFiling, document);
|
|
607
|
+
return !inlineContent.has(key) && !documentFetchStartedRef.current.has(key);
|
|
608
|
+
});
|
|
609
|
+
if (documentsToFetch.length === 0) return;
|
|
610
|
+
|
|
611
|
+
for (const document of documentsToFetch) {
|
|
612
|
+
documentFetchStartedRef.current.add(documentContentKey(matchedFiling, document));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
(async () => {
|
|
616
|
+
for (const document of documentsToFetch) {
|
|
617
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
618
|
+
const key = documentContentKey(matchedFiling, document);
|
|
619
|
+
try {
|
|
620
|
+
const entry = await coordinator.loadSecFilingContent(documentContentTarget(matchedFiling, document));
|
|
621
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
622
|
+
setInlineContent((prev) => new Map(prev).set(key, entry.data ?? entry.lastGoodData ?? null));
|
|
623
|
+
} catch {
|
|
624
|
+
if (documentFetchGenRef.current !== gen) return;
|
|
625
|
+
setInlineContent((prev) => new Map(prev).set(key, null));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
})();
|
|
629
|
+
}, [documents, inlineContent, matchedFiling]);
|
|
630
|
+
const detailBody = openRow
|
|
631
|
+
? buildEventDetailBody({
|
|
632
|
+
row: openRow,
|
|
633
|
+
secFilingsLoading,
|
|
634
|
+
filing: matchedFiling,
|
|
635
|
+
documents,
|
|
636
|
+
documentsLoading,
|
|
637
|
+
inlineContent,
|
|
638
|
+
primaryContent,
|
|
639
|
+
primaryContentLoading,
|
|
640
|
+
})
|
|
641
|
+
: "";
|
|
642
|
+
const detailTextWidth = Math.max(width - 2, 12);
|
|
643
|
+
const scrollDetailBy = useCallback((delta: number) => {
|
|
644
|
+
const scrollBox = detailScrollRef.current;
|
|
645
|
+
if (!scrollBox?.viewport) return;
|
|
646
|
+
const maxScrollTop = Math.max(0, scrollBox.scrollHeight - scrollBox.viewport.height);
|
|
647
|
+
scrollBox.scrollTop = Math.max(0, Math.min(maxScrollTop, scrollBox.scrollTop + delta));
|
|
648
|
+
}, []);
|
|
649
|
+
|
|
650
|
+
const handleDetailKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
651
|
+
if (isPlainKey(event, "j", "down")) {
|
|
652
|
+
event.stopPropagation?.();
|
|
653
|
+
event.preventDefault?.();
|
|
654
|
+
scrollDetailBy(1);
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
if (isPlainKey(event, "k", "up")) {
|
|
658
|
+
event.stopPropagation?.();
|
|
659
|
+
event.preventDefault?.();
|
|
660
|
+
scrollDetailBy(-1);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}, [scrollDetailBy]);
|
|
665
|
+
const detailContent = openRow ? (
|
|
666
|
+
<Box
|
|
667
|
+
flexDirection="column"
|
|
668
|
+
flexGrow={1}
|
|
669
|
+
flexBasis={0}
|
|
670
|
+
minHeight={0}
|
|
671
|
+
overflow="hidden"
|
|
672
|
+
paddingX={1}
|
|
673
|
+
paddingY={1}
|
|
674
|
+
>
|
|
675
|
+
<ScrollBox
|
|
676
|
+
ref={detailScrollRef}
|
|
677
|
+
flexGrow={1}
|
|
678
|
+
flexBasis={0}
|
|
679
|
+
minHeight={0}
|
|
680
|
+
scrollY
|
|
681
|
+
focusable={false}
|
|
682
|
+
>
|
|
683
|
+
<Box flexDirection="column">
|
|
684
|
+
{wrapTextLines(detailBody, detailTextWidth).map((line, index) => (
|
|
685
|
+
<Box key={`event-detail-${index}`} height={1}>
|
|
686
|
+
<Text fg={colors.text}>{line}</Text>
|
|
687
|
+
</Box>
|
|
688
|
+
))}
|
|
689
|
+
</Box>
|
|
690
|
+
</ScrollBox>
|
|
691
|
+
</Box>
|
|
692
|
+
) : (
|
|
693
|
+
<Box flexGrow={1} />
|
|
694
|
+
);
|
|
353
695
|
|
|
354
696
|
const renderCell = useCallback((
|
|
355
697
|
row: EventRow,
|
|
@@ -390,8 +732,19 @@ export function CorporateActionsView({
|
|
|
390
732
|
}), [error, footerPaneId, loading, reload]);
|
|
391
733
|
|
|
392
734
|
return (
|
|
393
|
-
<
|
|
735
|
+
<DataTableStackView<EventRow, EventColumn>
|
|
394
736
|
focused={focused}
|
|
737
|
+
detailOpen={!!openRow}
|
|
738
|
+
onBack={() => setOpenRowId(null)}
|
|
739
|
+
detailContent={detailContent}
|
|
740
|
+
detailTitle={openRow ? eventDetailTitle(openRow) : undefined}
|
|
741
|
+
selection={{
|
|
742
|
+
kind: "index",
|
|
743
|
+
selectedIndex: selectedIdx,
|
|
744
|
+
onChange: (index) => setSelectedIdx(index),
|
|
745
|
+
}}
|
|
746
|
+
onActivate={(row) => setOpenRowId(row.id)}
|
|
747
|
+
onDetailKeyDown={handleDetailKeyDown}
|
|
395
748
|
rootWidth={width}
|
|
396
749
|
rootHeight={height}
|
|
397
750
|
onRootKeyDown={handleKeyDown}
|
|
@@ -401,8 +754,6 @@ export function CorporateActionsView({
|
|
|
401
754
|
sortDirection="desc"
|
|
402
755
|
onHeaderClick={() => {}}
|
|
403
756
|
getItemKey={(row) => row.id}
|
|
404
|
-
isSelected={() => false}
|
|
405
|
-
onSelect={() => {}}
|
|
406
757
|
renderCell={renderCell}
|
|
407
758
|
getRowBackgroundColor={(row) => (
|
|
408
759
|
row.date > todayKey ? futureRowBackground : undefined
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
sortRatingRows,
|
|
9
9
|
type RatingSortPreference,
|
|
10
10
|
} from "./analyst-pane";
|
|
11
|
-
import { buildEventRows } from "./corporate-actions-pane";
|
|
11
|
+
import { buildEventRows, matchEarningsSecFiling } from "./corporate-actions-pane";
|
|
12
12
|
|
|
13
13
|
const ratings: AnalystRatingRecord[] = [
|
|
14
14
|
{
|
|
@@ -337,4 +337,33 @@ describe("event rows", () => {
|
|
|
337
337
|
expect(rows[2]?.qEps).toBeUndefined();
|
|
338
338
|
expect(rows[2]?.annualEps).toBeUndefined();
|
|
339
339
|
});
|
|
340
|
+
|
|
341
|
+
test("matches reported earnings to nearby SEC earnings-release filings", () => {
|
|
342
|
+
const row = buildEventRows({
|
|
343
|
+
symbol: "AAPL",
|
|
344
|
+
dividends: [],
|
|
345
|
+
splits: [],
|
|
346
|
+
earnings: [{ date: "2026-01-30", epsActual: 2.4 }],
|
|
347
|
+
}, null, null, "USD").find((candidate) => candidate.status === "Earnings");
|
|
348
|
+
const filings = [
|
|
349
|
+
{
|
|
350
|
+
accessionNumber: "0000320193-26-000010",
|
|
351
|
+
form: "10-Q",
|
|
352
|
+
filingDate: new Date("2026-02-04T00:00:00Z"),
|
|
353
|
+
cik: "0000320193",
|
|
354
|
+
filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000010-index.htm",
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
accessionNumber: "0000320193-26-000009",
|
|
358
|
+
form: "8-K",
|
|
359
|
+
filingDate: "2026-01-31T00:00:00.000Z" as unknown as Date,
|
|
360
|
+
items: "2.02,9.01",
|
|
361
|
+
primaryDocDescription: "Results of Operations and Financial Condition",
|
|
362
|
+
cik: "0000320193",
|
|
363
|
+
filingUrl: "https://www.sec.gov/Archives/edgar/data/320193/0000320193-26-000009-index.htm",
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
|
|
367
|
+
expect(matchEarningsSecFiling(row, filings)?.accessionNumber).toBe("0000320193-26-000009");
|
|
368
|
+
});
|
|
340
369
|
});
|
|
@@ -165,9 +165,12 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
165
165
|
return (
|
|
166
166
|
<DataTableView<RelativeRow, RelativeColumn>
|
|
167
167
|
focused={focused}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
selection={{
|
|
169
|
+
kind: "index",
|
|
170
|
+
selectedIndex: rows.length > 0 ? selectedIdx : -1,
|
|
171
|
+
onChange: (index) => setSelectedIdx(index),
|
|
172
|
+
}}
|
|
173
|
+
onActivate={(row) => navigateTicker(row.symbol)}
|
|
171
174
|
onRootKeyDown={handleKeyDown}
|
|
172
175
|
rootWidth={width}
|
|
173
176
|
rootHeight={height}
|
|
@@ -177,11 +180,6 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
177
180
|
sortDirection="asc"
|
|
178
181
|
onHeaderClick={() => {}}
|
|
179
182
|
getItemKey={(row) => row.symbol}
|
|
180
|
-
isSelected={(row) => rows[selectedIdx]?.symbol === row.symbol}
|
|
181
|
-
onSelect={(row, index) => {
|
|
182
|
-
setSelectedIdx(index);
|
|
183
|
-
navigateTicker(row.symbol);
|
|
184
|
-
}}
|
|
185
183
|
renderCell={renderCell}
|
|
186
184
|
emptyStateTitle={loading ? "Loading peers..." : error ?? "No peers"}
|
|
187
185
|
/>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
|
|
2
|
+
|
|
3
|
+
const SEC_EXHIBIT_TYPE_RE = /^EX[-\s]?\d/i;
|
|
4
|
+
const SEC_XBRL_EXHIBIT_TYPE_RE = /^EX[-\s]?101(?:\.|$)/i;
|
|
5
|
+
const SEC_NON_READABLE_DOCUMENT_RE = /\.(?:cal|def|gif|jpg|jpeg|json|lab|png|pre|sch|xsd|zip)(?:$|[?#])/i;
|
|
6
|
+
const SEC_SUPPORT_DOCUMENT_TYPE_RE = /^(?:GRAPHIC|XML|ZIP|EX[-\s]?101(?:\.|$)|EX-FILING FEES)/i;
|
|
7
|
+
|
|
8
|
+
export function documentContentKey(filing: SecFilingItem, document: SecFilingDocument): string {
|
|
9
|
+
return `${filing.accessionNumber}:${document.document || document.url}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function documentContentTarget(filing: SecFilingItem, document: SecFilingDocument): SecFilingItem {
|
|
13
|
+
return {
|
|
14
|
+
...filing,
|
|
15
|
+
accessionNumber: documentContentKey(filing, document),
|
|
16
|
+
form: document.type || filing.form,
|
|
17
|
+
primaryDocument: document.document,
|
|
18
|
+
primaryDocDescription: document.description,
|
|
19
|
+
primaryDocumentUrl: document.url,
|
|
20
|
+
filingUrl: document.url,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatDocumentLabel(document: SecFilingDocument): string {
|
|
25
|
+
const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
|
|
26
|
+
const description = document.description
|
|
27
|
+
&& document.description !== document.document
|
|
28
|
+
&& document.description !== document.type
|
|
29
|
+
? ` | ${document.description}`
|
|
30
|
+
: "";
|
|
31
|
+
const size = document.size ? ` | ${document.size}` : "";
|
|
32
|
+
return `${label.padEnd(8)} ${document.document}${description}${size}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function formatCompactDocumentLabel(document: SecFilingDocument): string {
|
|
36
|
+
const label = document.isPrimary ? "PRIMARY" : document.type || "DOCUMENT";
|
|
37
|
+
const description = document.description
|
|
38
|
+
&& document.description !== document.document
|
|
39
|
+
&& document.description !== document.type
|
|
40
|
+
? ` | ${document.description}`
|
|
41
|
+
: "";
|
|
42
|
+
return `${label} ${document.document}${description}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function documentHeading(document: SecFilingDocument): string {
|
|
46
|
+
const label = document.type || "DOCUMENT";
|
|
47
|
+
const description = document.description
|
|
48
|
+
&& document.description !== document.document
|
|
49
|
+
&& document.description !== document.type
|
|
50
|
+
? ` | ${document.description}`
|
|
51
|
+
: "";
|
|
52
|
+
return `${label} | ${document.document}${description}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function isInlineExhibitDocument(document: SecFilingDocument): boolean {
|
|
56
|
+
const type = document.type.trim();
|
|
57
|
+
if (!SEC_EXHIBIT_TYPE_RE.test(type)) return false;
|
|
58
|
+
if (SEC_XBRL_EXHIBIT_TYPE_RE.test(type)) return false;
|
|
59
|
+
return !SEC_NON_READABLE_DOCUMENT_RE.test(document.document) && !SEC_NON_READABLE_DOCUMENT_RE.test(document.url);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isDefaultVisibleFilingDocument(document: SecFilingDocument): boolean {
|
|
63
|
+
if (document.isPrimary) return true;
|
|
64
|
+
if (isInlineExhibitDocument(document)) return true;
|
|
65
|
+
return !SEC_SUPPORT_DOCUMENT_TYPE_RE.test(document.type.trim());
|
|
66
|
+
}
|