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.
Files changed (137) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/app/runtime/desktop-menu.ts +5 -1
  4. package/src/app/runtime/ticker-refresh.ts +4 -1
  5. package/src/app.tsx +10 -1
  6. package/src/brokers/account-cache.ts +1 -1
  7. package/src/brokers/sync-broker-instance.test.ts +107 -0
  8. package/src/brokers/sync-broker-instance.ts +22 -10
  9. package/src/cli/desktop-pane-shot.ts +2 -1
  10. package/src/cli/pane-functions/screenshot.ts +13 -1
  11. package/src/components/chart/bar-chart-renderer.test.ts +108 -3
  12. package/src/components/chart/bar-chart-renderer.ts +226 -27
  13. package/src/components/chart/core/pointer.ts +32 -6
  14. package/src/components/chart/price-axis-labels.tsx +3 -1
  15. package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
  16. package/src/components/chart/static/bar-chart-surface.tsx +151 -28
  17. package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
  18. package/src/components/chart/static/chart/surface.test.tsx +92 -0
  19. package/src/components/chart/static/chart/surface.tsx +167 -24
  20. package/src/components/chart/stock-chart/index.test.ts +25 -0
  21. package/src/components/command-bar/layout-items/current-layout.ts +5 -1
  22. package/src/components/layout/detached-pane-shell.tsx +6 -3
  23. package/src/components/layout/floating-pane.tsx +3 -2
  24. package/src/components/layout/pane/content.tsx +20 -8
  25. package/src/components/layout/pane/footer/registration.tsx +6 -2
  26. package/src/components/layout/pane/frame.tsx +4 -4
  27. package/src/components/layout/pane/header.tsx +40 -4
  28. package/src/components/layout/pane/index.tsx +3 -1
  29. package/src/components/layout/pane/sizing.test.ts +43 -0
  30. package/src/components/layout/pane/sizing.ts +13 -4
  31. package/src/components/layout/shell/cursor-occlusion.ts +196 -0
  32. package/src/components/layout/shell/drag/runtime.ts +9 -2
  33. package/src/components/layout/shell/index.test.tsx +92 -2
  34. package/src/components/layout/shell/index.tsx +37 -2
  35. package/src/components/layout/shell/layout-state.test.ts +33 -0
  36. package/src/components/layout/shell/layout-state.ts +4 -9
  37. package/src/components/layout/shell/pane/layers.tsx +8 -2
  38. package/src/components/layout/shell/visible-layout.ts +17 -0
  39. package/src/components/layout/status-bar.test.tsx +6 -1
  40. package/src/components/layout/status-bar.tsx +5 -1
  41. package/src/components/onboarding/onboarding-steps.tsx +8 -28
  42. package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
  43. package/src/components/onboarding/onboarding-wizard.tsx +2 -4
  44. package/src/components/onboarding/portfolio-step/index.tsx +0 -12
  45. package/src/components/onboarding/portfolio-step/types.ts +1 -3
  46. package/src/components/onboarding/wizard-keyboard.ts +2 -5
  47. package/src/components/table-view-shared.tsx +12 -4
  48. package/src/components/ui/data-table/opentui/index.tsx +25 -12
  49. package/src/components/ui/data-table/types.ts +4 -0
  50. package/src/components/ui/fields.tsx +10 -3
  51. package/src/components/ui/page-stack-view.tsx +6 -2
  52. package/src/components/ui/table-layout.test.ts +25 -0
  53. package/src/components/ui/table-layout.ts +22 -6
  54. package/src/components/ui/tabs.tsx +26 -10
  55. package/src/components/ui/ui.test.tsx +87 -0
  56. package/src/core/state/app/layout-reducer.ts +2 -2
  57. package/src/plugins/builtin/analytics/index.test.tsx +6 -2
  58. package/src/plugins/builtin/analytics/pane-model.ts +43 -8
  59. package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
  60. package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
  61. package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
  62. package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
  63. package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
  64. package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
  65. package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
  66. package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
  67. package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
  68. package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
  69. package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
  70. package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
  71. package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
  72. package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
  73. package/src/plugins/builtin/layout-manager/index.tsx +5 -1
  74. package/src/plugins/builtin/options/view.test.tsx +141 -4
  75. package/src/plugins/builtin/options/view.tsx +76 -15
  76. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
  77. package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
  78. package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
  79. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -0
  80. package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
  81. package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
  82. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
  83. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
  84. package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
  85. package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
  86. package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
  87. package/src/plugins/catalog-ui.ts +2 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +19 -0
  89. package/src/plugins/ibkr/config.test.ts +31 -2
  90. package/src/plugins/ibkr/config.ts +9 -2
  91. package/src/plugins/ibkr/flex/index.test.ts +18 -9
  92. package/src/plugins/ibkr/flex/index.ts +30 -0
  93. package/src/plugins/ibkr/flex/statement-client.ts +68 -14
  94. package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
  95. package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
  96. package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
  97. package/src/plugins/pane-manager/gridlock.ts +13 -5
  98. package/src/plugins/pane-manager/layout-state.ts +25 -0
  99. package/src/plugins/pane-manager.test.ts +33 -0
  100. package/src/plugins/pane-manager.ts +6 -1
  101. package/src/plugins/prediction-markets/chart.tsx +10 -4
  102. package/src/plugins/prediction-markets/detail/book.tsx +141 -96
  103. package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
  104. package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
  105. package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
  106. package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
  107. package/src/plugins/prediction-markets/detail.test.tsx +50 -0
  108. package/src/plugins/prediction-markets/pane.test.tsx +43 -0
  109. package/src/plugins/prediction-markets/pane.tsx +50 -22
  110. package/src/plugins/prediction-markets/test-helpers.tsx +41 -23
  111. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
  112. package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
  113. package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
  114. package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
  115. package/src/renderers/electrobun/view/host/input.tsx +115 -18
  116. package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
  117. package/src/renderers/electrobun/view/styles.css +11 -0
  118. package/src/renderers/opentui/host.tsx +17 -1
  119. package/src/renderers/opentui/ui-host.tsx +57 -25
  120. package/src/sources/provider-router/batches.ts +16 -2
  121. package/src/sources/provider-router/financial-routes.ts +5 -0
  122. package/src/sources/provider-router/financials.ts +11 -23
  123. package/src/sources/provider-router/index.test.ts +189 -0
  124. package/src/sources/provider-router/primary.ts +3 -2
  125. package/src/sources/sec-edgar.test.ts +83 -0
  126. package/src/sources/sec-edgar.ts +231 -0
  127. package/src/sources/yahoo-finance.test.ts +51 -0
  128. package/src/sources/yahoo-finance.ts +49 -1
  129. package/src/state/app/context/index.test.tsx +5 -8
  130. package/src/types/broker.ts +1 -0
  131. package/src/types/config.ts +10 -0
  132. package/src/types/trading.ts +2 -0
  133. package/src/ui/host.tsx +16 -0
  134. package/src/ui/index.tsx +2 -0
  135. package/src/utils/financial-statements.ts +23 -0
  136. package/src/version.ts +1 -1
  137. 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({ pluginRegistry }: { pluginRegistry: PluginRegistry }) {
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 = useMemo(() => {
113
- const builtIn = [
114
- { key: "DES AAPL", desc: "Open security details" },
115
- { key: "TH", desc: "Switch theme" },
116
- { key: "PL", desc: "Toggle plugins" },
117
- { key: "PS", desc: "Edit the current window settings" },
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}>{"Command-bar prefixes"}</Text>
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("uses backspace to go back when not editing and keeps backspace for text deletion while editing", async () => {
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("Main Portfolio");
136
- expect(frame).toContain("Main Portfolio");
137
-
138
- await act(async () => {
139
- testSetup!.mockInput.pressBackspace();
140
- await testSetup!.renderOnce();
141
- });
142
- frame = testSetup.captureCharFrame();
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 [portfolioName, setPortfolioName] = useState("Main Portfolio");
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 pluginRegistry={pluginRegistry} />
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" | "manual-name" | "broker-setup" | "broker-fields" | "broker-sync";
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 === "manual-name") {
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
- setPortfolioSub("manual-name");
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(() => getTableWidth(columns), [columns]);
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, measuredContentWidth),
118
- [columns, measuredContentWidth],
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(measuredContentWidth)}
268
- paddingX={1}
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 + 1}
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(measuredContentWidth)}
343
- paddingX={1}
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(measuredContentWidth)}
380
- paddingX={1}
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 + 1}
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(columns: readonly TableWidthColumn[]): number {
14
- return columns.reduce((sum, column) => sum + column.width + 1, 2);
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(tableWidth: number, viewportWidth: number): boolean {
18
- return viewportWidth > 0 && tableWidth - viewportWidth > TRAILING_COLUMN_GUTTER_WIDTH;
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(columns: readonly TableWidthColumn[]): string {
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
  }