gloomberb 0.9.3 → 0.9.4
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 +1 -0
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +10 -1
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/core/pointer.ts +32 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +9 -2
- package/src/components/layout/shell/index.test.tsx +92 -2
- package/src/components/layout/shell/index.tsx +37 -2
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/pane/layers.tsx +8 -2
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/status-bar.test.tsx +6 -1
- package/src/components/layout/status-bar.tsx +5 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +43 -0
- package/src/plugins/prediction-markets/pane.tsx +50 -22
- package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -4,28 +4,35 @@ const PANE_FOOTER_ROWS = 1;
|
|
|
4
4
|
|
|
5
5
|
const NATIVE_PANE_BODY_LAYOUT_PROPS = {
|
|
6
6
|
flexGrow: 1,
|
|
7
|
+
flexShrink: 1,
|
|
7
8
|
flexBasis: 0,
|
|
9
|
+
minWidth: 0,
|
|
8
10
|
minHeight: 0,
|
|
9
11
|
} as const;
|
|
10
12
|
|
|
11
|
-
export function shouldReservePaneFooter(nativePaneChrome: boolean | undefined,
|
|
12
|
-
return !nativePaneChrome
|
|
13
|
+
export function shouldReservePaneFooter(nativePaneChrome: boolean | undefined, _showFooter: boolean): boolean {
|
|
14
|
+
return !nativePaneChrome;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function resolvePaneBodyHeight({
|
|
16
18
|
height,
|
|
17
19
|
nativePaneChrome,
|
|
20
|
+
footerVisible,
|
|
18
21
|
reserveFooter = true,
|
|
19
22
|
headerRows = PANE_HEADER_ROWS,
|
|
20
23
|
}: {
|
|
21
24
|
height: number;
|
|
22
25
|
nativePaneChrome?: boolean;
|
|
26
|
+
footerVisible?: boolean;
|
|
23
27
|
reserveFooter?: boolean;
|
|
24
28
|
headerRows?: number;
|
|
25
29
|
}): number {
|
|
26
30
|
const finiteHeight = Number.isFinite(height) ? height : 1;
|
|
27
31
|
const normalizedHeight = nativePaneChrome ? finiteHeight : Math.max(1, Math.floor(finiteHeight));
|
|
28
|
-
|
|
32
|
+
const footerRows = nativePaneChrome
|
|
33
|
+
? footerVisible ? PANE_FOOTER_ROWS : 0
|
|
34
|
+
: reserveFooter ? PANE_FOOTER_ROWS : 0;
|
|
35
|
+
return Math.max(1, normalizedHeight - headerRows - footerRows);
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
function getPaneBodyLayoutProps(nativePaneChrome: boolean | undefined, bodyHeight: number | undefined) {
|
|
@@ -41,17 +48,19 @@ export function resolvePaneBodyFrame({
|
|
|
41
48
|
width,
|
|
42
49
|
height,
|
|
43
50
|
nativePaneChrome,
|
|
51
|
+
footerVisible,
|
|
44
52
|
reserveFooter = true,
|
|
45
53
|
headerRows = PANE_HEADER_ROWS,
|
|
46
54
|
}: {
|
|
47
55
|
width?: number;
|
|
48
56
|
height?: number;
|
|
49
57
|
nativePaneChrome?: boolean;
|
|
58
|
+
footerVisible?: boolean;
|
|
50
59
|
reserveFooter?: boolean;
|
|
51
60
|
headerRows?: number;
|
|
52
61
|
}) {
|
|
53
62
|
const bodyHeight = typeof height === "number"
|
|
54
|
-
? resolvePaneBodyHeight({ height, nativePaneChrome, reserveFooter, headerRows })
|
|
63
|
+
? resolvePaneBodyHeight({ height, nativePaneChrome, footerVisible, reserveFooter, headerRows })
|
|
55
64
|
: undefined;
|
|
56
65
|
return {
|
|
57
66
|
width: typeof width === "number" ? resolvePaneBodyWidth(width, nativePaneChrome) : undefined,
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useEffect, useRef, type RefObject } from "react";
|
|
2
|
+
import { useNativeRenderer, type NativeCursorState, type NativePostProcessFn } from "../../../ui";
|
|
3
|
+
import type { FloatingRect, LayoutBounds, ResolvedPane } from "../../../plugins/pane-manager";
|
|
4
|
+
import { constrainFloatingRectToBounds } from "./drag";
|
|
5
|
+
|
|
6
|
+
interface VisibleFloatingPane {
|
|
7
|
+
pane: ResolvedPane;
|
|
8
|
+
rect: FloatingRect;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ShellCursorOcclusionRect extends LayoutBounds {
|
|
12
|
+
paneId: string;
|
|
13
|
+
zIndex: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ResolveShellCursorOcclusionRectsOptions {
|
|
17
|
+
contentHeight: number;
|
|
18
|
+
dragFloatingRect: { paneId: string; rect: FloatingRect } | null;
|
|
19
|
+
nativePaneChrome: boolean;
|
|
20
|
+
overlayOpen: boolean;
|
|
21
|
+
transientFocusActive: boolean;
|
|
22
|
+
visibleFloatingPanes: readonly VisibleFloatingPane[];
|
|
23
|
+
width: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveShellCursorOcclusionRects({
|
|
27
|
+
contentHeight,
|
|
28
|
+
dragFloatingRect,
|
|
29
|
+
nativePaneChrome,
|
|
30
|
+
overlayOpen,
|
|
31
|
+
transientFocusActive,
|
|
32
|
+
visibleFloatingPanes,
|
|
33
|
+
width,
|
|
34
|
+
}: ResolveShellCursorOcclusionRectsOptions): ShellCursorOcclusionRect[] {
|
|
35
|
+
if (nativePaneChrome || overlayOpen || transientFocusActive || visibleFloatingPanes.length === 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return visibleFloatingPanes.map(({ pane, rect }) => {
|
|
40
|
+
const paneId = pane.instance.instanceId;
|
|
41
|
+
const visibleRect = dragFloatingRect?.paneId === paneId
|
|
42
|
+
? constrainFloatingRectToBounds(dragFloatingRect.rect, width, contentHeight)
|
|
43
|
+
: rect;
|
|
44
|
+
return {
|
|
45
|
+
paneId,
|
|
46
|
+
zIndex: pane.floating?.zIndex ?? 50,
|
|
47
|
+
x: Math.floor(visibleRect.x),
|
|
48
|
+
y: Math.floor(visibleRect.y),
|
|
49
|
+
width: Math.max(0, Math.ceil(visibleRect.width)),
|
|
50
|
+
height: Math.max(0, Math.ceil(visibleRect.height)),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readRenderableBounds(renderable: unknown): LayoutBounds | null {
|
|
56
|
+
if (!renderable || typeof renderable !== "object") return null;
|
|
57
|
+
const record = renderable as Record<string, unknown>;
|
|
58
|
+
const getRect = record.getBoundingClientRect;
|
|
59
|
+
if (typeof getRect === "function") {
|
|
60
|
+
const rect = getRect.call(renderable) as Partial<LayoutBounds> | null | undefined;
|
|
61
|
+
if (
|
|
62
|
+
rect
|
|
63
|
+
&& typeof rect.x === "number"
|
|
64
|
+
&& typeof rect.y === "number"
|
|
65
|
+
&& typeof rect.width === "number"
|
|
66
|
+
&& typeof rect.height === "number"
|
|
67
|
+
) {
|
|
68
|
+
return rect as LayoutBounds;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const absoluteBounds = record.absoluteBounds as Partial<LayoutBounds> | null | undefined;
|
|
73
|
+
if (
|
|
74
|
+
absoluteBounds
|
|
75
|
+
&& typeof absoluteBounds.x === "number"
|
|
76
|
+
&& typeof absoluteBounds.y === "number"
|
|
77
|
+
&& typeof absoluteBounds.width === "number"
|
|
78
|
+
&& typeof absoluteBounds.height === "number"
|
|
79
|
+
) {
|
|
80
|
+
return absoluteBounds as LayoutBounds;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const x = typeof record.screenX === "number"
|
|
84
|
+
? record.screenX
|
|
85
|
+
: typeof record.absoluteX === "number"
|
|
86
|
+
? record.absoluteX
|
|
87
|
+
: typeof record.x === "number"
|
|
88
|
+
? record.x
|
|
89
|
+
: null;
|
|
90
|
+
const y = typeof record.screenY === "number"
|
|
91
|
+
? record.screenY
|
|
92
|
+
: typeof record.absoluteY === "number"
|
|
93
|
+
? record.absoluteY
|
|
94
|
+
: typeof record.y === "number"
|
|
95
|
+
? record.y
|
|
96
|
+
: null;
|
|
97
|
+
const width = typeof record.width === "number" ? record.width : null;
|
|
98
|
+
const height = typeof record.height === "number" ? record.height : null;
|
|
99
|
+
if (x == null || y == null || width == null || height == null) return null;
|
|
100
|
+
return { x, y, width, height };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function readRenderablePaneId(renderable: unknown): string | null {
|
|
104
|
+
let current = renderable;
|
|
105
|
+
while (current && typeof current === "object") {
|
|
106
|
+
const record = current as Record<string, unknown>;
|
|
107
|
+
const paneId = record["data-gloom-pane-id"];
|
|
108
|
+
if (typeof paneId === "string") return paneId;
|
|
109
|
+
current = record.parent;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rectContainsCursor(rect: LayoutBounds, cursor: NativeCursorState): boolean {
|
|
115
|
+
return cursor.x >= rect.x
|
|
116
|
+
&& cursor.x < rect.x + rect.width
|
|
117
|
+
&& cursor.y >= rect.y
|
|
118
|
+
&& cursor.y < rect.y + rect.height;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toTerminalRect(shellBounds: LayoutBounds, rect: ShellCursorOcclusionRect): ShellCursorOcclusionRect {
|
|
122
|
+
return {
|
|
123
|
+
...rect,
|
|
124
|
+
x: Math.floor(shellBounds.x + rect.x + 1),
|
|
125
|
+
y: Math.floor(shellBounds.y + rect.y + 1),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveCursorOwnerZIndex(
|
|
130
|
+
cursorOwnerPaneId: string | null,
|
|
131
|
+
rects: readonly ShellCursorOcclusionRect[],
|
|
132
|
+
): number {
|
|
133
|
+
if (!cursorOwnerPaneId) return Number.NEGATIVE_INFINITY;
|
|
134
|
+
return rects.find((rect) => rect.paneId === cursorOwnerPaneId)?.zIndex ?? 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function shouldHideCursor({
|
|
138
|
+
cursor,
|
|
139
|
+
cursorOwnerPaneId,
|
|
140
|
+
terminalRects,
|
|
141
|
+
}: {
|
|
142
|
+
cursor: NativeCursorState;
|
|
143
|
+
cursorOwnerPaneId: string | null;
|
|
144
|
+
terminalRects: readonly ShellCursorOcclusionRect[];
|
|
145
|
+
}): boolean {
|
|
146
|
+
const ownerZIndex = resolveCursorOwnerZIndex(cursorOwnerPaneId, terminalRects);
|
|
147
|
+
return terminalRects.some((rect) => {
|
|
148
|
+
if (rect.paneId === cursorOwnerPaneId) return false;
|
|
149
|
+
if (rect.zIndex <= ownerZIndex) return false;
|
|
150
|
+
return rectContainsCursor(rect, cursor);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function useShellCursorOcclusionGuard({
|
|
155
|
+
occlusionRects,
|
|
156
|
+
shellRef,
|
|
157
|
+
}: {
|
|
158
|
+
occlusionRects: readonly ShellCursorOcclusionRect[];
|
|
159
|
+
shellRef: RefObject<unknown>;
|
|
160
|
+
}) {
|
|
161
|
+
const nativeRenderer = useNativeRenderer();
|
|
162
|
+
const stateRef = useRef({ occlusionRects, shellRef });
|
|
163
|
+
stateRef.current = { occlusionRects, shellRef };
|
|
164
|
+
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (
|
|
167
|
+
!nativeRenderer.addPostProcessFn
|
|
168
|
+
|| !nativeRenderer.removePostProcessFn
|
|
169
|
+
|| !nativeRenderer.getCursorState
|
|
170
|
+
|| !nativeRenderer.setCursorPosition
|
|
171
|
+
) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const guardCursor: NativePostProcessFn = () => {
|
|
176
|
+
const cursor = nativeRenderer.getCursorState?.();
|
|
177
|
+
if (!cursor?.visible) return;
|
|
178
|
+
const shellBounds = readRenderableBounds(stateRef.current.shellRef.current);
|
|
179
|
+
if (!shellBounds) return;
|
|
180
|
+
|
|
181
|
+
const terminalRects = stateRef.current.occlusionRects.map((rect) => toTerminalRect(shellBounds, rect));
|
|
182
|
+
if (terminalRects.length === 0) return;
|
|
183
|
+
|
|
184
|
+
const cursorOwnerPaneId = readRenderablePaneId(nativeRenderer.currentFocusedEditor);
|
|
185
|
+
if (!shouldHideCursor({ cursor, cursorOwnerPaneId, terminalRects })) return;
|
|
186
|
+
nativeRenderer.setCursorPosition?.(0, 0, false);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
nativeRenderer.addPostProcessFn(guardCursor);
|
|
190
|
+
nativeRenderer.requestRender();
|
|
191
|
+
return () => {
|
|
192
|
+
nativeRenderer.removePostProcessFn?.(guardCursor);
|
|
193
|
+
nativeRenderer.requestRender();
|
|
194
|
+
};
|
|
195
|
+
}, [nativeRenderer]);
|
|
196
|
+
}
|
|
@@ -86,9 +86,11 @@ export interface ShellDragRuntimeState {
|
|
|
86
86
|
|
|
87
87
|
export function useShellDragRuntimeState({
|
|
88
88
|
contentHeight,
|
|
89
|
+
throttleFloatingPreview = false,
|
|
89
90
|
width,
|
|
90
91
|
}: {
|
|
91
92
|
contentHeight: number;
|
|
93
|
+
throttleFloatingPreview?: boolean;
|
|
92
94
|
width: number;
|
|
93
95
|
}): ShellDragRuntimeState {
|
|
94
96
|
const dragRef = useRef<DragMode | null>(null);
|
|
@@ -104,15 +106,20 @@ export function useShellDragRuntimeState({
|
|
|
104
106
|
});
|
|
105
107
|
|
|
106
108
|
const updateDragFloatingRect = useCallback((next: { paneId: string; rect: FloatingRect } | null) => {
|
|
107
|
-
|
|
109
|
+
const constrainedNext = next
|
|
108
110
|
? { paneId: next.paneId, rect: constrainFloatingRectToBounds(next.rect, width, contentHeight) }
|
|
109
111
|
: null;
|
|
112
|
+
pendingDragFloatingRectRef.current = constrainedNext;
|
|
110
113
|
if (!next) {
|
|
111
114
|
setDragFloatingRect(null);
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
117
|
+
if (!throttleFloatingPreview) {
|
|
118
|
+
setDragFloatingRect(constrainedNext);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
114
121
|
flushDragFloatingRect();
|
|
115
|
-
}, [contentHeight, flushDragFloatingRect, width]);
|
|
122
|
+
}, [contentHeight, flushDragFloatingRect, throttleFloatingPreview, width]);
|
|
116
123
|
|
|
117
124
|
const updateDividerPreview = useCallback((next: DividerPreviewState | null) => {
|
|
118
125
|
dividerPreviewRef.current = next;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../../types/config";
|
|
14
14
|
import type { PluginRegistry } from "../../../plugins/registry";
|
|
15
15
|
import type { PaneProps } from "../../../types/plugin";
|
|
16
|
+
import { Textarea } from "../../../ui";
|
|
16
17
|
import { Header } from "../header";
|
|
17
18
|
import {
|
|
18
19
|
buildNativeWindowState,
|
|
@@ -222,7 +223,7 @@ describe("Header", () => {
|
|
|
222
223
|
);
|
|
223
224
|
|
|
224
225
|
await testSetup.renderOnce();
|
|
225
|
-
const frame = testSetup
|
|
226
|
+
const frame = testSetup!.captureCharFrame();
|
|
226
227
|
|
|
227
228
|
expect(frame).toContain("v0.3.0 available");
|
|
228
229
|
expect(frame).toContain("starting download");
|
|
@@ -242,7 +243,7 @@ describe("Header", () => {
|
|
|
242
243
|
);
|
|
243
244
|
|
|
244
245
|
await testSetup.renderOnce();
|
|
245
|
-
const frame = testSetup
|
|
246
|
+
const frame = testSetup!.captureCharFrame();
|
|
246
247
|
|
|
247
248
|
expect(frame).toContain("v0.3.0 available");
|
|
248
249
|
expect(frame).toContain("run npm install -g gloomberb@latest");
|
|
@@ -564,6 +565,95 @@ describe("Shell", () => {
|
|
|
564
565
|
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
565
566
|
});
|
|
566
567
|
|
|
568
|
+
test("updates the floating pane preview before mouse release", async () => {
|
|
569
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-live-floating-drag-test");
|
|
570
|
+
const floatingLayout = cloneLayout(config.layout);
|
|
571
|
+
floatingLayout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
|
|
572
|
+
floatingLayout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }];
|
|
573
|
+
await renderShellForWindowModeTest(
|
|
574
|
+
createShellStateWithLayout(config, floatingLayout, "ticker-detail:main"),
|
|
575
|
+
{ width: 80, height: 18 },
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
await act(async () => {
|
|
579
|
+
await testSetup!.mockMouse.pressDown(10, 3);
|
|
580
|
+
await testSetup!.renderOnce();
|
|
581
|
+
await testSetup!.mockMouse.moveTo(16, 6);
|
|
582
|
+
await testSetup!.renderOnce();
|
|
583
|
+
await testSetup!.renderOnce();
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const frame = testSetup!.captureCharFrame();
|
|
587
|
+
const rows = frame.split("\n");
|
|
588
|
+
expect(rows[2]?.indexOf("┌─:: Main Portfolio") ?? -1).toBeLessThan(0);
|
|
589
|
+
expect(rows[5]?.indexOf("┌─:: Main Portfolio")).toBeGreaterThanOrEqual(14);
|
|
590
|
+
|
|
591
|
+
await act(async () => {
|
|
592
|
+
await testSetup!.mockMouse.release(16, 6);
|
|
593
|
+
await testSetup!.renderOnce();
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test("keeps the focused textarea cursor visible when it is not covered", async () => {
|
|
598
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-cursor-visible-test");
|
|
599
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
600
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
601
|
+
const registry = createShellPluginRegistry({
|
|
602
|
+
portfolioListComponent: ({ focused, width, height }) => (
|
|
603
|
+
<Textarea
|
|
604
|
+
initialValue=""
|
|
605
|
+
focused={focused}
|
|
606
|
+
width={width}
|
|
607
|
+
height={Math.max(1, height)}
|
|
608
|
+
/>
|
|
609
|
+
),
|
|
610
|
+
tickerDetailComponent: () => <text>Chart Body</text>,
|
|
611
|
+
});
|
|
612
|
+
const layout = {
|
|
613
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
614
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
615
|
+
floating: [{ instanceId: "ticker-detail:main", x: 30, y: 2, width: 24, height: 8, zIndex: 75 }],
|
|
616
|
+
detached: [],
|
|
617
|
+
};
|
|
618
|
+
await renderShellForWindowModeTest(
|
|
619
|
+
createShellStateWithLayout(config, layout, "portfolio-list:main"),
|
|
620
|
+
{ registry, width: 80, height: 18 },
|
|
621
|
+
);
|
|
622
|
+
await testSetup!.renderOnce();
|
|
623
|
+
|
|
624
|
+
expect(testSetup!.renderer.getCursorState().visible).toBe(true);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("hides the textarea cursor when a higher floating pane covers it", async () => {
|
|
628
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-cursor-occlusion-test");
|
|
629
|
+
const mainPane = requireLayoutInstance(config, "portfolio-list:main");
|
|
630
|
+
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
631
|
+
const registry = createShellPluginRegistry({
|
|
632
|
+
portfolioListComponent: ({ focused, width, height }) => (
|
|
633
|
+
<Textarea
|
|
634
|
+
initialValue=""
|
|
635
|
+
focused={focused}
|
|
636
|
+
width={width}
|
|
637
|
+
height={Math.max(1, height)}
|
|
638
|
+
/>
|
|
639
|
+
),
|
|
640
|
+
tickerDetailComponent: () => <text>Chart Body</text>,
|
|
641
|
+
});
|
|
642
|
+
const layout = {
|
|
643
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
644
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
645
|
+
floating: [{ instanceId: "ticker-detail:main", x: 0, y: 0, width: 40, height: 8, zIndex: 75 }],
|
|
646
|
+
detached: [],
|
|
647
|
+
};
|
|
648
|
+
await renderShellForWindowModeTest(
|
|
649
|
+
createShellStateWithLayout(config, layout, "portfolio-list:main"),
|
|
650
|
+
{ registry, width: 80, height: 18 },
|
|
651
|
+
);
|
|
652
|
+
await testSetup!.renderOnce();
|
|
653
|
+
|
|
654
|
+
expect(testSetup!.renderer.getCursorState().visible).toBe(false);
|
|
655
|
+
});
|
|
656
|
+
|
|
567
657
|
test("keeps focused pane local detail state when maximizing a floating pane", async () => {
|
|
568
658
|
const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-local-state-test");
|
|
569
659
|
const detailPane = requireLayoutInstance(config, "ticker-detail:main");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost } from "../../../ui";
|
|
1
|
+
import { AsciiText, Box, Text, compactContextMenuItems, useContextMenu, useUiHost, type BoxRenderable } from "../../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useRendererHost, useUiCapabilities } from "../../../ui";
|
|
4
4
|
import { useViewport } from "../../../react/input";
|
|
@@ -61,6 +61,10 @@ import {
|
|
|
61
61
|
import { useShellPaneActions } from "./pane/actions";
|
|
62
62
|
import { resolvePaneFocusSourceLayout } from "./fullscreen";
|
|
63
63
|
import { useTransientLayout } from "../transient-layout";
|
|
64
|
+
import {
|
|
65
|
+
resolveShellCursorOcclusionRects,
|
|
66
|
+
useShellCursorOcclusionGuard,
|
|
67
|
+
} from "./cursor-occlusion";
|
|
64
68
|
|
|
65
69
|
export { resolveAppHeaderHeightCells } from "./chrome";
|
|
66
70
|
export { buildNativeWindowState } from "./native/window-state";
|
|
@@ -102,6 +106,7 @@ export function Shell({
|
|
|
102
106
|
const { nativePaneChrome = false, nativeContextMenu, precisePointer, titleBarOverlay, cellHeightPx } = useUiCapabilities();
|
|
103
107
|
const { showContextMenu } = useContextMenu();
|
|
104
108
|
const { width, height } = useViewport();
|
|
109
|
+
const shellRef = useRef<BoxRenderable | null>(null);
|
|
105
110
|
|
|
106
111
|
const appHeaderHeight = resolveAppHeaderHeightCells({ titleBarOverlay, cellHeightPx });
|
|
107
112
|
const contentHeight = Math.max(1, height - appHeaderHeight - (statusBarVisible ? 1 : 0));
|
|
@@ -124,7 +129,11 @@ export function Shell({
|
|
|
124
129
|
}, []);
|
|
125
130
|
const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
|
|
126
131
|
|
|
127
|
-
const dragRuntime = useShellDragRuntimeState({
|
|
132
|
+
const dragRuntime = useShellDragRuntimeState({
|
|
133
|
+
contentHeight,
|
|
134
|
+
throttleFloatingPreview: nativePaneChrome,
|
|
135
|
+
width,
|
|
136
|
+
});
|
|
128
137
|
const {
|
|
129
138
|
cancelActiveDrag,
|
|
130
139
|
dividerPreview,
|
|
@@ -209,6 +218,27 @@ export function Shell({
|
|
|
209
218
|
pluginRegistry,
|
|
210
219
|
width,
|
|
211
220
|
});
|
|
221
|
+
const cursorOcclusionRects = useMemo(() => resolveShellCursorOcclusionRects({
|
|
222
|
+
contentHeight,
|
|
223
|
+
dragFloatingRect,
|
|
224
|
+
nativePaneChrome,
|
|
225
|
+
overlayOpen,
|
|
226
|
+
transientFocusActive,
|
|
227
|
+
visibleFloatingPanes,
|
|
228
|
+
width,
|
|
229
|
+
}), [
|
|
230
|
+
contentHeight,
|
|
231
|
+
dragFloatingRect,
|
|
232
|
+
nativePaneChrome,
|
|
233
|
+
overlayOpen,
|
|
234
|
+
transientFocusActive,
|
|
235
|
+
visibleFloatingPanes,
|
|
236
|
+
width,
|
|
237
|
+
]);
|
|
238
|
+
useShellCursorOcclusionGuard({
|
|
239
|
+
occlusionRects: cursorOcclusionRects,
|
|
240
|
+
shellRef,
|
|
241
|
+
});
|
|
212
242
|
|
|
213
243
|
const {
|
|
214
244
|
closeAllFloatingPanes,
|
|
@@ -485,8 +515,13 @@ export function Shell({
|
|
|
485
515
|
|
|
486
516
|
return (
|
|
487
517
|
<Box
|
|
518
|
+
ref={shellRef}
|
|
488
519
|
flexDirection="row"
|
|
489
520
|
flexGrow={1}
|
|
521
|
+
flexShrink={1}
|
|
522
|
+
flexBasis={0}
|
|
523
|
+
minWidth={0}
|
|
524
|
+
minHeight={0}
|
|
490
525
|
height={nativePaneChrome ? undefined : contentHeight}
|
|
491
526
|
position={nativePaneChrome ? "relative" : undefined}
|
|
492
527
|
overflow="hidden"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getDockedPaneIds } from "../../../plugins/pane-manager";
|
|
3
|
+
import { createPaneInstance, type LayoutConfig } from "../../../types/config";
|
|
4
|
+
import { resolveShellVisibleLayout } from "./visible-layout";
|
|
5
|
+
|
|
6
|
+
describe("shell visible layout", () => {
|
|
7
|
+
test("removes unregistered pane types before resolving dock geometry", () => {
|
|
8
|
+
const layout: LayoutConfig = {
|
|
9
|
+
dockRoot: {
|
|
10
|
+
kind: "split",
|
|
11
|
+
axis: "horizontal",
|
|
12
|
+
ratio: 0.5,
|
|
13
|
+
first: { kind: "pane", instanceId: "missing:main" },
|
|
14
|
+
second: { kind: "pane", instanceId: "chat:main" },
|
|
15
|
+
},
|
|
16
|
+
instances: [
|
|
17
|
+
createPaneInstance("missing-plugin", { instanceId: "missing:main" }),
|
|
18
|
+
createPaneInstance("chat", { instanceId: "chat:main" }),
|
|
19
|
+
],
|
|
20
|
+
floating: [],
|
|
21
|
+
detached: [],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const visibleLayout = resolveShellVisibleLayout(
|
|
25
|
+
layout,
|
|
26
|
+
new Set(),
|
|
27
|
+
new Map([["chat", true]]),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(visibleLayout.instances.map((instance) => instance.instanceId)).toEqual(["chat:main"]);
|
|
31
|
+
expect(getDockedPaneIds(visibleLayout)).toEqual(["chat:main"]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
type ResolvedPane,
|
|
7
7
|
} from "../../../plugins/pane-manager";
|
|
8
8
|
import type { PluginRegistry } from "../../../plugins/registry";
|
|
9
|
-
import {
|
|
9
|
+
import type { LayoutConfig } from "../../../types/config";
|
|
10
10
|
import { constrainFloatingRectToBounds } from "./drag";
|
|
11
|
+
import { resolveShellVisibleLayout } from "./visible-layout";
|
|
11
12
|
|
|
12
13
|
interface ShellVisibleLayoutOptions {
|
|
13
14
|
disabledPlugins: readonly string[];
|
|
@@ -34,15 +35,9 @@ export function useShellVisibleLayout({
|
|
|
34
35
|
return paneIds;
|
|
35
36
|
}, [disabledPlugins, pluginRegistry]);
|
|
36
37
|
|
|
37
|
-
const hiddenInstanceIds = useMemo(() => (
|
|
38
|
-
layout.instances
|
|
39
|
-
.filter((instance) => disabledPaneIds.has(instance.paneId))
|
|
40
|
-
.map((instance) => instance.instanceId)
|
|
41
|
-
), [disabledPaneIds, layout.instances]);
|
|
42
|
-
|
|
43
38
|
const visibleLayout = useMemo(
|
|
44
|
-
() => (
|
|
45
|
-
[
|
|
39
|
+
() => resolveShellVisibleLayout(layout, disabledPaneIds, pluginRegistry.panes),
|
|
40
|
+
[disabledPaneIds, layout, pluginRegistry.panes],
|
|
46
41
|
);
|
|
47
42
|
|
|
48
43
|
return { disabledPaneIds, visibleLayout };
|
|
@@ -111,11 +111,14 @@ export function ShellPaneLayers({
|
|
|
111
111
|
>
|
|
112
112
|
<PaneFooterProvider>
|
|
113
113
|
{(footer) => {
|
|
114
|
-
const
|
|
114
|
+
const showFooter = hasPaneFooterContent(footer);
|
|
115
|
+
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
116
|
+
const renderFooter = reserveFooter || showFooter;
|
|
115
117
|
const bodyFrame = resolvePaneBodyFrame({
|
|
116
118
|
width: rect.width,
|
|
117
119
|
height: rect.height,
|
|
118
120
|
nativePaneChrome,
|
|
121
|
+
footerVisible: renderFooter,
|
|
119
122
|
reserveFooter,
|
|
120
123
|
});
|
|
121
124
|
return (
|
|
@@ -165,11 +168,14 @@ export function ShellPaneLayers({
|
|
|
165
168
|
return (
|
|
166
169
|
<PaneFooterProvider key={`float:${pane.instance.instanceId}`}>
|
|
167
170
|
{(footer) => {
|
|
168
|
-
const
|
|
171
|
+
const showFooter = hasPaneFooterContent(footer);
|
|
172
|
+
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
173
|
+
const renderFooter = reserveFooter || showFooter;
|
|
169
174
|
const bodyFrame = resolvePaneBodyFrame({
|
|
170
175
|
width: preview.width,
|
|
171
176
|
height: preview.height,
|
|
172
177
|
nativePaneChrome,
|
|
178
|
+
footerVisible: renderFooter,
|
|
173
179
|
reserveFooter,
|
|
174
180
|
});
|
|
175
181
|
return (
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
removeUnavailablePaneTypes,
|
|
3
|
+
type PaneTypeAvailability,
|
|
4
|
+
} from "../../../plugins/pane-manager";
|
|
5
|
+
import type { LayoutConfig } from "../../../types/config";
|
|
6
|
+
|
|
7
|
+
export function resolveShellVisibleLayout(
|
|
8
|
+
layout: LayoutConfig,
|
|
9
|
+
disabledPaneIds: ReadonlySet<string>,
|
|
10
|
+
registeredPaneIds: PaneTypeAvailability,
|
|
11
|
+
): LayoutConfig {
|
|
12
|
+
return removeUnavailablePaneTypes(
|
|
13
|
+
layout,
|
|
14
|
+
registeredPaneIds,
|
|
15
|
+
{ disabledPaneIds },
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
3
3
|
import { AppContext, createInitialState } from "../../state/app/context";
|
|
4
|
-
import { cloneLayout, createDefaultConfig, type LayoutConfig } from "../../types/config";
|
|
4
|
+
import { cloneLayout, createDefaultConfig, TICKER_RESEARCH_PANE_ID, type LayoutConfig } from "../../types/config";
|
|
5
5
|
import type { AppNotificationRequest } from "../../types/plugin";
|
|
6
6
|
import { StatusBar } from "./status-bar";
|
|
7
7
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
@@ -174,6 +174,10 @@ describe("StatusBar", () => {
|
|
|
174
174
|
const notifications: AppNotificationRequest[] = [];
|
|
175
175
|
|
|
176
176
|
setSharedRegistryForTests({
|
|
177
|
+
panes: new Map([
|
|
178
|
+
["portfolio-list", {}],
|
|
179
|
+
[TICKER_RESEARCH_PANE_ID, {}],
|
|
180
|
+
]),
|
|
177
181
|
getLayoutFn: () => state.config.layout,
|
|
178
182
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
179
183
|
updateLayoutFn: (layout: LayoutConfig) => { updatedLayout = layout; },
|
|
@@ -233,6 +237,7 @@ describe("StatusBar", () => {
|
|
|
233
237
|
globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
|
|
234
238
|
|
|
235
239
|
setSharedRegistryForTests({
|
|
240
|
+
panes: new Map(),
|
|
236
241
|
getLayoutFn: () => state.config.layout,
|
|
237
242
|
getTermSizeFn: () => ({ width: 120, height: 40 }),
|
|
238
243
|
updateLayoutFn: () => {},
|
|
@@ -114,7 +114,11 @@ export function StatusBar() {
|
|
|
114
114
|
event?.stopPropagation?.();
|
|
115
115
|
if (!registry) return;
|
|
116
116
|
const { width, height } = registry.getTermSizeFn();
|
|
117
|
-
registry.updateLayoutFn(gridlockAllPanes(
|
|
117
|
+
registry.updateLayoutFn(gridlockAllPanes(
|
|
118
|
+
registry.getLayoutFn(),
|
|
119
|
+
{ x: 0, y: 0, width, height },
|
|
120
|
+
registry.panes,
|
|
121
|
+
));
|
|
118
122
|
notifyGridlockComplete(registry.notify.bind(registry), () => {
|
|
119
123
|
dispatch({ type: "UNDO_LAYOUT" });
|
|
120
124
|
});
|