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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Box, type ScrollBoxRenderable } from "../ui";
|
|
10
|
+
|
|
11
|
+
export interface TableViewKeyEvent {
|
|
12
|
+
name?: string;
|
|
13
|
+
ctrl?: boolean;
|
|
14
|
+
meta?: boolean;
|
|
15
|
+
option?: boolean;
|
|
16
|
+
shift?: boolean;
|
|
17
|
+
preventDefault?: () => void;
|
|
18
|
+
stopPropagation?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TableViewFrameProps {
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
before?: ReactNode;
|
|
26
|
+
after?: ReactNode;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function TableViewFrame({
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
backgroundColor,
|
|
34
|
+
before,
|
|
35
|
+
after,
|
|
36
|
+
children,
|
|
37
|
+
}: TableViewFrameProps) {
|
|
38
|
+
return (
|
|
39
|
+
<Box
|
|
40
|
+
flexDirection="column"
|
|
41
|
+
flexGrow={1}
|
|
42
|
+
width={width}
|
|
43
|
+
height={height}
|
|
44
|
+
backgroundColor={backgroundColor}
|
|
45
|
+
overflow="hidden"
|
|
46
|
+
>
|
|
47
|
+
{before}
|
|
48
|
+
{children}
|
|
49
|
+
{after}
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTableActivationKey(name: string | undefined): boolean {
|
|
55
|
+
return name === "enter" || name === "return";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isNextTableRowKey(name: string | undefined): boolean {
|
|
59
|
+
return name === "j" || name === "down";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isPreviousTableRowKey(name: string | undefined): boolean {
|
|
63
|
+
return name === "k" || name === "up";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stopTableKey(event: TableViewKeyEvent) {
|
|
67
|
+
event.stopPropagation?.();
|
|
68
|
+
event.preventDefault?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useTableViewState({
|
|
72
|
+
headerScrollRef,
|
|
73
|
+
scrollRef,
|
|
74
|
+
syncHeaderScroll,
|
|
75
|
+
hoveredIdx,
|
|
76
|
+
setHoveredIdx,
|
|
77
|
+
}: {
|
|
78
|
+
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
79
|
+
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
80
|
+
syncHeaderScroll?: () => void;
|
|
81
|
+
hoveredIdx?: number | null;
|
|
82
|
+
setHoveredIdx?: (index: number | null) => void;
|
|
83
|
+
}) {
|
|
84
|
+
const internalHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
85
|
+
const internalScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
86
|
+
const [internalHoveredIdx, setInternalHoveredIdx] = useState<number | null>(null);
|
|
87
|
+
|
|
88
|
+
const effectiveHeaderScrollRef = headerScrollRef ?? internalHeaderScrollRef;
|
|
89
|
+
const effectiveScrollRef = scrollRef ?? internalScrollRef;
|
|
90
|
+
const effectiveHoveredIdx = hoveredIdx !== undefined ? hoveredIdx : internalHoveredIdx;
|
|
91
|
+
const effectiveSetHoveredIdx = setHoveredIdx ?? setInternalHoveredIdx;
|
|
92
|
+
|
|
93
|
+
const defaultSyncHeaderScroll = useCallback(() => {
|
|
94
|
+
const body = effectiveScrollRef.current;
|
|
95
|
+
const header = effectiveHeaderScrollRef.current;
|
|
96
|
+
if (!body || !header) return;
|
|
97
|
+
if (header.scrollLeft !== body.scrollLeft) {
|
|
98
|
+
header.scrollLeft = body.scrollLeft;
|
|
99
|
+
}
|
|
100
|
+
}, [effectiveHeaderScrollRef, effectiveScrollRef]);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
effectiveHeaderScrollRef,
|
|
104
|
+
effectiveScrollRef,
|
|
105
|
+
effectiveHoveredIdx,
|
|
106
|
+
effectiveSetHoveredIdx,
|
|
107
|
+
effectiveSyncHeaderScroll: syncHeaderScroll ?? defaultSyncHeaderScroll,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useTableBodyScrollActivity({
|
|
112
|
+
onBodyScrollActivity,
|
|
113
|
+
syncHeaderScroll,
|
|
114
|
+
afterScroll,
|
|
115
|
+
}: {
|
|
116
|
+
onBodyScrollActivity?: () => void;
|
|
117
|
+
syncHeaderScroll: () => void;
|
|
118
|
+
afterScroll?: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
return useCallback(() => {
|
|
121
|
+
if (onBodyScrollActivity) {
|
|
122
|
+
onBodyScrollActivity();
|
|
123
|
+
} else {
|
|
124
|
+
queueMicrotask(syncHeaderScroll);
|
|
125
|
+
}
|
|
126
|
+
if (afterScroll) {
|
|
127
|
+
queueMicrotask(afterScroll);
|
|
128
|
+
}
|
|
129
|
+
}, [afterScroll, onBodyScrollActivity, syncHeaderScroll]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useScrollBoxScrollActivity({
|
|
133
|
+
scrollRef,
|
|
134
|
+
onVerticalScroll,
|
|
135
|
+
onHorizontalScroll,
|
|
136
|
+
}: {
|
|
137
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
138
|
+
onVerticalScroll?: () => void;
|
|
139
|
+
onHorizontalScroll?: () => void;
|
|
140
|
+
}) {
|
|
141
|
+
const onVerticalScrollRef = useRef(onVerticalScroll);
|
|
142
|
+
const onHorizontalScrollRef = useRef(onHorizontalScroll);
|
|
143
|
+
const verticalScrollScheduledRef = useRef(false);
|
|
144
|
+
const horizontalScrollScheduledRef = useRef(false);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
onVerticalScrollRef.current = onVerticalScroll;
|
|
148
|
+
onHorizontalScrollRef.current = onHorizontalScroll;
|
|
149
|
+
}, [onHorizontalScroll, onVerticalScroll]);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const scrollBox = scrollRef.current;
|
|
153
|
+
if (!scrollBox) return;
|
|
154
|
+
let active = true;
|
|
155
|
+
|
|
156
|
+
const handleVerticalChange = () => {
|
|
157
|
+
if (verticalScrollScheduledRef.current) return;
|
|
158
|
+
verticalScrollScheduledRef.current = true;
|
|
159
|
+
queueMicrotask(() => {
|
|
160
|
+
verticalScrollScheduledRef.current = false;
|
|
161
|
+
if (!active) return;
|
|
162
|
+
onVerticalScrollRef.current?.();
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const handleHorizontalChange = () => {
|
|
166
|
+
if (horizontalScrollScheduledRef.current) return;
|
|
167
|
+
horizontalScrollScheduledRef.current = true;
|
|
168
|
+
queueMicrotask(() => {
|
|
169
|
+
horizontalScrollScheduledRef.current = false;
|
|
170
|
+
if (!active) return;
|
|
171
|
+
onHorizontalScrollRef.current?.();
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
scrollBox.verticalScrollBar.on("change", handleVerticalChange);
|
|
176
|
+
scrollBox.horizontalScrollBar.on("change", handleHorizontalChange);
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
active = false;
|
|
180
|
+
scrollBox.verticalScrollBar.off("change", handleVerticalChange);
|
|
181
|
+
scrollBox.horizontalScrollBar.off("change", handleHorizontalChange);
|
|
182
|
+
};
|
|
183
|
+
}, [scrollRef]);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function useResetTableScroll({
|
|
187
|
+
headerScrollRef,
|
|
188
|
+
scrollRef,
|
|
189
|
+
resetScrollKey,
|
|
190
|
+
afterReset,
|
|
191
|
+
}: {
|
|
192
|
+
headerScrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
193
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
194
|
+
resetScrollKey?: unknown;
|
|
195
|
+
afterReset?: () => void;
|
|
196
|
+
}) {
|
|
197
|
+
const afterResetRef = useRef(afterReset);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
afterResetRef.current = afterReset;
|
|
201
|
+
}, [afterReset]);
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (resetScrollKey === undefined) return;
|
|
205
|
+
const body = scrollRef.current;
|
|
206
|
+
if (body) {
|
|
207
|
+
body.scrollTop = 0;
|
|
208
|
+
body.scrollLeft = 0;
|
|
209
|
+
}
|
|
210
|
+
const header = headerScrollRef.current;
|
|
211
|
+
if (header) {
|
|
212
|
+
header.scrollLeft = 0;
|
|
213
|
+
}
|
|
214
|
+
if (afterResetRef.current) {
|
|
215
|
+
queueMicrotask(afterResetRef.current);
|
|
216
|
+
}
|
|
217
|
+
}, [headerScrollRef, resetScrollKey, scrollRef]);
|
|
218
|
+
}
|
|
@@ -2,15 +2,24 @@ import {
|
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
4
|
useRef,
|
|
5
|
-
useState,
|
|
6
5
|
type ReactNode,
|
|
7
6
|
type RefObject,
|
|
8
7
|
} from "react";
|
|
9
|
-
import {
|
|
8
|
+
import type { ScrollBoxRenderable } from "../ui";
|
|
10
9
|
import { useShortcut } from "../react/input";
|
|
11
10
|
import { TickerListTable, type TickerListTableProps } from "./ticker-list-table";
|
|
12
11
|
import type { TickerRecord } from "../types/ticker";
|
|
13
12
|
import type { DataTableKeyEvent } from "./data-table-view";
|
|
13
|
+
import {
|
|
14
|
+
isNextTableRowKey,
|
|
15
|
+
isPreviousTableRowKey,
|
|
16
|
+
isTableActivationKey,
|
|
17
|
+
stopTableKey,
|
|
18
|
+
TableViewFrame,
|
|
19
|
+
useResetTableScroll,
|
|
20
|
+
useTableBodyScrollActivity,
|
|
21
|
+
useTableViewState,
|
|
22
|
+
} from "./table-view-shared";
|
|
14
23
|
|
|
15
24
|
export interface TickerListVisibleRange {
|
|
16
25
|
start: number;
|
|
@@ -47,23 +56,6 @@ export interface TickerListTableViewProps extends Omit<
|
|
|
47
56
|
resetScrollKey?: unknown;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
|
-
function isActivationKey(name: string | undefined): boolean {
|
|
51
|
-
return name === "enter" || name === "return";
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isNextRowKey(name: string | undefined): boolean {
|
|
55
|
-
return name === "j" || name === "down";
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isPreviousRowKey(name: string | undefined): boolean {
|
|
59
|
-
return name === "k" || name === "up";
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function stopTableKey(event: DataTableKeyEvent) {
|
|
63
|
-
event.stopPropagation?.();
|
|
64
|
-
event.preventDefault?.();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
59
|
export function TickerListTableView({
|
|
68
60
|
focused = false,
|
|
69
61
|
rootBefore,
|
|
@@ -86,20 +78,23 @@ export function TickerListTableView({
|
|
|
86
78
|
resetScrollKey,
|
|
87
79
|
...tableProps
|
|
88
80
|
}: TickerListTableViewProps) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
81
|
+
const {
|
|
82
|
+
effectiveHeaderScrollRef,
|
|
83
|
+
effectiveScrollRef,
|
|
84
|
+
effectiveHoveredIdx,
|
|
85
|
+
effectiveSetHoveredIdx,
|
|
86
|
+
effectiveSyncHeaderScroll,
|
|
87
|
+
} = useTableViewState({
|
|
88
|
+
headerScrollRef,
|
|
89
|
+
scrollRef,
|
|
90
|
+
syncHeaderScroll,
|
|
91
|
+
hoveredIdx,
|
|
92
|
+
setHoveredIdx,
|
|
93
|
+
});
|
|
100
94
|
const selectedIndex = tableProps.tickers.findIndex(
|
|
101
95
|
(ticker) => ticker.metadata.ticker === tableProps.cursorSymbol,
|
|
102
96
|
);
|
|
97
|
+
const lastScrollCursorSymbolRef = useRef<string | null | undefined>(undefined);
|
|
103
98
|
|
|
104
99
|
const emitVisibleRange = useCallback(() => {
|
|
105
100
|
if (!onVisibleRangeChange) return;
|
|
@@ -113,24 +108,11 @@ export function TickerListTableView({
|
|
|
113
108
|
onVisibleRangeChange({ start, end });
|
|
114
109
|
}, [effectiveScrollRef, onVisibleRangeChange, tableProps.tickers.length, visibleRangeBuffer]);
|
|
115
110
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
header.scrollLeft = body.scrollLeft;
|
|
122
|
-
}
|
|
123
|
-
}, [effectiveHeaderScrollRef, effectiveScrollRef]);
|
|
124
|
-
const effectiveSyncHeaderScroll = syncHeaderScroll ?? defaultSyncHeaderScroll;
|
|
125
|
-
|
|
126
|
-
const handleBodyScrollActivity = useCallback(() => {
|
|
127
|
-
if (onBodyScrollActivity) {
|
|
128
|
-
onBodyScrollActivity();
|
|
129
|
-
} else {
|
|
130
|
-
queueMicrotask(effectiveSyncHeaderScroll);
|
|
131
|
-
}
|
|
132
|
-
queueMicrotask(emitVisibleRange);
|
|
133
|
-
}, [effectiveSyncHeaderScroll, emitVisibleRange, onBodyScrollActivity]);
|
|
111
|
+
const handleBodyScrollActivity = useTableBodyScrollActivity({
|
|
112
|
+
onBodyScrollActivity,
|
|
113
|
+
syncHeaderScroll: effectiveSyncHeaderScroll,
|
|
114
|
+
afterScroll: emitVisibleRange,
|
|
115
|
+
});
|
|
134
116
|
|
|
135
117
|
useEffect(() => {
|
|
136
118
|
const scrollBox = effectiveScrollRef.current;
|
|
@@ -139,19 +121,12 @@ export function TickerListTableView({
|
|
|
139
121
|
emitVisibleRange();
|
|
140
122
|
}, [emitVisibleRange, effectiveScrollRef, rootHeight, tableProps.tickers.length]);
|
|
141
123
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
const header = effectiveHeaderScrollRef.current;
|
|
150
|
-
if (header) {
|
|
151
|
-
header.scrollLeft = 0;
|
|
152
|
-
}
|
|
153
|
-
queueMicrotask(emitVisibleRange);
|
|
154
|
-
}, [effectiveHeaderScrollRef, effectiveScrollRef, emitVisibleRange, resetScrollKey]);
|
|
124
|
+
useResetTableScroll({
|
|
125
|
+
headerScrollRef: effectiveHeaderScrollRef,
|
|
126
|
+
scrollRef: effectiveScrollRef,
|
|
127
|
+
resetScrollKey,
|
|
128
|
+
afterReset: emitVisibleRange,
|
|
129
|
+
});
|
|
155
130
|
|
|
156
131
|
const selectIndex = useCallback((index: number) => {
|
|
157
132
|
if (index < 0 || index >= tableProps.tickers.length) return;
|
|
@@ -192,19 +167,19 @@ export function TickerListTableView({
|
|
|
192
167
|
if (onRootKeyDown?.(event)) return;
|
|
193
168
|
if (tableProps.tickers.length === 0) return;
|
|
194
169
|
|
|
195
|
-
if (
|
|
170
|
+
if (isNextTableRowKey(event.name)) {
|
|
196
171
|
stopTableKey(event);
|
|
197
172
|
selectByOffset(1);
|
|
198
173
|
return;
|
|
199
174
|
}
|
|
200
175
|
|
|
201
|
-
if (
|
|
176
|
+
if (isPreviousTableRowKey(event.name)) {
|
|
202
177
|
stopTableKey(event);
|
|
203
178
|
selectByOffset(-1);
|
|
204
179
|
return;
|
|
205
180
|
}
|
|
206
181
|
|
|
207
|
-
if (
|
|
182
|
+
if (isTableActivationKey(event.name)) {
|
|
208
183
|
stopTableKey(event);
|
|
209
184
|
activateSelection();
|
|
210
185
|
}
|
|
@@ -212,7 +187,19 @@ export function TickerListTableView({
|
|
|
212
187
|
|
|
213
188
|
useEffect(() => {
|
|
214
189
|
const scrollBox = effectiveScrollRef.current;
|
|
215
|
-
if (!scrollBox?.viewport || selectedIndex < 0)
|
|
190
|
+
if (!scrollBox?.viewport || selectedIndex < 0) {
|
|
191
|
+
queueMicrotask(emitVisibleRange);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const shouldScrollToCursor =
|
|
196
|
+
lastScrollCursorSymbolRef.current !== tableProps.cursorSymbol;
|
|
197
|
+
lastScrollCursorSymbolRef.current = tableProps.cursorSymbol;
|
|
198
|
+
|
|
199
|
+
if (!shouldScrollToCursor) {
|
|
200
|
+
queueMicrotask(emitVisibleRange);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
216
203
|
|
|
217
204
|
const viewportHeight = Math.max(scrollBox.viewport.height, 1);
|
|
218
205
|
if (selectedIndex < scrollBox.scrollTop) {
|
|
@@ -221,18 +208,16 @@ export function TickerListTableView({
|
|
|
221
208
|
scrollBox.scrollTo(selectedIndex - viewportHeight + 1);
|
|
222
209
|
}
|
|
223
210
|
queueMicrotask(emitVisibleRange);
|
|
224
|
-
}, [effectiveScrollRef, emitVisibleRange, selectedIndex]);
|
|
211
|
+
}, [effectiveScrollRef, emitVisibleRange, selectedIndex, tableProps.cursorSymbol]);
|
|
225
212
|
|
|
226
213
|
return (
|
|
227
|
-
<
|
|
228
|
-
flexDirection="column"
|
|
229
|
-
flexGrow={1}
|
|
214
|
+
<TableViewFrame
|
|
230
215
|
width={rootWidth}
|
|
231
216
|
height={rootHeight}
|
|
232
217
|
backgroundColor={rootBackgroundColor}
|
|
233
|
-
|
|
218
|
+
before={rootBefore}
|
|
219
|
+
after={rootAfter}
|
|
234
220
|
>
|
|
235
|
-
{rootBefore}
|
|
236
221
|
<TickerListTable
|
|
237
222
|
{...tableProps}
|
|
238
223
|
headerScrollRef={effectiveHeaderScrollRef}
|
|
@@ -242,7 +227,6 @@ export function TickerListTableView({
|
|
|
242
227
|
hoveredIdx={effectiveHoveredIdx}
|
|
243
228
|
setHoveredIdx={effectiveSetHoveredIdx}
|
|
244
229
|
/>
|
|
245
|
-
|
|
246
|
-
</Box>
|
|
230
|
+
</TableViewFrame>
|
|
247
231
|
);
|
|
248
232
|
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act, useState } from "react";
|
|
2
|
+
import { act, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
4
|
import type { ColumnConfig } from "../types/config";
|
|
5
5
|
import type { TickerFinancials } from "../types/financials";
|
|
6
6
|
import type { TickerRecord } from "../types/ticker";
|
|
7
|
+
import type { ScrollBoxRenderable } from "../ui";
|
|
7
8
|
import { TickerListTable, type TickerTableCell } from "./ticker-list-table";
|
|
9
|
+
import { TickerListTableView } from "./ticker-list-table-view";
|
|
8
10
|
|
|
9
11
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
12
|
let setHarnessCursorSymbol: ((symbol: string) => void) | null = null;
|
|
13
|
+
let setHarnessTickers: ((tickers: TickerRecord[]) => void) | null = null;
|
|
14
|
+
let tableScrollRef: ScrollBoxRenderable | null = null;
|
|
11
15
|
let resolveCellCallCount = 0;
|
|
12
16
|
|
|
13
17
|
const columns: ColumnConfig[] = [
|
|
@@ -60,6 +64,16 @@ function TickerListTableHarness() {
|
|
|
60
64
|
|
|
61
65
|
function LargeTickerListTableHarness() {
|
|
62
66
|
const [cursorSymbol, setCursorSymbol] = useState("T0");
|
|
67
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
tableScrollRef = scrollRef.current;
|
|
71
|
+
return () => {
|
|
72
|
+
if (tableScrollRef === scrollRef.current) {
|
|
73
|
+
tableScrollRef = null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
});
|
|
63
77
|
|
|
64
78
|
return (
|
|
65
79
|
<TickerListTable
|
|
@@ -71,6 +85,35 @@ function LargeTickerListTableHarness() {
|
|
|
71
85
|
setCursorSymbol={setCursorSymbol}
|
|
72
86
|
resolveCell={resolveCell}
|
|
73
87
|
financialsMap={financialsMap}
|
|
88
|
+
scrollRef={scrollRef}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function ReorderingTickerListTableViewHarness() {
|
|
94
|
+
const [rows, setRows] = useState(manyTickers);
|
|
95
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
96
|
+
setHarnessTickers = setRows;
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
tableScrollRef = scrollRef.current;
|
|
100
|
+
return () => {
|
|
101
|
+
if (tableScrollRef === scrollRef.current) {
|
|
102
|
+
tableScrollRef = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<TickerListTableView
|
|
109
|
+
focused
|
|
110
|
+
columns={columns}
|
|
111
|
+
tickers={rows}
|
|
112
|
+
cursorSymbol="T0"
|
|
113
|
+
setCursorSymbol={() => {}}
|
|
114
|
+
resolveCell={resolveCell}
|
|
115
|
+
financialsMap={financialsMap}
|
|
116
|
+
scrollRef={scrollRef}
|
|
74
117
|
/>
|
|
75
118
|
);
|
|
76
119
|
}
|
|
@@ -84,6 +127,8 @@ afterEach(async () => {
|
|
|
84
127
|
}
|
|
85
128
|
resolveCellCallCount = 0;
|
|
86
129
|
setHarnessCursorSymbol = null;
|
|
130
|
+
setHarnessTickers = null;
|
|
131
|
+
tableScrollRef = null;
|
|
87
132
|
});
|
|
88
133
|
|
|
89
134
|
describe("TickerListTable", () => {
|
|
@@ -125,5 +170,41 @@ describe("TickerListTable", () => {
|
|
|
125
170
|
expect(frame).toContain("T0");
|
|
126
171
|
expect(frame).not.toContain("T999");
|
|
127
172
|
expect(resolveCellCallCount).toBeLessThan(40);
|
|
173
|
+
|
|
174
|
+
await act(async () => {
|
|
175
|
+
for (let index = 0; index < 12; index++) {
|
|
176
|
+
await testSetup!.mockMouse.scroll(2, 2, "down");
|
|
177
|
+
}
|
|
178
|
+
await Promise.resolve();
|
|
179
|
+
await testSetup!.renderOnce();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const scrollTop = tableScrollRef?.scrollTop ?? 0;
|
|
183
|
+
expect(scrollTop).toBeGreaterThan(0);
|
|
184
|
+
expect(testSetup.captureCharFrame()).toContain(`T${scrollTop}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("preserves manual scroll when market data reorders rows around the same cursor", async () => {
|
|
188
|
+
testSetup = await testRender(
|
|
189
|
+
<ReorderingTickerListTableViewHarness />,
|
|
190
|
+
{ width: 20, height: 8 },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await testSetup!.renderOnce();
|
|
195
|
+
await testSetup!.renderOnce();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
tableScrollRef!.scrollTop = 20;
|
|
199
|
+
expect(tableScrollRef?.scrollTop).toBe(20);
|
|
200
|
+
|
|
201
|
+
await act(async () => {
|
|
202
|
+
setHarnessTickers?.([...manyTickers.slice(1), manyTickers[0]!]);
|
|
203
|
+
await Promise.resolve();
|
|
204
|
+
await testSetup!.renderOnce();
|
|
205
|
+
await testSetup!.renderOnce();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(tableScrollRef?.scrollTop).toBe(20);
|
|
128
209
|
});
|
|
129
210
|
});
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Box,
|
|
3
|
+
ScrollBox,
|
|
4
|
+
Text,
|
|
5
|
+
tickerContextMenuItems,
|
|
6
|
+
useContextMenu,
|
|
7
|
+
useNativeRenderer,
|
|
8
|
+
useRendererHost,
|
|
9
|
+
useUiCapabilities,
|
|
10
|
+
} from "../ui";
|
|
2
11
|
import { memo, useCallback, useMemo, useRef, useState, type RefObject } from "react";
|
|
3
12
|
import { TextAttributes, type ScrollBoxRenderable } from "../ui";
|
|
4
13
|
import { EmptyState } from "./ui";
|
|
5
14
|
import { colors, hoverBg } from "../theme/colors";
|
|
15
|
+
import { useThemeColors } from "../theme/theme-context";
|
|
6
16
|
import { getSharedRegistry } from "../plugins/registry";
|
|
7
17
|
import type { ColumnConfig } from "../types/config";
|
|
8
18
|
import type { TickerFinancials, PricePoint } from "../types/financials";
|
|
@@ -12,8 +22,8 @@ import { renderChart, resolveChartPalette, type StyledContent } from "./chart/ch
|
|
|
12
22
|
import type { ProjectedChartPoint } from "./chart/chart-data";
|
|
13
23
|
import { tableContentWidthProps, useMeasuredTableContentWidth } from "./ui/table-layout";
|
|
14
24
|
import { useViewport } from "../react/input";
|
|
15
|
-
import { useRafCallback } from "../react/use-raf-callback";
|
|
16
25
|
import { measurePerf } from "../utils/perf-marks";
|
|
26
|
+
import { useScrollBoxScrollActivity } from "./table-view-shared";
|
|
17
27
|
|
|
18
28
|
export interface TickerTableCell {
|
|
19
29
|
text: string;
|
|
@@ -102,6 +112,7 @@ const TickerListHeader = memo(function TickerListHeader({
|
|
|
102
112
|
onHeaderClick?: (columnId: string) => void;
|
|
103
113
|
onSizeChange?: () => void;
|
|
104
114
|
}) {
|
|
115
|
+
useThemeColors();
|
|
105
116
|
return (
|
|
106
117
|
<ScrollBox
|
|
107
118
|
ref={headerScrollRef}
|
|
@@ -211,6 +222,7 @@ const TickerListRow = memo(function TickerListRow({
|
|
|
211
222
|
priceHistory?: PricePoint[];
|
|
212
223
|
contentWidth: number;
|
|
213
224
|
}) {
|
|
225
|
+
useThemeColors();
|
|
214
226
|
const lastActivatedAtRef = useRef<number | null>(null);
|
|
215
227
|
const rowBg = isSelected ? colors.selected : isHovered ? hoverBg() : colors.bg;
|
|
216
228
|
|
|
@@ -328,6 +340,7 @@ export function TickerListTable({
|
|
|
328
340
|
const hoveredIdxRef = useRef(hoveredIdx);
|
|
329
341
|
const appViewport = useViewport();
|
|
330
342
|
const renderer = useRendererHost();
|
|
343
|
+
const nativeRenderer = useNativeRenderer();
|
|
331
344
|
const { showContextMenu } = useContextMenu();
|
|
332
345
|
const { nativeContextMenu } = useUiCapabilities();
|
|
333
346
|
const [scrollVersion, setScrollVersion] = useState(0);
|
|
@@ -387,12 +400,16 @@ export function TickerListTable({
|
|
|
387
400
|
setScrollVersion((current) => current + 1);
|
|
388
401
|
}
|
|
389
402
|
onBodyScrollActivity?.();
|
|
390
|
-
|
|
403
|
+
nativeRenderer.requestRender();
|
|
404
|
+
}, [nativeRenderer, onBodyScrollActivity, virtualize]);
|
|
391
405
|
const syncHeader = useCallback(() => {
|
|
392
406
|
syncHeaderScroll?.();
|
|
393
407
|
}, [syncHeaderScroll]);
|
|
394
|
-
|
|
395
|
-
|
|
408
|
+
useScrollBoxScrollActivity({
|
|
409
|
+
scrollRef: effectiveScrollRef,
|
|
410
|
+
onVerticalScroll: handleBodyScrollActivity,
|
|
411
|
+
onHorizontalScroll: syncHeader,
|
|
412
|
+
});
|
|
396
413
|
const setHoveredIdxIfChanged = useCallback((index: number | null) => {
|
|
397
414
|
if (hoveredIdxRef.current === index) return;
|
|
398
415
|
setHoveredIdx(index);
|
|
@@ -448,10 +465,6 @@ export function TickerListTable({
|
|
|
448
465
|
scrollX
|
|
449
466
|
scrollY
|
|
450
467
|
focusable={false}
|
|
451
|
-
onMouseDown={scheduleHeaderScrollSync}
|
|
452
|
-
onMouseUp={scheduleBodyScrollActivity}
|
|
453
|
-
onMouseDrag={scheduleBodyScrollActivity}
|
|
454
|
-
onMouseScroll={scheduleBodyScrollActivity}
|
|
455
468
|
onSizeChange={measureContentWidth}
|
|
456
469
|
>
|
|
457
470
|
{tickers.length === 0 ? (
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useCallback, type ReactNode } from "react";
|
|
2
|
+
import { Box, Text } from "../../ui";
|
|
3
|
+
import { colors } from "../../theme/colors";
|
|
4
|
+
import { type PromptContext, useDialogKeyboard } from "../../ui/dialog";
|
|
5
|
+
import { Button, type ButtonVariant } from "./button";
|
|
6
|
+
import { DialogFrame } from "./frame";
|
|
7
|
+
|
|
8
|
+
export interface ConfirmDialogProps extends PromptContext<boolean> {
|
|
9
|
+
title: string;
|
|
10
|
+
body: ReactNode | string | string[];
|
|
11
|
+
confirmLabel: string;
|
|
12
|
+
cancelLabel?: string;
|
|
13
|
+
confirmVariant?: ButtonVariant;
|
|
14
|
+
width?: number;
|
|
15
|
+
footer?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ConfirmDialog({
|
|
19
|
+
resolve,
|
|
20
|
+
title,
|
|
21
|
+
body,
|
|
22
|
+
confirmLabel,
|
|
23
|
+
cancelLabel = "Cancel",
|
|
24
|
+
confirmVariant = "danger",
|
|
25
|
+
width = 52,
|
|
26
|
+
footer,
|
|
27
|
+
}: ConfirmDialogProps) {
|
|
28
|
+
const confirm = useCallback(() => resolve(true), [resolve]);
|
|
29
|
+
const cancel = useCallback(() => resolve(false), [resolve]);
|
|
30
|
+
|
|
31
|
+
useDialogKeyboard((event) => {
|
|
32
|
+
event.stopPropagation();
|
|
33
|
+
if (event.name === "enter" || event.name === "return") {
|
|
34
|
+
confirm();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (event.name === "escape") {
|
|
38
|
+
cancel();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<DialogFrame title={title} footer={footer ?? `Enter ${confirmLabel.toLowerCase()} · Esc cancel`}>
|
|
44
|
+
<Box flexDirection="column" width={width}>
|
|
45
|
+
{renderBody(body)}
|
|
46
|
+
<Box height={1} />
|
|
47
|
+
<Box flexDirection="row" gap={1}>
|
|
48
|
+
<Button label={confirmLabel} variant={confirmVariant} onPress={confirm} />
|
|
49
|
+
<Button label={cancelLabel} variant="secondary" onPress={cancel} />
|
|
50
|
+
</Box>
|
|
51
|
+
</Box>
|
|
52
|
+
</DialogFrame>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderBody(body: ReactNode | string | string[]): ReactNode {
|
|
57
|
+
if (Array.isArray(body)) {
|
|
58
|
+
return body.map((line, index) => (
|
|
59
|
+
<Text key={`${line}:${index}`} fg={index === 0 ? colors.text : colors.textDim}>{line}</Text>
|
|
60
|
+
));
|
|
61
|
+
}
|
|
62
|
+
if (typeof body === "string") {
|
|
63
|
+
return <Text fg={colors.text}>{body}</Text>;
|
|
64
|
+
}
|
|
65
|
+
return body;
|
|
66
|
+
}
|