gloomberb 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/app.tsx +149 -51
- package/src/capabilities/factories.ts +3 -0
- package/src/components/chart/chart-renderer.ts +24 -12
- package/src/components/chart/chart.test.ts +43 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +15 -12
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +2 -0
- package/src/components/chart/stock-chart.tsx +31 -20
- package/src/components/chart/time-axis-label.tsx +78 -1
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +114 -31
- package/src/components/command-bar/command-bar.tsx +587 -335
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +1 -2
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -1
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +5 -2
- package/src/components/layout/header.tsx +2 -0
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +198 -0
- package/src/components/layout/shell.tsx +33 -2
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/speedometer-gauge.tsx +29 -25
- package/src/components/table-view-shared.tsx +63 -3
- package/src/components/ticker-list-table-view.tsx +16 -2
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/data-table.tsx +12 -9
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/table-layout.test.ts +27 -1
- package/src/components/ui/table-layout.ts +39 -1
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +68 -1
- package/src/core/state/app-state.ts +4 -14
- package/src/market-data/coordinator.test.ts +75 -0
- package/src/market-data/coordinator.ts +218 -48
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -0
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -2
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/chat-controller.test.ts +83 -1
- package/src/plugins/builtin/chat-controller.ts +491 -243
- package/src/plugins/builtin/chat.test.tsx +425 -7
- package/src/plugins/builtin/chat.tsx +1109 -211
- package/src/plugins/builtin/cloud-tweets.tsx +1 -0
- package/src/plugins/builtin/fear-greed/index.tsx +148 -39
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +54 -14
- package/src/plugins/builtin/market-movers/index.tsx +12 -18
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/notes.tsx +1 -0
- package/src/plugins/builtin/options.tsx +2 -16
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +159 -12
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/plugin-runtime.test.tsx +34 -0
- package/src/plugins/plugin-runtime.tsx +7 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/registry.ts +3 -3
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +1 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +111 -96
- package/src/renderers/electrobun/view/desktop-controls.tsx +8 -0
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +2 -0
- package/src/renderers/electrobun/view/input-host.tsx +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +6 -8
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -11
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +20 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.ts +13 -10
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +64 -3
- package/src/theme/theme-context.tsx +26 -0
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -1
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -1
- package/src/ui/host.tsx +6 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
|
@@ -487,6 +487,26 @@ describe("quote merging", () => {
|
|
|
487
487
|
});
|
|
488
488
|
|
|
489
489
|
describe("layout focus fallback", () => {
|
|
490
|
+
test("starts with the top floating pane focused when no session pane is active", () => {
|
|
491
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
492
|
+
const layout = {
|
|
493
|
+
...cloneLayout(config.layout),
|
|
494
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
495
|
+
floating: [
|
|
496
|
+
{ instanceId: "chat:main", x: 4, y: 2, width: 36, height: 10, zIndex: 70 },
|
|
497
|
+
{ instanceId: "ticker-detail:main", x: 12, y: 4, width: 36, height: 10, zIndex: 95 },
|
|
498
|
+
],
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const state = createInitialState({
|
|
502
|
+
...config,
|
|
503
|
+
layout,
|
|
504
|
+
layouts: [{ name: "Default", layout: cloneLayout(layout) }],
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
expect(state.focusedPaneId).toBe("ticker-detail:main");
|
|
508
|
+
});
|
|
509
|
+
|
|
490
510
|
test("cycles backward from a stale focused pane to the last visible pane", () => {
|
|
491
511
|
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
492
512
|
const state = createInitialState(config);
|
|
@@ -13,14 +13,14 @@ import {
|
|
|
13
13
|
type ReactNode,
|
|
14
14
|
} from "react";
|
|
15
15
|
import type { SessionStore } from "../data/session-store";
|
|
16
|
-
import {
|
|
16
|
+
import { ThemeProvider } from "../theme/theme-context";
|
|
17
17
|
import {
|
|
18
18
|
findPaneInstance,
|
|
19
19
|
materializeDetachedPanesAsFloating,
|
|
20
20
|
type AppConfig,
|
|
21
21
|
type PaneInstanceConfig,
|
|
22
22
|
} from "../types/config";
|
|
23
|
-
import type { DesktopSharedStateSnapshot, DesktopWindowBridge } from "../types/desktop-window";
|
|
23
|
+
import type { DesktopSharedStateSnapshot, DesktopThemePreviewState, DesktopWindowBridge } from "../types/desktop-window";
|
|
24
24
|
import { setPaneSetting } from "../pane-settings";
|
|
25
25
|
import { useTickerFinancials } from "../market-data/hooks";
|
|
26
26
|
import {
|
|
@@ -142,7 +142,7 @@ export function useAppSelector<T>(selector: (state: AppState) => T): T {
|
|
|
142
142
|
const context = useRequiredAppContext();
|
|
143
143
|
const selectorRef = useRef(selector);
|
|
144
144
|
selectorRef.current = selector;
|
|
145
|
-
const lastSnapshotRef = useRef<{ selection: T
|
|
145
|
+
const lastSnapshotRef = useRef<{ selection: T } | undefined>(undefined);
|
|
146
146
|
|
|
147
147
|
if (!isAppStoreContextValue(context)) {
|
|
148
148
|
return selector(context.state);
|
|
@@ -153,12 +153,11 @@ export function useAppSelector<T>(selector: (state: AppState) => T): T {
|
|
|
153
153
|
() => {
|
|
154
154
|
const state = context.getState();
|
|
155
155
|
const selection = selectorRef.current(state);
|
|
156
|
-
const themeId = getEffectiveThemeId(state);
|
|
157
156
|
const previous = lastSnapshotRef.current;
|
|
158
|
-
if (previous && Object.is(previous.selection, selection)
|
|
157
|
+
if (previous && Object.is(previous.selection, selection)) {
|
|
159
158
|
return previous;
|
|
160
159
|
}
|
|
161
|
-
const nextSnapshot = { selection
|
|
160
|
+
const nextSnapshot = { selection };
|
|
162
161
|
lastSnapshotRef.current = nextSnapshot;
|
|
163
162
|
return nextSnapshot;
|
|
164
163
|
},
|
|
@@ -166,7 +165,6 @@ export function useAppSelector<T>(selector: (state: AppState) => T): T {
|
|
|
166
165
|
const state = context.getState();
|
|
167
166
|
return {
|
|
168
167
|
selection: selectorRef.current(state),
|
|
169
|
-
themeId: getEffectiveThemeId(state),
|
|
170
168
|
};
|
|
171
169
|
},
|
|
172
170
|
);
|
|
@@ -303,6 +301,7 @@ export function AppProvider({
|
|
|
303
301
|
sessionSnapshot = null,
|
|
304
302
|
desktopBridge,
|
|
305
303
|
desktopSnapshot = null,
|
|
304
|
+
initialThemePreview = null,
|
|
306
305
|
}: {
|
|
307
306
|
config: AppConfig;
|
|
308
307
|
children: ReactNode;
|
|
@@ -310,34 +309,40 @@ export function AppProvider({
|
|
|
310
309
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
311
310
|
desktopBridge?: DesktopWindowBridge;
|
|
312
311
|
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
312
|
+
initialThemePreview?: DesktopThemePreviewState | null;
|
|
313
313
|
}) {
|
|
314
314
|
const [state, rawDispatch] = useReducer(
|
|
315
315
|
appReducer,
|
|
316
|
-
{ config, sessionSnapshot, desktopSnapshot },
|
|
316
|
+
{ config, sessionSnapshot, desktopSnapshot, initialThemePreview },
|
|
317
317
|
(initialState: {
|
|
318
318
|
config: AppConfig;
|
|
319
319
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
320
320
|
desktopSnapshot?: DesktopSharedStateSnapshot | null;
|
|
321
|
+
initialThemePreview?: DesktopThemePreviewState | null;
|
|
321
322
|
}) => {
|
|
322
323
|
const {
|
|
323
324
|
config: initialConfig,
|
|
324
325
|
sessionSnapshot: initialSessionSnapshot,
|
|
325
326
|
desktopSnapshot: initialDesktopSnapshot,
|
|
327
|
+
initialThemePreview: initialDesktopThemePreview,
|
|
326
328
|
} = initialState;
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
? appReducer(
|
|
329
|
+
const baseState = createInitialState(initialConfig, initialSessionSnapshot);
|
|
330
|
+
const nextState = initialDesktopSnapshot
|
|
331
|
+
? appReducer(baseState, { type: "HYDRATE_DESKTOP_SNAPSHOT", snapshot: initialDesktopSnapshot })
|
|
332
|
+
: baseState;
|
|
333
|
+
return initialDesktopThemePreview
|
|
334
|
+
? appReducer(nextState, { type: "PREVIEW_THEME", theme: initialDesktopThemePreview.theme })
|
|
330
335
|
: nextState;
|
|
331
336
|
},
|
|
332
337
|
);
|
|
333
|
-
|
|
334
|
-
syncTheme(getEffectiveThemeId(state));
|
|
338
|
+
const effectiveThemeId = getEffectiveThemeId(state);
|
|
335
339
|
const previousRecentTickers = useRef(state.recentTickers);
|
|
336
340
|
const stateRef = useRef(state);
|
|
337
341
|
const listenersRef = useRef(new Set<() => void>());
|
|
338
342
|
const storeRef = useRef<AppContextStoreValue | null>(null);
|
|
339
343
|
const lastMainSyncRef = useRef<string | null>(null);
|
|
340
344
|
const lastDetachedPaneSyncRef = useRef<string | null>(null);
|
|
345
|
+
const lastThemePreviewSyncRef = useRef<string | null | undefined>(undefined);
|
|
341
346
|
|
|
342
347
|
stateRef.current = state;
|
|
343
348
|
|
|
@@ -421,12 +426,35 @@ export function AppProvider({
|
|
|
421
426
|
|
|
422
427
|
useEffect(() => {
|
|
423
428
|
if (desktopBridge?.kind !== "main" || !desktopBridge.syncMainState) return;
|
|
424
|
-
const snapshot = buildDesktopSnapshot(
|
|
429
|
+
const snapshot = buildDesktopSnapshot(stateRef.current);
|
|
425
430
|
const signature = serializeDesktopSnapshot(snapshot);
|
|
426
431
|
if (signature === lastMainSyncRef.current) return;
|
|
427
432
|
lastMainSyncRef.current = signature;
|
|
428
433
|
void desktopBridge.syncMainState(snapshot);
|
|
429
|
-
}, [
|
|
434
|
+
}, [
|
|
435
|
+
buildDesktopSnapshot,
|
|
436
|
+
desktopBridge,
|
|
437
|
+
serializeDesktopSnapshot,
|
|
438
|
+
state.activePanel,
|
|
439
|
+
state.config,
|
|
440
|
+
state.focusedPaneId,
|
|
441
|
+
state.paneState,
|
|
442
|
+
state.statusBarVisible,
|
|
443
|
+
]);
|
|
444
|
+
|
|
445
|
+
useEffect(() => {
|
|
446
|
+
if (desktopBridge?.kind !== "main" || !desktopBridge.syncThemePreview) return;
|
|
447
|
+
if (lastThemePreviewSyncRef.current === state.themePreview) return;
|
|
448
|
+
lastThemePreviewSyncRef.current = state.themePreview;
|
|
449
|
+
void desktopBridge.syncThemePreview({ theme: state.themePreview });
|
|
450
|
+
}, [desktopBridge, state.themePreview]);
|
|
451
|
+
|
|
452
|
+
useEffect(() => {
|
|
453
|
+
if (desktopBridge?.kind !== "detached" || !desktopBridge.subscribeThemePreview) return;
|
|
454
|
+
return desktopBridge.subscribeThemePreview((preview) => {
|
|
455
|
+
dispatch({ type: "PREVIEW_THEME", theme: preview.theme });
|
|
456
|
+
});
|
|
457
|
+
}, [desktopBridge, dispatch]);
|
|
430
458
|
|
|
431
459
|
useEffect(() => {
|
|
432
460
|
if (desktopBridge?.kind !== "detached" || !desktopBridge.replaceDetachedPaneState || !desktopBridge.paneId) return;
|
|
@@ -445,5 +473,9 @@ export function AppProvider({
|
|
|
445
473
|
|
|
446
474
|
usePersistSessionSnapshot(sessionStore, state, APP_SESSION_ID, APP_SESSION_SCHEMA_VERSION);
|
|
447
475
|
|
|
448
|
-
return
|
|
476
|
+
return (
|
|
477
|
+
<ThemeProvider themeId={effectiveThemeId}>
|
|
478
|
+
<AppContext.Provider value={storeRef.current}>{children}</AppContext.Provider>
|
|
479
|
+
</ThemeProvider>
|
|
480
|
+
);
|
|
449
481
|
}
|
|
@@ -33,7 +33,7 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
33
33
|
const tickers = useAppSelector((state) => state.tickers);
|
|
34
34
|
const financials = useAppSelector((state) => state.financials);
|
|
35
35
|
const registry = getSharedRegistry();
|
|
36
|
-
const [, setRefreshVersion] = useState(0);
|
|
36
|
+
const [refreshVersion, setRefreshVersion] = useState(0);
|
|
37
37
|
const textsKey = texts.join("\u0000");
|
|
38
38
|
const symbols = useMemo(() => normalizeSymbols(texts), [textsKey]);
|
|
39
39
|
const symbolsKey = symbols.join("|");
|
|
@@ -168,15 +168,18 @@ export function useInlineTickers(texts: readonly string[]): {
|
|
|
168
168
|
registry?.pinTicker(symbol, { floating: true, paneType: "ticker-detail" });
|
|
169
169
|
}, [registry]);
|
|
170
170
|
|
|
171
|
-
const catalog
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
const catalog = useMemo(() => {
|
|
172
|
+
const entries: Record<string, InlineTickerCatalogEntry> = {};
|
|
173
|
+
for (const symbol of symbols) {
|
|
174
|
+
const ticker = tickers.get(symbol) ?? null;
|
|
175
|
+
const quote = financials.get(symbol)?.quote ?? null;
|
|
176
|
+
const status: InlineTickerStatus = ticker
|
|
177
|
+
? (quote ? "ready" : quoteMissingSymbols.has(symbol) ? "missing" : "loading")
|
|
178
|
+
: (missingSymbols.has(symbol) ? "missing" : "loading");
|
|
179
|
+
entries[symbol] = { status, ticker, quote };
|
|
180
|
+
}
|
|
181
|
+
return entries;
|
|
182
|
+
}, [financials, refreshVersion, symbols, tickers]);
|
|
180
183
|
|
|
181
184
|
return { catalog, openTicker };
|
|
182
185
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
applyTheme,
|
|
4
|
+
clearTransientThemePreview,
|
|
5
|
+
getCurrentThemeId,
|
|
6
|
+
previewTheme,
|
|
7
|
+
syncTheme,
|
|
8
|
+
} from "./colors";
|
|
9
|
+
import { DEFAULT_THEME, getTheme } from "./themes";
|
|
10
|
+
|
|
11
|
+
const originalDocument = (globalThis as Record<string, unknown>).document;
|
|
12
|
+
|
|
13
|
+
function setDocument(value: unknown): void {
|
|
14
|
+
Object.defineProperty(globalThis, "document", {
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installDocumentStyleMock() {
|
|
22
|
+
const values = new Map<string, string>();
|
|
23
|
+
setDocument({
|
|
24
|
+
documentElement: {
|
|
25
|
+
style: {
|
|
26
|
+
setProperty(name: string, value: string) {
|
|
27
|
+
values.set(name, value);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
return values;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
if (originalDocument === undefined) {
|
|
37
|
+
Reflect.deleteProperty(globalThis, "document");
|
|
38
|
+
} else {
|
|
39
|
+
setDocument(originalDocument);
|
|
40
|
+
}
|
|
41
|
+
clearTransientThemePreview();
|
|
42
|
+
syncTheme(DEFAULT_THEME);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("theme colors", () => {
|
|
46
|
+
test("syncs CSS variables when a theme is applied", () => {
|
|
47
|
+
const values = installDocumentStyleMock();
|
|
48
|
+
const theme = getTheme("midnight");
|
|
49
|
+
|
|
50
|
+
applyTheme("midnight");
|
|
51
|
+
|
|
52
|
+
expect(values.get("--gloom-bg")).toBe(theme.bg);
|
|
53
|
+
expect(values.get("--gloom-panel")).toBe(theme.panel);
|
|
54
|
+
expect(values.get("--gloom-text-dim")).toBe(theme.textDim);
|
|
55
|
+
expect(values.get("--gloom-selected")).toBe(theme.selected);
|
|
56
|
+
expect(values.get("--gloom-hover-bg")).toBeString();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("does not let provider sync clobber a pending preview", () => {
|
|
60
|
+
const values = installDocumentStyleMock();
|
|
61
|
+
const preview = getTheme("midnight");
|
|
62
|
+
|
|
63
|
+
previewTheme("midnight");
|
|
64
|
+
syncTheme(DEFAULT_THEME);
|
|
65
|
+
|
|
66
|
+
expect(getCurrentThemeId()).toBe("midnight");
|
|
67
|
+
expect(values.get("--gloom-bg")).toBe(preview.bg);
|
|
68
|
+
|
|
69
|
+
clearTransientThemePreview();
|
|
70
|
+
syncTheme(DEFAULT_THEME);
|
|
71
|
+
|
|
72
|
+
expect(getCurrentThemeId()).toBe(DEFAULT_THEME);
|
|
73
|
+
expect(values.get("--gloom-bg")).toBe(getTheme(DEFAULT_THEME).bg);
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/theme/colors.ts
CHANGED
|
@@ -5,7 +5,33 @@ import { getTheme, DEFAULT_THEME, type Theme } from "./themes";
|
|
|
5
5
|
// React components re-read these on each render triggered by the SET_THEME action.
|
|
6
6
|
export const colors: Omit<Theme, "name" | "description"> = { ...getTheme(DEFAULT_THEME) };
|
|
7
7
|
|
|
8
|
+
export type ColorKey = keyof typeof colors;
|
|
9
|
+
|
|
8
10
|
let currentThemeId = DEFAULT_THEME;
|
|
11
|
+
let transientPreviewThemeId: string | null = null;
|
|
12
|
+
let cssThemeId: string | null = null;
|
|
13
|
+
let cssThemeDocument: unknown = null;
|
|
14
|
+
|
|
15
|
+
const THEME_CSS_VARIABLES: Array<[ColorKey, string]> = [
|
|
16
|
+
["bg", "--gloom-bg"],
|
|
17
|
+
["panel", "--gloom-panel"],
|
|
18
|
+
["border", "--gloom-border"],
|
|
19
|
+
["borderFocused", "--gloom-border-focused"],
|
|
20
|
+
["text", "--gloom-text"],
|
|
21
|
+
["textDim", "--gloom-text-dim"],
|
|
22
|
+
["textBright", "--gloom-text-bright"],
|
|
23
|
+
["textMuted", "--gloom-text-muted"],
|
|
24
|
+
["positive", "--gloom-positive"],
|
|
25
|
+
["negative", "--gloom-negative"],
|
|
26
|
+
["neutral", "--gloom-neutral"],
|
|
27
|
+
["warning", "--gloom-warning"],
|
|
28
|
+
["header", "--gloom-header"],
|
|
29
|
+
["headerText", "--gloom-header-text"],
|
|
30
|
+
["selected", "--gloom-selected"],
|
|
31
|
+
["selectedText", "--gloom-selected-text"],
|
|
32
|
+
["commandBg", "--gloom-command-bg"],
|
|
33
|
+
["commandBorder", "--gloom-command-border"],
|
|
34
|
+
];
|
|
9
35
|
|
|
10
36
|
export function getCurrentThemeId(): string {
|
|
11
37
|
return currentThemeId;
|
|
@@ -16,14 +42,34 @@ export function applyTheme(id: string): void {
|
|
|
16
42
|
currentThemeId = id;
|
|
17
43
|
// Mutate in-place so every existing import sees the new values
|
|
18
44
|
Object.assign(colors, theme);
|
|
45
|
+
syncThemeCssVariables();
|
|
19
46
|
}
|
|
20
47
|
|
|
21
|
-
export function
|
|
22
|
-
|
|
48
|
+
export function previewTheme(id: string): void {
|
|
49
|
+
transientPreviewThemeId = id;
|
|
23
50
|
applyTheme(id);
|
|
24
51
|
}
|
|
25
52
|
|
|
26
|
-
export
|
|
53
|
+
export function clearTransientThemePreview(): void {
|
|
54
|
+
transientPreviewThemeId = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function syncTheme(id: string): void {
|
|
58
|
+
if (
|
|
59
|
+
transientPreviewThemeId
|
|
60
|
+
&& currentThemeId === transientPreviewThemeId
|
|
61
|
+
&& id !== transientPreviewThemeId
|
|
62
|
+
) {
|
|
63
|
+
syncThemeCssVariables();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
transientPreviewThemeId = null;
|
|
67
|
+
if (currentThemeId === id) {
|
|
68
|
+
syncThemeCssVariables();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
applyTheme(id);
|
|
72
|
+
}
|
|
27
73
|
|
|
28
74
|
export { blendHex } from "./color-utils";
|
|
29
75
|
|
|
@@ -49,6 +95,21 @@ export function hoverBg(): string {
|
|
|
49
95
|
return blendHex(colors.bg, colors.selected, 0.5);
|
|
50
96
|
}
|
|
51
97
|
|
|
98
|
+
function syncThemeCssVariables(): void {
|
|
99
|
+
const documentLike = (globalThis as {
|
|
100
|
+
document?: { documentElement?: { style?: { setProperty: (name: string, value: string) => void } } };
|
|
101
|
+
}).document;
|
|
102
|
+
const style = documentLike?.documentElement?.style;
|
|
103
|
+
if (!style) return;
|
|
104
|
+
if (cssThemeId === currentThemeId && cssThemeDocument === documentLike) return;
|
|
105
|
+
cssThemeId = currentThemeId;
|
|
106
|
+
cssThemeDocument = documentLike;
|
|
107
|
+
for (const [key, name] of THEME_CSS_VARIABLES) {
|
|
108
|
+
style.setProperty(name, colors[key]);
|
|
109
|
+
}
|
|
110
|
+
style.setProperty("--gloom-hover-bg", hoverBg());
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
export function commandBarBg(): string {
|
|
53
114
|
const base = higherContrast(colors.commandBg, colors.panel, colors.bg);
|
|
54
115
|
const accent = higherContrast(colors.textBright, colors.borderFocused, base);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
|
2
|
+
import { colors, getCurrentThemeId, syncTheme } from "./colors";
|
|
3
|
+
import type { Theme } from "./themes";
|
|
4
|
+
|
|
5
|
+
type ThemeColors = Omit<Theme, "name" | "description">;
|
|
6
|
+
|
|
7
|
+
interface ThemeContextValue {
|
|
8
|
+
themeId: string;
|
|
9
|
+
colors: ThemeColors;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
13
|
+
|
|
14
|
+
export function ThemeProvider({ themeId, children }: { themeId: string; children: ReactNode }) {
|
|
15
|
+
syncTheme(themeId);
|
|
16
|
+
const value = useMemo(() => ({ themeId, colors }), [themeId]);
|
|
17
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function useThemeId(): string {
|
|
21
|
+
return useContext(ThemeContext)?.themeId ?? getCurrentThemeId();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function useThemeColors(): ThemeColors {
|
|
25
|
+
return useContext(ThemeContext)?.colors ?? colors;
|
|
26
|
+
}
|
|
@@ -6,6 +6,7 @@ export interface NewsDataProvider {
|
|
|
6
6
|
supports?(query: NewsQuery): boolean;
|
|
7
7
|
getCachedNews?(query: NewsQuery): NewsArticle[];
|
|
8
8
|
fetchNews(query: NewsQuery): Promise<NewsArticle[]>;
|
|
9
|
+
fetchNewsStory?(storyId: string): Promise<NewsArticle | null>;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export interface CapabilityRouteSource {
|
|
@@ -92,6 +92,18 @@ export interface QuoteSubscriptionTarget {
|
|
|
92
92
|
route?: "auto" | "provider" | "broker";
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
export interface QuoteBatchResult {
|
|
96
|
+
target: QuoteSubscriptionTarget;
|
|
97
|
+
quote: Quote | null;
|
|
98
|
+
error?: unknown;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TickerFinancialsBatchResult {
|
|
102
|
+
target: CachedFinancialsTarget;
|
|
103
|
+
financials: TickerFinancials | null;
|
|
104
|
+
error?: unknown;
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
export interface AssetDataProvider {
|
|
96
108
|
readonly id: string;
|
|
97
109
|
readonly name: string;
|
|
@@ -99,7 +111,9 @@ export interface AssetDataProvider {
|
|
|
99
111
|
readonly cachePolicy?: CachePolicyMap;
|
|
100
112
|
|
|
101
113
|
canProvide?(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<boolean> | boolean;
|
|
102
|
-
getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean }): Map<string, TickerFinancials
|
|
114
|
+
getCachedFinancialsForTargets?(targets: CachedFinancialsTarget[], options?: { allowExpired?: boolean }): Map<string, TickerFinancials> | Promise<Map<string, TickerFinancials>>;
|
|
115
|
+
getQuotesBatch?(targets: QuoteSubscriptionTarget[], options?: { forceRefresh?: boolean }): Promise<QuoteBatchResult[]>;
|
|
116
|
+
getTickerFinancialsBatch?(targets: CachedFinancialsTarget[], options?: { forceRefresh?: boolean }): Promise<TickerFinancialsBatchResult[]>;
|
|
103
117
|
getTickerFinancials(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<TickerFinancials>;
|
|
104
118
|
getQuote(ticker: string, exchange?: string, context?: MarketDataRequestContext): Promise<Quote>;
|
|
105
119
|
getExchangeRate(fromCurrency: string): Promise<number>;
|
|
@@ -15,10 +15,15 @@ export interface DesktopDockPreviewState {
|
|
|
15
15
|
edge: "left" | "right" | "top" | "bottom" | null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export interface DesktopThemePreviewState {
|
|
19
|
+
theme: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
18
22
|
export interface DesktopWindowBridge {
|
|
19
23
|
kind: "main" | "detached";
|
|
20
24
|
paneId?: string;
|
|
21
25
|
syncMainState?(snapshot: DesktopSharedStateSnapshot): Promise<void>;
|
|
26
|
+
syncThemePreview?(preview: DesktopThemePreviewState): Promise<void>;
|
|
22
27
|
replaceDetachedPaneState?(paneId: string, paneState: PaneRuntimeState): Promise<void>;
|
|
23
28
|
popOutPane?(paneId: string): Promise<void>;
|
|
24
29
|
dockDetachedPane?(paneId: string): Promise<void>;
|
|
@@ -26,4 +31,5 @@ export interface DesktopWindowBridge {
|
|
|
26
31
|
focusDetachedPane?(paneId: string): Promise<void>;
|
|
27
32
|
subscribeState(listener: (snapshot: DesktopSharedStateSnapshot) => void): () => void;
|
|
28
33
|
subscribeDockPreview?(listener: (preview: DesktopDockPreviewState) => void): () => void;
|
|
34
|
+
subscribeThemePreview?(listener: (preview: DesktopThemePreviewState) => void): () => void;
|
|
29
35
|
}
|
package/src/types/financials.ts
CHANGED
package/src/types/plugin.ts
CHANGED
|
@@ -449,7 +449,7 @@ export interface GloomPluginContext {
|
|
|
449
449
|
hidePane(paneId: string): void;
|
|
450
450
|
focusPane(paneId: string): void;
|
|
451
451
|
pinTicker(symbol: string, options?: { floating?: boolean; paneType?: string }): void;
|
|
452
|
-
navigateTicker(symbol: string): void;
|
|
452
|
+
navigateTicker(symbol: string, options?: { sourcePaneId?: string | null }): void;
|
|
453
453
|
openPaneSettings(paneId?: string): void;
|
|
454
454
|
|
|
455
455
|
on<K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void): () => void;
|
package/src/ui/host.tsx
CHANGED
|
@@ -98,13 +98,19 @@ export interface ScrollBoxRenderable {
|
|
|
98
98
|
scrollTop: number;
|
|
99
99
|
scrollLeft?: number;
|
|
100
100
|
scrollHeight: number;
|
|
101
|
+
scrollTopPx?: number;
|
|
102
|
+
scrollLeftPx?: number;
|
|
103
|
+
scrollHeightPx?: number;
|
|
104
|
+
scrollWidthPx?: number;
|
|
101
105
|
viewport?: { width: number; height: number };
|
|
106
|
+
viewportPx?: { width: number; height: number };
|
|
102
107
|
visible?: boolean;
|
|
103
108
|
parent?: unknown;
|
|
104
109
|
getBoundingClientRect?: () => { x: number; y: number; width: number; height: number };
|
|
105
110
|
horizontalScrollBar?: { visible: boolean };
|
|
106
111
|
verticalScrollBar?: { visible: boolean };
|
|
107
112
|
scrollTo(target: number | { x?: number; y?: number }, y?: number): void;
|
|
113
|
+
scrollToPixels?(target: number | { x?: number; y?: number }, y?: number): void;
|
|
108
114
|
}
|
|
109
115
|
|
|
110
116
|
export interface InputRenderable {
|
|
@@ -320,4 +320,36 @@ describe("apiClient cloud news", () => {
|
|
|
320
320
|
expect(url.searchParams.get("cursor")).toBe("cursor-1");
|
|
321
321
|
expect(result).toEqual({ items: [], nextCursor: null });
|
|
322
322
|
});
|
|
323
|
+
|
|
324
|
+
test("fetches news story details from the story route", async () => {
|
|
325
|
+
let seenUrl = "";
|
|
326
|
+
globalThis.fetch = mockFetch(async (input: Request | string | URL) => {
|
|
327
|
+
seenUrl = String(input);
|
|
328
|
+
return createResponse({
|
|
329
|
+
id: "story-1",
|
|
330
|
+
headline: "Story headline",
|
|
331
|
+
summary: "Story summary",
|
|
332
|
+
category: "general",
|
|
333
|
+
sentiment: "neutral",
|
|
334
|
+
sectors: [],
|
|
335
|
+
firstPublishedAt: "2026-04-01T10:00:00.000Z",
|
|
336
|
+
lastPublishedAt: "2026-04-01T10:05:00.000Z",
|
|
337
|
+
firstSeenAt: "2026-04-01T10:00:10.000Z",
|
|
338
|
+
lastSeenAt: "2026-04-01T10:05:10.000Z",
|
|
339
|
+
primaryUrl: "https://example.com/story",
|
|
340
|
+
primarySource: "example-wire",
|
|
341
|
+
variantCount: 2,
|
|
342
|
+
sourceCount: 2,
|
|
343
|
+
sources: ["example-wire"],
|
|
344
|
+
entities: [],
|
|
345
|
+
tickerLinks: [],
|
|
346
|
+
items: [],
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const story = await apiClient.getCloudNewsStory("story-1");
|
|
351
|
+
|
|
352
|
+
expect(new URL(seenUrl).pathname).toBe("/news/story-1");
|
|
353
|
+
expect(story.id).toBe("story-1");
|
|
354
|
+
});
|
|
323
355
|
});
|