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
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useImperativeHandle, useLayoutEffect, useRef, type ForwardedRef, type RefObject } from "react";
|
|
2
|
+
import { useShortcut } from "../react/input";
|
|
3
|
+
import { useNativeRenderer, type ScrollBoxRenderable } from "../ui/host";
|
|
4
|
+
import { useAppSelector, useOptionalPaneInstanceId } from "./app-context";
|
|
5
|
+
|
|
6
|
+
type ScrollDirection = "up" | "down";
|
|
7
|
+
|
|
8
|
+
interface PaneScrollAction {
|
|
9
|
+
direction: ScrollDirection;
|
|
10
|
+
resolveTarget(scrollBox: ScrollBoxRenderable): number | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PaneScrollEntry {
|
|
14
|
+
order: number;
|
|
15
|
+
ref: RefObject<ScrollBoxRenderable | null>;
|
|
16
|
+
onScrollActivity?: (event: { scroll: { direction: ScrollDirection; delta: number } }) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface PaneScrollKeyEvent {
|
|
20
|
+
name?: string;
|
|
21
|
+
ctrl?: boolean;
|
|
22
|
+
meta?: boolean;
|
|
23
|
+
alt?: boolean;
|
|
24
|
+
option?: boolean;
|
|
25
|
+
defaultPrevented?: boolean;
|
|
26
|
+
propagationStopped?: boolean;
|
|
27
|
+
preventDefault?: () => void;
|
|
28
|
+
stopPropagation?: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const paneScrollBoxes = new Map<string, Map<number, PaneScrollEntry>>();
|
|
32
|
+
let nextScrollEntryId = 1;
|
|
33
|
+
let nextScrollEntryOrder = 1;
|
|
34
|
+
const MIN_ARROW_SCROLL_LINES = 3;
|
|
35
|
+
|
|
36
|
+
function hasHiddenAncestor(scrollBox: ScrollBoxRenderable): boolean {
|
|
37
|
+
let node: unknown = scrollBox;
|
|
38
|
+
const seen = new Set<unknown>();
|
|
39
|
+
|
|
40
|
+
while (node && typeof node === "object" && !seen.has(node)) {
|
|
41
|
+
seen.add(node);
|
|
42
|
+
const renderable = node as { visible?: unknown; parent?: unknown };
|
|
43
|
+
if (renderable.visible === false) return true;
|
|
44
|
+
node = renderable.parent;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function hasCollapsedBounds(scrollBox: ScrollBoxRenderable): boolean {
|
|
51
|
+
const bounds = scrollBox.getBoundingClientRect?.();
|
|
52
|
+
return bounds != null && (bounds.width <= 0 || bounds.height <= 0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isVisibleScrollBox(scrollBox: ScrollBoxRenderable): boolean {
|
|
56
|
+
return !hasHiddenAncestor(scrollBox) && !hasCollapsedBounds(scrollBox);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clampScrollTop(scrollBox: ScrollBoxRenderable, target: number): number | null {
|
|
60
|
+
const viewportHeight = Math.max(0, scrollBox.viewport?.height ?? 0);
|
|
61
|
+
if (viewportHeight <= 0) return null;
|
|
62
|
+
const maxScrollTop = Math.max(0, scrollBox.scrollHeight - viewportHeight);
|
|
63
|
+
if (maxScrollTop <= 0) return null;
|
|
64
|
+
return Math.max(0, Math.min(maxScrollTop, target));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function scrollTargetByDelta(scrollBox: ScrollBoxRenderable, delta: number): number | null {
|
|
68
|
+
const target = clampScrollTop(scrollBox, scrollBox.scrollTop + delta);
|
|
69
|
+
return target == null || target === scrollBox.scrollTop ? null : target;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function arrowScrollLines(scrollBox: ScrollBoxRenderable): number {
|
|
73
|
+
return Math.max(MIN_ARROW_SCROLL_LINES, Math.floor((scrollBox.viewport?.height ?? 0) / 4));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveKeyScrollAction(event: PaneScrollKeyEvent): PaneScrollAction | null {
|
|
77
|
+
if (event.ctrl || event.meta || event.alt || event.option) return null;
|
|
78
|
+
|
|
79
|
+
switch (event.name) {
|
|
80
|
+
case "down":
|
|
81
|
+
return {
|
|
82
|
+
direction: "down",
|
|
83
|
+
resolveTarget: (scrollBox) => scrollTargetByDelta(scrollBox, arrowScrollLines(scrollBox)),
|
|
84
|
+
};
|
|
85
|
+
case "up":
|
|
86
|
+
return {
|
|
87
|
+
direction: "up",
|
|
88
|
+
resolveTarget: (scrollBox) => scrollTargetByDelta(scrollBox, -arrowScrollLines(scrollBox)),
|
|
89
|
+
};
|
|
90
|
+
case "pagedown":
|
|
91
|
+
return {
|
|
92
|
+
direction: "down",
|
|
93
|
+
resolveTarget: (scrollBox) => scrollTargetByDelta(
|
|
94
|
+
scrollBox,
|
|
95
|
+
Math.max(1, (scrollBox.viewport?.height ?? 1) - 1),
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
case "pageup":
|
|
99
|
+
return {
|
|
100
|
+
direction: "up",
|
|
101
|
+
resolveTarget: (scrollBox) => scrollTargetByDelta(
|
|
102
|
+
scrollBox,
|
|
103
|
+
-Math.max(1, (scrollBox.viewport?.height ?? 1) - 1),
|
|
104
|
+
),
|
|
105
|
+
};
|
|
106
|
+
case "home":
|
|
107
|
+
return {
|
|
108
|
+
direction: "up",
|
|
109
|
+
resolveTarget: (scrollBox) => (
|
|
110
|
+
scrollBox.scrollTop > 0 ? 0 : null
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
case "end":
|
|
114
|
+
return {
|
|
115
|
+
direction: "down",
|
|
116
|
+
resolveTarget: (scrollBox) => {
|
|
117
|
+
const target = clampScrollTop(scrollBox, Number.MAX_SAFE_INTEGER);
|
|
118
|
+
return target == null || target === scrollBox.scrollTop ? null : target;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getEntryScore(entry: PaneScrollEntry, scrollBox: ScrollBoxRenderable): number {
|
|
127
|
+
const viewportHeight = scrollBox.viewport?.height ?? 0;
|
|
128
|
+
const scrollHeight = scrollBox.scrollHeight;
|
|
129
|
+
return viewportHeight * 100_000 + Math.min(scrollHeight, 99_999) + entry.order / 100_000;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findScrollableEntry(paneId: string, action: PaneScrollAction): {
|
|
133
|
+
entry: PaneScrollEntry;
|
|
134
|
+
scrollBox: ScrollBoxRenderable;
|
|
135
|
+
target: number;
|
|
136
|
+
} | null {
|
|
137
|
+
const entries = paneScrollBoxes.get(paneId);
|
|
138
|
+
if (!entries) return null;
|
|
139
|
+
|
|
140
|
+
let best: { entry: PaneScrollEntry; scrollBox: ScrollBoxRenderable; target: number; score: number } | null = null;
|
|
141
|
+
for (const entry of entries.values()) {
|
|
142
|
+
const scrollBox = entry.ref.current;
|
|
143
|
+
if (!scrollBox) continue;
|
|
144
|
+
if (!isVisibleScrollBox(scrollBox)) continue;
|
|
145
|
+
|
|
146
|
+
const target = action.resolveTarget(scrollBox);
|
|
147
|
+
if (target == null) continue;
|
|
148
|
+
|
|
149
|
+
const score = getEntryScore(entry, scrollBox);
|
|
150
|
+
if (!best || score > best.score) {
|
|
151
|
+
best = { entry, scrollBox, target, score };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!best) return null;
|
|
156
|
+
return { entry: best.entry, scrollBox: best.scrollBox, target: best.target };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function scrollPaneByKey(paneId: string, event: PaneScrollKeyEvent): boolean {
|
|
160
|
+
const action = resolveKeyScrollAction(event);
|
|
161
|
+
if (!action) return false;
|
|
162
|
+
|
|
163
|
+
const match = findScrollableEntry(paneId, action);
|
|
164
|
+
if (!match) return false;
|
|
165
|
+
|
|
166
|
+
const previousTop = match.scrollBox.scrollTop;
|
|
167
|
+
match.scrollBox.scrollTo(match.target);
|
|
168
|
+
if (match.scrollBox.scrollTop !== match.target) {
|
|
169
|
+
match.scrollBox.scrollTop = match.target;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const delta = Math.abs(match.scrollBox.scrollTop - previousTop);
|
|
173
|
+
if (delta > 0) {
|
|
174
|
+
match.entry.onScrollActivity?.({ scroll: { direction: action.direction, delta } });
|
|
175
|
+
}
|
|
176
|
+
return delta > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function useForwardedScrollBoxRef<T>(
|
|
180
|
+
forwardedRef: ForwardedRef<T>,
|
|
181
|
+
): RefObject<T | null> {
|
|
182
|
+
const localRef = useRef<T | null>(null);
|
|
183
|
+
useImperativeHandle(forwardedRef, () => localRef.current as T);
|
|
184
|
+
return localRef;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function useRegisterPaneScrollBox(
|
|
188
|
+
ref: RefObject<ScrollBoxRenderable | null>,
|
|
189
|
+
options: {
|
|
190
|
+
enabled: boolean;
|
|
191
|
+
onScrollActivity?: PaneScrollEntry["onScrollActivity"];
|
|
192
|
+
},
|
|
193
|
+
): void {
|
|
194
|
+
const paneId = useOptionalPaneInstanceId();
|
|
195
|
+
const entryIdRef = useRef<number | null>(null);
|
|
196
|
+
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
if (!paneId || !options.enabled) return;
|
|
199
|
+
|
|
200
|
+
const entryId = nextScrollEntryId++;
|
|
201
|
+
entryIdRef.current = entryId;
|
|
202
|
+
let entries = paneScrollBoxes.get(paneId);
|
|
203
|
+
if (!entries) {
|
|
204
|
+
entries = new Map();
|
|
205
|
+
paneScrollBoxes.set(paneId, entries);
|
|
206
|
+
}
|
|
207
|
+
entries.set(entryId, {
|
|
208
|
+
order: nextScrollEntryOrder++,
|
|
209
|
+
ref,
|
|
210
|
+
onScrollActivity: options.onScrollActivity,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return () => {
|
|
214
|
+
const currentEntries = paneScrollBoxes.get(paneId);
|
|
215
|
+
currentEntries?.delete(entryId);
|
|
216
|
+
if (currentEntries?.size === 0) {
|
|
217
|
+
paneScrollBoxes.delete(paneId);
|
|
218
|
+
}
|
|
219
|
+
if (entryIdRef.current === entryId) {
|
|
220
|
+
entryIdRef.current = null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}, [paneId, options.enabled, options.onScrollActivity, ref]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function PaneKeyboardScrollController({
|
|
227
|
+
paneId,
|
|
228
|
+
focused,
|
|
229
|
+
}: {
|
|
230
|
+
paneId: string;
|
|
231
|
+
focused: boolean;
|
|
232
|
+
}) {
|
|
233
|
+
const nativeRenderer = useNativeRenderer();
|
|
234
|
+
const inputCaptured = useAppSelector((state) => state.inputCaptured);
|
|
235
|
+
const focusedRef = useRef(focused);
|
|
236
|
+
const inputCapturedRef = useRef(inputCaptured);
|
|
237
|
+
const paneIdRef = useRef(paneId);
|
|
238
|
+
|
|
239
|
+
focusedRef.current = focused;
|
|
240
|
+
inputCapturedRef.current = inputCaptured;
|
|
241
|
+
paneIdRef.current = paneId;
|
|
242
|
+
|
|
243
|
+
useShortcut((event) => {
|
|
244
|
+
if (!focusedRef.current || inputCapturedRef.current) return;
|
|
245
|
+
if (event.defaultPrevented || event.propagationStopped) return;
|
|
246
|
+
if (!scrollPaneByKey(paneIdRef.current, event)) return;
|
|
247
|
+
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
event.stopPropagation();
|
|
250
|
+
nativeRenderer.requestRender();
|
|
251
|
+
}, { phase: "after" });
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { testRender } from "../renderers/opentui/test-utils";
|
|
4
|
+
import { setSharedMarketDataCoordinator } from "../market-data/coordinator";
|
|
5
|
+
import { setSharedRegistryForTests, type PluginRegistry } from "../plugins/registry";
|
|
6
|
+
import { createDefaultConfig } from "../types/config";
|
|
7
|
+
import { AppContext, createInitialState } from "./app-context";
|
|
8
|
+
import { useInlineTickers } from "./use-inline-tickers";
|
|
9
|
+
|
|
10
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
11
|
+
|
|
12
|
+
function InlineTickerHarness() {
|
|
13
|
+
const { catalog } = useInlineTickers(["$LGD1L"]);
|
|
14
|
+
return <text>{catalog.LGD1L?.status ?? "none"}</text>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
testSetup?.renderer.destroy();
|
|
19
|
+
testSetup = undefined;
|
|
20
|
+
setSharedMarketDataCoordinator(null);
|
|
21
|
+
setSharedRegistryForTests(undefined);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("useInlineTickers", () => {
|
|
25
|
+
test("does not leak rejected quote lookups for existing inline tickers", async () => {
|
|
26
|
+
const config = createDefaultConfig("/tmp/gloomberb-inline-tickers-test");
|
|
27
|
+
const state = createInitialState(config);
|
|
28
|
+
state.tickers.set("LGD1L", {
|
|
29
|
+
metadata: {
|
|
30
|
+
ticker: "LGD1L",
|
|
31
|
+
exchange: "NASDAQ",
|
|
32
|
+
currency: "USD",
|
|
33
|
+
name: "Unsupported quote",
|
|
34
|
+
portfolios: [],
|
|
35
|
+
watchlists: [],
|
|
36
|
+
positions: [],
|
|
37
|
+
broker_contracts: [],
|
|
38
|
+
custom: {},
|
|
39
|
+
tags: [],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
const actions: unknown[] = [];
|
|
43
|
+
setSharedRegistryForTests({
|
|
44
|
+
marketData: {
|
|
45
|
+
getQuote: async () => {
|
|
46
|
+
throw new Error("No quote provider available for LGD1L");
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
pinTicker: () => {},
|
|
50
|
+
} as unknown as PluginRegistry);
|
|
51
|
+
|
|
52
|
+
await act(async () => {
|
|
53
|
+
testSetup = await testRender(
|
|
54
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
55
|
+
<InlineTickerHarness />
|
|
56
|
+
</AppContext>,
|
|
57
|
+
{ width: 20, height: 1 },
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await act(async () => {
|
|
62
|
+
await testSetup!.renderOnce();
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
await Promise.resolve();
|
|
65
|
+
});
|
|
66
|
+
await act(async () => {
|
|
67
|
+
await testSetup!.renderOnce();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(testSetup.captureCharFrame()).toContain("missing");
|
|
71
|
+
expect(actions).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -18,6 +18,7 @@ export interface InlineTickerCatalogEntry {
|
|
|
18
18
|
const resolutionInFlight = new Map<string, Promise<void>>();
|
|
19
19
|
const quoteInFlight = new Map<string, Promise<void>>();
|
|
20
20
|
const missingSymbols = new Set<string>();
|
|
21
|
+
const quoteMissingSymbols = new Set<string>();
|
|
21
22
|
const initialQuoteRequested = new Set<string>();
|
|
22
23
|
|
|
23
24
|
function normalizeSymbols(texts: readonly string[]): string[] {
|
|
@@ -54,6 +55,7 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
54
55
|
|
|
55
56
|
if (currentTicker && currentQuote) {
|
|
56
57
|
missingSymbols.delete(symbol);
|
|
58
|
+
quoteMissingSymbols.delete(symbol);
|
|
57
59
|
initialQuoteRequested.delete(symbol);
|
|
58
60
|
continue;
|
|
59
61
|
}
|
|
@@ -123,8 +125,12 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
123
125
|
}
|
|
124
126
|
: undefined,
|
|
125
127
|
);
|
|
128
|
+
quoteMissingSymbols.delete(symbol);
|
|
126
129
|
current.dispatch({ type: "MERGE_QUOTE", symbol, quote });
|
|
127
130
|
})()
|
|
131
|
+
.catch(() => {
|
|
132
|
+
quoteMissingSymbols.add(symbol);
|
|
133
|
+
})
|
|
128
134
|
.finally(() => {
|
|
129
135
|
quoteInFlight.delete(symbol);
|
|
130
136
|
});
|
|
@@ -167,7 +173,7 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
167
173
|
const ticker = tickers.get(symbol) ?? null;
|
|
168
174
|
const quote = financials.get(symbol)?.quote ?? null;
|
|
169
175
|
const status: InlineTickerStatus = ticker
|
|
170
|
-
? (quote ? "ready" : "loading")
|
|
176
|
+
? (quote ? "ready" : quoteMissingSymbols.has(symbol) ? "missing" : "loading")
|
|
171
177
|
: (missingSymbols.has(symbol) ? "missing" : "loading");
|
|
172
178
|
catalog[symbol] = { status, ticker, quote };
|
|
173
179
|
}
|
|
@@ -23,8 +23,9 @@ export function createTestPluginRuntime(
|
|
|
23
23
|
switchTab() {},
|
|
24
24
|
switchPanel() {},
|
|
25
25
|
openCommandBar() {},
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
showPane() {},
|
|
27
|
+
hidePane() {},
|
|
28
|
+
openPaneSettings() {},
|
|
28
29
|
openPluginCommandWorkflow() {},
|
|
29
30
|
notify() {},
|
|
30
31
|
subscribeResumeState: () => () => {},
|
package/src/theme/colors.ts
CHANGED
|
@@ -74,7 +74,7 @@ export function commandBarOverlayBg(): string {
|
|
|
74
74
|
export function commandBarSelectedBg(): string {
|
|
75
75
|
const base = commandBarBg();
|
|
76
76
|
const accent = higherContrast(colors.selectedText, colors.textBright, colors.selected);
|
|
77
|
-
return blendForContrast(colors.selected, base, accent, 1.
|
|
77
|
+
return blendForContrast(colors.selected, base, accent, 1.45);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function commandBarHoverBg(): string {
|
|
@@ -89,7 +89,12 @@ export function commandBarText(): string {
|
|
|
89
89
|
|
|
90
90
|
export function commandBarHeadingText(): string {
|
|
91
91
|
const base = commandBarBg();
|
|
92
|
-
|
|
92
|
+
const fallback = higherContrast(
|
|
93
|
+
higherContrast(colors.textDim, colors.text, base),
|
|
94
|
+
higherContrast(colors.textBright, colors.selectedText, base),
|
|
95
|
+
base,
|
|
96
|
+
);
|
|
97
|
+
return blendForContrast(colors.textMuted, base, fallback, 3.6);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
export function commandBarSubtleText(): string {
|
|
@@ -101,7 +106,11 @@ export function commandBarSubtleText(): string {
|
|
|
101
106
|
export function commandBarSelectedText(): string {
|
|
102
107
|
const base = commandBarSelectedBg();
|
|
103
108
|
const preferred = higherContrast(colors.selectedText, colors.text, base);
|
|
104
|
-
const fallback = higherContrast(
|
|
109
|
+
const fallback = higherContrast(
|
|
110
|
+
higherContrast(colors.textBright, colors.text, base),
|
|
111
|
+
higherContrast("#ffffff", "#000000", base),
|
|
112
|
+
base,
|
|
113
|
+
);
|
|
105
114
|
return blendForContrast(preferred, base, fallback, 4.5);
|
|
106
115
|
}
|
|
107
116
|
|
package/src/theme/themes.test.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { afterEach, describe, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
applyTheme,
|
|
4
|
+
commandBarBg,
|
|
5
|
+
commandBarHeadingText,
|
|
6
|
+
commandBarPanelBg,
|
|
7
|
+
commandBarSelectedBg,
|
|
8
|
+
commandBarSelectedText,
|
|
9
|
+
commandBarSubtleText,
|
|
10
|
+
commandBarText,
|
|
11
|
+
floatingPaneTitleBg,
|
|
12
|
+
paneTitleBg,
|
|
13
|
+
paneTitleText,
|
|
14
|
+
} from "./colors";
|
|
3
15
|
import { contrastRatio } from "./color-utils";
|
|
4
16
|
import { DEFAULT_THEME, themes } from "./themes";
|
|
5
17
|
|
|
@@ -44,4 +56,14 @@ describe("theme contrast", () => {
|
|
|
44
56
|
assertMinContrast(id, "floatingPaneTitleText/unfocused", paneTitleText(false, true), floatingPaneTitleBg(false), CHROME_TEXT_MIN);
|
|
45
57
|
}
|
|
46
58
|
});
|
|
59
|
+
|
|
60
|
+
test("keeps command bar text readable", () => {
|
|
61
|
+
for (const id of Object.keys(themes)) {
|
|
62
|
+
applyTheme(id);
|
|
63
|
+
assertMinContrast(id, "commandBarText/bg", commandBarText(), commandBarBg(), BODY_TEXT_MIN);
|
|
64
|
+
assertMinContrast(id, "commandBarHeadingText/bg", commandBarHeadingText(), commandBarBg(), SUBTLE_TEXT_MIN);
|
|
65
|
+
assertMinContrast(id, "commandBarSubtleText/panel", commandBarSubtleText(), commandBarPanelBg(), SUBTLE_TEXT_MIN);
|
|
66
|
+
assertMinContrast(id, "commandBarSelectedText/selected", commandBarSelectedText(), commandBarSelectedBg(), BODY_TEXT_MIN);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
47
69
|
});
|
package/src/theme/themes.ts
CHANGED
|
@@ -305,6 +305,121 @@ const rawThemes: Record<string, Theme> = {
|
|
|
305
305
|
commandBorder: "#cccccc",
|
|
306
306
|
},
|
|
307
307
|
|
|
308
|
+
paper: {
|
|
309
|
+
name: "Paper",
|
|
310
|
+
description: "Warm light terminal palette",
|
|
311
|
+
bg: "#f7f3e8",
|
|
312
|
+
panel: "#fffaf0",
|
|
313
|
+
border: "#d8cdb7",
|
|
314
|
+
borderFocused: "#a05a2c",
|
|
315
|
+
text: "#2f2a23",
|
|
316
|
+
textDim: "#5f5548",
|
|
317
|
+
textBright: "#17130f",
|
|
318
|
+
textMuted: "#776b5d",
|
|
319
|
+
positive: "#2f7d4a",
|
|
320
|
+
negative: "#b33630",
|
|
321
|
+
neutral: "#6e6256",
|
|
322
|
+
warning: "#946200",
|
|
323
|
+
header: "#eadcc4",
|
|
324
|
+
headerText: "#7a3f1f",
|
|
325
|
+
selected: "#ead1ab",
|
|
326
|
+
selectedText: "#211810",
|
|
327
|
+
commandBg: "#fff7e8",
|
|
328
|
+
commandBorder: "#a05a2c",
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
"github-light": {
|
|
332
|
+
name: "GitHub Light",
|
|
333
|
+
description: "Clean light code palette",
|
|
334
|
+
bg: "#ffffff",
|
|
335
|
+
panel: "#f6f8fa",
|
|
336
|
+
border: "#d0d7de",
|
|
337
|
+
borderFocused: "#0969da",
|
|
338
|
+
text: "#24292f",
|
|
339
|
+
textDim: "#57606a",
|
|
340
|
+
textBright: "#0b1220",
|
|
341
|
+
textMuted: "#6e7781",
|
|
342
|
+
positive: "#1a7f37",
|
|
343
|
+
negative: "#cf222e",
|
|
344
|
+
neutral: "#57606a",
|
|
345
|
+
warning: "#9a6700",
|
|
346
|
+
header: "#eaeef2",
|
|
347
|
+
headerText: "#0969da",
|
|
348
|
+
selected: "#ddf4ff",
|
|
349
|
+
selectedText: "#0b1220",
|
|
350
|
+
commandBg: "#f6f8fa",
|
|
351
|
+
commandBorder: "#0969da",
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
"solarized-light": {
|
|
355
|
+
name: "Solarized Light",
|
|
356
|
+
description: "Ethan Schoonover's light palette",
|
|
357
|
+
bg: "#fdf6e3",
|
|
358
|
+
panel: "#eee8d5",
|
|
359
|
+
border: "#93a1a1",
|
|
360
|
+
borderFocused: "#b58900",
|
|
361
|
+
text: "#586e75",
|
|
362
|
+
textDim: "#657b83",
|
|
363
|
+
textBright: "#073642",
|
|
364
|
+
textMuted: "#7c8a8a",
|
|
365
|
+
positive: "#859900",
|
|
366
|
+
negative: "#dc322f",
|
|
367
|
+
neutral: "#657b83",
|
|
368
|
+
warning: "#b58900",
|
|
369
|
+
header: "#e6dfc9",
|
|
370
|
+
headerText: "#268bd2",
|
|
371
|
+
selected: "#dce6d6",
|
|
372
|
+
selectedText: "#073642",
|
|
373
|
+
commandBg: "#eee8d5",
|
|
374
|
+
commandBorder: "#b58900",
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
"gruvbox-light": {
|
|
378
|
+
name: "Gruvbox Light",
|
|
379
|
+
description: "Retro earthy light palette",
|
|
380
|
+
bg: "#fbf1c7",
|
|
381
|
+
panel: "#f2e5bc",
|
|
382
|
+
border: "#d5c4a1",
|
|
383
|
+
borderFocused: "#af3a03",
|
|
384
|
+
text: "#3c3836",
|
|
385
|
+
textDim: "#665c54",
|
|
386
|
+
textBright: "#282828",
|
|
387
|
+
textMuted: "#7c6f64",
|
|
388
|
+
positive: "#79740e",
|
|
389
|
+
negative: "#9d0006",
|
|
390
|
+
neutral: "#665c54",
|
|
391
|
+
warning: "#b57614",
|
|
392
|
+
header: "#ebdbb2",
|
|
393
|
+
headerText: "#af3a03",
|
|
394
|
+
selected: "#d5c4a1",
|
|
395
|
+
selectedText: "#282828",
|
|
396
|
+
commandBg: "#f2e5bc",
|
|
397
|
+
commandBorder: "#af3a03",
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
"nord-light": {
|
|
401
|
+
name: "Nord Light",
|
|
402
|
+
description: "Soft arctic light palette",
|
|
403
|
+
bg: "#eceff4",
|
|
404
|
+
panel: "#e5e9f0",
|
|
405
|
+
border: "#d8dee9",
|
|
406
|
+
borderFocused: "#5e81ac",
|
|
407
|
+
text: "#2e3440",
|
|
408
|
+
textDim: "#4c566a",
|
|
409
|
+
textBright: "#1f2530",
|
|
410
|
+
textMuted: "#687286",
|
|
411
|
+
positive: "#4b7f36",
|
|
412
|
+
negative: "#bf616a",
|
|
413
|
+
neutral: "#4c566a",
|
|
414
|
+
warning: "#9a7500",
|
|
415
|
+
header: "#d8dee9",
|
|
416
|
+
headerText: "#5e81ac",
|
|
417
|
+
selected: "#c8d5e8",
|
|
418
|
+
selectedText: "#1f2530",
|
|
419
|
+
commandBg: "#e5e9f0",
|
|
420
|
+
commandBorder: "#5e81ac",
|
|
421
|
+
},
|
|
422
|
+
|
|
308
423
|
tokyo: {
|
|
309
424
|
name: "Tokyo Night",
|
|
310
425
|
description: "Cool blue-purple palette",
|
package/src/types/broker.ts
CHANGED
package/src/types/plugin.ts
CHANGED
|
@@ -455,8 +455,6 @@ export interface GloomPluginContext {
|
|
|
455
455
|
on<K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void): () => void;
|
|
456
456
|
emit<K extends keyof PluginEvents>(event: K, payload: PluginEvents[K]): void;
|
|
457
457
|
|
|
458
|
-
showWidget(widgetId: string): void;
|
|
459
|
-
hideWidget(widgetId: string): void;
|
|
460
458
|
notify(notification: AppNotificationRequest): void;
|
|
461
459
|
}
|
|
462
460
|
|
package/src/ui/host.tsx
CHANGED
|
@@ -99,6 +99,9 @@ export interface ScrollBoxRenderable {
|
|
|
99
99
|
scrollLeft?: number;
|
|
100
100
|
scrollHeight: number;
|
|
101
101
|
viewport?: { width: number; height: number };
|
|
102
|
+
visible?: boolean;
|
|
103
|
+
parent?: unknown;
|
|
104
|
+
getBoundingClientRect?: () => { x: number; y: number; width: number; height: number };
|
|
102
105
|
horizontalScrollBar?: { visible: boolean };
|
|
103
106
|
verticalScrollBar?: { visible: boolean };
|
|
104
107
|
scrollTo(target: number | { x?: number; y?: number }, y?: number): void;
|
|
@@ -242,6 +245,7 @@ export interface UiHost {
|
|
|
242
245
|
cellHeightPx?: number;
|
|
243
246
|
pixelRatio?: number;
|
|
244
247
|
canvasCharts?: boolean;
|
|
248
|
+
nativeCharts?: boolean;
|
|
245
249
|
nativeContextMenu?: boolean;
|
|
246
250
|
};
|
|
247
251
|
Box: ComponentType<BoxProps>;
|
package/src/ui/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createElement, forwardRef } from "react";
|
|
2
|
+
import { useForwardedScrollBoxRef, useRegisterPaneScrollBox } from "../state/pane-scroll-registry";
|
|
2
3
|
import { useUiHost } from "./host";
|
|
3
4
|
export {
|
|
4
5
|
RGBA,
|
|
@@ -85,7 +86,14 @@ Underline.displayName = "Underline";
|
|
|
85
86
|
|
|
86
87
|
export const ScrollBox = forwardRef<any, import("./host").ScrollBoxProps>((props, ref) => {
|
|
87
88
|
const { ScrollBox: HostScrollBox } = useUiHost();
|
|
88
|
-
|
|
89
|
+
const localRef = useForwardedScrollBoxRef<any>(ref);
|
|
90
|
+
useRegisterPaneScrollBox(localRef, {
|
|
91
|
+
enabled: props.scrollY === true,
|
|
92
|
+
onScrollActivity: typeof props.onMouseScroll === "function"
|
|
93
|
+
? props.onMouseScroll as (event: { scroll: { direction: "up" | "down"; delta: number } }) => void
|
|
94
|
+
: undefined,
|
|
95
|
+
});
|
|
96
|
+
return createElement(HostScrollBox as any, { ...props, ref: localRef });
|
|
89
97
|
});
|
|
90
98
|
ScrollBox.displayName = "ScrollBox";
|
|
91
99
|
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
export interface BackNavigationEventLike {
|
|
2
2
|
name?: string;
|
|
3
|
+
key?: string;
|
|
3
4
|
ctrl?: boolean;
|
|
4
5
|
meta?: boolean;
|
|
5
6
|
option?: boolean;
|
|
6
7
|
shift?: boolean;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
export interface BackNavigationMouseEventLike {
|
|
11
|
+
button?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MOUSE_BACK_NAVIGATION_EVENT_NAME = "mouse-back";
|
|
15
|
+
export const WEB_MOUSE_BACK_NAVIGATION_BUTTON = 3;
|
|
16
|
+
export const TERMINAL_MOUSE_BACK_NAVIGATION_BUTTON = 8;
|
|
17
|
+
|
|
9
18
|
export function isPlainBackspace(event: BackNavigationEventLike): boolean {
|
|
10
19
|
return event.name === "backspace"
|
|
11
20
|
&& !event.ctrl
|
|
@@ -29,5 +38,19 @@ export function isBackNavigationKey(event: BackNavigationEventLike): boolean {
|
|
|
29
38
|
export function isDetailBackNavigationKey(
|
|
30
39
|
event: BackNavigationEventLike,
|
|
31
40
|
): boolean {
|
|
32
|
-
return isPlainBackspace(event)
|
|
41
|
+
return isPlainBackspace(event)
|
|
42
|
+
|| isPlainEscape(event)
|
|
43
|
+
|| event.name === MOUSE_BACK_NAVIGATION_EVENT_NAME
|
|
44
|
+
|| event.key === MOUSE_BACK_NAVIGATION_EVENT_NAME;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isMouseBackNavigationButton(button: unknown): boolean {
|
|
48
|
+
return button === WEB_MOUSE_BACK_NAVIGATION_BUTTON
|
|
49
|
+
|| button === TERMINAL_MOUSE_BACK_NAVIGATION_BUTTON;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isMouseBackNavigationEvent(
|
|
53
|
+
event: BackNavigationMouseEventLike | null | undefined,
|
|
54
|
+
): boolean {
|
|
55
|
+
return isMouseBackNavigationButton(event?.button);
|
|
33
56
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.7.0";
|