gloomberb 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { AppAction } from "../../state/app-context";
|
|
2
|
-
import { themes, getThemeIds } from "../../theme/themes";
|
|
3
2
|
|
|
4
3
|
export type CommandExecutor = (dispatch: React.Dispatch<AppAction>, context: CommandContext) => void | Promise<void>;
|
|
5
4
|
|
|
@@ -239,15 +238,6 @@ export const commands: Command[] = [
|
|
|
239
238
|
},
|
|
240
239
|
];
|
|
241
240
|
|
|
242
|
-
/** Get theme options for the command bar */
|
|
243
|
-
export function getThemeOptions(): Array<{ id: string; name: string; description: string }> {
|
|
244
|
-
return getThemeIds().map((id) => ({
|
|
245
|
-
id,
|
|
246
|
-
name: themes[id]!.name,
|
|
247
|
-
description: themes[id]!.description,
|
|
248
|
-
}));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
241
|
/** Find a command whose prefix matches the start of the input */
|
|
252
242
|
export function matchPrefix(input: string, commandList: Command[] = commands): { command: Command; arg: string } | null {
|
|
253
243
|
const upper = input.toUpperCase().trim();
|
|
@@ -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
|
+
}
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "./workflow-types";
|
|
15
15
|
import type { CollectionKind, CollectionMembershipAction } from "./workflow-ops";
|
|
16
16
|
|
|
17
|
-
export type RouteCommandId = "security-description" | "
|
|
17
|
+
export type RouteCommandId = "security-description" | "plugins" | "layout";
|
|
18
18
|
export type CollectionCommandId = "add-watchlist" | "add-portfolio" | "remove-watchlist" | "remove-portfolio";
|
|
19
19
|
|
|
20
20
|
export function summarizeError(error: unknown): Record<string, string> {
|
|
@@ -30,7 +30,6 @@ export function summarizeError(error: unknown): Record<string, string> {
|
|
|
30
30
|
|
|
31
31
|
export function isRouteCommandId(commandId: string): commandId is RouteCommandId {
|
|
32
32
|
return commandId === "security-description"
|
|
33
|
-
|| commandId === "theme"
|
|
34
33
|
|| commandId === "plugins"
|
|
35
34
|
|| commandId === "layout";
|
|
36
35
|
}
|
|
@@ -237,12 +236,10 @@ export function normalizeWizardFields(steps: WizardStep[]): {
|
|
|
237
236
|
return { fields, description, initialValues, pendingLabel, successLabel };
|
|
238
237
|
}
|
|
239
238
|
|
|
240
|
-
export function routeCommandIdToScreen(commandId: RouteCommandId): "ticker-search" | "
|
|
239
|
+
export function routeCommandIdToScreen(commandId: RouteCommandId): "ticker-search" | "plugins" | "layout" {
|
|
241
240
|
switch (commandId) {
|
|
242
241
|
case "security-description":
|
|
243
242
|
return "ticker-search";
|
|
244
|
-
case "theme":
|
|
245
|
-
return "themes";
|
|
246
243
|
case "plugins":
|
|
247
244
|
return "plugins";
|
|
248
245
|
case "layout":
|
|
@@ -0,0 +1,76 @@
|
|
|
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" | "plugin" | "action" | "info";
|
|
9
|
+
right?: string;
|
|
10
|
+
searchText?: string;
|
|
11
|
+
pluginToggle?: () => void | Promise<void>;
|
|
12
|
+
secondaryAction?: () => void | Promise<void>;
|
|
13
|
+
checked?: boolean;
|
|
14
|
+
current?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
action: () => void | Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ListScreenKind = "root" | "mode" | "picker" | "pane-settings";
|
|
20
|
+
|
|
21
|
+
export interface ListScreenState {
|
|
22
|
+
kind: ListScreenKind;
|
|
23
|
+
title: string;
|
|
24
|
+
subtitle?: string;
|
|
25
|
+
query: string;
|
|
26
|
+
selectedIdx: number;
|
|
27
|
+
hoveredIdx: number | null;
|
|
28
|
+
results: ResultItem[];
|
|
29
|
+
searching: boolean;
|
|
30
|
+
emptyLabel: string;
|
|
31
|
+
emptyDetail: string;
|
|
32
|
+
footerLeft: string;
|
|
33
|
+
footerRight: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type CommandBarListRow =
|
|
37
|
+
| { kind: "spacer"; id: string }
|
|
38
|
+
| { kind: "heading"; id: string; label: string }
|
|
39
|
+
| { kind: "item"; item: ResultItem; globalIdx: number }
|
|
40
|
+
| { kind: "message"; id: string; label: string; dim?: boolean }
|
|
41
|
+
| { kind: "spinner"; id: string; label: string }
|
|
42
|
+
| { kind: "filler"; id: string };
|
|
43
|
+
|
|
44
|
+
export function orderListResults(results: ResultItem[]): ResultItem[] {
|
|
45
|
+
return buildSections(results).flatMap((section) => section.items);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildListRows(listState: ListScreenState): CommandBarListRow[] {
|
|
49
|
+
const rows: CommandBarListRow[] = [];
|
|
50
|
+
const sections = buildSections(listState.results);
|
|
51
|
+
let globalIdx = 0;
|
|
52
|
+
sections.forEach((section, sectionIndex) => {
|
|
53
|
+
if (sectionIndex > 0) {
|
|
54
|
+
rows.push({ kind: "spacer", id: `spacer:${sectionIndex}:${section.category}` });
|
|
55
|
+
}
|
|
56
|
+
rows.push({ kind: "heading", id: `heading:${sectionIndex}:${section.category}`, label: section.category });
|
|
57
|
+
for (const item of section.items) {
|
|
58
|
+
rows.push({ kind: "item", item, globalIdx });
|
|
59
|
+
globalIdx += 1;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildNativeListRows(listState: ListScreenState, rows: CommandBarListRow[]): CommandBarListRow[] {
|
|
66
|
+
if (listState.searching && rows.length === 0) {
|
|
67
|
+
return [{ kind: "spinner", id: "searching", label: "Searching…" }];
|
|
68
|
+
}
|
|
69
|
+
if (rows.length === 0) {
|
|
70
|
+
return [{ kind: "message", id: "empty", label: listState.emptyLabel }];
|
|
71
|
+
}
|
|
72
|
+
if (listState.searching) {
|
|
73
|
+
return [...rows, { kind: "spinner", id: "searching", label: "Searching…" }];
|
|
74
|
+
}
|
|
75
|
+
return rows;
|
|
76
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
memo,
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useImperativeHandle,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
12
|
+
import { Box, ScrollBox, Text } from "../../ui";
|
|
13
|
+
import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
|
|
14
|
+
import { getThemeIds, themes as themeRegistry } from "../../theme/themes";
|
|
15
|
+
import { truncateText } from "./view-model";
|
|
16
|
+
|
|
17
|
+
const THEME_PREVIEW_DEBOUNCE_MS = 120;
|
|
18
|
+
const THEME_OPTIONS = getThemeIds().map((id) => ({
|
|
19
|
+
id,
|
|
20
|
+
name: themeRegistry[id]!.name,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
interface ThemePickerScrollEvent {
|
|
24
|
+
stopPropagation: () => void;
|
|
25
|
+
preventDefault: () => void;
|
|
26
|
+
scroll?: { direction?: string; delta?: number };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ThemePickerProps {
|
|
30
|
+
filter: string;
|
|
31
|
+
committedThemeId: string;
|
|
32
|
+
height: number;
|
|
33
|
+
contentPadding: number;
|
|
34
|
+
labelWidth: number;
|
|
35
|
+
trailingWidth: number;
|
|
36
|
+
queryDisplayWidth: number;
|
|
37
|
+
nativePaneChrome: boolean;
|
|
38
|
+
paletteBg: string;
|
|
39
|
+
paletteHoverBg: string;
|
|
40
|
+
paletteSelectedBg: string;
|
|
41
|
+
paletteSelectedText: string;
|
|
42
|
+
paletteSubtleText: string;
|
|
43
|
+
paletteText: string;
|
|
44
|
+
panelBg: string;
|
|
45
|
+
onPreview: (themeId: string | null) => void;
|
|
46
|
+
onCommit: (themeId: string) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ThemePickerHandle {
|
|
50
|
+
move: (delta: number) => boolean;
|
|
51
|
+
commit: () => boolean;
|
|
52
|
+
cancelPreview: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clampIndex(index: number, length: number): number {
|
|
56
|
+
return Math.max(0, Math.min(index, Math.max(0, length - 1)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const ThemePicker = memo(forwardRef<ThemePickerHandle, ThemePickerProps>(function ThemePicker({
|
|
60
|
+
filter,
|
|
61
|
+
committedThemeId,
|
|
62
|
+
height,
|
|
63
|
+
contentPadding,
|
|
64
|
+
labelWidth,
|
|
65
|
+
trailingWidth,
|
|
66
|
+
queryDisplayWidth,
|
|
67
|
+
nativePaneChrome,
|
|
68
|
+
paletteBg,
|
|
69
|
+
paletteHoverBg,
|
|
70
|
+
paletteSelectedBg,
|
|
71
|
+
paletteSelectedText,
|
|
72
|
+
paletteSubtleText,
|
|
73
|
+
paletteText,
|
|
74
|
+
panelBg,
|
|
75
|
+
onPreview,
|
|
76
|
+
onCommit,
|
|
77
|
+
}: ThemePickerProps, ref) {
|
|
78
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
79
|
+
const previewTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
80
|
+
const pendingPreviewIdRef = useRef<string | null>(null);
|
|
81
|
+
const committedThemeIdRef = useRef(committedThemeId);
|
|
82
|
+
const onPreviewRef = useRef(onPreview);
|
|
83
|
+
const onCommitRef = useRef(onCommit);
|
|
84
|
+
const normalizedFilter = filter.trim().toLowerCase();
|
|
85
|
+
const themes = useMemo(() => {
|
|
86
|
+
if (!normalizedFilter) return THEME_OPTIONS;
|
|
87
|
+
return THEME_OPTIONS.filter((theme) => (
|
|
88
|
+
theme.name.toLowerCase().includes(normalizedFilter)
|
|
89
|
+
|| theme.id.toLowerCase().includes(normalizedFilter)
|
|
90
|
+
));
|
|
91
|
+
}, [normalizedFilter]);
|
|
92
|
+
const [selectedIndex, setSelectedIndex] = useState(() => (
|
|
93
|
+
Math.max(0, themes.findIndex((theme) => theme.id === committedThemeId))
|
|
94
|
+
));
|
|
95
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
96
|
+
const themesRef = useRef(themes);
|
|
97
|
+
const selectedIndexRef = useRef(selectedIndex);
|
|
98
|
+
|
|
99
|
+
themesRef.current = themes;
|
|
100
|
+
selectedIndexRef.current = selectedIndex;
|
|
101
|
+
committedThemeIdRef.current = committedThemeId;
|
|
102
|
+
onPreviewRef.current = onPreview;
|
|
103
|
+
onCommitRef.current = onCommit;
|
|
104
|
+
|
|
105
|
+
const cancelPreview = useCallback(() => {
|
|
106
|
+
if (previewTimerRef.current) {
|
|
107
|
+
clearTimeout(previewTimerRef.current);
|
|
108
|
+
previewTimerRef.current = null;
|
|
109
|
+
}
|
|
110
|
+
pendingPreviewIdRef.current = null;
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const requestPreview = useCallback((themeId: string) => {
|
|
114
|
+
pendingPreviewIdRef.current = themeId;
|
|
115
|
+
if (previewTimerRef.current) {
|
|
116
|
+
clearTimeout(previewTimerRef.current);
|
|
117
|
+
}
|
|
118
|
+
previewTimerRef.current = setTimeout(() => {
|
|
119
|
+
previewTimerRef.current = null;
|
|
120
|
+
const nextThemeId = pendingPreviewIdRef.current;
|
|
121
|
+
pendingPreviewIdRef.current = null;
|
|
122
|
+
if (!nextThemeId) return;
|
|
123
|
+
onPreviewRef.current(nextThemeId === committedThemeIdRef.current ? null : nextThemeId);
|
|
124
|
+
}, THEME_PREVIEW_DEBOUNCE_MS);
|
|
125
|
+
}, []);
|
|
126
|
+
|
|
127
|
+
const move = useCallback((delta: number): boolean => {
|
|
128
|
+
const options = themesRef.current;
|
|
129
|
+
if (options.length === 0 || delta === 0) return false;
|
|
130
|
+
const nextIndex = clampIndex(selectedIndexRef.current + delta, options.length);
|
|
131
|
+
if (nextIndex === selectedIndexRef.current) return false;
|
|
132
|
+
selectedIndexRef.current = nextIndex;
|
|
133
|
+
setSelectedIndex(nextIndex);
|
|
134
|
+
setHoveredIndex(null);
|
|
135
|
+
requestPreview(options[nextIndex]!.id);
|
|
136
|
+
return true;
|
|
137
|
+
}, [requestPreview]);
|
|
138
|
+
|
|
139
|
+
const commit = useCallback((): boolean => {
|
|
140
|
+
const selected = themesRef.current[selectedIndexRef.current];
|
|
141
|
+
if (!selected) return false;
|
|
142
|
+
cancelPreview();
|
|
143
|
+
onCommitRef.current(selected.id);
|
|
144
|
+
return true;
|
|
145
|
+
}, [cancelPreview]);
|
|
146
|
+
|
|
147
|
+
useImperativeHandle(ref, () => ({
|
|
148
|
+
move,
|
|
149
|
+
commit,
|
|
150
|
+
cancelPreview,
|
|
151
|
+
}), [cancelPreview, commit, move]);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const preferredIndex = themes.findIndex((theme) => theme.id === committedThemeId);
|
|
155
|
+
const nextIndex = preferredIndex >= 0 ? preferredIndex : 0;
|
|
156
|
+
selectedIndexRef.current = nextIndex;
|
|
157
|
+
setSelectedIndex(nextIndex);
|
|
158
|
+
setHoveredIndex(null);
|
|
159
|
+
}, [committedThemeId, themes]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => cancelPreview, [cancelPreview]);
|
|
162
|
+
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
const scrollBox = scrollRef.current;
|
|
165
|
+
if (!scrollBox) return;
|
|
166
|
+
if (scrollBox.verticalScrollBar) scrollBox.verticalScrollBar.visible = false;
|
|
167
|
+
if (themes.length === 0) return;
|
|
168
|
+
const viewportHeight = Math.max(1, scrollBox.viewport?.height ?? height);
|
|
169
|
+
if (selectedIndex < scrollBox.scrollTop) {
|
|
170
|
+
scrollBox.scrollTo(selectedIndex);
|
|
171
|
+
} else if (selectedIndex >= scrollBox.scrollTop + viewportHeight) {
|
|
172
|
+
scrollBox.scrollTo(selectedIndex - viewportHeight + 1);
|
|
173
|
+
}
|
|
174
|
+
}, [height, selectedIndex, themes.length]);
|
|
175
|
+
|
|
176
|
+
const handleScroll = useCallback((event: ThemePickerScrollEvent) => {
|
|
177
|
+
event.stopPropagation();
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
const delta = Math.max(1, Math.round(event.scroll?.delta ?? 1));
|
|
180
|
+
const direction = event.scroll?.direction;
|
|
181
|
+
if (direction === "down" || direction === "right") {
|
|
182
|
+
move(delta);
|
|
183
|
+
} else if (direction === "up" || direction === "left") {
|
|
184
|
+
move(-delta);
|
|
185
|
+
}
|
|
186
|
+
}, [move]);
|
|
187
|
+
|
|
188
|
+
if (themes.length === 0) {
|
|
189
|
+
return (
|
|
190
|
+
<Box height={height} paddingX={contentPadding}>
|
|
191
|
+
<Text fg={paletteSubtleText}>{truncateText("No themes match", queryDisplayWidth)}</Text>
|
|
192
|
+
</Box>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<ScrollBox
|
|
198
|
+
ref={scrollRef}
|
|
199
|
+
flexDirection="column"
|
|
200
|
+
height={height}
|
|
201
|
+
scrollY
|
|
202
|
+
focusable={false}
|
|
203
|
+
{...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
|
|
204
|
+
>
|
|
205
|
+
{themes.map((theme, index) => {
|
|
206
|
+
const selected = index === selectedIndex;
|
|
207
|
+
const hovered = index === hoveredIndex && !selected;
|
|
208
|
+
const current = theme.id === committedThemeId;
|
|
209
|
+
const label = truncateText(theme.name, labelWidth);
|
|
210
|
+
const trailing = current ? "current" : "";
|
|
211
|
+
return (
|
|
212
|
+
<Box
|
|
213
|
+
key={theme.id}
|
|
214
|
+
flexDirection="row"
|
|
215
|
+
height={1}
|
|
216
|
+
paddingX={contentPadding}
|
|
217
|
+
backgroundColor={selected
|
|
218
|
+
? paletteSelectedBg
|
|
219
|
+
: hovered
|
|
220
|
+
? paletteHoverBg
|
|
221
|
+
: (nativePaneChrome ? panelBg : paletteBg)}
|
|
222
|
+
onMouseMove={() => setHoveredIndex(index)}
|
|
223
|
+
onMouseOut={() => setHoveredIndex(null)}
|
|
224
|
+
onMouseDown={(event: any) => {
|
|
225
|
+
event.stopPropagation?.();
|
|
226
|
+
event.preventDefault?.();
|
|
227
|
+
selectedIndexRef.current = index;
|
|
228
|
+
setSelectedIndex(index);
|
|
229
|
+
cancelPreview();
|
|
230
|
+
onCommitRef.current(theme.id);
|
|
231
|
+
}}
|
|
232
|
+
{...(!nativePaneChrome ? { onMouseScroll: handleScroll } : {})}
|
|
233
|
+
data-command-bar-row-selected={nativePaneChrome && selected ? "true" : undefined}
|
|
234
|
+
style={nativePaneChrome ? { borderRadius: 6 } : undefined}
|
|
235
|
+
>
|
|
236
|
+
<Box width={labelWidth}>
|
|
237
|
+
<Text
|
|
238
|
+
fg={selected ? paletteSelectedText : paletteText}
|
|
239
|
+
attributes={current ? TextAttributes.BOLD : undefined}
|
|
240
|
+
>
|
|
241
|
+
{label}
|
|
242
|
+
</Text>
|
|
243
|
+
</Box>
|
|
244
|
+
<Box width={trailingWidth}>
|
|
245
|
+
<Text fg={selected ? paletteSelectedText : paletteSubtleText}>
|
|
246
|
+
{truncateText(trailing, trailingWidth)}
|
|
247
|
+
</Text>
|
|
248
|
+
</Box>
|
|
249
|
+
</Box>
|
|
250
|
+
);
|
|
251
|
+
})}
|
|
252
|
+
</ScrollBox>
|
|
253
|
+
);
|
|
254
|
+
}));
|
|
@@ -46,7 +46,7 @@ describe("command bar view model helpers", () => {
|
|
|
46
46
|
expect(sections.map((section) => section.category)).toEqual(["Tickers", "Config", "Danger", "Debug"]);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test("derives row presentation for toggles and current
|
|
49
|
+
test("derives row presentation for toggles and current rows", () => {
|
|
50
50
|
expect(getRowPresentation({
|
|
51
51
|
id: "plugin:news",
|
|
52
52
|
label: "News",
|
|
@@ -61,11 +61,11 @@ describe("command bar view model helpers", () => {
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
expect(getRowPresentation({
|
|
64
|
-
id: "
|
|
64
|
+
id: "current:amber",
|
|
65
65
|
label: "Amber",
|
|
66
66
|
detail: "Warm terminal palette",
|
|
67
|
-
category: "
|
|
68
|
-
kind: "
|
|
67
|
+
category: "Config",
|
|
68
|
+
kind: "command",
|
|
69
69
|
right: "amber",
|
|
70
70
|
current: true,
|
|
71
71
|
}, false, true)).toMatchObject({
|
|
@@ -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[];
|
|
@@ -25,7 +20,7 @@ export interface CommandBarItemView {
|
|
|
25
20
|
label: string;
|
|
26
21
|
detail: string;
|
|
27
22
|
category: string;
|
|
28
|
-
kind: "command" | "ticker" | "search" | "
|
|
23
|
+
kind: "command" | "ticker" | "search" | "plugin" | "action" | "info";
|
|
29
24
|
right?: string;
|
|
30
25
|
checked?: boolean;
|
|
31
26
|
current?: boolean;
|
|
@@ -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),
|
|
@@ -38,11 +38,10 @@ export interface CommandBarRouteBase {
|
|
|
38
38
|
|
|
39
39
|
export interface CommandBarModeRoute extends CommandBarRouteBase {
|
|
40
40
|
kind: "mode";
|
|
41
|
-
screen: "ticker-search" | "
|
|
41
|
+
screen: "ticker-search" | "plugins" | "layout";
|
|
42
42
|
query: string;
|
|
43
43
|
selectedIdx: number;
|
|
44
44
|
hoveredIdx: number | null;
|
|
45
|
-
themeBaseId?: string;
|
|
46
45
|
payload?: Record<string, unknown>;
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -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
|
|