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
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
|
-
import { Box, ChartSurface, Text } from "../../../../ui";
|
|
1
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, ChartSurface, Text, type BoxRenderable, type ChartSurfaceProps, useUiCapabilities } from "../../../../ui";
|
|
3
|
+
import { colors as themeColors } from "../../../../theme/colors";
|
|
3
4
|
import { useThemeColors } from "../../../../theme/theme-context";
|
|
5
|
+
import { PriceAxisLabels } from "../../price-axis-labels";
|
|
4
6
|
import type { ProjectedChartPoint } from "../../core/data";
|
|
5
7
|
import {
|
|
6
8
|
buildChartScene,
|
|
7
9
|
computeGridLines,
|
|
8
|
-
formatAxisCell,
|
|
9
10
|
renderChart,
|
|
10
11
|
type RenderChartOptions,
|
|
11
12
|
} from "../../core/renderer";
|
|
12
13
|
import { renderNativeChartBase, type NativeChartBitmap } from "../../native/chart-rasterizer";
|
|
13
14
|
import { useStaticChartBitmapSize } from "./bitmap";
|
|
15
|
+
import {
|
|
16
|
+
StaticXAxisLabels,
|
|
17
|
+
StaticXMarkerLabels,
|
|
18
|
+
StaticXMarkerOverlay,
|
|
19
|
+
clampRatio,
|
|
20
|
+
type StaticChartXMarker,
|
|
21
|
+
} from "./axis-overlays";
|
|
22
|
+
|
|
23
|
+
export type { StaticChartXMarker } from "./axis-overlays";
|
|
14
24
|
|
|
15
25
|
export interface StaticChartSurfaceProps extends Omit<
|
|
16
26
|
RenderChartOptions,
|
|
@@ -23,11 +33,52 @@ export interface StaticChartSurfaceProps extends Omit<
|
|
|
23
33
|
volumeHeight?: number;
|
|
24
34
|
showTimeAxis?: boolean;
|
|
25
35
|
timeAxisColor?: string;
|
|
36
|
+
xAxisLabels?: string[];
|
|
37
|
+
xAxisColor?: string;
|
|
38
|
+
formatXAxisCursorValue?: (xRatio: number) => string;
|
|
39
|
+
xMarkers?: StaticChartXMarker[];
|
|
26
40
|
yAxisLabel?: string;
|
|
27
41
|
yAxisColor?: string;
|
|
28
42
|
formatYAxisValue?: (value: number) => string;
|
|
29
43
|
}
|
|
30
44
|
|
|
45
|
+
interface ChartMouseEventLike {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
preciseX?: number;
|
|
49
|
+
preciseY?: number;
|
|
50
|
+
preventDefault?: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function localPlotPointer(
|
|
54
|
+
event: ChartMouseEventLike,
|
|
55
|
+
renderable: BoxRenderable | null,
|
|
56
|
+
): { x: number; y: number } | null {
|
|
57
|
+
if (!renderable) return null;
|
|
58
|
+
const originX = typeof renderable.absoluteX === "number"
|
|
59
|
+
? renderable.absoluteX
|
|
60
|
+
: typeof renderable.x === "number"
|
|
61
|
+
? renderable.x
|
|
62
|
+
: 0;
|
|
63
|
+
const originY = typeof renderable.absoluteY === "number"
|
|
64
|
+
? renderable.absoluteY
|
|
65
|
+
: typeof renderable.y === "number"
|
|
66
|
+
? renderable.y
|
|
67
|
+
: 0;
|
|
68
|
+
const rawX = typeof event.preciseX === "number" ? event.preciseX : event.x;
|
|
69
|
+
const rawY = typeof event.preciseY === "number" ? event.preciseY : event.y;
|
|
70
|
+
const x = rawX - originX;
|
|
71
|
+
const y = rawY - originY;
|
|
72
|
+
const width = typeof renderable.width === "number" ? renderable.width : 0;
|
|
73
|
+
const height = typeof renderable.height === "number" ? renderable.height : 0;
|
|
74
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || width <= 0 || height <= 0) return null;
|
|
75
|
+
if (x < 0 || y < 0 || x >= width || y >= height) return null;
|
|
76
|
+
return {
|
|
77
|
+
x: Math.max(0, Math.min(x, Math.max(width - 1, 0))),
|
|
78
|
+
y: Math.max(0, Math.min(y, Math.max(height - 1, 0))),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
31
82
|
export function StaticChartSurface({
|
|
32
83
|
points,
|
|
33
84
|
width,
|
|
@@ -43,16 +94,24 @@ export function StaticChartSurface({
|
|
|
43
94
|
volumeHeight = 0,
|
|
44
95
|
showTimeAxis = false,
|
|
45
96
|
timeAxisColor,
|
|
97
|
+
xAxisLabels,
|
|
98
|
+
xAxisColor,
|
|
99
|
+
formatXAxisCursorValue,
|
|
100
|
+
xMarkers,
|
|
46
101
|
yAxisLabel,
|
|
47
102
|
yAxisColor,
|
|
48
103
|
formatYAxisValue,
|
|
49
104
|
}: StaticChartSurfaceProps) {
|
|
50
105
|
useThemeColors();
|
|
51
|
-
const
|
|
106
|
+
const { cellWidthPx = 8, cellHeightPx = 18 } = useUiCapabilities();
|
|
107
|
+
const plotRef = useRef<BoxRenderable | null>(null);
|
|
108
|
+
const [cursor, setCursor] = useState<{ x: number; y: number } | null>(null);
|
|
109
|
+
const xAxisRows = showTimeAxis || (xAxisLabels?.length ?? 0) > 0 ? 1 : 0;
|
|
110
|
+
const xMarkerRows = xMarkers?.some((marker) => marker.label) ? 1 : 0;
|
|
52
111
|
const labelRows = yAxisLabel ? 1 : 0;
|
|
53
112
|
const totalWidth = Math.max(1, Math.floor(width));
|
|
54
113
|
const totalHeight = Math.max(1, Math.floor(height));
|
|
55
|
-
const plotHeight = Math.max(1, totalHeight -
|
|
114
|
+
const plotHeight = Math.max(1, totalHeight - xAxisRows - xMarkerRows - labelRows);
|
|
56
115
|
const axisSourceOptions = useMemo<RenderChartOptions>(() => ({
|
|
57
116
|
width: totalWidth,
|
|
58
117
|
height: plotHeight,
|
|
@@ -104,8 +163,8 @@ export function StaticChartSurface({
|
|
|
104
163
|
height: plotHeight,
|
|
105
164
|
showVolume,
|
|
106
165
|
volumeHeight,
|
|
107
|
-
cursorX: null,
|
|
108
|
-
cursorY: null,
|
|
166
|
+
cursorX: cursor?.x ?? null,
|
|
167
|
+
cursorY: cursor?.y ?? null,
|
|
109
168
|
mode,
|
|
110
169
|
axisMode,
|
|
111
170
|
currency,
|
|
@@ -122,6 +181,7 @@ export function StaticChartSurface({
|
|
|
122
181
|
mode,
|
|
123
182
|
plotHeight,
|
|
124
183
|
plotWidth,
|
|
184
|
+
cursor,
|
|
125
185
|
showVolume,
|
|
126
186
|
timeAxisDates,
|
|
127
187
|
volumeHeight,
|
|
@@ -133,6 +193,14 @@ export function StaticChartSurface({
|
|
|
133
193
|
const effectiveAxisLabelsByRow = useMemo(() => {
|
|
134
194
|
return new Map(axisLabels.map((entry) => [entry.row, entry.label] as const));
|
|
135
195
|
}, [axisLabels]);
|
|
196
|
+
const cursorAxisLabel = useMemo(() => {
|
|
197
|
+
if (!formatYAxisValue || textResult.crosshairPrice === null) return null;
|
|
198
|
+
return formatYAxisValue(textResult.crosshairPrice);
|
|
199
|
+
}, [formatYAxisValue, textResult.crosshairPrice]);
|
|
200
|
+
const cursorXAxisLabel = useMemo(() => {
|
|
201
|
+
if (!formatXAxisCursorValue || !cursor) return null;
|
|
202
|
+
return formatXAxisCursorValue(clampRatio(cursor.x / Math.max(plotWidth - 1, 1)));
|
|
203
|
+
}, [cursor, formatXAxisCursorValue, plotWidth]);
|
|
136
204
|
const effectiveAxisWidth = axisWidth;
|
|
137
205
|
const effectiveAxisGap = effectiveAxisWidth > 0 ? 1 : 0;
|
|
138
206
|
const bitmap = useMemo<NativeChartBitmap | null>(() => {
|
|
@@ -141,40 +209,115 @@ export function StaticChartSurface({
|
|
|
141
209
|
if (!scene) return null;
|
|
142
210
|
return renderNativeChartBase(scene, bitmapSize.pixelWidth, bitmapSize.pixelHeight);
|
|
143
211
|
}, [bitmapSize, points, renderOptions]);
|
|
212
|
+
const canvasCrosshair = useMemo<ChartSurfaceProps["crosshair"]>(() => {
|
|
213
|
+
if (!bitmap || !cursor) return null;
|
|
214
|
+
return {
|
|
215
|
+
pixelX: Math.round(clampRatio(cursor.x / Math.max(plotWidth - 1, 1)) * Math.max(bitmap.width - 1, 0)),
|
|
216
|
+
pixelY: Math.round(clampRatio(cursor.y / Math.max(plotHeight - 1, 1)) * Math.max(bitmap.height - 1, 0)),
|
|
217
|
+
color: colors.crosshairColor,
|
|
218
|
+
};
|
|
219
|
+
}, [bitmap, colors.crosshairColor, cursor, plotHeight, plotWidth]);
|
|
220
|
+
const handleCursorEvent = useCallback((event: ChartMouseEventLike) => {
|
|
221
|
+
const pointer = localPlotPointer(event, plotRef.current);
|
|
222
|
+
if (!pointer) return;
|
|
223
|
+
event.preventDefault?.();
|
|
224
|
+
setCursor((current) => (
|
|
225
|
+
current && current.x === pointer.x && current.y === pointer.y ? current : pointer
|
|
226
|
+
));
|
|
227
|
+
}, []);
|
|
228
|
+
const clearCursor = useCallback(() => {
|
|
229
|
+
setCursor(null);
|
|
230
|
+
}, []);
|
|
144
231
|
|
|
145
232
|
return (
|
|
146
|
-
<Box flexDirection="column" width={totalWidth} height={plotHeight +
|
|
233
|
+
<Box flexDirection="column" width={totalWidth} height={plotHeight + xAxisRows + xMarkerRows + labelRows}>
|
|
147
234
|
{yAxisLabel ? (
|
|
148
235
|
<Box height={1}>
|
|
149
236
|
<Text fg={yAxisColor}>{yAxisLabel}</Text>
|
|
150
237
|
</Box>
|
|
151
238
|
) : null}
|
|
152
239
|
<Box flexDirection="row" height={plotHeight}>
|
|
153
|
-
<
|
|
240
|
+
<Box
|
|
241
|
+
ref={plotRef}
|
|
242
|
+
position="relative"
|
|
154
243
|
width={plotWidth}
|
|
155
244
|
height={plotHeight}
|
|
156
|
-
|
|
157
|
-
|
|
245
|
+
onMouseMove={handleCursorEvent}
|
|
246
|
+
onMouseDown={handleCursorEvent}
|
|
247
|
+
onMouseOut={clearCursor}
|
|
248
|
+
data-gloom-role="static-chart-plot"
|
|
158
249
|
>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
250
|
+
<ChartSurface
|
|
251
|
+
width={plotWidth}
|
|
252
|
+
height={plotHeight}
|
|
253
|
+
flexDirection="column"
|
|
254
|
+
bitmaps={bitmap ? [bitmap] : null}
|
|
255
|
+
crosshair={canvasCrosshair}
|
|
256
|
+
>
|
|
257
|
+
{textResult.lines.map((line, index) => (
|
|
258
|
+
<Text key={index} content={line} />
|
|
259
|
+
))}
|
|
260
|
+
</ChartSurface>
|
|
261
|
+
{xMarkers ? (
|
|
262
|
+
<StaticXMarkerOverlay
|
|
263
|
+
markers={xMarkers}
|
|
264
|
+
width={plotWidth}
|
|
265
|
+
height={plotHeight}
|
|
266
|
+
fallbackColor={xAxisColor ?? timeAxisColor}
|
|
267
|
+
/>
|
|
268
|
+
) : null}
|
|
269
|
+
</Box>
|
|
163
270
|
{effectiveAxisWidth > 0 ? (
|
|
164
271
|
<>
|
|
165
272
|
<Box width={effectiveAxisGap} />
|
|
166
|
-
<
|
|
167
|
-
{
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
273
|
+
<PriceAxisLabels
|
|
274
|
+
axisLabels={effectiveAxisLabelsByRow}
|
|
275
|
+
axisWidth={effectiveAxisWidth}
|
|
276
|
+
axisSectionWidth={effectiveAxisWidth}
|
|
277
|
+
height={plotHeight}
|
|
278
|
+
cursorRow={textResult.cursorRow}
|
|
279
|
+
cursorPixelY={cursor ? cursor.y * cellHeightPx : null}
|
|
280
|
+
cursorLabel={cursorAxisLabel}
|
|
281
|
+
cursorColor={colors.crosshairColor}
|
|
282
|
+
axisColor={yAxisColor}
|
|
283
|
+
/>
|
|
173
284
|
</>
|
|
174
285
|
) : null}
|
|
175
286
|
</Box>
|
|
176
|
-
{showTimeAxis ? (
|
|
177
|
-
<
|
|
287
|
+
{showTimeAxis || (xAxisLabels?.length ?? 0) > 0 ? (
|
|
288
|
+
<Box height={1} flexDirection="row">
|
|
289
|
+
{xAxisLabels ? (
|
|
290
|
+
<StaticXAxisLabels
|
|
291
|
+
labels={xAxisLabels}
|
|
292
|
+
width={plotWidth}
|
|
293
|
+
color={xAxisColor ?? timeAxisColor}
|
|
294
|
+
cursorColumn={textResult.cursorColumn}
|
|
295
|
+
cursorPixelX={cursor ? cursor.x * cellWidthPx : null}
|
|
296
|
+
cursorLabel={cursorXAxisLabel}
|
|
297
|
+
cursorColor={colors.crosshairColor}
|
|
298
|
+
cursorBackgroundColor={colors.bgColor ?? themeColors.bg}
|
|
299
|
+
/>
|
|
300
|
+
) : (
|
|
301
|
+
<Text fg={timeAxisColor}>{textResult.timeLabels}</Text>
|
|
302
|
+
)}
|
|
303
|
+
{effectiveAxisWidth > 0 ? (
|
|
304
|
+
<>
|
|
305
|
+
<Box width={effectiveAxisGap} />
|
|
306
|
+
<Box width={effectiveAxisWidth} />
|
|
307
|
+
</>
|
|
308
|
+
) : null}
|
|
309
|
+
</Box>
|
|
310
|
+
) : null}
|
|
311
|
+
{xMarkers && xMarkerRows > 0 ? (
|
|
312
|
+
<Box height={1} flexDirection="row">
|
|
313
|
+
<StaticXMarkerLabels markers={xMarkers} width={plotWidth} fallbackColor={xAxisColor ?? timeAxisColor} />
|
|
314
|
+
{effectiveAxisWidth > 0 ? (
|
|
315
|
+
<>
|
|
316
|
+
<Box width={effectiveAxisGap} />
|
|
317
|
+
<Box width={effectiveAxisWidth} />
|
|
318
|
+
</>
|
|
319
|
+
) : null}
|
|
320
|
+
</Box>
|
|
178
321
|
) : null}
|
|
179
322
|
</Box>
|
|
180
323
|
);
|
|
@@ -76,6 +76,31 @@ describe("stock chart pointer helpers", () => {
|
|
|
76
76
|
});
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
test("uses absolute renderable coordinates for desktop mouse hit testing", () => {
|
|
80
|
+
const pointer = getLocalPlotPointer({
|
|
81
|
+
x: 49,
|
|
82
|
+
y: 10,
|
|
83
|
+
preciseX: 49.5,
|
|
84
|
+
preciseY: 10.25,
|
|
85
|
+
modifiers: { shift: false, alt: false, ctrl: false },
|
|
86
|
+
}, {
|
|
87
|
+
x: 0,
|
|
88
|
+
y: 0,
|
|
89
|
+
absoluteX: 10,
|
|
90
|
+
absoluteY: 5,
|
|
91
|
+
width: 40,
|
|
92
|
+
height: 12,
|
|
93
|
+
}, renderer);
|
|
94
|
+
|
|
95
|
+
expect(pointer).toMatchObject({
|
|
96
|
+
hasPixelPrecision: false,
|
|
97
|
+
pixelX: null,
|
|
98
|
+
pixelY: null,
|
|
99
|
+
});
|
|
100
|
+
expect(pointer!.cellX).toBeCloseTo(39.5, 2);
|
|
101
|
+
expect(pointer!.cellY).toBeCloseTo(5.25, 2);
|
|
102
|
+
});
|
|
103
|
+
|
|
79
104
|
test("can derive local pixels from cell coordinates for non-pixel fallbacks", () => {
|
|
80
105
|
const pixels = projectCellCursorToLocalPixels(14, 5, renderable, renderer);
|
|
81
106
|
|
|
@@ -87,7 +87,11 @@ export function buildCurrentLayoutItems({
|
|
|
87
87
|
kind: "action",
|
|
88
88
|
action: () => {
|
|
89
89
|
const { width, height } = pluginRegistry.getTermSizeFn();
|
|
90
|
-
persistLayoutChange(gridlockAllPanes(
|
|
90
|
+
persistLayoutChange(gridlockAllPanes(
|
|
91
|
+
currentLayout,
|
|
92
|
+
{ x: 0, y: 0, width, height },
|
|
93
|
+
pluginRegistry.panes,
|
|
94
|
+
));
|
|
91
95
|
notifyGridlockRevert();
|
|
92
96
|
closeAll({ revertThemePreview: false });
|
|
93
97
|
},
|
|
@@ -10,7 +10,7 @@ import { useThemeColors } from "../../theme/theme-context";
|
|
|
10
10
|
import { hasPaneFooterContent, PaneFooterBar, PaneFooterProvider } from "./pane/footer";
|
|
11
11
|
import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
|
|
12
12
|
import { PaneContent } from "./pane/content";
|
|
13
|
-
import { resolvePaneBodyFrame } from "./pane/sizing";
|
|
13
|
+
import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./pane/sizing";
|
|
14
14
|
import { getPaneDisplayTitle } from "./pane/title";
|
|
15
15
|
import { TITLEBAR_OVERLAY_HEIGHT_PX, getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
16
16
|
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
@@ -130,6 +130,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
130
130
|
<PaneFooterProvider>
|
|
131
131
|
{(footer) => {
|
|
132
132
|
const showFooter = hasPaneFooterContent(footer);
|
|
133
|
+
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
134
|
+
const renderFooter = reserveFooter || showFooter;
|
|
133
135
|
const headerHeightRows = titleBarOverlay ? TITLEBAR_OVERLAY_HEIGHT_PX / cellHeightPx : 1;
|
|
134
136
|
const background = floatingPaneBg(focused);
|
|
135
137
|
const titleBackground = floatingPaneTitleBg(focused);
|
|
@@ -137,7 +139,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
137
139
|
width,
|
|
138
140
|
height,
|
|
139
141
|
nativePaneChrome,
|
|
140
|
-
|
|
142
|
+
footerVisible: renderFooter,
|
|
143
|
+
reserveFooter,
|
|
141
144
|
headerRows: headerHeightRows,
|
|
142
145
|
});
|
|
143
146
|
const bodyWidth = bodyFrame.width ?? 1;
|
|
@@ -208,7 +211,7 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
208
211
|
height={bodyHeight}
|
|
209
212
|
/>
|
|
210
213
|
</PaneBodyFrame>
|
|
211
|
-
{
|
|
214
|
+
{renderFooter && <PaneFooterBar footer={footer} focused={focused} width={width} />}
|
|
212
215
|
</Box>
|
|
213
216
|
);
|
|
214
217
|
}}
|
|
@@ -80,7 +80,8 @@ export function FloatingPaneWrapper({
|
|
|
80
80
|
const bg = floatingPaneBg(focused);
|
|
81
81
|
const showFooter = hasPaneFooterContent(footer);
|
|
82
82
|
const reserveFooter = shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
83
|
-
const
|
|
83
|
+
const renderFooter = reserveFooter || showFooter;
|
|
84
|
+
const bodyFrame = resolvePaneBodyFrame({ height, nativePaneChrome, footerVisible: renderFooter, reserveFooter });
|
|
84
85
|
|
|
85
86
|
return (
|
|
86
87
|
<Box
|
|
@@ -124,7 +125,7 @@ export function FloatingPaneWrapper({
|
|
|
124
125
|
{children}
|
|
125
126
|
</PaneBodyFrame>
|
|
126
127
|
|
|
127
|
-
{
|
|
128
|
+
{renderFooter && (
|
|
128
129
|
<PaneFooterBar
|
|
129
130
|
footer={footer}
|
|
130
131
|
focused={focused}
|
|
@@ -3,6 +3,7 @@ import { PaneInstanceProvider } from "../../../state/app/context";
|
|
|
3
3
|
import { PaneKeyboardScrollController } from "../../../state/pane-scroll-registry";
|
|
4
4
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
5
5
|
import type { PaneDef } from "../../../types/plugin";
|
|
6
|
+
import { Box } from "../../../ui";
|
|
6
7
|
|
|
7
8
|
interface PaneContentProps {
|
|
8
9
|
component: PaneDef["component"];
|
|
@@ -31,14 +32,25 @@ export const PaneContent = memo(function PaneContent({
|
|
|
31
32
|
return (
|
|
32
33
|
<PaneInstanceProvider paneId={paneId}>
|
|
33
34
|
<PaneKeyboardScrollController paneId={paneId} focused={focused} />
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
<Box
|
|
36
|
+
flexDirection="column"
|
|
37
|
+
flexGrow={1}
|
|
38
|
+
flexShrink={1}
|
|
39
|
+
flexBasis={0}
|
|
40
|
+
minWidth={0}
|
|
41
|
+
minHeight={0}
|
|
42
|
+
overflow="hidden"
|
|
43
|
+
data-gloom-role="pane-content"
|
|
44
|
+
>
|
|
45
|
+
<Component
|
|
46
|
+
paneId={paneId}
|
|
47
|
+
paneType={paneType}
|
|
48
|
+
focused={focused}
|
|
49
|
+
width={width}
|
|
50
|
+
height={height}
|
|
51
|
+
close={onClose ? close : undefined}
|
|
52
|
+
/>
|
|
53
|
+
</Box>
|
|
42
54
|
</PaneInstanceProvider>
|
|
43
55
|
);
|
|
44
56
|
});
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
useCallback,
|
|
4
4
|
useContext,
|
|
5
5
|
useEffect,
|
|
6
|
+
useLayoutEffect,
|
|
6
7
|
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
@@ -17,6 +18,9 @@ import {
|
|
|
17
18
|
type PaneHint,
|
|
18
19
|
} from "./model";
|
|
19
20
|
|
|
21
|
+
const usePaneFooterRegistrationEffect =
|
|
22
|
+
typeof document === "undefined" ? useEffect : useLayoutEffect;
|
|
23
|
+
|
|
20
24
|
interface PaneFooterContextValue {
|
|
21
25
|
register(registrationId: string, registration: PaneFooterRegistration | null): void;
|
|
22
26
|
unregister(registrationId: string): void;
|
|
@@ -85,14 +89,14 @@ export function usePaneFooter(
|
|
|
85
89
|
const context = useContext(PaneFooterContext);
|
|
86
90
|
const previousRegistrationRef = useRef<PaneFooterRegistration | null>(null);
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
usePaneFooterRegistrationEffect(() => {
|
|
89
93
|
return () => {
|
|
90
94
|
previousRegistrationRef.current = null;
|
|
91
95
|
context?.unregister(registrationId);
|
|
92
96
|
};
|
|
93
97
|
}, [context, registrationId]);
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
usePaneFooterRegistrationEffect(() => {
|
|
96
100
|
if (!context) return;
|
|
97
101
|
const nextRegistration = factory() ?? null;
|
|
98
102
|
if (samePaneFooterRegistration(previousRegistrationRef.current, nextRegistration)) return;
|
|
@@ -19,13 +19,13 @@ export function getPaneWindowAttributes({
|
|
|
19
19
|
windowModeSelected?: boolean;
|
|
20
20
|
showBorderColor?: boolean;
|
|
21
21
|
}): Record<string, unknown> {
|
|
22
|
-
if (!enabled) return {};
|
|
23
|
-
|
|
24
22
|
const attributes: Record<string, unknown> = {
|
|
25
23
|
"data-gloom-role": role,
|
|
26
|
-
"data-focused": focused ? "true" : "false",
|
|
27
24
|
};
|
|
28
25
|
if (paneId) attributes["data-gloom-pane-id"] = paneId;
|
|
26
|
+
if (!enabled) return attributes;
|
|
27
|
+
|
|
28
|
+
attributes["data-focused"] = focused ? "true" : "false";
|
|
29
29
|
if (floating != null) attributes["data-floating"] = floating ? "true" : "false";
|
|
30
30
|
if (windowModeSelected != null) {
|
|
31
31
|
attributes["data-window-mode-selected"] = windowModeSelected ? "true" : "false";
|
|
@@ -48,7 +48,7 @@ export function PaneBodyFrame({
|
|
|
48
48
|
children: ReactNode;
|
|
49
49
|
}) {
|
|
50
50
|
return (
|
|
51
|
-
<Box {...layoutProps} overflow="hidden" backgroundColor={backgroundColor}>
|
|
51
|
+
<Box {...layoutProps} overflow="hidden" backgroundColor={backgroundColor} data-gloom-role="pane-body">
|
|
52
52
|
{children}
|
|
53
53
|
</Box>
|
|
54
54
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Box, Span, Text, useUiCapabilities } from "../../../ui";
|
|
2
|
-
import type
|
|
1
|
+
import { Box, Span, Text, useNativeRenderer, useUiCapabilities } from "../../../ui";
|
|
2
|
+
import { useCallback, useRef, type ReactNode } from "react";
|
|
3
3
|
import { blendHex, colors, floatingPaneTitleBg, paneTitleBg, paneTitleText } from "../../../theme/colors";
|
|
4
4
|
|
|
5
5
|
const PANE_HEADER_HEIGHT = 1;
|
|
@@ -29,6 +29,18 @@ function truncateTitle(title: string, maxWidth: number): string {
|
|
|
29
29
|
return `${title.slice(0, maxWidth - 2)}..`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function captureTerminalPointerDrag(renderer: unknown, renderable: unknown): void {
|
|
33
|
+
if (!renderable) return;
|
|
34
|
+
const hostCapture = (renderer as { captureMouseRenderable?: (target: unknown) => void }).captureMouseRenderable;
|
|
35
|
+
if (typeof hostCapture === "function") {
|
|
36
|
+
hostCapture.call(renderer, renderable);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const capture = (renderer as { setCapturedRenderable?: (target: unknown) => void }).setCapturedRenderable;
|
|
40
|
+
if (typeof capture !== "function") return;
|
|
41
|
+
capture.call(renderer, renderable);
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
function DesktopPaneButton({
|
|
33
45
|
icon,
|
|
34
46
|
onMouseDown,
|
|
@@ -107,11 +119,17 @@ export function PaneHeader({
|
|
|
107
119
|
onCloseMouseDown,
|
|
108
120
|
}: PaneHeaderProps) {
|
|
109
121
|
const { nativePaneChrome } = useUiCapabilities();
|
|
122
|
+
const nativeRenderer = useNativeRenderer();
|
|
123
|
+
const terminalHeaderRef = useRef<unknown>(null);
|
|
110
124
|
const visuallyFocused = focused || windowModeSelected;
|
|
111
125
|
const backgroundColor = floating ? floatingPaneTitleBg(visuallyFocused) : paneTitleBg(visuallyFocused);
|
|
112
126
|
const actionText = showActions ? PANE_HEADER_ACTION : " ";
|
|
113
127
|
const closeText = floating ? PANE_HEADER_CLOSE : "";
|
|
114
128
|
const textColor = paneTitleText(visuallyFocused, floating);
|
|
129
|
+
const handleTerminalHeaderMouseDown = useCallback((event: any) => {
|
|
130
|
+
captureTerminalPointerDrag(nativeRenderer, terminalHeaderRef.current);
|
|
131
|
+
onHeaderMouseDown?.(event);
|
|
132
|
+
}, [nativeRenderer, onHeaderMouseDown]);
|
|
115
133
|
|
|
116
134
|
if (nativePaneChrome) {
|
|
117
135
|
return (
|
|
@@ -201,7 +219,16 @@ export function PaneHeader({
|
|
|
201
219
|
const fill = "─".repeat(fillLen);
|
|
202
220
|
|
|
203
221
|
return (
|
|
204
|
-
<Box
|
|
222
|
+
<Box
|
|
223
|
+
ref={terminalHeaderRef}
|
|
224
|
+
height={PANE_HEADER_HEIGHT}
|
|
225
|
+
width={width}
|
|
226
|
+
backgroundColor={backgroundColor}
|
|
227
|
+
flexDirection="row"
|
|
228
|
+
onMouseDown={handleTerminalHeaderMouseDown}
|
|
229
|
+
onMouseDrag={onHeaderMouseDrag}
|
|
230
|
+
onMouseDragEnd={onHeaderMouseDragEnd}
|
|
231
|
+
>
|
|
205
232
|
<Text fg={borderColor} selectable={false}>{"┌─"}</Text>
|
|
206
233
|
<Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
|
|
207
234
|
<Text fg={borderColor} selectable={false}>{fill}</Text>
|
|
@@ -229,7 +256,16 @@ export function PaneHeader({
|
|
|
229
256
|
const padding = " ".repeat(Math.max(0, titleWidth - clippedTitle.length));
|
|
230
257
|
|
|
231
258
|
return (
|
|
232
|
-
<Box
|
|
259
|
+
<Box
|
|
260
|
+
ref={terminalHeaderRef}
|
|
261
|
+
height={PANE_HEADER_HEIGHT}
|
|
262
|
+
width={width}
|
|
263
|
+
backgroundColor={backgroundColor}
|
|
264
|
+
flexDirection="row"
|
|
265
|
+
onMouseDown={handleTerminalHeaderMouseDown}
|
|
266
|
+
onMouseDrag={onHeaderMouseDrag}
|
|
267
|
+
onMouseDragEnd={onHeaderMouseDragEnd}
|
|
268
|
+
>
|
|
233
269
|
<Text fg={textColor} selectable={false}>
|
|
234
270
|
{`${PANE_HEADER_GRIP}${clippedTitle}${padding}`}
|
|
235
271
|
</Text>
|
|
@@ -49,9 +49,11 @@ export function PaneWrapper({
|
|
|
49
49
|
const bg = paneBg(focused);
|
|
50
50
|
const showFooter = hasPaneFooterContent(footer);
|
|
51
51
|
const reserveFooter = !!title && shouldReservePaneFooter(nativePaneChrome, showFooter);
|
|
52
|
+
const renderFooter = !!title && (reserveFooter || showFooter);
|
|
52
53
|
const bodyFrame = resolvePaneBodyFrame({
|
|
53
54
|
height: typeof height === "number" ? height : undefined,
|
|
54
55
|
nativePaneChrome,
|
|
56
|
+
footerVisible: renderFooter,
|
|
55
57
|
reserveFooter,
|
|
56
58
|
headerRows: title ? 1 : 0,
|
|
57
59
|
});
|
|
@@ -93,7 +95,7 @@ export function PaneWrapper({
|
|
|
93
95
|
<PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={bg}>
|
|
94
96
|
{children}
|
|
95
97
|
</PaneBodyFrame>
|
|
96
|
-
{
|
|
98
|
+
{renderFooter && (
|
|
97
99
|
<PaneFooterBar
|
|
98
100
|
footer={footer}
|
|
99
101
|
focused={focused}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolvePaneBodyFrame, shouldReservePaneFooter } from "./sizing";
|
|
3
|
+
|
|
4
|
+
describe("pane sizing", () => {
|
|
5
|
+
test("lets native pane chrome lay out footer bars in normal flex flow", () => {
|
|
6
|
+
expect(shouldReservePaneFooter(true, true)).toBe(false);
|
|
7
|
+
|
|
8
|
+
const bodyFrame = resolvePaneBodyFrame({
|
|
9
|
+
width: 80,
|
|
10
|
+
height: 30,
|
|
11
|
+
nativePaneChrome: true,
|
|
12
|
+
footerVisible: true,
|
|
13
|
+
reserveFooter: false,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expect(bodyFrame.height).toBe(28);
|
|
17
|
+
expect(bodyFrame.layoutProps).toEqual({
|
|
18
|
+
flexGrow: 1,
|
|
19
|
+
flexShrink: 1,
|
|
20
|
+
flexBasis: 0,
|
|
21
|
+
minWidth: 0,
|
|
22
|
+
minHeight: 0,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("keeps terminal pane footer rows reserved for border rendering", () => {
|
|
27
|
+
expect(shouldReservePaneFooter(false, false)).toBe(true);
|
|
28
|
+
|
|
29
|
+
const bodyFrame = resolvePaneBodyFrame({
|
|
30
|
+
width: 80,
|
|
31
|
+
height: 30,
|
|
32
|
+
nativePaneChrome: false,
|
|
33
|
+
reserveFooter: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(bodyFrame.height).toBe(28);
|
|
37
|
+
expect(bodyFrame.layoutProps).toEqual({
|
|
38
|
+
height: 28,
|
|
39
|
+
flexGrow: 0,
|
|
40
|
+
flexBasis: undefined,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|