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.
- package/package.json +2 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- 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 +70 -0
- 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 +17 -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-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- 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 +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- 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 +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- 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 +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- 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 +0 -3
- 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 +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -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 +478 -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.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -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 +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- 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 +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -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 +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- 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 +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- 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/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- 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
package/src/components/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
export { PriceSelectorDialog } from "./price-selector-dialog";
|
|
3
3
|
export { StaticChartSurface } from "./chart/static-chart-surface";
|
|
4
|
+
export { SpeedometerGauge } from "./speedometer-gauge";
|
|
5
|
+
export type { SpeedometerSegment } from "./speedometer-gauge";
|
|
4
6
|
export type { QuoteFlashDirection } from "./ticker-list-table";
|
|
5
7
|
export { TickerListTableView } from "./ticker-list-table-view";
|
|
6
8
|
export type { TickerListVisibleRange } from "./ticker-list-table-view";
|
|
@@ -10,14 +12,16 @@ export type { DataTableKeyEvent } from "./data-table-view";
|
|
|
10
12
|
export { DataTableStackView } from "./data-table-stack-view";
|
|
11
13
|
export { FeedDataTableStackView } from "./feed-data-table-stack-view";
|
|
12
14
|
export type { FeedDataTableItem } from "./feed-data-table-stack-view";
|
|
13
|
-
export {
|
|
15
|
+
export { activeStackIndex, sortStackItems } from "./feed-stack-controller";
|
|
16
|
+
export type { StackSortPreference } from "./feed-stack-controller";
|
|
17
|
+
export { PaneFooterScope, usePaneFooter } from "./layout/pane-footer";
|
|
14
18
|
export type { PaneFooterSegment, PaneHint } from "./layout/pane-footer";
|
|
15
19
|
export { useExternalLinkFooter } from "./use-external-link-footer";
|
|
16
20
|
export { Button } from "./ui/button";
|
|
21
|
+
export { ConfirmDialog } from "./ui/confirm-dialog";
|
|
17
22
|
export { ChoiceDialog } from "./ui/choice-dialog";
|
|
18
23
|
export type { ChoiceDialogChoice, ChoiceDialogProps } from "./ui/choice-dialog";
|
|
19
24
|
export type { DataTableCell, DataTableColumn } from "./ui/data-table";
|
|
20
|
-
export { DialogFrame } from "./ui/frame";
|
|
21
25
|
export { EmptyState } from "./ui/status";
|
|
22
26
|
export { getMessageComposerBlockHeight, MessageComposer } from "./ui/message-composer";
|
|
23
27
|
export { NumberField, TextField } from "./ui/fields";
|
|
@@ -30,6 +30,24 @@ interface FloatingPaneWrapperProps {
|
|
|
30
30
|
children: ReactNode;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function TerminalFloatingPaneBorder({ width, height }: { width: number; height: number }) {
|
|
34
|
+
const borderWidth = Math.max(0, Math.floor(width));
|
|
35
|
+
const borderHeight = Math.max(0, Math.floor(height));
|
|
36
|
+
const bodyHeight = Math.max(0, borderHeight - 2);
|
|
37
|
+
if (borderWidth < 2 || bodyHeight <= 0) return null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
<Box position="absolute" top={1} left={0} width={1} height={bodyHeight}>
|
|
42
|
+
<Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
<Box position="absolute" top={1} left={borderWidth - 1} width={1} height={bodyHeight}>
|
|
45
|
+
<Text fg={colors.border} selectable={false}>{"│".repeat(bodyHeight)}</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
/** Pure visual wrapper; Shell owns the interaction state and supplies handlers. */
|
|
34
52
|
export function FloatingPaneWrapper({
|
|
35
53
|
paneId,
|
|
@@ -101,7 +119,17 @@ export function FloatingPaneWrapper({
|
|
|
101
119
|
{children}
|
|
102
120
|
</Box>
|
|
103
121
|
|
|
104
|
-
{reserveFooter &&
|
|
122
|
+
{reserveFooter && (
|
|
123
|
+
<PaneFooterBar
|
|
124
|
+
footer={footer}
|
|
125
|
+
focused={focused}
|
|
126
|
+
width={width}
|
|
127
|
+
reserveRight={2}
|
|
128
|
+
showBorder={!nativePaneChrome && !focused}
|
|
129
|
+
/>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{!nativePaneChrome && !focused && <TerminalFloatingPaneBorder width={width} height={height} />}
|
|
105
133
|
|
|
106
134
|
{nativePaneChrome ? (
|
|
107
135
|
<Box
|
|
@@ -117,7 +145,7 @@ export function FloatingPaneWrapper({
|
|
|
117
145
|
/>
|
|
118
146
|
) : (
|
|
119
147
|
<Box position="absolute" bottom={0} right={0} width={2} height={1}>
|
|
120
|
-
<Text fg={focused ? colors.borderFocused : colors.
|
|
148
|
+
<Text fg={focused ? colors.borderFocused : colors.border} selectable={false}>{"─◢"}</Text>
|
|
121
149
|
</Box>
|
|
122
150
|
)}
|
|
123
151
|
</Box>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box, SpinnerMark, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
2
2
|
import { useEffect, type ReactNode } from "react";
|
|
3
|
-
import { colors, priceColor } from "../../theme/colors";
|
|
3
|
+
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
4
4
|
import { useAppActive } from "../../state/app-activity";
|
|
5
5
|
import { useAppDispatch, useAppSelector } from "../../state/app-context";
|
|
6
6
|
import {
|
|
@@ -23,8 +23,8 @@ const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
|
23
23
|
|
|
24
24
|
function DesktopHeaderPill({
|
|
25
25
|
children,
|
|
26
|
-
backgroundColor =
|
|
27
|
-
borderColor =
|
|
26
|
+
backgroundColor = blendHex(colors.header, colors.bg, 0.28),
|
|
27
|
+
borderColor = blendHex(colors.border, colors.headerText, 0.22),
|
|
28
28
|
}: {
|
|
29
29
|
children: ReactNode;
|
|
30
30
|
backgroundColor?: string;
|
|
@@ -173,8 +173,8 @@ export function Header() {
|
|
|
173
173
|
Gloomberb
|
|
174
174
|
</Text>
|
|
175
175
|
<DesktopHeaderPill
|
|
176
|
-
backgroundColor=
|
|
177
|
-
borderColor=
|
|
176
|
+
backgroundColor={blendHex(colors.header, colors.headerText, 0.1)}
|
|
177
|
+
borderColor={blendHex(colors.border, colors.headerText, 0.28)}
|
|
178
178
|
>
|
|
179
179
|
<Text fg={colors.headerText} style={{ fontSize: 11 }}>v{VERSION}</Text>
|
|
180
180
|
</DesktopHeaderPill>
|
|
@@ -184,8 +184,11 @@ export function Header() {
|
|
|
184
184
|
</Box>
|
|
185
185
|
{mktLabel ? (
|
|
186
186
|
<Box paddingRight={1}>
|
|
187
|
-
<DesktopHeaderPill
|
|
188
|
-
|
|
187
|
+
<DesktopHeaderPill
|
|
188
|
+
backgroundColor={blendHex(colors.header, mktColor, 0.12)}
|
|
189
|
+
borderColor={blendHex(colors.border, mktColor, 0.3)}
|
|
190
|
+
>
|
|
191
|
+
<Text fg={mktColor} style={{ fontSize: 11, fontWeight: 700 }}>{mktLabel}</Text>
|
|
189
192
|
</DesktopHeaderPill>
|
|
190
193
|
</Box>
|
|
191
194
|
) : null}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { memo, useCallback } from "react";
|
|
2
2
|
import { PaneInstanceProvider } from "../../state/app-context";
|
|
3
|
+
import { PaneKeyboardScrollController } from "../../state/pane-scroll-registry";
|
|
3
4
|
import type { PaneDef } from "../../types/plugin";
|
|
4
5
|
|
|
5
6
|
interface PaneContentProps {
|
|
@@ -27,6 +28,7 @@ export const PaneContent = memo(function PaneContent({
|
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<PaneInstanceProvider paneId={paneId}>
|
|
31
|
+
<PaneKeyboardScrollController paneId={paneId} focused={focused} />
|
|
30
32
|
<Component
|
|
31
33
|
paneId={paneId}
|
|
32
34
|
paneType={paneType}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
3
|
+
import { createDefaultConfig } from "../../types/config";
|
|
4
|
+
import { getVisiblePaneCycleOrder } from "./pane-cycle-order";
|
|
5
|
+
|
|
6
|
+
function createRegistry(options: {
|
|
7
|
+
paneIds: readonly string[];
|
|
8
|
+
disabledPaneIds?: Record<string, readonly string[]>;
|
|
9
|
+
}): PluginRegistry {
|
|
10
|
+
return {
|
|
11
|
+
panes: new Map(options.paneIds.map((paneId) => [paneId, {}])),
|
|
12
|
+
getPluginPaneIds: (pluginId: string) => [...(options.disabledPaneIds?.[pluginId] ?? [])],
|
|
13
|
+
} as unknown as PluginRegistry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("getVisiblePaneCycleOrder", () => {
|
|
17
|
+
test("skips disabled and unregistered panes when cycling focus", () => {
|
|
18
|
+
const config = createDefaultConfig("/tmp/gloomberb-pane-cycle-order");
|
|
19
|
+
const registry = createRegistry({
|
|
20
|
+
paneIds: ["portfolio-list", "chat", "ticker-detail"],
|
|
21
|
+
disabledPaneIds: { chat: ["chat"] },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(getVisiblePaneCycleOrder(config.layout, registry, ["chat"])).toEqual([
|
|
25
|
+
"portfolio-list:main",
|
|
26
|
+
"ticker-detail:main",
|
|
27
|
+
"ticker-detail:nvda",
|
|
28
|
+
]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getDockedPaneIds } from "../../plugins/pane-manager";
|
|
2
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
3
|
+
import { findPaneInstance, type LayoutConfig } from "../../types/config";
|
|
4
|
+
|
|
5
|
+
function disabledPaneTypes(pluginRegistry: PluginRegistry, disabledPlugins: readonly string[]): Set<string> {
|
|
6
|
+
const paneTypes = new Set<string>();
|
|
7
|
+
for (const pluginId of disabledPlugins) {
|
|
8
|
+
for (const paneId of pluginRegistry.getPluginPaneIds(pluginId)) {
|
|
9
|
+
paneTypes.add(paneId);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return paneTypes;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getVisiblePaneCycleOrder(
|
|
16
|
+
layout: LayoutConfig,
|
|
17
|
+
pluginRegistry: PluginRegistry,
|
|
18
|
+
disabledPlugins: readonly string[],
|
|
19
|
+
): string[] {
|
|
20
|
+
const disabledTypes = disabledPaneTypes(pluginRegistry, disabledPlugins);
|
|
21
|
+
const isVisiblePane = (instanceId: string) => {
|
|
22
|
+
const instance = findPaneInstance(layout, instanceId);
|
|
23
|
+
return !!instance
|
|
24
|
+
&& !disabledTypes.has(instance.paneId)
|
|
25
|
+
&& pluginRegistry.panes.has(instance.paneId);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
...getDockedPaneIds(layout),
|
|
30
|
+
...layout.floating.map((entry) => entry.instanceId),
|
|
31
|
+
].filter(isVisiblePane);
|
|
32
|
+
}
|
|
@@ -158,6 +158,21 @@ export function PaneFooterProvider({
|
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
export function PaneFooterScope({
|
|
162
|
+
active,
|
|
163
|
+
children,
|
|
164
|
+
}: {
|
|
165
|
+
active: boolean;
|
|
166
|
+
children: ReactNode;
|
|
167
|
+
}) {
|
|
168
|
+
const context = useContext(PaneFooterContext);
|
|
169
|
+
return (
|
|
170
|
+
<PaneFooterContext.Provider value={active ? context : null}>
|
|
171
|
+
{children}
|
|
172
|
+
</PaneFooterContext.Provider>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
161
176
|
export function usePaneFooter(
|
|
162
177
|
registrationId: string,
|
|
163
178
|
factory: () => PaneFooterRegistration | null | undefined,
|
|
@@ -342,17 +357,24 @@ export function PaneFooterBar({
|
|
|
342
357
|
focused,
|
|
343
358
|
width = 0,
|
|
344
359
|
reserveRight = 0,
|
|
360
|
+
showBorder = false,
|
|
345
361
|
}: {
|
|
346
362
|
footer?: CombinedPaneFooter | null;
|
|
347
363
|
focused: boolean;
|
|
348
364
|
width?: number;
|
|
349
365
|
reserveRight?: number;
|
|
366
|
+
showBorder?: boolean;
|
|
350
367
|
}) {
|
|
351
368
|
const { nativePaneChrome } = useUiCapabilities();
|
|
352
369
|
const resolvedFooter = footer ?? EMPTY_FOOTER;
|
|
353
370
|
const empty = !hasPaneFooterContent(resolvedFooter);
|
|
354
371
|
const borderColor = focused ? colors.borderFocused : colors.border;
|
|
355
372
|
const topBorderColor = colors.border;
|
|
373
|
+
const nativeBackgroundColor = empty
|
|
374
|
+
? "transparent"
|
|
375
|
+
: focused
|
|
376
|
+
? blendHex(colors.bg, colors.borderFocused, 0.06)
|
|
377
|
+
: blendHex(colors.panel, colors.border, 0.12);
|
|
356
378
|
const reservedRight = Math.max(0, reserveRight);
|
|
357
379
|
const rightPadding = reservedRight > 0 ? reservedRight : 1;
|
|
358
380
|
|
|
@@ -370,7 +392,7 @@ export function PaneFooterBar({
|
|
|
370
392
|
style={{
|
|
371
393
|
"--pane-footer-border-color": empty ? "transparent" : topBorderColor,
|
|
372
394
|
borderTop: `1px solid ${empty ? "transparent" : topBorderColor}`,
|
|
373
|
-
backgroundColor:
|
|
395
|
+
backgroundColor: nativeBackgroundColor,
|
|
374
396
|
boxShadow: empty ? "none" : "inset 0 1px 0 rgba(255,255,255,0.03)",
|
|
375
397
|
}}
|
|
376
398
|
>
|
|
@@ -384,15 +406,15 @@ export function PaneFooterBar({
|
|
|
384
406
|
);
|
|
385
407
|
}
|
|
386
408
|
|
|
387
|
-
if (focused) {
|
|
409
|
+
if (focused || showBorder) {
|
|
388
410
|
const contentWidth = Math.max(0, Math.floor(width) - 1 - reservedRight - (reservedRight > 0 ? 0 : 1));
|
|
389
411
|
return (
|
|
390
|
-
<Box height={1} width={width} flexDirection="row" data-gloom-role="pane-footer" data-focused="true" data-empty={empty ? "true" : "false"}>
|
|
412
|
+
<Box height={1} width={width} flexDirection="row" data-gloom-role="pane-footer" data-focused={focused ? "true" : "false"} data-empty={empty ? "true" : "false"}>
|
|
391
413
|
<Text fg={borderColor} selectable={false}>└</Text>
|
|
392
414
|
<Box width={contentWidth} height={1} overflow="hidden">
|
|
393
415
|
{empty
|
|
394
416
|
? <Text fg={borderColor} selectable={false}>{"─".repeat(contentWidth)}</Text>
|
|
395
|
-
: <FooterContent footer={resolvedFooter} focused width={contentWidth} />}
|
|
417
|
+
: <FooterContent footer={resolvedFooter} focused={focused} width={contentWidth} />}
|
|
396
418
|
</Box>
|
|
397
419
|
{reservedRight === 0 && <Text fg={borderColor} selectable={false}>┘</Text>}
|
|
398
420
|
</Box>
|
|
@@ -108,7 +108,6 @@ export function PaneHeader({
|
|
|
108
108
|
const backgroundColor = floating ? floatingPaneTitleBg(focused) : paneTitleBg(focused);
|
|
109
109
|
const actionText = showActions ? PANE_HEADER_ACTION : " ";
|
|
110
110
|
const closeText = floating ? PANE_HEADER_CLOSE : "";
|
|
111
|
-
const bc = colors.borderFocused;
|
|
112
111
|
const textColor = paneTitleText(focused, floating);
|
|
113
112
|
|
|
114
113
|
if (nativePaneChrome) {
|
|
@@ -184,9 +183,10 @@ export function PaneHeader({
|
|
|
184
183
|
);
|
|
185
184
|
}
|
|
186
185
|
|
|
187
|
-
if (focused) {
|
|
186
|
+
if (focused || floating) {
|
|
188
187
|
// Build: ┌─:: Title ─────────── ... x─┐
|
|
189
188
|
// Reserve 2 for corners, 1 for ─ after ┌, 1 for ─ before ┐
|
|
189
|
+
const borderColor = focused ? colors.borderFocused : colors.border;
|
|
190
190
|
const innerWidth = Math.max(0, width - 4);
|
|
191
191
|
const contentWidth = PANE_HEADER_GRIP.length + closeText.length + actionText.length;
|
|
192
192
|
const titleWidth = Math.max(0, innerWidth - contentWidth);
|
|
@@ -196,9 +196,9 @@ export function PaneHeader({
|
|
|
196
196
|
|
|
197
197
|
return (
|
|
198
198
|
<Box height={PANE_HEADER_HEIGHT} width={width} backgroundColor={backgroundColor} flexDirection="row">
|
|
199
|
-
<Text fg={
|
|
199
|
+
<Text fg={borderColor} selectable={false}>{"┌─"}</Text>
|
|
200
200
|
<Text fg={textColor} selectable={false}>{`${PANE_HEADER_GRIP}${clippedTitle}`}</Text>
|
|
201
|
-
<Text fg={
|
|
201
|
+
<Text fg={borderColor} selectable={false}>{fill}</Text>
|
|
202
202
|
<TerminalPaneButton
|
|
203
203
|
text={actionText}
|
|
204
204
|
fg={textColor}
|
|
@@ -213,7 +213,7 @@ export function PaneHeader({
|
|
|
213
213
|
onMouseDown={onCloseMouseDown}
|
|
214
214
|
/>
|
|
215
215
|
)}
|
|
216
|
-
<Text fg={
|
|
216
|
+
<Text fg={borderColor} selectable={false}>{"─┐"}</Text>
|
|
217
217
|
</Box>
|
|
218
218
|
);
|
|
219
219
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useReducer, type ReactNode } from "react";
|
|
3
|
+
import { testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
+
import { useShortcut } from "../../react/input";
|
|
5
|
+
import { AppContext, appReducer, createInitialState } from "../../state/app-context";
|
|
6
|
+
import { Box, ScrollBox, Text, type ScrollBoxRenderable } from "../../ui";
|
|
7
|
+
import { DataTableStackView } from "../data-table-stack-view";
|
|
8
|
+
import { type DataTableColumn } from "../ui";
|
|
9
|
+
import { createDefaultConfig } from "../../types/config";
|
|
10
|
+
import type { PaneProps } from "../../types/plugin";
|
|
11
|
+
import { PaneContent } from "./pane-content";
|
|
12
|
+
|
|
13
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
14
|
+
let scrollRef: ScrollBoxRenderable | null = null;
|
|
15
|
+
let hiddenScrollRef: ScrollBoxRenderable | null = null;
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
if (testSetup) {
|
|
19
|
+
act(() => {
|
|
20
|
+
testSetup!.renderer.destroy();
|
|
21
|
+
});
|
|
22
|
+
testSetup = undefined;
|
|
23
|
+
}
|
|
24
|
+
scrollRef = null;
|
|
25
|
+
hiddenScrollRef = null;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function emitDownArrow() {
|
|
29
|
+
return act(async () => {
|
|
30
|
+
testSetup!.mockInput.pressArrow("down");
|
|
31
|
+
await testSetup!.renderOnce();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function renderHarness(component: ReactNode) {
|
|
36
|
+
await act(async () => {
|
|
37
|
+
testSetup = await testRender(component, { width: 30, height: 5 });
|
|
38
|
+
});
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await testSetup!.renderOnce();
|
|
41
|
+
await testSetup!.renderOnce();
|
|
42
|
+
await testSetup!.renderOnce();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function LongScrollPane({ width, height }: PaneProps) {
|
|
47
|
+
return (
|
|
48
|
+
<Box width={width} height={height}>
|
|
49
|
+
<ScrollBox
|
|
50
|
+
ref={(node) => {
|
|
51
|
+
scrollRef = node;
|
|
52
|
+
}}
|
|
53
|
+
height={height}
|
|
54
|
+
scrollY
|
|
55
|
+
focusable={false}
|
|
56
|
+
>
|
|
57
|
+
<Box flexDirection="column">
|
|
58
|
+
{Array.from({ length: 20 }, (_, index) => (
|
|
59
|
+
<Box key={index} height={1}>
|
|
60
|
+
<Text>{`Row ${index}`}</Text>
|
|
61
|
+
</Box>
|
|
62
|
+
))}
|
|
63
|
+
</Box>
|
|
64
|
+
</ScrollBox>
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ConsumingScrollPane(props: PaneProps) {
|
|
70
|
+
useShortcut((event) => {
|
|
71
|
+
if (event.name !== "down") return;
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
event.stopPropagation();
|
|
74
|
+
});
|
|
75
|
+
return <LongScrollPane {...props} />;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function HiddenMountedTabPane({ width, height }: PaneProps) {
|
|
79
|
+
const renderRows = () => Array.from({ length: 20 }, (_, index) => (
|
|
80
|
+
<Box key={index} height={1}>
|
|
81
|
+
<Text>{`Tab row ${index}`}</Text>
|
|
82
|
+
</Box>
|
|
83
|
+
));
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Box width={width} height={height}>
|
|
87
|
+
<ScrollBox
|
|
88
|
+
ref={(node) => {
|
|
89
|
+
scrollRef = node;
|
|
90
|
+
}}
|
|
91
|
+
height={height}
|
|
92
|
+
scrollY
|
|
93
|
+
focusable={false}
|
|
94
|
+
>
|
|
95
|
+
<Box flexDirection="column">{renderRows()}</Box>
|
|
96
|
+
</ScrollBox>
|
|
97
|
+
<Box visible={false} height={height}>
|
|
98
|
+
<ScrollBox
|
|
99
|
+
ref={(node) => {
|
|
100
|
+
hiddenScrollRef = node;
|
|
101
|
+
}}
|
|
102
|
+
height={height}
|
|
103
|
+
scrollY
|
|
104
|
+
focusable={false}
|
|
105
|
+
>
|
|
106
|
+
<Box flexDirection="column">{renderRows()}</Box>
|
|
107
|
+
</ScrollBox>
|
|
108
|
+
</Box>
|
|
109
|
+
</Box>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface TestRow {
|
|
114
|
+
id: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const columns: DataTableColumn[] = [{ id: "label", label: "Label", width: 10, align: "left" }];
|
|
118
|
+
const rows: TestRow[] = [{ id: "row-1" }];
|
|
119
|
+
|
|
120
|
+
function StackDetailPane({ focused, width, height }: PaneProps) {
|
|
121
|
+
const detailContent = (
|
|
122
|
+
<ScrollBox
|
|
123
|
+
ref={(node) => {
|
|
124
|
+
scrollRef = node;
|
|
125
|
+
}}
|
|
126
|
+
flexGrow={1}
|
|
127
|
+
scrollY
|
|
128
|
+
focusable={false}
|
|
129
|
+
>
|
|
130
|
+
<Box flexDirection="column">
|
|
131
|
+
{Array.from({ length: 20 }, (_, index) => (
|
|
132
|
+
<Box key={index} height={1}>
|
|
133
|
+
<Text>{`Tweet detail row ${index}`}</Text>
|
|
134
|
+
</Box>
|
|
135
|
+
))}
|
|
136
|
+
</Box>
|
|
137
|
+
</ScrollBox>
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<DataTableStackView<TestRow>
|
|
142
|
+
focused={focused}
|
|
143
|
+
detailOpen
|
|
144
|
+
onBack={() => {}}
|
|
145
|
+
detailContent={detailContent}
|
|
146
|
+
detailTitle="Tweet"
|
|
147
|
+
selectedIndex={0}
|
|
148
|
+
rootWidth={width}
|
|
149
|
+
rootHeight={height}
|
|
150
|
+
columns={columns}
|
|
151
|
+
items={rows}
|
|
152
|
+
sortColumnId={null}
|
|
153
|
+
sortDirection="asc"
|
|
154
|
+
onHeaderClick={() => {}}
|
|
155
|
+
getItemKey={(row) => row.id}
|
|
156
|
+
isSelected={(_row, index) => index === 0}
|
|
157
|
+
onSelect={() => {}}
|
|
158
|
+
renderCell={(row) => ({ text: row.id })}
|
|
159
|
+
emptyStateTitle="No rows"
|
|
160
|
+
/>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Harness({
|
|
165
|
+
component = LongScrollPane,
|
|
166
|
+
focused = true,
|
|
167
|
+
}: {
|
|
168
|
+
component?: (props: PaneProps) => ReactNode;
|
|
169
|
+
focused?: boolean;
|
|
170
|
+
}) {
|
|
171
|
+
const initialState = createInitialState(createDefaultConfig("/tmp/gloomberb-pane-keyboard-scroll-test"));
|
|
172
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<AppContext value={{ state, dispatch }}>
|
|
176
|
+
<PaneContent
|
|
177
|
+
component={component}
|
|
178
|
+
paneId="test-pane:main"
|
|
179
|
+
paneType="test-pane"
|
|
180
|
+
focused={focused}
|
|
181
|
+
width={30}
|
|
182
|
+
height={5}
|
|
183
|
+
/>
|
|
184
|
+
</AppContext>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
describe("pane keyboard scrolling", () => {
|
|
189
|
+
test("scrolls the focused pane's vertical scrollbox with arrow keys", async () => {
|
|
190
|
+
await renderHarness(<Harness />);
|
|
191
|
+
|
|
192
|
+
expect(scrollRef?.scrollTop).toBe(0);
|
|
193
|
+
await emitDownArrow();
|
|
194
|
+
|
|
195
|
+
expect(scrollRef?.scrollTop).toBe(3);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("does not steal arrows from existing pane shortcut handlers", async () => {
|
|
199
|
+
await renderHarness(<Harness component={ConsumingScrollPane} />);
|
|
200
|
+
|
|
201
|
+
expect(scrollRef?.scrollTop).toBe(0);
|
|
202
|
+
await emitDownArrow();
|
|
203
|
+
|
|
204
|
+
expect(scrollRef?.scrollTop).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("ignores mounted scrollboxes inside hidden tabs", async () => {
|
|
208
|
+
await renderHarness(<Harness component={HiddenMountedTabPane} />);
|
|
209
|
+
|
|
210
|
+
expect(scrollRef?.scrollTop).toBe(0);
|
|
211
|
+
expect(hiddenScrollRef?.scrollTop).toBe(0);
|
|
212
|
+
await emitDownArrow();
|
|
213
|
+
|
|
214
|
+
expect(scrollRef?.scrollTop).toBe(3);
|
|
215
|
+
expect(hiddenScrollRef?.scrollTop).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("scrolls stack detail content without requiring a pane-local detail handler", async () => {
|
|
219
|
+
await renderHarness(<Harness component={StackDetailPane} />);
|
|
220
|
+
|
|
221
|
+
expect(scrollRef?.scrollTop).toBe(0);
|
|
222
|
+
await emitDownArrow();
|
|
223
|
+
|
|
224
|
+
expect(scrollRef?.scrollTop).toBe(3);
|
|
225
|
+
});
|
|
226
|
+
});
|