gloomberb 0.6.1 → 0.7.1
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/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import { Box, ScrollBox, Text, useUiHost } from "../../ui";
|
|
1
|
+
import { Box, ScrollBox, Text, useNativeRenderer, useUiHost } from "../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState, type ComponentType, type ReactNode, type RefObject } from "react";
|
|
3
3
|
import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
|
|
4
4
|
import { colors, hoverBg } from "../../theme/colors";
|
|
5
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
5
6
|
import type { ColumnConfig } from "../../types/config";
|
|
6
7
|
import { useAppDispatch, usePaneInstance } from "../../state/app-context";
|
|
7
8
|
import { useViewport } from "../../react/input";
|
|
8
9
|
import { padTo } from "../../utils/format";
|
|
9
|
-
import { useRafCallback } from "../../react/use-raf-callback";
|
|
10
10
|
import { measurePerf } from "../../utils/perf-marks";
|
|
11
11
|
import { useDoubleClickActivation } from "../use-double-click-activation";
|
|
12
|
+
import { useScrollBoxScrollActivity } from "../table-view-shared";
|
|
12
13
|
import { EmptyState } from "./status";
|
|
13
14
|
import {
|
|
14
15
|
expandTableColumns,
|
|
15
16
|
getTableWidth,
|
|
17
|
+
hasMeaningfulTableHorizontalOverflow,
|
|
16
18
|
tableContentWidthProps,
|
|
17
19
|
useMeasuredTableContentWidth,
|
|
18
20
|
} from "./table-layout";
|
|
@@ -154,9 +156,11 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
154
156
|
scrollToIndexAlign = "nearest",
|
|
155
157
|
scrollToIndexVersion = 0,
|
|
156
158
|
}: DataTableProps<T, C>) {
|
|
159
|
+
useThemeColors();
|
|
157
160
|
const dispatch = useAppDispatch();
|
|
158
161
|
const paneInstanceId = usePaneInstance()?.instanceId ?? null;
|
|
159
162
|
const appViewport = useViewport();
|
|
163
|
+
const nativeRenderer = useNativeRenderer();
|
|
160
164
|
const [scrollVersion, setScrollVersion] = useState(0);
|
|
161
165
|
const scrollTop = virtualize ? (scrollRef.current?.scrollTop ?? 0) : 0;
|
|
162
166
|
const measuredViewportHeight = scrollRef.current?.viewport?.height;
|
|
@@ -199,10 +203,16 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
199
203
|
],
|
|
200
204
|
);
|
|
201
205
|
const tableWidth = useMemo(() => getTableWidth(columns), [columns]);
|
|
202
|
-
const {
|
|
206
|
+
const {
|
|
207
|
+
contentWidth: measuredContentWidth,
|
|
208
|
+
viewportWidth: measuredViewportWidth,
|
|
209
|
+
measureContentWidth,
|
|
210
|
+
} = useMeasuredTableContentWidth(tableWidth, headerScrollRef, scrollRef);
|
|
211
|
+
const horizontalScrollbarVisible = showHorizontalScrollbar
|
|
212
|
+
&& hasMeaningfulTableHorizontalOverflow(tableWidth, measuredViewportWidth);
|
|
203
213
|
const displayColumns = useMemo(
|
|
204
|
-
() => expandTableColumns(columns,
|
|
205
|
-
[columns,
|
|
214
|
+
() => expandTableColumns(columns, measuredContentWidth),
|
|
215
|
+
[columns, measuredContentWidth],
|
|
206
216
|
);
|
|
207
217
|
const handleRowMouseDown =
|
|
208
218
|
useDoubleClickActivation<DataTableRowPointerTarget<T>>({
|
|
@@ -221,9 +231,13 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
221
231
|
setScrollVersion((current) => current + 1);
|
|
222
232
|
}
|
|
223
233
|
onBodyScrollActivity();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
234
|
+
nativeRenderer.requestRender();
|
|
235
|
+
}, [nativeRenderer, onBodyScrollActivity, virtualize]);
|
|
236
|
+
useScrollBoxScrollActivity({
|
|
237
|
+
scrollRef,
|
|
238
|
+
onVerticalScroll: handleBodyScrollActivity,
|
|
239
|
+
onHorizontalScroll: syncHeaderScroll,
|
|
240
|
+
});
|
|
227
241
|
const focusPane = useCallback(() => {
|
|
228
242
|
if (!paneInstanceId) return;
|
|
229
243
|
dispatch({ type: "FOCUS_PANE", paneId: paneInstanceId });
|
|
@@ -269,17 +283,18 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
269
283
|
useEffect(() => {
|
|
270
284
|
if (headerScrollRef.current) {
|
|
271
285
|
headerScrollRef.current.horizontalScrollBar.visible = false;
|
|
272
|
-
if (!
|
|
286
|
+
if (!horizontalScrollbarVisible) {
|
|
273
287
|
headerScrollRef.current.scrollLeft = 0;
|
|
274
288
|
}
|
|
275
289
|
}
|
|
276
290
|
if (scrollRef.current) {
|
|
277
|
-
scrollRef.current.horizontalScrollBar.visible =
|
|
278
|
-
|
|
291
|
+
scrollRef.current.horizontalScrollBar.visible = horizontalScrollbarVisible;
|
|
292
|
+
scrollRef.current.verticalScrollBar.visible = items.length > scrollRef.current.viewport.height;
|
|
293
|
+
if (!horizontalScrollbarVisible) {
|
|
279
294
|
scrollRef.current.scrollLeft = 0;
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
|
-
}, [columns.length, headerScrollRef, items.length,
|
|
297
|
+
}, [columns.length, headerScrollRef, horizontalScrollbarVisible, items.length, measuredViewportHeight, scrollRef]);
|
|
283
298
|
|
|
284
299
|
useEffect(() => {
|
|
285
300
|
if (applyScrollToIndex()) return;
|
|
@@ -318,7 +333,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
318
333
|
<Box
|
|
319
334
|
flexDirection="row"
|
|
320
335
|
height={1}
|
|
321
|
-
{...tableContentWidthProps(
|
|
336
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
322
337
|
paddingX={1}
|
|
323
338
|
backgroundColor={colors.panel}
|
|
324
339
|
>
|
|
@@ -368,11 +383,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
368
383
|
focusable={false}
|
|
369
384
|
onMouseDown={() => {
|
|
370
385
|
focusPane();
|
|
371
|
-
scheduleHeaderScrollSync();
|
|
372
386
|
}}
|
|
373
|
-
onMouseUp={scheduleBodyScrollActivity}
|
|
374
|
-
onMouseDrag={scheduleBodyScrollActivity}
|
|
375
|
-
onMouseScroll={scheduleBodyScrollActivity}
|
|
376
387
|
onSizeChange={measureContentWidth}
|
|
377
388
|
>
|
|
378
389
|
{items.length === 0 ? (
|
|
@@ -396,7 +407,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
396
407
|
key={getItemKey(item, index)}
|
|
397
408
|
flexDirection="row"
|
|
398
409
|
height={1}
|
|
399
|
-
{...tableContentWidthProps(
|
|
410
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
400
411
|
paddingX={1}
|
|
401
412
|
backgroundColor={sectionHeader.backgroundColor ?? colors.bg}
|
|
402
413
|
onMouseDown={(event) => {
|
|
@@ -433,7 +444,7 @@ function OpenTuiDataTable<T, C extends DataTableColumn = DataTableColumn>({
|
|
|
433
444
|
key={getItemKey(item, index)}
|
|
434
445
|
flexDirection="row"
|
|
435
446
|
height={1}
|
|
436
|
-
{...tableContentWidthProps(
|
|
447
|
+
{...tableContentWidthProps(measuredContentWidth)}
|
|
437
448
|
paddingX={1}
|
|
438
449
|
backgroundColor={rowBg}
|
|
439
450
|
onMouseMove={() => {
|
|
@@ -2,6 +2,7 @@ import { Box, Text, useUiHost } from "../../ui";
|
|
|
2
2
|
import { TextAttributes } from "../../ui";
|
|
3
3
|
import { type ComponentType, type ReactNode } from "react";
|
|
4
4
|
import { colors } from "../../theme/colors";
|
|
5
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
5
6
|
|
|
6
7
|
export interface DialogFrameProps {
|
|
7
8
|
title: string;
|
|
@@ -11,6 +12,7 @@ export interface DialogFrameProps {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function DialogFrame({ title, children, footer, showTitleDivider = true }: DialogFrameProps) {
|
|
15
|
+
useThemeColors();
|
|
14
16
|
const HostDialogFrame = useUiHost().DialogFrame as ComponentType<DialogFrameProps> | undefined;
|
|
15
17
|
if (HostDialogFrame) {
|
|
16
18
|
return (
|
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
65
|
-
event.stopPropagation?.();
|
|
66
|
-
onBack();
|
|
88
|
+
goBack(event);
|
|
67
89
|
}}
|
|
90
|
+
backgroundColor={colors.selected}
|
|
68
91
|
>
|
|
69
|
-
<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
|
<>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildTableGridTemplateColumns,
|
|
4
|
+
getTableWidth,
|
|
5
|
+
hasMeaningfulTableHorizontalOverflow,
|
|
6
|
+
} from "./table-layout";
|
|
7
|
+
|
|
8
|
+
describe("table layout", () => {
|
|
9
|
+
test("detects only meaningful horizontal table overflow", () => {
|
|
10
|
+
const tableWidth = getTableWidth([
|
|
11
|
+
{ width: 12 },
|
|
12
|
+
{ width: 8 },
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, 0)).toBe(false);
|
|
16
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth)).toBe(false);
|
|
17
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 1)).toBe(false);
|
|
18
|
+
expect(hasMeaningfulTableHorizontalOverflow(tableWidth, tableWidth - 2)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("keeps non-flex web table columns fixed", () => {
|
|
22
|
+
const template = buildTableGridTemplateColumns([
|
|
23
|
+
{ width: 4, align: "right" },
|
|
24
|
+
{ width: 40, flexGrow: 1 },
|
|
25
|
+
{ width: 24 },
|
|
26
|
+
{ width: 5, align: "right" },
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
expect(template).toBe(
|
|
30
|
+
"minmax(4ch, 4ch) minmax(14ch, 40fr) minmax(8ch, 24ch) minmax(5ch, 5ch)",
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("keeps proportional web table columns when no column is flexible", () => {
|
|
35
|
+
const template = buildTableGridTemplateColumns([
|
|
36
|
+
{ width: 12 },
|
|
37
|
+
{ width: 8, align: "right" },
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
expect(template).toBe("minmax(8ch, 12fr) minmax(8ch, 8fr)");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState, type RefObject } from "react";
|
|
2
2
|
import type { ScrollBoxRenderable } from "../../ui";
|
|
3
3
|
|
|
4
|
-
interface TableWidthColumn {
|
|
4
|
+
export interface TableWidthColumn {
|
|
5
5
|
width: number;
|
|
6
6
|
flexGrow?: number;
|
|
7
|
+
align?: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
const TRAILING_COLUMN_GUTTER_WIDTH = 1;
|
|
11
|
+
|
|
9
12
|
export function getTableWidth(columns: readonly TableWidthColumn[]): number {
|
|
10
13
|
return columns.reduce((sum, column) => sum + column.width + 1, 2);
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
export function hasMeaningfulTableHorizontalOverflow(tableWidth: number, viewportWidth: number): boolean {
|
|
17
|
+
return viewportWidth > 0 && tableWidth - viewportWidth > TRAILING_COLUMN_GUTTER_WIDTH;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
export function expandTableColumns<C extends TableWidthColumn>(
|
|
14
21
|
columns: readonly C[],
|
|
15
22
|
targetWidth: number,
|
|
@@ -26,6 +33,43 @@ export function expandTableColumns<C extends TableWidthColumn>(
|
|
|
26
33
|
});
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
function normalizedColumnWidth(column: TableWidthColumn): number {
|
|
37
|
+
return Math.max(1, Math.floor(column.width));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function columnMinCh(column: TableWidthColumn): number {
|
|
41
|
+
const width = normalizedColumnWidth(column);
|
|
42
|
+
if ((column.flexGrow ?? 0) > 0) {
|
|
43
|
+
return Math.max(8, Math.min(18, Math.floor(width * 0.35)));
|
|
44
|
+
}
|
|
45
|
+
if (column.align === "right") {
|
|
46
|
+
return Math.max(3, Math.min(width, 8));
|
|
47
|
+
}
|
|
48
|
+
if (width >= 16) {
|
|
49
|
+
return Math.max(8, Math.min(16, Math.floor(width * 0.35)));
|
|
50
|
+
}
|
|
51
|
+
return Math.max(1, Math.min(width, 8));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function columnFlexWeight(column: TableWidthColumn): number {
|
|
55
|
+
const flexGrow = column.flexGrow ?? 0;
|
|
56
|
+
const baseWeight = normalizedColumnWidth(column);
|
|
57
|
+
return flexGrow > 0 ? baseWeight * Math.max(1, flexGrow) : baseWeight;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildTableGridTemplateColumns(columns: readonly TableWidthColumn[]): string {
|
|
61
|
+
const hasFlexColumn = columns.some((column) => (column.flexGrow ?? 0) > 0);
|
|
62
|
+
return columns
|
|
63
|
+
.map((column) => {
|
|
64
|
+
const width = normalizedColumnWidth(column);
|
|
65
|
+
if (!hasFlexColumn || (column.flexGrow ?? 0) > 0) {
|
|
66
|
+
return `minmax(${columnMinCh(column)}ch, ${columnFlexWeight(column)}fr)`;
|
|
67
|
+
}
|
|
68
|
+
return `minmax(${columnMinCh(column)}ch, ${width}ch)`;
|
|
69
|
+
})
|
|
70
|
+
.join(" ");
|
|
71
|
+
}
|
|
72
|
+
|
|
29
73
|
export function useMeasuredTableContentWidth(
|
|
30
74
|
tableWidth: number,
|
|
31
75
|
headerScrollRef: RefObject<ScrollBoxRenderable | null> | undefined,
|
|
@@ -34,9 +78,9 @@ export function useMeasuredTableContentWidth(
|
|
|
34
78
|
const [viewportWidth, setViewportWidth] = useState(0);
|
|
35
79
|
|
|
36
80
|
const measureContentWidth = useCallback(() => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
81
|
+
const bodyWidth = scrollRef?.current?.viewport?.width || scrollRef?.current?.width || 0;
|
|
82
|
+
const headerWidth = headerScrollRef?.current?.viewport?.width || headerScrollRef?.current?.width || 0;
|
|
83
|
+
const nextWidth = bodyWidth || headerWidth;
|
|
40
84
|
setViewportWidth((current) => current === nextWidth ? current : nextWidth);
|
|
41
85
|
}, [headerScrollRef, scrollRef]);
|
|
42
86
|
const scheduleContentWidthMeasure = useCallback(() => {
|
|
@@ -48,6 +92,7 @@ export function useMeasuredTableContentWidth(
|
|
|
48
92
|
|
|
49
93
|
return {
|
|
50
94
|
contentWidth: Math.max(tableWidth, viewportWidth),
|
|
95
|
+
viewportWidth,
|
|
51
96
|
measureContentWidth: scheduleContentWidthMeasure,
|
|
52
97
|
};
|
|
53
98
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useShortcut } from "../../react/input";
|
|
2
3
|
import { Box, ScrollBox, Text, useUiHost } from "../../ui";
|
|
3
4
|
import { TextAttributes, type ScrollBoxRenderable } from "../../ui";
|
|
4
5
|
import { colors, hoverBg } from "../../theme/colors";
|
|
@@ -21,6 +22,8 @@ export interface TabsProps {
|
|
|
21
22
|
closeMode?: "active" | "always";
|
|
22
23
|
addLabel?: string;
|
|
23
24
|
onAdd?: () => void;
|
|
25
|
+
focused?: boolean;
|
|
26
|
+
keyboardNavigation?: boolean;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
const WHEEL_DELTA_PER_CELL = 8;
|
|
@@ -34,6 +37,8 @@ export function Tabs({
|
|
|
34
37
|
closeMode = "always",
|
|
35
38
|
addLabel = "+",
|
|
36
39
|
onAdd,
|
|
40
|
+
focused = false,
|
|
41
|
+
keyboardNavigation = true,
|
|
37
42
|
}: TabsProps) {
|
|
38
43
|
const ui = useUiHost();
|
|
39
44
|
const NativeTabs = ui.Tabs;
|
|
@@ -51,6 +56,34 @@ export function Tabs({
|
|
|
51
56
|
closeFg: colors.textMuted,
|
|
52
57
|
addFg: colors.textMuted,
|
|
53
58
|
};
|
|
59
|
+
const selectAdjacentTab = useCallback((direction: -1 | 1) => {
|
|
60
|
+
const enabledTabs = tabs.filter((tab) => !tab.disabled);
|
|
61
|
+
if (enabledTabs.length === 0) return;
|
|
62
|
+
|
|
63
|
+
const activeIndex = enabledTabs.findIndex((tab) => tab.value === activeValue);
|
|
64
|
+
const nextIndex = activeIndex >= 0
|
|
65
|
+
? Math.max(0, Math.min(activeIndex + direction, enabledTabs.length - 1))
|
|
66
|
+
: direction > 0 ? 0 : enabledTabs.length - 1;
|
|
67
|
+
const nextTab = enabledTabs[nextIndex];
|
|
68
|
+
if (!nextTab || nextTab.value === activeValue) return;
|
|
69
|
+
onSelect(nextTab.value);
|
|
70
|
+
}, [activeValue, onSelect, tabs]);
|
|
71
|
+
|
|
72
|
+
useShortcut((event) => {
|
|
73
|
+
if (event.ctrl || event.meta || event.alt || event.targetEditable) return;
|
|
74
|
+
|
|
75
|
+
if (event.name === "h" || event.name === "left") {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
selectAdjacentTab(-1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (event.name === "l" || event.name === "right") {
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
event.stopPropagation();
|
|
84
|
+
selectAdjacentTab(1);
|
|
85
|
+
}
|
|
86
|
+
}, { enabled: focused && keyboardNavigation });
|
|
54
87
|
|
|
55
88
|
if (NativeTabs) {
|
|
56
89
|
return (
|