gloomberb 0.9.2 → 0.9.3
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/README.md +23 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app.tsx +10 -7
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/drag/runtime.ts +21 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +395 -2
- package/src/components/layout/shell/index.tsx +128 -2
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +26 -14
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +105 -0
- package/src/components/layout/status-bar.tsx +32 -6
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/price-performance.tsx +9 -1
- package/src/core/app-services.ts +7 -4
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/pane.test.tsx +32 -0
- package/src/plugins/prediction-markets/pane.tsx +21 -1
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -0
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/plugin.ts +28 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
|
@@ -24,17 +24,21 @@ export function useComparisonChartSymbolSources(symbols: string[]): {
|
|
|
24
24
|
const marketFinancials = useTickerFinancialsMap(stableTickers);
|
|
25
25
|
const symbolSources = useMemo<ComparisonChartSymbolSource[]>(() => stableSymbols.map((symbol) => {
|
|
26
26
|
const ticker = tickers.get(symbol) ?? null;
|
|
27
|
-
const
|
|
27
|
+
const marketFinancial = marketFinancials.get(symbol) ?? null;
|
|
28
|
+
const storedFinancial = financials.get(symbol) ?? null;
|
|
28
29
|
const instrument = ticker?.metadata.broker_contracts?.[0] ?? null;
|
|
30
|
+
const quote = marketFinancial?.quote ?? storedFinancial?.quote;
|
|
29
31
|
return {
|
|
30
32
|
symbol,
|
|
31
|
-
currency:
|
|
32
|
-
quote
|
|
33
|
+
currency: quote?.currency ?? ticker?.metadata.currency,
|
|
34
|
+
quote,
|
|
33
35
|
exchange: ticker?.metadata.exchange ?? "",
|
|
34
36
|
brokerId: instrument?.brokerId,
|
|
35
37
|
brokerInstanceId: instrument?.brokerInstanceId,
|
|
36
38
|
instrument,
|
|
37
|
-
priceHistory:
|
|
39
|
+
priceHistory: marketFinancial?.priceHistory?.length
|
|
40
|
+
? marketFinancial.priceHistory
|
|
41
|
+
: storedFinancial?.priceHistory ?? [],
|
|
38
42
|
};
|
|
39
43
|
}), [financials, marketFinancials, stableSymbols, tickers]);
|
|
40
44
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ComparisonChartRenderMode,
|
|
13
13
|
type TimeRange,
|
|
14
14
|
} from "../core/types";
|
|
15
|
+
import type { MouseInteractionEvent } from "../core/pointer";
|
|
15
16
|
|
|
16
17
|
const MODE_CHIPS: Record<ComparisonChartRenderMode, string> = {
|
|
17
18
|
area: "A",
|
|
@@ -22,6 +23,7 @@ interface ComparisonChartToolbarProps {
|
|
|
22
23
|
activePreset: TimeRange | null;
|
|
23
24
|
availableManualResolutions: ManualChartResolution[];
|
|
24
25
|
effectiveResolution: ChartResolution;
|
|
26
|
+
focusPaneForMouseInteraction: (event: MouseInteractionEvent | null | undefined) => void;
|
|
25
27
|
isUpdating: boolean;
|
|
26
28
|
onRangeSelect: (range: TimeRange) => void;
|
|
27
29
|
onRenderModeSelect: (mode: ComparisonChartRenderMode) => void;
|
|
@@ -36,6 +38,7 @@ export function ComparisonChartToolbar({
|
|
|
36
38
|
activePreset,
|
|
37
39
|
availableManualResolutions,
|
|
38
40
|
effectiveResolution,
|
|
41
|
+
focusPaneForMouseInteraction,
|
|
39
42
|
isUpdating,
|
|
40
43
|
onRangeSelect,
|
|
41
44
|
onRenderModeSelect,
|
|
@@ -54,7 +57,8 @@ export function ComparisonChartToolbar({
|
|
|
54
57
|
key={range}
|
|
55
58
|
fg={activePreset === range ? colors.textBright : (isRangePresetSupported(range, availableManualResolutions) ? colors.textDim : colors.textMuted)}
|
|
56
59
|
attributes={activePreset === range ? TextAttributes.BOLD : 0}
|
|
57
|
-
onMouseDown={() => {
|
|
60
|
+
onMouseDown={(event: any) => {
|
|
61
|
+
focusPaneForMouseInteraction(event);
|
|
58
62
|
if (isRangePresetSupported(range, availableManualResolutions)) {
|
|
59
63
|
onRangeSelect(range);
|
|
60
64
|
}
|
|
@@ -73,7 +77,10 @@ export function ComparisonChartToolbar({
|
|
|
73
77
|
key={resolution}
|
|
74
78
|
fg={effectiveResolution === resolution ? colors.textBright : colors.textDim}
|
|
75
79
|
attributes={effectiveResolution === resolution ? TextAttributes.BOLD : 0}
|
|
76
|
-
onMouseDown={() =>
|
|
80
|
+
onMouseDown={(event: any) => {
|
|
81
|
+
focusPaneForMouseInteraction(event);
|
|
82
|
+
onResolutionSelect(resolution);
|
|
83
|
+
}}
|
|
77
84
|
>
|
|
78
85
|
{getChartResolutionLabel(resolution)}
|
|
79
86
|
</Text>
|
|
@@ -89,7 +96,10 @@ export function ComparisonChartToolbar({
|
|
|
89
96
|
key={mode}
|
|
90
97
|
fg={renderMode === mode ? colors.textBright : colors.textDim}
|
|
91
98
|
attributes={renderMode === mode ? TextAttributes.BOLD : 0}
|
|
92
|
-
onMouseDown={() =>
|
|
99
|
+
onMouseDown={(event: any) => {
|
|
100
|
+
focusPaneForMouseInteraction(event);
|
|
101
|
+
onRenderModeSelect(mode);
|
|
102
|
+
}}
|
|
93
103
|
>
|
|
94
104
|
{MODE_CHIPS[mode]}
|
|
95
105
|
</Text>
|
|
@@ -270,6 +270,7 @@ function ComparisonStockChartView({
|
|
|
270
270
|
isUpdating,
|
|
271
271
|
resolutionChips,
|
|
272
272
|
selectionSupportMap,
|
|
273
|
+
performanceHistoryBySymbol,
|
|
273
274
|
series,
|
|
274
275
|
supportMap,
|
|
275
276
|
} = useComparisonChartRenderData({
|
|
@@ -279,7 +280,11 @@ function ComparisonStockChartView({
|
|
|
279
280
|
const performanceBySymbol = useMemo(() => {
|
|
280
281
|
const bySymbol = new Map<string, PriceReturnField[]>();
|
|
281
282
|
for (const source of symbolSources) {
|
|
282
|
-
const
|
|
283
|
+
const baselineHistory = performanceHistoryBySymbol.get(source.symbol) ?? [];
|
|
284
|
+
const sourceHistory = appendQuoteToPriceReturnHistory(
|
|
285
|
+
baselineHistory.length >= 2 ? baselineHistory : source.priceHistory,
|
|
286
|
+
source.quote,
|
|
287
|
+
);
|
|
283
288
|
const fallbackHistory = series.find((entry) => entry.symbol === source.symbol)?.points ?? [];
|
|
284
289
|
bySymbol.set(
|
|
285
290
|
source.symbol,
|
|
@@ -287,7 +292,7 @@ function ComparisonStockChartView({
|
|
|
287
292
|
);
|
|
288
293
|
}
|
|
289
294
|
return bySymbol;
|
|
290
|
-
}, [series, symbolSources]);
|
|
295
|
+
}, [performanceHistoryBySymbol, series, symbolSources]);
|
|
291
296
|
const summarySymbols = useMemo(
|
|
292
297
|
() => sortSymbolsByOneYearReturn(symbols, performanceBySymbol),
|
|
293
298
|
[performanceBySymbol, symbols],
|
|
@@ -340,6 +345,7 @@ function ComparisonStockChartView({
|
|
|
340
345
|
displayCursor,
|
|
341
346
|
displayCursorX,
|
|
342
347
|
displayCursorY,
|
|
348
|
+
focusPaneForMouseInteraction,
|
|
343
349
|
mouseCrosshairDisabledRef,
|
|
344
350
|
plotRef,
|
|
345
351
|
scrollPanCellRemainderRef,
|
|
@@ -351,6 +357,7 @@ function ComparisonStockChartView({
|
|
|
351
357
|
chartWidth,
|
|
352
358
|
cursorX: viewState.cursorX,
|
|
353
359
|
cursorY: viewState.cursorY,
|
|
360
|
+
focused,
|
|
354
361
|
renderer,
|
|
355
362
|
setViewState,
|
|
356
363
|
symbols: summarySymbols,
|
|
@@ -444,6 +451,7 @@ function ComparisonStockChartView({
|
|
|
444
451
|
chartWidth,
|
|
445
452
|
commitSelectionCursor,
|
|
446
453
|
expandBufferRange,
|
|
454
|
+
focusPaneForMouseInteraction,
|
|
447
455
|
mouseCrosshairDisabledRef,
|
|
448
456
|
plotRef,
|
|
449
457
|
pointCount: projection.dates.length,
|
|
@@ -522,6 +530,7 @@ function ComparisonStockChartView({
|
|
|
522
530
|
legendActiveIndex={legendActiveIndex}
|
|
523
531
|
legendItemWidth={legendItemWidth}
|
|
524
532
|
legendRows={legendRows}
|
|
533
|
+
onFocusInteraction={focusPaneForMouseInteraction}
|
|
525
534
|
onOpenSymbol={onOpenSymbol}
|
|
526
535
|
onSelectSymbol={setSelectedSymbol}
|
|
527
536
|
performanceBySymbol={performanceBySymbol}
|
|
@@ -542,6 +551,7 @@ function ComparisonStockChartView({
|
|
|
542
551
|
activePreset={activePreset}
|
|
543
552
|
availableManualResolutions={availableManualResolutions}
|
|
544
553
|
effectiveResolution={effectiveResolution}
|
|
554
|
+
focusPaneForMouseInteraction={focusPaneForMouseInteraction}
|
|
545
555
|
isUpdating={isUpdating}
|
|
546
556
|
onRangeSelect={setRangePreset}
|
|
547
557
|
onRenderModeSelect={(mode) => setViewState((current) => ({ ...current, renderMode: mode }))}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createDefaultConfig,
|
|
4
|
+
findPaneInstance,
|
|
5
|
+
} from "../../../types/config";
|
|
2
6
|
import {
|
|
3
7
|
consumeStoredChartSelectionChange,
|
|
8
|
+
createChartRenderModeConfig,
|
|
4
9
|
createStoredChartSelectionSyncState,
|
|
5
10
|
markStoredChartSelectionLocallyApplied,
|
|
6
11
|
} from "./pane-settings";
|
|
@@ -22,4 +27,26 @@ describe("chart pane setting sync", () => {
|
|
|
22
27
|
expect(consumeStoredChartSelectionChange(state, "5Y", "1wk")).toBe(true);
|
|
23
28
|
expect(state.lastAppliedKey).toBe("5Y:1wk");
|
|
24
29
|
});
|
|
30
|
+
|
|
31
|
+
test("persists chart render mode as both pane state and global default", () => {
|
|
32
|
+
const config = createDefaultConfig("/tmp/gloomberb-chart-mode-test");
|
|
33
|
+
|
|
34
|
+
const nextConfig = createChartRenderModeConfig({
|
|
35
|
+
activePanel: "right",
|
|
36
|
+
config,
|
|
37
|
+
focusedPaneId: "ticker-detail:main",
|
|
38
|
+
paneState: {},
|
|
39
|
+
}, "ticker-detail:main", "candles");
|
|
40
|
+
|
|
41
|
+
expect(nextConfig.chartPreferences.defaultRenderMode).toBe("candles");
|
|
42
|
+
expect(findPaneInstance(nextConfig.layout, "ticker-detail:main")?.settings).toMatchObject({
|
|
43
|
+
chartRangePreset: "5Y",
|
|
44
|
+
chartRenderMode: "candles",
|
|
45
|
+
chartResolution: "auto",
|
|
46
|
+
});
|
|
47
|
+
expect(findPaneInstance(
|
|
48
|
+
nextConfig.layouts[nextConfig.activeLayoutIndex]!.layout,
|
|
49
|
+
"ticker-detail:main",
|
|
50
|
+
)?.settings?.chartRenderMode).toBe("candles");
|
|
51
|
+
});
|
|
25
52
|
});
|
|
@@ -7,7 +7,18 @@ import {
|
|
|
7
7
|
syncConfigActiveLayoutState,
|
|
8
8
|
} from "../../../state/app/context";
|
|
9
9
|
import { scheduleConfigSave } from "../../../state/config-save-scheduler";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
findPaneInstance,
|
|
12
|
+
type AppConfig,
|
|
13
|
+
} from "../../../types/config";
|
|
14
|
+
import type { ChartRenderMode, ChartResolution, TimeRange } from "./types";
|
|
15
|
+
|
|
16
|
+
interface ChartRenderModePersistenceState {
|
|
17
|
+
activePanel: "left" | "right";
|
|
18
|
+
config: AppConfig;
|
|
19
|
+
focusedPaneId: string | null;
|
|
20
|
+
paneState: Record<string, Record<string, unknown>>;
|
|
21
|
+
}
|
|
11
22
|
|
|
12
23
|
export interface StoredChartSelectionSyncState {
|
|
13
24
|
lastAppliedKey: string;
|
|
@@ -80,3 +91,51 @@ export function usePersistChartControlSelection(rangePresetKey: string): (
|
|
|
80
91
|
scheduleConfigSave(nextConfig);
|
|
81
92
|
};
|
|
82
93
|
}
|
|
94
|
+
|
|
95
|
+
export function createChartRenderModeConfig(
|
|
96
|
+
currentState: ChartRenderModePersistenceState,
|
|
97
|
+
paneId: string,
|
|
98
|
+
renderMode: ChartRenderMode,
|
|
99
|
+
): AppConfig {
|
|
100
|
+
const currentPane = findPaneInstance(currentState.config.layout, paneId);
|
|
101
|
+
const layout = currentPane
|
|
102
|
+
? setPaneSettings(currentState.config.layout, paneId, {
|
|
103
|
+
...(currentPane.settings ?? {}),
|
|
104
|
+
chartRenderMode: renderMode,
|
|
105
|
+
})
|
|
106
|
+
: currentState.config.layout;
|
|
107
|
+
return syncConfigActiveLayoutState(
|
|
108
|
+
{
|
|
109
|
+
...currentState.config,
|
|
110
|
+
chartPreferences: {
|
|
111
|
+
...currentState.config.chartPreferences,
|
|
112
|
+
defaultRenderMode: renderMode,
|
|
113
|
+
},
|
|
114
|
+
layout,
|
|
115
|
+
},
|
|
116
|
+
currentState.paneState,
|
|
117
|
+
currentState.focusedPaneId,
|
|
118
|
+
currentState.activePanel,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function usePersistChartRenderMode(): (renderMode: ChartRenderMode) => void {
|
|
123
|
+
const dispatch = useAppDispatch();
|
|
124
|
+
const stateRef = useAppStateRef();
|
|
125
|
+
const paneId = usePaneInstanceId();
|
|
126
|
+
|
|
127
|
+
return (renderMode) => {
|
|
128
|
+
const currentState = stateRef.current;
|
|
129
|
+
const currentPane = findPaneInstance(currentState.config.layout, paneId);
|
|
130
|
+
if (
|
|
131
|
+
currentState.config.chartPreferences.defaultRenderMode === renderMode
|
|
132
|
+
&& currentPane?.settings?.chartRenderMode === renderMode
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const nextConfig = createChartRenderModeConfig(currentState, paneId, renderMode);
|
|
138
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
139
|
+
scheduleConfigSave(nextConfig);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -31,6 +31,8 @@ export interface ChartMouseEvent {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export type MouseInteractionEvent = Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">;
|
|
35
|
+
|
|
34
36
|
export function consumeChartMouseEvent(event: Pick<ChartMouseEvent, "stopPropagation" | "preventDefault">): void {
|
|
35
37
|
event.stopPropagation?.();
|
|
36
38
|
event.preventDefault?.();
|
|
@@ -221,12 +221,12 @@ export function useResolvedStockChartRuntime({
|
|
|
221
221
|
pendingCanonicalResetRef: interactionRuntime.pendingCanonicalResetRef,
|
|
222
222
|
pendingExpansionRef: interactionRuntime.pendingExpansionRef,
|
|
223
223
|
persistChartControls: settings.persistChartControls,
|
|
224
|
+
persistChartRenderMode: settings.persistChartRenderMode,
|
|
224
225
|
renderedResolution: dataRuntime.renderedResolution,
|
|
225
226
|
selectionSupportMap: resolutionRuntime.selectionSupportMap,
|
|
226
227
|
setPendingAutoWindowOverride: dataRuntime.setPendingAutoWindowOverride,
|
|
227
228
|
setRenderedAutoView: dataRuntime.setRenderedAutoView,
|
|
228
229
|
setRequestedResolution: settings.setRequestedResolution,
|
|
229
|
-
setStoredRenderMode: settings.setStoredRenderMode,
|
|
230
230
|
setViewState: settings.setViewState,
|
|
231
231
|
storedRangePreset: settings.storedRangePreset,
|
|
232
232
|
storedRenderMode: settings.storedRenderMode,
|
|
@@ -3,7 +3,10 @@ import {
|
|
|
3
3
|
usePaneInstance,
|
|
4
4
|
usePaneSettingValue,
|
|
5
5
|
} from "../../../state/app/context";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
usePersistChartControlSelection,
|
|
8
|
+
usePersistChartRenderMode,
|
|
9
|
+
} from "../core/pane-settings";
|
|
7
10
|
import type { IndicatorConfig } from "../indicators/types";
|
|
8
11
|
import {
|
|
9
12
|
DEFAULT_TICKER_CHART_RANGE_PRESET,
|
|
@@ -30,8 +33,9 @@ export function useStockChartSettings({
|
|
|
30
33
|
const pane = usePaneInstance();
|
|
31
34
|
const [storedRangePreset] = usePaneSettingValue("chartRangePreset", DEFAULT_TICKER_CHART_RANGE_PRESET);
|
|
32
35
|
const [storedResolution] = usePaneSettingValue<ChartResolution>("chartResolution", DEFAULT_TICKER_CHART_RESOLUTION);
|
|
33
|
-
const [storedRenderMode
|
|
36
|
+
const [storedRenderMode] = usePaneSettingValue<ChartRenderMode>("chartRenderMode", defaultRenderMode);
|
|
34
37
|
const persistChartControls = usePersistChartControlSelection("chartRangePreset");
|
|
38
|
+
const persistChartRenderMode = usePersistChartRenderMode();
|
|
35
39
|
const [viewState, setViewState] = useState<StockChartViewportState>({
|
|
36
40
|
presetRange: compact ? "1Y" : storedRangePreset,
|
|
37
41
|
bufferRange: compact ? "1Y" : storedRangePreset,
|
|
@@ -67,7 +71,7 @@ export function useStockChartSettings({
|
|
|
67
71
|
persistChartControls,
|
|
68
72
|
requestedResolution,
|
|
69
73
|
setRequestedResolution,
|
|
70
|
-
|
|
74
|
+
persistChartRenderMode,
|
|
71
75
|
setViewState,
|
|
72
76
|
storedRangePreset,
|
|
73
77
|
storedRenderMode,
|
|
@@ -67,10 +67,10 @@ interface UseStockChartViewportRuntimeOptions {
|
|
|
67
67
|
persistChartControls: (range: TimeRange, resolution: ChartResolution) => void;
|
|
68
68
|
renderedResolution: ChartResolution;
|
|
69
69
|
selectionSupportMap: ReadonlyMap<ManualChartResolution, TimeRange>;
|
|
70
|
+
persistChartRenderMode: (mode: ChartRenderMode) => void;
|
|
70
71
|
setPendingAutoWindowOverride: Dispatch<SetStateAction<DateWindowRange | null>>;
|
|
71
72
|
setRenderedAutoView: Dispatch<SetStateAction<AutoRenderedView | null>>;
|
|
72
73
|
setRequestedResolution: Dispatch<SetStateAction<ChartResolution>>;
|
|
73
|
-
setStoredRenderMode: Dispatch<SetStateAction<ChartRenderMode>>;
|
|
74
74
|
setViewState: Dispatch<SetStateAction<StockChartViewportState>>;
|
|
75
75
|
storedRangePreset: TimeRange;
|
|
76
76
|
storedRenderMode: ChartRenderMode;
|
|
@@ -101,12 +101,12 @@ export function useStockChartViewportRuntime({
|
|
|
101
101
|
pendingCanonicalResetRef,
|
|
102
102
|
pendingExpansionRef,
|
|
103
103
|
persistChartControls,
|
|
104
|
+
persistChartRenderMode,
|
|
104
105
|
renderedResolution,
|
|
105
106
|
selectionSupportMap,
|
|
106
107
|
setPendingAutoWindowOverride,
|
|
107
108
|
setRenderedAutoView,
|
|
108
109
|
setRequestedResolution,
|
|
109
|
-
setStoredRenderMode,
|
|
110
110
|
setViewState,
|
|
111
111
|
storedRangePreset,
|
|
112
112
|
storedRenderMode,
|
|
@@ -172,10 +172,10 @@ export function useStockChartViewportRuntime({
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
const persistRenderMode = useCallback((nextMode: ChartRenderMode) => {
|
|
175
|
-
if (!compact
|
|
176
|
-
|
|
175
|
+
if (!compact) {
|
|
176
|
+
persistChartRenderMode(nextMode);
|
|
177
177
|
}
|
|
178
|
-
}, [compact,
|
|
178
|
+
}, [compact, persistChartRenderMode]);
|
|
179
179
|
|
|
180
180
|
const expandBufferRange = useCallback((action: PendingExpansionAction): boolean => {
|
|
181
181
|
if (compact) return false;
|
|
@@ -16,7 +16,17 @@ import {
|
|
|
16
16
|
getPaneTemplateDisplayLabel,
|
|
17
17
|
} from "./items";
|
|
18
18
|
|
|
19
|
+
function paneTemplateHasConfigFields(template: PaneTemplateDef): boolean {
|
|
20
|
+
if (template.wizard?.some((step) => step.type !== "info")) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return !!buildGeneratedTemplateField(template, null).field;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export function shouldOpenPaneTemplateConfig(template: PaneTemplateDef, arg?: string): boolean {
|
|
27
|
+
if (!paneTemplateHasConfigFields(template)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
20
30
|
if (template.wizard && template.wizard.length > 0) {
|
|
21
31
|
if (!arg?.trim()) {
|
|
22
32
|
return true;
|
|
@@ -117,6 +117,42 @@ function registerOptionalTextPane(pluginRegistry: MutablePaneRegistry): void {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
function registerQueryOnlyPane(pluginRegistry: MutablePaneRegistry): void {
|
|
121
|
+
mutableRegistryMap(pluginRegistry.panes).set("prediction-markets", {
|
|
122
|
+
id: "prediction-markets",
|
|
123
|
+
name: "Prediction Markets",
|
|
124
|
+
component: () => null,
|
|
125
|
+
defaultPosition: "left",
|
|
126
|
+
defaultMode: "floating",
|
|
127
|
+
});
|
|
128
|
+
mutableRegistryMap(pluginRegistry.paneTemplates).set("new-prediction-markets-pane", {
|
|
129
|
+
id: "new-prediction-markets-pane",
|
|
130
|
+
paneId: "prediction-markets",
|
|
131
|
+
label: "Prediction Markets",
|
|
132
|
+
description: "Open a new prediction markets browser pane",
|
|
133
|
+
keywords: ["prediction", "markets", "polymarket", "kalshi", "events"],
|
|
134
|
+
shortcut: { prefix: "PM", argPlaceholder: "query", argKind: "text" },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function registerShortcutOnlyPane(pluginRegistry: MutablePaneRegistry): void {
|
|
139
|
+
mutableRegistryMap(pluginRegistry.panes).set("top-news", {
|
|
140
|
+
id: "top-news",
|
|
141
|
+
name: "Top News",
|
|
142
|
+
component: () => null,
|
|
143
|
+
defaultPosition: "right",
|
|
144
|
+
defaultMode: "floating",
|
|
145
|
+
});
|
|
146
|
+
mutableRegistryMap(pluginRegistry.paneTemplates).set("top-news-pane", {
|
|
147
|
+
id: "top-news-pane",
|
|
148
|
+
paneId: "top-news",
|
|
149
|
+
label: "Top News",
|
|
150
|
+
description: "Curated top market stories ranked by importance",
|
|
151
|
+
keywords: ["top", "news", "headlines", "stories"],
|
|
152
|
+
shortcut: { prefix: "TOP" },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
120
156
|
describe("CommandBar pane and layout routes", () => {
|
|
121
157
|
const layoutModeConfig = (config: AppConfig): AppConfig => {
|
|
122
158
|
const research = cloneLayout(config.layout);
|
|
@@ -271,6 +307,46 @@ describe("CommandBar pane and layout routes", () => {
|
|
|
271
307
|
expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
|
|
272
308
|
});
|
|
273
309
|
|
|
310
|
+
for (const scenario of [
|
|
311
|
+
{
|
|
312
|
+
query: "prediction markets",
|
|
313
|
+
register: registerQueryOnlyPane,
|
|
314
|
+
templateId: "new-prediction-markets-pane",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
query: "top news",
|
|
318
|
+
register: registerShortcutOnlyPane,
|
|
319
|
+
templateId: "top-news-pane",
|
|
320
|
+
},
|
|
321
|
+
] as const) {
|
|
322
|
+
test(`creates ${scenario.templateId} directly when it has no config fields`, async () => {
|
|
323
|
+
const created: CreatedPaneCall[] = [];
|
|
324
|
+
|
|
325
|
+
testSetup = await testRender(<CommandBarHarness
|
|
326
|
+
query={scenario.query}
|
|
327
|
+
live
|
|
328
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
329
|
+
scenario.register(pluginRegistry);
|
|
330
|
+
recordPaneCreations(pluginRegistry, created);
|
|
331
|
+
}}
|
|
332
|
+
/>, {
|
|
333
|
+
width: 100,
|
|
334
|
+
height: 18,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
await testSetup.renderOnce();
|
|
338
|
+
|
|
339
|
+
await act(async () => {
|
|
340
|
+
testSetup!.mockInput.pressEnter();
|
|
341
|
+
await Bun.sleep(0);
|
|
342
|
+
await testSetup!.renderOnce();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
expect(created).toEqual([{ templateId: scenario.templateId, options: undefined }]);
|
|
346
|
+
expect(testSetup.captureCharFrame()).not.toContain("Create Pane");
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
274
350
|
test("shows pane templates that share the same shortcut", async () => {
|
|
275
351
|
testSetup = await testRender(<CommandBarHarness
|
|
276
352
|
query="DUP"
|
|
@@ -26,6 +26,11 @@ export type DataTableKeyEvent = TableViewKeyEvent;
|
|
|
26
26
|
|
|
27
27
|
const DATA_TABLE_SELECTION_COMMIT_DELAY_MS = 150;
|
|
28
28
|
|
|
29
|
+
export interface DataTableRootKeyContext {
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
itemCount: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export type DataTableSelectionChangeReason =
|
|
30
35
|
| "keyboard"
|
|
31
36
|
| "pointer"
|
|
@@ -95,7 +100,10 @@ export interface DataTableViewProps<
|
|
|
95
100
|
hoveredIdx?: number | null;
|
|
96
101
|
setHoveredIdx?: (index: number | null) => void;
|
|
97
102
|
keyboardNavigation?: boolean;
|
|
98
|
-
onRootKeyDown?: (
|
|
103
|
+
onRootKeyDown?: (
|
|
104
|
+
event: DataTableKeyEvent,
|
|
105
|
+
context: DataTableRootKeyContext,
|
|
106
|
+
) => boolean | void;
|
|
99
107
|
resetScrollKey?: unknown;
|
|
100
108
|
}
|
|
101
109
|
|
|
@@ -486,7 +494,10 @@ export function DataTableView<
|
|
|
486
494
|
if (event.defaultPrevented || event.propagationStopped) return;
|
|
487
495
|
if (!focused || !keyboardNavigation) return;
|
|
488
496
|
|
|
489
|
-
if (onRootKeyDown?.(event
|
|
497
|
+
if (onRootKeyDown?.(event, {
|
|
498
|
+
selectedIndex: effectiveSelectedIndexRef.current,
|
|
499
|
+
itemCount: tableProps.items.length,
|
|
500
|
+
})) return;
|
|
490
501
|
if (tableProps.items.length === 0) return;
|
|
491
502
|
|
|
492
503
|
if (isNextTableRowKey(event)) {
|
|
@@ -6,6 +6,7 @@ import { formatTimeAgo } from "../../utils/format";
|
|
|
6
6
|
import { isPlainKey } from "../../utils/keyboard";
|
|
7
7
|
import { toTimestampMillis } from "../../utils/timestamp";
|
|
8
8
|
import { DataTableStackView } from "../data-table/stack-view";
|
|
9
|
+
import type { DataTableRootKeyContext } from "../data-table/view";
|
|
9
10
|
import {
|
|
10
11
|
activeStackIndex,
|
|
11
12
|
sortIndexedStackRows,
|
|
@@ -45,9 +46,16 @@ interface FeedDataTableStackViewProps {
|
|
|
45
46
|
rootAfter?: ReactNode;
|
|
46
47
|
onRootKeyDown?: (event: {
|
|
47
48
|
name?: string;
|
|
49
|
+
sequence?: string;
|
|
50
|
+
ctrl?: boolean;
|
|
51
|
+
meta?: boolean;
|
|
52
|
+
super?: boolean;
|
|
53
|
+
alt?: boolean;
|
|
54
|
+
option?: boolean;
|
|
55
|
+
shift?: boolean;
|
|
48
56
|
preventDefault?: () => void;
|
|
49
57
|
stopPropagation?: () => void;
|
|
50
|
-
}) => boolean | void;
|
|
58
|
+
}, context: DataTableRootKeyContext) => boolean | void;
|
|
51
59
|
sourceLabel?: string;
|
|
52
60
|
titleLabel?: string;
|
|
53
61
|
emptyStateTitle?: string;
|
package/src/components/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type { TickerListVisibleRange } from "./ticker/list-table-view";
|
|
|
8
8
|
export { TickerBadgeList } from "./ticker/badge/list";
|
|
9
9
|
export { InputSearchBar } from "./input-search-bar";
|
|
10
10
|
export { DataTableView } from "./data-table/view";
|
|
11
|
-
export type { DataTableKeyEvent } from "./data-table/view";
|
|
11
|
+
export type { DataTableKeyEvent, DataTableRootKeyContext } from "./data-table/view";
|
|
12
12
|
export { DataTableStackView } from "./data-table/stack-view";
|
|
13
13
|
export { FeedDataTableStackView } from "./feed-data-table/stack-view";
|
|
14
14
|
export type { FeedDataTableItem } from "./feed-data-table/stack-view";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useCallback, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
+
import { AppContext, createInitialState, type AppAction } from "../state/app/context";
|
|
5
|
+
import { createDefaultConfig } from "../types/config";
|
|
6
|
+
import type { InputRenderable } from "../ui";
|
|
7
|
+
import { InputSearchBar } from "./input-search-bar";
|
|
8
|
+
|
|
9
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
|
+
let setSearchActive: Dispatch<SetStateAction<boolean>> | null = null;
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
setSearchActive = null;
|
|
14
|
+
if (!testSetup) return;
|
|
15
|
+
await act(async () => {
|
|
16
|
+
testSetup!.renderer.destroy();
|
|
17
|
+
});
|
|
18
|
+
testSetup = undefined;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function Harness({
|
|
22
|
+
actions,
|
|
23
|
+
onNavigateDown,
|
|
24
|
+
}: {
|
|
25
|
+
actions: AppAction[];
|
|
26
|
+
onNavigateDown?: () => void;
|
|
27
|
+
}) {
|
|
28
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-input-search-bar"));
|
|
29
|
+
const inputRef = useRef<InputRenderable | null>(null);
|
|
30
|
+
const [active, setActive] = useState(true);
|
|
31
|
+
setSearchActive = setActive;
|
|
32
|
+
const dispatch = useCallback((action: AppAction) => {
|
|
33
|
+
actions.push(action);
|
|
34
|
+
}, [actions]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AppContext value={{ state, dispatch }}>
|
|
38
|
+
<InputSearchBar
|
|
39
|
+
value=""
|
|
40
|
+
focused
|
|
41
|
+
active={active}
|
|
42
|
+
width={30}
|
|
43
|
+
focusToken={0}
|
|
44
|
+
inputRef={inputRef}
|
|
45
|
+
placeholder="search"
|
|
46
|
+
debounceMs={100}
|
|
47
|
+
onNavigateDown={onNavigateDown}
|
|
48
|
+
onFocus={() => {}}
|
|
49
|
+
onBlur={() => {}}
|
|
50
|
+
onQueryChange={() => {}}
|
|
51
|
+
/>
|
|
52
|
+
</AppContext>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("InputSearchBar", () => {
|
|
57
|
+
test("captures app input while the search input is active", async () => {
|
|
58
|
+
const actions: AppAction[] = [];
|
|
59
|
+
|
|
60
|
+
testSetup = await testRender(<Harness actions={actions} />, { width: 40, height: 4 });
|
|
61
|
+
await act(async () => {
|
|
62
|
+
await testSetup!.renderOnce();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(actions).toEqual([{ type: "SET_INPUT_CAPTURED", captured: true }]);
|
|
66
|
+
if (!setSearchActive) throw new Error("search setter was not registered");
|
|
67
|
+
|
|
68
|
+
await act(async () => {
|
|
69
|
+
setSearchActive(false);
|
|
70
|
+
await testSetup!.renderOnce();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(actions).toEqual([
|
|
74
|
+
{ type: "SET_INPUT_CAPTURED", captured: true },
|
|
75
|
+
{ type: "SET_INPUT_CAPTURED", captured: false },
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("moves from the active search input to the table with Down", async () => {
|
|
80
|
+
const actions: AppAction[] = [];
|
|
81
|
+
let navigatedDown = 0;
|
|
82
|
+
|
|
83
|
+
testSetup = await testRender(
|
|
84
|
+
<Harness
|
|
85
|
+
actions={actions}
|
|
86
|
+
onNavigateDown={() => {
|
|
87
|
+
navigatedDown += 1;
|
|
88
|
+
}}
|
|
89
|
+
/>,
|
|
90
|
+
{ width: 40, height: 4 },
|
|
91
|
+
);
|
|
92
|
+
await act(async () => {
|
|
93
|
+
await testSetup!.renderOnce();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await act(async () => {
|
|
97
|
+
testSetup!.mockInput.pressArrow("down");
|
|
98
|
+
await testSetup!.renderOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(navigatedDown).toBe(1);
|
|
102
|
+
});
|
|
103
|
+
});
|