gloomberb 0.6.0 → 0.7.0

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 (156) hide show
  1. package/package.json +2 -1
  2. package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
  3. package/src/app.tsx +9 -6
  4. package/src/cli/registry.test.ts +0 -1
  5. package/src/components/chart/chart-controller.ts +0 -1
  6. package/src/components/chart/chart-data.ts +1 -31
  7. package/src/components/chart/chart-renderer.ts +70 -0
  8. package/src/components/chart/chart-resolution.test.ts +0 -36
  9. package/src/components/chart/chart-resolution.ts +0 -27
  10. package/src/components/chart/chart.test.ts +17 -45
  11. package/src/components/chart/comparison-chart-data.test.ts +1 -5
  12. package/src/components/chart/comparison-chart-data.ts +1 -4
  13. package/src/components/chart/comparison-stock-chart.tsx +62 -77
  14. package/src/components/chart/native/kitty.test.ts +23 -0
  15. package/src/components/chart/native/surface-visibility.ts +48 -0
  16. package/src/components/chart/static-chart-surface.tsx +28 -4
  17. package/src/components/chart/stock-chart.test.ts +0 -16
  18. package/src/components/chart/stock-chart.tsx +52 -71
  19. package/src/components/chart/time-axis-label.tsx +43 -0
  20. package/src/components/command-bar/command-bar.test.tsx +71 -8
  21. package/src/components/command-bar/command-bar.tsx +388 -713
  22. package/src/components/command-bar/confirm-body.tsx +51 -0
  23. package/src/components/command-bar/list-model.ts +77 -0
  24. package/src/components/command-bar/view-model.ts +0 -19
  25. package/src/components/command-bar/workflow-ops.ts +4 -0
  26. package/src/components/data-table-stack-view.test.tsx +39 -4
  27. package/src/components/data-table-stack-view.tsx +11 -104
  28. package/src/components/data-table-view.tsx +43 -79
  29. package/src/components/feed-data-table-stack-view.tsx +10 -17
  30. package/src/components/feed-stack-controller.ts +39 -0
  31. package/src/components/index.ts +6 -2
  32. package/src/components/layout/floating-pane.tsx +30 -2
  33. package/src/components/layout/header.tsx +10 -7
  34. package/src/components/layout/pane-content.tsx +2 -0
  35. package/src/components/layout/pane-cycle-order.test.ts +30 -0
  36. package/src/components/layout/pane-cycle-order.ts +32 -0
  37. package/src/components/layout/pane-footer.tsx +26 -4
  38. package/src/components/layout/pane-header.tsx +5 -5
  39. package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
  40. package/src/components/layout/shell.test.tsx +66 -22
  41. package/src/components/layout/shell.tsx +13 -6
  42. package/src/components/onboarding/onboarding-wizard.tsx +1 -2
  43. package/src/components/pane-settings-dialog.tsx +12 -4
  44. package/src/components/pane-template-wizard.test.tsx +3 -4
  45. package/src/components/speedometer-gauge.test.tsx +45 -0
  46. package/src/components/speedometer-gauge.tsx +558 -0
  47. package/src/components/table-view-shared.tsx +158 -0
  48. package/src/components/ticker-list-table-view.tsx +42 -72
  49. package/src/components/ui/confirm-dialog.tsx +66 -0
  50. package/src/components/ui/data-table.tsx +18 -10
  51. package/src/components/ui/index.ts +0 -1
  52. package/src/components/ui/page-stack-view.tsx +39 -10
  53. package/src/components/ui/remote-image.tsx +15 -40
  54. package/src/components/ui/table-layout.test.ts +16 -0
  55. package/src/components/ui/table-layout.ts +10 -3
  56. package/src/components/ui/ui.test.tsx +100 -20
  57. package/src/core/state/app-state.ts +6 -3
  58. package/src/market-data/coordinator.test.ts +1 -1
  59. package/src/market-data/coordinator.ts +0 -1
  60. package/src/news/aggregator.ts +0 -3
  61. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
  62. package/src/plugins/builtin/ai/providers.ts +0 -17
  63. package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
  64. package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
  65. package/src/plugins/builtin/ask-ai.test.tsx +89 -2
  66. package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
  67. package/src/plugins/builtin/broker-manager/index.tsx +19 -36
  68. package/src/plugins/builtin/chat-controller.test.ts +105 -0
  69. package/src/plugins/builtin/chat-controller.ts +75 -6
  70. package/src/plugins/builtin/chat.test.tsx +6 -6
  71. package/src/plugins/builtin/chat.tsx +29 -20
  72. package/src/plugins/builtin/cloud-tweets.tsx +0 -1
  73. package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
  74. package/src/plugins/builtin/comparison-chart.tsx +11 -5
  75. package/src/plugins/builtin/correlation/index.tsx +1 -6
  76. package/src/plugins/builtin/debug.tsx +1 -3
  77. package/src/plugins/builtin/econ/index.tsx +1 -2
  78. package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
  79. package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
  80. package/src/plugins/builtin/fear-greed/index.tsx +478 -0
  81. package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
  82. package/src/plugins/builtin/help.test.tsx +2 -2
  83. package/src/plugins/builtin/help.tsx +2 -2
  84. package/src/plugins/builtin/holders/index.tsx +10 -15
  85. package/src/plugins/builtin/insider/index.tsx +22 -89
  86. package/src/plugins/builtin/market-movers/index.tsx +1 -1
  87. package/src/plugins/builtin/news-wire/index.tsx +41 -2
  88. package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
  89. package/src/plugins/builtin/news.tsx +4 -16
  90. package/src/plugins/builtin/notes.tsx +18 -55
  91. package/src/plugins/builtin/options.test.tsx +58 -2
  92. package/src/plugins/builtin/options.tsx +108 -49
  93. package/src/plugins/builtin/plugin-groups.ts +2 -0
  94. package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
  95. package/src/plugins/builtin/research/index.tsx +5 -21
  96. package/src/plugins/builtin/sec-filing-display.tsx +84 -0
  97. package/src/plugins/builtin/sec.tsx +13 -66
  98. package/src/plugins/builtin/sectors/index.tsx +1 -1
  99. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
  100. package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
  101. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
  102. package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
  103. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
  104. package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
  105. package/src/plugins/builtin/world-indices/index.tsx +1 -1
  106. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  107. package/src/plugins/ibkr/broker-adapter.ts +1 -3
  108. package/src/plugins/ibkr/gateway-service-native.ts +1 -5
  109. package/src/plugins/ibkr/gateway-service.ts +1 -1
  110. package/src/plugins/ibkr/index.test.tsx +0 -2
  111. package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
  112. package/src/plugins/ibkr/trade-tab.tsx +13 -11
  113. package/src/plugins/pane-manager.ts +0 -13
  114. package/src/plugins/plugin-runtime.test.tsx +6 -6
  115. package/src/plugins/plugin-runtime.tsx +13 -8
  116. package/src/plugins/prediction-markets/cli.ts +0 -1
  117. package/src/plugins/prediction-markets/controller-data.ts +0 -2
  118. package/src/plugins/prediction-markets/pane.test.tsx +3 -11
  119. package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
  120. package/src/plugins/registry.ts +10 -39
  121. package/src/react/input.ts +11 -2
  122. package/src/renderers/electrobun/view/data-table.tsx +28 -6
  123. package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
  124. package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
  125. package/src/renderers/electrobun/view/input-host.tsx +123 -12
  126. package/src/renderers/electrobun/view/key-event.ts +1 -1
  127. package/src/renderers/electrobun/view/styles.css +331 -0
  128. package/src/renderers/electrobun/view/ui-host.tsx +22 -3
  129. package/src/renderers/opentui/chart-surface.tsx +179 -0
  130. package/src/renderers/opentui/host.tsx +6 -0
  131. package/src/renderers/opentui/image-loader.test.ts +25 -0
  132. package/src/renderers/opentui/image-loader.ts +105 -0
  133. package/src/renderers/opentui/image-surface.tsx +266 -0
  134. package/src/renderers/opentui/input-host.tsx +45 -6
  135. package/src/renderers/opentui/start.tsx +48 -30
  136. package/src/renderers/opentui/test-utils.tsx +17 -0
  137. package/src/renderers/opentui/ui-host.tsx +7 -3
  138. package/src/sources/provider-router.ts +0 -4
  139. package/src/state/app-context.test.ts +45 -0
  140. package/src/state/app-context.tsx +4 -0
  141. package/src/state/pane-scroll-registry.tsx +254 -0
  142. package/src/state/use-inline-tickers.test.tsx +73 -0
  143. package/src/state/use-inline-tickers.ts +7 -1
  144. package/src/test-support/plugin-runtime.ts +3 -2
  145. package/src/theme/colors.ts +12 -3
  146. package/src/theme/themes.test.ts +23 -1
  147. package/src/theme/themes.ts +115 -0
  148. package/src/types/broker.ts +1 -1
  149. package/src/types/data-provider.ts +0 -1
  150. package/src/types/plugin.ts +0 -2
  151. package/src/ui/host.tsx +4 -0
  152. package/src/ui/index.tsx +9 -1
  153. package/src/utils/back-navigation.ts +24 -1
  154. package/src/version.ts +1 -1
  155. package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
  156. package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
