gloomberb 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +70 -0
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +17 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +478 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Text } from "../../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GloomPlugin } from "../../../types/plugin";
|
|
4
4
|
import type { SecFilingItem } from "../../../types/data-provider";
|
|
5
5
|
import {
|
|
6
6
|
useResolvedEntryValue,
|
|
@@ -14,50 +14,19 @@ import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTa
|
|
|
14
14
|
import { usePluginPaneState } from "../../plugin-runtime";
|
|
15
15
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
16
16
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
17
|
-
import { parseForm4Xml,
|
|
17
|
+
import { parseForm4Xml, type InsiderTransaction } from "./insider-data";
|
|
18
18
|
import { createTickerSurfacePaneTemplate } from "../ticker-surface";
|
|
19
|
+
import {
|
|
20
|
+
buildInsiderTransactionDetailBody,
|
|
21
|
+
buildInsiderTransactionTitle,
|
|
22
|
+
formatFilingFormLabel,
|
|
23
|
+
formatFilingShortDate,
|
|
24
|
+
renderFilingNotice,
|
|
25
|
+
} from "../sec-filing-display";
|
|
19
26
|
|
|
20
27
|
const FORM4_LIMIT = 20;
|
|
21
28
|
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
22
29
|
|
|
23
|
-
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
24
|
-
|
|
25
|
-
function formatDate(d: Date | string | number): string {
|
|
26
|
-
const date = d instanceof Date ? d : new Date(d);
|
|
27
|
-
if (isNaN(date.getTime())) return "—";
|
|
28
|
-
return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function wrapLines(text: string, width: number): string[] {
|
|
32
|
-
const words = text.trim().split(/\s+/);
|
|
33
|
-
const lines: string[] = [];
|
|
34
|
-
let current = "";
|
|
35
|
-
for (const word of words) {
|
|
36
|
-
if (!current) { current = word; continue; }
|
|
37
|
-
if (current.length + 1 + word.length <= width) {
|
|
38
|
-
current = `${current} ${word}`;
|
|
39
|
-
} else {
|
|
40
|
-
lines.push(current);
|
|
41
|
-
current = word;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (current) lines.push(current);
|
|
45
|
-
return lines;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function renderNotice(message: string, width: number) {
|
|
49
|
-
const lines = wrapLines(message, width - 4);
|
|
50
|
-
return (
|
|
51
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
52
|
-
{lines.map((line, i) => (
|
|
53
|
-
<Box key={i} height={1}>
|
|
54
|
-
<Text fg={colors.textDim}>{line}</Text>
|
|
55
|
-
</Box>
|
|
56
|
-
))}
|
|
57
|
-
</Box>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
30
|
interface ParsedFiling {
|
|
62
31
|
filing: SecFilingItem;
|
|
63
32
|
transaction: InsiderTransaction | null;
|
|
@@ -99,46 +68,18 @@ function truncateText(text: string, width: number): string {
|
|
|
99
68
|
return `${text.slice(0, width - 3)}...`;
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
function formatFilingForm(form: string): string {
|
|
103
|
-
const value = form.trim();
|
|
104
|
-
return value ? `FORM ${value}` : "FORM 4";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildTransactionTitle(transaction: InsiderTransaction): string {
|
|
108
|
-
const type = transactionTypeLabel(transaction.transactionType);
|
|
109
|
-
const price = transaction.pricePerShare != null
|
|
110
|
-
? ` @ ${formatCurrency(transaction.pricePerShare)}`
|
|
111
|
-
: "";
|
|
112
|
-
const value = transaction.totalValue != null
|
|
113
|
-
? ` | ${formatCurrency(transaction.totalValue)}`
|
|
114
|
-
: "";
|
|
115
|
-
return `${type} ${formatCompact(transaction.shares)} shares${price}${value}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildTransactionDetailBody(transaction: InsiderTransaction): string {
|
|
119
|
-
const lines = [
|
|
120
|
-
`Transaction: ${transactionTypeLabel(transaction.transactionType)}`,
|
|
121
|
-
`Date: ${formatDate(transaction.filingDate)}`,
|
|
122
|
-
`Shares: ${formatCompact(transaction.shares)}`,
|
|
123
|
-
`Price/Share: ${transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}`,
|
|
124
|
-
`Total Value: ${transaction.totalValue != null ? formatCurrency(transaction.totalValue) : "—"}`,
|
|
125
|
-
`Shares Owned After: ${transaction.sharesOwned != null ? formatCompact(transaction.sharesOwned) : "—"}`,
|
|
126
|
-
];
|
|
127
|
-
return lines.join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
71
|
function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
131
72
|
return parsed.map(({ filing, transaction, isLoading }) => {
|
|
132
73
|
const filingMeta = [
|
|
133
|
-
`Filed ${
|
|
74
|
+
`Filed ${formatFilingShortDate(filing.filingDate)}`,
|
|
134
75
|
`Accession ${filing.accessionNumber}`,
|
|
135
|
-
|
|
76
|
+
formatFilingFormLabel(filing.form),
|
|
136
77
|
];
|
|
137
78
|
|
|
138
79
|
if (!transaction) {
|
|
139
80
|
return {
|
|
140
81
|
id: filing.accessionNumber,
|
|
141
|
-
eyebrow:
|
|
82
|
+
eyebrow: formatFilingFormLabel(filing.form),
|
|
142
83
|
title: isLoading ? "Loading Form 4 filing..." : "Form 4 transaction unavailable",
|
|
143
84
|
timestamp: filing.filingDate,
|
|
144
85
|
detailTitle: isLoading ? "Loading Form 4 filing..." : "Form 4 transaction unavailable",
|
|
@@ -152,7 +93,7 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
|
152
93
|
return {
|
|
153
94
|
id: filing.accessionNumber,
|
|
154
95
|
eyebrow: transaction.reportedName,
|
|
155
|
-
title:
|
|
96
|
+
title: buildInsiderTransactionTitle(transaction),
|
|
156
97
|
timestamp: transaction.filingDate,
|
|
157
98
|
preview: transaction.title || undefined,
|
|
158
99
|
detailTitle: transaction.reportedName,
|
|
@@ -160,12 +101,12 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
|
160
101
|
...(transaction.title ? [transaction.title] : []),
|
|
161
102
|
...filingMeta,
|
|
162
103
|
],
|
|
163
|
-
detailBody:
|
|
104
|
+
detailBody: buildInsiderTransactionDetailBody(transaction),
|
|
164
105
|
};
|
|
165
106
|
});
|
|
166
107
|
}
|
|
167
108
|
|
|
168
|
-
|
|
109
|
+
function InsiderView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
|
|
169
110
|
const { ticker } = usePaneTicker();
|
|
170
111
|
const tickerKey = ticker?.metadata.ticker ?? "none";
|
|
171
112
|
const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>(`insider:selectedIdx:${tickerKey}`, 0);
|
|
@@ -290,18 +231,18 @@ export function InsiderView({ width, height, focused }: Pick<DetailTabProps, "wi
|
|
|
290
231
|
registrationId: "insider",
|
|
291
232
|
focused,
|
|
292
233
|
url: error ? null : openFiling?.filingUrl,
|
|
293
|
-
source: openFiling?.form ?
|
|
234
|
+
source: openFiling?.form ? formatFilingFormLabel(openFiling.form) : null,
|
|
294
235
|
info: footerInfo,
|
|
295
236
|
hints: footerHints,
|
|
296
237
|
label: "filing",
|
|
297
238
|
});
|
|
298
239
|
|
|
299
240
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view insider activity.</Text>;
|
|
300
|
-
if (!eligibleTicker) return
|
|
241
|
+
if (!eligibleTicker) return renderFilingNotice("Insider transactions are only shown for US equities.", width);
|
|
301
242
|
if (loading && allFilings.length === 0) return <Spinner label="Loading insider filings..." />;
|
|
302
|
-
if (error) return
|
|
243
|
+
if (error) return renderFilingNotice(`Error: ${error}`, width);
|
|
303
244
|
if (!loading && form4Filings.length === 0) {
|
|
304
|
-
return
|
|
245
|
+
return renderFilingNotice(`No Form 4 filings found for ${ticker.metadata.ticker}.`, width);
|
|
305
246
|
}
|
|
306
247
|
|
|
307
248
|
return (
|
|
@@ -321,14 +262,6 @@ export function InsiderView({ width, height, focused }: Pick<DetailTabProps, "wi
|
|
|
321
262
|
);
|
|
322
263
|
}
|
|
323
264
|
|
|
324
|
-
function InsiderTab(props: DetailTabProps) {
|
|
325
|
-
return <InsiderView {...props} />;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function InsiderPane({ focused, width, height }: PaneProps) {
|
|
329
|
-
return <InsiderView focused={focused} width={width} height={height} />;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
265
|
export const insiderPlugin: GloomPlugin = {
|
|
333
266
|
id: "insider",
|
|
334
267
|
name: "Insider Trading",
|
|
@@ -340,7 +273,7 @@ export const insiderPlugin: GloomPlugin = {
|
|
|
340
273
|
id: "insider",
|
|
341
274
|
name: "Insider",
|
|
342
275
|
icon: "I",
|
|
343
|
-
component:
|
|
276
|
+
component: InsiderView,
|
|
344
277
|
defaultPosition: "right",
|
|
345
278
|
defaultMode: "floating",
|
|
346
279
|
defaultFloatingSize: { width: 100, height: 30 },
|
|
@@ -364,7 +297,7 @@ export const insiderPlugin: GloomPlugin = {
|
|
|
364
297
|
id: "insider",
|
|
365
298
|
name: "Insider",
|
|
366
299
|
order: 47,
|
|
367
|
-
component:
|
|
300
|
+
component: InsiderView,
|
|
368
301
|
isVisible: ({ ticker }) => isUsEquityTicker(ticker),
|
|
369
302
|
});
|
|
370
303
|
},
|
|
@@ -191,7 +191,7 @@ const INDEX_SHORT: Record<string, string> = {
|
|
|
191
191
|
"^RUT": "RUT",
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
195
195
|
const dataProvider = useAssetData();
|
|
196
196
|
const { pinTicker } = usePluginTickerActions();
|
|
197
197
|
const [activeTab, setActiveTab] = useState<TabId>("gainers");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import type { PaneProps } from "../../../types/plugin";
|
|
1
2
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
3
|
+
import type { NewsQuery } from "../../../news/types";
|
|
2
4
|
import { createRssNewsCapability } from "./rss-source";
|
|
3
|
-
import { TopPane } from "./top-pane";
|
|
4
|
-
import { FeedPane } from "./feed-pane";
|
|
5
5
|
import { IndustryPane } from "./industry-pane";
|
|
6
6
|
import { BreakingPane } from "./breaking-pane";
|
|
7
7
|
import {
|
|
@@ -15,6 +15,45 @@ import {
|
|
|
15
15
|
loadNewsFeedSettings,
|
|
16
16
|
saveNewsFeedSettings,
|
|
17
17
|
} from "./feed-config";
|
|
18
|
+
import { NEWS_QUERY_PRESETS } from "./news-query-presets";
|
|
19
|
+
import { NewsPresetPane } from "./news-preset-pane";
|
|
20
|
+
import type { NewsColumnId, NewsSortPreference } from "./news-table";
|
|
21
|
+
|
|
22
|
+
interface NewsPresetPaneConfig {
|
|
23
|
+
paneKey: string;
|
|
24
|
+
title: string;
|
|
25
|
+
query: NewsQuery;
|
|
26
|
+
columns: NewsColumnId[];
|
|
27
|
+
defaultSort: NewsSortPreference;
|
|
28
|
+
emptyStateTitle: string;
|
|
29
|
+
emptyStateHint: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createNewsPresetPane(config: NewsPresetPaneConfig) {
|
|
33
|
+
return function PresetNewsPane(props: PaneProps) {
|
|
34
|
+
return <NewsPresetPane {...props} {...config} />;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const TopPane = createNewsPresetPane({
|
|
39
|
+
paneKey: "top",
|
|
40
|
+
title: "Top News",
|
|
41
|
+
query: NEWS_QUERY_PRESETS.top,
|
|
42
|
+
columns: ["rank", "time", "source", "title", "tickers", "importance"],
|
|
43
|
+
defaultSort: { columnId: "importance", direction: "desc" },
|
|
44
|
+
emptyStateTitle: "No top stories yet",
|
|
45
|
+
emptyStateHint: "Try refreshing later as new headlines are ranked.",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const FeedPane = createNewsPresetPane({
|
|
49
|
+
paneKey: "feed",
|
|
50
|
+
title: "News Feed",
|
|
51
|
+
query: NEWS_QUERY_PRESETS.feed,
|
|
52
|
+
columns: ["time", "source", "title", "tickers", "categories"],
|
|
53
|
+
defaultSort: { columnId: "time", direction: "desc" },
|
|
54
|
+
emptyStateTitle: "No feed stories yet",
|
|
55
|
+
emptyStateHint: "Try refreshing later as wire stories arrive.",
|
|
56
|
+
});
|
|
18
57
|
|
|
19
58
|
export function registerNewsWireFeatures(ctx: GloomPluginContext): () => void {
|
|
20
59
|
setDigestPersistence(ctx.persistence);
|
|
@@ -3,8 +3,11 @@ import { TextAttributes } from "../../../ui";
|
|
|
3
3
|
import {
|
|
4
4
|
DataTableStackView,
|
|
5
5
|
TickerBadgeList,
|
|
6
|
+
activeStackIndex,
|
|
7
|
+
sortStackItems,
|
|
6
8
|
type DataTableCell,
|
|
7
9
|
type DataTableColumn,
|
|
10
|
+
type StackSortPreference,
|
|
8
11
|
} from "../../../components";
|
|
9
12
|
import type { MarketNewsItem } from "../../../types/news-source";
|
|
10
13
|
import { colors } from "../../../theme/colors";
|
|
@@ -13,10 +16,7 @@ import { useInlineTickers } from "../../../state/use-inline-tickers";
|
|
|
13
16
|
|
|
14
17
|
export type NewsColumnId = "rank" | "time" | "source" | "title" | "tickers" | "categories" | "importance";
|
|
15
18
|
|
|
16
|
-
export
|
|
17
|
-
columnId: NewsColumnId;
|
|
18
|
-
direction: "asc" | "desc";
|
|
19
|
-
}
|
|
19
|
+
export type NewsSortPreference = StackSortPreference<NewsColumnId>;
|
|
20
20
|
|
|
21
21
|
type NewsTableColumn = DataTableColumn & { id: NewsColumnId };
|
|
22
22
|
|
|
@@ -78,12 +78,12 @@ export function sortNewsArticles(
|
|
|
78
78
|
articles: MarketNewsItem[],
|
|
79
79
|
preference: NewsSortPreference,
|
|
80
80
|
): MarketNewsItem[] {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
return sortStackItems(
|
|
82
|
+
articles,
|
|
83
|
+
preference,
|
|
84
|
+
compareArticle,
|
|
85
|
+
(a, b) => b.publishedAt.getTime() - a.publishedAt.getTime(),
|
|
86
|
+
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function nextSortPreference(current: NewsSortPreference, columnId: NewsColumnId): NewsSortPreference {
|
|
@@ -190,7 +190,7 @@ export function NewsArticleStackView({
|
|
|
190
190
|
const { catalog: tickerCatalog, openTicker } = useInlineTickers(tableTickerTexts);
|
|
191
191
|
const columns = useMemo(() => buildColumns(width, columnIds), [columnIds, width]);
|
|
192
192
|
const selectedIdx = sortedArticles.findIndex((article) => article.id === selectedArticleId);
|
|
193
|
-
const activeIdx =
|
|
193
|
+
const activeIdx = activeStackIndex(sortedArticles.length, selectedIdx);
|
|
194
194
|
|
|
195
195
|
const selectIndex = useCallback((index: number) => {
|
|
196
196
|
setSelectedArticleId(sortedArticles[index]?.id ?? null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from "../../ui";
|
|
2
2
|
import { useRef, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GloomPlugin } from "../../types/plugin";
|
|
4
4
|
import { usePaneTicker } from "../../state/app-context";
|
|
5
5
|
import { colors } from "../../theme/colors";
|
|
6
6
|
import type { NewsArticle } from "../../types/news-source";
|
|
@@ -15,10 +15,6 @@ import { usePersistedNewsArticles } from "./news-wire/persisted-articles";
|
|
|
15
15
|
import { useNewsReadState } from "./news-wire/read-state";
|
|
16
16
|
import { createTickerSurfacePaneTemplate } from "./ticker-surface";
|
|
17
17
|
|
|
18
|
-
const ARTICLE_SUMMARY_CACHE_POLICY = {
|
|
19
|
-
staleMs: 30 * 24 * 60 * 60_000,
|
|
20
|
-
expireMs: 90 * 24 * 60 * 60_000,
|
|
21
|
-
};
|
|
22
18
|
const NEWS_ITEM_LIMIT = 50;
|
|
23
19
|
let disposeNewsWireFeatures: (() => void) | null = null;
|
|
24
20
|
|
|
@@ -56,7 +52,7 @@ function getFeedItems(
|
|
|
56
52
|
});
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
function TickerNewsView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
|
|
60
56
|
const { ticker } = usePaneTicker();
|
|
61
57
|
const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
|
|
62
58
|
const [selectedIdx, setSelectedIdx] = useDebouncedPluginPaneState<number>(selectionKey, 0);
|
|
@@ -151,14 +147,6 @@ export function TickerNewsView({ width, height, focused }: Pick<DetailTabProps,
|
|
|
151
147
|
);
|
|
152
148
|
}
|
|
153
149
|
|
|
154
|
-
function NewsTab(props: DetailTabProps) {
|
|
155
|
-
return <TickerNewsView {...props} />;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function TickerNewsPane({ focused, width, height }: PaneProps) {
|
|
159
|
-
return <TickerNewsView focused={focused} width={width} height={height} />;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
150
|
export const newsPlugin: GloomPlugin = {
|
|
163
151
|
id: "news",
|
|
164
152
|
name: "News",
|
|
@@ -171,7 +159,7 @@ export const newsPlugin: GloomPlugin = {
|
|
|
171
159
|
id: "ticker-news",
|
|
172
160
|
name: "Ticker News",
|
|
173
161
|
icon: "C",
|
|
174
|
-
component:
|
|
162
|
+
component: TickerNewsView,
|
|
175
163
|
defaultPosition: "right",
|
|
176
164
|
defaultMode: "floating",
|
|
177
165
|
defaultFloatingSize: { width: 100, height: 32 },
|
|
@@ -194,7 +182,7 @@ export const newsPlugin: GloomPlugin = {
|
|
|
194
182
|
id: "news",
|
|
195
183
|
name: "News",
|
|
196
184
|
order: 40,
|
|
197
|
-
component:
|
|
185
|
+
component: TickerNewsView,
|
|
198
186
|
});
|
|
199
187
|
disposeNewsWireFeatures = registerNewsWireFeatures(ctx);
|
|
200
188
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, Input, ScrollBox, Text
|
|
1
|
+
import { Box, Input, ScrollBox, Text } from "../../ui";
|
|
2
2
|
import { useRef, useEffect, useState, useCallback } from "react";
|
|
3
3
|
import { useShortcut } from "../../react/input";
|
|
4
4
|
import { type InputRenderable, type TextareaRenderable } from "../../ui";
|
|
@@ -7,8 +7,8 @@ import { usePaneTicker } from "../../state/app-context";
|
|
|
7
7
|
import { colors } from "../../theme/colors";
|
|
8
8
|
import { MarkdownEditor } from "../../components/markdown-editor";
|
|
9
9
|
import { MarkdownText } from "../../components/markdown-text";
|
|
10
|
-
import {
|
|
11
|
-
import { type PromptContext, useDialog
|
|
10
|
+
import { ConfirmDialog, Tabs, usePaneFooter } from "../../components";
|
|
11
|
+
import { type PromptContext, useDialog } from "../../ui/dialog";
|
|
12
12
|
import { NotesFiles, type QuickNoteEntry } from "./notes-files";
|
|
13
13
|
|
|
14
14
|
function useSyncedText(initialValue = "") {
|
|
@@ -50,6 +50,10 @@ function formatLastEdited(updatedAt: number | undefined): string {
|
|
|
50
50
|
return `${Math.floor(elapsedDays / 365)}y ago`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function formatDeleteNoteTitle(title: string): string {
|
|
54
|
+
return title.length > 28 ? `${title.slice(0, 25)}...` : title;
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
function MarkdownNotePreview({
|
|
54
58
|
text,
|
|
55
59
|
width,
|
|
@@ -82,56 +86,6 @@ function MarkdownNotePreview({
|
|
|
82
86
|
);
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
function ConfirmDeleteNoteDialog({
|
|
86
|
-
resolve,
|
|
87
|
-
title,
|
|
88
|
-
}: PromptContext<boolean> & {
|
|
89
|
-
title: string;
|
|
90
|
-
}) {
|
|
91
|
-
const confirm = useCallback(() => resolve(true), [resolve]);
|
|
92
|
-
const cancel = useCallback(() => resolve(false), [resolve]);
|
|
93
|
-
const displayTitle = title.length > 28 ? `${title.slice(0, 25)}...` : title;
|
|
94
|
-
|
|
95
|
-
useDialogKeyboard((event) => {
|
|
96
|
-
event.stopPropagation();
|
|
97
|
-
if (event.name === "enter" || event.name === "return") {
|
|
98
|
-
confirm();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (event.name === "escape") {
|
|
102
|
-
cancel();
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<DialogFrame title="Delete note?" footer="Enter delete · Esc cancel">
|
|
108
|
-
<Box flexDirection="column" width={44}>
|
|
109
|
-
<Text fg={colors.text}>{`Delete "${displayTitle}"?`}</Text>
|
|
110
|
-
<Box height={1} />
|
|
111
|
-
<Text fg={colors.textDim}>This note has content.</Text>
|
|
112
|
-
<Text fg={colors.textDim}>Deleting it cannot be undone.</Text>
|
|
113
|
-
<Box height={1} />
|
|
114
|
-
<Box flexDirection="row" gap={1}>
|
|
115
|
-
<Box
|
|
116
|
-
backgroundColor={colors.negative}
|
|
117
|
-
onMouseDown={confirm}
|
|
118
|
-
data-gloom-interactive="true"
|
|
119
|
-
>
|
|
120
|
-
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{" Delete "}</Text>
|
|
121
|
-
</Box>
|
|
122
|
-
<Box
|
|
123
|
-
backgroundColor={colors.panel}
|
|
124
|
-
onMouseDown={cancel}
|
|
125
|
-
data-gloom-interactive="true"
|
|
126
|
-
>
|
|
127
|
-
<Text fg={colors.text}>{" Cancel "}</Text>
|
|
128
|
-
</Box>
|
|
129
|
-
</Box>
|
|
130
|
-
</Box>
|
|
131
|
-
</DialogFrame>
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
89
|
function createNotesTab(notesFiles: NotesFiles) {
|
|
136
90
|
return function NotesTab({ focused, width, onCapture }: DetailTabProps) {
|
|
137
91
|
const { ticker } = usePaneTicker();
|
|
@@ -383,9 +337,18 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
|
|
|
383
337
|
const confirmed = await dialog.prompt<boolean>({
|
|
384
338
|
closeOnClickOutside: true,
|
|
385
339
|
content: (ctx: PromptContext<boolean>) => (
|
|
386
|
-
<
|
|
340
|
+
<ConfirmDialog
|
|
387
341
|
{...ctx}
|
|
388
|
-
title=
|
|
342
|
+
title="Delete note?"
|
|
343
|
+
body={[
|
|
344
|
+
`Delete "${formatDeleteNoteTitle(tab?.title ?? "Note")}"?`,
|
|
345
|
+
"This note has content.",
|
|
346
|
+
"Deleting it cannot be undone.",
|
|
347
|
+
]}
|
|
348
|
+
confirmLabel="Delete"
|
|
349
|
+
cancelLabel="Cancel"
|
|
350
|
+
width={44}
|
|
351
|
+
footer="Enter delete · Esc cancel"
|
|
389
352
|
/>
|
|
390
353
|
),
|
|
391
354
|
}).catch(() => false);
|
|
@@ -7,9 +7,28 @@ import { createTestDataProvider } from "../../test-support/data-provider";
|
|
|
7
7
|
import { cloneLayout, createDefaultConfig } from "../../types/config";
|
|
8
8
|
import type { OptionContract, OptionsChain, TickerFinancials } from "../../types/financials";
|
|
9
9
|
import type { TickerRecord } from "../../types/ticker";
|
|
10
|
-
import {
|
|
10
|
+
import { applyTheme, colors, hoverBg } from "../../theme/colors";
|
|
11
|
+
import { blendHex, contrastRatio } from "../../theme/color-utils";
|
|
12
|
+
import { DEFAULT_THEME, themes } from "../../theme/themes";
|
|
13
|
+
import { OptionsView, optionColumnColor, optionMutedColor, type OptionColumnId } from "./options";
|
|
11
14
|
|
|
12
15
|
const TEST_PANE_ID = "ticker-detail:options-test";
|
|
16
|
+
const OPTION_TEST_COLUMN_IDS: OptionColumnId[] = [
|
|
17
|
+
"callOpenInterest",
|
|
18
|
+
"callVolume",
|
|
19
|
+
"callLast",
|
|
20
|
+
"callIv",
|
|
21
|
+
"callBid",
|
|
22
|
+
"callAsk",
|
|
23
|
+
"strike",
|
|
24
|
+
"putBid",
|
|
25
|
+
"putAsk",
|
|
26
|
+
"putIv",
|
|
27
|
+
"putLast",
|
|
28
|
+
"putVolume",
|
|
29
|
+
"putOpenInterest",
|
|
30
|
+
];
|
|
31
|
+
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
13
32
|
|
|
14
33
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
15
34
|
|
|
@@ -98,7 +117,7 @@ function OptionsHarness({ ticker, quotePrice }: { ticker: TickerRecord; quotePri
|
|
|
98
117
|
return (
|
|
99
118
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
100
119
|
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
101
|
-
<
|
|
120
|
+
<OptionsView width={122} height={14} focused onCapture={() => {}} />
|
|
102
121
|
</PaneInstanceProvider>
|
|
103
122
|
</AppContext>
|
|
104
123
|
);
|
|
@@ -120,9 +139,46 @@ afterEach(async () => {
|
|
|
120
139
|
});
|
|
121
140
|
testSetup = undefined;
|
|
122
141
|
}
|
|
142
|
+
applyTheme(DEFAULT_THEME);
|
|
123
143
|
setSharedMarketDataCoordinator(null);
|
|
124
144
|
});
|
|
125
145
|
|
|
146
|
+
test("keeps option role colors readable on themed table surfaces", () => {
|
|
147
|
+
for (const themeId of Object.keys(themes)) {
|
|
148
|
+
applyTheme(themeId);
|
|
149
|
+
const surfaces = [
|
|
150
|
+
colors.bg,
|
|
151
|
+
colors.panel,
|
|
152
|
+
hoverBg(),
|
|
153
|
+
blendHex(colors.bg, colors.positive, 0.13),
|
|
154
|
+
blendHex(colors.bg, colors.negative, 0.13),
|
|
155
|
+
blendHex(colors.bg, colors.neutral, 0.055),
|
|
156
|
+
blendHex(colors.bg, colors.header, 0.1),
|
|
157
|
+
blendHex(colors.bg, colors.borderFocused, 0.18),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
for (const columnId of OPTION_TEST_COLUMN_IDS) {
|
|
161
|
+
for (const surface of surfaces) {
|
|
162
|
+
const color = optionColumnColor(columnId, surface);
|
|
163
|
+
const ratio = contrastRatio(color, surface);
|
|
164
|
+
expect(
|
|
165
|
+
ratio,
|
|
166
|
+
`${themeId} ${columnId} ${color} on ${surface}`,
|
|
167
|
+
).toBeGreaterThanOrEqual(OPTION_TEXT_MIN_CONTRAST);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const surface of surfaces) {
|
|
172
|
+
const color = optionMutedColor(surface);
|
|
173
|
+
const ratio = contrastRatio(color, surface);
|
|
174
|
+
expect(
|
|
175
|
+
ratio,
|
|
176
|
+
`${themeId} muted ${color} on ${surface}`,
|
|
177
|
+
).toBeGreaterThanOrEqual(OPTION_TEXT_MIN_CONTRAST);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
126
182
|
test("renders the options chain with the shared table columns", async () => {
|
|
127
183
|
const provider = createTestDataProvider({
|
|
128
184
|
getOptionsChain: async () => makeChain(),
|