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,16 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
|
-
useRef,
|
|
5
|
-
useState,
|
|
6
4
|
type ReactNode,
|
|
7
5
|
type RefObject,
|
|
8
6
|
} from "react";
|
|
9
|
-
import {
|
|
7
|
+
import type { ScrollBoxRenderable } from "../ui";
|
|
10
8
|
import { useShortcut } from "../react/input";
|
|
11
9
|
import { TickerListTable, type TickerListTableProps } from "./ticker-list-table";
|
|
12
10
|
import type { TickerRecord } from "../types/ticker";
|
|
13
11
|
import type { DataTableKeyEvent } from "./data-table-view";
|
|
12
|
+
import {
|
|
13
|
+
isNextTableRowKey,
|
|
14
|
+
isPreviousTableRowKey,
|
|
15
|
+
isTableActivationKey,
|
|
16
|
+
stopTableKey,
|
|
17
|
+
TableViewFrame,
|
|
18
|
+
useResetTableScroll,
|
|
19
|
+
useTableBodyScrollActivity,
|
|
20
|
+
useTableViewState,
|
|
21
|
+
} from "./table-view-shared";
|
|
14
22
|
|
|
15
23
|
export interface TickerListVisibleRange {
|
|
16
24
|
start: number;
|
|
@@ -47,23 +55,6 @@ export interface TickerListTableViewProps extends Omit<
|
|
|
47
55
|
resetScrollKey?: unknown;
|
|
48
56
|
}
|
|
49
57
|
|
|
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
58
|
export function TickerListTableView({
|
|
68
59
|
focused = false,
|
|
69
60
|
rootBefore,
|
|
@@ -86,17 +77,19 @@ export function TickerListTableView({
|
|
|
86
77
|
resetScrollKey,
|
|
87
78
|
...tableProps
|
|
88
79
|
}: TickerListTableViewProps) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
80
|
+
const {
|
|
81
|
+
effectiveHeaderScrollRef,
|
|
82
|
+
effectiveScrollRef,
|
|
83
|
+
effectiveHoveredIdx,
|
|
84
|
+
effectiveSetHoveredIdx,
|
|
85
|
+
effectiveSyncHeaderScroll,
|
|
86
|
+
} = useTableViewState({
|
|
87
|
+
headerScrollRef,
|
|
88
|
+
scrollRef,
|
|
89
|
+
syncHeaderScroll,
|
|
90
|
+
hoveredIdx,
|
|
91
|
+
setHoveredIdx,
|
|
92
|
+
});
|
|
100
93
|
const selectedIndex = tableProps.tickers.findIndex(
|
|
101
94
|
(ticker) => ticker.metadata.ticker === tableProps.cursorSymbol,
|
|
102
95
|
);
|
|
@@ -113,24 +106,11 @@ export function TickerListTableView({
|
|
|
113
106
|
onVisibleRangeChange({ start, end });
|
|
114
107
|
}, [effectiveScrollRef, onVisibleRangeChange, tableProps.tickers.length, visibleRangeBuffer]);
|
|
115
108
|
|
|
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]);
|
|
109
|
+
const handleBodyScrollActivity = useTableBodyScrollActivity({
|
|
110
|
+
onBodyScrollActivity,
|
|
111
|
+
syncHeaderScroll: effectiveSyncHeaderScroll,
|
|
112
|
+
afterScroll: emitVisibleRange,
|
|
113
|
+
});
|
|
134
114
|
|
|
135
115
|
useEffect(() => {
|
|
136
116
|
const scrollBox = effectiveScrollRef.current;
|
|
@@ -139,19 +119,12 @@ export function TickerListTableView({
|
|
|
139
119
|
emitVisibleRange();
|
|
140
120
|
}, [emitVisibleRange, effectiveScrollRef, rootHeight, tableProps.tickers.length]);
|
|
141
121
|
|
|
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]);
|
|
122
|
+
useResetTableScroll({
|
|
123
|
+
headerScrollRef: effectiveHeaderScrollRef,
|
|
124
|
+
scrollRef: effectiveScrollRef,
|
|
125
|
+
resetScrollKey,
|
|
126
|
+
afterReset: emitVisibleRange,
|
|
127
|
+
});
|
|
155
128
|
|
|
156
129
|
const selectIndex = useCallback((index: number) => {
|
|
157
130
|
if (index < 0 || index >= tableProps.tickers.length) return;
|
|
@@ -192,19 +165,19 @@ export function TickerListTableView({
|
|
|
192
165
|
if (onRootKeyDown?.(event)) return;
|
|
193
166
|
if (tableProps.tickers.length === 0) return;
|
|
194
167
|
|
|
195
|
-
if (
|
|
168
|
+
if (isNextTableRowKey(event.name)) {
|
|
196
169
|
stopTableKey(event);
|
|
197
170
|
selectByOffset(1);
|
|
198
171
|
return;
|
|
199
172
|
}
|
|
200
173
|
|
|
201
|
-
if (
|
|
174
|
+
if (isPreviousTableRowKey(event.name)) {
|
|
202
175
|
stopTableKey(event);
|
|
203
176
|
selectByOffset(-1);
|
|
204
177
|
return;
|
|
205
178
|
}
|
|
206
179
|
|
|
207
|
-
if (
|
|
180
|
+
if (isTableActivationKey(event.name)) {
|
|
208
181
|
stopTableKey(event);
|
|
209
182
|
activateSelection();
|
|
210
183
|
}
|
|
@@ -224,15 +197,13 @@ export function TickerListTableView({
|
|
|
224
197
|
}, [effectiveScrollRef, emitVisibleRange, selectedIndex]);
|
|
225
198
|
|
|
226
199
|
return (
|
|
227
|
-
<
|
|
228
|
-
flexDirection="column"
|
|
229
|
-
flexGrow={1}
|
|
200
|
+
<TableViewFrame
|
|
230
201
|
width={rootWidth}
|
|
231
202
|
height={rootHeight}
|
|
232
203
|
backgroundColor={rootBackgroundColor}
|
|
233
|
-
|
|
204
|
+
before={rootBefore}
|
|
205
|
+
after={rootAfter}
|
|
234
206
|
>
|
|
235
|
-
{rootBefore}
|
|
236
207
|
<TickerListTable
|
|
237
208
|
{...tableProps}
|
|
238
209
|
headerScrollRef={effectiveHeaderScrollRef}
|
|
@@ -242,7 +213,6 @@ export function TickerListTableView({
|
|
|
242
213
|
hoveredIdx={effectiveHoveredIdx}
|
|
243
214
|
setHoveredIdx={effectiveSetHoveredIdx}
|
|
244
215
|
/>
|
|
245
|
-
|
|
246
|
-
</Box>
|
|
216
|
+
</TableViewFrame>
|
|
247
217
|
);
|
|
248
218
|
}
|
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,7 @@ import { EmptyState } from "./status";
|
|
|
13
13
|
import {
|
|
14
14
|
expandTableColumns,
|
|
15
15
|
getTableWidth,
|
|
16
|
+
hasMeaningfulTableHorizontalOverflow,
|
|
16
17
|
tableContentWidthProps,
|
|
17
18
|
useMeasuredTableContentWidth,
|
|
18
19
|
} from "./table-layout";
|
|
@@ -199,10 +200,16 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
199
200
|
],
|
|
200
201
|
);
|
|
201
202
|
const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
|
|
202
|
-
const {
|
|
203
|
+
const {
|
|
204
|
+
contentWidth: measuredContentWidth,
|
|
205
|
+
viewportWidth: measuredViewportWidth,
|
|
206
|
+
measureContentWidth,
|
|
207
|
+
} = useMeasuredTableContentWidth(tableWidth, headerScrollRef, scrollRef);
|
|
208
|
+
const horizontalScrollbarVisible = showHorizontalScrollbar
|
|
209
|
+
&& hasMeaningfulTableHorizontalOverflow(tableWidth, measuredViewportWidth);
|
|
203
210
|
const displayColumns = useMemo(
|
|
204
|
-
() => expandTableColumns(columns,
|
|
205
|
-
[columns,
|
|
211
|
+
() => expandTableColumns(columns, measuredContentWidth),
|
|
212
|
+
[columns, measuredContentWidth],
|
|
206
213
|
);
|
|
207
214
|
const handleRowMouseDown =
|
|
208
215
|
useDoubleClickActivation<DataTableRowPointerTarget<T>>({
|
|
@@ -269,17 +276,18 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
269
276
|
useEffect(() => {
|
|
270
277
|
if (headerScrollRef.current) {
|
|
271
278
|
headerScrollRef.current.horizontalScrollBar.visible = false;
|
|
272
|
-
if (!
|
|
279
|
+
if (!horizontalScrollbarVisible) {
|
|
273
280
|
headerScrollRef.current.scrollLeft = 0;
|
|
274
281
|
}
|
|
275
282
|
}
|
|
276
283
|
if (scrollRef.current) {
|
|
277
|
-
scrollRef.current.horizontalScrollBar.visible =
|
|
278
|
-
|
|
284
|
+
scrollRef.current.horizontalScrollBar.visible = horizontalScrollbarVisible;
|
|
285
|
+
scrollRef.current.verticalScrollBar.visible = items.length > scrollRef.current.viewport.height;
|
|
286
|
+
if (!horizontalScrollbarVisible) {
|
|
279
287
|
scrollRef.current.scrollLeft = 0;
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
|
-
}, [columns.length, headerScrollRef, items.length,
|
|
290
|
+
}, [columns.length, headerScrollRef, horizontalScrollbarVisible, items.length, measuredViewportHeight, scrollRef]);
|
|
283
291
|
|
|
284
292
|
useEffect(() => {
|
|
285
293
|
if (applyScrollToIndex()) return;
|
|
@@ -318,7 +326,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
318
326
|
<Box
|
|
319
327
|
flexDirection="row"
|
|
320
328
|
height={1}
|
|
321
|
-
{...tableContentWidthProps(
|
|
329
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
322
330
|
paddingX={1}
|
|
323
331
|
backgroundColor={colors.panel}
|
|
324
332
|
>
|
|
@@ -396,7 +404,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
396
404
|
key={getItemKey(item, index)}
|
|
397
405
|
flexDirection="row"
|
|
398
406
|
height={1}
|
|
399
|
-
{...tableContentWidthProps(
|
|
407
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
400
408
|
paddingX={1}
|
|
401
409
|
backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
|
|
402
410
|
onMouseDown={(event) => {
|
|
@@ -433,7 +441,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
433
441
|
key={getItemKey(item, index)}
|
|
434
442
|
flexDirection="row"
|
|
435
443
|
height={1}
|
|
436
|
-
{...tableContentWidthProps(
|
|
444
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
437
445
|
paddingX={1}
|
|
438
446
|
backgroundColor={rowBg}
|
|
439
447
|
onMouseMove={() => {
|
|
@@ -6,7 +6,6 @@ export { DataTable } from "./data-table";
|
|
|
6
6
|
export type { DataTableCell, DataTableColumn, DataTableProps } from "./data-table";
|
|
7
7
|
|
|
8
8
|
export { Button } from "./button";
|
|
9
|
-
|
|
10
9
|
export { ShortcutHint } from "./shortcut-hint";
|
|
11
10
|
|
|
12
11
|
export { MultiSelectDialogButton, MultiSelectDialogContent } from "./multi-select-dialog";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Box, Text, TextAttributes, useUiHost } from "../../ui";
|
|
2
2
|
import { useShortcut } from "../../react/input";
|
|
3
|
-
import { type ComponentType, type ReactNode } from "react";
|
|
3
|
+
import { useCallback, type ComponentType, type ReactNode } from "react";
|
|
4
4
|
import { colors } from "../../theme/colors";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
isDetailBackNavigationKey,
|
|
7
|
+
isMouseBackNavigationEvent,
|
|
8
|
+
} from "../../utils/back-navigation";
|
|
6
9
|
|
|
7
10
|
export interface PageStackViewProps {
|
|
8
11
|
focused: boolean;
|
|
@@ -41,13 +44,29 @@ export function PageStackView({
|
|
|
41
44
|
);
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const goBack = useCallback((event?: {
|
|
48
|
+
preventDefault?: () => void;
|
|
49
|
+
stopPropagation?: () => void;
|
|
50
|
+
}) => {
|
|
51
|
+
event?.stopPropagation?.();
|
|
52
|
+
event?.preventDefault?.();
|
|
53
|
+
onBack();
|
|
54
|
+
}, [onBack]);
|
|
55
|
+
|
|
44
56
|
useShortcut((event) => {
|
|
45
57
|
if (!focused || !detailOpen || !isDetailBackNavigationKey(event)) return;
|
|
46
|
-
event
|
|
47
|
-
event.preventDefault?.();
|
|
48
|
-
onBack();
|
|
58
|
+
goBack(event);
|
|
49
59
|
});
|
|
50
60
|
|
|
61
|
+
const handleMouseDown = useCallback((event: {
|
|
62
|
+
button?: unknown;
|
|
63
|
+
preventDefault?: () => void;
|
|
64
|
+
stopPropagation?: () => void;
|
|
65
|
+
}) => {
|
|
66
|
+
if (!focused || !detailOpen || !isMouseBackNavigationEvent(event)) return;
|
|
67
|
+
goBack(event);
|
|
68
|
+
}, [detailOpen, focused, goBack]);
|
|
69
|
+
|
|
51
70
|
if (!detailOpen) {
|
|
52
71
|
return (
|
|
53
72
|
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
@@ -57,16 +76,26 @@ export function PageStackView({
|
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
return (
|
|
60
|
-
<Box
|
|
79
|
+
<Box
|
|
80
|
+
flexDirection="column"
|
|
81
|
+
flexGrow={1}
|
|
82
|
+
overflow="hidden"
|
|
83
|
+
onMouseDown={handleMouseDown}
|
|
84
|
+
>
|
|
61
85
|
<Box height={1} flexDirection="row">
|
|
62
86
|
<Box
|
|
63
87
|
onMouseDown={(event) => {
|
|
64
|
-
event
|
|
65
|
-
event.stopPropagation?.();
|
|
66
|
-
onBack();
|
|
88
|
+
goBack(event);
|
|
67
89
|
}}
|
|
90
|
+
backgroundColor={colors.selected}
|
|
68
91
|
>
|
|
69
|
-
<Text
|
|
92
|
+
<Text
|
|
93
|
+
fg={colors.selectedText}
|
|
94
|
+
bg={colors.selected}
|
|
95
|
+
attributes={TextAttributes.BOLD}
|
|
96
|
+
>
|
|
97
|
+
{`← ${backLabel}`}
|
|
98
|
+
</Text>
|
|
70
99
|
</Box>
|
|
71
100
|
{detailTitle ? (
|
|
72
101
|
<>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, ImageSurface, Text
|
|
1
|
+
import { Box, ImageSurface, Text } from "../../ui";
|
|
2
2
|
import { colors } from "../../theme/colors";
|
|
3
3
|
import { ExternalLinkText } from "./external-link";
|
|
4
4
|
import { normalizedHttpUrl } from "../../utils/url";
|
|
@@ -25,51 +25,26 @@ export function RemoteImage({
|
|
|
25
25
|
height = 12,
|
|
26
26
|
label = "image",
|
|
27
27
|
}: RemoteImageProps) {
|
|
28
|
-
const ui = useUiHost();
|
|
29
28
|
const resolvedWidth = Math.max(12, width);
|
|
30
29
|
const imageUrl = normalizedHttpUrl(src);
|
|
31
30
|
if (!imageUrl) return null;
|
|
32
31
|
|
|
33
|
-
if (ui.kind === "desktop-web") {
|
|
34
|
-
return (
|
|
35
|
-
<ImageSurface
|
|
36
|
-
src={imageUrl}
|
|
37
|
-
alt={alt}
|
|
38
|
-
width={resolvedWidth}
|
|
39
|
-
height={Math.max(4, height)}
|
|
40
|
-
border
|
|
41
|
-
borderColor={colors.border}
|
|
42
|
-
backgroundColor={colors.panel}
|
|
43
|
-
objectFit="contain"
|
|
44
|
-
>
|
|
45
|
-
<Box paddingX={1} paddingY={1} flexDirection="column" gap={1}>
|
|
46
|
-
<Text fg={colors.textDim}>{label}</Text>
|
|
47
|
-
<ExternalLinkText
|
|
48
|
-
url={imageUrl}
|
|
49
|
-
label={truncate(imageUrl, Math.max(1, resolvedWidth - 2))}
|
|
50
|
-
color={colors.textBright}
|
|
51
|
-
/>
|
|
52
|
-
</Box>
|
|
53
|
-
</ImageSurface>
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
32
|
return (
|
|
58
|
-
<
|
|
33
|
+
<ImageSurface
|
|
34
|
+
src={imageUrl}
|
|
35
|
+
alt={alt}
|
|
59
36
|
width={resolvedWidth}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
borderColor={colors.border}
|
|
63
|
-
backgroundColor={colors.panel}
|
|
64
|
-
paddingX={1}
|
|
65
|
-
flexDirection="column"
|
|
37
|
+
height={Math.max(4, height)}
|
|
38
|
+
objectFit="contain"
|
|
66
39
|
>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
40
|
+
<Box flexDirection="column" gap={1}>
|
|
41
|
+
<Text fg={colors.textDim}>{label}</Text>
|
|
42
|
+
<ExternalLinkText
|
|
43
|
+
url={imageUrl}
|
|
44
|
+
label={truncate(imageUrl, resolvedWidth)}
|
|
45
|
+
color={colors.textBright}
|
|
46
|
+
/>
|
|
47
|
+
</Box>
|
|
48
|
+
</ImageSurface>
|
|
74
49
|
);
|
|
75
50
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getTableWidth, hasMeaningfulTableHorizontalOverflow } from "./table-layout";
|
|
3
|
+
|
|
4
|
+
describe("table layout", () => {
|
|
5
|
+
test("detects only meaningful horizontal table overflow", () => {
|
|
6
|
+
const tableWidth = getTableWidth([
|
|
7
|
+
{ width: 12 },
|
|
8
|
+
{ width: 8 },
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, 0)).toBe(false);
|
|
12
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth)).toBe(false);
|
|
13
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 1)).toBe(false);
|
|
14
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 2)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -6,10 +6,16 @@ interface TableWidthColumn {
|
|
|
6
6
|
flexGrow?: number;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
const TRAILING_COLUMN_GUTTER_WIDTH = 1;
|
|
10
|
+
|
|
9
11
|
export function getTableWidth(columns: readonly TableWidthColumn[]): number {
|
|
10
12
|
return columns.reduce((sum, column) => sum + column.width + 1, 2);
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
export function hasMeaningfulTableHorizontalOverflow(tableWidth: number, viewportWidth: number): boolean {
|
|
16
|
+
return viewportWidth > 0 && tableWidth - viewportWidth > TRAILING_COLUMN_GUTTER_WIDTH;
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
export function expandTableColumns<C extends TableWidthColumn>(
|
|
14
20
|
columns: readonly C[],
|
|
15
21
|
targetWidth: number,
|
|
@@ -34,9 +40,9 @@ export function useMeasuredTableContentWidth(
|
|
|
34
40
|
const [viewportWidth, setViewportWidth] = useState(0);
|
|
35
41
|
|
|
36
42
|
const measureContentWidth = useCallback(() => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const bodyWidth = scrollRef?.current?.viewport?.width || scrollRef?.current?.width || 0;
|
|
44
|
+
const headerWidth = headerScrollRef?.current?.viewport?.width || headerScrollRef?.current?.width || 0;
|
|
45
|
+
const nextWidth = bodyWidth || headerWidth;
|
|
40
46
|
setViewportWidth((current) => current === nextWidth ? current : nextWidth);
|
|
41
47
|
}, [headerScrollRef, scrollRef]);
|
|
42
48
|
const scheduleContentWidthMeasure = useCallback(() => {
|
|
@@ -48,6 +54,7 @@ export function useMeasuredTableContentWidth(
|
|
|
48
54
|
|
|
49
55
|
return {
|
|
50
56
|
contentWidth: Math.max(tableWidth, viewportWidth),
|
|
57
|
+
viewportWidth,
|
|
51
58
|
measureContentWidth: scheduleContentWidthMeasure,
|
|
52
59
|
};
|
|
53
60
|
}
|