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,8 +1,6 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
2
1
|
import { AsciiText, Box, Span, Strong, Text, TextAttributes, useUiHost } from "../../ui";
|
|
3
2
|
import { colors } from "../../theme/colors";
|
|
4
3
|
import { themes } from "../../theme/themes";
|
|
5
|
-
import type { PluginRegistry } from "../../plugins/registry";
|
|
6
4
|
import { detectShortcutPlatform, formatPrimaryShortcut, getShortcutDisplayMode } from "../../utils/shortcut-labels";
|
|
7
5
|
export { PortfolioStep, type PortfolioSub } from "./portfolio-step";
|
|
8
6
|
|
|
@@ -95,7 +93,7 @@ export function ThemeStep({ themeIds, selectedIdx, height }: { themeIds: string[
|
|
|
95
93
|
);
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
export function ShortcutsStep(
|
|
96
|
+
export function ShortcutsStep() {
|
|
99
97
|
const uiHost = useUiHost();
|
|
100
98
|
const shortcutPlatform = detectShortcutPlatform();
|
|
101
99
|
const shortcutDisplayMode = getShortcutDisplayMode(uiHost.kind);
|
|
@@ -109,30 +107,12 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
|
|
|
109
107
|
{ key: "q", desc: "Quit" },
|
|
110
108
|
];
|
|
111
109
|
|
|
112
|
-
const commandPrefixes =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{ key: "HELP", desc: "Open the help window" },
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
const builtInKeys = new Set(builtIn.map((entry) => entry.key.split(" ")[0]));
|
|
122
|
-
const pluginPrefixes: { key: string; desc: string }[] = [];
|
|
123
|
-
|
|
124
|
-
for (const [, template] of pluginRegistry.paneTemplates) {
|
|
125
|
-
if (!template.shortcut) continue;
|
|
126
|
-
if (builtInKeys.has(template.shortcut.prefix)) continue;
|
|
127
|
-
const label = template.shortcut.argPlaceholder
|
|
128
|
-
? `${template.shortcut.prefix} <${template.shortcut.argPlaceholder}>`
|
|
129
|
-
: template.shortcut.prefix;
|
|
130
|
-
pluginPrefixes.push({ key: label, desc: template.label });
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
pluginPrefixes.sort((a, b) => a.key.localeCompare(b.key));
|
|
134
|
-
return [...builtIn, ...pluginPrefixes];
|
|
135
|
-
}, [pluginRegistry]);
|
|
110
|
+
const commandPrefixes = [
|
|
111
|
+
{ key: "DES AAPL", desc: "Open security details" },
|
|
112
|
+
{ key: "TH", desc: "Switch theme" },
|
|
113
|
+
{ key: "PL", desc: "Toggle plugins" },
|
|
114
|
+
{ key: "HELP", desc: "Open the help window" },
|
|
115
|
+
];
|
|
136
116
|
|
|
137
117
|
const COL = 20;
|
|
138
118
|
|
|
@@ -152,7 +132,7 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
|
|
|
152
132
|
|
|
153
133
|
<Box height={2} />
|
|
154
134
|
<Box height={1}>
|
|
155
|
-
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"
|
|
135
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{"Basic command prefixes"}</Text>
|
|
156
136
|
</Box>
|
|
157
137
|
<Box height={1} />
|
|
158
138
|
<Box height={1}>
|
|
@@ -93,11 +93,14 @@ afterEach(async () => {
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
describe("OnboardingWizard", () => {
|
|
96
|
-
test("
|
|
96
|
+
test("skips manual portfolio naming and shows compact starter shortcuts", async () => {
|
|
97
97
|
const pluginRegistry = {
|
|
98
98
|
allPlugins: new Map(),
|
|
99
99
|
brokers: new Map(),
|
|
100
|
-
paneTemplates: new Map(
|
|
100
|
+
paneTemplates: new Map([[
|
|
101
|
+
"ai",
|
|
102
|
+
{ label: "AI Screener", shortcut: { prefix: "AI", argPlaceholder: "prompt" } },
|
|
103
|
+
]]),
|
|
101
104
|
getPaneTemplatePluginId: () => undefined,
|
|
102
105
|
tickerRepository: createTickerRepository(),
|
|
103
106
|
persistence: { resources: undefined },
|
|
@@ -132,16 +135,14 @@ describe("OnboardingWizard", () => {
|
|
|
132
135
|
expect(frame).toContain("Create Manual Portfolio");
|
|
133
136
|
|
|
134
137
|
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
135
|
-
frame = await waitForFrameToContain("
|
|
136
|
-
expect(frame).toContain("
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
frame
|
|
143
|
-
expect(frame).toContain("Main Portfoli");
|
|
144
|
-
expect(frame).not.toContain("Create Manual Portfolio");
|
|
138
|
+
frame = await waitForFrameToContain("After launch shortcuts");
|
|
139
|
+
expect(frame).toContain("Basic command prefixes");
|
|
140
|
+
expect(frame).toContain("DES AAPL");
|
|
141
|
+
expect(frame).toContain("HELP");
|
|
142
|
+
expect(frame).not.toContain("Name your portfolio");
|
|
143
|
+
expect(frame).not.toContain("Command-bar prefixes");
|
|
144
|
+
expect(frame).not.toContain("AI <prompt>");
|
|
145
|
+
expect(frame).not.toContain("AI Screener");
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test("waits for broker positions to import before completing onboarding", async () => {
|
|
@@ -36,7 +36,7 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
36
36
|
const [themeIdx, setThemeIdx] = useState(0);
|
|
37
37
|
const [portfolioSub, setPortfolioSub] = useState<PortfolioSub>("choose");
|
|
38
38
|
const [portfolioOptionIdx, setPortfolioOptionIdx] = useState(0);
|
|
39
|
-
const
|
|
39
|
+
const portfolioName = "Main Portfolio";
|
|
40
40
|
const [brokerValues, setBrokerValues] = useState<Record<string, Record<string, string>>>({});
|
|
41
41
|
const [selectedBrokerId, setSelectedBrokerId] = useState<string | null>(null);
|
|
42
42
|
const [brokerFieldIdx, setBrokerFieldIdx] = useState(0);
|
|
@@ -255,8 +255,6 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
255
255
|
choices={portfolioChoices}
|
|
256
256
|
optionIdx={portfolioOptionIdx}
|
|
257
257
|
onOptionSelect={setPortfolioOptionIdx}
|
|
258
|
-
portfolioName={portfolioName}
|
|
259
|
-
onNameChange={setPortfolioName}
|
|
260
258
|
selectedBrokerId={selectedBrokerId}
|
|
261
259
|
brokerFields={activeBrokerFields}
|
|
262
260
|
brokerFieldIdx={brokerFieldIdx}
|
|
@@ -270,7 +268,7 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
270
268
|
/>
|
|
271
269
|
)}
|
|
272
270
|
{step === "shortcuts" && (
|
|
273
|
-
<ShortcutsStep
|
|
271
|
+
<ShortcutsStep />
|
|
274
272
|
)}
|
|
275
273
|
{step === "ready" && (
|
|
276
274
|
<ReadyStep
|
|
@@ -2,7 +2,6 @@ import { BrokerFieldsPanel } from "./broker-fields-panel";
|
|
|
2
2
|
import { BrokerSetupPanel } from "./broker-setup-panel";
|
|
3
3
|
import { BrokerSyncPanel } from "./broker-sync-panel";
|
|
4
4
|
import { PortfolioChoicePanel } from "./choose-panel";
|
|
5
|
-
import { ManualPortfolioNamePanel } from "./manual-name-panel";
|
|
6
5
|
import type { PortfolioStepProps, PortfolioSub } from "./types";
|
|
7
6
|
|
|
8
7
|
export type { PortfolioSub };
|
|
@@ -18,17 +17,6 @@ export function PortfolioStep(props: PortfolioStepProps) {
|
|
|
18
17
|
);
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
if (props.sub === "manual-name") {
|
|
22
|
-
return (
|
|
23
|
-
<ManualPortfolioNamePanel
|
|
24
|
-
portfolioName={props.portfolioName}
|
|
25
|
-
onNameChange={props.onNameChange}
|
|
26
|
-
editing={props.editing}
|
|
27
|
-
inputRef={props.inputRef}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
20
|
if (props.sub === "broker-setup" && props.selectedBrokerId) {
|
|
33
21
|
return (
|
|
34
22
|
<BrokerSetupPanel
|
|
@@ -3,15 +3,13 @@ import type { InputRenderable } from "../../../ui";
|
|
|
3
3
|
import type { BrokerConfigField } from "../../../types/broker";
|
|
4
4
|
import type { ListViewItem } from "../../ui";
|
|
5
5
|
|
|
6
|
-
export type PortfolioSub = "choose" | "
|
|
6
|
+
export type PortfolioSub = "choose" | "broker-setup" | "broker-fields" | "broker-sync";
|
|
7
7
|
|
|
8
8
|
export interface PortfolioStepProps {
|
|
9
9
|
sub: PortfolioSub;
|
|
10
10
|
choices: ListViewItem[];
|
|
11
11
|
optionIdx: number;
|
|
12
12
|
onOptionSelect: (idx: number) => void;
|
|
13
|
-
portfolioName: string;
|
|
14
|
-
onNameChange: (name: string) => void;
|
|
15
13
|
selectedBrokerId: string | null;
|
|
16
14
|
brokerFields: BrokerConfigField[];
|
|
17
15
|
brokerFieldIdx: number;
|
|
@@ -74,9 +74,7 @@ export function useOnboardingKeyboard({
|
|
|
74
74
|
if (editingField) {
|
|
75
75
|
if (event.name === "return") {
|
|
76
76
|
setEditingField(false);
|
|
77
|
-
if (portfolioSub === "
|
|
78
|
-
nextStep();
|
|
79
|
-
} else if (portfolioSub === "broker-fields" && selectedBrokerId) {
|
|
77
|
+
if (portfolioSub === "broker-fields" && selectedBrokerId) {
|
|
80
78
|
const currentField = activeBrokerFields[brokerFieldIdx];
|
|
81
79
|
if (!currentField) {
|
|
82
80
|
void syncSelectedBroker();
|
|
@@ -125,8 +123,7 @@ export function useOnboardingKeyboard({
|
|
|
125
123
|
if (choice.id === "manual") {
|
|
126
124
|
resetBrokerSync();
|
|
127
125
|
setSelectedBrokerId(null);
|
|
128
|
-
|
|
129
|
-
setEditingField(true);
|
|
126
|
+
nextStep();
|
|
130
127
|
} else {
|
|
131
128
|
resetBrokerSync();
|
|
132
129
|
setSelectedBrokerId(choice.id);
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
type ReactNode,
|
|
7
7
|
type RefObject,
|
|
8
8
|
} from "react";
|
|
9
|
-
import { Box, type ScrollBoxRenderable } from "../ui";
|
|
9
|
+
import { Box, useUiCapabilities, type ScrollBoxRenderable } from "../ui";
|
|
10
10
|
import { isPlainKeyboardEvent } from "../utils/keyboard";
|
|
11
11
|
|
|
12
12
|
function listenToScrollBarChange(
|
|
@@ -56,15 +56,23 @@ export function TableViewFrame({
|
|
|
56
56
|
after,
|
|
57
57
|
children,
|
|
58
58
|
}: TableViewFrameProps) {
|
|
59
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
60
|
+
const nativeFlexibleFrame = nativePaneChrome === true;
|
|
61
|
+
|
|
59
62
|
return (
|
|
60
63
|
<Box
|
|
61
64
|
flexDirection="column"
|
|
62
|
-
flexGrow={height == null ? 1 : undefined}
|
|
63
|
-
flexShrink={height == null ? undefined : 0}
|
|
65
|
+
flexGrow={nativeFlexibleFrame || height == null ? 1 : undefined}
|
|
66
|
+
flexShrink={nativeFlexibleFrame ? 1 : height == null ? undefined : 0}
|
|
67
|
+
flexBasis={nativeFlexibleFrame ? 0 : undefined}
|
|
64
68
|
width={width}
|
|
65
|
-
height={height}
|
|
69
|
+
height={nativeFlexibleFrame ? undefined : height}
|
|
70
|
+
minWidth={nativeFlexibleFrame ? 0 : undefined}
|
|
71
|
+
minHeight={nativeFlexibleFrame ? 0 : undefined}
|
|
72
|
+
maxHeight={nativeFlexibleFrame ? "100%" : undefined}
|
|
66
73
|
backgroundColor={backgroundColor}
|
|
67
74
|
overflow="hidden"
|
|
75
|
+
data-gloom-role="table-view-frame"
|
|
68
76
|
>
|
|
69
77
|
{before}
|
|
70
78
|
{children}
|
|
@@ -48,6 +48,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
48
48
|
isSelected,
|
|
49
49
|
onSelect,
|
|
50
50
|
onActivate,
|
|
51
|
+
onTableMouseDown,
|
|
51
52
|
onRowMouseDown,
|
|
52
53
|
onRowContextMenu,
|
|
53
54
|
rowContextMenuSurface = false,
|
|
@@ -60,6 +61,9 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
60
61
|
emptyStateHint,
|
|
61
62
|
virtualize = true,
|
|
62
63
|
overscan = 3,
|
|
64
|
+
columnGap = 1,
|
|
65
|
+
horizontalPadding = 1,
|
|
66
|
+
fillAvailableWidth = true,
|
|
63
67
|
showHorizontalScrollbar = true,
|
|
64
68
|
scrollToIndex,
|
|
65
69
|
scrollToIndexAlign = "nearest",
|
|
@@ -105,17 +109,21 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
105
109
|
],
|
|
106
110
|
);
|
|
107
111
|
const { endIndex, startIndex, viewportHeight, visibleItems } = tableWindow;
|
|
108
|
-
const tableWidth = useMemo(
|
|
112
|
+
const tableWidth = useMemo(
|
|
113
|
+
() => getTableWidth(columns, columnGap, horizontalPadding),
|
|
114
|
+
[columnGap, columns, horizontalPadding],
|
|
115
|
+
);
|
|
109
116
|
const {
|
|
110
117
|
contentWidth: measuredContentWidth,
|
|
111
118
|
viewportWidth: measuredViewportWidth,
|
|
112
119
|
measureContentWidth,
|
|
113
120
|
} = useMeasuredTableContentWidth(tableWidth, headerScrollRef, scrollRef);
|
|
114
121
|
const horizontalScrollbarVisible = showHorizontalScrollbar
|
|
115
|
-
&& hasMeaningfulTableHorizontalOverflow(tableWidth, measuredViewportWidth);
|
|
122
|
+
&& hasMeaningfulTableHorizontalOverflow(tableWidth, measuredViewportWidth, columnGap);
|
|
123
|
+
const contentWidth = fillAvailableWidth ? measuredContentWidth : tableWidth;
|
|
116
124
|
const displayColumns = useMemo(
|
|
117
|
-
() => expandTableColumns(columns,
|
|
118
|
-
[columns,
|
|
125
|
+
() => expandTableColumns(columns, contentWidth, columnGap, horizontalPadding),
|
|
126
|
+
[columnGap, columns, contentWidth, horizontalPadding],
|
|
119
127
|
);
|
|
120
128
|
const handleRowMouseDown =
|
|
121
129
|
useDoubleClickActivation<DataTableRowPointerTarget<T>>({
|
|
@@ -264,8 +272,8 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
264
272
|
<Box
|
|
265
273
|
flexDirection="row"
|
|
266
274
|
height={1}
|
|
267
|
-
{...tableContentWidthProps(
|
|
268
|
-
paddingX={
|
|
275
|
+
{...tableContentWidthProps(contentWidth)}
|
|
276
|
+
paddingX={horizontalPadding}
|
|
269
277
|
backgroundColor={colors.panel}
|
|
270
278
|
>
|
|
271
279
|
{displayColumns.map((column) => {
|
|
@@ -283,10 +291,11 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
283
291
|
return (
|
|
284
292
|
<Box
|
|
285
293
|
key={column.id}
|
|
286
|
-
width={column.width +
|
|
294
|
+
width={column.width + columnGap}
|
|
287
295
|
backgroundColor={column.headerBackgroundColor ?? colors.panel}
|
|
288
296
|
onMouseDown={(event: any) => {
|
|
289
297
|
focusPane();
|
|
298
|
+
onTableMouseDown?.(event);
|
|
290
299
|
event.preventDefault();
|
|
291
300
|
onHeaderClick(column.id);
|
|
292
301
|
}}
|
|
@@ -315,6 +324,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
315
324
|
focusable={false}
|
|
316
325
|
onMouseDown={() => {
|
|
317
326
|
focusPane();
|
|
327
|
+
onTableMouseDown?.({});
|
|
318
328
|
}}
|
|
319
329
|
onSizeChange={measureContentWidth}
|
|
320
330
|
>
|
|
@@ -339,11 +349,12 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
339
349
|
key={getItemKey(item, index)}
|
|
340
350
|
flexDirection="row"
|
|
341
351
|
height={1}
|
|
342
|
-
{...tableContentWidthProps(
|
|
343
|
-
paddingX={
|
|
352
|
+
{...tableContentWidthProps(contentWidth)}
|
|
353
|
+
paddingX={horizontalPadding}
|
|
344
354
|
backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
|
|
345
355
|
onMouseDown={(event: any) => {
|
|
346
356
|
focusPane();
|
|
357
|
+
onTableMouseDown?.(event);
|
|
347
358
|
event.preventDefault();
|
|
348
359
|
}}
|
|
349
360
|
>
|
|
@@ -376,8 +387,8 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
376
387
|
key={getItemKey(item, index)}
|
|
377
388
|
flexDirection="row"
|
|
378
389
|
height={1}
|
|
379
|
-
{...tableContentWidthProps(
|
|
380
|
-
paddingX={
|
|
390
|
+
{...tableContentWidthProps(contentWidth)}
|
|
391
|
+
paddingX={horizontalPadding}
|
|
381
392
|
backgroundColor={rowBg}
|
|
382
393
|
data-gloom-context-menu-surface={rowContextMenuSurface ? "true" : undefined}
|
|
383
394
|
onMouseMove={() => {
|
|
@@ -385,6 +396,7 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
385
396
|
}}
|
|
386
397
|
onMouseDown={(event: any) => {
|
|
387
398
|
focusPane();
|
|
399
|
+
onTableMouseDown?.(event);
|
|
388
400
|
if (onRowMouseDown?.(item, index, event) === true) {
|
|
389
401
|
return;
|
|
390
402
|
}
|
|
@@ -404,10 +416,11 @@ export function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>
|
|
|
404
416
|
return (
|
|
405
417
|
<Box
|
|
406
418
|
key={column.id}
|
|
407
|
-
width={column.width +
|
|
419
|
+
width={column.width + columnGap}
|
|
408
420
|
backgroundColor={cell.backgroundColor ?? rowBg}
|
|
409
421
|
onMouseDown={(event: any) => {
|
|
410
422
|
focusPane();
|
|
423
|
+
onTableMouseDown?.(event);
|
|
411
424
|
if (cell.onMouseDown) {
|
|
412
425
|
cell.onMouseDown(event);
|
|
413
426
|
return;
|
|
@@ -50,6 +50,7 @@ export interface DataTableProps<
|
|
|
50
50
|
isSelected: (item: T, index: number) => boolean;
|
|
51
51
|
onSelect: (item: T, index: number) => void;
|
|
52
52
|
onActivate?: (item: T, index: number) => void;
|
|
53
|
+
onTableMouseDown?: (event: any) => void;
|
|
53
54
|
onRowMouseDown?: (item: T, index: number, event: any) => boolean | void;
|
|
54
55
|
onRowContextMenu?: (item: T, index: number, event: any) => void;
|
|
55
56
|
rowContextMenuSurface?: boolean;
|
|
@@ -74,6 +75,9 @@ export interface DataTableProps<
|
|
|
74
75
|
emptyStateHint?: string;
|
|
75
76
|
virtualize?: boolean;
|
|
76
77
|
overscan?: number;
|
|
78
|
+
columnGap?: number;
|
|
79
|
+
horizontalPadding?: number;
|
|
80
|
+
fillAvailableWidth?: boolean;
|
|
77
81
|
showHorizontalScrollbar?: boolean;
|
|
78
82
|
scrollToIndex?: number | null;
|
|
79
83
|
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
|
}
|
|
@@ -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
|
}
|