gloomberb 0.9.3 → 0.9.5
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 +3 -2
- package/patches/@opentui%2Fcore@0.3.2.patch +231 -29
- package/patches/react-reconciler@0.33.0.patch +10 -0
- 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/entry.ts +8 -1
- package/src/cli/native-smoke.ts +5 -0
- 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/native/bitmap-support.test.ts +27 -0
- package/src/components/chart/native/bitmap-support.ts +55 -0
- package/src/components/chart/native/kitty/index.test.ts +47 -0
- package/src/components/chart/native/surface/visibility.ts +8 -1
- 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/bitmap.ts +16 -9
- 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/command-bar/list/view.tsx +1 -1
- package/src/components/command-bar/theme-picker.tsx +1 -1
- package/src/components/data-table/view.tsx +0 -12
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +6 -5
- 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 +45 -4
- package/src/components/layout/pane/index.tsx +6 -4
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/action-menu-overlay.tsx +1 -1
- package/src/components/layout/shell/cursor-occlusion.ts +204 -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 +45 -3
- 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 +10 -4
- 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 +10 -6
- 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/pane-settings-dialog/desktop.tsx +1 -1
- package/src/components/price-performance.tsx +1 -1
- package/src/components/price-sparkline/view.tsx +15 -14
- package/src/components/speedometer-gauge/terminal.tsx +18 -15
- package/src/components/table-view-shared.tsx +12 -14
- package/src/components/ticker/list-table-view.tsx +1 -7
- package/src/components/ticker/list-table.test.tsx +0 -4
- package/src/components/ui/data-table/opentui/index.tsx +29 -22
- package/src/components/ui/data-table/types.ts +10 -4
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/list-view.tsx +1 -1
- 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 +27 -12
- package/src/components/ui/ui.test.tsx +87 -9
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/plugins/builtin/account-management/form-components.tsx +2 -2
- package/src/plugins/builtin/alerts/pane.tsx +1 -1
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/buildout/cells.tsx +1 -1
- package/src/plugins/builtin/buildout/pane/index.tsx +1 -1
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -1
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +1 -1
- package/src/plugins/builtin/chat/message/terminal.tsx +2 -2
- package/src/plugins/builtin/chat/sidebar.tsx +1 -1
- package/src/plugins/builtin/chat/status-widget.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-actions.tsx +2 -2
- package/src/plugins/builtin/correlation/matrix/symbol-cell.tsx +1 -1
- package/src/plugins/builtin/debug/index.tsx +3 -4
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/holders/treemap.tsx +1 -1
- 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/table.ts +5 -5
- 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/ibkr/trade/ticket-panel.tsx +1 -1
- 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 -12
- package/src/renderers/electrobun/view/data-table/row.tsx +32 -18
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/box.tsx +6 -1
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/style.ts +1 -0
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/styles.css +18 -0
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/native-smoke.ts +1 -0
- 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
|
@@ -29,6 +29,10 @@ export interface DataTableSectionHeader {
|
|
|
29
29
|
|
|
30
30
|
export type DataTableScrollAlign = "nearest" | "center";
|
|
31
31
|
|
|
32
|
+
export interface DataTableRowState {
|
|
33
|
+
selected: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
export interface DataTableProps<
|
|
33
37
|
T,
|
|
34
38
|
C extends DataTableColumn = DataTableColumn,
|
|
@@ -42,14 +46,13 @@ export interface DataTableProps<
|
|
|
42
46
|
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
43
47
|
syncHeaderScroll: () => void;
|
|
44
48
|
onBodyScrollActivity: () => void;
|
|
45
|
-
hoveredIdx: number | null;
|
|
46
|
-
setHoveredIdx: (index: number | null) => void;
|
|
47
49
|
headerScrollId?: string;
|
|
48
50
|
bodyScrollId?: string;
|
|
49
51
|
getItemKey: (item: T, index: number) => string;
|
|
50
52
|
isSelected: (item: T, index: number) => boolean;
|
|
51
53
|
onSelect: (item: T, index: number) => void;
|
|
52
54
|
onActivate?: (item: T, index: number) => void;
|
|
55
|
+
onTableMouseDown?: (event: any) => void;
|
|
53
56
|
onRowMouseDown?: (item: T, index: number, event: any) => boolean | void;
|
|
54
57
|
onRowContextMenu?: (item: T, index: number, event: any) => void;
|
|
55
58
|
rowContextMenuSurface?: boolean;
|
|
@@ -57,7 +60,7 @@ export interface DataTableProps<
|
|
|
57
60
|
item: T,
|
|
58
61
|
column: C,
|
|
59
62
|
index: number,
|
|
60
|
-
rowState:
|
|
63
|
+
rowState: DataTableRowState,
|
|
61
64
|
) => DataTableCell;
|
|
62
65
|
renderSectionHeader?: (
|
|
63
66
|
item: T,
|
|
@@ -66,7 +69,7 @@ export interface DataTableProps<
|
|
|
66
69
|
getRowBackgroundColor?: (
|
|
67
70
|
item: T,
|
|
68
71
|
index: number,
|
|
69
|
-
rowState:
|
|
72
|
+
rowState: DataTableRowState,
|
|
70
73
|
) => string | undefined;
|
|
71
74
|
emptyContent?: ReactNode;
|
|
72
75
|
bodyAfter?: ReactNode;
|
|
@@ -74,6 +77,9 @@ export interface DataTableProps<
|
|
|
74
77
|
emptyStateHint?: string;
|
|
75
78
|
virtualize?: boolean;
|
|
76
79
|
overscan?: number;
|
|
80
|
+
columnGap?: number;
|
|
81
|
+
horizontalPadding?: number;
|
|
82
|
+
fillAvailableWidth?: boolean;
|
|
77
83
|
showHorizontalScrollbar?: boolean;
|
|
78
84
|
scrollToIndex?: number | null;
|
|
79
85
|
scrollToIndexAlign?: DataTableScrollAlign;
|
|
@@ -12,6 +12,7 @@ export interface TextFieldProps {
|
|
|
12
12
|
inputRef?: RefObject<InputRenderable | null>;
|
|
13
13
|
onChange?: (value: string) => void;
|
|
14
14
|
onSubmit?: (value: string) => void;
|
|
15
|
+
onBlur?: (value: string) => void;
|
|
15
16
|
hint?: string;
|
|
16
17
|
type?: "text" | "password";
|
|
17
18
|
variant?: "default" | "plain";
|
|
@@ -36,6 +37,7 @@ export function TextField({
|
|
|
36
37
|
inputRef,
|
|
37
38
|
onChange,
|
|
38
39
|
onSubmit,
|
|
40
|
+
onBlur,
|
|
39
41
|
hint,
|
|
40
42
|
type = "text",
|
|
41
43
|
variant = "default",
|
|
@@ -56,6 +58,7 @@ export function TextField({
|
|
|
56
58
|
inputRef={inputRef}
|
|
57
59
|
onChange={onChange}
|
|
58
60
|
onSubmit={onSubmit}
|
|
61
|
+
onBlur={onBlur}
|
|
59
62
|
hint={hint}
|
|
60
63
|
type={type}
|
|
61
64
|
variant={variant}
|
|
@@ -118,17 +121,18 @@ export function TextField({
|
|
|
118
121
|
selectionFg={isPassword ? backgroundColor : undefined}
|
|
119
122
|
showCursor={!isPassword}
|
|
120
123
|
onCursorChange={() => syncCursorOffset()}
|
|
121
|
-
onInput={(nextValue) => {
|
|
124
|
+
onInput={(nextValue: string) => {
|
|
122
125
|
currentValueRef.current = nextValue;
|
|
123
126
|
syncCursorOffset(nextValue);
|
|
124
127
|
onChange?.(nextValue);
|
|
125
128
|
}}
|
|
126
|
-
onChange={(nextValue) => {
|
|
129
|
+
onChange={(nextValue: string) => {
|
|
127
130
|
currentValueRef.current = nextValue;
|
|
128
131
|
syncCursorOffset(nextValue);
|
|
129
132
|
onChange?.(nextValue);
|
|
130
133
|
}}
|
|
131
134
|
onSubmit={() => onSubmit?.(currentValueRef.current)}
|
|
135
|
+
onBlur={() => onBlur?.(currentValueRef.current)}
|
|
132
136
|
/>
|
|
133
137
|
{isPassword && (
|
|
134
138
|
<Box
|
|
@@ -178,11 +182,12 @@ function sanitizeNumberInput(value: string, allowDecimal: boolean, allowNegative
|
|
|
178
182
|
return next;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
export interface NumberFieldProps extends Omit<TextFieldProps, "onChange" | "onSubmit"> {
|
|
185
|
+
export interface NumberFieldProps extends Omit<TextFieldProps, "onChange" | "onSubmit" | "onBlur"> {
|
|
182
186
|
allowDecimal?: boolean;
|
|
183
187
|
allowNegative?: boolean;
|
|
184
188
|
onChange?: (value: string) => void;
|
|
185
189
|
onSubmit?: (value: string) => void;
|
|
190
|
+
onBlur?: (value: string) => void;
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
export function NumberField({
|
|
@@ -190,6 +195,7 @@ export function NumberField({
|
|
|
190
195
|
allowNegative = false,
|
|
191
196
|
onChange,
|
|
192
197
|
onSubmit,
|
|
198
|
+
onBlur,
|
|
193
199
|
...props
|
|
194
200
|
}: NumberFieldProps) {
|
|
195
201
|
return (
|
|
@@ -197,6 +203,7 @@ export function NumberField({
|
|
|
197
203
|
{...props}
|
|
198
204
|
onChange={(nextValue) => onChange?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
|
|
199
205
|
onSubmit={(nextValue) => onSubmit?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
|
|
206
|
+
onBlur={(nextValue) => onBlur?.(sanitizeNumberInput(nextValue, allowDecimal, allowNegative))}
|
|
200
207
|
/>
|
|
201
208
|
);
|
|
202
209
|
}
|
|
@@ -166,7 +166,7 @@ export function ListView({
|
|
|
166
166
|
key={item.id}
|
|
167
167
|
height={1}
|
|
168
168
|
backgroundColor={rowBg}
|
|
169
|
-
|
|
169
|
+
onMouseOver={() => {
|
|
170
170
|
if (!disabled) {
|
|
171
171
|
setHoveredIndex((current) => (current === index ? current : index));
|
|
172
172
|
if (selectOnHover) onSelect?.(index);
|
|
@@ -69,7 +69,7 @@ export function PageStackView({
|
|
|
69
69
|
|
|
70
70
|
if (!detailOpen) {
|
|
71
71
|
return (
|
|
72
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
72
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
73
73
|
{rootContent}
|
|
74
74
|
</Box>
|
|
75
75
|
);
|
|
@@ -79,6 +79,10 @@ export function PageStackView({
|
|
|
79
79
|
<Box
|
|
80
80
|
flexDirection="column"
|
|
81
81
|
flexGrow={1}
|
|
82
|
+
flexShrink={1}
|
|
83
|
+
flexBasis={0}
|
|
84
|
+
minWidth={0}
|
|
85
|
+
minHeight={0}
|
|
82
86
|
overflow="hidden"
|
|
83
87
|
onMouseDown={handleMouseDown}
|
|
84
88
|
>
|
|
@@ -111,7 +115,7 @@ export function PageStackView({
|
|
|
111
115
|
)}
|
|
112
116
|
{backHint ? <Text fg={colors.textMuted}>{backHint}</Text> : null}
|
|
113
117
|
</Box>
|
|
114
|
-
<Box flexDirection="column" flexGrow={1} overflow="hidden">
|
|
118
|
+
<Box flexDirection="column" flexGrow={1} flexShrink={1} flexBasis={0} minWidth={0} minHeight={0} overflow="hidden">
|
|
115
119
|
{detailContent}
|
|
116
120
|
</Box>
|
|
117
121
|
</Box>
|
|
@@ -18,6 +18,22 @@ describe("table layout", () => {
|
|
|
18
18
|
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 2)).toBe(true);
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
test("supports tables without inter-column gaps", () => {
|
|
22
|
+
const tableWidth = getTableWidth([
|
|
23
|
+
{ width: 12 },
|
|
24
|
+
{ width: 8 },
|
|
25
|
+
], 0);
|
|
26
|
+
const noPaddingWidth = getTableWidth([
|
|
27
|
+
{ width: 12 },
|
|
28
|
+
{ width: 8 },
|
|
29
|
+
], 0, 0);
|
|
30
|
+
|
|
31
|
+
expect(tableWidth).toBe(22);
|
|
32
|
+
expect(noPaddingWidth).toBe(20);
|
|
33
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth, 0)).toBe(false);
|
|
34
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 1, 0)).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
21
37
|
test("keeps non-flex web table columns fixed", () => {
|
|
22
38
|
const template = buildTableGridTemplateColumns([
|
|
23
39
|
{ width: 4, align: "right" },
|
|
@@ -39,4 +55,13 @@ describe("table layout", () => {
|
|
|
39
55
|
|
|
40
56
|
expect(template).toBe("minmax(calc(8 * var(--cell-w)), 12fr) minmax(calc(8 * var(--cell-w)), 8fr)");
|
|
41
57
|
});
|
|
58
|
+
|
|
59
|
+
test("keeps fixed web table columns when fill width is disabled", () => {
|
|
60
|
+
const template = buildTableGridTemplateColumns([
|
|
61
|
+
{ width: 12 },
|
|
62
|
+
{ width: 8, align: "right" },
|
|
63
|
+
], false);
|
|
64
|
+
|
|
65
|
+
expect(template).toBe("minmax(calc(8 * var(--cell-w)), calc(12 * var(--cell-w))) minmax(calc(8 * var(--cell-w)), calc(8 * var(--cell-w)))");
|
|
66
|
+
});
|
|
42
67
|
});
|
|
@@ -10,19 +10,29 @@ export interface TableWidthColumn {
|
|
|
10
10
|
const TRAILING_COLUMN_GUTTER_WIDTH = 1;
|
|
11
11
|
const WEB_CELL_UNIT = "var(--cell-w)";
|
|
12
12
|
|
|
13
|
-
export function getTableWidth(
|
|
14
|
-
|
|
13
|
+
export function getTableWidth(
|
|
14
|
+
columns: readonly TableWidthColumn[],
|
|
15
|
+
columnGap = 1,
|
|
16
|
+
horizontalPadding = 1,
|
|
17
|
+
): number {
|
|
18
|
+
return columns.reduce((sum, column) => sum + column.width + columnGap, horizontalPadding * 2);
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
export function hasMeaningfulTableHorizontalOverflow(
|
|
18
|
-
|
|
21
|
+
export function hasMeaningfulTableHorizontalOverflow(
|
|
22
|
+
tableWidth: number,
|
|
23
|
+
viewportWidth: number,
|
|
24
|
+
columnGap = TRAILING_COLUMN_GUTTER_WIDTH,
|
|
25
|
+
): boolean {
|
|
26
|
+
return viewportWidth > 0 && tableWidth - viewportWidth > columnGap;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export function expandTableColumns<C extends TableWidthColumn>(
|
|
22
30
|
columns: readonly C[],
|
|
23
31
|
targetWidth: number,
|
|
32
|
+
columnGap = 1,
|
|
33
|
+
horizontalPadding = 1,
|
|
24
34
|
): C[] {
|
|
25
|
-
const currentWidth = getTableWidth(columns);
|
|
35
|
+
const currentWidth = getTableWidth(columns, columnGap, horizontalPadding);
|
|
26
36
|
const extraWidth = Math.floor(targetWidth) - currentWidth;
|
|
27
37
|
if (extraWidth <= 0) return [...columns];
|
|
28
38
|
|
|
@@ -62,12 +72,18 @@ function cellWidthCss(width: number): string {
|
|
|
62
72
|
return `calc(${width} * ${WEB_CELL_UNIT})`;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
export function buildTableGridTemplateColumns(
|
|
75
|
+
export function buildTableGridTemplateColumns(
|
|
76
|
+
columns: readonly TableWidthColumn[],
|
|
77
|
+
fillAvailableWidth = true,
|
|
78
|
+
): string {
|
|
66
79
|
const hasFlexColumn = columns.some((column) => (column.flexGrow ?? 0) > 0);
|
|
67
80
|
return columns
|
|
68
81
|
.map((column) => {
|
|
69
82
|
const width = normalizedColumnWidth(column);
|
|
70
83
|
const minWidth = cellWidthCss(columnMinCh(column));
|
|
84
|
+
if (!fillAvailableWidth) {
|
|
85
|
+
return `minmax(${minWidth}, ${cellWidthCss(width)})`;
|
|
86
|
+
}
|
|
71
87
|
if (!hasFlexColumn || (column.flexGrow ?? 0) > 0) {
|
|
72
88
|
return `minmax(${minWidth}, ${columnFlexWeight(column)}fr)`;
|
|
73
89
|
}
|
|
@@ -31,6 +31,7 @@ export interface TabsProps {
|
|
|
31
31
|
focused?: boolean;
|
|
32
32
|
keyboardNavigation?: boolean;
|
|
33
33
|
scrollable?: boolean;
|
|
34
|
+
scrollId?: string;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const WHEEL_DELTA_PER_CELL = 8;
|
|
@@ -47,6 +48,7 @@ export function Tabs({
|
|
|
47
48
|
focused = false,
|
|
48
49
|
keyboardNavigation = true,
|
|
49
50
|
scrollable = true,
|
|
51
|
+
scrollId,
|
|
50
52
|
}: TabsProps) {
|
|
51
53
|
const ui = useUiHost();
|
|
52
54
|
const NativeTabs = ui.Tabs;
|
|
@@ -57,11 +59,11 @@ export function Tabs({
|
|
|
57
59
|
navigationValueRef.current = activeValue;
|
|
58
60
|
}
|
|
59
61
|
const palette = {
|
|
60
|
-
activeFg: variant === "bare" ? colors.textBright : colors.text,
|
|
62
|
+
activeFg: focused || variant === "bare" ? colors.textBright : colors.text,
|
|
61
63
|
inactiveFg: colors.textDim,
|
|
62
64
|
disabledFg: colors.textMuted,
|
|
63
65
|
hoverFg: colors.text,
|
|
64
|
-
activeUnderline: colors.borderFocused,
|
|
66
|
+
activeUnderline: focused ? colors.textBright : colors.borderFocused,
|
|
65
67
|
inactiveUnderline: colors.bg,
|
|
66
68
|
hoverUnderline: colors.border,
|
|
67
69
|
hoverBg: hoverBg(),
|
|
@@ -118,6 +120,7 @@ export function Tabs({
|
|
|
118
120
|
closeMode={closeMode}
|
|
119
121
|
addLabel={addLabel}
|
|
120
122
|
onAdd={onAdd}
|
|
123
|
+
focused={focused}
|
|
121
124
|
palette={palette}
|
|
122
125
|
/>
|
|
123
126
|
);
|
|
@@ -133,7 +136,9 @@ export function Tabs({
|
|
|
133
136
|
closeMode={closeMode}
|
|
134
137
|
addLabel={addLabel}
|
|
135
138
|
onAdd={onAdd}
|
|
139
|
+
focused={focused}
|
|
136
140
|
scrollable={scrollable}
|
|
141
|
+
scrollId={scrollId}
|
|
137
142
|
palette={palette}
|
|
138
143
|
/>
|
|
139
144
|
);
|
|
@@ -157,7 +162,9 @@ function OpenTuiTabs({
|
|
|
157
162
|
closeMode = "always",
|
|
158
163
|
addLabel = "+",
|
|
159
164
|
onAdd,
|
|
165
|
+
focused = false,
|
|
160
166
|
scrollable = true,
|
|
167
|
+
scrollId,
|
|
161
168
|
palette,
|
|
162
169
|
}: TabsProps & {
|
|
163
170
|
palette: {
|
|
@@ -187,7 +194,7 @@ function OpenTuiTabs({
|
|
|
187
194
|
useEffect(() => {
|
|
188
195
|
const scrollBox = scrollRef.current;
|
|
189
196
|
const activeIndex = tabs.findIndex((tab) => tab.value === activeValue);
|
|
190
|
-
const viewportWidth = scrollBox?.viewport?.width
|
|
197
|
+
const viewportWidth = scrollBox?.viewport?.width || scrollBox?.width || 0;
|
|
191
198
|
if (!scrollBox || activeIndex < 0 || viewportWidth <= 0) return;
|
|
192
199
|
|
|
193
200
|
const activeLeft = tabWidths.slice(0, activeIndex).reduce((sum, width) => sum + width, 0);
|
|
@@ -195,14 +202,15 @@ function OpenTuiTabs({
|
|
|
195
202
|
const currentLeft = scrollBox.scrollLeft ?? 0;
|
|
196
203
|
const currentRight = currentLeft + viewportWidth;
|
|
197
204
|
const maxScrollLeft = Math.max(0, totalWidth - viewportWidth);
|
|
205
|
+
const scrollToLeft = (left: number) => {
|
|
206
|
+
scrollBox.scrollLeft = left;
|
|
207
|
+
scrollBox.scrollTo({ x: left, y: scrollBox.scrollTop });
|
|
208
|
+
};
|
|
198
209
|
|
|
199
210
|
if (activeLeft < currentLeft) {
|
|
200
|
-
|
|
211
|
+
scrollToLeft(activeLeft);
|
|
201
212
|
} else if (activeRight > currentRight) {
|
|
202
|
-
|
|
203
|
-
x: Math.min(activeRight - viewportWidth, maxScrollLeft),
|
|
204
|
-
y: scrollBox.scrollTop,
|
|
205
|
-
});
|
|
213
|
+
scrollToLeft(Math.min(activeRight - viewportWidth, maxScrollLeft));
|
|
206
214
|
}
|
|
207
215
|
}, [activeValue, tabWidths, tabs, totalWidth]);
|
|
208
216
|
|
|
@@ -214,7 +222,7 @@ function OpenTuiTabs({
|
|
|
214
222
|
const direction = event?.scroll?.direction;
|
|
215
223
|
if (!direction) return;
|
|
216
224
|
const scrollBox = scrollRef.current;
|
|
217
|
-
const viewportWidth = scrollBox?.viewport?.width
|
|
225
|
+
const viewportWidth = scrollBox?.viewport?.width || scrollBox?.width || 0;
|
|
218
226
|
if (!scrollBox || viewportWidth <= 0 || totalWidth <= viewportWidth) return;
|
|
219
227
|
|
|
220
228
|
event.preventDefault?.();
|
|
@@ -228,6 +236,7 @@ function OpenTuiTabs({
|
|
|
228
236
|
0,
|
|
229
237
|
Math.min(maxScrollLeft, (scrollBox.scrollLeft ?? 0) + directionSign * deltaCells),
|
|
230
238
|
);
|
|
239
|
+
scrollBox.scrollLeft = nextLeft;
|
|
231
240
|
scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
|
|
232
241
|
};
|
|
233
242
|
|
|
@@ -236,10 +245,16 @@ function OpenTuiTabs({
|
|
|
236
245
|
{tabs.map((tab, index) => {
|
|
237
246
|
const active = tab.value === activeValue;
|
|
238
247
|
const hovered = hoveredValue === tab.value && !tab.disabled;
|
|
248
|
+
const focusedActive = focused && active;
|
|
239
249
|
const tabWidth = tabWidths[index] ?? tab.label.length + 2;
|
|
240
250
|
const showClose = !!tab.onClose && (closeMode === "always" || active);
|
|
241
251
|
const attributes = (active ? TextAttributes.BOLD : 0)
|
|
242
|
-
| (
|
|
252
|
+
| (
|
|
253
|
+
(variant === "underline" && !compact && (active || hovered))
|
|
254
|
+
|| (variant === "bare" && focusedActive)
|
|
255
|
+
? TextAttributes.UNDERLINE
|
|
256
|
+
: 0
|
|
257
|
+
);
|
|
243
258
|
const startHover = tab.disabled
|
|
244
259
|
? undefined
|
|
245
260
|
: () => {
|
|
@@ -272,7 +287,6 @@ function OpenTuiTabs({
|
|
|
272
287
|
flexDirection="row"
|
|
273
288
|
backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
|
|
274
289
|
onMouseOver={startHover}
|
|
275
|
-
onMouseMove={startHover}
|
|
276
290
|
onMouseOut={endHover}
|
|
277
291
|
onMouseDown={selectTab}
|
|
278
292
|
onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
|
|
@@ -304,7 +318,7 @@ function OpenTuiTabs({
|
|
|
304
318
|
width={addWidth}
|
|
305
319
|
height={1}
|
|
306
320
|
backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
|
|
307
|
-
|
|
321
|
+
onMouseOver={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
|
|
308
322
|
onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
|
|
309
323
|
onMouseDown={(event: TabPointerEvent) => {
|
|
310
324
|
event.preventDefault();
|
|
@@ -323,6 +337,7 @@ function OpenTuiTabs({
|
|
|
323
337
|
|
|
324
338
|
return (
|
|
325
339
|
<ScrollBox
|
|
340
|
+
id={scrollId}
|
|
326
341
|
ref={scrollRef}
|
|
327
342
|
width="100%"
|
|
328
343
|
height={1}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
+
import { TextAttributes } from "../../ui";
|
|
4
5
|
import type { BoxRenderable, ScrollBoxRenderable } from "@opentui/core";
|
|
5
6
|
import { ChoiceDialog } from "./choice-dialog";
|
|
6
7
|
import { DataTable, type DataTableColumn } from "./data-table";
|
|
@@ -50,7 +51,6 @@ function DataTableActivationHarness() {
|
|
|
50
51
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
51
52
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
52
53
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
53
|
-
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
56
|
<DataTable
|
|
@@ -63,8 +63,6 @@ function DataTableActivationHarness() {
|
|
|
63
63
|
scrollRef={scrollRef}
|
|
64
64
|
syncHeaderScroll={() => {}}
|
|
65
65
|
onBodyScrollActivity={() => {}}
|
|
66
|
-
hoveredIdx={hoveredIdx}
|
|
67
|
-
setHoveredIdx={setHoveredIdx}
|
|
68
66
|
getItemKey={(row) => row.id}
|
|
69
67
|
isSelected={(row) => selectedId === row.id}
|
|
70
68
|
onSelect={(row) => {
|
|
@@ -88,7 +86,6 @@ function DataTableSectionHarness() {
|
|
|
88
86
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
89
87
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
90
88
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
91
|
-
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
92
89
|
const rows: SectionTableRow[] = [
|
|
93
90
|
{ kind: "header", id: "macro", label: "Macro Releases" },
|
|
94
91
|
{ kind: "row", id: "cpi", name: "CPI" },
|
|
@@ -105,8 +102,6 @@ function DataTableSectionHarness() {
|
|
|
105
102
|
scrollRef={scrollRef}
|
|
106
103
|
syncHeaderScroll={() => {}}
|
|
107
104
|
onBodyScrollActivity={() => {}}
|
|
108
|
-
hoveredIdx={hoveredIdx}
|
|
109
|
-
setHoveredIdx={setHoveredIdx}
|
|
110
105
|
getItemKey={(row) => row.id}
|
|
111
106
|
isSelected={(row) => row.kind === "row" && row.id === selectedId}
|
|
112
107
|
onSelect={(row) => {
|
|
@@ -128,12 +123,18 @@ function DataTableHorizontalScrollHarness({
|
|
|
128
123
|
containerWidth = 32,
|
|
129
124
|
containerHeight = 5,
|
|
130
125
|
rowCount = 1,
|
|
126
|
+
columnGap,
|
|
127
|
+
horizontalPadding,
|
|
128
|
+
fillAvailableWidth,
|
|
131
129
|
showHorizontalScrollbar,
|
|
132
130
|
}: {
|
|
133
131
|
columns?: DataTableColumn[];
|
|
134
132
|
containerWidth?: number;
|
|
135
133
|
containerHeight?: number;
|
|
136
134
|
rowCount?: number;
|
|
135
|
+
columnGap?: number;
|
|
136
|
+
horizontalPadding?: number;
|
|
137
|
+
fillAvailableWidth?: boolean;
|
|
137
138
|
showHorizontalScrollbar?: boolean;
|
|
138
139
|
}) {
|
|
139
140
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
@@ -162,15 +163,48 @@ function DataTableHorizontalScrollHarness({
|
|
|
162
163
|
getItemKey={(row) => row.id}
|
|
163
164
|
renderCell={(row) => ({ text: row.name })}
|
|
164
165
|
emptyStateTitle="No rows."
|
|
166
|
+
columnGap={columnGap}
|
|
167
|
+
horizontalPadding={horizontalPadding}
|
|
168
|
+
fillAvailableWidth={fillAvailableWidth}
|
|
165
169
|
showHorizontalScrollbar={showHorizontalScrollbar}
|
|
166
170
|
/>
|
|
167
171
|
);
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
function DataTableColumnGapHarness() {
|
|
175
|
+
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
176
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
177
|
+
const items = [{ id: "row-0", left: "AA", right: "BB" }];
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<DataTableView
|
|
181
|
+
rootWidth={8}
|
|
182
|
+
rootHeight={4}
|
|
183
|
+
selection={{ kind: "none" }}
|
|
184
|
+
columns={[
|
|
185
|
+
{ id: "left", label: "L", width: 2, align: "left" },
|
|
186
|
+
{ id: "right", label: "R", width: 2, align: "left" },
|
|
187
|
+
]}
|
|
188
|
+
items={items}
|
|
189
|
+
sortColumnId={null}
|
|
190
|
+
sortDirection="asc"
|
|
191
|
+
onHeaderClick={() => {}}
|
|
192
|
+
headerScrollRef={headerScrollRef}
|
|
193
|
+
scrollRef={scrollRef}
|
|
194
|
+
getItemKey={(row) => row.id}
|
|
195
|
+
renderCell={(row, column) => ({ text: column.id === "left" ? row.left : row.right })}
|
|
196
|
+
emptyStateTitle="No rows."
|
|
197
|
+
columnGap={0}
|
|
198
|
+
horizontalPadding={0}
|
|
199
|
+
fillAvailableWidth={false}
|
|
200
|
+
showHorizontalScrollbar={false}
|
|
201
|
+
/>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
170
205
|
function DataTableVirtualizationHarness() {
|
|
171
206
|
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
172
207
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
173
|
-
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
174
208
|
const rows = Array.from({ length: 100 }, (_, index) => ({
|
|
175
209
|
id: `row-${index}`,
|
|
176
210
|
name: `Row ${index}`,
|
|
@@ -196,8 +230,6 @@ function DataTableVirtualizationHarness() {
|
|
|
196
230
|
scrollRef={scrollRef}
|
|
197
231
|
syncHeaderScroll={() => {}}
|
|
198
232
|
onBodyScrollActivity={() => {}}
|
|
199
|
-
hoveredIdx={hoveredIdx}
|
|
200
|
-
setHoveredIdx={setHoveredIdx}
|
|
201
233
|
getItemKey={(row) => row.id}
|
|
202
234
|
isSelected={() => false}
|
|
203
235
|
onSelect={() => {}}
|
|
@@ -360,6 +392,31 @@ describe("shared UI kit", () => {
|
|
|
360
392
|
expect(lastSelected).toBe("chart");
|
|
361
393
|
});
|
|
362
394
|
|
|
395
|
+
test("marks the active bare tab when it has keyboard focus", async () => {
|
|
396
|
+
testSetup = await testRender(
|
|
397
|
+
<Tabs
|
|
398
|
+
tabs={[
|
|
399
|
+
{ label: "One", value: "one" },
|
|
400
|
+
{ label: "Two", value: "two" },
|
|
401
|
+
]}
|
|
402
|
+
activeValue="one"
|
|
403
|
+
onSelect={() => {}}
|
|
404
|
+
variant="bare"
|
|
405
|
+
compact
|
|
406
|
+
focused
|
|
407
|
+
/>,
|
|
408
|
+
{ width: 24, height: 2 },
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
await act(async () => {
|
|
412
|
+
await testSetup!.renderOnce();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const activeSpan = testSetup.captureSpans().lines[0]?.spans.find((span) => span.text.includes("One"));
|
|
416
|
+
expect(activeSpan).toBeDefined();
|
|
417
|
+
expect((activeSpan!.attributes & TextAttributes.UNDERLINE)).toBe(TextAttributes.UNDERLINE);
|
|
418
|
+
});
|
|
419
|
+
|
|
363
420
|
test("selects tabs by clicking their text labels", async () => {
|
|
364
421
|
let lastSelected = "overview";
|
|
365
422
|
const selectedValues: string[] = [];
|
|
@@ -737,6 +794,27 @@ describe("shared UI kit", () => {
|
|
|
737
794
|
expect(tableScrollBoxForTest?.horizontalScrollBar.visible).toBe(false);
|
|
738
795
|
});
|
|
739
796
|
|
|
797
|
+
test("renders data table columns without spacer cells when column gap is zero", async () => {
|
|
798
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
799
|
+
testSetup = await testRender(
|
|
800
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
801
|
+
<PaneInstanceProvider paneId="portfolio-list:main">
|
|
802
|
+
<DataTableColumnGapHarness />
|
|
803
|
+
</PaneInstanceProvider>
|
|
804
|
+
</AppContext>,
|
|
805
|
+
{ width: 8, height: 4 },
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
await act(async () => {
|
|
809
|
+
await testSetup!.renderOnce();
|
|
810
|
+
await testSetup!.renderOnce();
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const frame = testSetup.captureCharFrame();
|
|
814
|
+
expect(frame).toContain("AABB");
|
|
815
|
+
expect(frame).not.toContain("AA BB");
|
|
816
|
+
});
|
|
817
|
+
|
|
740
818
|
test("hides data table horizontal scrolling when content fits", async () => {
|
|
741
819
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
742
820
|
tableScrollBoxForTest = null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneLayout,
|
|
1
|
+
import { cloneLayout, createBlankLayout, type SavedLayout } from "../../../types/config";
|
|
2
2
|
import {
|
|
3
3
|
clonePaneStateMap,
|
|
4
4
|
cloneSavedLayout,
|
|
@@ -96,7 +96,7 @@ export function reduceLayoutAction(state: AppState, action: AppAction): AppState
|
|
|
96
96
|
);
|
|
97
97
|
const newLayout: SavedLayout = {
|
|
98
98
|
name: action.name,
|
|
99
|
-
layout:
|
|
99
|
+
layout: createBlankLayout(),
|
|
100
100
|
paneState: {},
|
|
101
101
|
};
|
|
102
102
|
const layouts = [...currentConfig.layouts, newLayout];
|
|
@@ -85,7 +85,7 @@ export function PickerRow({
|
|
|
85
85
|
return (
|
|
86
86
|
<Box
|
|
87
87
|
flexDirection="column"
|
|
88
|
-
|
|
88
|
+
onMouseOver={onFocus}
|
|
89
89
|
onMouseDown={(event?: { stopPropagation?: () => void; preventDefault?: () => void }) => {
|
|
90
90
|
event?.stopPropagation?.();
|
|
91
91
|
event?.preventDefault?.();
|
|
@@ -135,7 +135,7 @@ export function CheckboxRow({
|
|
|
135
135
|
<Box
|
|
136
136
|
flexDirection="column"
|
|
137
137
|
backgroundColor={hovered ? hoverBg() : undefined}
|
|
138
|
-
|
|
138
|
+
onMouseOver={() => {
|
|
139
139
|
setHovered(true);
|
|
140
140
|
onFocus();
|
|
141
141
|
}}
|
|
@@ -214,7 +214,7 @@ export function AlertsPane({ focused, width, height, close }: PaneProps) {
|
|
|
214
214
|
alert: AlertRule,
|
|
215
215
|
column: AlertColumn,
|
|
216
216
|
_index: number,
|
|
217
|
-
rowState: { selected: boolean
|
|
217
|
+
rowState: { selected: boolean },
|
|
218
218
|
): DataTableCell => {
|
|
219
219
|
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
220
220
|
const actionMouseDown = (handler: () => void) => (
|
|
@@ -293,12 +293,14 @@ describe("PortfolioAnalyticsPane", () => {
|
|
|
293
293
|
currency: "USD",
|
|
294
294
|
source: "flex",
|
|
295
295
|
updatedAt: new Date(2026, 2, 27).getTime(),
|
|
296
|
+
asOfDate: "2026-03-26",
|
|
296
297
|
totalCashValue: -50000,
|
|
297
298
|
settledCash: -45000,
|
|
298
299
|
availableFunds: 15000,
|
|
299
300
|
excessLiquidity: 12000,
|
|
300
301
|
buyingPower: 30000,
|
|
301
302
|
netLiquidation: 125000,
|
|
303
|
+
grossPositionValue: 113636,
|
|
302
304
|
dailyPnl: 900,
|
|
303
305
|
unrealizedPnl: 777,
|
|
304
306
|
realizedPnl: -25,
|
|
@@ -318,6 +320,8 @@ describe("PortfolioAnalyticsPane", () => {
|
|
|
318
320
|
|
|
319
321
|
const frame = testSetup!.captureCharFrame();
|
|
320
322
|
expect(frame).toContain("Net Liq 125k");
|
|
323
|
+
expect(frame).toContain("Val 113.6k");
|
|
324
|
+
expect(frame).toContain("Margin Lev 1.1x");
|
|
321
325
|
expect(frame).toContain("Cash -50k");
|
|
322
326
|
expect(frame).toContain("Day +900");
|
|
323
327
|
expect(frame).toContain("P&L +777");
|
|
@@ -326,8 +330,8 @@ describe("PortfolioAnalyticsPane", () => {
|
|
|
326
330
|
expect(frame).toContain("Avail 15k");
|
|
327
331
|
expect(frame).toContain("Excess 12k");
|
|
328
332
|
expect(frame).toContain("BP 30k");
|
|
329
|
-
expect(frame).toContain("
|
|
330
|
-
expect(frame).toContain("Source Flex Mar
|
|
333
|
+
expect(frame).toContain("As Of Mar 26");
|
|
334
|
+
expect(frame).toContain("Source Flex Mar 26");
|
|
331
335
|
});
|
|
332
336
|
|
|
333
337
|
test("falls back from a Gateway portfolio to a configured Flex profile for IBKR history", async () => {
|