gloomberb 0.6.1 → 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 +1 -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 +48 -27
- 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 +38 -21
- 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 +5 -1
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- 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/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/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-chart-tab-switch.test.tsx +3 -4
- 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 +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 +1 -0
- 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 +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- 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/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -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 +1 -0
- 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";
|
|
15
|
+
export { activeStackIndex, sortStackItems } from "./feed-stack-controller";
|
|
16
|
+
export type { StackSortPreference } from "./feed-stack-controller";
|
|
13
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}
|
|
@@ -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
|
+
}
|
|
@@ -357,17 +357,24 @@ export function PaneFooterBar({
|
|
|
357
357
|
focused,
|
|
358
358
|
width = 0,
|
|
359
359
|
reserveRight = 0,
|
|
360
|
+
showBorder = false,
|
|
360
361
|
}: {
|
|
361
362
|
footer?: CombinedPaneFooter | null;
|
|
362
363
|
focused: boolean;
|
|
363
364
|
width?: number;
|
|
364
365
|
reserveRight?: number;
|
|
366
|
+
showBorder?: boolean;
|
|
365
367
|
}) {
|
|
366
368
|
const { nativePaneChrome } = useUiCapabilities();
|
|
367
369
|
const resolvedFooter = footer ?? EMPTY_FOOTER;
|
|
368
370
|
const empty = !hasPaneFooterContent(resolvedFooter);
|
|
369
371
|
const borderColor = focused ? colors.borderFocused : colors.border;
|
|
370
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);
|
|
371
378
|
const reservedRight = Math.max(0, reserveRight);
|
|
372
379
|
const rightPadding = reservedRight > 0 ? reservedRight : 1;
|
|
373
380
|
|
|
@@ -385,7 +392,7 @@ export function PaneFooterBar({
|
|
|
385
392
|
style={{
|
|
386
393
|
"--pane-footer-border-color": empty ? "transparent" : topBorderColor,
|
|
387
394
|
borderTop: `1px solid ${empty ? "transparent" : topBorderColor}`,
|
|
388
|
-
backgroundColor:
|
|
395
|
+
backgroundColor: nativeBackgroundColor,
|
|
389
396
|
boxShadow: empty ? "none" : "inset 0 1px 0 rgba(255,255,255,0.03)",
|
|
390
397
|
}}
|
|
391
398
|
>
|
|
@@ -399,15 +406,15 @@ export function PaneFooterBar({
|
|
|
399
406
|
);
|
|
400
407
|
}
|
|
401
408
|
|
|
402
|
-
if (focused) {
|
|
409
|
+
if (focused || showBorder) {
|
|
403
410
|
const contentWidth = Math.max(0, Math.floor(width) - 1 - reservedRight - (reservedRight > 0 ? 0 : 1));
|
|
404
411
|
return (
|
|
405
|
-
<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"}>
|
|
406
413
|
<Text fg={borderColor} selectable={false}>└</Text>
|
|
407
414
|
<Box width={contentWidth} height={1} overflow="hidden">
|
|
408
415
|
{empty
|
|
409
416
|
? <Text fg={borderColor} selectable={false}>{"─".repeat(contentWidth)}</Text>
|
|
410
|
-
: <FooterContent footer={resolvedFooter} focused width={contentWidth} />}
|
|
417
|
+
: <FooterContent footer={resolvedFooter} focused={focused} width={contentWidth} />}
|
|
411
418
|
</Box>
|
|
412
419
|
{reservedRight === 0 && <Text fg={borderColor} selectable={false}>┘</Text>}
|
|
413
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
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { TextAttributes } from "@opentui/core";
|
|
3
|
-
import { testRender } from "../../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, testRender } from "../../renderers/opentui/test-utils";
|
|
5
4
|
import type { ReactNode } from "react";
|
|
6
5
|
import { act, createElement, useReducer } from "react";
|
|
7
6
|
import { AppContext, appReducer, createInitialState } from "../../state/app-context";
|
|
@@ -76,7 +75,7 @@ function createShellPluginRegistry(options?: {
|
|
|
76
75
|
openPaneSettingsFn: () => {},
|
|
77
76
|
openCommandBar: () => {},
|
|
78
77
|
updateLayoutFn: () => {},
|
|
79
|
-
|
|
78
|
+
hidePane: () => {},
|
|
80
79
|
} as unknown as PluginRegistry;
|
|
81
80
|
}
|
|
82
81
|
|
|
@@ -114,7 +113,7 @@ function createBrokerPortfolioRegistry(): PluginRegistry {
|
|
|
114
113
|
openPaneSettingsFn: () => {},
|
|
115
114
|
openCommandBar: () => {},
|
|
116
115
|
updateLayoutFn: () => {},
|
|
117
|
-
|
|
116
|
+
hidePane: () => {},
|
|
118
117
|
focusPaneFn: () => {},
|
|
119
118
|
pinTicker: () => {},
|
|
120
119
|
navigateTicker: () => {},
|
|
@@ -337,9 +336,9 @@ function ChartShellHarness({
|
|
|
337
336
|
|
|
338
337
|
return (
|
|
339
338
|
<AppContext value={{ state, dispatch }}>
|
|
340
|
-
<
|
|
339
|
+
<TestDialogProvider>
|
|
341
340
|
<Shell pluginRegistry={pluginRegistry} />
|
|
342
|
-
</
|
|
341
|
+
</TestDialogProvider>
|
|
343
342
|
</AppContext>
|
|
344
343
|
);
|
|
345
344
|
}
|
|
@@ -613,9 +612,9 @@ describe("Shell", () => {
|
|
|
613
612
|
|
|
614
613
|
testSetup = await testRender(
|
|
615
614
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
616
|
-
<
|
|
615
|
+
<TestDialogProvider>
|
|
617
616
|
<Shell pluginRegistry={pluginRegistry} />
|
|
618
|
-
</
|
|
617
|
+
</TestDialogProvider>
|
|
619
618
|
</AppContext>,
|
|
620
619
|
{ width: 40, height: 10 },
|
|
621
620
|
);
|
|
@@ -655,7 +654,7 @@ describe("Shell", () => {
|
|
|
655
654
|
|
|
656
655
|
testSetup = await testRender(
|
|
657
656
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
658
|
-
<
|
|
657
|
+
<TestDialogProvider>
|
|
659
658
|
<Shell
|
|
660
659
|
pluginRegistry={pluginRegistry}
|
|
661
660
|
desktopWindowBridge={{
|
|
@@ -665,7 +664,7 @@ describe("Shell", () => {
|
|
|
665
664
|
subscribeDockPreview: () => () => {},
|
|
666
665
|
}}
|
|
667
666
|
/>
|
|
668
|
-
</
|
|
667
|
+
</TestDialogProvider>
|
|
669
668
|
</AppContext>,
|
|
670
669
|
{ width: 40, height: 10 },
|
|
671
670
|
);
|
|
@@ -704,9 +703,9 @@ describe("Shell", () => {
|
|
|
704
703
|
|
|
705
704
|
testSetup = await testRender(
|
|
706
705
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
707
|
-
<
|
|
706
|
+
<TestDialogProvider>
|
|
708
707
|
<Shell pluginRegistry={pluginRegistry} />
|
|
709
|
-
</
|
|
708
|
+
</TestDialogProvider>
|
|
710
709
|
</AppContext>,
|
|
711
710
|
{ width: 40, height: 10 },
|
|
712
711
|
);
|
|
@@ -823,9 +822,9 @@ describe("Shell", () => {
|
|
|
823
822
|
setSharedRegistryForTests(pluginRegistry);
|
|
824
823
|
|
|
825
824
|
testSetup = await testRender(
|
|
826
|
-
<
|
|
825
|
+
<TestDialogProvider>
|
|
827
826
|
<BrokerShellHarness pluginRegistry={pluginRegistry} />
|
|
828
|
-
</
|
|
827
|
+
</TestDialogProvider>,
|
|
829
828
|
{ width: 100, height: 24 },
|
|
830
829
|
);
|
|
831
830
|
|
|
@@ -916,9 +915,9 @@ describe("Shell", () => {
|
|
|
916
915
|
|
|
917
916
|
testSetup = await testRender(
|
|
918
917
|
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
919
|
-
<
|
|
918
|
+
<TestDialogProvider>
|
|
920
919
|
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
921
|
-
</
|
|
920
|
+
</TestDialogProvider>
|
|
922
921
|
</AppContext>,
|
|
923
922
|
{ width: 40, height: 10 },
|
|
924
923
|
);
|
|
@@ -946,6 +945,7 @@ describe("Shell", () => {
|
|
|
946
945
|
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
947
946
|
instances: [{ ...mainPane }, { ...detailPane }],
|
|
948
947
|
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
|
|
948
|
+
detached: [],
|
|
949
949
|
};
|
|
950
950
|
const state = {
|
|
951
951
|
...createInitialState({
|
|
@@ -959,9 +959,9 @@ describe("Shell", () => {
|
|
|
959
959
|
|
|
960
960
|
testSetup = await testRender(
|
|
961
961
|
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
962
|
-
<
|
|
962
|
+
<TestDialogProvider>
|
|
963
963
|
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
964
|
-
</
|
|
964
|
+
</TestDialogProvider>
|
|
965
965
|
</AppContext>,
|
|
966
966
|
{ width: 40, height: 12 },
|
|
967
967
|
);
|
|
@@ -989,6 +989,7 @@ describe("Shell", () => {
|
|
|
989
989
|
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
990
990
|
instances: [{ ...mainPane }, { ...detailPane }],
|
|
991
991
|
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
|
|
992
|
+
detached: [],
|
|
992
993
|
};
|
|
993
994
|
const state = {
|
|
994
995
|
...createInitialState({
|
|
@@ -1002,9 +1003,9 @@ describe("Shell", () => {
|
|
|
1002
1003
|
|
|
1003
1004
|
testSetup = await testRender(
|
|
1004
1005
|
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
1005
|
-
<
|
|
1006
|
+
<TestDialogProvider>
|
|
1006
1007
|
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
1007
|
-
</
|
|
1008
|
+
</TestDialogProvider>
|
|
1008
1009
|
</AppContext>,
|
|
1009
1010
|
{ width: 40, height: 12 },
|
|
1010
1011
|
);
|
|
@@ -1020,6 +1021,49 @@ describe("Shell", () => {
|
|
|
1020
1021
|
expect(updateLayout?.layout.floating).toEqual([]);
|
|
1021
1022
|
});
|
|
1022
1023
|
|
|
1024
|
+
test("closes the focused floating pane with Ctrl+W while plugin input is captured", async () => {
|
|
1025
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
1026
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
1027
|
+
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
1028
|
+
if (!mainPane || !detailPane) throw new Error("missing default panes");
|
|
1029
|
+
|
|
1030
|
+
const mixedLayout = {
|
|
1031
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
1032
|
+
instances: [{ ...mainPane }, { ...detailPane }],
|
|
1033
|
+
floating: [{ instanceId: "ticker-detail:main", x: 4, y: 2, width: 30, height: 8 }],
|
|
1034
|
+
detached: [],
|
|
1035
|
+
};
|
|
1036
|
+
const state = {
|
|
1037
|
+
...createInitialState({
|
|
1038
|
+
...config,
|
|
1039
|
+
layout: cloneLayout(mixedLayout),
|
|
1040
|
+
layouts: [{ name: "Default", layout: cloneLayout(mixedLayout) }],
|
|
1041
|
+
}),
|
|
1042
|
+
focusedPaneId: "ticker-detail:main",
|
|
1043
|
+
inputCaptured: true,
|
|
1044
|
+
};
|
|
1045
|
+
const actions: Array<any> = [];
|
|
1046
|
+
|
|
1047
|
+
testSetup = await testRender(
|
|
1048
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
1049
|
+
<TestDialogProvider>
|
|
1050
|
+
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
1051
|
+
</TestDialogProvider>
|
|
1052
|
+
</AppContext>,
|
|
1053
|
+
{ width: 40, height: 12 },
|
|
1054
|
+
);
|
|
1055
|
+
|
|
1056
|
+
await testSetup.renderOnce();
|
|
1057
|
+
await act(async () => {
|
|
1058
|
+
testSetup!.mockInput.pressKey("w", { ctrl: true });
|
|
1059
|
+
await testSetup!.renderOnce();
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
1063
|
+
expect(updateLayout?.layout.instances.map((instance: { instanceId: string }) => instance.instanceId)).toEqual(["portfolio-list:main"]);
|
|
1064
|
+
expect(updateLayout?.layout.floating).toEqual([]);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1023
1067
|
test("keeps a floating pane at the preview rect after a free drag release", () => {
|
|
1024
1068
|
const config = createDefaultConfig("/tmp/gloomberb-shell-drag-test");
|
|
1025
1069
|
const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main");
|
|
@@ -1153,7 +1197,7 @@ describe("Shell", () => {
|
|
|
1153
1197
|
|
|
1154
1198
|
testSetup = await testRender(
|
|
1155
1199
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
1156
|
-
<
|
|
1200
|
+
<TestDialogProvider>
|
|
1157
1201
|
<Shell pluginRegistry={createShellPluginRegistry({
|
|
1158
1202
|
tickerDetailComponent: ({ width, height }) => (
|
|
1159
1203
|
<box flexDirection="column" width={width} height={height}>
|
|
@@ -1164,7 +1208,7 @@ describe("Shell", () => {
|
|
|
1164
1208
|
</box>
|
|
1165
1209
|
),
|
|
1166
1210
|
})} />
|
|
1167
|
-
</
|
|
1211
|
+
</TestDialogProvider>
|
|
1168
1212
|
</AppContext>,
|
|
1169
1213
|
{ width: 40, height: 12 },
|
|
1170
1214
|
);
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
MIN_FLOAT_HEIGHT,
|
|
10
10
|
MIN_FLOAT_WIDTH,
|
|
11
11
|
applyDrop,
|
|
12
|
-
findDockLeaf,
|
|
13
12
|
floatAtRect,
|
|
14
13
|
floatPane,
|
|
15
14
|
getDockDividerLayouts,
|
|
@@ -22,7 +21,6 @@ import {
|
|
|
22
21
|
resolveDocked,
|
|
23
22
|
resolveFloating,
|
|
24
23
|
simulateDrop,
|
|
25
|
-
swapPanes,
|
|
26
24
|
type DockDividerLayout,
|
|
27
25
|
type DockLeafLayout,
|
|
28
26
|
type DropTarget,
|
|
@@ -218,6 +216,14 @@ export function resolvePaneManagementShortcut(
|
|
|
218
216
|
return null;
|
|
219
217
|
}
|
|
220
218
|
|
|
219
|
+
function inputCaptureAllowsPaneManagementShortcut(
|
|
220
|
+
shortcut: PaneManagementShortcut,
|
|
221
|
+
event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
|
|
222
|
+
): boolean {
|
|
223
|
+
if (shortcut !== "close") return false;
|
|
224
|
+
return event.meta || event.super || event.targetEditable !== true;
|
|
225
|
+
}
|
|
226
|
+
|
|
221
227
|
export function resolveAppHeaderHeightCells(options: { titleBarOverlay?: boolean; cellHeightPx?: number }): number {
|
|
222
228
|
if (!options.titleBarOverlay || !options.cellHeightPx || options.cellHeightPx <= 0) return DEFAULT_HEADER_HEIGHT;
|
|
223
229
|
return TITLEBAR_OVERLAY_HEIGHT_PX / options.cellHeightPx;
|
|
@@ -588,8 +594,8 @@ function resolveHeaderHitAreas(
|
|
|
588
594
|
actionStart: number | null;
|
|
589
595
|
closeStart: number | null;
|
|
590
596
|
} {
|
|
591
|
-
//
|
|
592
|
-
let rightEdge = options.focused ? width - 2 : width;
|
|
597
|
+
// Focused panes and all floating panes render ┌─...─┐, adding 2 chars on each side.
|
|
598
|
+
let rightEdge = options.focused || options.floating ? width - 2 : width;
|
|
593
599
|
let closeStart: number | null = null;
|
|
594
600
|
let actionStart: number | null = null;
|
|
595
601
|
|
|
@@ -966,7 +972,8 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
966
972
|
}
|
|
967
973
|
|
|
968
974
|
const shortcut = resolvePaneManagementShortcut(event);
|
|
969
|
-
if (!shortcut || dragRef.current || overlayOpen
|
|
975
|
+
if (!shortcut || dragRef.current || overlayOpen) return;
|
|
976
|
+
if (inputCaptured && !inputCaptureAllowsPaneManagementShortcut(shortcut, event)) return;
|
|
970
977
|
|
|
971
978
|
let handled = false;
|
|
972
979
|
switch (shortcut) {
|
|
@@ -1701,7 +1708,7 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
1701
1708
|
"data-active": active ? "true" : "false",
|
|
1702
1709
|
style: { "--divider-color": active ? colors.borderFocused : colors.border } as any,
|
|
1703
1710
|
} : {})}
|
|
1704
|
-
onMouseDown={nativePaneChrome ? (event) => startNativeDividerDrag(divider, event) : undefined}
|
|
1711
|
+
onMouseDown={nativePaneChrome ? (event: ShellMouseEvent) => startNativeDividerDrag(divider, event) : undefined}
|
|
1705
1712
|
onMouseDrag={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1706
1713
|
onMouseDragEnd={nativePaneChrome ? handleNativeDrag : undefined}
|
|
1707
1714
|
/>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AsciiText, Box,
|
|
1
|
+
import { AsciiText, Box, Span, Strong, Text, Underline, useUiHost } from "../../ui";
|
|
2
2
|
import { useState, useCallback, useEffect, useRef, useMemo, type RefObject } from "react";
|
|
3
3
|
import { useShortcut, useViewport } from "../../react/input";
|
|
4
4
|
import { TextAttributes } from "../../ui";
|
|
@@ -963,7 +963,6 @@ function PortfolioStep({
|
|
|
963
963
|
|
|
964
964
|
// broker-fields: dynamically render fields from the broker's configSchema
|
|
965
965
|
if (!selectedBrokerId) return null;
|
|
966
|
-
const currentField = brokerFields[brokerFieldIdx];
|
|
967
966
|
const values = brokerValues[selectedBrokerId] ?? {};
|
|
968
967
|
|
|
969
968
|
return (
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
PaneSettingTextField,
|
|
10
10
|
} from "../types/plugin";
|
|
11
11
|
import type { PluginRegistry } from "../plugins/registry";
|
|
12
|
-
import { colors } from "../theme/colors";
|
|
12
|
+
import { blendHex, colors } from "../theme/colors";
|
|
13
13
|
import { Button, DialogFrame, ListView, MultiSelectDialogContent, TextField } from "./ui";
|
|
14
14
|
import { NativeSelect, openNativeSelect, type NativeSelectElement } from "./ui/native-select";
|
|
15
15
|
|
|
@@ -80,6 +80,14 @@ function desktopText(weight?: number) {
|
|
|
80
80
|
return weight ? { ...DESKTOP_TEXT_STYLE, fontWeight: weight } : DESKTOP_TEXT_STYLE;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function desktopSubtleSurface(): string {
|
|
84
|
+
return blendHex(colors.panel, colors.bg, 0.22);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function desktopHoverSurface(): string {
|
|
88
|
+
return blendHex(colors.bg, colors.borderFocused, 0.08);
|
|
89
|
+
}
|
|
90
|
+
|
|
83
91
|
function DesktopDialogSurface({
|
|
84
92
|
title,
|
|
85
93
|
subtitle,
|
|
@@ -149,7 +157,7 @@ function DesktopSwitch({
|
|
|
149
157
|
flexDirection="row"
|
|
150
158
|
alignItems="center"
|
|
151
159
|
justifyContent={checked ? "flex-end" : "flex-start"}
|
|
152
|
-
backgroundColor={checked ? colors.selected :
|
|
160
|
+
backgroundColor={checked ? colors.selected : desktopSubtleSurface()}
|
|
153
161
|
onMouseDown={(event: any) => {
|
|
154
162
|
event.stopPropagation?.();
|
|
155
163
|
event.preventDefault?.();
|
|
@@ -182,7 +190,7 @@ function DesktopValuePill({ value }: { value: string }) {
|
|
|
182
190
|
<Box
|
|
183
191
|
flexDirection="row"
|
|
184
192
|
alignItems="center"
|
|
185
|
-
backgroundColor=
|
|
193
|
+
backgroundColor={desktopSubtleSurface()}
|
|
186
194
|
style={{
|
|
187
195
|
border: `1px solid ${colors.border}`,
|
|
188
196
|
borderRadius: 6,
|
|
@@ -236,7 +244,7 @@ function DesktopSettingsRow({
|
|
|
236
244
|
<Box
|
|
237
245
|
flexDirection="column"
|
|
238
246
|
minHeight={field.description ? undefined : 2}
|
|
239
|
-
backgroundColor={hovered || selected ?
|
|
247
|
+
backgroundColor={hovered || selected ? desktopHoverSurface() : "transparent"}
|
|
240
248
|
onMouseMove={onHover}
|
|
241
249
|
onMouseDown={field.type === "select" ? undefined : (event: any) => {
|
|
242
250
|
event.stopPropagation?.();
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act } from "react";
|
|
3
|
-
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
-
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
3
|
+
import { TestDialogProvider, testRender } from "../renderers/opentui/test-utils";
|
|
5
4
|
import { PaneTemplateTextareaStep } from "./pane-template-wizard";
|
|
6
5
|
|
|
7
6
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
@@ -18,7 +17,7 @@ describe("PaneTemplateTextareaStep", () => {
|
|
|
18
17
|
let resolved = "";
|
|
19
18
|
|
|
20
19
|
testSetup = await testRender(
|
|
21
|
-
<
|
|
20
|
+
<TestDialogProvider>
|
|
22
21
|
<PaneTemplateTextareaStep
|
|
23
22
|
resolve={(value) => { resolved = value; }}
|
|
24
23
|
dialogId="textarea-test"
|
|
@@ -30,7 +29,7 @@ describe("PaneTemplateTextareaStep", () => {
|
|
|
30
29
|
defaultValue: "Find cash-generative semiconductor suppliers",
|
|
31
30
|
}}
|
|
32
31
|
/>
|
|
33
|
-
</
|
|
32
|
+
</TestDialogProvider>,
|
|
34
33
|
{ width: 80, height: 16 },
|
|
35
34
|
);
|
|
36
35
|
|