gloomberb 0.9.1 → 0.9.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 +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +45 -44
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +56 -6
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +5 -2
- package/src/components/layout/header.tsx +13 -5
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-controls.tsx +19 -10
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/price-performance.tsx +183 -0
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +218 -15
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +9 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +11 -2
- package/src/renderers/electrobun/bun/desktop/window-style.ts +27 -1
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +11 -64
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +22 -2
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +9 -8
- package/src/renderers/electrobun/view/ui-host.tsx +102 -77
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -117,6 +117,42 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
|
|
121
|
+
mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
|
|
122
|
+
id: "prediction-markets",
|
|
123
|
+
name: "Prediction Markets",
|
|
124
|
+
component: () => null,
|
|
125
|
+
defaultPosition: "left",
|
|
126
|
+
defaultMode: "floating",
|
|
127
|
+
});
|
|
128
|
+
mutableRegistryMap(pluginRegistry.paneTemplates).set("new-prediction-markets-pane", {
|
|
129
|
+
id: "new-prediction-markets-pane",
|
|
130
|
+
paneId: "prediction-markets",
|
|
131
|
+
label: "Prediction Markets",
|
|
132
|
+
description: "Open a new prediction markets browser pane",
|
|
133
|
+
keywords: ["prediction", "markets", "polymarket", "kalshi", "events"],
|
|
134
|
+
shortcut: { prefix: "PM", argPlaceholder: "query", argKind: "text" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function registerShortcutOnlyPane(pluginRegistry: MutablePaneRegistry): void {
|
|
139
|
+
mutableRegistryMap(pluginRegistry.panes).set("top-news", {
|
|
140
|
+
id: "top-news",
|
|
141
|
+
name: "Top News",
|
|
142
|
+
component: () => null,
|
|
143
|
+
defaultPosition: "right",
|
|
144
|
+
defaultMode: "floating",
|
|
145
|
+
});
|
|
146
|
+
mutableRegistryMap(pluginRegistry.paneTemplates).set("top-news-pane", {
|
|
147
|
+
id: "top-news-pane",
|
|
148
|
+
paneId: "top-news",
|
|
149
|
+
label: "Top News",
|
|
150
|
+
description: "Curated top market stories ranked by importance",
|
|
151
|
+
keywords: ["top", "news", "headlines", "stories"],
|
|
152
|
+
shortcut: { prefix: "TOP" },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
describe("CommandBar pane and layout routes", () => {
|
|
121
157
|
const layoutModeConfig = (config: AppConfig): AppConfig => {
|
|
122
158
|
const research = cloneLayout(config.layout);
|
|
@@ -271,6 +307,46 @@ describe("CommandBar pane and layout routes", () => {
|
|
|
271
307
|
expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
|
|
272
308
|
});
|
|
273
309
|
|
|
310
|
+
for (const scenario of [
|
|
311
|
+
{
|
|
312
|
+
query: "prediction markets",
|
|
313
|
+
register: registerQueryOnlyPane,
|
|
314
|
+
templateId: "new-prediction-markets-pane",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
query: "top news",
|
|
318
|
+
register: registerShortcutOnlyPane,
|
|
319
|
+
templateId: "top-news-pane",
|
|
320
|
+
},
|
|
321
|
+
] as const) {
|
|
322
|
+
test(`creates ${scenario.templateId} directly when it has no config fields`, async () => {
|
|
323
|
+
const created: CreatedPaneCall[] = [];
|
|
324
|
+
|
|
325
|
+
testSetup = await testRender(<CommandBarHarness
|
|
326
|
+
query={scenario.query}
|
|
327
|
+
live
|
|
328
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
329
|
+
scenario.register(pluginRegistry);
|
|
330
|
+
recordPaneCreations(pluginRegistry, created);
|
|
331
|
+
}}
|
|
332
|
+
/>, {
|
|
333
|
+
width: 100,
|
|
334
|
+
height: 18,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await testSetup.renderOnce();
|
|
338
|
+
|
|
339
|
+
await act(async () => {
|
|
340
|
+
testSetup!.mockInput.pressEnter();
|
|
341
|
+
await Bun.sleep(0);
|
|
342
|
+
await testSetup!.renderOnce();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(created).toEqual([{ templateId: scenario.templateId, options: undefined }]);
|
|
346
|
+
expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
274
350
|
test("shows pane templates that share the same shortcut", async () => {
|
|
275
351
|
testSetup = await testRender(<CommandBarHarness
|
|
276
352
|
query="DUP"
|
|
@@ -26,6 +26,11 @@ export type DataTableKeyEvent = TableViewKeyEvent;
|
|
|
26
26
|
|
|
27
27
|
const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
|
|
28
28
|
|
|
29
|
+
export interface DataTableRootKeyContext {
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
itemCount: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export type DataTableSelectionChangeReason =
|
|
30
35
|
| "keyboard"
|
|
31
36
|
| "pointer"
|
|
@@ -95,7 +100,10 @@ export interface DataTableViewProps<
|
|
|
95
100
|
hoveredIdx?: number | null;
|
|
96
101
|
setHoveredIdx?: (index: number | null) => void;
|
|
97
102
|
keyboardNavigation?: boolean;
|
|
98
|
-
onRootKeyDown?: (
|
|
103
|
+
onRootKeyDown?: (
|
|
104
|
+
event: DataTableKeyEvent,
|
|
105
|
+
context: DataTableRootKeyContext,
|
|
106
|
+
) => boolean | void;
|
|
99
107
|
resetScrollKey?: unknown;
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -486,7 +494,10 @@ export function DataTableView<
|
|
|
486
494
|
if (event.defaultPrevented || event.propagationStopped) return;
|
|
487
495
|
if (!focused || !keyboardNavigation) return;
|
|
488
496
|
|
|
489
|
-
if (onRootKeyDown?.(event
|
|
497
|
+
if (onRootKeyDown?.(event, {
|
|
498
|
+
selectedIndex: effectiveSelectedIndexRef.current,
|
|
499
|
+
itemCount: tableProps.items.length,
|
|
500
|
+
})) return;
|
|
490
501
|
if (tableProps.items.length === 0) return;
|
|
491
502
|
|
|
492
503
|
if (isNextTableRowKey(event)) {
|
|
@@ -6,6 +6,7 @@ import { formatTimeAgo } from "../../utils/format";
|
|
|
6
6
|
import { isPlainKey } from "../../utils/keyboard";
|
|
7
7
|
import { toTimestampMillis } from "../../utils/timestamp";
|
|
8
8
|
import { DataTableStackView } from "../data-table/stack-view";
|
|
9
|
+
import type { DataTableRootKeyContext } from "../data-table/view";
|
|
9
10
|
import {
|
|
10
11
|
activeStackIndex,
|
|
11
12
|
sortIndexedStackRows,
|
|
@@ -45,9 +46,16 @@ interface FeedDataTableStackViewProps {
|
|
|
45
46
|
rootAfter?: ReactNode;
|
|
46
47
|
onRootKeyDown?: (event: {
|
|
47
48
|
name?: string;
|
|
49
|
+
sequence?: string;
|
|
50
|
+
ctrl?: boolean;
|
|
51
|
+
meta?: boolean;
|
|
52
|
+
super?: boolean;
|
|
53
|
+
alt?: boolean;
|
|
54
|
+
option?: boolean;
|
|
55
|
+
shift?: boolean;
|
|
48
56
|
preventDefault?: () => void;
|
|
49
57
|
stopPropagation?: () => void;
|
|
50
|
-
}) => boolean | void;
|
|
58
|
+
}, context: DataTableRootKeyContext) => boolean | void;
|
|
51
59
|
sourceLabel?: string;
|
|
52
60
|
titleLabel?: string;
|
|
53
61
|
emptyStateTitle?: string;
|
package/src/components/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { TickerListVisibleRange } from "./ticker/list-table-view";
|
|
|
8
8
|
export { TickerBadgeList } from "./ticker/badge/list";
|
|
9
9
|
export { InputSearchBar } from "./input-search-bar";
|
|
10
10
|
export { DataTableView } from "./data-table/view";
|
|
11
|
-
export type { DataTableKeyEvent } from "./data-table/view";
|
|
11
|
+
export type { DataTableKeyEvent, DataTableRootKeyContext } from "./data-table/view";
|
|
12
12
|
export { DataTableStackView } from "./data-table/stack-view";
|
|
13
13
|
export { FeedDataTableStackView } from "./feed-data-table/stack-view";
|
|
14
14
|
export type { FeedDataTableItem } from "./feed-data-table/stack-view";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useCallback, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
+
import { AppContext, createInitialState, type AppAction } from "../state/app/context";
|
|
5
|
+
import { createDefaultConfig } from "../types/config";
|
|
6
|
+
import type { InputRenderable } from "../ui";
|
|
7
|
+
import { InputSearchBar } from "./input-search-bar";
|
|
8
|
+
|
|
9
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
|
+
let setSearchActive: Dispatch<SetStateAction<boolean>> | null = null;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
setSearchActive = null;
|
|
14
|
+
if (!testSetup) return;
|
|
15
|
+
await act(async () => {
|
|
16
|
+
testSetup!.renderer.destroy();
|
|
17
|
+
});
|
|
18
|
+
testSetup = undefined;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function Harness({
|
|
22
|
+
actions,
|
|
23
|
+
onNavigateDown,
|
|
24
|
+
}: {
|
|
25
|
+
actions: AppAction[];
|
|
26
|
+
onNavigateDown?: () => void;
|
|
27
|
+
}) {
|
|
28
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-input-search-bar"));
|
|
29
|
+
const inputRef = useRef<InputRenderable | null>(null);
|
|
30
|
+
const [active, setActive] = useState(true);
|
|
31
|
+
setSearchActive = setActive;
|
|
32
|
+
const dispatch = useCallback((action: AppAction) => {
|
|
33
|
+
actions.push(action);
|
|
34
|
+
}, [actions]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AppContext value={{ state, dispatch }}>
|
|
38
|
+
<InputSearchBar
|
|
39
|
+
value=""
|
|
40
|
+
focused
|
|
41
|
+
active={active}
|
|
42
|
+
width={30}
|
|
43
|
+
focusToken={0}
|
|
44
|
+
inputRef={inputRef}
|
|
45
|
+
placeholder="search"
|
|
46
|
+
debounceMs={100}
|
|
47
|
+
onNavigateDown={onNavigateDown}
|
|
48
|
+
onFocus={() => {}}
|
|
49
|
+
onBlur={() => {}}
|
|
50
|
+
onQueryChange={() => {}}
|
|
51
|
+
/>
|
|
52
|
+
</AppContext>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("InputSearchBar", () => {
|
|
57
|
+
test("captures app input while the search input is active", async () => {
|
|
58
|
+
const actions: AppAction[] = [];
|
|
59
|
+
|
|
60
|
+
testSetup = await testRender(<Harness actions={actions} />, { width: 40, height: 4 });
|
|
61
|
+
await act(async () => {
|
|
62
|
+
await testSetup!.renderOnce();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(actions).toEqual([{ type: "SET_INPUT_CAPTURED", captured: true }]);
|
|
66
|
+
if (!setSearchActive) throw new Error("search setter was not registered");
|
|
67
|
+
|
|
68
|
+
await act(async () => {
|
|
69
|
+
setSearchActive(false);
|
|
70
|
+
await testSetup!.renderOnce();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(actions).toEqual([
|
|
74
|
+
{ type: "SET_INPUT_CAPTURED", captured: true },
|
|
75
|
+
{ type: "SET_INPUT_CAPTURED", captured: false },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("moves from the active search input to the table with Down", async () => {
|
|
80
|
+
const actions: AppAction[] = [];
|
|
81
|
+
let navigatedDown = 0;
|
|
82
|
+
|
|
83
|
+
testSetup = await testRender(
|
|
84
|
+
<Harness
|
|
85
|
+
actions={actions}
|
|
86
|
+
onNavigateDown={() => {
|
|
87
|
+
navigatedDown += 1;
|
|
88
|
+
}}
|
|
89
|
+
/>,
|
|
90
|
+
{ width: 40, height: 4 },
|
|
91
|
+
);
|
|
92
|
+
await act(async () => {
|
|
93
|
+
await testSetup!.renderOnce();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
testSetup!.mockInput.pressArrow("down");
|
|
98
|
+
await testSetup!.renderOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(navigatedDown).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, type RefObject } from "react";
|
|
2
2
|
import { Box, Input, Text, type InputRenderable } from "../ui";
|
|
3
3
|
import { colors } from "../theme/colors";
|
|
4
|
+
import { useAppInputCapture } from "../state/app/input-capture";
|
|
5
|
+
import { useShortcut } from "../react/input";
|
|
6
|
+
import { isPlainArrowDown, stopSearchFocusNavigation } from "../utils/search-focus-navigation";
|
|
4
7
|
|
|
5
8
|
export function InputSearchBar({
|
|
6
9
|
value,
|
|
@@ -12,6 +15,7 @@ export function InputSearchBar({
|
|
|
12
15
|
placeholder,
|
|
13
16
|
debounceMs,
|
|
14
17
|
normalizeValue = identity,
|
|
18
|
+
onNavigateDown,
|
|
15
19
|
onFocus,
|
|
16
20
|
onBlur,
|
|
17
21
|
onQueryChange,
|
|
@@ -25,11 +29,23 @@ export function InputSearchBar({
|
|
|
25
29
|
placeholder: string;
|
|
26
30
|
debounceMs: number;
|
|
27
31
|
normalizeValue?: (value: string) => string;
|
|
32
|
+
onNavigateDown?: () => void;
|
|
28
33
|
onFocus: () => void;
|
|
29
34
|
onBlur: () => void;
|
|
30
35
|
onQueryChange: (query: string) => void;
|
|
31
36
|
}) {
|
|
32
37
|
const [draft, setDraft] = useState(value);
|
|
38
|
+
useAppInputCapture(focused && active);
|
|
39
|
+
|
|
40
|
+
useShortcut((event) => {
|
|
41
|
+
if (!onNavigateDown || !isPlainArrowDown(event)) return;
|
|
42
|
+
stopSearchFocusNavigation(event);
|
|
43
|
+
onNavigateDown();
|
|
44
|
+
}, {
|
|
45
|
+
allowEditable: true,
|
|
46
|
+
enabled: focused && active && !!onNavigateDown,
|
|
47
|
+
phase: "before",
|
|
48
|
+
});
|
|
33
49
|
|
|
34
50
|
useEffect(() => {
|
|
35
51
|
setDraft(value);
|
|
@@ -13,7 +13,7 @@ import { PaneContent } from "./pane/content";
|
|
|
13
13
|
import { resolvePaneBodyFrame } from "./pane/sizing";
|
|
14
14
|
import { getPaneDisplayTitle } from "./pane/title";
|
|
15
15
|
import { TITLEBAR_OVERLAY_HEIGHT_PX, getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
16
|
-
import { WindowControls } from "./window-controls";
|
|
16
|
+
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
17
17
|
import {
|
|
18
18
|
createDoubleEscapeCloseState,
|
|
19
19
|
recordDoubleEscapeClose,
|
|
@@ -174,8 +174,10 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
174
174
|
alignItems="center"
|
|
175
175
|
flexGrow={1}
|
|
176
176
|
minWidth={0}
|
|
177
|
+
backgroundColor={titleBackground}
|
|
177
178
|
paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}
|
|
178
179
|
paddingRight={showWindowControls ? 0 : 1}
|
|
180
|
+
style={{ position: "relative" }}
|
|
179
181
|
>
|
|
180
182
|
<Box flexGrow={1} minWidth={0} overflow="hidden">
|
|
181
183
|
<Text fg={paneTitleText(focused, true)} selectable={false} data-gloom-role="pane-title">{title}</Text>
|
|
@@ -192,7 +194,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
192
194
|
{" ... "}
|
|
193
195
|
</Text>
|
|
194
196
|
)}
|
|
195
|
-
{showWindowControls ? <
|
|
197
|
+
{showWindowControls ? <Box flexShrink={0} width={`${WINDOWS_CONTROL_GROUP_WIDTH_PX}px`} /> : null}
|
|
198
|
+
{showWindowControls ? <WindowControls windowKind="detached" /> : null}
|
|
196
199
|
</Box>
|
|
197
200
|
</Box>
|
|
198
201
|
<PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={background}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Box, SpinnerMark, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
2
|
-
import { useEffect, type ReactNode } from "react";
|
|
1
|
+
import { Box, SpinnerMark, Text, TextAttributes, useRendererHost, useUiCapabilities } from "../../ui";
|
|
2
|
+
import { useCallback, useEffect, type ReactNode } from "react";
|
|
3
3
|
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
4
4
|
import { useThemeColors } from "../../theme/theme-context";
|
|
5
5
|
import { useAppActive } from "../../state/app/activity";
|
|
@@ -18,7 +18,7 @@ import { formatMarketPrice } from "../../market-data/market/format";
|
|
|
18
18
|
import { marketStateLabel, marketStateColor, getActiveQuoteDisplay } from "../../market-data/market/status";
|
|
19
19
|
import { VERSION } from "../../version";
|
|
20
20
|
import { getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
21
|
-
import { WindowControls } from "./window-controls";
|
|
21
|
+
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
22
22
|
|
|
23
23
|
const SPY_REFRESH_MS = 5 * 60_000; // 5 min
|
|
24
24
|
const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
@@ -124,6 +124,7 @@ function UpdateStatus() {
|
|
|
124
124
|
|
|
125
125
|
export function Header() {
|
|
126
126
|
useThemeColors();
|
|
127
|
+
const rendererHost = useRendererHost();
|
|
127
128
|
const baseCurrency = useAppSelector(selectBaseCurrency);
|
|
128
129
|
const appActive = useAppActive();
|
|
129
130
|
const { titleBarOverlay, windowControls } = useUiCapabilities();
|
|
@@ -150,6 +151,11 @@ export function Header() {
|
|
|
150
151
|
? `SPY ${formatMarketPrice(activeSpyQuote.price, { assetCategory: "ETF" })} ${formatPercentRaw(activeSpyQuote.changePercent)}`
|
|
151
152
|
: "SPY —";
|
|
152
153
|
|
|
154
|
+
const startWindowDrag = useCallback(() => {
|
|
155
|
+
if (!titleBarOverlay) return;
|
|
156
|
+
void rendererHost.startWindowDrag?.();
|
|
157
|
+
}, [rendererHost, titleBarOverlay]);
|
|
158
|
+
|
|
153
159
|
// Market status
|
|
154
160
|
const mktState = spyQuote?.marketState;
|
|
155
161
|
const mktLabel = mktState ? marketStateLabel(mktState) : "";
|
|
@@ -164,11 +170,12 @@ export function Header() {
|
|
|
164
170
|
backgroundColor={colors.header}
|
|
165
171
|
data-gloom-role="app-header"
|
|
166
172
|
data-titlebar-overlay="true"
|
|
167
|
-
|
|
173
|
+
onMouseDown={startWindowDrag}
|
|
168
174
|
style={{
|
|
169
175
|
boxShadow: `0 -1px 0 ${colors.header}, inset 0 1px 0 ${colors.header}`,
|
|
170
176
|
paddingLeft: 8,
|
|
171
177
|
paddingRight: showWindowControls ? 0 : 12,
|
|
178
|
+
position: "relative",
|
|
172
179
|
}}
|
|
173
180
|
>
|
|
174
181
|
<Box paddingLeft={titlebarLeadingInset} flexDirection="row" alignItems="center" gap={1}>
|
|
@@ -201,6 +208,7 @@ export function Header() {
|
|
|
201
208
|
<Box paddingRight={showWindowControls ? 1 : 0}>
|
|
202
209
|
<Text fg={colors.headerText}>{baseCurrency}</Text>
|
|
203
210
|
</Box>
|
|
211
|
+
{showWindowControls ? <Box flexShrink={0} width={`${WINDOWS_CONTROL_GROUP_WIDTH_PX}px`} /> : null}
|
|
204
212
|
{showWindowControls ? <WindowControls /> : null}
|
|
205
213
|
</Box>
|
|
206
214
|
);
|
|
@@ -213,7 +221,7 @@ export function Header() {
|
|
|
213
221
|
backgroundColor={colors.header}
|
|
214
222
|
data-gloom-role="app-header"
|
|
215
223
|
data-titlebar-overlay={titleBarOverlay ? "true" : undefined}
|
|
216
|
-
|
|
224
|
+
onMouseDown={startWindowDrag}
|
|
217
225
|
>
|
|
218
226
|
<Box paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}>
|
|
219
227
|
<Text attributes={TextAttributes.BOLD} fg={colors.headerText}>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getRememberedFloatingRect,
|
|
5
5
|
resizeSplitAtPath,
|
|
6
6
|
simulateDrop,
|
|
7
|
+
type DockGeometryOptions,
|
|
7
8
|
type DockLeafLayout,
|
|
8
9
|
type DropTarget,
|
|
9
10
|
type LayoutBounds,
|
|
@@ -32,6 +33,7 @@ interface UseShellActiveDragOptions {
|
|
|
32
33
|
bounds: LayoutBounds;
|
|
33
34
|
contentHeight: number;
|
|
34
35
|
dispatch: Dispatch<AppAction>;
|
|
36
|
+
dockGeometryOptions: DockGeometryOptions;
|
|
35
37
|
dockLeafLayouts: DockLeafLayout[];
|
|
36
38
|
dragRuntime: ShellDragRuntimeState;
|
|
37
39
|
focusPane: (paneId: string) => void;
|
|
@@ -51,6 +53,7 @@ export function useShellActiveDrag({
|
|
|
51
53
|
bounds,
|
|
52
54
|
contentHeight,
|
|
53
55
|
dispatch,
|
|
56
|
+
dockGeometryOptions,
|
|
54
57
|
dockLeafLayouts,
|
|
55
58
|
dragRuntime,
|
|
56
59
|
focusPane,
|
|
@@ -117,7 +120,7 @@ export function useShellActiveDrag({
|
|
|
117
120
|
const hoveredCell = hoveredOverlay.cells.find((cell) => pointInRect(cell.rect, hitX, hitShellY));
|
|
118
121
|
if (hoveredCell) {
|
|
119
122
|
const target: DropTarget = { kind: "leaf", targetId: hoveredOverlay.targetId, position: hoveredCell.position };
|
|
120
|
-
const simulation = simulateDrop(baseLayout, drag.paneId, target, bounds);
|
|
123
|
+
const simulation = simulateDrop(baseLayout, drag.paneId, target, bounds, dockGeometryOptions);
|
|
121
124
|
if (simulation.previewRect) {
|
|
122
125
|
updateDockPreview({ kind: "dock", target, rect: simulation.previewRect });
|
|
123
126
|
} else {
|
|
@@ -199,6 +202,7 @@ export function useShellActiveDrag({
|
|
|
199
202
|
bounds,
|
|
200
203
|
contentHeight,
|
|
201
204
|
dispatch,
|
|
205
|
+
dockGeometryOptions,
|
|
202
206
|
dividerPreviewRef,
|
|
203
207
|
dockLeafLayouts,
|
|
204
208
|
dockPreviewRef,
|
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
type MutableRefObject,
|
|
7
7
|
type SetStateAction,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { useRafCallback } from "../../../../react/use-raf-callback";
|
|
9
10
|
import {
|
|
10
11
|
type DockDividerLayout,
|
|
12
|
+
type DockGeometryOptions,
|
|
11
13
|
type DockLeafLayout,
|
|
12
14
|
type FloatingRect,
|
|
13
15
|
type LayoutBounds,
|
|
@@ -91,17 +93,26 @@ export function useShellDragRuntimeState({
|
|
|
91
93
|
}): ShellDragRuntimeState {
|
|
92
94
|
const dragRef = useRef<DragMode | null>(null);
|
|
93
95
|
const [dragFloatingRect, setDragFloatingRect] = useState<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
96
|
+
const pendingDragFloatingRectRef = useRef<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
94
97
|
const [dragCursor, setDragCursor] = useState<{ x: number; y: number } | null>(null);
|
|
95
98
|
const [dividerPreview, setDividerPreview] = useState<DividerPreviewState | null>(null);
|
|
96
99
|
const [dockPreview, setDockPreview] = useState<DragPreview | null>(null);
|
|
97
100
|
const dividerPreviewRef = useRef<DividerPreviewState | null>(null);
|
|
98
101
|
const dockPreviewRef = useRef<DragPreview | null>(null);
|
|
102
|
+
const flushDragFloatingRect = useRafCallback(() => {
|
|
103
|
+
setDragFloatingRect(pendingDragFloatingRectRef.current);
|
|
104
|
+
});
|
|
99
105
|
|
|
100
106
|
const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
|
|
101
|
-
|
|
107
|
+
pendingDragFloatingRectRef.current = next
|
|
102
108
|
? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
|
|
103
|
-
: null
|
|
104
|
-
|
|
109
|
+
: null;
|
|
110
|
+
if (!next) {
|
|
111
|
+
setDragFloatingRect(null);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
flushDragFloatingRect();
|
|
115
|
+
}, [contentHeight, flushDragFloatingRect, width]);
|
|
105
116
|
|
|
106
117
|
const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
|
|
107
118
|
dividerPreviewRef.current = next;
|
|
@@ -146,6 +157,7 @@ interface UseShellPointerRuntimeOptions {
|
|
|
146
157
|
closePaneMenu: () => void;
|
|
147
158
|
contentHeight: number;
|
|
148
159
|
dispatch: Dispatch<AppAction>;
|
|
160
|
+
dockGeometryOptions: DockGeometryOptions;
|
|
149
161
|
dockDividerLayouts: DockDividerLayout[];
|
|
150
162
|
dockLeafLayouts: DockLeafLayout[];
|
|
151
163
|
dragRuntime: ShellDragRuntimeState;
|
|
@@ -166,6 +178,7 @@ interface UseShellPointerRuntimeOptions {
|
|
|
166
178
|
setHoveredMenuItemId: Dispatch<SetStateAction<string | null>>;
|
|
167
179
|
setMenuState: Dispatch<SetStateAction<ActionMenuState | null>>;
|
|
168
180
|
snapGuides: ReturnType<typeof makeSnapGuides>;
|
|
181
|
+
transientFocusActive: boolean;
|
|
169
182
|
updateWindowModePreviewLayout: (nextLayout: LayoutConfig, paneId?: string) => void;
|
|
170
183
|
visibleFloatingPanes: VisibleFloatingPane[];
|
|
171
184
|
visibleLayout: LayoutConfig;
|
|
@@ -179,6 +192,7 @@ export function useShellPointerRuntime({
|
|
|
179
192
|
closePaneMenu,
|
|
180
193
|
contentHeight,
|
|
181
194
|
dispatch,
|
|
195
|
+
dockGeometryOptions,
|
|
182
196
|
dockDividerLayouts,
|
|
183
197
|
dockLeafLayouts,
|
|
184
198
|
dragRuntime,
|
|
@@ -195,6 +209,7 @@ export function useShellPointerRuntime({
|
|
|
195
209
|
setHoveredMenuItemId,
|
|
196
210
|
setMenuState,
|
|
197
211
|
snapGuides,
|
|
212
|
+
transientFocusActive,
|
|
198
213
|
updateWindowModePreviewLayout,
|
|
199
214
|
visibleFloatingPanes,
|
|
200
215
|
visibleLayout,
|
|
@@ -206,6 +221,7 @@ export function useShellPointerRuntime({
|
|
|
206
221
|
bounds,
|
|
207
222
|
contentHeight,
|
|
208
223
|
dispatch,
|
|
224
|
+
dockGeometryOptions,
|
|
209
225
|
dockLeafLayouts,
|
|
210
226
|
focusPane,
|
|
211
227
|
dragRuntime,
|
|
@@ -237,6 +253,7 @@ export function useShellPointerRuntime({
|
|
|
237
253
|
selectWindowModePane,
|
|
238
254
|
setHoveredMenuItemId,
|
|
239
255
|
setMenuState,
|
|
256
|
+
transientFocusActive,
|
|
240
257
|
visibleFloatingPanes,
|
|
241
258
|
width,
|
|
242
259
|
windowMode,
|
|
@@ -254,6 +271,7 @@ export function useShellPointerRuntime({
|
|
|
254
271
|
selectWindowModePane,
|
|
255
272
|
setHoveredMenuItemId,
|
|
256
273
|
setMenuState,
|
|
274
|
+
transientFocusActive,
|
|
257
275
|
windowMode,
|
|
258
276
|
});
|
|
259
277
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { isPaneInLayout } from "../../../plugins/pane-manager";
|
|
2
|
+
import { cloneLayout, type LayoutConfig } from "../../../types/config";
|
|
3
|
+
|
|
4
|
+
export function resolvePaneFocusSourceLayout(
|
|
5
|
+
layout: LayoutConfig,
|
|
6
|
+
paneId: string | null,
|
|
7
|
+
): LayoutConfig | null {
|
|
8
|
+
if (!paneId || !isPaneInLayout(layout, paneId)) return null;
|
|
9
|
+
return cloneLayout(layout);
|
|
10
|
+
}
|