gloomberb 0.4.1 → 0.5.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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/pane-sizing.ts +10 -0
- package/src/components/layout/pane.tsx +9 -2
- package/src/components/layout/shell.test.tsx +282 -28
- package/src/components/layout/shell.tsx +10 -7
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +83 -18
- package/src/plugins/builtin/chat-controller.ts +26 -14
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +424 -120
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/api-client.test.ts +90 -0
- package/src/utils/api-client.ts +56 -16
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -325,7 +325,7 @@ export function getScreenFooterLeft(route: CommandBarRoute | null): string {
|
|
|
325
325
|
}
|
|
326
326
|
|
|
327
327
|
export function getScreenFooterRight(route: CommandBarRoute | null): string {
|
|
328
|
-
if (!route) return "esc
|
|
328
|
+
if (!route) return "esc cancel";
|
|
329
329
|
|
|
330
330
|
if (route.kind === "workflow") return "esc back";
|
|
331
331
|
if (route.kind === "confirm") return "backspace/esc back";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PaneTemplateDef } from "../../types/plugin";
|
|
1
|
+
import type { CommandDef, PaneTemplateDef } from "../../types/plugin";
|
|
2
2
|
import type { Command } from "./command-registry";
|
|
3
3
|
import { getPaneTemplateDisplayLabel } from "./pane-template-display";
|
|
4
4
|
|
|
@@ -26,10 +26,16 @@ export interface PaneTemplateShortcutIntent extends ShortcutIntentBase {
|
|
|
26
26
|
template: PaneTemplateDef;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface PluginCommandShortcutIntent extends ShortcutIntentBase {
|
|
30
|
+
source: "plugin-command";
|
|
31
|
+
command: CommandDef;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export type ShortcutIntent =
|
|
30
35
|
| { kind: "none" }
|
|
31
36
|
| CommandShortcutIntent
|
|
32
|
-
| PaneTemplateShortcutIntent
|
|
37
|
+
| PaneTemplateShortcutIntent
|
|
38
|
+
| PluginCommandShortcutIntent;
|
|
33
39
|
|
|
34
40
|
interface ShortcutParseCandidate {
|
|
35
41
|
prefix: string;
|
|
@@ -37,8 +43,9 @@ interface ShortcutParseCandidate {
|
|
|
37
43
|
description: string;
|
|
38
44
|
argKind: RootShortcutArgKind | null;
|
|
39
45
|
argPlaceholder?: string;
|
|
40
|
-
source: "command" | "pane-template";
|
|
46
|
+
source: "command" | "pane-template" | "plugin-command";
|
|
41
47
|
command?: Command;
|
|
48
|
+
pluginCommand?: CommandDef;
|
|
42
49
|
template?: PaneTemplateDef;
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -71,6 +78,7 @@ function inferShortcutArg(argKind: RootShortcutArgKind | null, activeTicker: str
|
|
|
71
78
|
|
|
72
79
|
function buildShortcutCandidates(
|
|
73
80
|
commands: Command[],
|
|
81
|
+
pluginCommands: CommandDef[],
|
|
74
82
|
paneTemplates: PaneTemplateDef[],
|
|
75
83
|
): ShortcutParseCandidate[] {
|
|
76
84
|
return [
|
|
@@ -85,6 +93,17 @@ function buildShortcutCandidates(
|
|
|
85
93
|
source: "command" as const,
|
|
86
94
|
command,
|
|
87
95
|
})),
|
|
96
|
+
...pluginCommands
|
|
97
|
+
.filter((command) => command.shortcut?.trim().length)
|
|
98
|
+
.map((command) => ({
|
|
99
|
+
prefix: normalizeShortcutPrefix(command.shortcut!),
|
|
100
|
+
label: command.label,
|
|
101
|
+
description: command.description ?? "",
|
|
102
|
+
argKind: command.shortcutArg?.kind ?? (command.shortcutArg ? "text" : null),
|
|
103
|
+
argPlaceholder: command.shortcutArg?.placeholder,
|
|
104
|
+
source: "plugin-command" as const,
|
|
105
|
+
pluginCommand: command,
|
|
106
|
+
})),
|
|
88
107
|
...paneTemplates
|
|
89
108
|
.filter((template) => template.shortcut?.prefix)
|
|
90
109
|
.map((template) => ({
|
|
@@ -102,11 +121,13 @@ function buildShortcutCandidates(
|
|
|
102
121
|
export function parseRootShortcutIntent({
|
|
103
122
|
query,
|
|
104
123
|
commands,
|
|
124
|
+
pluginCommands = [],
|
|
105
125
|
paneTemplates,
|
|
106
126
|
activeTicker,
|
|
107
127
|
}: {
|
|
108
128
|
query: string;
|
|
109
129
|
commands: Command[];
|
|
130
|
+
pluginCommands?: CommandDef[];
|
|
110
131
|
paneTemplates: PaneTemplateDef[];
|
|
111
132
|
activeTicker: string | null;
|
|
112
133
|
}): ShortcutIntent {
|
|
@@ -114,7 +135,7 @@ export function parseRootShortcutIntent({
|
|
|
114
135
|
if (!trimmed) return { kind: "none" };
|
|
115
136
|
|
|
116
137
|
const upper = trimmed.toUpperCase();
|
|
117
|
-
const match = buildShortcutCandidates(commands, paneTemplates).find((candidate) => (
|
|
138
|
+
const match = buildShortcutCandidates(commands, pluginCommands, paneTemplates).find((candidate) => (
|
|
118
139
|
upper === candidate.prefix || upper.startsWith(`${candidate.prefix} `)
|
|
119
140
|
));
|
|
120
141
|
if (!match) return { kind: "none" };
|
|
@@ -151,6 +172,14 @@ export function parseRootShortcutIntent({
|
|
|
151
172
|
};
|
|
152
173
|
}
|
|
153
174
|
|
|
175
|
+
if (match.source === "plugin-command" && match.pluginCommand) {
|
|
176
|
+
return {
|
|
177
|
+
...base,
|
|
178
|
+
source: "plugin-command",
|
|
179
|
+
command: match.pluginCommand,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
154
183
|
return {
|
|
155
184
|
...base,
|
|
156
185
|
source: "pane-template",
|
|
@@ -45,15 +45,15 @@ describe("command bar view model helpers", () => {
|
|
|
45
45
|
test("returns footer hints for plugin toggles", () => {
|
|
46
46
|
expect(getFooterHints("plugins", false)).toEqual({
|
|
47
47
|
left: "up/down move enter select space toggle",
|
|
48
|
-
right: "esc
|
|
48
|
+
right: "esc cancel",
|
|
49
49
|
});
|
|
50
50
|
expect(getFooterHints("layout", false)).toEqual({
|
|
51
51
|
left: "up/down move enter select",
|
|
52
|
-
right: "esc
|
|
52
|
+
right: "esc cancel",
|
|
53
53
|
});
|
|
54
54
|
expect(getFooterHints("new-pane", false)).toEqual({
|
|
55
55
|
left: "up/down move enter select",
|
|
56
|
-
right: "esc
|
|
56
|
+
right: "esc cancel",
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
@@ -111,16 +111,16 @@ export function getFooterHints(mode: CommandBarMode, isNarrow: boolean): Command
|
|
|
111
111
|
if (mode === "plugins") {
|
|
112
112
|
return {
|
|
113
113
|
left: isNarrow ? "space toggle" : `${moveAndSelect} space toggle`,
|
|
114
|
-
right: "esc
|
|
114
|
+
right: "esc cancel",
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
117
|
if (mode === "layout") {
|
|
118
|
-
return { left: moveAndSelect, right: "esc
|
|
118
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
119
119
|
}
|
|
120
120
|
if (mode === "new-pane") {
|
|
121
|
-
return { left: moveAndSelect, right: "esc
|
|
121
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
122
122
|
}
|
|
123
|
-
return { left: moveAndSelect, right: "esc
|
|
123
|
+
return { left: moveAndSelect, right: "esc cancel" };
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export function getEmptyState(mode: CommandBarMode, query: string, searchQuery?: string): { label: string; detail: string } {
|
|
@@ -152,6 +152,16 @@ export function resolvePreferredCollectionTarget(
|
|
|
152
152
|
return options.some((option) => option.id === activeCollectionId) ? activeCollectionId : null;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export function resolveSoleCollectionTarget(
|
|
156
|
+
state: AppState,
|
|
157
|
+
kind: CollectionKind,
|
|
158
|
+
action: CollectionMembershipAction,
|
|
159
|
+
ticker: TickerRecord | null,
|
|
160
|
+
): string | null {
|
|
161
|
+
const options = getCollectionTargetOptions(state, kind, action, ticker);
|
|
162
|
+
return options.length === 1 ? options[0]!.id : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
155
165
|
export async function resolveTickerInput(
|
|
156
166
|
rawInput: string | undefined,
|
|
157
167
|
activeTicker: string | null,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { colors, floatingPaneBg } from "../../theme/colors";
|
|
3
3
|
import { PaneHeader } from "./pane-header";
|
|
4
|
-
import { getPaneBodyHeight } from "./pane-sizing";
|
|
4
|
+
import { getPaneBodyHeight, getPaneBodyHorizontalInset } from "./pane-sizing";
|
|
5
5
|
|
|
6
6
|
interface FloatingPaneWrapperProps {
|
|
7
7
|
title: string;
|
|
@@ -22,6 +22,7 @@ export function FloatingPaneWrapper({
|
|
|
22
22
|
}: FloatingPaneWrapperProps) {
|
|
23
23
|
const bg = floatingPaneBg(focused);
|
|
24
24
|
const bodyHeight = getPaneBodyHeight(height);
|
|
25
|
+
const bodyInset = getPaneBodyHorizontalInset(focused);
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
28
|
<box
|
|
@@ -38,7 +39,7 @@ export function FloatingPaneWrapper({
|
|
|
38
39
|
<PaneHeader title={title} width={width} focused={focused} floating showActions={showActions} />
|
|
39
40
|
|
|
40
41
|
{/* Content */}
|
|
41
|
-
<box height={bodyHeight} overflow="hidden">
|
|
42
|
+
<box height={bodyHeight} overflow="hidden" paddingLeft={bodyInset} paddingRight={bodyInset}>
|
|
42
43
|
{children}
|
|
43
44
|
</box>
|
|
44
45
|
|
|
@@ -7,6 +7,7 @@ import { useAppState } from "../../state/app-context";
|
|
|
7
7
|
import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
8
8
|
import { useQuoteEntry, useResolvedEntryValue } from "../../market-data/hooks";
|
|
9
9
|
import { formatPercentRaw } from "../../utils/format";
|
|
10
|
+
import { formatMarketPrice } from "../../utils/market-format";
|
|
10
11
|
import { marketStateLabel, marketStateColor, getExtendedHoursInfo } from "../../utils/market-status";
|
|
11
12
|
import { VERSION } from "../../version";
|
|
12
13
|
|
|
@@ -103,7 +104,7 @@ export function Header() {
|
|
|
103
104
|
|
|
104
105
|
const spyColor = spyQuote ? priceColor(spyQuote.change) : colors.headerText;
|
|
105
106
|
const spyText = spyQuote
|
|
106
|
-
? `SPY ${spyQuote.price
|
|
107
|
+
? `SPY ${formatMarketPrice(spyQuote.price, { assetCategory: "ETF" })} ${formatPercentRaw(spyQuote.changePercent)}`
|
|
107
108
|
: "SPY —";
|
|
108
109
|
|
|
109
110
|
// Extended hours info
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
const RESERVED_PANE_CHROME_ROWS = 2;
|
|
2
|
+
const FOCUSED_PANE_BODY_INSET = 1;
|
|
3
|
+
const RESERVED_FOCUSED_PANE_CHROME_COLUMNS = FOCUSED_PANE_BODY_INSET * 2;
|
|
2
4
|
|
|
3
5
|
export function getPaneBodyHeight(height: number): number {
|
|
4
6
|
// Reserve the header row plus the bottom border/resize row.
|
|
5
7
|
return Math.max(1, height - RESERVED_PANE_CHROME_ROWS);
|
|
6
8
|
}
|
|
9
|
+
|
|
10
|
+
export function getPaneBodyHorizontalInset(focused: boolean): number {
|
|
11
|
+
return focused ? FOCUSED_PANE_BODY_INSET : 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getPaneBodyWidth(width: number, focused: boolean): number {
|
|
15
|
+
return Math.max(1, width - (focused ? RESERVED_FOCUSED_PANE_CHROME_COLUMNS : 0));
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { paneBg } from "../../theme/colors";
|
|
3
3
|
import { PaneHeader } from "./pane-header";
|
|
4
|
-
import { getPaneBodyHeight } from "./pane-sizing";
|
|
4
|
+
import { getPaneBodyHeight, getPaneBodyHorizontalInset } from "./pane-sizing";
|
|
5
5
|
|
|
6
6
|
interface PaneWrapperProps {
|
|
7
7
|
title?: string;
|
|
@@ -30,6 +30,7 @@ export function PaneWrapper({
|
|
|
30
30
|
const bodyHeight = typeof height === "number"
|
|
31
31
|
? title ? getPaneBodyHeight(height) : height
|
|
32
32
|
: undefined;
|
|
33
|
+
const bodyInset = getPaneBodyHorizontalInset(focused);
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
36
|
<box
|
|
@@ -45,7 +46,13 @@ export function PaneWrapper({
|
|
|
45
46
|
{title && (
|
|
46
47
|
<PaneHeader title={title} width={width} focused={focused} showActions={showActions} />
|
|
47
48
|
)}
|
|
48
|
-
<box
|
|
49
|
+
<box
|
|
50
|
+
height={bodyHeight}
|
|
51
|
+
flexGrow={bodyHeight == null ? 1 : 0}
|
|
52
|
+
overflow="hidden"
|
|
53
|
+
paddingLeft={bodyInset}
|
|
54
|
+
paddingRight={bodyInset}
|
|
55
|
+
>
|
|
49
56
|
{children}
|
|
50
57
|
</box>
|
|
51
58
|
</box>
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { TextAttributes } from "@opentui/core";
|
|
2
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
3
4
|
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
4
5
|
import type { ReactNode } from "react";
|
|
5
6
|
import { act, useReducer } from "react";
|
|
6
7
|
import { AppContext, appReducer, createInitialState } from "../../state/app-context";
|
|
8
|
+
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
9
|
+
import { setSharedDataProviderForTests } from "../../plugins/registry";
|
|
7
10
|
import { cloneLayout, createDefaultConfig } from "../../types/config";
|
|
8
11
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
9
12
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
10
13
|
import { portfolioListPlugin } from "../../plugins/builtin/portfolio-list";
|
|
11
14
|
import type { PaneProps } from "../../types/plugin";
|
|
15
|
+
import { StockChart } from "../chart/stock-chart";
|
|
12
16
|
import { StatusBar } from "./status-bar";
|
|
13
17
|
import { Header } from "./header";
|
|
14
18
|
import { buildNativeWindowState, finalizePaneDragRelease, resolveNativeDockDividers, Shell } from "./shell";
|
|
15
19
|
import type { DataProvider } from "../../types/data-provider";
|
|
16
|
-
import type { Quote } from "../../types/financials";
|
|
20
|
+
import type { PricePoint, Quote, TickerFinancials } from "../../types/financials";
|
|
17
21
|
import type { TickerRecord } from "../../types/ticker";
|
|
18
22
|
|
|
19
23
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
@@ -25,10 +29,13 @@ afterEach(() => {
|
|
|
25
29
|
testSetup = undefined;
|
|
26
30
|
}
|
|
27
31
|
harnessState = null;
|
|
32
|
+
setSharedMarketDataCoordinator(null);
|
|
33
|
+
setSharedDataProviderForTests(undefined);
|
|
28
34
|
setSharedRegistryForTests(undefined);
|
|
29
35
|
});
|
|
30
36
|
|
|
31
37
|
function createShellPluginRegistry(options?: {
|
|
38
|
+
portfolioListComponent?: (props: PaneProps) => ReactNode;
|
|
32
39
|
tickerDetailComponent?: (props: PaneProps) => ReactNode;
|
|
33
40
|
}): PluginRegistry {
|
|
34
41
|
return {
|
|
@@ -36,7 +43,7 @@ function createShellPluginRegistry(options?: {
|
|
|
36
43
|
["portfolio-list", {
|
|
37
44
|
id: "portfolio-list",
|
|
38
45
|
name: "Portfolio List",
|
|
39
|
-
component: () => <text>Portfolio Body</text
|
|
46
|
+
component: options?.portfolioListComponent ?? (() => <text>Portfolio Body</text>),
|
|
40
47
|
defaultPosition: "left",
|
|
41
48
|
}],
|
|
42
49
|
["ticker-detail", {
|
|
@@ -134,31 +141,42 @@ function makeTicker(overrides: Partial<TickerRecord["metadata"]> = {}): TickerRe
|
|
|
134
141
|
};
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
function
|
|
144
|
+
function makePriceHistory(
|
|
145
|
+
length: number,
|
|
146
|
+
startDate = new Date(Date.UTC(2025, 0, 1)),
|
|
147
|
+
): PricePoint[] {
|
|
148
|
+
return Array.from({ length }, (_, index) => ({
|
|
149
|
+
date: new Date(startDate.getTime() + index * 24 * 3600_000),
|
|
150
|
+
open: 100 + index * 0.2,
|
|
151
|
+
high: 101 + index * 0.2,
|
|
152
|
+
low: 99 + index * 0.2,
|
|
153
|
+
close: 100.5 + index * 0.2,
|
|
154
|
+
volume: 1_000 + index * 10,
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createChartShellDataProvider(historyBySymbol: Record<string, PricePoint[]>): DataProvider {
|
|
138
159
|
return {
|
|
139
|
-
id: "test",
|
|
140
|
-
name: "Test",
|
|
141
|
-
async
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
async getPriceHistory() {
|
|
160
|
-
return [];
|
|
161
|
-
},
|
|
160
|
+
id: "shell-test-provider",
|
|
161
|
+
name: "Shell Test Provider",
|
|
162
|
+
getTickerFinancials: async (symbol) => ({
|
|
163
|
+
annualStatements: [],
|
|
164
|
+
quarterlyStatements: [],
|
|
165
|
+
priceHistory: historyBySymbol[symbol] ?? [],
|
|
166
|
+
}),
|
|
167
|
+
getQuote: async (symbol) => ({
|
|
168
|
+
symbol,
|
|
169
|
+
price: historyBySymbol[symbol]?.[historyBySymbol[symbol]!.length - 1]?.close ?? 0,
|
|
170
|
+
currency: "USD",
|
|
171
|
+
change: 0,
|
|
172
|
+
changePercent: 0,
|
|
173
|
+
lastUpdated: Date.now(),
|
|
174
|
+
}),
|
|
175
|
+
getExchangeRate: async () => 1,
|
|
176
|
+
search: async () => [],
|
|
177
|
+
getNews: async () => [],
|
|
178
|
+
getArticleSummary: async () => null,
|
|
179
|
+
getPriceHistory: async (symbol) => historyBySymbol[symbol] ?? [],
|
|
162
180
|
};
|
|
163
181
|
}
|
|
164
182
|
|
|
@@ -256,7 +274,7 @@ function BrokerShellHarness({ pluginRegistry }: { pluginRegistry: PluginRegistry
|
|
|
256
274
|
return (
|
|
257
275
|
<AppContext value={{ state, dispatch }}>
|
|
258
276
|
<box flexDirection="column">
|
|
259
|
-
<Header
|
|
277
|
+
<Header />
|
|
260
278
|
<Shell pluginRegistry={pluginRegistry} />
|
|
261
279
|
<StatusBar />
|
|
262
280
|
</box>
|
|
@@ -264,6 +282,50 @@ function BrokerShellHarness({ pluginRegistry }: { pluginRegistry: PluginRegistry
|
|
|
264
282
|
);
|
|
265
283
|
}
|
|
266
284
|
|
|
285
|
+
function FocusBorderProbe({ width, height }: Pick<PaneProps, "width" | "height">) {
|
|
286
|
+
return (
|
|
287
|
+
<box flexDirection="column" width={width} height={height}>
|
|
288
|
+
<box height={1} width={width}>
|
|
289
|
+
<text>Read Probe</text>
|
|
290
|
+
</box>
|
|
291
|
+
<box height={1} width={width} flexDirection="row">
|
|
292
|
+
<box flexGrow={1} />
|
|
293
|
+
<text>Right</text>
|
|
294
|
+
</box>
|
|
295
|
+
</box>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function ChartShellHarness({
|
|
300
|
+
pluginRegistry,
|
|
301
|
+
config,
|
|
302
|
+
ticker,
|
|
303
|
+
financials,
|
|
304
|
+
focusedPaneId,
|
|
305
|
+
}: {
|
|
306
|
+
pluginRegistry: PluginRegistry;
|
|
307
|
+
config: ReturnType<typeof createDefaultConfig>;
|
|
308
|
+
ticker: TickerRecord;
|
|
309
|
+
financials: TickerFinancials;
|
|
310
|
+
focusedPaneId: string | null;
|
|
311
|
+
}) {
|
|
312
|
+
const initialState = createInitialState(config);
|
|
313
|
+
initialState.focusedPaneId = focusedPaneId;
|
|
314
|
+
initialState.tickers = new Map([[ticker.metadata.ticker, ticker]]);
|
|
315
|
+
initialState.financials = new Map([[ticker.metadata.ticker, financials]]);
|
|
316
|
+
|
|
317
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
318
|
+
harnessState = state;
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<AppContext value={{ state, dispatch }}>
|
|
322
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
323
|
+
<Shell pluginRegistry={pluginRegistry} />
|
|
324
|
+
</DialogProvider>
|
|
325
|
+
</AppContext>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
267
329
|
describe("Header", () => {
|
|
268
330
|
test("shows automatic self-update status for standalone binaries", async () => {
|
|
269
331
|
testSetup = await testRender(
|
|
@@ -524,13 +586,100 @@ describe("Shell", () => {
|
|
|
524
586
|
expect(testSetup.captureCharFrame()).toContain("Settings");
|
|
525
587
|
});
|
|
526
588
|
|
|
589
|
+
test("applies a chart resolution chip on the first click even when the chart pane is unfocused", async () => {
|
|
590
|
+
const symbol = "AAPL";
|
|
591
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-chart-test");
|
|
592
|
+
config.layout = cloneLayout(config.layout);
|
|
593
|
+
config.layouts = [{ name: "Default", layout: cloneLayout(config.layout) }];
|
|
594
|
+
config.layout.instances = config.layout.instances.map((instance) => (
|
|
595
|
+
instance.instanceId === "ticker-detail:main"
|
|
596
|
+
? {
|
|
597
|
+
...instance,
|
|
598
|
+
binding: { kind: "fixed" as const, symbol },
|
|
599
|
+
settings: {
|
|
600
|
+
...(instance.settings ?? {}),
|
|
601
|
+
chartResolution: "auto",
|
|
602
|
+
chartRangePreset: "5Y",
|
|
603
|
+
},
|
|
604
|
+
}
|
|
605
|
+
: instance
|
|
606
|
+
));
|
|
607
|
+
|
|
608
|
+
const ticker = makeTicker({ ticker: symbol, name: symbol });
|
|
609
|
+
const history = makePriceHistory(260);
|
|
610
|
+
const financials: TickerFinancials = {
|
|
611
|
+
annualStatements: [],
|
|
612
|
+
quarterlyStatements: [],
|
|
613
|
+
priceHistory: history,
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const provider = createChartShellDataProvider({ [symbol]: history });
|
|
617
|
+
setSharedDataProviderForTests(provider);
|
|
618
|
+
setSharedMarketDataCoordinator(new MarketDataCoordinator(provider));
|
|
619
|
+
|
|
620
|
+
const pluginRegistry = createShellPluginRegistry({
|
|
621
|
+
tickerDetailComponent: (props) => (
|
|
622
|
+
<StockChart width={props.width} height={props.height} focused={props.focused} />
|
|
623
|
+
),
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
testSetup = await testRender(
|
|
627
|
+
<ChartShellHarness
|
|
628
|
+
pluginRegistry={pluginRegistry}
|
|
629
|
+
config={config}
|
|
630
|
+
ticker={ticker}
|
|
631
|
+
financials={financials}
|
|
632
|
+
focusedPaneId="portfolio-list:main"
|
|
633
|
+
/>,
|
|
634
|
+
{ width: 120, height: 32 },
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
await testSetup.renderOnce();
|
|
638
|
+
await testSetup.renderOnce();
|
|
639
|
+
|
|
640
|
+
const initialLines = testSetup.captureCharFrame().split("\n");
|
|
641
|
+
const resolutionRow = initialLines.findIndex((line) => line.includes("AUTO 1M 5M 15M"));
|
|
642
|
+
const targetResolution = "1W";
|
|
643
|
+
expect(resolutionRow).toBeGreaterThanOrEqual(0);
|
|
644
|
+
const resolutionSpansBefore = testSetup.captureSpans().lines[resolutionRow]?.spans ?? [];
|
|
645
|
+
let resolutionCol = -1;
|
|
646
|
+
let spanColumn = 0;
|
|
647
|
+
for (const span of resolutionSpansBefore) {
|
|
648
|
+
const chipOffset = span.text.indexOf(targetResolution);
|
|
649
|
+
if (chipOffset >= 0) {
|
|
650
|
+
resolutionCol = spanColumn + chipOffset + Math.floor(targetResolution.length / 2);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
spanColumn += span.width;
|
|
654
|
+
}
|
|
655
|
+
expect(resolutionCol).toBeGreaterThanOrEqual(0);
|
|
656
|
+
expect(testSetup.captureCharFrame()).toContain("AAPL - AUTO");
|
|
657
|
+
|
|
658
|
+
await act(async () => {
|
|
659
|
+
await testSetup!.mockMouse.click(resolutionCol, resolutionRow);
|
|
660
|
+
await testSetup!.renderOnce();
|
|
661
|
+
});
|
|
662
|
+
await testSetup.renderOnce();
|
|
663
|
+
await testSetup.renderOnce();
|
|
664
|
+
|
|
665
|
+
expect(
|
|
666
|
+
harnessState?.config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main")?.settings?.chartResolution,
|
|
667
|
+
).toBe("1wk");
|
|
668
|
+
const resolutionSpans = testSetup.captureSpans().lines[resolutionRow]?.spans ?? [];
|
|
669
|
+
const boldResolutionText = resolutionSpans
|
|
670
|
+
.filter((span) => (span.attributes & TextAttributes.BOLD) !== 0)
|
|
671
|
+
.map((span) => span.text)
|
|
672
|
+
.join("");
|
|
673
|
+
expect(boldResolutionText).toContain(targetResolution);
|
|
674
|
+
});
|
|
675
|
+
|
|
527
676
|
test("keeps the cash drawer and gridlock tip on distinct click rows in the full app layout", async () => {
|
|
528
677
|
const pluginRegistry = createBrokerPortfolioRegistry();
|
|
529
678
|
const layoutUpdates: unknown[] = [];
|
|
530
679
|
const toasts: string[] = [];
|
|
531
680
|
pluginRegistry.getLayoutFn = () => harnessState?.config.layout ?? { dockRoot: null, instances: [], floating: [] };
|
|
532
681
|
pluginRegistry.updateLayoutFn = (layout) => { layoutUpdates.push(layout); };
|
|
533
|
-
pluginRegistry.
|
|
682
|
+
pluginRegistry.notify = ({ body }: { body: string }) => { toasts.push(body); };
|
|
534
683
|
setSharedRegistryForTests(pluginRegistry);
|
|
535
684
|
|
|
536
685
|
testSetup = await testRender(
|
|
@@ -568,6 +717,84 @@ describe("Shell", () => {
|
|
|
568
717
|
expect(toasts).toEqual(["Retiled all panes"]);
|
|
569
718
|
});
|
|
570
719
|
|
|
720
|
+
test("keeps focused docked pane body text inside the focus border", async () => {
|
|
721
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-focus-border-test");
|
|
722
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
723
|
+
if (!mainPane) throw new Error("missing default portfolio pane");
|
|
724
|
+
|
|
725
|
+
const singlePaneLayout = {
|
|
726
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
727
|
+
instances: [{ ...mainPane }],
|
|
728
|
+
floating: [],
|
|
729
|
+
};
|
|
730
|
+
const state = {
|
|
731
|
+
...createInitialState({
|
|
732
|
+
...config,
|
|
733
|
+
layout: cloneLayout(singlePaneLayout),
|
|
734
|
+
layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
|
|
735
|
+
}),
|
|
736
|
+
focusedPaneId: "portfolio-list:main",
|
|
737
|
+
};
|
|
738
|
+
|
|
739
|
+
testSetup = await testRender(
|
|
740
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
741
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
742
|
+
<Shell pluginRegistry={createShellPluginRegistry({
|
|
743
|
+
portfolioListComponent: FocusBorderProbe,
|
|
744
|
+
})} />
|
|
745
|
+
</DialogProvider>
|
|
746
|
+
</AppContext>,
|
|
747
|
+
{ width: 40, height: 10 },
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
await testSetup.renderOnce();
|
|
751
|
+
|
|
752
|
+
const frame = testSetup.captureCharFrame();
|
|
753
|
+
expect(frame).toContain("│Read Probe");
|
|
754
|
+
expect(frame).toContain("Right│");
|
|
755
|
+
expect(frame).not.toContain("│ead Probe");
|
|
756
|
+
expect(frame).not.toContain("Righ│");
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
test("keeps focused floating pane body text inside the focus border", async () => {
|
|
760
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-floating-focus-border-test");
|
|
761
|
+
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
762
|
+
if (!detailPane) throw new Error("missing detail pane");
|
|
763
|
+
|
|
764
|
+
const floatingOnlyLayout = {
|
|
765
|
+
dockRoot: null,
|
|
766
|
+
instances: [{ ...detailPane }],
|
|
767
|
+
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8, zIndex: 75 }],
|
|
768
|
+
};
|
|
769
|
+
const state = {
|
|
770
|
+
...createInitialState({
|
|
771
|
+
...config,
|
|
772
|
+
layout: cloneLayout(floatingOnlyLayout),
|
|
773
|
+
layouts: [{ name: "Default", layout: cloneLayout(floatingOnlyLayout) }],
|
|
774
|
+
}),
|
|
775
|
+
focusedPaneId: "ticker-detail:main",
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
testSetup = await testRender(
|
|
779
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
780
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
781
|
+
<Shell pluginRegistry={createShellPluginRegistry({
|
|
782
|
+
tickerDetailComponent: FocusBorderProbe,
|
|
783
|
+
})} />
|
|
784
|
+
</DialogProvider>
|
|
785
|
+
</AppContext>,
|
|
786
|
+
{ width: 40, height: 12 },
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
await testSetup.renderOnce();
|
|
790
|
+
|
|
791
|
+
const frame = testSetup.captureCharFrame();
|
|
792
|
+
expect(frame).toContain("│Read Probe");
|
|
793
|
+
expect(frame).toContain("Right│");
|
|
794
|
+
expect(frame).not.toContain("│ead Probe");
|
|
795
|
+
expect(frame).not.toContain("Righ│");
|
|
796
|
+
});
|
|
797
|
+
|
|
571
798
|
test("shows the gridlock tip after snapping a pane to a half screen", () => {
|
|
572
799
|
const config = createDefaultConfig("/tmp/gloomberb-shell-snap-test");
|
|
573
800
|
const snapLayout = cloneLayout(config.layout);
|
|
@@ -676,6 +903,33 @@ describe("Shell", () => {
|
|
|
676
903
|
expect(updateLayout?.layout.dockRoot).toEqual({ kind: "pane", instanceId: "portfolio-list:main" });
|
|
677
904
|
});
|
|
678
905
|
|
|
906
|
+
test("keeps a floating pane at the preview rect after a free drag release", () => {
|
|
907
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
|
|
908
|
+
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
909
|
+
if (!detailPane) throw new Error("missing detail pane");
|
|
910
|
+
const floatingOnlyLayout = {
|
|
911
|
+
dockRoot: null,
|
|
912
|
+
instances: [{ ...detailPane, binding: { kind: "fixed" as const, symbol: "AAPL" } }],
|
|
913
|
+
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8, zIndex: 75 }],
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
const result = finalizePaneDragRelease(
|
|
917
|
+
floatingOnlyLayout,
|
|
918
|
+
"ticker-detail:main",
|
|
919
|
+
{ x: 14, y: 4, width: 30, height: 8 },
|
|
920
|
+
null,
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
expect(result.shouldShowGridlockTip).toBe(false);
|
|
924
|
+
expect(result.nextLayout.floating[0]).toEqual(expect.objectContaining({
|
|
925
|
+
instanceId: "ticker-detail:main",
|
|
926
|
+
x: 14,
|
|
927
|
+
y: 4,
|
|
928
|
+
width: 30,
|
|
929
|
+
height: 8,
|
|
930
|
+
}));
|
|
931
|
+
});
|
|
932
|
+
|
|
679
933
|
test("keeps the last floating pane body row visible above the border", async () => {
|
|
680
934
|
const config = createDefaultConfig("/tmp/gloomberb-shell-footer-test");
|
|
681
935
|
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|