gloomberb 0.6.1 → 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 +1 -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 +48 -27
- 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 +38 -21
- 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 +5 -1
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- 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/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/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-chart-tab-switch.test.tsx +3 -4
- 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 +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 +1 -0
- 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 +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- 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/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -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 +1 -0
- 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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Box, Text } from "../../ui";
|
|
2
|
+
import { colors } from "../../theme/colors";
|
|
3
|
+
import { Button, Spinner } from "../ui";
|
|
4
|
+
import { truncateText } from "./view-model";
|
|
5
|
+
import type { CommandBarConfirmRoute } from "./workflow-types";
|
|
6
|
+
|
|
7
|
+
export function CommandBarConfirmBody({
|
|
8
|
+
route,
|
|
9
|
+
bodyHeight,
|
|
10
|
+
contentPadding,
|
|
11
|
+
paletteText,
|
|
12
|
+
queryDisplayWidth,
|
|
13
|
+
onConfirm,
|
|
14
|
+
}: {
|
|
15
|
+
route: CommandBarConfirmRoute;
|
|
16
|
+
bodyHeight: number;
|
|
17
|
+
contentPadding: number;
|
|
18
|
+
paletteText: string;
|
|
19
|
+
queryDisplayWidth: number;
|
|
20
|
+
onConfirm: () => void;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<Box flexDirection="column" height={bodyHeight} paddingX={contentPadding}>
|
|
24
|
+
{route.body.map((line, index) => (
|
|
25
|
+
<Box key={`confirm:${index}`} height={1}>
|
|
26
|
+
<Text fg={paletteText}>{truncateText(line, queryDisplayWidth)}</Text>
|
|
27
|
+
</Box>
|
|
28
|
+
))}
|
|
29
|
+
<Box height={1} />
|
|
30
|
+
{route.error && (
|
|
31
|
+
<Box height={1}>
|
|
32
|
+
<Text fg={colors.negative}>{truncateText(route.error, queryDisplayWidth)}</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
)}
|
|
35
|
+
{route.pending && (
|
|
36
|
+
<Box height={1}>
|
|
37
|
+
<Spinner label="Working…" />
|
|
38
|
+
</Box>
|
|
39
|
+
)}
|
|
40
|
+
<Box flexGrow={1} />
|
|
41
|
+
<Box flexDirection="row" gap={1}>
|
|
42
|
+
<Button
|
|
43
|
+
label={route.confirmLabel}
|
|
44
|
+
variant={route.tone === "danger" ? "danger" : "primary"}
|
|
45
|
+
onPress={onConfirm}
|
|
46
|
+
disabled={route.pending}
|
|
47
|
+
/>
|
|
48
|
+
</Box>
|
|
49
|
+
</Box>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { buildSections } from "./view-model";
|
|
2
|
+
|
|
3
|
+
export interface ResultItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
detail: string;
|
|
7
|
+
category: string;
|
|
8
|
+
kind: "command" | "ticker" | "search" | "theme" | "plugin" | "action" | "info";
|
|
9
|
+
right?: string;
|
|
10
|
+
searchText?: string;
|
|
11
|
+
themeId?: string;
|
|
12
|
+
pluginToggle?: () => void | Promise<void>;
|
|
13
|
+
secondaryAction?: () => void | Promise<void>;
|
|
14
|
+
checked?: boolean;
|
|
15
|
+
current?: boolean;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
action: () => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type ListScreenKind = "root" | "mode" | "picker" | "pane-settings";
|
|
21
|
+
|
|
22
|
+
export interface ListScreenState {
|
|
23
|
+
kind: ListScreenKind;
|
|
24
|
+
title: string;
|
|
25
|
+
subtitle?: string;
|
|
26
|
+
query: string;
|
|
27
|
+
selectedIdx: number;
|
|
28
|
+
hoveredIdx: number | null;
|
|
29
|
+
results: ResultItem[];
|
|
30
|
+
searching: boolean;
|
|
31
|
+
emptyLabel: string;
|
|
32
|
+
emptyDetail: string;
|
|
33
|
+
footerLeft: string;
|
|
34
|
+
footerRight: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type CommandBarListRow =
|
|
38
|
+
| { kind: "spacer"; id: string }
|
|
39
|
+
| { kind: "heading"; id: string; label: string }
|
|
40
|
+
| { kind: "item"; item: ResultItem; globalIdx: number }
|
|
41
|
+
| { kind: "message"; id: string; label: string; dim?: boolean }
|
|
42
|
+
| { kind: "spinner"; id: string; label: string }
|
|
43
|
+
| { kind: "filler"; id: string };
|
|
44
|
+
|
|
45
|
+
export function orderListResults(results: ResultItem[]): ResultItem[] {
|
|
46
|
+
return buildSections(results).flatMap((section) => section.items);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildListRows(listState: ListScreenState): CommandBarListRow[] {
|
|
50
|
+
const rows: CommandBarListRow[] = [];
|
|
51
|
+
const sections = buildSections(listState.results);
|
|
52
|
+
let globalIdx = 0;
|
|
53
|
+
sections.forEach((section, sectionIndex) => {
|
|
54
|
+
if (sectionIndex > 0) {
|
|
55
|
+
rows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
|
|
56
|
+
}
|
|
57
|
+
rows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
|
|
58
|
+
for (const item of section.items) {
|
|
59
|
+
rows.push({ kind: "item", item, globalIdx });
|
|
60
|
+
globalIdx += 1;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function buildNativeListRows(listState: ListScreenState, rows: CommandBarListRow[]): CommandBarListRow[] {
|
|
67
|
+
if (listState.searching && rows.length === 0) {
|
|
68
|
+
return [{ kind: "spinner", id: "searching", label: "Searching…" }];
|
|
69
|
+
}
|
|
70
|
+
if (rows.length === 0) {
|
|
71
|
+
return [{ kind: "message", id: "empty", label: listState.emptyLabel }];
|
|
72
|
+
}
|
|
73
|
+
if (listState.searching) {
|
|
74
|
+
return [...rows, { kind: "spinner", id: "searching", label: "Searching…" }];
|
|
75
|
+
}
|
|
76
|
+
return rows;
|
|
77
|
+
}
|
|
@@ -10,11 +10,6 @@ export interface CommandBarModeInfo {
|
|
|
10
10
|
hint: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface CommandBarFooterHints {
|
|
14
|
-
left: string;
|
|
15
|
-
right: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
export interface CommandBarSection<T> {
|
|
19
14
|
category: string;
|
|
20
15
|
items: T[];
|
|
@@ -83,20 +78,6 @@ export function buildSections<T extends { category: string }>(items: T[]): Array
|
|
|
83
78
|
.map(({ section }) => section);
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
export function getFooterHints(mode: CommandBarMode, isNarrow: boolean): CommandBarFooterHints {
|
|
87
|
-
const moveAndSelect = isNarrow ? "up/down move enter select" : "up/down move enter select";
|
|
88
|
-
if (mode === "plugins") {
|
|
89
|
-
return {
|
|
90
|
-
left: isNarrow ? "space toggle" : `${moveAndSelect} space toggle`,
|
|
91
|
-
right: "esc cancel",
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
if (mode === "layout") {
|
|
95
|
-
return { left: moveAndSelect, right: "esc cancel" };
|
|
96
|
-
}
|
|
97
|
-
return { left: moveAndSelect, right: "esc cancel" };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
81
|
export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?: string): { label: string; detail: string } {
|
|
101
82
|
switch (mode) {
|
|
102
83
|
case "search":
|
|
@@ -14,6 +14,7 @@ import { cleanPortfolioPaneSettings, resolvePortfolioPaneCollectionId } from "..
|
|
|
14
14
|
import {
|
|
15
15
|
buildComparisonChartPaneTitle,
|
|
16
16
|
COMPARISON_CHART_PANE_ID,
|
|
17
|
+
MIN_COMPARISON_CHART_SYMBOLS,
|
|
17
18
|
} from "../../plugins/builtin/comparison-chart";
|
|
18
19
|
import { getPaneTemplateDisplayLabel } from "./pane-template-display";
|
|
19
20
|
|
|
@@ -415,6 +416,9 @@ export async function applyPaneSettingFieldValue(
|
|
|
415
416
|
descriptor.context.activeCollectionId,
|
|
416
417
|
deps,
|
|
417
418
|
);
|
|
419
|
+
if (symbols.length < MIN_COMPARISON_CHART_SYMBOLS) {
|
|
420
|
+
throw new Error(`Enter at least ${MIN_COMPARISON_CHART_SYMBOLS} tickers.`);
|
|
421
|
+
}
|
|
418
422
|
const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
|
|
419
423
|
...instance,
|
|
420
424
|
title: buildComparisonChartPaneTitle(symbols),
|
|
@@ -160,6 +160,13 @@ async function emitKeypress(event: { name?: string; sequence?: string }) {
|
|
|
160
160
|
});
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
async function emitTerminalMouseBack(x: number, y: number) {
|
|
164
|
+
await act(async () => {
|
|
165
|
+
await testSetup!.mockMouse.emitMouseEvent("down", x, y, 128 as any);
|
|
166
|
+
await testSetup!.renderOnce();
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
describe("DataTableStackView", () => {
|
|
164
171
|
test("owns table navigation, detail open, and back navigation", async () => {
|
|
165
172
|
testSetup = await testRender(<Harness />, { width: 60, height: 12 });
|
|
@@ -179,16 +186,44 @@ describe("DataTableStackView", () => {
|
|
|
179
186
|
|
|
180
187
|
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
181
188
|
await renderSettled();
|
|
182
|
-
expect(testSetup.captureCharFrame()).toContain("Second detail");
|
|
183
|
-
|
|
184
|
-
await emitKeypress({ name: "backspace", sequence: "\u007f" });
|
|
185
|
-
await renderSettled();
|
|
186
189
|
|
|
187
190
|
const rootFrame = testSetup.captureCharFrame();
|
|
188
191
|
expect(rootFrame).toContain("Second row");
|
|
189
192
|
expect(rootFrame).not.toContain("Second detail");
|
|
190
193
|
});
|
|
191
194
|
|
|
195
|
+
test("closes detail from backspace", async () => {
|
|
196
|
+
testSetup = await testRender(<Harness />, { width: 60, height: 12 });
|
|
197
|
+
|
|
198
|
+
await renderSettled();
|
|
199
|
+
await emitKeypress({ name: "enter", sequence: "\r" });
|
|
200
|
+
await renderSettled();
|
|
201
|
+
expect(testSetup.captureCharFrame()).toContain("First detail");
|
|
202
|
+
|
|
203
|
+
await emitKeypress({ name: "backspace", sequence: "\u007f" });
|
|
204
|
+
await renderSettled();
|
|
205
|
+
|
|
206
|
+
const frame = testSetup.captureCharFrame();
|
|
207
|
+
expect(frame).toContain("First row");
|
|
208
|
+
expect(frame).not.toContain("First detail");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("closes detail from the terminal mouse back button", async () => {
|
|
212
|
+
testSetup = await testRender(<Harness />, { width: 60, height: 12 });
|
|
213
|
+
|
|
214
|
+
await renderSettled();
|
|
215
|
+
await emitKeypress({ name: "enter", sequence: "\r" });
|
|
216
|
+
await renderSettled();
|
|
217
|
+
expect(testSetup.captureCharFrame()).toContain("First detail");
|
|
218
|
+
|
|
219
|
+
await emitTerminalMouseBack(30, 4);
|
|
220
|
+
await renderSettled();
|
|
221
|
+
|
|
222
|
+
const frame = testSetup.captureCharFrame();
|
|
223
|
+
expect(frame).toContain("First row");
|
|
224
|
+
expect(frame).not.toContain("First detail");
|
|
225
|
+
});
|
|
226
|
+
|
|
192
227
|
test("skips section rows while navigating through the stack view", async () => {
|
|
193
228
|
testSetup = await testRender(<GroupedHarness />, { width: 60, height: 12 });
|
|
194
229
|
|
|
@@ -1,50 +1,23 @@
|
|
|
1
1
|
import { useShortcut } from "../react/input";
|
|
2
|
-
import { type
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
preventDefault?: () => void;
|
|
10
|
-
stopPropagation?: () => void;
|
|
11
|
-
}
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
DataTableView,
|
|
5
|
+
type DataTableKeyEvent,
|
|
6
|
+
type DataTableViewProps,
|
|
7
|
+
} from "./data-table-view";
|
|
8
|
+
import { PageStackView, type DataTableColumn } from "./ui";
|
|
12
9
|
|
|
13
10
|
export interface DataTableStackViewProps<
|
|
14
11
|
T,
|
|
15
12
|
C extends DataTableColumn = DataTableColumn,
|
|
16
|
-
> extends Omit<
|
|
17
|
-
DataTableProps<T, C>,
|
|
18
|
-
| "headerScrollRef"
|
|
19
|
-
| "scrollRef"
|
|
20
|
-
| "syncHeaderScroll"
|
|
21
|
-
| "onBodyScrollActivity"
|
|
22
|
-
| "hoveredIdx"
|
|
23
|
-
| "setHoveredIdx"
|
|
24
|
-
> {
|
|
13
|
+
> extends Omit<DataTableViewProps<T, C>, "focused" | "selectedIndex"> {
|
|
25
14
|
focused: boolean;
|
|
26
15
|
detailOpen: boolean;
|
|
27
16
|
onBack: () => void;
|
|
28
17
|
detailContent: ReactNode;
|
|
29
18
|
detailTitle?: string;
|
|
30
19
|
selectedIndex: number;
|
|
31
|
-
|
|
32
|
-
onActivateIndex?: (index: number, item: T) => void;
|
|
33
|
-
isNavigable?: (item: T, index: number) => boolean;
|
|
34
|
-
rootBefore?: ReactNode;
|
|
35
|
-
rootAfter?: ReactNode;
|
|
36
|
-
rootWidth?: number;
|
|
37
|
-
rootHeight?: number;
|
|
38
|
-
rootBackgroundColor?: string;
|
|
39
|
-
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
40
|
-
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
41
|
-
syncHeaderScroll?: () => void;
|
|
42
|
-
onBodyScrollActivity?: () => void;
|
|
43
|
-
hoveredIdx?: number | null;
|
|
44
|
-
setHoveredIdx?: (index: number | null) => void;
|
|
45
|
-
keyboardNavigation?: boolean;
|
|
46
|
-
onRootKeyDown?: (event: KeyboardEventLike) => boolean | void;
|
|
47
|
-
onDetailKeyDown?: (event: KeyboardEventLike) => boolean | void;
|
|
20
|
+
onDetailKeyDown?: (event: DataTableKeyEvent) => boolean | void;
|
|
48
21
|
}
|
|
49
22
|
|
|
50
23
|
export function DataTableStackView<
|
|
@@ -56,42 +29,9 @@ export function DataTableStackView<
|
|
|
56
29
|
onBack,
|
|
57
30
|
detailContent,
|
|
58
31
|
detailTitle,
|
|
59
|
-
selectedIndex,
|
|
60
|
-
onSelectIndex,
|
|
61
|
-
onActivateIndex,
|
|
62
|
-
isNavigable,
|
|
63
|
-
rootBefore,
|
|
64
|
-
rootAfter,
|
|
65
|
-
rootWidth,
|
|
66
|
-
rootHeight,
|
|
67
|
-
rootBackgroundColor,
|
|
68
|
-
headerScrollRef,
|
|
69
|
-
scrollRef,
|
|
70
|
-
syncHeaderScroll,
|
|
71
|
-
onBodyScrollActivity,
|
|
72
|
-
hoveredIdx,
|
|
73
|
-
setHoveredIdx,
|
|
74
32
|
keyboardNavigation = true,
|
|
75
|
-
onRootKeyDown,
|
|
76
33
|
onDetailKeyDown,
|
|
77
|
-
|
|
78
|
-
items,
|
|
79
|
-
sortColumnId,
|
|
80
|
-
sortDirection,
|
|
81
|
-
onHeaderClick,
|
|
82
|
-
getItemKey,
|
|
83
|
-
isSelected,
|
|
84
|
-
onSelect,
|
|
85
|
-
onActivate,
|
|
86
|
-
renderCell,
|
|
87
|
-
renderSectionHeader,
|
|
88
|
-
getRowBackgroundColor,
|
|
89
|
-
emptyContent,
|
|
90
|
-
emptyStateTitle,
|
|
91
|
-
emptyStateHint,
|
|
92
|
-
virtualize = true,
|
|
93
|
-
overscan,
|
|
94
|
-
showHorizontalScrollbar,
|
|
34
|
+
...tableProps
|
|
95
35
|
}: DataTableStackViewProps<T, C>) {
|
|
96
36
|
useShortcut((event) => {
|
|
97
37
|
if (!focused || !detailOpen || !keyboardNavigation) return;
|
|
@@ -100,42 +40,9 @@ export function DataTableStackView<
|
|
|
100
40
|
|
|
101
41
|
const rootContent = (
|
|
102
42
|
<DataTableView<T, C>
|
|
43
|
+
{...tableProps}
|
|
103
44
|
focused={focused && !detailOpen}
|
|
104
|
-
selectedIndex={selectedIndex}
|
|
105
|
-
onSelectIndex={onSelectIndex}
|
|
106
|
-
onActivateIndex={onActivateIndex}
|
|
107
|
-
isNavigable={isNavigable}
|
|
108
|
-
rootBefore={rootBefore}
|
|
109
|
-
rootAfter={rootAfter}
|
|
110
|
-
rootWidth={rootWidth}
|
|
111
|
-
rootHeight={rootHeight}
|
|
112
|
-
rootBackgroundColor={rootBackgroundColor}
|
|
113
|
-
headerScrollRef={headerScrollRef}
|
|
114
|
-
scrollRef={scrollRef}
|
|
115
|
-
syncHeaderScroll={syncHeaderScroll}
|
|
116
|
-
onBodyScrollActivity={onBodyScrollActivity}
|
|
117
|
-
hoveredIdx={hoveredIdx}
|
|
118
|
-
setHoveredIdx={setHoveredIdx}
|
|
119
45
|
keyboardNavigation={keyboardNavigation}
|
|
120
|
-
onRootKeyDown={onRootKeyDown}
|
|
121
|
-
columns={columns}
|
|
122
|
-
items={items}
|
|
123
|
-
sortColumnId={sortColumnId}
|
|
124
|
-
sortDirection={sortDirection}
|
|
125
|
-
onHeaderClick={onHeaderClick}
|
|
126
|
-
getItemKey={getItemKey}
|
|
127
|
-
isSelected={isSelected}
|
|
128
|
-
onSelect={onSelect}
|
|
129
|
-
onActivate={onActivate}
|
|
130
|
-
renderCell={renderCell}
|
|
131
|
-
renderSectionHeader={renderSectionHeader}
|
|
132
|
-
getRowBackgroundColor={getRowBackgroundColor}
|
|
133
|
-
emptyContent={emptyContent}
|
|
134
|
-
emptyStateTitle={emptyStateTitle}
|
|
135
|
-
emptyStateHint={emptyStateHint}
|
|
136
|
-
virtualize={virtualize}
|
|
137
|
-
overscan={overscan}
|
|
138
|
-
showHorizontalScrollbar={showHorizontalScrollbar}
|
|
139
46
|
/>
|
|
140
47
|
);
|
|
141
48
|
|
|
@@ -2,24 +2,26 @@ import {
|
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
4
|
useMemo,
|
|
5
|
-
useRef,
|
|
6
5
|
useState,
|
|
7
6
|
type ReactNode,
|
|
8
7
|
type RefObject,
|
|
9
8
|
} from "react";
|
|
10
|
-
import {
|
|
9
|
+
import type { ScrollBoxRenderable } from "../ui";
|
|
11
10
|
import { useShortcut } from "../react/input";
|
|
12
11
|
import { DataTable, type DataTableColumn, type DataTableProps } from "./ui";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
import {
|
|
13
|
+
isNextTableRowKey,
|
|
14
|
+
isPreviousTableRowKey,
|
|
15
|
+
isTableActivationKey,
|
|
16
|
+
stopTableKey,
|
|
17
|
+
TableViewFrame,
|
|
18
|
+
type TableViewKeyEvent,
|
|
19
|
+
useResetTableScroll,
|
|
20
|
+
useTableBodyScrollActivity,
|
|
21
|
+
useTableViewState,
|
|
22
|
+
} from "./table-view-shared";
|
|
23
|
+
|
|
24
|
+
export type DataTableKeyEvent = TableViewKeyEvent;
|
|
23
25
|
|
|
24
26
|
export interface DataTableViewProps<
|
|
25
27
|
T,
|
|
@@ -54,23 +56,6 @@ export interface DataTableViewProps<
|
|
|
54
56
|
resetScrollKey?: unknown;
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
function isActivationKey(name: string | undefined): boolean {
|
|
58
|
-
return name === "enter" || name === "return";
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isNextRowKey(name: string | undefined): boolean {
|
|
62
|
-
return name === "j" || name === "down";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isPreviousRowKey(name: string | undefined): boolean {
|
|
66
|
-
return name === "k" || name === "up";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function stopTableKey(event: DataTableKeyEvent) {
|
|
70
|
-
event.stopPropagation?.();
|
|
71
|
-
event.preventDefault?.();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
59
|
export function DataTableView<
|
|
75
60
|
T,
|
|
76
61
|
C extends DataTableColumn = DataTableColumn,
|
|
@@ -97,17 +82,19 @@ export function DataTableView<
|
|
|
97
82
|
scrollToIndex,
|
|
98
83
|
...tableProps
|
|
99
84
|
}: DataTableViewProps<T, C>) {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
85
|
+
const {
|
|
86
|
+
effectiveHeaderScrollRef,
|
|
87
|
+
effectiveScrollRef,
|
|
88
|
+
effectiveHoveredIdx,
|
|
89
|
+
effectiveSetHoveredIdx,
|
|
90
|
+
effectiveSyncHeaderScroll,
|
|
91
|
+
} = useTableViewState({
|
|
92
|
+
headerScrollRef,
|
|
93
|
+
scrollRef,
|
|
94
|
+
syncHeaderScroll,
|
|
95
|
+
hoveredIdx,
|
|
96
|
+
setHoveredIdx,
|
|
97
|
+
});
|
|
111
98
|
const [fallbackSelectedIndex, setFallbackSelectedIndex] = useState<
|
|
112
99
|
number | null
|
|
113
100
|
>(null);
|
|
@@ -148,36 +135,16 @@ export function DataTableView<
|
|
|
148
135
|
setFallbackSelectedIndex(nextIndex);
|
|
149
136
|
}, [fallbackSelectedIndex, navigableIndices, usesFallbackSelection]);
|
|
150
137
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (header.scrollLeft !== body.scrollLeft) {
|
|
156
|
-
header.scrollLeft = body.scrollLeft;
|
|
157
|
-
}
|
|
158
|
-
}, [effectiveHeaderScrollRef, effectiveScrollRef]);
|
|
159
|
-
const effectiveSyncHeaderScroll = syncHeaderScroll ?? defaultSyncHeaderScroll;
|
|
160
|
-
|
|
161
|
-
const handleBodyScrollActivity = useCallback(() => {
|
|
162
|
-
if (onBodyScrollActivity) {
|
|
163
|
-
onBodyScrollActivity();
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
queueMicrotask(effectiveSyncHeaderScroll);
|
|
167
|
-
}, [effectiveSyncHeaderScroll, onBodyScrollActivity]);
|
|
138
|
+
const handleBodyScrollActivity = useTableBodyScrollActivity({
|
|
139
|
+
onBodyScrollActivity,
|
|
140
|
+
syncHeaderScroll: effectiveSyncHeaderScroll,
|
|
141
|
+
});
|
|
168
142
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
body.scrollLeft = 0;
|
|
175
|
-
}
|
|
176
|
-
const header = effectiveHeaderScrollRef.current;
|
|
177
|
-
if (header) {
|
|
178
|
-
header.scrollLeft = 0;
|
|
179
|
-
}
|
|
180
|
-
}, [effectiveHeaderScrollRef, effectiveScrollRef, resetScrollKey]);
|
|
143
|
+
useResetTableScroll({
|
|
144
|
+
headerScrollRef: effectiveHeaderScrollRef,
|
|
145
|
+
scrollRef: effectiveScrollRef,
|
|
146
|
+
resetScrollKey,
|
|
147
|
+
});
|
|
181
148
|
|
|
182
149
|
const selectIndex = useCallback((index: number) => {
|
|
183
150
|
if (index < 0 || index >= tableProps.items.length) return;
|
|
@@ -226,34 +193,32 @@ export function DataTableView<
|
|
|
226
193
|
if (onRootKeyDown?.(event)) return;
|
|
227
194
|
if (tableProps.items.length === 0) return;
|
|
228
195
|
|
|
229
|
-
if (
|
|
196
|
+
if (isNextTableRowKey(event.name)) {
|
|
230
197
|
stopTableKey(event);
|
|
231
198
|
selectByOffset(1);
|
|
232
199
|
return;
|
|
233
200
|
}
|
|
234
201
|
|
|
235
|
-
if (
|
|
202
|
+
if (isPreviousTableRowKey(event.name)) {
|
|
236
203
|
stopTableKey(event);
|
|
237
204
|
selectByOffset(-1);
|
|
238
205
|
return;
|
|
239
206
|
}
|
|
240
207
|
|
|
241
|
-
if (
|
|
208
|
+
if (isTableActivationKey(event.name)) {
|
|
242
209
|
stopTableKey(event);
|
|
243
210
|
activateSelection();
|
|
244
211
|
}
|
|
245
212
|
});
|
|
246
213
|
|
|
247
214
|
return (
|
|
248
|
-
<
|
|
249
|
-
flexDirection="column"
|
|
250
|
-
flexGrow={1}
|
|
215
|
+
<TableViewFrame
|
|
251
216
|
width={rootWidth}
|
|
252
217
|
height={rootHeight}
|
|
253
218
|
backgroundColor={rootBackgroundColor}
|
|
254
|
-
|
|
219
|
+
before={rootBefore}
|
|
220
|
+
after={rootAfter}
|
|
255
221
|
>
|
|
256
|
-
{rootBefore}
|
|
257
222
|
<DataTable<T, C>
|
|
258
223
|
{...tableProps}
|
|
259
224
|
headerScrollRef={effectiveHeaderScrollRef}
|
|
@@ -273,7 +238,6 @@ export function DataTableView<
|
|
|
273
238
|
)
|
|
274
239
|
)}
|
|
275
240
|
/>
|
|
276
|
-
|
|
277
|
-
</Box>
|
|
241
|
+
</TableViewFrame>
|
|
278
242
|
);
|
|
279
243
|
}
|
|
@@ -5,6 +5,12 @@ import { colors } from "../theme/colors";
|
|
|
5
5
|
import { formatTimeAgo } from "../utils/format";
|
|
6
6
|
import { toTimestampMillis } from "../utils/timestamp";
|
|
7
7
|
import { DataTableStackView } from "./data-table-stack-view";
|
|
8
|
+
import {
|
|
9
|
+
activeStackIndex,
|
|
10
|
+
sortIndexedStackRows,
|
|
11
|
+
type IndexedStackRow,
|
|
12
|
+
type StackSortPreference,
|
|
13
|
+
} from "./feed-stack-controller";
|
|
8
14
|
import { ExternalLink, type DataTableCell, type DataTableColumn } from "./ui";
|
|
9
15
|
import { wrapTextLines } from "../utils/text-wrap";
|
|
10
16
|
|
|
@@ -23,15 +29,9 @@ export interface FeedDataTableItem {
|
|
|
23
29
|
type DetailColumnId = "time" | "source" | "title";
|
|
24
30
|
type DetailColumn = DataTableColumn & { id: DetailColumnId };
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
item: FeedDataTableItem;
|
|
28
|
-
itemIndex: number;
|
|
29
|
-
}
|
|
32
|
+
type DetailRow = IndexedStackRow<FeedDataTableItem>;
|
|
30
33
|
|
|
31
|
-
|
|
32
|
-
columnId: DetailColumnId;
|
|
33
|
-
direction: "asc" | "desc";
|
|
34
|
-
}
|
|
34
|
+
type SortPreference = StackSortPreference<DetailColumnId>;
|
|
35
35
|
|
|
36
36
|
interface FeedDataTableStackViewProps {
|
|
37
37
|
width: number;
|
|
@@ -151,19 +151,12 @@ export function FeedDataTableStackView({
|
|
|
151
151
|
[items, sourceLabel, titleLabel, width],
|
|
152
152
|
);
|
|
153
153
|
const sortedRows = useMemo(() => {
|
|
154
|
-
|
|
155
|
-
return items
|
|
156
|
-
.map((item, itemIndex) => ({ item, itemIndex }))
|
|
157
|
-
.sort((a, b) => {
|
|
158
|
-
const primary = compareRows(a, b, sortPreference.columnId) * direction;
|
|
159
|
-
return primary !== 0 ? primary : a.itemIndex - b.itemIndex;
|
|
160
|
-
});
|
|
154
|
+
return sortIndexedStackRows(items, sortPreference, compareRows);
|
|
161
155
|
}, [items, sortPreference]);
|
|
162
156
|
const selectedRowIndex = sortedRows.findIndex(
|
|
163
157
|
(row) => row.itemIndex === selectedIdx,
|
|
164
158
|
);
|
|
165
|
-
const activeRowIndex =
|
|
166
|
-
selectedRowIndex >= 0 ? selectedRowIndex : sortedRows.length > 0 ? 0 : -1;
|
|
159
|
+
const activeRowIndex = activeStackIndex(sortedRows.length, selectedRowIndex);
|
|
167
160
|
const openItem = useMemo(
|
|
168
161
|
() =>
|
|
169
162
|
openItemId
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface StackSortPreference<C extends string> {
|
|
2
|
+
columnId: C;
|
|
3
|
+
direction: "asc" | "desc";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface IndexedStackRow<T> {
|
|
7
|
+
item: T;
|
|
8
|
+
itemIndex: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function sortStackItems<T, C extends string>(
|
|
12
|
+
items: T[],
|
|
13
|
+
preference: StackSortPreference<C>,
|
|
14
|
+
compare: (a: T, b: T, columnId: C) => number,
|
|
15
|
+
tieBreak: (a: T, b: T) => number = () => 0,
|
|
16
|
+
): T[] {
|
|
17
|
+
const direction = preference.direction === "asc" ? 1 : -1;
|
|
18
|
+
return [...items].sort((a, b) => {
|
|
19
|
+
const primary = compare(a, b, preference.columnId) * direction;
|
|
20
|
+
return primary !== 0 ? primary : tieBreak(a, b);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sortIndexedStackRows<T, C extends string>(
|
|
25
|
+
items: T[],
|
|
26
|
+
preference: StackSortPreference<C>,
|
|
27
|
+
compare: (a: IndexedStackRow<T>, b: IndexedStackRow<T>, columnId: C) => number,
|
|
28
|
+
): Array<IndexedStackRow<T>> {
|
|
29
|
+
return sortStackItems(
|
|
30
|
+
items.map((item, itemIndex) => ({ item, itemIndex })),
|
|
31
|
+
preference,
|
|
32
|
+
compare,
|
|
33
|
+
(a, b) => a.itemIndex - b.itemIndex,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function activeStackIndex(length: number, selectedIndex: number): number {
|
|
38
|
+
return selectedIndex >= 0 ? selectedIndex : length > 0 ? 0 : -1;
|
|
39
|
+
}
|