gloomberb 0.7.2 → 0.7.3
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -10,6 +10,7 @@ export interface CommandContext {
|
|
|
10
10
|
export interface Command {
|
|
11
11
|
id: string;
|
|
12
12
|
prefix: string; // e.g., "DES", "AW", "RP"
|
|
13
|
+
aliases?: string[];
|
|
13
14
|
label: string;
|
|
14
15
|
description: string;
|
|
15
16
|
hasArg?: boolean; // true if prefix takes an argument (e.g., "DES AMD")
|
|
@@ -24,6 +25,7 @@ export const commands: Command[] = [
|
|
|
24
25
|
{
|
|
25
26
|
id: "security-description",
|
|
26
27
|
prefix: "DES",
|
|
28
|
+
aliases: ["T"],
|
|
27
29
|
label: "Description",
|
|
28
30
|
description: "Open security details for a ticker",
|
|
29
31
|
hasArg: true,
|
|
@@ -239,19 +241,27 @@ export const commands: Command[] = [
|
|
|
239
241
|
];
|
|
240
242
|
|
|
241
243
|
/** Find a command whose prefix matches the start of the input */
|
|
242
|
-
export function matchPrefix(input: string, commandList: Command[] = commands): { command: Command; arg: string } | null {
|
|
244
|
+
export function matchPrefix(input: string, commandList: Command[] = commands): { command: Command; arg: string; prefix: string } | null {
|
|
243
245
|
const upper = input.toUpperCase().trim();
|
|
244
246
|
if (!upper) return null;
|
|
245
247
|
|
|
246
248
|
// Sort by prefix length descending so longer prefixes match first (e.g., "AW" before "A")
|
|
247
|
-
const sorted =
|
|
249
|
+
const sorted = commandList
|
|
250
|
+
.flatMap((command) => getCommandPrefixes(command).map((prefix) => ({ command, prefix })))
|
|
251
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
248
252
|
|
|
249
|
-
for (const
|
|
250
|
-
if (upper.startsWith(
|
|
251
|
-
const arg = input.slice(
|
|
252
|
-
return { command
|
|
253
|
+
for (const { command, prefix } of sorted) {
|
|
254
|
+
if (upper.startsWith(`${prefix} `) || upper === prefix) {
|
|
255
|
+
const arg = input.slice(prefix.length).trim();
|
|
256
|
+
return { command, arg, prefix };
|
|
253
257
|
}
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
return null;
|
|
257
261
|
}
|
|
262
|
+
|
|
263
|
+
export function getCommandPrefixes(command: Command): string[] {
|
|
264
|
+
return [command.prefix, ...(command.aliases ?? [])]
|
|
265
|
+
.map((prefix) => prefix.trim().toUpperCase())
|
|
266
|
+
.filter(Boolean);
|
|
267
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CommandDef, PaneTemplateDef } from "../../types/plugin";
|
|
2
|
-
import type
|
|
2
|
+
import { getCommandPrefixes, type Command } from "./command-registry";
|
|
3
3
|
import { getPaneTemplateDisplayLabel } from "./pane-template-display";
|
|
4
4
|
|
|
5
5
|
type RootShortcutArgKind = "text" | "ticker" | "ticker-list";
|
|
@@ -83,16 +83,15 @@ function buildShortcutCandidates(
|
|
|
83
83
|
): ShortcutParseCandidate[] {
|
|
84
84
|
return [
|
|
85
85
|
...commands
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
prefix: normalizeShortcutPrefix(command.prefix),
|
|
86
|
+
.flatMap((command) => getCommandPrefixes(command).map((prefix) => ({
|
|
87
|
+
prefix: normalizeShortcutPrefix(prefix),
|
|
89
88
|
label: command.label,
|
|
90
89
|
description: command.description,
|
|
91
90
|
argKind: getCommandShortcutArgKind(command),
|
|
92
91
|
argPlaceholder: command.argPlaceholder,
|
|
93
92
|
source: "command" as const,
|
|
94
93
|
command,
|
|
95
|
-
})),
|
|
94
|
+
}))),
|
|
96
95
|
...pluginCommands
|
|
97
96
|
.filter((command) => command.shortcut?.trim().length)
|
|
98
97
|
.map((command) => ({
|
|
@@ -11,6 +11,7 @@ describe("command bar view model helpers", () => {
|
|
|
11
11
|
test("resolves prefix-driven modes", () => {
|
|
12
12
|
expect(resolveCommandBarMode("")).toMatchObject({ kind: "default", badge: "BROWSE" });
|
|
13
13
|
expect(resolveCommandBarMode("DES NVDA")).toMatchObject({ kind: "search", badge: "DES" });
|
|
14
|
+
expect(resolveCommandBarMode("T NVDA")).toMatchObject({ kind: "search", badge: "T" });
|
|
14
15
|
expect(resolveCommandBarMode("TH ")).toMatchObject({ kind: "themes", badge: "THEMES" });
|
|
15
16
|
expect(resolveCommandBarMode("PL notes")).toMatchObject({ kind: "plugins", badge: "PLUGINS" });
|
|
16
17
|
expect(resolveCommandBarMode("LAY ")).toMatchObject({ kind: "layout", badge: "LAYOUT" });
|
|
@@ -47,7 +47,7 @@ export function resolveCommandBarMode(query: string, commandList?: Command[]): C
|
|
|
47
47
|
|
|
48
48
|
switch (match.command.id) {
|
|
49
49
|
case "security-description":
|
|
50
|
-
return { kind: "search", badge:
|
|
50
|
+
return { kind: "search", badge: match.prefix, hint: "Open security details for a ticker" };
|
|
51
51
|
case "theme":
|
|
52
52
|
return { kind: "themes", badge: "THEMES", hint: "Preview with arrows, Enter to save, Esc to revert" };
|
|
53
53
|
case "plugins":
|
|
@@ -128,6 +128,7 @@ export function truncateText(text: string, width: number): string {
|
|
|
128
128
|
|
|
129
129
|
function getCategoryPriority(category: string): number {
|
|
130
130
|
const normalized = category.trim().toLowerCase();
|
|
131
|
+
if (normalized === "exact match") return -50;
|
|
131
132
|
if (normalized === "saved") return -40;
|
|
132
133
|
if (normalized === "primary listing") return -30;
|
|
133
134
|
if (normalized === "other listings") return -20;
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
COMPARISON_CHART_PANE_ID,
|
|
17
17
|
MIN_COMPARISON_CHART_SYMBOLS,
|
|
18
18
|
} from "../../plugins/builtin/comparison-chart";
|
|
19
|
+
import { buildQuoteMonitorPaneTitle } from "../../plugins/builtin/ticker-detail/settings";
|
|
19
20
|
import { getPaneTemplateDisplayLabel } from "./pane-template-display";
|
|
20
21
|
|
|
21
22
|
export interface SharedWorkflowDeps {
|
|
@@ -366,43 +367,21 @@ export async function applyPaneSettingFieldValue(
|
|
|
366
367
|
return;
|
|
367
368
|
}
|
|
368
369
|
|
|
369
|
-
if (descriptor.pane.paneId === "quote-monitor" && field.key === "symbol") {
|
|
370
|
+
if (descriptor.pane.paneId === "quote-monitor" && (field.key === "symbol" || field.key === "symbolsText")) {
|
|
370
371
|
const rawQuery = typeof value === "string" ? value.trim() : "";
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
tickers: state.tickers,
|
|
375
|
-
dataProvider: deps.dataProvider,
|
|
376
|
-
});
|
|
377
|
-
if (!resolvedTicker) {
|
|
378
|
-
throw new Error(`No ticker match found for "${rawQuery}".`);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const symbol = resolvedTicker.kind === "local"
|
|
382
|
-
? resolvedTicker.ticker.metadata.ticker
|
|
383
|
-
: resolvedTicker.symbol;
|
|
384
|
-
|
|
385
|
-
if (resolvedTicker.kind === "provider") {
|
|
386
|
-
const { ticker, created } = await upsertTickerFromSearchResult(
|
|
387
|
-
deps.tickerRepository,
|
|
388
|
-
resolvedTicker.result,
|
|
389
|
-
);
|
|
390
|
-
deps.dispatch({ type: "UPDATE_TICKER", ticker });
|
|
391
|
-
if (created) {
|
|
392
|
-
deps.pluginRegistry.events.emit("ticker:added", {
|
|
393
|
-
symbol: ticker.metadata.ticker,
|
|
394
|
-
ticker,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
372
|
+
const symbols = await resolveTickerListInput(rawQuery, null, deps);
|
|
373
|
+
const primarySymbol = symbols[0]!;
|
|
374
|
+
const symbolsText = formatTickerListInput(symbols);
|
|
398
375
|
|
|
399
376
|
const nextLayout = updatePaneInstance(state.config.layout, targetId, (instance) => ({
|
|
400
377
|
...instance,
|
|
401
|
-
title:
|
|
402
|
-
binding: { kind: "fixed", symbol },
|
|
378
|
+
title: buildQuoteMonitorPaneTitle(symbols),
|
|
379
|
+
binding: { kind: "fixed", symbol: primarySymbol },
|
|
403
380
|
settings: {
|
|
404
381
|
...(instance.settings ?? {}),
|
|
405
|
-
symbol,
|
|
382
|
+
symbol: primarySymbol,
|
|
383
|
+
symbols,
|
|
384
|
+
symbolsText,
|
|
406
385
|
},
|
|
407
386
|
}));
|
|
408
387
|
deps.persistLayout(nextLayout, { pushHistory: shouldPushHistory });
|
|
@@ -433,7 +412,7 @@ export async function applyPaneSettingFieldValue(
|
|
|
433
412
|
}
|
|
434
413
|
|
|
435
414
|
let nextSettings: Record<string, unknown> = {
|
|
436
|
-
...descriptor.context.settings,
|
|
415
|
+
...(descriptor.rawSettings ?? descriptor.context.settings),
|
|
437
416
|
[field.key]: value,
|
|
438
417
|
};
|
|
439
418
|
|
|
@@ -130,7 +130,16 @@ async function renderSettled() {
|
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
async function emitKeypress(event: {
|
|
133
|
+
async function emitKeypress(event: {
|
|
134
|
+
name?: string;
|
|
135
|
+
sequence?: string;
|
|
136
|
+
ctrl?: boolean;
|
|
137
|
+
meta?: boolean;
|
|
138
|
+
shift?: boolean;
|
|
139
|
+
option?: boolean;
|
|
140
|
+
defaultPrevented?: boolean;
|
|
141
|
+
propagationStopped?: boolean;
|
|
142
|
+
}) {
|
|
134
143
|
await act(async () => {
|
|
135
144
|
testSetup!.renderer.keyInput.emit("keypress", {
|
|
136
145
|
ctrl: false,
|
|
@@ -139,6 +148,8 @@ async function emitKeypress(event: { name?: string; sequence?: string }) {
|
|
|
139
148
|
shift: false,
|
|
140
149
|
eventType: "press",
|
|
141
150
|
repeated: false,
|
|
151
|
+
defaultPrevented: false,
|
|
152
|
+
propagationStopped: false,
|
|
142
153
|
preventDefault: () => {},
|
|
143
154
|
stopPropagation: () => {},
|
|
144
155
|
...event,
|
|
@@ -158,6 +169,10 @@ describe("DataTableView", () => {
|
|
|
158
169
|
await renderSettled();
|
|
159
170
|
expect(testSetup.captureCharFrame()).toContain("selected=Second row");
|
|
160
171
|
|
|
172
|
+
await emitKeypress({ name: "k", sequence: "k", meta: true });
|
|
173
|
+
await renderSettled();
|
|
174
|
+
expect(testSetup.captureCharFrame()).toContain("selected=Second row");
|
|
175
|
+
|
|
161
176
|
await emitKeypress({ name: "k", sequence: "k" });
|
|
162
177
|
await emitKeypress({ name: "k", sequence: "k" });
|
|
163
178
|
await renderSettled();
|
|
@@ -166,6 +181,14 @@ describe("DataTableView", () => {
|
|
|
166
181
|
await emitKeypress({ name: "enter", sequence: "\r" });
|
|
167
182
|
await renderSettled();
|
|
168
183
|
expect(testSetup.captureCharFrame()).toContain("activated=First row");
|
|
184
|
+
|
|
185
|
+
await emitKeypress({ name: "j", sequence: "j" });
|
|
186
|
+
await renderSettled();
|
|
187
|
+
expect(testSetup.captureCharFrame()).toContain("selected=Second row activated=First row");
|
|
188
|
+
|
|
189
|
+
await emitKeypress({ name: "enter", sequence: "\r", defaultPrevented: true });
|
|
190
|
+
await renderSettled();
|
|
191
|
+
expect(testSetup.captureCharFrame()).toContain("selected=Second row activated=First row");
|
|
169
192
|
});
|
|
170
193
|
|
|
171
194
|
test("keeps keyboard navigation usable without external row selection", async () => {
|
|
@@ -188,18 +188,19 @@ export function DataTableView<
|
|
|
188
188
|
}, [activateIndex, effectiveSelectedIndex, navigableIndices]);
|
|
189
189
|
|
|
190
190
|
useShortcut((event) => {
|
|
191
|
+
if (event.defaultPrevented || event.propagationStopped) return;
|
|
191
192
|
if (!focused || !keyboardNavigation) return;
|
|
192
193
|
|
|
193
194
|
if (onRootKeyDown?.(event)) return;
|
|
194
195
|
if (tableProps.items.length === 0) return;
|
|
195
196
|
|
|
196
|
-
if (isNextTableRowKey(event
|
|
197
|
+
if (isNextTableRowKey(event)) {
|
|
197
198
|
stopTableKey(event);
|
|
198
199
|
selectByOffset(1);
|
|
199
200
|
return;
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
if (isPreviousTableRowKey(event
|
|
203
|
+
if (isPreviousTableRowKey(event)) {
|
|
203
204
|
stopTableKey(event);
|
|
204
205
|
selectByOffset(-1);
|
|
205
206
|
return;
|
|
@@ -3,6 +3,7 @@ import { TextAttributes, type ScrollBoxRenderable } from "../ui";
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
|
4
4
|
import { colors } from "../theme/colors";
|
|
5
5
|
import { formatTimeAgo } from "../utils/format";
|
|
6
|
+
import { isPlainKey } from "../utils/keyboard";
|
|
6
7
|
import { toTimestampMillis } from "../utils/timestamp";
|
|
7
8
|
import { DataTableStackView } from "./data-table-stack-view";
|
|
8
9
|
import {
|
|
@@ -245,13 +246,13 @@ export function FeedDataTableStackView({
|
|
|
245
246
|
preventDefault?: () => void;
|
|
246
247
|
stopPropagation?: () => void;
|
|
247
248
|
}) => {
|
|
248
|
-
if (event
|
|
249
|
+
if (isPlainKey(event, "j", "down")) {
|
|
249
250
|
event.stopPropagation?.();
|
|
250
251
|
event.preventDefault?.();
|
|
251
252
|
scrollDetailBy(1);
|
|
252
253
|
return true;
|
|
253
254
|
}
|
|
254
|
-
if (event
|
|
255
|
+
if (isPlainKey(event, "k", "up")) {
|
|
255
256
|
event.stopPropagation?.();
|
|
256
257
|
event.preventDefault?.();
|
|
257
258
|
scrollDetailBy(-1);
|
|
@@ -9,7 +9,7 @@ import { colors, floatingPaneBg, floatingPaneTitleBg, paneTitleText } from "../.
|
|
|
9
9
|
import { useThemeColors } from "../../theme/theme-context";
|
|
10
10
|
import { hasPaneFooterContent, PaneFooterBar, PaneFooterProvider } from "./pane-footer";
|
|
11
11
|
import { PaneContent } from "./pane-content";
|
|
12
|
-
import { getPaneBodyWidth, NATIVE_PANE_BODY_LAYOUT_PROPS } from "./pane-sizing";
|
|
12
|
+
import { getNativePaneBodyWidth, getPaneBodyWidth, NATIVE_PANE_BODY_LAYOUT_PROPS } from "./pane-sizing";
|
|
13
13
|
import { getPaneDisplayTitle } from "./pane-title";
|
|
14
14
|
import { TITLEBAR_OVERLAY_HEIGHT_PX, TITLEBAR_TRAFFIC_LIGHT_WIDTH } from "./titlebar-overlay";
|
|
15
15
|
import {
|
|
@@ -52,7 +52,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
52
52
|
? getPaneDisplayTitle(titleState, instance, paneDef)
|
|
53
53
|
: "Detached Pane";
|
|
54
54
|
const focused = windowFocused;
|
|
55
|
-
const bodyWidth = nativePaneChrome ?
|
|
55
|
+
const bodyWidth = nativePaneChrome ? getNativePaneBodyWidth(width) : getPaneBodyWidth(width);
|
|
56
56
|
|
|
57
57
|
const focusPane = useCallback(() => {
|
|
58
58
|
setWindowFocused(true);
|
|
@@ -3,7 +3,12 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { colors, floatingPaneBg } from "../../theme/colors";
|
|
4
4
|
import { PaneHeader } from "./pane-header";
|
|
5
5
|
import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane-footer";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getNativePaneBodyHeight,
|
|
8
|
+
getPaneBodyHeight,
|
|
9
|
+
NATIVE_PANE_BODY_LAYOUT_PROPS,
|
|
10
|
+
shouldReservePaneFooter,
|
|
11
|
+
} from "./pane-sizing";
|
|
7
12
|
|
|
8
13
|
interface FloatingPaneWrapperProps {
|
|
9
14
|
paneId?: string;
|
|
@@ -77,7 +82,9 @@ export function FloatingPaneWrapper({
|
|
|
77
82
|
const bg = floatingPaneBg(focused);
|
|
78
83
|
const showFooter = hasPaneFooterContent(footer);
|
|
79
84
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
80
|
-
const bodyHeight =
|
|
85
|
+
const bodyHeight = nativePaneChrome
|
|
86
|
+
? getNativePaneBodyHeight(height, reserveFooter)
|
|
87
|
+
: getPaneBodyHeight(height, reserveFooter);
|
|
81
88
|
const bodyLayoutProps = nativePaneChrome
|
|
82
89
|
? NATIVE_PANE_BODY_LAYOUT_PROPS
|
|
83
90
|
: { height: bodyHeight };
|
|
@@ -393,7 +393,7 @@ export function PaneFooterBar({
|
|
|
393
393
|
"--pane-footer-border-color": empty ? "transparent" : topBorderColor,
|
|
394
394
|
borderTop: `1px solid ${empty ? "transparent" : topBorderColor}`,
|
|
395
395
|
backgroundColor: nativeBackgroundColor,
|
|
396
|
-
boxShadow: empty ? "none" :
|
|
396
|
+
boxShadow: empty ? "none" : `inset 0 1px 0 ${blendHex(nativeBackgroundColor, colors.textBright, 0.03)}`,
|
|
397
397
|
}}
|
|
398
398
|
>
|
|
399
399
|
<FooterContent
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, Span, Text, useUiCapabilities } from "../../ui";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { colors, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "../../theme/colors";
|
|
3
|
+
import { blendHex, colors, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "../../theme/colors";
|
|
4
4
|
|
|
5
5
|
const PANE_HEADER_HEIGHT = 1;
|
|
6
6
|
const PANE_HEADER_GRIP = ":: ";
|
|
@@ -127,7 +127,9 @@ export function PaneHeader({
|
|
|
127
127
|
style={{
|
|
128
128
|
borderBottom: `1px solid ${focused ? colors.borderFocused : colors.border}`,
|
|
129
129
|
paddingInline: 6,
|
|
130
|
-
boxShadow: focused
|
|
130
|
+
boxShadow: focused
|
|
131
|
+
? `inset 0 -1px 0 ${blendHex(paneTitleBg(focused), colors.borderFocused, 0.18)}`
|
|
132
|
+
: `inset 0 -1px 0 ${blendHex(paneTitleBg(focused), colors.textBright, 0.04)}`,
|
|
131
133
|
}}
|
|
132
134
|
>
|
|
133
135
|
<Text fg={focused ? colors.borderFocused : colors.textMuted} selectable={false} data-gloom-role="pane-grip">
|
|
@@ -20,3 +20,15 @@ export function getPaneBodyHeight(height: number, reserveFooter = true): number
|
|
|
20
20
|
export function getPaneBodyWidth(width: number): number {
|
|
21
21
|
return Math.max(1, Math.floor(width));
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
export function getNativePaneBodyHeight(height: number, reserveFooter = true): number {
|
|
25
|
+
const preciseHeight = Number.isFinite(height) ? height : 1;
|
|
26
|
+
return Math.max(
|
|
27
|
+
1,
|
|
28
|
+
preciseHeight - PANE_HEADER_ROWS - (reserveFooter ? PANE_FOOTER_ROWS : 0),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getNativePaneBodyWidth(width: number): number {
|
|
33
|
+
return Math.max(1, Number.isFinite(width) ? width : 1);
|
|
34
|
+
}
|
|
@@ -3,7 +3,12 @@ import type { ReactNode } from "react";
|
|
|
3
3
|
import { colors, paneBg } from "../../theme/colors";
|
|
4
4
|
import { PaneHeader } from "./pane-header";
|
|
5
5
|
import { hasPaneFooterContent, PaneFooterBar, type CombinedPaneFooter } from "./pane-footer";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getNativePaneBodyHeight,
|
|
8
|
+
getPaneBodyHeight,
|
|
9
|
+
NATIVE_PANE_BODY_LAYOUT_PROPS,
|
|
10
|
+
shouldReservePaneFooter,
|
|
11
|
+
} from "./pane-sizing";
|
|
7
12
|
|
|
8
13
|
interface PaneWrapperProps {
|
|
9
14
|
paneId?: string;
|
|
@@ -24,6 +29,23 @@ interface PaneWrapperProps {
|
|
|
24
29
|
children: ReactNode;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
function resolvePaneBodyHeight(
|
|
33
|
+
height: number,
|
|
34
|
+
hasTitle: boolean,
|
|
35
|
+
reserveFooter: boolean,
|
|
36
|
+
nativePaneChrome: boolean | undefined,
|
|
37
|
+
): number {
|
|
38
|
+
if (nativePaneChrome) {
|
|
39
|
+
return hasTitle
|
|
40
|
+
? getNativePaneBodyHeight(height, reserveFooter)
|
|
41
|
+
: Math.max(1, height);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return hasTitle
|
|
45
|
+
? getPaneBodyHeight(height, reserveFooter)
|
|
46
|
+
: Math.max(1, Math.floor(height));
|
|
47
|
+
}
|
|
48
|
+
|
|
27
49
|
export function PaneWrapper({
|
|
28
50
|
paneId,
|
|
29
51
|
title,
|
|
@@ -47,7 +69,7 @@ export function PaneWrapper({
|
|
|
47
69
|
const showFooter = hasPaneFooterContent(footer);
|
|
48
70
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
49
71
|
const bodyHeight = typeof height === "number"
|
|
50
|
-
?
|
|
72
|
+
? resolvePaneBodyHeight(height, !!title, reserveFooter, nativePaneChrome)
|
|
51
73
|
: undefined;
|
|
52
74
|
const bodyLayoutProps = nativePaneChrome
|
|
53
75
|
? NATIVE_PANE_BODY_LAYOUT_PROPS
|
|
@@ -33,6 +33,7 @@ import { removePaneInstances, type LayoutConfig } from "../../types/config";
|
|
|
33
33
|
import { contextMenuDivider, type ContextMenuItem } from "../../types/context-menu";
|
|
34
34
|
import {
|
|
35
35
|
resolveTickerForPane,
|
|
36
|
+
syncConfigActiveLayoutState,
|
|
36
37
|
useAppDispatch,
|
|
37
38
|
useAppSelector,
|
|
38
39
|
} from "../../state/app-context";
|
|
@@ -50,7 +51,13 @@ import { FloatingPaneWrapper } from "./floating-pane";
|
|
|
50
51
|
import { PaneContent } from "./pane-content";
|
|
51
52
|
import { PaneWrapper } from "./pane";
|
|
52
53
|
import { hasPaneFooterContent, PaneFooterProvider } from "./pane-footer";
|
|
53
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
getNativePaneBodyHeight,
|
|
56
|
+
getNativePaneBodyWidth,
|
|
57
|
+
getPaneBodyHeight,
|
|
58
|
+
getPaneBodyWidth,
|
|
59
|
+
shouldReservePaneFooter,
|
|
60
|
+
} from "./pane-sizing";
|
|
54
61
|
import { getPaneDisplayTitle } from "./pane-title";
|
|
55
62
|
import { TITLEBAR_OVERLAY_HEIGHT_PX } from "./titlebar-overlay";
|
|
56
63
|
import { capturePaneScreenshotPngBase64 } from "../../utils/dom-screenshot";
|
|
@@ -788,6 +795,7 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
788
795
|
const config = useAppSelector((state) => state.config);
|
|
789
796
|
const paneState = useAppSelector((state) => state.paneState);
|
|
790
797
|
const focusedPaneId = useAppSelector(selectFocusedPaneId);
|
|
798
|
+
const activePanel = useAppSelector((state) => state.activePanel);
|
|
791
799
|
const commandBarOpen = useAppSelector(selectCommandBarOpen);
|
|
792
800
|
const inputCaptured = useAppSelector((state) => state.inputCaptured);
|
|
793
801
|
const statusBarVisible = useAppSelector(selectStatusBarVisible);
|
|
@@ -870,15 +878,17 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
870
878
|
);
|
|
871
879
|
|
|
872
880
|
const persistLayout = useCallback((nextLayout: LayoutConfig, options?: { pushHistory?: boolean }) => {
|
|
873
|
-
const layouts = config.layouts.map((savedLayout, index) => (
|
|
874
|
-
index === config.activeLayoutIndex ? { ...savedLayout, layout: nextLayout } : savedLayout
|
|
875
|
-
));
|
|
876
881
|
if (options?.pushHistory !== false) {
|
|
877
882
|
dispatch({ type: "PUSH_LAYOUT_HISTORY" });
|
|
878
883
|
}
|
|
879
884
|
dispatch({ type: "UPDATE_LAYOUT", layout: nextLayout });
|
|
880
|
-
scheduleConfigSave(
|
|
881
|
-
|
|
885
|
+
scheduleConfigSave(syncConfigActiveLayoutState(
|
|
886
|
+
{ ...config, layout: nextLayout },
|
|
887
|
+
paneState,
|
|
888
|
+
focusedPaneId,
|
|
889
|
+
activePanel,
|
|
890
|
+
));
|
|
891
|
+
}, [activePanel, config, dispatch, focusedPaneId, paneState]);
|
|
882
892
|
|
|
883
893
|
const closeFocusedPane = useCallback(() => {
|
|
884
894
|
if (!focusedPaneId || !isPaneInLayout(visibleLayout, focusedPaneId)) return false;
|
|
@@ -1621,7 +1631,7 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
1621
1631
|
if (!pane) return null;
|
|
1622
1632
|
const focused = focusedPaneId === leaf.instanceId && (!overlayOpen || menuState?.paneId === leaf.instanceId);
|
|
1623
1633
|
const showActions = focused || hoveredPaneId === leaf.instanceId || menuState?.paneId === leaf.instanceId;
|
|
1624
|
-
const bodyWidth = nativePaneChrome ?
|
|
1634
|
+
const bodyWidth = nativePaneChrome ? getNativePaneBodyWidth(leaf.rect.width) : getPaneBodyWidth(leaf.rect.width);
|
|
1625
1635
|
return (
|
|
1626
1636
|
<Box
|
|
1627
1637
|
key={`dock:${leaf.instanceId}`}
|
|
@@ -1634,7 +1644,9 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
1634
1644
|
<PaneFooterProvider>
|
|
1635
1645
|
{(footer) => {
|
|
1636
1646
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
|
|
1637
|
-
const bodyHeight =
|
|
1647
|
+
const bodyHeight = nativePaneChrome
|
|
1648
|
+
? getNativePaneBodyHeight(leaf.rect.height, reserveFooter)
|
|
1649
|
+
: getPaneBodyHeight(leaf.rect.height, reserveFooter);
|
|
1638
1650
|
return (
|
|
1639
1651
|
<PaneWrapper
|
|
1640
1652
|
paneId={leaf.instanceId}
|
|
@@ -1674,12 +1686,14 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
1674
1686
|
: rect;
|
|
1675
1687
|
const focused = focusedPaneId === pane.instance.instanceId && (!overlayOpen || menuState?.paneId === pane.instance.instanceId);
|
|
1676
1688
|
const showActions = focused || hoveredPaneId === pane.instance.instanceId || menuState?.paneId === pane.instance.instanceId;
|
|
1677
|
-
const bodyWidth = nativePaneChrome ?
|
|
1689
|
+
const bodyWidth = nativePaneChrome ? getNativePaneBodyWidth(preview.width) : getPaneBodyWidth(preview.width);
|
|
1678
1690
|
return (
|
|
1679
1691
|
<PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
|
|
1680
1692
|
{(footer) => {
|
|
1681
1693
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, hasPaneFooterContent(footer));
|
|
1682
|
-
const bodyHeight =
|
|
1694
|
+
const bodyHeight = nativePaneChrome
|
|
1695
|
+
? getNativePaneBodyHeight(preview.height, reserveFooter)
|
|
1696
|
+
: getPaneBodyHeight(preview.height, reserveFooter);
|
|
1683
1697
|
return (
|
|
1684
1698
|
<FloatingPaneWrapper
|
|
1685
1699
|
paneId={pane.instance.instanceId}
|
|
@@ -187,7 +187,7 @@ describe("StatusBar", () => {
|
|
|
187
187
|
}
|
|
188
188
|
});
|
|
189
189
|
|
|
190
|
-
test("omits focused ticker
|
|
190
|
+
test("omits focused ticker quote details", async () => {
|
|
191
191
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
192
192
|
config.layout.instances = config.layout.instances.map((instance) => (
|
|
193
193
|
instance.instanceId === "ticker-detail:main"
|
|
@@ -247,6 +247,9 @@ describe("StatusBar", () => {
|
|
|
247
247
|
await testSetup.renderOnce();
|
|
248
248
|
|
|
249
249
|
const frame = testSetup.captureCharFrame();
|
|
250
|
+
expect(frame).not.toContain("AMD");
|
|
251
|
+
expect(frame).not.toContain("Pre");
|
|
252
|
+
expect(frame).not.toContain("101");
|
|
250
253
|
expect(frame).not.toContain("NASDAQ PRE-MKT");
|
|
251
254
|
expect(frame).not.toContain("px IBKR live");
|
|
252
255
|
expect(frame).not.toContain("ses Yahoo");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Box, Span, Text, TextAttributes, contextMenuDivider, useContextMenu, useUiCapabilities } from "../../ui";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
|
-
import { colors, hoverBg } from "../../theme/colors";
|
|
3
|
+
import { blendHex, colors, hoverBg } from "../../theme/colors";
|
|
4
4
|
import { useThemeColors } from "../../theme/theme-context";
|
|
5
|
-
import { useAppDispatch, useAppSelector
|
|
5
|
+
import { useAppDispatch, useAppSelector } from "../../state/app-context";
|
|
6
6
|
import {
|
|
7
7
|
selectActiveLayoutIndex,
|
|
8
8
|
selectGridlockTipSequence,
|
|
@@ -10,9 +10,6 @@ import {
|
|
|
10
10
|
selectSavedLayouts,
|
|
11
11
|
selectStatusBarVisible,
|
|
12
12
|
} from "../../state/selectors-ui";
|
|
13
|
-
import {
|
|
14
|
-
getExtendedHoursInfo,
|
|
15
|
-
} from "../../utils/market-status";
|
|
16
13
|
import { getSharedRegistry } from "../../plugins/registry";
|
|
17
14
|
import { gridlockAllPanes } from "../../plugins/pane-manager";
|
|
18
15
|
import { notifyGridlockComplete } from "../../plugins/gridlock-notification";
|
|
@@ -35,8 +32,6 @@ type LayoutTabItem = {
|
|
|
35
32
|
type StatusBarViewProps = {
|
|
36
33
|
activeLayoutIdx: number;
|
|
37
34
|
dismissGridlockTip: (event?: StatusBarEvent) => void;
|
|
38
|
-
extColor: string;
|
|
39
|
-
extText: string;
|
|
40
35
|
handleGridlockTip: (event?: StatusBarEvent) => void;
|
|
41
36
|
handleLayoutSelect: (value: string) => void;
|
|
42
37
|
hasMultipleLayouts: boolean;
|
|
@@ -47,7 +42,6 @@ type StatusBarViewProps = {
|
|
|
47
42
|
openLayoutContextMenu: (index: number, event: any) => void | Promise<unknown>;
|
|
48
43
|
setHoveredControl: SetHoveredControl;
|
|
49
44
|
showGridlockTip: boolean;
|
|
50
|
-
symbol: string | null | undefined;
|
|
51
45
|
};
|
|
52
46
|
|
|
53
47
|
function truncate(text: string, width: number): string {
|
|
@@ -68,14 +62,8 @@ export function StatusBar() {
|
|
|
68
62
|
const statusBarVisible = useAppSelector(selectStatusBarVisible);
|
|
69
63
|
const gridlockTipVisible = useAppSelector(selectGridlockTipVisible);
|
|
70
64
|
const gridlockTipSequence = useAppSelector(selectGridlockTipSequence);
|
|
71
|
-
const { symbol, financials: focusedFinancials } = useFocusedTicker();
|
|
72
65
|
const [hoveredControl, setHoveredControl] = useState<string | null>(null);
|
|
73
66
|
|
|
74
|
-
// Extended hours info for selected ticker
|
|
75
|
-
const extInfo = getExtendedHoursInfo(focusedFinancials?.quote);
|
|
76
|
-
const extText = extInfo?.text ?? "";
|
|
77
|
-
const extColor = extInfo?.color ?? colors.textDim;
|
|
78
|
-
|
|
79
67
|
const hasMultipleLayouts = layouts.length > 1;
|
|
80
68
|
const showGridlockTip = gridlockTipVisible && !!registry;
|
|
81
69
|
const layoutTabs = layouts.map((layout, index) => ({
|
|
@@ -211,8 +199,6 @@ export function StatusBar() {
|
|
|
211
199
|
const viewProps: StatusBarViewProps = {
|
|
212
200
|
activeLayoutIdx,
|
|
213
201
|
dismissGridlockTip,
|
|
214
|
-
extColor,
|
|
215
|
-
extText,
|
|
216
202
|
handleGridlockTip,
|
|
217
203
|
handleLayoutSelect,
|
|
218
204
|
hasMultipleLayouts,
|
|
@@ -223,7 +209,6 @@ export function StatusBar() {
|
|
|
223
209
|
openLayoutContextMenu,
|
|
224
210
|
setHoveredControl,
|
|
225
211
|
showGridlockTip,
|
|
226
|
-
symbol,
|
|
227
212
|
};
|
|
228
213
|
|
|
229
214
|
if (nativePaneChrome) {
|
|
@@ -251,7 +236,7 @@ function NativeStatusBar({
|
|
|
251
236
|
}}
|
|
252
237
|
style={{
|
|
253
238
|
borderTop: `1px solid ${colors.border}`,
|
|
254
|
-
boxShadow:
|
|
239
|
+
boxShadow: `inset 0 1px 0 ${blendHex(colors.panel, colors.textBright, 0.03)}`,
|
|
255
240
|
paddingInline: 8,
|
|
256
241
|
}}
|
|
257
242
|
>
|
|
@@ -416,15 +401,10 @@ function TerminalGridlockTip({
|
|
|
416
401
|
);
|
|
417
402
|
}
|
|
418
403
|
|
|
419
|
-
function StatusBarWidgets(
|
|
404
|
+
function StatusBarWidgets() {
|
|
420
405
|
return (
|
|
421
406
|
<>
|
|
422
407
|
<Box flexGrow={1} />
|
|
423
|
-
{extText && (
|
|
424
|
-
<Box paddingRight={1}>
|
|
425
|
-
<Text fg={extColor}>{symbol} {extText}</Text>
|
|
426
|
-
</Box>
|
|
427
|
-
)}
|
|
428
408
|
<PluginSlot name="status:widget" />
|
|
429
409
|
</>
|
|
430
410
|
);
|