@@ -1,16 +1,24 @@
1
1
  import {
2
2
  useCallback,
3
3
  useEffect,
4
- useRef,
5
- useState,
6
4
  type ReactNode,
7
5
  type RefObject,
8
6
  } from "react";
9
- import { Box, type ScrollBoxRenderable } from "../ui";
7
+ import type { ScrollBoxRenderable } from "../ui";
10
8
  import { useShortcut } from "../react/input";
11
9
  import { TickerListTable, type TickerListTableProps } from "./ticker-list-table";
12
10
  import type { TickerRecord } from "../types/ticker";
13
11
  import type { DataTableKeyEvent } from "./data-table-view";
12
+ import {
13
+ isNextTableRowKey,
14
+ isPreviousTableRowKey,
15
+ isTableActivationKey,
16
+ stopTableKey,
17
+ TableViewFrame,
18
+ useResetTableScroll,
19
+ useTableBodyScrollActivity,
20
+ useTableViewState,
21
+ } from "./table-view-shared";
14
22
 
15
23
  export interface TickerListVisibleRange {
16
24
  start: number;
@@ -47,23 +55,6 @@ export interface TickerListTableViewProps extends Omit<
47
55
  resetScrollKey?: unknown;
48
56
  }
49
57
 
50
- function isActivationKey(name: string | undefined): boolean {
51
- return name === "enter" || name === "return";
52
- }
53
-
54
- function isNextRowKey(name: string | undefined): boolean {
55
- return name === "j" || name === "down";
56
- }
57
-
58
- function isPreviousRowKey(name: string | undefined): boolean {
59
- return name === "k" || name === "up";
60
- }
61
-
62
- function stopTableKey(event: DataTableKeyEvent) {
63
- event.stopPropagation?.();
64
- event.preventDefault?.();
65
- }
66
-
67
58
  export function TickerListTableView({
68
59
  focused = false,
69
60
  rootBefore,
@@ -86,17 +77,19 @@ export function TickerListTableView({
86
77
  resetScrollKey,
87
78
  ...tableProps
88
79
  }: TickerListTableViewProps) {
89
- const internalHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
90
- const internalScrollRef = useRef<ScrollBoxRenderable>(null);
91
- const [internalHoveredIdx, setInternalHoveredIdx] = useState<number | null>(
92
- null,
93
- );
94
-
95
- const effectiveHeaderScrollRef = headerScrollRef ?? internalHeaderScrollRef;
96
- const effectiveScrollRef = scrollRef ?? internalScrollRef;
97
- const effectiveHoveredIdx =
98
- hoveredIdx !== undefined ? hoveredIdx : internalHoveredIdx;
99
- const effectiveSetHoveredIdx = setHoveredIdx ?? setInternalHoveredIdx;
80
+ const {
81
+ effectiveHeaderScrollRef,
82
+ effectiveScrollRef,
83
+ effectiveHoveredIdx,
84
+ effectiveSetHoveredIdx,
85
+ effectiveSyncHeaderScroll,
86
+ } = useTableViewState({
87
+ headerScrollRef,
88
+ scrollRef,
89
+ syncHeaderScroll,
90
+ hoveredIdx,
91
+ setHoveredIdx,
92
+ });
100
93
  const selectedIndex = tableProps.tickers.findIndex(
101
94
  (ticker) => ticker.metadata.ticker === tableProps.cursorSymbol,
102
95
  );
@@ -113,24 +106,11 @@ export function TickerListTableView({
113
106
  onVisibleRangeChange({ start, end });
114
107
  }, [effectiveScrollRef, onVisibleRangeChange, tableProps.tickers.length, visibleRangeBuffer]);
115
108
 
116
- const defaultSyncHeaderScroll = useCallback(() => {
117
- const body = effectiveScrollRef.current;
118
- const header = effectiveHeaderScrollRef.current;
119
- if (!body || !header) return;
120
- if (header.scrollLeft !== body.scrollLeft) {
121
- header.scrollLeft = body.scrollLeft;
122
- }
123
- }, [effectiveHeaderScrollRef, effectiveScrollRef]);
124
- const effectiveSyncHeaderScroll = syncHeaderScroll ?? defaultSyncHeaderScroll;
125
-
126
- const handleBodyScrollActivity = useCallback(() => {
127
- if (onBodyScrollActivity) {
128
- onBodyScrollActivity();
129
- } else {
130
- queueMicrotask(effectiveSyncHeaderScroll);
131
- }
132
- queueMicrotask(emitVisibleRange);
133
- }, [effectiveSyncHeaderScroll, emitVisibleRange, onBodyScrollActivity]);
109
+ const handleBodyScrollActivity = useTableBodyScrollActivity({
110
+ onBodyScrollActivity,
111
+ syncHeaderScroll: effectiveSyncHeaderScroll,
112
+ afterScroll: emitVisibleRange,
113
+ });
134
114
 
135
115
  useEffect(() => {
136
116
  const scrollBox = effectiveScrollRef.current;
@@ -139,19 +119,12 @@ export function TickerListTableView({
139
119
  emitVisibleRange();
140
120
  }, [emitVisibleRange, effectiveScrollRef, rootHeight, tableProps.tickers.length]);
141
121
 
142
- useEffect(() => {
143
- if (resetScrollKey === undefined) return;
144
- const body = effectiveScrollRef.current;
145
- if (body) {
146
- body.scrollTop = 0;
147
- body.scrollLeft = 0;
148
- }
149
- const header = effectiveHeaderScrollRef.current;
150
- if (header) {
151
- header.scrollLeft = 0;
152
- }
153
- queueMicrotask(emitVisibleRange);
154
- }, [effectiveHeaderScrollRef, effectiveScrollRef, emitVisibleRange, resetScrollKey]);
122
+ useResetTableScroll({
123
+ headerScrollRef: effectiveHeaderScrollRef,
124
+ scrollRef: effectiveScrollRef,
125
+ resetScrollKey,
126
+ afterReset: emitVisibleRange,
127
+ });
155
128
 
156
129
  const selectIndex = useCallback((index: number) => {
157
130
  if (index < 0 || index >= tableProps.tickers.length) return;
@@ -192,19 +165,19 @@ export function TickerListTableView({
192
165
  if (onRootKeyDown?.(event)) return;
193
166
  if (tableProps.tickers.length === 0) return;
194
167
 
195
- if (isNextRowKey(event.name)) {
168
+ if (isNextTableRowKey(event.name)) {
196
169
  stopTableKey(event);
197
170
  selectByOffset(1);
198
171
  return;
199
172
  }
200
173
 
201
- if (isPreviousRowKey(event.name)) {
174
+ if (isPreviousTableRowKey(event.name)) {
202
175
  stopTableKey(event);
203
176
  selectByOffset(-1);
204
177
  return;
205
178
  }
206
179
 
207
- if (isActivationKey(event.name)) {
180
+ if (isTableActivationKey(event.name)) {
208
181
  stopTableKey(event);
209
182
  activateSelection();
210
183
  }
@@ -224,15 +197,13 @@ export function TickerListTableView({
224
197
  }, [effectiveScrollRef, emitVisibleRange, selectedIndex]);
225
198
 
226
199
  return (
227
- <Box
228
- flexDirection="column"
229
- flexGrow={1}
200
+ <TableViewFrame
230
201
  width={rootWidth}
231
202
  height={rootHeight}
232
203
  backgroundColor={rootBackgroundColor}
233
- overflow="hidden"
204
+ before={rootBefore}
205
+ after={rootAfter}
234
206
  >
235
- {rootBefore}
236
207
  <TickerListTable
237
208
  {...tableProps}
238
209
  headerScrollRef={effectiveHeaderScrollRef}
@@ -242,7 +213,6 @@ export function TickerListTableView({
242
213
  hoveredIdx={effectiveHoveredIdx}
243
214
  setHoveredIdx={effectiveSetHoveredIdx}
244
215
  />
245
- {rootAfter}
246
- </Box>
216
+ </TableViewFrame>
247
217
  );
248
218
  }
@@ -0,0 +1,66 @@
1
+ import { useCallback, type ReactNode } from "react";
2
+ import { Box, Text } from "../../ui";
3
+ import { colors } from "../../theme/colors";
4
+ import { type PromptContext, useDialogKeyboard } from "../../ui/dialog";
5
+ import { Button, type ButtonVariant } from "./button";
6
+ import { DialogFrame } from "./frame";
7
+
8
+ export interface ConfirmDialogProps extends PromptContext<boolean> {
9
+ title: string;
10
+ body: ReactNode | string | string[];
11
+ confirmLabel: string;
12
+ cancelLabel?: string;
13
+ confirmVariant?: ButtonVariant;
14
+ width?: number;
15
+ footer?: string;
16
+ }
17
+
18
+ export function ConfirmDialog({
19
+ resolve,
20
+ title,
21
+ body,
22
+ confirmLabel,
23
+ cancelLabel = "Cancel",
24
+ confirmVariant = "danger",
25
+ width = 52,
26
+ footer,
27
+ }: ConfirmDialogProps) {
28
+ const confirm = useCallback(() => resolve(true), [resolve]);
29
+ const cancel = useCallback(() => resolve(false), [resolve]);
30
+
31
+ useDialogKeyboard((event) => {
32
+ event.stopPropagation();
33
+ if (event.name === "enter" || event.name === "return") {
34
+ confirm();
35
+ return;
36
+ }
37
+ if (event.name === "escape") {
38
+ cancel();
39
+ }
40
+ });
41
+
42
+ return (
43
+ <DialogFrame title={title} footer={footer ?? `Enter ${confirmLabel.toLowerCase()} · Esc cancel`}>
44
+ <Box flexDirection="column" width={width}>
45
+ {renderBody(body)}
46
+ <Box height={1} />
47
+ <Box flexDirection="row" gap={1}>
48
+ <Button label={confirmLabel} variant={confirmVariant} onPress={confirm} />
49
+ <Button label={cancelLabel} variant="secondary" onPress={cancel} />
50
+ </Box>
51
+ </Box>
52
+ </DialogFrame>
53
+ );
54
+ }
55
+
56
+ function renderBody(body: ReactNode | string | string[]): ReactNode {
57
+ if (Array.isArray(body)) {
58
+ return body.map((line, index) => (
59
+ <Text key={`${line}:${index}`} fg={index === 0 ? colors.text : colors.textDim}>{line}</Text>
60
+ ));
61
+ }
62
+ if (typeof body === "string") {
63
+ return <Text fg={colors.text}>{body}</Text>;
64
+ }
65
+ return body;
66
+ }
@@ -13,6 +13,7 @@ import { EmptyState } from "./status";
13
13
  import {
14
14
  expandTableColumns,
15
15
  getTableWidth,
16
+ hasMeaningfulTableHorizontalOverflow,
16
17
  tableContentWidthProps,
17
18
  useMeasuredTableContentWidth,
18
19
  } from "./table-layout";
@@ -199,10 +200,16 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
199
200
  ],
200
201
  );
201
202
  const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
202
- const { contentWidth, measureContentWidth } = useMeasuredTableContentWidth(tableWidth, headerScrollRef, scrollRef);
203
+ const {
204
+ contentWidth: measuredContentWidth,
205
+ viewportWidth: measuredViewportWidth,
206
+ measureContentWidth,
207
+ } = useMeasuredTableContentWidth(tableWidth, headerScrollRef, scrollRef);
208
+ const horizontalScrollbarVisible = showHorizontalScrollbar
209
+ && hasMeaningfulTableHorizontalOverflow(tableWidth, measuredViewportWidth);
203
210
  const displayColumns = useMemo(
204
- () => expandTableColumns(columns, contentWidth),
205
- [columns, contentWidth],
211
+ () => expandTableColumns(columns, measuredContentWidth),
212
+ [columns, measuredContentWidth],
206
213
  );
207
214
  const handleRowMouseDown =
208
215
  useDoubleClickActivation<DataTableRowPointerTarget<T>>({
@@ -269,17 +276,18 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
269
276
  useEffect(() => {
270
277
  if (headerScrollRef.current) {
271
278
  headerScrollRef.current.horizontalScrollBar.visible = false;
272
- if (!showHorizontalScrollbar) {
279
+ if (!horizontalScrollbarVisible) {
273
280
  headerScrollRef.current.scrollLeft = 0;
274
281
  }
275
282
  }
276
283
  if (scrollRef.current) {
277
- scrollRef.current.horizontalScrollBar.visible = showHorizontalScrollbar;
278
- if (!showHorizontalScrollbar) {
284
+ scrollRef.current.horizontalScrollBar.visible = horizontalScrollbarVisible;
285
+ scrollRef.current.verticalScrollBar.visible = items.length > scrollRef.current.viewport.height;
286
+ if (!horizontalScrollbarVisible) {
279
287
  scrollRef.current.scrollLeft = 0;
280
288
  }
281
289
  }
282
- }, [columns.length, headerScrollRef, items.length, scrollRef, showHorizontalScrollbar]);
290
+ }, [columns.length, headerScrollRef, horizontalScrollbarVisible, items.length, measuredViewportHeight, scrollRef]);
283
291
 
284
292
  useEffect(() => {
285
293
  if (applyScrollToIndex()) return;
@@ -318,7 +326,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
318
326
  <Box
319
327
  flexDirection="row"
320
328
  height={1}
321
- {...tableContentWidthProps(contentWidth)}
329
+ {...tableContentWidthProps(measuredContentWidth)}
322
330
  paddingX={1}
323
331
  backgroundColor={colors.panel}
324
332
  >
@@ -396,7 +404,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
396
404
  key={getItemKey(item, index)}
397
405
  flexDirection="row"
398
406
  height={1}
399
- {...tableContentWidthProps(contentWidth)}
407
+ {...tableContentWidthProps(measuredContentWidth)}
400
408
  paddingX={1}
401
409
  backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
402
410
  onMouseDown={(event) => {
@@ -433,7 +441,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
433
441
  key={getItemKey(item, index)}
434
442
  flexDirection="row"
435
443
  height={1}
436
- {...tableContentWidthProps(contentWidth)}
444
+ {...tableContentWidthProps(measuredContentWidth)}
437
445
  paddingX={1}
438
446
  backgroundColor={rowBg}
439
447
  onMouseMove={() => {
@@ -6,7 +6,6 @@ export { DataTable } from "./data-table";
6
6
  export type { DataTableCell, DataTableColumn, DataTableProps } from "./data-table";
7
7
 
8
8
  export { Button } from "./button";
9
-
10
9
  export { ShortcutHint } from "./shortcut-hint";
11
10
 
12
11
  export { MultiSelectDialogButton, MultiSelectDialogContent } from "./multi-select-dialog";
@@ -1,8 +1,11 @@
1
1
  import { Box, Text, TextAttributes, useUiHost } from "../../ui";
2
2
  import { useShortcut } from "../../react/input";
3
- import { type ComponentType, type ReactNode } from "react";
3
+ import { useCallback, type ComponentType, type ReactNode } from "react";
4
4
  import { colors } from "../../theme/colors";
5
- import { isDetailBackNavigationKey } from "../../utils/back-navigation";
5
+ import {
6
+ isDetailBackNavigationKey,
7
+ isMouseBackNavigationEvent,
8
+ } from "../../utils/back-navigation";
6
9
 
7
10
  export interface PageStackViewProps {
8
11
  focused: boolean;
@@ -41,13 +44,29 @@ export function PageStackView({
41
44
  );
42
45
  }
43
46
 
47
+ const goBack = useCallback((event?: {
48
+ preventDefault?: () => void;
49
+ stopPropagation?: () => void;
50
+ }) => {
51
+ event?.stopPropagation?.();
52
+ event?.preventDefault?.();
53
+ onBack();
54
+ }, [onBack]);
55
+
44
56
  useShortcut((event) => {
45
57
  if (!focused || !detailOpen || !isDetailBackNavigationKey(event)) return;
46
- event.stopPropagation?.();
47
- event.preventDefault?.();
48
- onBack();
58
+ goBack(event);
49
59
  });
50
60
 
61
+ const handleMouseDown = useCallback((event: {
62
+ button?: unknown;
63
+ preventDefault?: () => void;
64
+ stopPropagation?: () => void;
65
+ }) => {
66
+ if (!focused || !detailOpen || !isMouseBackNavigationEvent(event)) return;
67
+ goBack(event);
68
+ }, [detailOpen, focused, goBack]);
69
+
51
70
  if (!detailOpen) {
52
71
  return (
53
72
  <Box flexDirection="column" flexGrow={1} overflow="hidden">
@@ -57,16 +76,26 @@ export function PageStackView({
57
76
  }
58
77
 
59
78
  return (
60
- <Box flexDirection="column" flexGrow={1} overflow="hidden">
79
+ <Box
80
+ flexDirection="column"
81
+ flexGrow={1}
82
+ overflow="hidden"
83
+ onMouseDown={handleMouseDown}
84
+ >
61
85
  <Box height={1} flexDirection="row">
62
86
  <Box
63
87
  onMouseDown={(event) => {
64
- event.preventDefault?.();
65
- event.stopPropagation?.();
66
- onBack();
88
+ goBack(event);
67
89
  }}
90
+ backgroundColor={colors.selected}
68
91
  >
69
- <Text fg={colors.textBright}>{`← ${backLabel}`}</Text>
92
+ <Text
93
+ fg={colors.selectedText}
94
+ bg={colors.selected}
95
+ attributes={TextAttributes.BOLD}
96
+ >
97
+ {`← ${backLabel}`}
98
+ </Text>
70
99
  </Box>
71
100
  {detailTitle ? (
72
101
  <>
@@ -1,4 +1,4 @@
1
- import { Box, ImageSurface, Text, useUiHost } from "../../ui";
1
+ import { Box, ImageSurface, Text } from "../../ui";
2
2
  import { colors } from "../../theme/colors";
3
3
  import { ExternalLinkText } from "./external-link";
4
4
  import { normalizedHttpUrl } from "../../utils/url";
@@ -25,51 +25,26 @@ export function RemoteImage({
25
25
  height = 12,
26
26
  label = "image",
27
27
  }: RemoteImageProps) {
28
- const ui = useUiHost();
29
28
  const resolvedWidth = Math.max(12, width);
30
29
  const imageUrl = normalizedHttpUrl(src);
31
30
  if (!imageUrl) return null;
32
31
 
33
- if (ui.kind === "desktop-web") {
34
- return (
35
- <ImageSurface
36
- src={imageUrl}
37
- alt={alt}
38
- width={resolvedWidth}
39
- height={Math.max(4, height)}
40
- border
41
- borderColor={colors.border}
42
- backgroundColor={colors.panel}
43
- objectFit="contain"
44
- >
45
- <Box paddingX={1} paddingY={1} flexDirection="column" gap={1}>
46
- <Text fg={colors.textDim}>{label}</Text>
47
- <ExternalLinkText
48
- url={imageUrl}
49
- label={truncate(imageUrl, Math.max(1, resolvedWidth - 2))}
50
- color={colors.textBright}
51
- />
52
- </Box>
53
- </ImageSurface>
54
- );
55
- }
56
-
57
32
  return (
58
- <Box
33
+ <ImageSurface
34
+ src={imageUrl}
35
+ alt={alt}
59
36
  width={resolvedWidth}
60
- minHeight={2}
61
- border
62
- borderColor={colors.border}
63
- backgroundColor={colors.panel}
64
- paddingX={1}
65
- flexDirection="column"
37
+ height={Math.max(4, height)}
38
+ objectFit="contain"
66
39
  >
67
- <Text fg={colors.textDim}>{label}</Text>
68
- <ExternalLinkText
69
- url={imageUrl}
70
- label={truncate(imageUrl, Math.max(1, resolvedWidth - 2))}
71
- color={colors.textBright}
72
- />
73
- </Box>
40
+ <Box flexDirection="column" gap={1}>
41
+ <Text fg={colors.textDim}>{label}</Text>
42
+ <ExternalLinkText
43
+ url={imageUrl}
44
+ label={truncate(imageUrl, resolvedWidth)}
45
+ color={colors.textBright}
46
+ />
47
+ </Box>
48
+ </ImageSurface>
74
49
  );
75
50
  }
@@ -0,0 +1,16 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getTableWidth, hasMeaningfulTableHorizontalOverflow } from "./table-layout";
3
+
4
+ describe("table layout", () => {
5
+ test("detects only meaningful horizontal table overflow", () => {
6
+ const tableWidth = getTableWidth([
7
+ { width: 12 },
8
+ { width: 8 },
9
+ ]);
10
+
11
+ expect(hasMeaningfulTableHorizontalOverflow(tableWidth, 0)).toBe(false);
12
+ expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth)).toBe(false);
13
+ expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 1)).toBe(false);
14
+ expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 2)).toBe(true);
15
+ });
16
+ });
@@ -6,10 +6,16 @@ interface TableWidthColumn {
6
6
  flexGrow?: number;
7
7
  }
8
8
 
9
+ const TRAILING_COLUMN_GUTTER_WIDTH = 1;
10
+
9
11
  export function getTableWidth(columns: readonly TableWidthColumn[]): number {
10
12
  return columns.reduce((sum, column) => sum + column.width + 1, 2);
11
13
  }
12
14
 
15
+ export function hasMeaningfulTableHorizontalOverflow(tableWidth: number, viewportWidth: number): boolean {
16
+ return viewportWidth > 0 && tableWidth - viewportWidth > TRAILING_COLUMN_GUTTER_WIDTH;
17
+ }
18
+
13
19
  export function expandTableColumns<C extends TableWidthColumn>(
14
20
  columns: readonly C[],
15
21
  targetWidth: number,
@@ -34,9 +40,9 @@ export function useMeasuredTableContentWidth(
34
40
  const [viewportWidth, setViewportWidth] = useState(0);
35
41
 
36
42
  const measureContentWidth = useCallback(() => {
37
- const nextWidth = scrollRef?.current?.viewport?.width
38
- ?? headerScrollRef?.current?.viewport?.width
39
- ?? 0;
43
+ const bodyWidth = scrollRef?.current?.viewport?.width || scrollRef?.current?.width || 0;
44
+ const headerWidth = headerScrollRef?.current?.viewport?.width || headerScrollRef?.current?.width || 0;
45
+ const nextWidth = bodyWidth || headerWidth;
40
46
  setViewportWidth((current) => current === nextWidth ? current : nextWidth);
41
47
  }, [headerScrollRef, scrollRef]);
42
48
  const scheduleContentWidthMeasure = useCallback(() => {
@@ -48,6 +54,7 @@ export function useMeasuredTableContentWidth(
48
54
 
49
55
  return {
50
56
  contentWidth: Math.max(tableWidth, viewportWidth),
57
+ viewportWidth,
51
58
  measureContentWidth: scheduleContentWidthMeasure,
52
59
  };
53
60
  }