gloomberb 0.9.0 → 0.9.2
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 +6 -2
- package/patches/electrobun@1.18.1.patch +85 -0
- package/src/app/global-shortcuts.test.tsx +12 -0
- package/src/app/global-shortcuts.ts +1 -1
- package/src/assets/gloomberb-logo-windows.ico +0 -0
- package/src/assets/gloomberb-logo-windows.png +0 -0
- package/src/assets/gloomberb-logo.ico +0 -0
- package/src/components/chart/comparison-stock-chart/helpers.ts +0 -32
- package/src/components/chart/comparison-stock-chart/layout-metrics.ts +6 -6
- package/src/components/chart/comparison-stock-chart/legend.tsx +41 -44
- package/src/components/chart/comparison-stock-chart/view.tsx +46 -6
- package/src/components/layout/detached-pane-shell.tsx +11 -4
- package/src/components/layout/header.tsx +25 -10
- package/src/components/layout/titlebar-overlay.ts +16 -0
- package/src/components/layout/window-controls.tsx +99 -0
- package/src/components/price-performance.tsx +175 -0
- package/src/market-data/performance.test.ts +54 -0
- package/src/market-data/performance.ts +109 -0
- package/src/plugins/builtin/comparison-chart/index.test.tsx +88 -9
- package/src/plugins/builtin/congress-trades/detail.tsx +204 -68
- package/src/plugins/builtin/congress-trades/footer.ts +14 -8
- package/src/plugins/builtin/congress-trades/keyboard.ts +4 -23
- package/src/plugins/builtin/congress-trades/model.ts +29 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +8 -13
- package/src/plugins/builtin/congress-trades/table.tsx +10 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +0 -14
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +12 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +4 -0
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +51 -3
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +73 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/windows-custom-chrome.ts +58 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +70 -0
- package/src/renderers/electrobun/bun/desktop/windows-native.ts +75 -0
- package/src/renderers/electrobun/bun/index.ts +36 -7
- package/src/renderers/electrobun/bun/window/frame.test.ts +7 -0
- package/src/renderers/electrobun/bun/window/frame.ts +5 -0
- package/src/renderers/electrobun/shared/protocol.ts +1 -0
- package/src/renderers/electrobun/view/build-assets.ts +2 -1
- package/src/renderers/electrobun/view/host/text.tsx +29 -9
- package/src/renderers/electrobun/view/main.tsx +2 -1
- package/src/renderers/electrobun/view/styles.css +29 -8
- package/src/renderers/electrobun/view/ui-host.tsx +113 -74
- package/src/ui/host.tsx +2 -0
- package/src/updater.test.ts +47 -2
- package/src/updater.ts +67 -10
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gloomberb",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Market research and portfolio tracker for the terminal",
|
|
5
5
|
"module": "src/index.tsx",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"desktop:dev": "electrobun dev --watch",
|
|
22
22
|
"desktop:build": "electrobun build",
|
|
23
23
|
"desktop:release": "electrobun build --env=stable",
|
|
24
|
+
"desktop:release:windows": "electrobun build --env=stable --targets=win-x64",
|
|
24
25
|
"test": "bun test",
|
|
25
26
|
"release": "./scripts/release.sh"
|
|
26
27
|
},
|
|
@@ -40,6 +41,8 @@
|
|
|
40
41
|
"@types/bun": "^1.3.14",
|
|
41
42
|
"@types/react-dom": "^19.2.3",
|
|
42
43
|
"electrobun": "^1.18.1",
|
|
44
|
+
"png-to-ico": "^2.1.8",
|
|
45
|
+
"rcedit": "^4.0.1",
|
|
43
46
|
"react-dom": "19.2.7"
|
|
44
47
|
},
|
|
45
48
|
"peerDependencies": {
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
"web-tree-sitter": "0.25.10"
|
|
60
63
|
},
|
|
61
64
|
"patchedDependencies": {
|
|
62
|
-
"@opentui/core@0.3.2": "patches/@opentui%2Fcore@0.3.2.patch"
|
|
65
|
+
"@opentui/core@0.3.2": "patches/@opentui%2Fcore@0.3.2.patch",
|
|
66
|
+
"electrobun@1.18.1": "patches/electrobun@1.18.1.patch"
|
|
63
67
|
}
|
|
64
68
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
diff --git a/dist/api/bun/core/BrowserWindow.ts b/dist/api/bun/core/BrowserWindow.ts
|
|
2
|
+
index c26c2c977ee1fc25a8dc144b85bac8626a45f572..8920a2905380228c23cba66c20d9bfd19ee13e02 100644
|
|
3
|
+
--- a/dist/api/bun/core/BrowserWindow.ts
|
|
4
|
+
+++ b/dist/api/bun/core/BrowserWindow.ts
|
|
5
|
+
@@ -8,9 +8,18 @@ import { type RPCWithTransport } from "../../shared/rpc.js";
|
|
6
|
+
import { getNextWindowId } from "./windowIds";
|
|
7
|
+
import { GpuWindowMap } from "./GpuWindow";
|
|
8
|
+
import { WGPUView } from "./WGPUView";
|
|
9
|
+
+import { existsSync } from "fs";
|
|
10
|
+
+import { resolve } from "path";
|
|
11
|
+
|
|
12
|
+
const buildConfig = await BuildConfig.get();
|
|
13
|
+
|
|
14
|
+
+function defaultWindowIconPath(): string | null {
|
|
15
|
+
+ if (process.platform !== "win32") return null;
|
|
16
|
+
+
|
|
17
|
+
+ const iconPath = resolve("../Resources/app.ico");
|
|
18
|
+
+ return existsSync(iconPath) ? iconPath : null;
|
|
19
|
+
+}
|
|
20
|
+
+
|
|
21
|
+
export type WindowOptionsType<T = undefined> = {
|
|
22
|
+
trafficLightOffset?: {
|
|
23
|
+
x: number;
|
|
24
|
+
@@ -28,6 +37,7 @@ export type WindowOptionsType<T = undefined> = {
|
|
25
|
+
html: string | null;
|
|
26
|
+
preload: string | null;
|
|
27
|
+
viewsRoot: string | null;
|
|
28
|
+
+ iconPath?: string | null;
|
|
29
|
+
renderer: "native" | "cef";
|
|
30
|
+
rpc?: T;
|
|
31
|
+
styleMask?: {};
|
|
32
|
+
@@ -124,6 +134,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
33
|
+
html: string | null = null;
|
|
34
|
+
preload: string | null = null;
|
|
35
|
+
viewsRoot: string | null = null;
|
|
36
|
+
+ iconPath: string | null = null;
|
|
37
|
+
renderer: "native" | "cef" = "native";
|
|
38
|
+
transparent: boolean = false;
|
|
39
|
+
passthrough: boolean = false;
|
|
40
|
+
@@ -155,6 +166,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
41
|
+
this.html = options.html || null;
|
|
42
|
+
this.preload = options.preload || null;
|
|
43
|
+
this.viewsRoot = options.viewsRoot || null;
|
|
44
|
+
+ this.iconPath = options.iconPath ?? defaultWindowIconPath();
|
|
45
|
+
this.renderer = options.renderer || defaultOptions.renderer;
|
|
46
|
+
this.transparent = options.transparent ?? false;
|
|
47
|
+
this.passthrough = options.passthrough ?? false;
|
|
48
|
+
@@ -181,6 +193,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
|
|
49
|
+
id: this.id,
|
|
50
|
+
title: this.title,
|
|
51
|
+
url: this.url || "",
|
|
52
|
+
+ iconPath: this.iconPath,
|
|
53
|
+
frame: {
|
|
54
|
+
width: this.frame.width,
|
|
55
|
+
height: this.frame.height,
|
|
56
|
+
diff --git a/dist/api/bun/proc/native.ts b/dist/api/bun/proc/native.ts
|
|
57
|
+
index f0f6c2532ba2eb467ec1d452a25c835b6db72661..ae77cf58d32392ad4cc8e2103ac7ed27baae8bea 100644
|
|
58
|
+
--- a/dist/api/bun/proc/native.ts
|
|
59
|
+
+++ b/dist/api/bun/proc/native.ts
|
|
60
|
+
@@ -829,6 +829,7 @@ const _ffiImpl = {
|
|
61
|
+
id: number;
|
|
62
|
+
url: string | null;
|
|
63
|
+
title: string;
|
|
64
|
+
+ iconPath?: string | null;
|
|
65
|
+
frame: {
|
|
66
|
+
width: number;
|
|
67
|
+
height: number;
|
|
68
|
+
@@ -862,6 +863,7 @@ const _ffiImpl = {
|
|
69
|
+
id,
|
|
70
|
+
url: _url,
|
|
71
|
+
title,
|
|
72
|
+
+ iconPath,
|
|
73
|
+
frame: { x, y, width, height },
|
|
74
|
+
styleMask: {
|
|
75
|
+
Borderless,
|
|
76
|
+
@@ -926,6 +928,9 @@ const _ffiImpl = {
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
native_.symbols.setWindowTitle(windowPtr, toCString(title));
|
|
80
|
+
+ if (iconPath) {
|
|
81
|
+
+ native_.symbols.setWindowIcon(windowPtr, toCString(iconPath));
|
|
82
|
+
+ }
|
|
83
|
+
if (!hidden) {
|
|
84
|
+
native_.symbols.showWindow(windowPtr, activate);
|
|
85
|
+
}
|
|
@@ -135,6 +135,18 @@ describe("useAppGlobalShortcuts", () => {
|
|
|
135
135
|
expect(event.propagationStopped).toBe(true);
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
test("toggles the command bar with Ctrl-K", async () => {
|
|
139
|
+
const actions: AppAction[] = [];
|
|
140
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-global-shortcuts"));
|
|
141
|
+
await renderHarness(state, createRegistry(), (action) => actions.push(action));
|
|
142
|
+
|
|
143
|
+
const event = await emitKeypress({ name: "k", ctrl: true });
|
|
144
|
+
|
|
145
|
+
expect(actions).toEqual([{ type: "TOGGLE_COMMAND_BAR" }]);
|
|
146
|
+
expect(event.defaultPrevented).toBe(true);
|
|
147
|
+
expect(event.propagationStopped).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
138
150
|
test("opens ticker search with backtick", async () => {
|
|
139
151
|
const actions: AppAction[] = [];
|
|
140
152
|
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-global-shortcuts"));
|
|
@@ -52,7 +52,7 @@ export function useAppGlobalShortcuts({
|
|
|
52
52
|
|
|
53
53
|
if (!isDetachedWindow && (
|
|
54
54
|
(event.name === "p" && event.ctrl)
|
|
55
|
-
|| (event.name === "k" && (event.meta || event.super))
|
|
55
|
+
|| (event.name === "k" && (event.ctrl || event.meta || event.super))
|
|
56
56
|
)) {
|
|
57
57
|
event.preventDefault();
|
|
58
58
|
event.stopPropagation();
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,8 +2,6 @@ import type { PricePoint } from "../../../types/financials";
|
|
|
2
2
|
import type { LocalPlotPointer } from "../core/pointer";
|
|
3
3
|
import type { ComparisonChartRenderMode } from "../core/types";
|
|
4
4
|
import type { projectComparisonChartData } from "../comparison/data";
|
|
5
|
-
import { formatPercentRaw } from "../../../utils/format";
|
|
6
|
-
import { formatMarketPriceWithCurrency } from "../../../market-data/market/format";
|
|
7
5
|
|
|
8
6
|
export function clamp(value: number, min: number, max: number): number {
|
|
9
7
|
return Math.max(min, Math.min(max, value));
|
|
@@ -42,36 +40,6 @@ export function buildComparisonNativeBitmapKey(
|
|
|
42
40
|
].join("::");
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
export function getLegendColumns(width: number): number {
|
|
46
|
-
if (width >= 110) return 3;
|
|
47
|
-
if (width >= 72) return 2;
|
|
48
|
-
return 1;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function clipText(text: string, width: number): string {
|
|
52
|
-
if (width <= 0) return "";
|
|
53
|
-
if (text.length <= width) return text.padEnd(width);
|
|
54
|
-
if (width <= 3) return text.slice(0, width);
|
|
55
|
-
return `${text.slice(0, width - 3)}...`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function formatLegendSummary(
|
|
59
|
-
symbol: string,
|
|
60
|
-
rawValue: number | null,
|
|
61
|
-
baseValue: number | null,
|
|
62
|
-
currency: string,
|
|
63
|
-
priceRange: number | undefined,
|
|
64
|
-
): string {
|
|
65
|
-
if (rawValue === null) return `${symbol} waiting`;
|
|
66
|
-
const price = formatMarketPriceWithCurrency(rawValue, currency, {
|
|
67
|
-
minimumFractionDigits: 2,
|
|
68
|
-
precisionOffset: 1,
|
|
69
|
-
priceRange,
|
|
70
|
-
});
|
|
71
|
-
if (baseValue === null || baseValue === 0) return `${symbol} ${price}`;
|
|
72
|
-
return `${symbol} ${price} ${formatPercentRaw(((rawValue - baseValue) / baseValue) * 100)}`;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
43
|
export function getInitialComparisonMode(mode: string | undefined): ComparisonChartRenderMode {
|
|
76
44
|
return mode === "line" ? "line" : "area";
|
|
77
45
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ChartAxisMode } from "../core/types";
|
|
2
|
-
import { getLegendColumns } from "./helpers";
|
|
3
2
|
|
|
4
3
|
export interface ComparisonChartLayoutMetrics {
|
|
5
4
|
axisGap: number;
|
|
@@ -36,15 +35,16 @@ export function resolveComparisonChartLayoutMetrics({
|
|
|
36
35
|
const controlRows = 2;
|
|
37
36
|
const timeAxisRows = 1;
|
|
38
37
|
const helpRows = 0;
|
|
39
|
-
const legendColumns =
|
|
40
|
-
const legendNeededRows = symbolCount > 0 ?
|
|
41
|
-
const
|
|
42
|
-
|
|
38
|
+
const legendColumns = 1;
|
|
39
|
+
const legendNeededRows = symbolCount > 0 ? symbolCount + 1 : 0;
|
|
40
|
+
const legendAvailableRows = Math.max(height - (headerRows + controlRows + timeAxisRows + helpRows + 6), 0);
|
|
41
|
+
const legendRows = legendNeededRows > 0 && legendAvailableRows > 0
|
|
42
|
+
? Math.min(5, Math.max(2, Math.min(legendAvailableRows, legendNeededRows)))
|
|
43
43
|
: 0;
|
|
44
44
|
const chartHeight = Math.max(height - headerRows - controlRows - timeAxisRows - helpRows - legendRows, 4);
|
|
45
45
|
const minChartWidth = 20;
|
|
46
46
|
const measurementChartWidth = Math.max(width - axisSectionWidthBudget - axisGap, minChartWidth);
|
|
47
|
-
const legendItemWidth = Math.max(
|
|
47
|
+
const legendItemWidth = Math.max(width, 20);
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
50
|
axisGap,
|
|
@@ -1,73 +1,70 @@
|
|
|
1
|
-
import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
|
|
2
|
-
import { blendHex, colors } from "../../../theme/colors";
|
|
3
1
|
import type { ComparisonChartProjection } from "../comparison/data";
|
|
4
|
-
import {
|
|
2
|
+
import { PriceReturnTable, type PriceReturnTableRow } from "../../price-performance";
|
|
3
|
+
import type { PriceReturnField } from "../../../market-data/performance";
|
|
5
4
|
|
|
6
5
|
interface ComparisonChartLegendProps {
|
|
7
6
|
legendActiveIndex: number | null;
|
|
8
|
-
legendColumns: number;
|
|
9
7
|
legendItemWidth: number;
|
|
10
8
|
legendRows: number;
|
|
11
9
|
onOpenSymbol: (symbol: string) => void;
|
|
12
10
|
onSelectSymbol: (symbol: string) => void;
|
|
11
|
+
performanceBySymbol: ReadonlyMap<string, PriceReturnField[]>;
|
|
13
12
|
projection: ComparisonChartProjection;
|
|
14
13
|
selectedSymbol: string | null;
|
|
15
14
|
symbols: string[];
|
|
16
|
-
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function buildRangeReturnField(
|
|
18
|
+
item: ComparisonChartProjection["series"][number] | null,
|
|
19
|
+
legendActiveIndex: number | null,
|
|
20
|
+
): PriceReturnField {
|
|
21
|
+
const activeRaw = legendActiveIndex !== null
|
|
22
|
+
? item?.points[legendActiveIndex]?.rawValue ?? null
|
|
23
|
+
: item?.latestRawValue ?? null;
|
|
24
|
+
const baseValue = item?.baseValue ?? null;
|
|
25
|
+
const value = activeRaw != null && baseValue != null && baseValue !== 0
|
|
26
|
+
? (activeRaw - baseValue) / baseValue
|
|
27
|
+
: null;
|
|
28
|
+
return {
|
|
29
|
+
id: "RNG",
|
|
30
|
+
label: "Rng",
|
|
31
|
+
value,
|
|
32
|
+
};
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
export function ComparisonChartLegend({
|
|
20
36
|
legendActiveIndex,
|
|
21
|
-
legendColumns,
|
|
22
37
|
legendItemWidth,
|
|
23
38
|
legendRows,
|
|
24
39
|
onOpenSymbol,
|
|
25
40
|
onSelectSymbol,
|
|
41
|
+
performanceBySymbol,
|
|
26
42
|
projection,
|
|
27
43
|
selectedSymbol,
|
|
28
44
|
symbols,
|
|
29
|
-
visiblePriceRange,
|
|
30
45
|
}: ComparisonChartLegendProps) {
|
|
31
46
|
if (legendRows <= 0) return null;
|
|
32
47
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
const rows: PriceReturnTableRow[] = symbols.map((symbol) => {
|
|
49
|
+
const item = projection.series.find((entry) => entry.symbol === symbol) ?? null;
|
|
50
|
+
return {
|
|
51
|
+
symbol,
|
|
52
|
+
color: item?.color ?? "#888888",
|
|
53
|
+
fields: [
|
|
54
|
+
buildRangeReturnField(item, legendActiveIndex),
|
|
55
|
+
...(performanceBySymbol.get(symbol) ?? []),
|
|
56
|
+
],
|
|
57
|
+
selected: selectedSymbol === symbol,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
36
60
|
|
|
37
61
|
return (
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const activeRaw = legendActiveIndex !== null
|
|
46
|
-
? item?.points[legendActiveIndex]?.rawValue ?? null
|
|
47
|
-
: item?.latestRawValue ?? null;
|
|
48
|
-
const currency = item?.currency ?? "USD";
|
|
49
|
-
const summary = formatLegendSummary(symbol, activeRaw, item?.baseValue ?? null, currency, visiblePriceRange);
|
|
50
|
-
|
|
51
|
-
return (
|
|
52
|
-
<Box
|
|
53
|
-
key={symbol}
|
|
54
|
-
width={legendItemWidth}
|
|
55
|
-
backgroundColor={isSelected ? blendHex(colors.panel, colors.borderFocused, 0.18) : colors.panel}
|
|
56
|
-
onMouseMove={() => onSelectSymbol(symbol)}
|
|
57
|
-
onMouseDown={() => {
|
|
58
|
-
onSelectSymbol(symbol);
|
|
59
|
-
onOpenSymbol(symbol);
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
<Text fg={item?.color ?? colors.textDim} attributes={isSelected ? TextAttributes.BOLD : 0}>
|
|
63
|
-
{clipText(`${isSelected ? ">" : " "} ${summary}`, legendItemWidth)}
|
|
64
|
-
</Text>
|
|
65
|
-
</Box>
|
|
66
|
-
);
|
|
67
|
-
})}
|
|
68
|
-
</Box>
|
|
69
|
-
))}
|
|
70
|
-
</Box>
|
|
71
|
-
</ScrollBox>
|
|
62
|
+
<PriceReturnTable
|
|
63
|
+
height={legendRows}
|
|
64
|
+
onOpenSymbol={onOpenSymbol}
|
|
65
|
+
onSelectSymbol={onSelectSymbol}
|
|
66
|
+
rows={rows}
|
|
67
|
+
width={legendItemWidth}
|
|
68
|
+
/>
|
|
72
69
|
);
|
|
73
70
|
}
|
|
@@ -5,6 +5,11 @@ import { blendHex, colors } from "../../../theme/colors";
|
|
|
5
5
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
6
6
|
import { usePaneSettingValue } from "../../../state/app/context";
|
|
7
7
|
import { useQuoteStreaming } from "../../../state/hooks/quote-streaming";
|
|
8
|
+
import {
|
|
9
|
+
appendQuoteToPriceReturnHistory,
|
|
10
|
+
buildPriceReturnFields,
|
|
11
|
+
type PriceReturnField,
|
|
12
|
+
} from "../../../market-data/performance";
|
|
8
13
|
import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
9
14
|
import {
|
|
10
15
|
type ChartRendererPreference,
|
|
@@ -170,6 +175,27 @@ function useComparisonChartQuoteStreaming({
|
|
|
170
175
|
useQuoteStreaming(streamingTargets);
|
|
171
176
|
}
|
|
172
177
|
|
|
178
|
+
function getOneYearReturn(fields: readonly PriceReturnField[]): number | null {
|
|
179
|
+
return fields.find((field) => field.id === "1Y")?.value ?? null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sortSymbolsByOneYearReturn(
|
|
183
|
+
symbols: readonly string[],
|
|
184
|
+
performanceBySymbol: ReadonlyMap<string, PriceReturnField[]>,
|
|
185
|
+
): string[] {
|
|
186
|
+
const originalIndex = new Map(symbols.map((symbol, index) => [symbol, index]));
|
|
187
|
+
return [...symbols].sort((left, right) => {
|
|
188
|
+
const leftReturn = getOneYearReturn(performanceBySymbol.get(left) ?? []);
|
|
189
|
+
const rightReturn = getOneYearReturn(performanceBySymbol.get(right) ?? []);
|
|
190
|
+
if (leftReturn != null && rightReturn != null && leftReturn !== rightReturn) {
|
|
191
|
+
return rightReturn - leftReturn;
|
|
192
|
+
}
|
|
193
|
+
if (leftReturn != null && rightReturn == null) return -1;
|
|
194
|
+
if (leftReturn == null && rightReturn != null) return 1;
|
|
195
|
+
return (originalIndex.get(left) ?? 0) - (originalIndex.get(right) ?? 0);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
173
199
|
function ComparisonStockChartView({
|
|
174
200
|
paneId,
|
|
175
201
|
width,
|
|
@@ -250,6 +276,22 @@ function ComparisonStockChartView({
|
|
|
250
276
|
symbolSources,
|
|
251
277
|
viewState,
|
|
252
278
|
});
|
|
279
|
+
const performanceBySymbol = useMemo(() => {
|
|
280
|
+
const bySymbol = new Map<string, PriceReturnField[]>();
|
|
281
|
+
for (const source of symbolSources) {
|
|
282
|
+
const sourceHistory = appendQuoteToPriceReturnHistory(source.priceHistory, source.quote);
|
|
283
|
+
const fallbackHistory = series.find((entry) => entry.symbol === source.symbol)?.points ?? [];
|
|
284
|
+
bySymbol.set(
|
|
285
|
+
source.symbol,
|
|
286
|
+
buildPriceReturnFields(sourceHistory.length >= 2 ? sourceHistory : fallbackHistory),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return bySymbol;
|
|
290
|
+
}, [series, symbolSources]);
|
|
291
|
+
const summarySymbols = useMemo(
|
|
292
|
+
() => sortSymbolsByOneYearReturn(symbols, performanceBySymbol),
|
|
293
|
+
[performanceBySymbol, symbols],
|
|
294
|
+
);
|
|
253
295
|
|
|
254
296
|
useEffect(() => {
|
|
255
297
|
if (symbols.includes(viewState.selectedSymbol ?? "")) return;
|
|
@@ -311,7 +353,7 @@ function ComparisonStockChartView({
|
|
|
311
353
|
cursorY: viewState.cursorY,
|
|
312
354
|
renderer,
|
|
313
355
|
setViewState,
|
|
314
|
-
symbols,
|
|
356
|
+
symbols: summarySymbols,
|
|
315
357
|
});
|
|
316
358
|
const {
|
|
317
359
|
activePreset,
|
|
@@ -352,7 +394,6 @@ function ComparisonStockChartView({
|
|
|
352
394
|
result,
|
|
353
395
|
staticScene,
|
|
354
396
|
timeAxisLabel,
|
|
355
|
-
visiblePriceRange,
|
|
356
397
|
} = useComparisonChartRenderOutput({
|
|
357
398
|
axisMode,
|
|
358
399
|
chartColors,
|
|
@@ -386,7 +427,7 @@ function ComparisonStockChartView({
|
|
|
386
427
|
setResolution,
|
|
387
428
|
setViewState,
|
|
388
429
|
supportMap,
|
|
389
|
-
symbols,
|
|
430
|
+
symbols: summarySymbols,
|
|
390
431
|
updateDisplayCursorTarget,
|
|
391
432
|
viewState,
|
|
392
433
|
visibleDateWindow,
|
|
@@ -479,15 +520,14 @@ function ComparisonStockChartView({
|
|
|
479
520
|
legend={(
|
|
480
521
|
<ComparisonChartLegend
|
|
481
522
|
legendActiveIndex={legendActiveIndex}
|
|
482
|
-
legendColumns={legendColumns}
|
|
483
523
|
legendItemWidth={legendItemWidth}
|
|
484
524
|
legendRows={legendRows}
|
|
485
525
|
onOpenSymbol={onOpenSymbol}
|
|
486
526
|
onSelectSymbol={setSelectedSymbol}
|
|
527
|
+
performanceBySymbol={performanceBySymbol}
|
|
487
528
|
projection={projection}
|
|
488
529
|
selectedSymbol={viewState.selectedSymbol}
|
|
489
|
-
symbols={
|
|
490
|
-
visiblePriceRange={visiblePriceRange}
|
|
530
|
+
symbols={summarySymbols}
|
|
491
531
|
/>
|
|
492
532
|
)}
|
|
493
533
|
plotBitmaps={plotBitmaps}
|
|
@@ -12,7 +12,8 @@ import { PaneBodyFrame, getPaneWindowAttributes } from "./pane/frame";
|
|
|
12
12
|
import { PaneContent } from "./pane/content";
|
|
13
13
|
import { resolvePaneBodyFrame } from "./pane/sizing";
|
|
14
14
|
import { getPaneDisplayTitle } from "./pane/title";
|
|
15
|
-
import { TITLEBAR_OVERLAY_HEIGHT_PX,
|
|
15
|
+
import { TITLEBAR_OVERLAY_HEIGHT_PX, getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
16
|
+
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
16
17
|
import {
|
|
17
18
|
createDoubleEscapeCloseState,
|
|
18
19
|
recordDoubleEscapeClose,
|
|
@@ -41,7 +42,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
41
42
|
typeof document === "undefined" ? true : document.hasFocus()
|
|
42
43
|
));
|
|
43
44
|
const { width, height } = useViewport();
|
|
44
|
-
const { cellHeightPx = 18, nativePaneChrome, titleBarOverlay } = useUiCapabilities();
|
|
45
|
+
const { cellHeightPx = 18, nativePaneChrome, titleBarOverlay, windowControls } = useUiCapabilities();
|
|
46
|
+
const showWindowControls = windowControls === "windows";
|
|
47
|
+
const titlebarLeadingInset = titleBarOverlay ? getTitlebarLeadingInset() : 0;
|
|
45
48
|
const instance = useAppSelector((state) => findPaneInstance(state.config.layout, desktopWindowBridge.paneId) ?? null);
|
|
46
49
|
const paneDef = instance ? pluginRegistry.panes.get(instance.paneId) ?? null : null;
|
|
47
50
|
const hasPaneSettings = !!instance && pluginRegistry.hasPaneSettings(instance.instanceId);
|
|
@@ -171,8 +174,10 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
171
174
|
alignItems="center"
|
|
172
175
|
flexGrow={1}
|
|
173
176
|
minWidth={0}
|
|
174
|
-
|
|
175
|
-
|
|
177
|
+
backgroundColor={titleBackground}
|
|
178
|
+
paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}
|
|
179
|
+
paddingRight={showWindowControls ? 0 : 1}
|
|
180
|
+
style={{ position: "relative" }}
|
|
176
181
|
>
|
|
177
182
|
<Box flexGrow={1} minWidth={0} overflow="hidden">
|
|
178
183
|
<Text fg={paneTitleText(focused, true)} selectable={false} data-gloom-role="pane-title">{title}</Text>
|
|
@@ -189,6 +194,8 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
189
194
|
{" ... "}
|
|
190
195
|
</Text>
|
|
191
196
|
)}
|
|
197
|
+
{showWindowControls ? <Box flexShrink={0} width={`${WINDOWS_CONTROL_GROUP_WIDTH_PX}px`} /> : null}
|
|
198
|
+
{showWindowControls ? <WindowControls windowKind="detached" /> : null}
|
|
192
199
|
</Box>
|
|
193
200
|
</Box>
|
|
194
201
|
<PaneBodyFrame layoutProps={bodyFrame.layoutProps} backgroundColor={background}>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Box, SpinnerMark, Text, TextAttributes, useUiCapabilities } from "../../ui";
|
|
2
|
-
import { useEffect, type ReactNode } from "react";
|
|
1
|
+
import { Box, SpinnerMark, Text, TextAttributes, useRendererHost, useUiCapabilities } from "../../ui";
|
|
2
|
+
import { useCallback, useEffect, type ReactNode } from "react";
|
|
3
3
|
import { blendHex, colors, priceColor } from "../../theme/colors";
|
|
4
4
|
import { useThemeColors } from "../../theme/theme-context";
|
|
5
5
|
import { useAppActive } from "../../state/app/activity";
|
|
@@ -17,7 +17,8 @@ import { formatPercentRaw } from "../../utils/format";
|
|
|
17
17
|
import { formatMarketPrice } from "../../market-data/market/format";
|
|
18
18
|
import { marketStateLabel, marketStateColor, getActiveQuoteDisplay } from "../../market-data/market/status";
|
|
19
19
|
import { VERSION } from "../../version";
|
|
20
|
-
import {
|
|
20
|
+
import { getTitlebarLeadingInset } from "./titlebar-overlay";
|
|
21
|
+
import { WindowControls, WINDOWS_CONTROL_GROUP_WIDTH_PX } from "./window-controls";
|
|
21
22
|
|
|
22
23
|
const SPY_REFRESH_MS = 5 * 60_000; // 5 min
|
|
23
24
|
const UPDATE_NOTICE_DURATION_MS = 5_000;
|
|
@@ -123,9 +124,12 @@ function UpdateStatus() {
|
|
|
123
124
|
|
|
124
125
|
export function Header() {
|
|
125
126
|
useThemeColors();
|
|
127
|
+
const rendererHost = useRendererHost();
|
|
126
128
|
const baseCurrency = useAppSelector(selectBaseCurrency);
|
|
127
129
|
const appActive = useAppActive();
|
|
128
|
-
const { titleBarOverlay } = useUiCapabilities();
|
|
130
|
+
const { titleBarOverlay, windowControls } = useUiCapabilities();
|
|
131
|
+
const showWindowControls = windowControls === "windows";
|
|
132
|
+
const titlebarLeadingInset = titleBarOverlay ? getTitlebarLeadingInset() : 0;
|
|
129
133
|
const spyQuoteEntry = useQuoteEntry("SPY", null);
|
|
130
134
|
const spyQuote = useResolvedEntryValue(spyQuoteEntry);
|
|
131
135
|
|
|
@@ -147,6 +151,11 @@ export function Header() {
|
|
|
147
151
|
? `SPY ${formatMarketPrice(activeSpyQuote.price, { assetCategory: "ETF" })} ${formatPercentRaw(activeSpyQuote.changePercent)}`
|
|
148
152
|
: "SPY —";
|
|
149
153
|
|
|
154
|
+
const startWindowDrag = useCallback(() => {
|
|
155
|
+
if (!titleBarOverlay) return;
|
|
156
|
+
void rendererHost.startWindowDrag?.();
|
|
157
|
+
}, [rendererHost, titleBarOverlay]);
|
|
158
|
+
|
|
150
159
|
// Market status
|
|
151
160
|
const mktState = spyQuote?.marketState;
|
|
152
161
|
const mktLabel = mktState ? marketStateLabel(mktState) : "";
|
|
@@ -161,14 +170,15 @@ export function Header() {
|
|
|
161
170
|
backgroundColor={colors.header}
|
|
162
171
|
data-gloom-role="app-header"
|
|
163
172
|
data-titlebar-overlay="true"
|
|
164
|
-
|
|
173
|
+
onMouseDown={startWindowDrag}
|
|
165
174
|
style={{
|
|
166
175
|
boxShadow: `0 -1px 0 ${colors.header}, inset 0 1px 0 ${colors.header}`,
|
|
167
176
|
paddingLeft: 8,
|
|
168
|
-
paddingRight: 12,
|
|
177
|
+
paddingRight: showWindowControls ? 0 : 12,
|
|
178
|
+
position: "relative",
|
|
169
179
|
}}
|
|
170
180
|
>
|
|
171
|
-
<Box paddingLeft={
|
|
181
|
+
<Box paddingLeft={titlebarLeadingInset} flexDirection="row" alignItems="center" gap={1}>
|
|
172
182
|
<Text attributes={TextAttributes.BOLD} fg={colors.headerText}>
|
|
173
183
|
Gloomberb
|
|
174
184
|
</Text>
|
|
@@ -195,7 +205,11 @@ export function Header() {
|
|
|
195
205
|
<Box paddingRight={1}>
|
|
196
206
|
<Text fg={spyColor}>{spyText}</Text>
|
|
197
207
|
</Box>
|
|
198
|
-
<
|
|
208
|
+
<Box paddingRight={showWindowControls ? 1 : 0}>
|
|
209
|
+
<Text fg={colors.headerText}>{baseCurrency}</Text>
|
|
210
|
+
</Box>
|
|
211
|
+
{showWindowControls ? <Box flexShrink={0} width={`${WINDOWS_CONTROL_GROUP_WIDTH_PX}px`} /> : null}
|
|
212
|
+
{showWindowControls ? <WindowControls /> : null}
|
|
199
213
|
</Box>
|
|
200
214
|
);
|
|
201
215
|
}
|
|
@@ -207,9 +221,9 @@ export function Header() {
|
|
|
207
221
|
backgroundColor={colors.header}
|
|
208
222
|
data-gloom-role="app-header"
|
|
209
223
|
data-titlebar-overlay={titleBarOverlay ? "true" : undefined}
|
|
210
|
-
|
|
224
|
+
onMouseDown={startWindowDrag}
|
|
211
225
|
>
|
|
212
|
-
<Box paddingLeft={titleBarOverlay ?
|
|
226
|
+
<Box paddingLeft={titleBarOverlay ? titlebarLeadingInset : 1}>
|
|
213
227
|
<Text attributes={TextAttributes.BOLD} fg={colors.headerText}>
|
|
214
228
|
Gloomberb v{VERSION}
|
|
215
229
|
</Text>
|
|
@@ -230,6 +244,7 @@ export function Header() {
|
|
|
230
244
|
{baseCurrency}
|
|
231
245
|
</Text>
|
|
232
246
|
</Box>
|
|
247
|
+
{showWindowControls ? <WindowControls /> : null}
|
|
233
248
|
</Box>
|
|
234
249
|
);
|
|
235
250
|
}
|
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
export const TITLEBAR_TRAFFIC_LIGHT_WIDTH = 8;
|
|
2
2
|
export const TITLEBAR_OVERLAY_HEIGHT_PX = 28;
|
|
3
|
+
|
|
4
|
+
function currentPlatform(): string {
|
|
5
|
+
const globalWithNavigator = globalThis as typeof globalThis & {
|
|
6
|
+
navigator?: {
|
|
7
|
+
platform?: string;
|
|
8
|
+
userAgentData?: { platform?: string };
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
return globalWithNavigator.navigator?.userAgentData?.platform
|
|
12
|
+
?? globalWithNavigator.navigator?.platform
|
|
13
|
+
?? "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getTitlebarLeadingInset(platform = currentPlatform()): number {
|
|
17
|
+
return /mac/i.test(platform) ? TITLEBAR_TRAFFIC_LIGHT_WIDTH : 0;
|
|
18
|
+
}
|