@gloom-sh/terminal 0.19.0 → 0.19.1
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 +1 -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/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/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- 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 +1 -1
- 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 +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- 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/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- 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/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/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- 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 +2 -2
- 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/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 +3 -0
- 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/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 +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- 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/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 +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- 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/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- 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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -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/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- 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/create.ts +43 -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/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- 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/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { formatActionChords, hasKeybindingCaptureRequest, subscribeKeybindingCapture, useKeybindings } from "../../../app/keybindings";
|
|
3
|
-
import { Notice,
|
|
3
|
+
import { Notice, usePaneFooter, usePaneMenuItems, usePaneTabs, type PaneHint, type TableSection } from "../../../components";
|
|
4
4
|
import { t, tf } from "../../../i18n";
|
|
5
5
|
import { colors } from "../../../theme/colors";
|
|
6
6
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -18,6 +18,7 @@ import { FunctionsTable } from "./functions-table";
|
|
|
18
18
|
import { KeybindingsEditor } from "./keybindings-editor";
|
|
19
19
|
import { ShortcutTable, type ShortcutTableEntry } from "./shortcut-table";
|
|
20
20
|
import { resolveCommandShortcuts, resolveWindowTemplates } from "./shortcut-model";
|
|
21
|
+
import { badgeColumnWidth } from "./table-cells";
|
|
21
22
|
|
|
22
23
|
const HELP_TABS = [
|
|
23
24
|
{ label: "Basics", value: "basics" },
|
|
@@ -47,6 +48,9 @@ const TRY_THESE = [
|
|
|
47
48
|
{ query: "EE TSLA", description: "What analysts expect from Tesla" },
|
|
48
49
|
] as const;
|
|
49
50
|
|
|
51
|
+
/** The most of a table its key column takes, however many chips a row carries. */
|
|
52
|
+
const KEY_COLUMN_MAX_SHARE = 0.4;
|
|
53
|
+
|
|
50
54
|
function HelpPane({ focused, width, height }: PaneProps) {
|
|
51
55
|
const registry = getSharedRegistry();
|
|
52
56
|
const { openCommandBar, showPane, createPaneFromTemplate } = usePluginAppActions();
|
|
@@ -83,7 +87,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
83
87
|
// The UPGRADE line says how long the trial is; the length comes from /pricing.
|
|
84
88
|
const [pricing, setPricing] = useState<CloudPricing | null>(null);
|
|
85
89
|
useEffect(() => {
|
|
86
|
-
if (activeTabId !== "
|
|
90
|
+
if (activeTabId !== "reference" || pricing) return;
|
|
87
91
|
let live = true;
|
|
88
92
|
void apiClient.getCloudPricing()
|
|
89
93
|
.then((next) => {
|
|
@@ -131,10 +135,11 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
131
135
|
// Each tab's actions. `l` moves between tabs, so layout actions take `a`.
|
|
132
136
|
const tabHints = useMemo<PaneHint[]>(() => {
|
|
133
137
|
if (activeTabId === "basics") {
|
|
134
|
-
return [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
+
return [{ id: "docs", key: "d", label: "ocs", title: "Open Docs", onPress: openDocs }];
|
|
139
|
+
}
|
|
140
|
+
// The layout rows are here, so their action is.
|
|
141
|
+
if (activeTabId === "reference") {
|
|
142
|
+
return [{ id: "layout-actions", key: "a", label: " layout actions", title: "Layout Actions", onPress: openLayoutActions }];
|
|
138
143
|
}
|
|
139
144
|
if (activeTabId === "functions") {
|
|
140
145
|
return [{ id: "plugins", key: "p", label: "lugins", title: "Manage Plugins", onPress: openPluginManager }];
|
|
@@ -192,32 +197,31 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
192
197
|
].filter((item) => hasCode(item.badges[0]!)),
|
|
193
198
|
}], [hasCode]);
|
|
194
199
|
|
|
195
|
-
const commandBarSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [{
|
|
196
|
-
label: "Command Bar",
|
|
197
|
-
items: [
|
|
198
|
-
...(commandBarBadges.length > 0
|
|
199
|
-
? [entry("open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
|
|
200
|
-
: []),
|
|
201
|
-
...(tickerSearchBadges.length > 0
|
|
202
|
-
? [entry("ticker-search", tickerSearchBadges, "Open ticker search directly.")]
|
|
203
|
-
: []),
|
|
204
|
-
entry("des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
|
|
205
|
-
entry("help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
|
|
206
|
-
entry("docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
|
|
207
|
-
entry("upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
|
|
208
|
-
entry("mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
|
|
209
|
-
entry("move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
|
|
210
|
-
entry("page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
|
|
211
|
-
entry("run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
|
|
212
|
-
entry("accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
|
|
213
|
-
entry("close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
|
|
214
|
-
entry("clear", ["Ctrl+U"], "Clear command text."),
|
|
215
|
-
entry("delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
|
|
216
|
-
entry("back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
|
|
217
|
-
],
|
|
218
|
-
}], [commandBarBadges, tickerSearchBadges, trialDays]);
|
|
219
|
-
|
|
220
200
|
const referenceSections = useMemo<Array<TableSection<ShortcutTableEntry>>>(() => [
|
|
201
|
+
{
|
|
202
|
+
label: "Command Bar",
|
|
203
|
+
items: [
|
|
204
|
+
...(commandBarBadges.length > 0
|
|
205
|
+
? [entry("cmd-open", commandBarBadges, "Open command mode for actions, pane commands, and typed prefixes.")]
|
|
206
|
+
: []),
|
|
207
|
+
...(tickerSearchBadges.length > 0
|
|
208
|
+
? [entry("cmd-ticker-search", tickerSearchBadges, "Open ticker search directly.")]
|
|
209
|
+
: []),
|
|
210
|
+
entry("cmd-des", ["DES", "<ticker>"], "Open security details for a specific ticker."),
|
|
211
|
+
entry("cmd-help-card", ["HELP", "<function>"], "Open a function's help card: what it shows, its keys, how fresh its data is. HELP HELP reaches support."),
|
|
212
|
+
entry("cmd-docs", ["DOCS", "<function>"], "Read the docs and Academy lessons in the app. The ? on a pane opens its entry."),
|
|
213
|
+
entry("cmd-upgrade", ["UPGRADE"], tf("Go Pro for real-time data at gloom.sh/cloud, free for {days} days.", { days: trialDays })),
|
|
214
|
+
entry("cmd-mcp", ["MCP"], "Connect Claude Code, Codex, Cursor or any MCP client to Gloom's research tools."),
|
|
215
|
+
entry("cmd-move", ["Up/Down", "Ctrl+P/N"], "Move through command bar results."),
|
|
216
|
+
entry("cmd-page", ["PageUp/PageDown", "Ctrl+Home/End"], "Jump a page, or to the first or last result."),
|
|
217
|
+
entry("cmd-run", ["Enter", "Shift+Enter"], "Run the selected result or its secondary action."),
|
|
218
|
+
entry("cmd-accept-arg", ["Tab"], "Accept a suggested command argument when one is available."),
|
|
219
|
+
entry("cmd-close", ["Esc", ...tickerSearchBadges.filter((badge) => badge === "`" || badge.includes("+"))], "Close the command bar."),
|
|
220
|
+
entry("cmd-clear", ["Ctrl+U"], "Clear command text."),
|
|
221
|
+
entry("cmd-delete-word", ["Ctrl+W"], "Delete the previous word in command text."),
|
|
222
|
+
entry("cmd-back", ["Backspace"], "Go back from a nested command screen when the query is empty."),
|
|
223
|
+
],
|
|
224
|
+
},
|
|
221
225
|
{
|
|
222
226
|
label: "Navigation",
|
|
223
227
|
items: [
|
|
@@ -235,7 +239,8 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
235
239
|
entry("search", ["/"], "Search in the focused pane."),
|
|
236
240
|
entry("sidebar", ["<", ">"], "Narrow or widen the list beside the content, as in MAP and ASKG."),
|
|
237
241
|
entry("warnings", ["!"], "Open the focused pane's data warnings."),
|
|
238
|
-
entry("notification",
|
|
242
|
+
entry("notification", actionBadges("notification-action"), "Run the newest notification."),
|
|
243
|
+
entry("notification-dismiss", actionBadges("notification-dismiss"), "Dismiss the newest notification."),
|
|
239
244
|
],
|
|
240
245
|
},
|
|
241
246
|
{
|
|
@@ -303,6 +308,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
303
308
|
entry("close-pane", ["Esc", "Esc"], "Close the focused pane when nothing is being dragged."),
|
|
304
309
|
],
|
|
305
310
|
},
|
|
311
|
+
{
|
|
312
|
+
// Docked panes keep the saved layout and floating ones follow the pointer; the keyboard's way is Window Mode below.
|
|
313
|
+
label: "Layout",
|
|
314
|
+
items: [
|
|
315
|
+
entry("layout-drag", ["Drag"], "Floating panes can be dragged by the title bar and resized from the lower-right corner."),
|
|
316
|
+
{ ...entry("layout-actions", ["LMA"], "Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets."), run: openLayoutActions },
|
|
317
|
+
],
|
|
318
|
+
},
|
|
306
319
|
{
|
|
307
320
|
label: "Window Mode",
|
|
308
321
|
items: [
|
|
@@ -320,7 +333,14 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
320
333
|
entry("commit", ["Enter", "Esc"], "Commit pending changes or exit window mode."),
|
|
321
334
|
],
|
|
322
335
|
},
|
|
323
|
-
], [askBadges, copyBadges, functionHelpBadges, pasteBadges, windowMoveBadges, windowResizeBadges]);
|
|
336
|
+
], [askBadges, commandBarBadges, copyBadges, functionHelpBadges, openLayoutActions, pasteBadges, tickerSearchBadges, trialDays, windowMoveBadges, windowResizeBadges]);
|
|
337
|
+
|
|
338
|
+
// One key column for every key table in Help, wide enough for the longest group of chips, so the descriptions
|
|
339
|
+
// line up from table to table. A narrow pane caps it, and a row past the cap shows the chips that fit whole.
|
|
340
|
+
const keyColumnWidth = useMemo(() => badgeColumnWidth(
|
|
341
|
+
[...trySections, ...startSections, ...referenceSections].flatMap((section) => section.items.map((item) => item.badges)),
|
|
342
|
+
{ min: 8, max: Math.max(8, Math.floor(width * KEY_COLUMN_MAX_SHARE)) },
|
|
343
|
+
), [referenceSections, startSections, trySections, width]);
|
|
324
344
|
|
|
325
345
|
const renderContent = () => {
|
|
326
346
|
switch (activeTabId) {
|
|
@@ -339,38 +359,19 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
339
359
|
<>
|
|
340
360
|
{trySections.length > 0 && (
|
|
341
361
|
<Box marginBottom={1}>
|
|
342
|
-
<ShortcutTable sections={trySections} width={bodyWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
|
|
362
|
+
<ShortcutTable sections={trySections} width={bodyWidth} keyWidth={keyColumnWidth} keyLabel="TYPE" descriptionLabel="OPENS" />
|
|
343
363
|
</Box>
|
|
344
364
|
)}
|
|
345
365
|
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{commandBarBadges[0]
|
|
352
|
-
? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
|
|
353
|
-
: t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
|
|
354
|
-
</Text>
|
|
355
|
-
</Box>
|
|
356
|
-
</Box>
|
|
366
|
+
<Text fg={colors.textDim} wrapText>
|
|
367
|
+
{commandBarBadges[0]
|
|
368
|
+
? tf("Not sure of a code? Click the search bar at the top, or press {key}, and type what you want in plain words, like \"top news\". The Functions tab lists everything.", { key: commandBarBadges[0] })
|
|
369
|
+
: t("Not sure of a code? Click the search bar at the top and type what you want in plain words, like \"top news\". The Functions tab lists everything.")}
|
|
370
|
+
</Text>
|
|
357
371
|
|
|
358
372
|
<Box marginTop={1}>
|
|
359
|
-
<ShortcutTable sections={startSections} width={bodyWidth} />
|
|
373
|
+
<ShortcutTable sections={startSections} width={bodyWidth} keyWidth={keyColumnWidth} />
|
|
360
374
|
</Box>
|
|
361
|
-
|
|
362
|
-
<Box marginTop={1}>
|
|
363
|
-
<ShortcutTable sections={commandBarSections} width={bodyWidth} />
|
|
364
|
-
</Box>
|
|
365
|
-
|
|
366
|
-
<Section title="Layout Basics">
|
|
367
|
-
<Text fg={colors.text}>{t("Docked panes stay in the saved layout.")}</Text>
|
|
368
|
-
<Text fg={colors.text} wrapText>{t("Floating panes can be dragged by the title bar and resized from the lower-right corner.")}</Text>
|
|
369
|
-
{windowMoveBadges[0] && windowResizeBadges[0] && (
|
|
370
|
-
<Text fg={colors.text} wrapText>{tf("From the keyboard, {move} moves the focused window and {resize} resizes it.", { move: windowMoveBadges[0], resize: windowResizeBadges[0] })}</Text>
|
|
371
|
-
)}
|
|
372
|
-
<Text fg={colors.text} wrapText>{t("Use Layout Actions for split, move, duplicate, close all floating panes, undo, redo, and layout presets.")}</Text>
|
|
373
|
-
</Section>
|
|
374
375
|
</>
|
|
375
376
|
);
|
|
376
377
|
}
|
|
@@ -396,6 +397,7 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
396
397
|
<ShortcutTable
|
|
397
398
|
sections={referenceSections}
|
|
398
399
|
width={width}
|
|
400
|
+
keyWidth={keyColumnWidth}
|
|
399
401
|
height={contentHeight}
|
|
400
402
|
after={(
|
|
401
403
|
<Box paddingX={1} flexShrink={0}>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
import { t } from "../../../i18n";
|
|
23
23
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
24
24
|
import { Box, useUiCapabilities } from "../../../ui";
|
|
25
|
-
import { badgeCell
|
|
25
|
+
import { badgeCell } from "./table-cells";
|
|
26
26
|
|
|
27
27
|
export interface ShortcutTableEntry {
|
|
28
28
|
id: string;
|
|
@@ -38,6 +38,7 @@ type ShortcutRow = SectionedRow<ShortcutTableEntry>;
|
|
|
38
38
|
export function ShortcutTable({
|
|
39
39
|
sections,
|
|
40
40
|
width,
|
|
41
|
+
keyWidth,
|
|
41
42
|
height: fillHeight,
|
|
42
43
|
after,
|
|
43
44
|
keyLabel = "KEY",
|
|
@@ -46,6 +47,8 @@ export function ShortcutTable({
|
|
|
46
47
|
sections: ReadonlyArray<TableSection<ShortcutTableEntry>>;
|
|
47
48
|
/** Content width available inside the tab body. */
|
|
48
49
|
width: number;
|
|
50
|
+
/** The key column's width, one for every table in Help so their descriptions line up. */
|
|
51
|
+
keyWidth: number;
|
|
49
52
|
/** Fill this many rows and scroll, instead of growing to fit every row. */
|
|
50
53
|
height?: number;
|
|
51
54
|
/** Sits under a full-height table inside the same frame. */
|
|
@@ -59,10 +62,6 @@ export function ShortcutTable({
|
|
|
59
62
|
() => buildSectionedRows(sections, (entry) => entry.id),
|
|
60
63
|
[sections],
|
|
61
64
|
);
|
|
62
|
-
const keyWidth = useMemo(
|
|
63
|
-
() => badgeColumnWidth(rows.filter(isSectionedItemRow).map((row) => row.item.badges), { min: 8, max: 24 }),
|
|
64
|
-
[rows],
|
|
65
|
-
);
|
|
66
65
|
const columns = useMemo<ShortcutColumn[]>(() => [
|
|
67
66
|
{ id: "key", label: keyLabel, width: keyWidth, align: "left" },
|
|
68
67
|
{ id: "description", label: descriptionLabel, width: 20, align: "left", flexGrow: 1 },
|
|
@@ -72,7 +71,7 @@ export function ShortcutTable({
|
|
|
72
71
|
if (!isSectionedItemRow(row)) return EMPTY_TABLE_CELL;
|
|
73
72
|
const run = row.item.run;
|
|
74
73
|
const cell: DataTableCell = column.id === "key"
|
|
75
|
-
? badgeCell(row.item.badges, column.width)
|
|
74
|
+
? badgeCell(row.item.badges, column.width, { wholeOnly: true })
|
|
76
75
|
: { text: t(row.item.description), color: colors.text };
|
|
77
76
|
if (!run) return cell;
|
|
78
77
|
return {
|
|
@@ -31,10 +31,21 @@ export function badgeColumnWidth(
|
|
|
31
31
|
return Math.max(min, Math.min(max, widest));
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The chips of a group that fit a column whole, in order. A chip that would be
|
|
36
|
+
* cut ("Ctrl+Hor") is left out instead, so what shows is always a real key; the
|
|
37
|
+
* first chip stays whatever the width.
|
|
38
|
+
*/
|
|
39
|
+
export function wholeBadges(labels: readonly string[], width: number): readonly string[] {
|
|
40
|
+
let count = Math.min(1, labels.length);
|
|
41
|
+
while (count < labels.length && badgeGroupWidth(labels.slice(0, count + 1)) <= width) count += 1;
|
|
42
|
+
return labels.slice(0, count);
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
export function badgeCell(
|
|
35
46
|
labels: readonly string[],
|
|
36
47
|
width: number,
|
|
37
|
-
options: { tone?: "neutral" | "accent"; emptyLabel?: string; emptyColor?: string } = {},
|
|
48
|
+
options: { tone?: "neutral" | "accent"; emptyLabel?: string; emptyColor?: string; wholeOnly?: boolean } = {},
|
|
38
49
|
): DataTableCell {
|
|
39
50
|
if (labels.length === 0) {
|
|
40
51
|
return options.emptyLabel
|
|
@@ -45,7 +56,7 @@ export function badgeCell(
|
|
|
45
56
|
text: labels.join(" "),
|
|
46
57
|
content: (
|
|
47
58
|
<Box flexDirection="row" gap={BADGE_GAP} width={width} height={1} overflow="hidden">
|
|
48
|
-
{labels.map((label, index) => (
|
|
59
|
+
{(options.wholeOnly ? wholeBadges(labels, width) : labels).map((label, index) => (
|
|
49
60
|
<Badge key={`${label}:${index}`} label={label} variant="solid" tone={options.tone ?? "neutral"} />
|
|
50
61
|
))}
|
|
51
62
|
</Box>
|
|
@@ -11,6 +11,7 @@ import { listingAbroad, type IssuerListingParams } from "../../../api-client/pat
|
|
|
11
11
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
12
12
|
import { secListingKey } from "../../../utils/sec";
|
|
13
13
|
import { finiteOrNull, recordOrNull } from "../../../utils/guards";
|
|
14
|
+
import { readCompanyFrom } from "../../../types/company-from";
|
|
14
15
|
import { cachedCloudResource, loadCloudResource, type CloudResource } from "../shared/cloud-resource";
|
|
15
16
|
import { CLOUD_SESSION_REQUIRED } from "../shared/research-cloud-session";
|
|
16
17
|
|
|
@@ -110,8 +111,11 @@ function normalizeBeneficialOwnersPayload(value: unknown, ticker: string): Benef
|
|
|
110
111
|
if (!raw || !Array.isArray(raw.owners)) throw new Error("The server returned an invalid 13D/13G list");
|
|
111
112
|
const coverage = recordOrNull(raw.coverage);
|
|
112
113
|
const nextOffset = finiteOrNull(raw.nextOffset);
|
|
114
|
+
const companyFrom = readCompanyFrom(raw.companyFrom);
|
|
115
|
+
const { companyFrom: _unread, ...rest } = raw;
|
|
113
116
|
return {
|
|
114
|
-
...
|
|
117
|
+
...rest,
|
|
118
|
+
...(companyFrom ? { companyFrom } : {}),
|
|
115
119
|
ticker: text(raw.ticker) ?? ticker,
|
|
116
120
|
cik: cikOrNull(raw.cik) ?? "",
|
|
117
121
|
companyName: text(raw.companyName) ?? "",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type DataTableCell,
|
|
10
10
|
type DataTableKeyEvent,
|
|
11
11
|
} from "../../../components";
|
|
12
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
12
13
|
import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
|
|
13
14
|
import type { IssuerListingParams } from "../../../api-client/paths";
|
|
14
15
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
@@ -233,6 +234,8 @@ export function BeneficialOwnersView({
|
|
|
233
234
|
focused,
|
|
234
235
|
title: "13D/13G coverage",
|
|
235
236
|
});
|
|
237
|
+
// A foreign line's owners are its company's, read from the home line.
|
|
238
|
+
useCompanyDataFooter(payload?.companyFrom);
|
|
236
239
|
usePaneFooter("holders-13dg", () => ({
|
|
237
240
|
info: [
|
|
238
241
|
...(payload?.asOf ? [{ id: "as-of", parts: [{ text: `as of ${payload.asOf.slice(0, 10)}`, tone: "muted" as const }] }] : []),
|
|
@@ -7,7 +7,7 @@ import { loadHolderSnapshot } from "./client";
|
|
|
7
7
|
import { holderValueCurrency } from "./format";
|
|
8
8
|
import { logFailure } from "../../../utils/log-failure";
|
|
9
9
|
import type { HolderData } from "../../../types/financials";
|
|
10
|
-
import { SEC_FILINGS } from "../shared/report-freshness";
|
|
10
|
+
import { SEC_FILING_ROWS, SEC_FILINGS } from "../shared/report-freshness";
|
|
11
11
|
import type { BeneficialOwnersPayload } from "../../../api-client/beneficial-owners";
|
|
12
12
|
import { fetchBeneficialOwners, type BeneficialOwnersRequest } from "./beneficial-client";
|
|
13
13
|
import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "./report-rows";
|
|
35
35
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
36
36
|
import { fundHoldersAnswer } from "./fund-holders";
|
|
37
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
37
38
|
import {
|
|
38
39
|
HOLDER_ROWS_MAX,
|
|
39
40
|
indexedHoldersPartialNote,
|
|
@@ -197,7 +198,7 @@ export function createHoldersHeadless(
|
|
|
197
198
|
rows: limited,
|
|
198
199
|
// Reports that could not be read this time are missing from the rows.
|
|
199
200
|
complete: beneficialListComplete(payload),
|
|
200
|
-
freshness: { ...
|
|
201
|
+
freshness: { ...SEC_FILING_ROWS, basis: "13D/13G filings" },
|
|
201
202
|
metadata: {
|
|
202
203
|
symbol: payload.ticker || symbol,
|
|
203
204
|
name: payload.companyName || null,
|
|
@@ -212,6 +213,7 @@ export function createHoldersHeadless(
|
|
|
212
213
|
[payload.companyName || symbol, ...beneficialListFacts(payload, form, rows.length, history)].join(" · "),
|
|
213
214
|
...(legend ? [legend] : []),
|
|
214
215
|
...beneficialCoverageNotices(payload.coverage),
|
|
216
|
+
...companyDataNotes(payload.companyFrom),
|
|
215
217
|
],
|
|
216
218
|
returned: limited.length,
|
|
217
219
|
total: rows.length,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type DataTableKeyEvent,
|
|
13
13
|
type PageRequest,
|
|
14
14
|
} from "../../../components";
|
|
15
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
15
16
|
import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
|
|
16
17
|
import { useAsyncResource } from "../../../react/async-resource";
|
|
17
18
|
import { useShortcut } from "../../../react/input";
|
|
@@ -321,6 +322,8 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
321
322
|
}
|
|
322
323
|
}, [currency, fundMatches, marketCap]);
|
|
323
324
|
|
|
325
|
+
// Holders read from the company's home line say whose; the SEC index's rows are the listing's own.
|
|
326
|
+
useCompanyDataFooter(indexed ? null : listData?.companyFrom, viewMode !== "13dg");
|
|
324
327
|
// The 13D/G tab registers its own footer.
|
|
325
328
|
usePaneFooter("holders", () => {
|
|
326
329
|
if (viewMode === "13dg") return null;
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { HOLDER_ROWS_MAX } from "./index-holders";
|
|
26
26
|
import { buildRows, DEFAULT_SORT, sortRows } from "./table-model";
|
|
27
27
|
import type { HolderColumnId, SortPreference } from "./types";
|
|
28
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* A holder as `holders` and `fn HDS` report it; the keys are the JSON
|
|
@@ -231,7 +232,9 @@ export function holderReportNotices({
|
|
|
231
232
|
? `The source lists its ${reported} largest holders; fn 13F ${requested} lists all ${total.toLocaleString("en-US")}.`
|
|
232
233
|
: null;
|
|
233
234
|
return {
|
|
234
|
-
notices: [header, basis, cut, positionsBasis, holderShareBasisNote(data, shown), unvaluedHoldersNote(shown, sort)
|
|
235
|
+
notices: [header, basis, cut, positionsBasis, holderShareBasisNote(data, shown), unvaluedHoldersNote(shown, sort),
|
|
236
|
+
// The SEC index's rows are the listing's own; the source's, its company's home line's when it says so.
|
|
237
|
+
...(indexed ? [] : companyDataNotes(data.companyFrom))]
|
|
235
238
|
.filter((line): line is string => !!line),
|
|
236
239
|
valueBasis,
|
|
237
240
|
positionsBasis,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./model";
|
|
21
21
|
import { isAmendedInsiderFiling, relevantInsiderAmendments } from "./amendments";
|
|
22
22
|
import { formatInsiderName } from "./display";
|
|
23
|
-
import {
|
|
23
|
+
import { SEC_FILING_ROWS } from "../shared/report-freshness";
|
|
24
24
|
import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "../shared/headless-market-data";
|
|
25
25
|
import { noSecReportsNotice } from "../../../utils/sec";
|
|
26
26
|
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
@@ -329,7 +329,7 @@ export function createInsiderHeadless(
|
|
|
329
329
|
): HeadlessPaneDefinition<"rows"> {
|
|
330
330
|
return {
|
|
331
331
|
shape: "rows",
|
|
332
|
-
freshness: { ...
|
|
332
|
+
freshness: { ...SEC_FILING_ROWS, basis: "Form 4 filings" },
|
|
333
333
|
argument: {
|
|
334
334
|
kind: "ticker",
|
|
335
335
|
optional: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
-
import type { IpoCalendarParams, IpoCalendarPayload, IpoDeal, IpoSourceHealth } from "../../../api-client/ipo";
|
|
2
|
+
import type { IpoCalendarParams, IpoCalendarPayload, IpoDeal, IpoLockup, IpoSourceHealth } from "../../../api-client/ipo";
|
|
3
3
|
import type { HeadlessPaneApiClient } from "../../../types/plugin";
|
|
4
4
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
5
5
|
import { cachedCloudResource, loadCloudResource, unavailableOnServer, type CloudResource } from "../shared/cloud-resource";
|
|
@@ -45,6 +45,21 @@ function validDeal(deal: IpoDeal): boolean {
|
|
|
45
45
|
&& validFirstDay(deal.firstDay);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function validLockup(value: unknown): value is IpoLockup {
|
|
49
|
+
const lockup = value as IpoLockup | null;
|
|
50
|
+
return !!lockup && typeof lockup === "object" && typeof lockup.endsOn === "string" && dayOrNull(lockup.endsOn)
|
|
51
|
+
&& typeof lockup.days === "number" && Number.isFinite(lockup.days) && lockup.days > 0
|
|
52
|
+
&& (lockup.basis === "prospectus" || lockup.basis === "typical")
|
|
53
|
+
&& dayOrNull(lockup.prospectusDate ?? null) && textOrNull(lockup.prospectusUrl ?? null);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A lock-up estimate the client cannot read is left off its deal; the deal stays. */
|
|
57
|
+
function withReadableLockup(deal: IpoDeal): IpoDeal {
|
|
58
|
+
if (deal.lockup == null || validLockup(deal.lockup)) return deal;
|
|
59
|
+
const { lockup: _unreadable, ...rest } = deal;
|
|
60
|
+
return rest;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
function validSource(source: IpoSourceHealth): boolean {
|
|
49
64
|
return !!source && text(source.id) && Array.isArray(source.mics) && source.mics.every(text)
|
|
50
65
|
&& typeof source.ok === "boolean" && textOrNull(source.asOf);
|
|
@@ -61,7 +76,8 @@ export function validateIpoCalendar(data: IpoCalendarPayload): IpoCalendarPayloa
|
|
|
61
76
|
}
|
|
62
77
|
return {
|
|
63
78
|
asOf: data.asOf,
|
|
64
|
-
deals: data.deals.filter(validDeal).map(
|
|
79
|
+
deals: data.deals.filter(validDeal).map(withReadableLockup)
|
|
80
|
+
.map((deal) => (REGIONS.has(deal.region) ? deal : { ...deal, region: "other" })),
|
|
65
81
|
sources: data.sources.filter(validSource),
|
|
66
82
|
};
|
|
67
83
|
}
|
|
@@ -9,11 +9,13 @@ import { fetchIpoCalendar } from "./client";
|
|
|
9
9
|
import {
|
|
10
10
|
calendarDate,
|
|
11
11
|
filterIpoDeals,
|
|
12
|
+
formatIpoLockupEnd,
|
|
12
13
|
formatIpoPrice,
|
|
13
14
|
formatIpoReturn,
|
|
14
15
|
formatIpoSize,
|
|
15
16
|
IPO_STATUSES,
|
|
16
17
|
ipoBoardStatus,
|
|
18
|
+
ipoKeyword,
|
|
17
19
|
isIpoTab,
|
|
18
20
|
marketsBehind,
|
|
19
21
|
marketsBehindText,
|
|
@@ -21,7 +23,10 @@ import {
|
|
|
21
23
|
DEFAULT_IPO_SORT,
|
|
22
24
|
type IpoTab,
|
|
23
25
|
} from "./model";
|
|
26
|
+
import { ipoDetailSentences, loadIpoLockupNews, type IpoLockupStory } from "./listing-ipo";
|
|
24
27
|
import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
|
|
28
|
+
import { DATE_OPTION_EXAMPLES, isoDateOption } from "../../../utils/calendar-date";
|
|
29
|
+
import { logFailure } from "../../../utils/log-failure";
|
|
25
30
|
|
|
26
31
|
const COLUMNS: HeadlessPaneColumn[] = [
|
|
27
32
|
{ key: "ticker", header: "Ticker" },
|
|
@@ -33,6 +38,8 @@ const COLUMNS: HeadlessPaneColumn[] = [
|
|
|
33
38
|
{ key: "price", header: "Price", align: "right" },
|
|
34
39
|
{ key: "offerSizeUsd", header: "Size", align: "right", format: (value) => formatIpoSize(value as number | null) },
|
|
35
40
|
{ key: "firstDayReturn", header: "Return", align: "right", format: (value) => formatIpoReturn(value as number | null) },
|
|
41
|
+
// A US IPO's estimated lock-up end: "~2026-11-02".
|
|
42
|
+
{ key: "lockupEnds", header: "Lockup ends", format: (value) => formatIpoLockupEnd(typeof value === "string" ? value : null) },
|
|
36
43
|
];
|
|
37
44
|
|
|
38
45
|
type StatusOption = IpoStatus | "all";
|
|
@@ -68,7 +75,7 @@ function toRow(deal: IpoDeal, now: Date) {
|
|
|
68
75
|
status: ipoBoardStatus(deal, now),
|
|
69
76
|
listingType: deal.listingType,
|
|
70
77
|
currency: deal.currency,
|
|
71
|
-
price: formatIpoPrice(deal),
|
|
78
|
+
price: formatIpoPrice(deal, { full: true }),
|
|
72
79
|
priceLow: deal.priceLow,
|
|
73
80
|
priceHigh: deal.priceHigh,
|
|
74
81
|
offerPrice: deal.offerPrice,
|
|
@@ -78,26 +85,60 @@ function toRow(deal: IpoDeal, now: Date) {
|
|
|
78
85
|
firstDayClose: deal.firstDay?.close ?? null,
|
|
79
86
|
firstDayReturn: deal.firstDay?.returnPct ?? null,
|
|
80
87
|
filedDate: deal.filedDate,
|
|
88
|
+
lockupEnds: deal.lockup?.endsOn ?? null,
|
|
89
|
+
lockupBasis: deal.lockup?.basis ?? null,
|
|
90
|
+
prospectusDate: deal.lockup?.prospectusDate ?? null,
|
|
81
91
|
};
|
|
82
92
|
}
|
|
83
93
|
|
|
84
|
-
|
|
94
|
+
/** One deal found: its IPO in words, as `ticker` gives it, with its price now and its lock-up news. */
|
|
95
|
+
interface IpoDealDetail {
|
|
96
|
+
price: number | null;
|
|
97
|
+
news: IpoLockupStory[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Lock-up estimates soonest first, then the ones that have ended, latest first. */
|
|
101
|
+
function byLockupEnd(deals: readonly IpoDeal[], now: Date): IpoDeal[] {
|
|
102
|
+
const today = now.toISOString().slice(0, 10);
|
|
103
|
+
const rank = (deal: IpoDeal) => (deal.lockup!.endsOn >= today ? 0 : 1);
|
|
104
|
+
return [...deals].sort((a, b) => rank(a) - rank(b)
|
|
105
|
+
|| (rank(a) === 0 ? a.lockup!.endsOn.localeCompare(b.lockup!.endsOn) : b.lockup!.endsOn.localeCompare(a.lockup!.endsOn))
|
|
106
|
+
|| a.id.localeCompare(b.id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The most rows `--limit` lists. */
|
|
110
|
+
const MAX_ROWS = 500;
|
|
111
|
+
|
|
112
|
+
const SPAC_NOTE = "A SPAC's merger with its target (de-SPAC) is no IPO, so the calendar lists the SPACs' own IPOs.";
|
|
113
|
+
|
|
114
|
+
export function projectIpoCalendarHeadless(
|
|
115
|
+
payload: IpoCalendarPayload,
|
|
116
|
+
args: HeadlessPaneLoadArgs,
|
|
117
|
+
detail: IpoDealDetail | null = null,
|
|
118
|
+
now = new Date(),
|
|
119
|
+
): HeadlessRowsResult {
|
|
85
120
|
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
86
121
|
const region = regionOption(args);
|
|
87
122
|
const status = statusOption(args);
|
|
88
123
|
const limit = Number(args.options.limit ?? 50);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
DEFAULT_IPO_SORT,
|
|
93
|
-
now,
|
|
94
|
-
);
|
|
124
|
+
const keyword = ipoKeyword(query);
|
|
125
|
+
const found = filterIpoDeals(payload.deals, region, query, now).filter((deal) => status === "all" || deal.status === status);
|
|
126
|
+
const matching = keyword === "lockup" ? byLockupEnd(found, now) : sortIpoDeals(found, DEFAULT_IPO_SORT, now);
|
|
95
127
|
const rows = matching.slice(0, limit).map((deal) => toRow(deal, now));
|
|
96
128
|
const behind = marketsBehindText(marketsBehind(payload.sources, region, payload.deals));
|
|
129
|
+
const single = matching.length === 1 && query && !keyword ? matching[0]! : null;
|
|
130
|
+
const notes = [
|
|
131
|
+
...rows.length < matching.length
|
|
132
|
+
? [`Showing ${rows.length} of ${matching.length} deals; --limit ${Math.min(matching.length, MAX_ROWS)} lists ${matching.length <= MAX_ROWS ? "them all" : "more"}.`]
|
|
133
|
+
: [],
|
|
134
|
+
...single ? ipoDetailSentences(single, detail?.price ?? null, detail?.news ?? [], now) : [],
|
|
135
|
+
...keyword === "spac" || keyword === "de-spac" ? [SPAC_NOTE] : [],
|
|
136
|
+
];
|
|
97
137
|
return {
|
|
98
138
|
columns: COLUMNS,
|
|
99
139
|
rows,
|
|
100
140
|
...(behind ? { errors: [behind] } : {}),
|
|
141
|
+
...(notes.length ? { notes } : {}),
|
|
101
142
|
metadata: {
|
|
102
143
|
asOf: payload.asOf,
|
|
103
144
|
total: matching.length,
|
|
@@ -149,7 +190,15 @@ export const ipoCalendarHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
149
190
|
type: "integer",
|
|
150
191
|
defaultValue: 50,
|
|
151
192
|
minimum: 1,
|
|
152
|
-
maximum:
|
|
193
|
+
maximum: MAX_ROWS,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "from",
|
|
197
|
+
type: "string",
|
|
198
|
+
placeholder: "date",
|
|
199
|
+
description: `First listing date, as ${DATE_OPTION_EXAMPLES}; the calendar keeps a year of listings.`,
|
|
200
|
+
example: "--from 2025-11-01",
|
|
201
|
+
normalize: (value) => isoDateOption(value, "from"),
|
|
153
202
|
},
|
|
154
203
|
],
|
|
155
204
|
columns: COLUMNS,
|
|
@@ -164,7 +213,22 @@ export const ipoCalendarHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
164
213
|
async load(args, ctx) {
|
|
165
214
|
// The default window has no filed or withdrawn deals, so a stage is asked for by name.
|
|
166
215
|
const status = statusOption(args);
|
|
167
|
-
const
|
|
168
|
-
|
|
216
|
+
const from = typeof args.options.from === "string" && args.options.from ? args.options.from : undefined;
|
|
217
|
+
const params = { ...(status === "all" ? {} : { status }), ...(from ? { from } : {}) };
|
|
218
|
+
const payload = await fetchIpoCalendar(ctx.apiClient, ctx.signal, params);
|
|
219
|
+
const projected = projectIpoCalendarHeadless(payload, args);
|
|
220
|
+
// One deal found: its price now and its lock-up news, which never fail the report.
|
|
221
|
+
const query = typeof args.argument === "string" ? args.argument.trim() : "";
|
|
222
|
+
const matched = projected.rows.length === 1 && query && !ipoKeyword(query)
|
|
223
|
+
? payload.deals.find((deal) => deal.id === projected.rows[0]!.id) : undefined;
|
|
224
|
+
if (!matched?.symbol) return projected;
|
|
225
|
+
const [price, news] = await Promise.all([
|
|
226
|
+
matched.status === "listed"
|
|
227
|
+
? ctx.marketData.getQuote(matched.symbol, matched.exchange ?? undefined).then((quote) => quote.price ?? null,
|
|
228
|
+
logFailure("plugins.ipo-calendar", "Load the price of an IPO failed", null))
|
|
229
|
+
: Promise.resolve(null),
|
|
230
|
+
loadIpoLockupNews(ctx.apiClient, matched).catch(logFailure("plugins.ipo-calendar", "Load an IPO's lock-up news failed", [] as IpoLockupStory[])),
|
|
231
|
+
]);
|
|
232
|
+
return projectIpoCalendarHeadless(payload, args, { price, news });
|
|
169
233
|
},
|
|
170
234
|
};
|