gloomberb 0.6.1 → 0.7.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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- 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 +94 -12
- 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 +60 -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-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- 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 +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- 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 +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- 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 +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- 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 +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- 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 +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -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 +587 -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.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -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 +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- 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 +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -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 +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- 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 +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- 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/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- 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,14 +1,54 @@
|
|
|
1
|
-
import { createThrottledFetch } from "../../../utils/throttled-fetch";
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { createThrottledFetch, type ThrottledFetchTransport } from "../../../utils/throttled-fetch";
|
|
2
|
+
|
|
3
|
+
const YAHOO_FINANCE_HOSTS = [
|
|
4
|
+
"query2.finance.yahoo.com",
|
|
5
|
+
"query1.finance.yahoo.com",
|
|
6
|
+
] as const;
|
|
7
|
+
|
|
8
|
+
const YAHOO_FINANCE_HEADERS = {
|
|
9
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
|
|
10
|
+
Accept: "application/json,text/plain,*/*",
|
|
11
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
12
|
+
Referer: "https://finance.yahoo.com/",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export interface YahooScreenerApi {
|
|
16
|
+
fetchJson<T = unknown>(path: string, params: Record<string, string | number>): Promise<T>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createYahooScreenerApi(transport?: ThrottledFetchTransport): YahooScreenerApi {
|
|
20
|
+
const client = createThrottledFetch({
|
|
21
|
+
requestsPerMinute: 15,
|
|
22
|
+
maxRetries: 2,
|
|
23
|
+
timeoutMs: 10_000,
|
|
24
|
+
defaultHeaders: YAHOO_FINANCE_HEADERS,
|
|
25
|
+
transport,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
async fetchJson<T = unknown>(path: string, params: Record<string, string | number>): Promise<T> {
|
|
30
|
+
let lastError: unknown;
|
|
31
|
+
for (const host of YAHOO_FINANCE_HOSTS) {
|
|
32
|
+
const url = new URL(`https://${host}${path}`);
|
|
33
|
+
for (const [key, value] of Object.entries(params)) {
|
|
34
|
+
url.searchParams.set(key, String(value));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return await client.fetchJson<T>(url.toString());
|
|
39
|
+
} catch (error) {
|
|
40
|
+
lastError = error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw lastError instanceof Error
|
|
45
|
+
? lastError
|
|
46
|
+
: new Error("Yahoo Finance screener request failed");
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const screenerApi = createYahooScreenerApi();
|
|
12
52
|
|
|
13
53
|
export type ScreenerCategory = "day_gainers" | "day_losers" | "most_actives";
|
|
14
54
|
|
|
@@ -75,9 +115,18 @@ export function parseScreenerResponse(data: any): ScreenerQuote[] {
|
|
|
75
115
|
}
|
|
76
116
|
}
|
|
77
117
|
|
|
78
|
-
export async function fetchScreener(
|
|
79
|
-
|
|
80
|
-
|
|
118
|
+
export async function fetchScreener(
|
|
119
|
+
category: ScreenerCategory,
|
|
120
|
+
count = 25,
|
|
121
|
+
api: YahooScreenerApi = screenerApi,
|
|
122
|
+
): Promise<ScreenerQuote[]> {
|
|
123
|
+
const data = await api.fetchJson("/v1/finance/screener/predefined/saved", {
|
|
124
|
+
formatted: "false",
|
|
125
|
+
lang: "en-US",
|
|
126
|
+
region: "US",
|
|
127
|
+
scrIds: category,
|
|
128
|
+
count,
|
|
129
|
+
});
|
|
81
130
|
return parseScreenerResponse(data);
|
|
82
131
|
}
|
|
83
132
|
|
|
@@ -96,9 +145,13 @@ export function parseTrendingResponse(data: any): TrendingSymbol[] {
|
|
|
96
145
|
}
|
|
97
146
|
}
|
|
98
147
|
|
|
99
|
-
export async function fetchTrending(
|
|
100
|
-
|
|
101
|
-
|
|
148
|
+
export async function fetchTrending(
|
|
149
|
+
count = 25,
|
|
150
|
+
api: YahooScreenerApi = screenerApi,
|
|
151
|
+
): Promise<TrendingSymbol[]> {
|
|
152
|
+
const data = await api.fetchJson("/v1/finance/trending/US", {
|
|
153
|
+
count,
|
|
154
|
+
});
|
|
102
155
|
return parseTrendingResponse(data);
|
|
103
156
|
}
|
|
104
157
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box } from "../../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import type { PaneProps } from "../../../types/plugin";
|
|
4
|
-
import { useNewsArticles } from "../../../news/hooks";
|
|
4
|
+
import { useLoadNewsStory, useNewsArticles } from "../../../news/hooks";
|
|
5
5
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../plugin-runtime";
|
|
6
6
|
import { Spinner } from "../../../components";
|
|
7
7
|
import type { MarketNewsItem } from "../../../types/news-source";
|
|
@@ -41,7 +41,8 @@ export function BreakingPane({ focused, width, height }: PaneProps) {
|
|
|
41
41
|
const [aiRunning, setAiRunning] = useState(false);
|
|
42
42
|
const [spinFrame, setSpinFrame] = useState(0);
|
|
43
43
|
const processingRef = useRef(false);
|
|
44
|
-
const
|
|
44
|
+
const loadNewsStory = useLoadNewsStory();
|
|
45
|
+
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(articles, loadNewsStory);
|
|
45
46
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
46
47
|
|
|
47
48
|
useEffect(() => {
|
|
@@ -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);
|
|
@@ -2,7 +2,7 @@ import { Box } from "../../../ui";
|
|
|
2
2
|
import { useEffect, useMemo } from "react";
|
|
3
3
|
import type { PaneProps } from "../../../types/plugin";
|
|
4
4
|
import type { MarketNewsItem } from "../../../types/news-source";
|
|
5
|
-
import { useNewsArticles } from "../../../news/hooks";
|
|
5
|
+
import { useLoadNewsStory, useNewsArticles } from "../../../news/hooks";
|
|
6
6
|
import type { NewsQueryPhase } from "../../../news/types";
|
|
7
7
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../plugin-runtime";
|
|
8
8
|
import { Spinner, Tabs } from "../../../components";
|
|
@@ -43,7 +43,8 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
43
43
|
const [sortPreference, setSortPreference] = usePluginPaneState<NewsSortPreference>("industry:sort", DEFAULT_SORT);
|
|
44
44
|
const { articles, allArticles, phase } = useIndustryArticles(category);
|
|
45
45
|
const loading = phase === "loading" || (phase === "refreshing" && articles.length === 0);
|
|
46
|
-
const
|
|
46
|
+
const loadNewsStory = useLoadNewsStory();
|
|
47
|
+
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(articles, loadNewsStory);
|
|
47
48
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
48
49
|
const counts = useMemo(() => {
|
|
49
50
|
const next: Record<string, number> = { all: allArticles.length };
|
|
@@ -64,21 +65,6 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
64
65
|
setSelectedArticleId(null);
|
|
65
66
|
}, [category, setSelectedArticleId]);
|
|
66
67
|
|
|
67
|
-
const handleRootKeyDown = (event: {
|
|
68
|
-
name?: string;
|
|
69
|
-
preventDefault?: () => void;
|
|
70
|
-
stopPropagation?: () => void;
|
|
71
|
-
}) => {
|
|
72
|
-
if (event.name !== "left" && event.name !== "right" && event.name !== "h" && event.name !== "l") return;
|
|
73
|
-
event.stopPropagation?.();
|
|
74
|
-
event.preventDefault?.();
|
|
75
|
-
const index = SECTOR_TABS.indexOf(category);
|
|
76
|
-
const delta = event.name === "left" || event.name === "h" ? -1 : 1;
|
|
77
|
-
const nextIndex = Math.max(0, Math.min(SECTOR_TABS.length - 1, index + delta));
|
|
78
|
-
setCategory(SECTOR_TABS[nextIndex]!);
|
|
79
|
-
return true;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
68
|
useNewsArticleFooter({
|
|
83
69
|
registrationId: "news-wire:industry",
|
|
84
70
|
focused,
|
|
@@ -93,6 +79,7 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
93
79
|
onSelect={(value) => setCategory(value as SectorNewsSelection)}
|
|
94
80
|
compact
|
|
95
81
|
variant="bare"
|
|
82
|
+
focused={focused}
|
|
96
83
|
/>
|
|
97
84
|
</Box>
|
|
98
85
|
);
|
|
@@ -126,7 +113,6 @@ export function IndustryPane({ focused, width, height }: PaneProps) {
|
|
|
126
113
|
detailContent={detailContent}
|
|
127
114
|
detailTitle={detailArticle?.title}
|
|
128
115
|
rootBefore={rootBefore}
|
|
129
|
-
onRootKeyDown={handleRootKeyDown}
|
|
130
116
|
columns={["time", "source", "title", "tickers", "categories"]}
|
|
131
117
|
emptyContent={loading && articles.length === 0 ? (
|
|
132
118
|
<Box width="100%" paddingX={1} paddingY={1}>
|
|
@@ -10,9 +10,27 @@ import { collectNewsDisplayTickers } from "../../../news/ticker-symbols";
|
|
|
10
10
|
import { useInlineTickers } from "../../../state/use-inline-tickers";
|
|
11
11
|
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
function hasStoryItems(article: MarketNewsItem | null): boolean {
|
|
14
|
+
return (article?.items?.length ?? 0) > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mergeLoadedArticle(base: MarketNewsItem, loaded: MarketNewsItem | null | undefined): MarketNewsItem {
|
|
18
|
+
if (!loaded) return base;
|
|
19
|
+
return {
|
|
20
|
+
...base,
|
|
21
|
+
...loaded,
|
|
22
|
+
items: hasStoryItems(loaded) ? loaded.items : base.items,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useNewsArticleDetail(
|
|
27
|
+
articles: MarketNewsItem[],
|
|
28
|
+
loadArticleDetail?: (articleId: string) => Promise<MarketNewsItem | null>,
|
|
29
|
+
) {
|
|
14
30
|
const [detailArticleId, setDetailArticleId] = useState<string | null>(null);
|
|
15
|
-
const
|
|
31
|
+
const [loadedArticles, setLoadedArticles] = useState<Map<string, MarketNewsItem>>(() => new Map());
|
|
32
|
+
const requestedArticleIds = useRef<Set<string>>(new Set());
|
|
33
|
+
const baseDetailArticle = useMemo(
|
|
16
34
|
() => (
|
|
17
35
|
detailArticleId
|
|
18
36
|
? articles.find((article) => article.id === detailArticleId) ?? null
|
|
@@ -20,12 +38,36 @@ export function useNewsArticleDetail(articles: MarketNewsItem[]) {
|
|
|
20
38
|
),
|
|
21
39
|
[articles, detailArticleId],
|
|
22
40
|
);
|
|
41
|
+
const detailArticle = useMemo(() => (
|
|
42
|
+
baseDetailArticle && detailArticleId
|
|
43
|
+
? mergeLoadedArticle(baseDetailArticle, loadedArticles.get(detailArticleId))
|
|
44
|
+
: baseDetailArticle
|
|
45
|
+
), [baseDetailArticle, detailArticleId, loadedArticles]);
|
|
23
46
|
|
|
24
47
|
useEffect(() => {
|
|
25
|
-
if (detailArticleId && !
|
|
48
|
+
if (detailArticleId && !baseDetailArticle) {
|
|
26
49
|
setDetailArticleId(null);
|
|
27
50
|
}
|
|
28
|
-
}, [
|
|
51
|
+
}, [baseDetailArticle, detailArticleId]);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!detailArticleId || !baseDetailArticle || hasStoryItems(detailArticle)) return;
|
|
55
|
+
if (!loadArticleDetail || requestedArticleIds.current.has(detailArticleId)) return;
|
|
56
|
+
|
|
57
|
+
requestedArticleIds.current.add(detailArticleId);
|
|
58
|
+
void loadArticleDetail(detailArticleId)
|
|
59
|
+
.then((loadedArticle) => {
|
|
60
|
+
if (!loadedArticle) return;
|
|
61
|
+
setLoadedArticles((current) => {
|
|
62
|
+
const next = new Map(current);
|
|
63
|
+
next.set(detailArticleId, loadedArticle);
|
|
64
|
+
return next;
|
|
65
|
+
});
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {
|
|
68
|
+
requestedArticleIds.current.delete(detailArticleId);
|
|
69
|
+
});
|
|
70
|
+
}, [baseDetailArticle, detailArticle, detailArticleId, loadArticleDetail]);
|
|
29
71
|
|
|
30
72
|
const openArticle = useCallback((article: MarketNewsItem) => {
|
|
31
73
|
setDetailArticleId(article.id);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box } from "../../../ui";
|
|
2
2
|
import type { NewsQuery } from "../../../news/types";
|
|
3
|
-
import { useNewsArticles } from "../../../news/hooks";
|
|
3
|
+
import { useLoadNewsStory, useNewsArticles } from "../../../news/hooks";
|
|
4
4
|
import type { PaneProps } from "../../../types/plugin";
|
|
5
5
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../plugin-runtime";
|
|
6
6
|
import { Spinner } from "../../../components";
|
|
@@ -45,7 +45,8 @@ export function NewsPresetPane({
|
|
|
45
45
|
`${paneKey}:sort`,
|
|
46
46
|
defaultSort,
|
|
47
47
|
);
|
|
48
|
-
const
|
|
48
|
+
const loadNewsStory = useLoadNewsStory();
|
|
49
|
+
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(articles, loadNewsStory);
|
|
49
50
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
50
51
|
|
|
51
52
|
useNewsArticleFooter({
|
|
@@ -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);
|
|
@@ -510,6 +473,7 @@ function createQuickNotesPane(notesFiles: NotesFiles) {
|
|
|
510
473
|
variant="pill"
|
|
511
474
|
closeMode="active"
|
|
512
475
|
onAdd={addTab}
|
|
476
|
+
focused={focused && !editing && !renaming}
|
|
513
477
|
/>
|
|
514
478
|
</Box>
|
|
515
479
|
{/* Rename input */}
|
|
@@ -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(),
|