gloomberb 0.4.0 → 0.5.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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.tsx +184 -69
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +296 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/floating-pane.tsx +3 -1
- package/src/components/layout/header.tsx +5 -4
- package/src/components/layout/pane-sizing.ts +6 -0
- package/src/components/layout/pane.tsx +5 -1
- package/src/components/layout/shell.test.tsx +263 -32
- package/src/components/layout/shell.tsx +10 -6
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +62 -2
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +82 -5
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +92 -18
- package/src/plugins/builtin/chat-controller.ts +40 -11
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +420 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +33 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/gateway-service.test.ts +22 -0
- package/src/plugins/ibkr/gateway-service.ts +6 -0
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +8 -2
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +129 -14
- package/src/plugins/prediction-markets/controller.ts +1 -1
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/index.tsx +5 -1
- package/src/plugins/prediction-markets/pane.test.tsx +78 -0
- package/src/plugins/prediction-markets/pane.tsx +7 -3
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +17 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
- package/src/plugins/registry.ts +45 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/updater.test.ts +2 -0
- package/src/utils/api-client.ts +12 -0
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
package/src/plugins/registry.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { BrokerInstanceConfig, LayoutConfig, PaneInstanceConfig } from "../
|
|
|
8
8
|
import type { DataProvider } from "../types/data-provider";
|
|
9
9
|
import type { TickerFinancials } from "../types/financials";
|
|
10
10
|
import type {
|
|
11
|
+
AppNotificationRequest,
|
|
11
12
|
CommandDef,
|
|
12
13
|
CustomColumnDef,
|
|
13
14
|
DetailTabDef,
|
|
@@ -27,7 +28,7 @@ import type {
|
|
|
27
28
|
import type { TickerRecord } from "../types/ticker";
|
|
28
29
|
import { addPaneFloating, removePane } from "./pane-manager";
|
|
29
30
|
import { EventBus, type PluginEvents } from "./event-bus";
|
|
30
|
-
import { createPaneInstance } from "../types/config";
|
|
31
|
+
import { createPaneInstance, resolvePaneInstance } from "../types/config";
|
|
31
32
|
import { createPluginPersistence } from "./plugin-persistence";
|
|
32
33
|
import { debugLog } from "../utils/debug-log";
|
|
33
34
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "./plugin-runtime";
|
|
@@ -104,6 +105,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
104
105
|
switchPanelFn: ((panel: "left" | "right") => void) = () => {};
|
|
105
106
|
switchTabFn: ((tabId: string, paneId?: string) => void) = () => {};
|
|
106
107
|
openCommandBarFn: ((query?: string) => void) = () => {};
|
|
108
|
+
openPluginCommandWorkflowFn: ((commandId: string) => void) = () => {};
|
|
107
109
|
openPaneSettingsFn: ((paneId?: string) => void) = () => {};
|
|
108
110
|
showPaneFn: ((paneId: string) => void) = () => {};
|
|
109
111
|
createPaneFromTemplateFn: ((templateId: string, options?: PaneTemplateCreateOptions) => void) = () => {};
|
|
@@ -111,12 +113,22 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
111
113
|
hidePaneFn: ((paneId: string) => void) = () => {};
|
|
112
114
|
focusPaneFn: ((paneId: string) => void) = () => {};
|
|
113
115
|
pinTickerFn: ((symbol: string, options?: { floating?: boolean; paneType?: string }) => void) = () => {};
|
|
116
|
+
navigateTickerFn: ((symbol: string) => void) = () => {};
|
|
117
|
+
pinTicker = (symbol: string, options?: { floating?: boolean; paneType?: string }) => {
|
|
118
|
+
this.pinTickerFn(symbol, options);
|
|
119
|
+
};
|
|
120
|
+
navigateTicker = (symbol: string) => {
|
|
121
|
+
this.navigateTickerFn(symbol);
|
|
122
|
+
};
|
|
123
|
+
openPluginCommandWorkflow = (commandId: string) => {
|
|
124
|
+
this.openPluginCommandWorkflowFn(commandId);
|
|
125
|
+
};
|
|
114
126
|
|
|
115
127
|
getLayoutFn: (() => LayoutConfig) = () => ({ dockRoot: null, instances: [], floating: [] });
|
|
116
128
|
updateLayoutFn: ((layout: LayoutConfig) => void) = () => {};
|
|
117
129
|
getTermSizeFn: (() => { width: number; height: number }) = () => ({ width: 120, height: 40 });
|
|
118
130
|
|
|
119
|
-
|
|
131
|
+
notifyFn: ((notification: AppNotificationRequest) => void) = () => {};
|
|
120
132
|
getPaneRuntimeStateFn: ((paneId: string) => PaneRuntimeState | null) = () => null;
|
|
121
133
|
updatePaneRuntimeStateFn: ((paneId: string, patch: Partial<PaneRuntimeState>) => void) = () => {};
|
|
122
134
|
applyPaneSettingValueFn: ((paneId: string, field: import("../types/plugin").PaneSettingField, value: unknown) => Promise<void>) = async () => {};
|
|
@@ -177,6 +189,10 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
177
189
|
return this.pluginItems.get(pluginId)?.paneTemplates ?? [];
|
|
178
190
|
}
|
|
179
191
|
|
|
192
|
+
notify(notification: AppNotificationRequest): void {
|
|
193
|
+
this.notifyFn(notification);
|
|
194
|
+
}
|
|
195
|
+
|
|
180
196
|
private getOrCreatePluginItems(pluginId: string): PluginItems {
|
|
181
197
|
const existing = this.pluginItems.get(pluginId);
|
|
182
198
|
if (existing) return existing;
|
|
@@ -272,16 +288,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
272
288
|
return Object.keys(this.getConfigFn().pluginConfig[pluginId] ?? {}).sort();
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
private resolvePrimaryPaneInstanceId(paneId: string): string | undefined {
|
|
276
|
-
const layout = this.getLayoutFn();
|
|
277
|
-
return layout.instances.find((instance) => instance.paneId === paneId)?.instanceId;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
291
|
private resolvePaneTarget(paneId: string): string | undefined {
|
|
281
|
-
|
|
282
|
-
const isInstanceId = layout.instances.some((instance) => instance.instanceId === paneId);
|
|
283
|
-
if (isInstanceId) return paneId;
|
|
284
|
-
return this.resolvePrimaryPaneInstanceId(paneId);
|
|
292
|
+
return resolvePaneInstance(this.getLayoutFn(), paneId)?.instanceId;
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
resolvePaneSettings(paneId: string): {
|
|
@@ -514,7 +522,8 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
514
522
|
createPaneFromTemplate: (templateId, options) => this.createPaneFromTemplateFn(templateId, options),
|
|
515
523
|
hidePane: (paneId) => this.hidePaneFn(paneId),
|
|
516
524
|
focusPane: (paneId) => this.focusPaneFn(paneId),
|
|
517
|
-
pinTicker:
|
|
525
|
+
pinTicker: this.pinTicker,
|
|
526
|
+
navigateTicker: this.navigateTicker,
|
|
518
527
|
openPaneSettings: (paneId) => this.openPaneSettingsFn(paneId),
|
|
519
528
|
|
|
520
529
|
on: <K extends keyof PluginEvents>(event: K, handler: (payload: PluginEvents[K]) => void) => {
|
|
@@ -526,7 +535,7 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
526
535
|
|
|
527
536
|
showWidget: (widgetId) => this.showWidget(widgetId),
|
|
528
537
|
hideWidget: (widgetId) => this.hideWidget(widgetId),
|
|
529
|
-
|
|
538
|
+
notify: (notification) => this.notifyFn(notification),
|
|
530
539
|
};
|
|
531
540
|
}
|
|
532
541
|
|
|
@@ -630,4 +639,27 @@ export class PluginRegistry implements PluginRuntimeAccess {
|
|
|
630
639
|
this.plugins.delete(pluginId);
|
|
631
640
|
this.events.emit("plugin:unregistered", { pluginId });
|
|
632
641
|
}
|
|
642
|
+
|
|
643
|
+
destroy(): void {
|
|
644
|
+
for (const pluginId of [...this.plugins.keys()].reverse()) {
|
|
645
|
+
try {
|
|
646
|
+
this.unregister(pluginId);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
this.registryLog.error("Failed to unregister plugin during registry destroy", {
|
|
649
|
+
pluginId,
|
|
650
|
+
error: error instanceof Error ? error.message : String(error),
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
this.pluginResumeListeners.clear();
|
|
655
|
+
if (sharedRegistry === this) {
|
|
656
|
+
sharedRegistry = undefined;
|
|
657
|
+
}
|
|
658
|
+
if (sharedDataProvider === this.dataProvider) {
|
|
659
|
+
sharedDataProvider = undefined;
|
|
660
|
+
}
|
|
661
|
+
if ((globalThis as any).__gloomRegistry === this) {
|
|
662
|
+
delete (globalThis as any).__gloomRegistry;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
633
665
|
}
|
|
@@ -1293,7 +1293,8 @@ describe("ProviderRouter", () => {
|
|
|
1293
1293
|
test("drops stale cached cloud quotes while preserving cached cloud fundamentals", () => {
|
|
1294
1294
|
const dbPath = createTempDbPath("stale-cloud-cached-financials");
|
|
1295
1295
|
const persistence = new AppPersistence(dbPath);
|
|
1296
|
-
const now = Date.
|
|
1296
|
+
const now = Date.now();
|
|
1297
|
+
const previousSession = now - 24 * 60 * 60_000;
|
|
1297
1298
|
|
|
1298
1299
|
persistence.resources.set(
|
|
1299
1300
|
{
|
|
@@ -1313,7 +1314,7 @@ describe("ProviderRouter", () => {
|
|
|
1313
1314
|
currency: "EUR",
|
|
1314
1315
|
change: 28,
|
|
1315
1316
|
changePercent: 5.6,
|
|
1316
|
-
lastUpdated:
|
|
1317
|
+
lastUpdated: previousSession,
|
|
1317
1318
|
marketState: "REGULAR",
|
|
1318
1319
|
exchangeName: "FWB2",
|
|
1319
1320
|
listingExchangeName: "FWB2",
|
package/src/sources/sec-edgar.ts
CHANGED
|
@@ -507,9 +507,21 @@ export class SecEdgarClient {
|
|
|
507
507
|
}
|
|
508
508
|
|
|
509
509
|
async getFilingContent(filing: Pick<SecFilingItem, "primaryDocumentUrl" | "filingUrl" | "form">): Promise<string | null> {
|
|
510
|
-
|
|
510
|
+
let targetUrl = filing.primaryDocumentUrl || filing.filingUrl;
|
|
511
511
|
if (!targetUrl) return null;
|
|
512
512
|
|
|
513
|
+
// SEC ownership forms (3/4/5) have XSL-prefixed primaryDocument paths
|
|
514
|
+
// (e.g., "xslF345X06/wk-form4_xxx.xml") which return rendered HTML.
|
|
515
|
+
// Strip the prefix and return the raw XML for programmatic parsing.
|
|
516
|
+
const ownershipForms = new Set(["3", "4", "5"]);
|
|
517
|
+
if (filing.primaryDocumentUrl && filing.form && ownershipForms.has(filing.form.trim())) {
|
|
518
|
+
const rawUrl = filing.primaryDocumentUrl.replace(/\/xsl[^/]+\//, "/");
|
|
519
|
+
if (rawUrl !== filing.primaryDocumentUrl) {
|
|
520
|
+
const { body } = await this.fetchText(rawUrl);
|
|
521
|
+
return body;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
513
525
|
if (filing.primaryDocumentUrl && isPdfDocument("", "", filing.primaryDocumentUrl)) {
|
|
514
526
|
const alternativeUrl = filing.filingUrl
|
|
515
527
|
? await this.findAlternativeHtmlDocumentUrl(filing.filingUrl, filing.primaryDocumentUrl)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Quote, Fundamentals, FinancialStatement, PricePoint, TickerFinancials, MarketState, OptionContract, OptionsChain, CompanyProfile } from "../types/financials";
|
|
2
|
-
import type { DataProvider, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
|
|
2
|
+
import type { DataProvider, EarningsEvent, MarketDataRequestContext, NewsItem, SecFilingItem } from "../types/data-provider";
|
|
3
3
|
import type { TimeRange } from "../components/chart/chart-types";
|
|
4
4
|
import {
|
|
5
5
|
normalizeChartResolutionSupport,
|
|
@@ -1065,4 +1065,78 @@ export class YahooFinanceClient implements DataProvider {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
throw lastError || new Error(`No options chain for ${ticker}`);
|
|
1067
1067
|
}
|
|
1068
|
+
|
|
1069
|
+
async getEarningsCalendar(symbols: string[], _context?: MarketDataRequestContext): Promise<EarningsEvent[]> {
|
|
1070
|
+
const results: EarningsEvent[] = [];
|
|
1071
|
+
const BATCH_SIZE = 5;
|
|
1072
|
+
|
|
1073
|
+
for (let i = 0; i < symbols.length; i += BATCH_SIZE) {
|
|
1074
|
+
if (i > 0) await new Promise((r) => setTimeout(r, 200));
|
|
1075
|
+
const batch = symbols.slice(i, i + BATCH_SIZE);
|
|
1076
|
+
|
|
1077
|
+
const settled = await Promise.allSettled(
|
|
1078
|
+
batch.map(async (symbol) => {
|
|
1079
|
+
const params = new URLSearchParams({ modules: "calendarEvents,earningsTrend,quoteType" });
|
|
1080
|
+
const url = `https://query1.finance.yahoo.com/v10/finance/quoteSummary/${encodeURIComponent(symbol)}?${params}`;
|
|
1081
|
+
const data = await this.fetchJsonWithCrumb<{
|
|
1082
|
+
quoteSummary?: {
|
|
1083
|
+
result?: Array<{
|
|
1084
|
+
calendarEvents?: {
|
|
1085
|
+
earnings?: {
|
|
1086
|
+
earningsDate?: Array<{ raw: number }>;
|
|
1087
|
+
earningsAverage?: { raw: number };
|
|
1088
|
+
revenueAverage?: { raw: number };
|
|
1089
|
+
};
|
|
1090
|
+
};
|
|
1091
|
+
earningsTrend?: {
|
|
1092
|
+
trend?: Array<{
|
|
1093
|
+
period: string;
|
|
1094
|
+
earningsEstimate?: { avg?: { raw: number } };
|
|
1095
|
+
revenueEstimate?: { avg?: { raw: number } };
|
|
1096
|
+
}>;
|
|
1097
|
+
};
|
|
1098
|
+
quoteType?: {
|
|
1099
|
+
shortName?: string;
|
|
1100
|
+
longName?: string;
|
|
1101
|
+
};
|
|
1102
|
+
}>;
|
|
1103
|
+
};
|
|
1104
|
+
}>(`earnings ${symbol}`, url);
|
|
1105
|
+
|
|
1106
|
+
const mod = data.quoteSummary?.result?.[0];
|
|
1107
|
+
if (!mod) return null;
|
|
1108
|
+
|
|
1109
|
+
const cal = mod.calendarEvents?.earnings;
|
|
1110
|
+
if (!cal?.earningsDate?.length) return null;
|
|
1111
|
+
|
|
1112
|
+
const earningsDate = new Date(cal.earningsDate[0]!.raw * 1000);
|
|
1113
|
+
if (isNaN(earningsDate.getTime())) return null;
|
|
1114
|
+
|
|
1115
|
+
const currentQtr = mod.earningsTrend?.trend?.find((t) => t.period === "0q");
|
|
1116
|
+
const name = mod.quoteType?.shortName || mod.quoteType?.longName || symbol;
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
symbol,
|
|
1120
|
+
name,
|
|
1121
|
+
earningsDate,
|
|
1122
|
+
epsEstimate: currentQtr?.earningsEstimate?.avg?.raw ?? cal.earningsAverage?.raw ?? null,
|
|
1123
|
+
epsActual: null,
|
|
1124
|
+
revenueEstimate: currentQtr?.revenueEstimate?.avg?.raw ?? cal.revenueAverage?.raw ?? null,
|
|
1125
|
+
revenueActual: null,
|
|
1126
|
+
surprise: null,
|
|
1127
|
+
timing: "" as const,
|
|
1128
|
+
};
|
|
1129
|
+
}),
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
for (const result of settled) {
|
|
1133
|
+
if (result.status === "fulfilled" && result.value) {
|
|
1134
|
+
results.push(result.value);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
results.sort((a, b) => a.earningsDate.getTime() - b.earningsDate.getTime());
|
|
1140
|
+
return results;
|
|
1141
|
+
}
|
|
1068
1142
|
}
|
|
@@ -3,7 +3,7 @@ import { act, useRef, type Dispatch } from "react";
|
|
|
3
3
|
import { testRender } from "@opentui/react/test-utils";
|
|
4
4
|
import { AppProvider, PaneInstanceProvider, useAppDispatch, useAppSelector, usePaneTicker, type AppAction } from "./app-context";
|
|
5
5
|
import { cloneLayout, createDefaultConfig, type AppConfig } from "../types/config";
|
|
6
|
-
import { getCurrentThemeId } from "../theme/colors";
|
|
6
|
+
import { applyTheme, getCurrentThemeId } from "../theme/colors";
|
|
7
7
|
|
|
8
8
|
const TEST_PANE_ID = "ticker-detail:test";
|
|
9
9
|
|
|
@@ -51,6 +51,7 @@ describe("pane selectors", () => {
|
|
|
51
51
|
testSetup?.renderer.destroy();
|
|
52
52
|
testSetup = undefined;
|
|
53
53
|
capturedDispatch = null;
|
|
54
|
+
applyTheme("amber");
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
test("does not rerender usePaneTicker consumers for unrelated app state updates", async () => {
|
|
@@ -96,4 +97,19 @@ describe("pane selectors", () => {
|
|
|
96
97
|
await testSetup.renderOnce();
|
|
97
98
|
expect(testSetup.captureCharFrame()).toContain(`${TEST_PANE_ID}:green`);
|
|
98
99
|
});
|
|
100
|
+
|
|
101
|
+
test("uses the configured theme on the first provider render", async () => {
|
|
102
|
+
const config = createTickerDetailConfig("AAPL");
|
|
103
|
+
config.theme = "green";
|
|
104
|
+
|
|
105
|
+
testSetup = await testRender(
|
|
106
|
+
<AppProvider config={config}>
|
|
107
|
+
<ThemeSelectorHarness />
|
|
108
|
+
</AppProvider>,
|
|
109
|
+
{ width: 32, height: 4 },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
await testSetup.renderOnce();
|
|
113
|
+
expect(testSetup.captureCharFrame()).toContain(`${TEST_PANE_ID}:green`);
|
|
114
|
+
});
|
|
99
115
|
});
|
|
@@ -22,6 +22,30 @@ describe("appReducer command bar state", () => {
|
|
|
22
22
|
expect(next.commandBarQuery).toBe("");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
+
test("stores and clears command bar launch requests", () => {
|
|
26
|
+
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
27
|
+
const opened = appReducer(initial, {
|
|
28
|
+
type: "SET_COMMAND_BAR",
|
|
29
|
+
open: true,
|
|
30
|
+
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
31
|
+
});
|
|
32
|
+
const relaunched = appReducer(opened, {
|
|
33
|
+
type: "SET_COMMAND_BAR",
|
|
34
|
+
open: true,
|
|
35
|
+
launch: { kind: "plugin-command", commandId: "set-alert" },
|
|
36
|
+
});
|
|
37
|
+
const closed = appReducer(relaunched, { type: "SET_COMMAND_BAR", open: false });
|
|
38
|
+
|
|
39
|
+
expect(opened.commandBarOpen).toBe(true);
|
|
40
|
+
expect(opened.commandBarLaunchRequest).toEqual({
|
|
41
|
+
kind: "plugin-command",
|
|
42
|
+
commandId: "set-alert",
|
|
43
|
+
sequence: 1,
|
|
44
|
+
});
|
|
45
|
+
expect(relaunched.commandBarLaunchRequest?.sequence).toBe(2);
|
|
46
|
+
expect(closed.commandBarLaunchRequest).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
|
|
25
49
|
test("toggle opens blank and clears on close", () => {
|
|
26
50
|
const initial = createInitialState(createDefaultConfig("/tmp/gloomberb-test"));
|
|
27
51
|
const opened = appReducer(initial, { type: "TOGGLE_COMMAND_BAR" });
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "react";
|
|
14
14
|
import type { SessionStore } from "../data/session-store";
|
|
15
15
|
import { saveConfig } from "../data/config-store";
|
|
16
|
-
import { applyTheme } from "../theme/colors";
|
|
16
|
+
import { applyTheme, syncTheme } from "../theme/colors";
|
|
17
17
|
import { isBrokerPortfolioId } from "../utils/broker-instances";
|
|
18
18
|
import { resolveTickerFinancialsQuoteState } from "../utils/quote-resolution";
|
|
19
19
|
import {
|
|
@@ -61,6 +61,12 @@ export interface CollectionSortPreference {
|
|
|
61
61
|
direction: SortDirection;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface CommandBarLaunchRequest {
|
|
65
|
+
kind: "plugin-command";
|
|
66
|
+
commandId: string;
|
|
67
|
+
sequence: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
export interface AppState {
|
|
65
71
|
config: AppConfig;
|
|
66
72
|
tickers: Map<string, TickerRecord>;
|
|
@@ -73,6 +79,7 @@ export interface AppState {
|
|
|
73
79
|
recentTickers: string[];
|
|
74
80
|
commandBarOpen: boolean;
|
|
75
81
|
commandBarQuery: string;
|
|
82
|
+
commandBarLaunchRequest: CommandBarLaunchRequest | null;
|
|
76
83
|
refreshing: Set<string>;
|
|
77
84
|
initialized: boolean;
|
|
78
85
|
statusBarVisible: boolean;
|
|
@@ -97,7 +104,12 @@ export type AppAction =
|
|
|
97
104
|
| { type: "TRACK_TICKER"; symbol: string | null }
|
|
98
105
|
| { type: "SET_ACTIVE_PANEL"; panel: "left" | "right" }
|
|
99
106
|
| { type: "TOGGLE_COMMAND_BAR" }
|
|
100
|
-
| {
|
|
107
|
+
| {
|
|
108
|
+
type: "SET_COMMAND_BAR";
|
|
109
|
+
open: boolean;
|
|
110
|
+
query?: string;
|
|
111
|
+
launch?: { kind: "plugin-command"; commandId: string } | null;
|
|
112
|
+
}
|
|
101
113
|
| { type: "SET_COMMAND_BAR_QUERY"; query: string }
|
|
102
114
|
| { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
|
|
103
115
|
| { type: "SET_BROKER_ACCOUNTS"; instanceId: string; accounts: BrokerAccount[] }
|
|
@@ -492,15 +504,23 @@ export function appReducer(state: AppState, action: AppAction): AppState {
|
|
|
492
504
|
|
|
493
505
|
case "TOGGLE_COMMAND_BAR":
|
|
494
506
|
return state.commandBarOpen
|
|
495
|
-
? { ...state, commandBarOpen: false, commandBarQuery: "" }
|
|
496
|
-
: { ...state, commandBarOpen: true, commandBarQuery: "" };
|
|
497
|
-
|
|
498
|
-
case "SET_COMMAND_BAR":
|
|
507
|
+
? { ...state, commandBarOpen: false, commandBarQuery: "", commandBarLaunchRequest: null }
|
|
508
|
+
: { ...state, commandBarOpen: true, commandBarQuery: "", commandBarLaunchRequest: null };
|
|
509
|
+
|
|
510
|
+
case "SET_COMMAND_BAR": {
|
|
511
|
+
const launchRequest = action.open && action.launch
|
|
512
|
+
? {
|
|
513
|
+
...action.launch,
|
|
514
|
+
sequence: (state.commandBarLaunchRequest?.sequence ?? 0) + 1,
|
|
515
|
+
}
|
|
516
|
+
: null;
|
|
499
517
|
return {
|
|
500
518
|
...state,
|
|
501
519
|
commandBarOpen: action.open,
|
|
502
520
|
commandBarQuery: action.open ? (action.query ?? "") : "",
|
|
521
|
+
commandBarLaunchRequest: launchRequest,
|
|
503
522
|
};
|
|
523
|
+
}
|
|
504
524
|
|
|
505
525
|
case "SET_COMMAND_BAR_QUERY":
|
|
506
526
|
return { ...state, commandBarQuery: action.query };
|
|
@@ -1018,6 +1038,7 @@ export function createInitialState(config: AppConfig, sessionSnapshot: AppSessio
|
|
|
1018
1038
|
recentTickers: config.recentTickers,
|
|
1019
1039
|
commandBarOpen: false,
|
|
1020
1040
|
commandBarQuery: "",
|
|
1041
|
+
commandBarLaunchRequest: null,
|
|
1021
1042
|
refreshing: new Set(),
|
|
1022
1043
|
initialized: false,
|
|
1023
1044
|
statusBarVisible: sessionSnapshot?.statusBarVisible !== false,
|
|
@@ -1043,6 +1064,8 @@ export function AppProvider({
|
|
|
1043
1064
|
sessionStore?: SessionStore;
|
|
1044
1065
|
sessionSnapshot?: AppSessionSnapshot | null;
|
|
1045
1066
|
}) {
|
|
1067
|
+
// Sync the configured theme before descendants read the shared palette on first render.
|
|
1068
|
+
syncTheme(config.theme);
|
|
1046
1069
|
const [state, dispatch] = useReducer(appReducer, { config, sessionSnapshot }, ({ config, sessionSnapshot: initialSessionSnapshot }) => (
|
|
1047
1070
|
createInitialState(config, initialSessionSnapshot)
|
|
1048
1071
|
));
|
package/src/theme/colors.ts
CHANGED
|
@@ -18,6 +18,11 @@ export function applyTheme(id: string): void {
|
|
|
18
18
|
Object.assign(colors, theme);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function syncTheme(id: string): void {
|
|
22
|
+
if (currentThemeId === id) return;
|
|
23
|
+
applyTheme(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
export type ColorKey = keyof typeof colors;
|
|
22
27
|
|
|
23
28
|
export { blendHex } from "./color-utils";
|
package/src/theme/themes.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface Theme {
|
|
|
17
17
|
positive: string;
|
|
18
18
|
negative: string;
|
|
19
19
|
neutral: string;
|
|
20
|
+
warning: string;
|
|
20
21
|
|
|
21
22
|
header: string;
|
|
22
23
|
headerText: string;
|
|
@@ -79,6 +80,12 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
79
80
|
highestMinimumContrast(bodySurfaces, [textDim, text, "#c0c7d1"]),
|
|
80
81
|
SUBTLE_TEXT_MIN,
|
|
81
82
|
);
|
|
83
|
+
const warning = blendForContrastOnSurfaces(
|
|
84
|
+
theme.warning,
|
|
85
|
+
bodySurfaces,
|
|
86
|
+
blendHex(theme.warning, theme.textBright, 0.5),
|
|
87
|
+
SUBTLE_TEXT_MIN,
|
|
88
|
+
);
|
|
82
89
|
const selected = blendForContrastOnSurfaces(
|
|
83
90
|
theme.selected,
|
|
84
91
|
bodySurfaces,
|
|
@@ -106,6 +113,7 @@ function normalizeTheme(theme: Theme): Theme {
|
|
|
106
113
|
positive,
|
|
107
114
|
negative,
|
|
108
115
|
neutral,
|
|
116
|
+
warning,
|
|
109
117
|
headerText,
|
|
110
118
|
selected,
|
|
111
119
|
selectedText,
|
|
@@ -127,6 +135,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
127
135
|
positive: "#00cc66",
|
|
128
136
|
negative: "#ff3333",
|
|
129
137
|
neutral: "#888888",
|
|
138
|
+
warning: "#ffaa00",
|
|
130
139
|
header: "#0044aa",
|
|
131
140
|
headerText: "#ffffff",
|
|
132
141
|
selected: "#1a3a5c",
|
|
@@ -149,6 +158,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
149
158
|
positive: "#00ff66",
|
|
150
159
|
negative: "#ff4444",
|
|
151
160
|
neutral: "#666666",
|
|
161
|
+
warning: "#cccc00",
|
|
152
162
|
header: "#003300",
|
|
153
163
|
headerText: "#00ff00",
|
|
154
164
|
selected: "#003300",
|
|
@@ -171,6 +181,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
171
181
|
positive: "#00ff88",
|
|
172
182
|
negative: "#ff4444",
|
|
173
183
|
neutral: "#666666",
|
|
184
|
+
warning: "#ddcc00",
|
|
174
185
|
header: "#002222",
|
|
175
186
|
headerText: "#00ffff",
|
|
176
187
|
selected: "#002a2a",
|
|
@@ -193,6 +204,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
193
204
|
positive: "#44cc44",
|
|
194
205
|
negative: "#ff6666",
|
|
195
206
|
neutral: "#666666",
|
|
207
|
+
warning: "#ddaa00",
|
|
196
208
|
header: "#220000",
|
|
197
209
|
headerText: "#ff3333",
|
|
198
210
|
selected: "#2a0000",
|
|
@@ -215,6 +227,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
215
227
|
positive: "#00cc66",
|
|
216
228
|
negative: "#ff4444",
|
|
217
229
|
neutral: "#666666",
|
|
230
|
+
warning: "#ddaa00",
|
|
218
231
|
header: "#000033",
|
|
219
232
|
headerText: "#4488ff",
|
|
220
233
|
selected: "#001144",
|
|
@@ -237,6 +250,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
237
250
|
positive: "#44dd66",
|
|
238
251
|
negative: "#ff4444",
|
|
239
252
|
neutral: "#666666",
|
|
253
|
+
warning: "#ddaa00",
|
|
240
254
|
header: "#1a0028",
|
|
241
255
|
headerText: "#bb55ff",
|
|
242
256
|
selected: "#220033",
|
|
@@ -259,6 +273,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
259
273
|
positive: "#44dd66",
|
|
260
274
|
negative: "#ff5555",
|
|
261
275
|
neutral: "#666666",
|
|
276
|
+
warning: "#ddaa00",
|
|
262
277
|
header: "#22000f",
|
|
263
278
|
headerText: "#ff44aa",
|
|
264
279
|
selected: "#2a0018",
|
|
@@ -281,6 +296,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
281
296
|
positive: "#00cc66",
|
|
282
297
|
negative: "#ff3333",
|
|
283
298
|
neutral: "#888888",
|
|
299
|
+
warning: "#ddaa00",
|
|
284
300
|
header: "#1a1a1a",
|
|
285
301
|
headerText: "#ffffff",
|
|
286
302
|
selected: "#222222",
|
|
@@ -303,6 +319,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
303
319
|
positive: "#9ece6a",
|
|
304
320
|
negative: "#f7768e",
|
|
305
321
|
neutral: "#565f89",
|
|
322
|
+
warning: "#e0af68",
|
|
306
323
|
header: "#24283b",
|
|
307
324
|
headerText: "#7aa2f7",
|
|
308
325
|
selected: "#283457",
|
|
@@ -325,6 +342,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
325
342
|
positive: "#859900",
|
|
326
343
|
negative: "#dc322f",
|
|
327
344
|
neutral: "#657b83",
|
|
345
|
+
warning: "#b58900",
|
|
328
346
|
header: "#073642",
|
|
329
347
|
headerText: "#268bd2",
|
|
330
348
|
selected: "#0a4a5c",
|
|
@@ -347,6 +365,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
347
365
|
positive: "#50fa7b",
|
|
348
366
|
negative: "#ff5555",
|
|
349
367
|
neutral: "#6272a4",
|
|
368
|
+
warning: "#f1fa8c",
|
|
350
369
|
header: "#44475a",
|
|
351
370
|
headerText: "#bd93f9",
|
|
352
371
|
selected: "#44475a",
|
|
@@ -369,6 +388,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
369
388
|
positive: "#a3be8c",
|
|
370
389
|
negative: "#bf616a",
|
|
371
390
|
neutral: "#4c566a",
|
|
391
|
+
warning: "#ebcb8b",
|
|
372
392
|
header: "#3b4252",
|
|
373
393
|
headerText: "#88c0d0",
|
|
374
394
|
selected: "#4c566a",
|
|
@@ -391,6 +411,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
391
411
|
positive: "#a6e22e",
|
|
392
412
|
negative: "#f92672",
|
|
393
413
|
neutral: "#75715e",
|
|
414
|
+
warning: "#e6db74",
|
|
394
415
|
header: "#49483e",
|
|
395
416
|
headerText: "#e6db74",
|
|
396
417
|
selected: "#49483e",
|
|
@@ -413,6 +434,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
413
434
|
positive: "#a6e3a1",
|
|
414
435
|
negative: "#f38ba8",
|
|
415
436
|
neutral: "#6c7086",
|
|
437
|
+
warning: "#f9e2af",
|
|
416
438
|
header: "#313244",
|
|
417
439
|
headerText: "#cba6f7",
|
|
418
440
|
selected: "#3e3f56",
|
|
@@ -435,6 +457,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
435
457
|
positive: "#b8bb26",
|
|
436
458
|
negative: "#fb4934",
|
|
437
459
|
neutral: "#928374",
|
|
460
|
+
warning: "#fabd2f",
|
|
438
461
|
header: "#3c3836",
|
|
439
462
|
headerText: "#fabd2f",
|
|
440
463
|
selected: "#3c3836",
|
|
@@ -457,6 +480,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
457
480
|
positive: "#31748f",
|
|
458
481
|
negative: "#eb6f92",
|
|
459
482
|
neutral: "#6e6a86",
|
|
483
|
+
warning: "#f6c177",
|
|
460
484
|
header: "#26233a",
|
|
461
485
|
headerText: "#c4a7e7",
|
|
462
486
|
selected: "#332f4a",
|
|
@@ -479,6 +503,7 @@ const rawThemes: Record<string, Theme> = {
|
|
|
479
503
|
positive: "#00cc66",
|
|
480
504
|
negative: "#ff3333",
|
|
481
505
|
neutral: "#6688bb",
|
|
506
|
+
warning: "#ff9900",
|
|
482
507
|
header: "#001144",
|
|
483
508
|
headerText: "#ff6600",
|
|
484
509
|
selected: "#002255",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
createPaneInstance,
|
|
4
|
+
findPrimaryPaneInstance,
|
|
5
|
+
resolveFollowBindingInstance,
|
|
6
|
+
resolvePaneInstance,
|
|
7
|
+
type LayoutConfig,
|
|
8
|
+
type PaneInstanceConfig,
|
|
9
|
+
} from "./config";
|
|
10
|
+
|
|
11
|
+
function createLayout(instances: PaneInstanceConfig[]): LayoutConfig {
|
|
12
|
+
return {
|
|
13
|
+
dockRoot: null,
|
|
14
|
+
instances,
|
|
15
|
+
floating: [],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("findPrimaryPaneInstance", () => {
|
|
20
|
+
test("prefers the main non-fixed ticker pane", () => {
|
|
21
|
+
const layout = createLayout([
|
|
22
|
+
createPaneInstance("portfolio-list", {
|
|
23
|
+
instanceId: "portfolio-list:main",
|
|
24
|
+
binding: { kind: "none" },
|
|
25
|
+
}),
|
|
26
|
+
createPaneInstance("ticker-detail", {
|
|
27
|
+
instanceId: "ticker-detail:pinned",
|
|
28
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
29
|
+
}),
|
|
30
|
+
createPaneInstance("ticker-detail", {
|
|
31
|
+
instanceId: "ticker-detail:main",
|
|
32
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")?.instanceId).toBe("ticker-detail:main");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("does not treat fixed ticker panes as the primary pane", () => {
|
|
40
|
+
const layout = createLayout([
|
|
41
|
+
createPaneInstance("ticker-detail", {
|
|
42
|
+
instanceId: "ticker-detail:aapl",
|
|
43
|
+
binding: { kind: "fixed", symbol: "AAPL" },
|
|
44
|
+
}),
|
|
45
|
+
createPaneInstance("ticker-detail", {
|
|
46
|
+
instanceId: "ticker-detail:msft",
|
|
47
|
+
binding: { kind: "fixed", symbol: "MSFT" },
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
expect(findPrimaryPaneInstance(layout, "ticker-detail")).toBeUndefined();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("resolvePaneInstance", () => {
|
|
56
|
+
test("accepts either an instance id or a pane id", () => {
|
|
57
|
+
const layout = createLayout([
|
|
58
|
+
createPaneInstance("portfolio-list", {
|
|
59
|
+
instanceId: "portfolio-list:main",
|
|
60
|
+
binding: { kind: "none" },
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(resolvePaneInstance(layout, "portfolio-list:main")?.instanceId).toBe("portfolio-list:main");
|
|
65
|
+
expect(resolvePaneInstance(layout, "portfolio-list")?.instanceId).toBe("portfolio-list:main");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("resolveFollowBindingInstance", () => {
|
|
70
|
+
test("walks follow bindings until it finds a matching pane", () => {
|
|
71
|
+
const layout = createLayout([
|
|
72
|
+
createPaneInstance("portfolio-list", {
|
|
73
|
+
instanceId: "portfolio-list:main",
|
|
74
|
+
binding: { kind: "none" },
|
|
75
|
+
}),
|
|
76
|
+
createPaneInstance("ticker-detail", {
|
|
77
|
+
instanceId: "ticker-detail:main",
|
|
78
|
+
binding: { kind: "follow", sourceInstanceId: "portfolio-list:main" },
|
|
79
|
+
}),
|
|
80
|
+
createPaneInstance("quote-monitor", {
|
|
81
|
+
instanceId: "quote-monitor:main",
|
|
82
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:main" },
|
|
83
|
+
}),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
resolveFollowBindingInstance(layout, "quote-monitor:main", (instance) => instance.paneId === "portfolio-list")?.instanceId,
|
|
88
|
+
).toBe("portfolio-list:main");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("stops on follow cycles", () => {
|
|
92
|
+
const layout = createLayout([
|
|
93
|
+
createPaneInstance("ticker-detail", {
|
|
94
|
+
instanceId: "ticker-detail:first",
|
|
95
|
+
binding: { kind: "follow", sourceInstanceId: "quote-monitor:first" },
|
|
96
|
+
}),
|
|
97
|
+
createPaneInstance("quote-monitor", {
|
|
98
|
+
instanceId: "quote-monitor:first",
|
|
99
|
+
binding: { kind: "follow", sourceInstanceId: "ticker-detail:first" },
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
resolveFollowBindingInstance(layout, "ticker-detail:first", (instance) => instance.paneId === "portfolio-list"),
|
|
105
|
+
).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|