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
|
@@ -6,6 +6,7 @@ import { act, createElement, useReducer } from "react";
|
|
|
6
6
|
import { AppContext, appReducer, createInitialState } from "../../state/app-context";
|
|
7
7
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
8
8
|
import { setSharedMarketDataForTests } from "../../plugins/registry";
|
|
9
|
+
import { useShortcut } from "../../react/input";
|
|
9
10
|
import { cloneLayout, createDefaultConfig } from "../../types/config";
|
|
10
11
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
11
12
|
import { setSharedRegistryForTests } from "../../plugins/registry";
|
|
@@ -16,6 +17,7 @@ import type { PaneProps } from "../../types/plugin";
|
|
|
16
17
|
import { StockChart } from "../chart/stock-chart";
|
|
17
18
|
import { StatusBar } from "./status-bar";
|
|
18
19
|
import { Header } from "./header";
|
|
20
|
+
import { DetachedPaneShell } from "./detached-pane-shell";
|
|
19
21
|
import {
|
|
20
22
|
buildNativeWindowState,
|
|
21
23
|
constrainFloatingRectToBounds,
|
|
@@ -211,6 +213,31 @@ function createChartShellDataProvider(historyBySymbol: Record<string, PricePoint
|
|
|
211
213
|
};
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
async function emitKeypress(event: { name?: string; sequence?: string }) {
|
|
217
|
+
await act(async () => {
|
|
218
|
+
const keyEvent = {
|
|
219
|
+
ctrl: false,
|
|
220
|
+
meta: false,
|
|
221
|
+
option: false,
|
|
222
|
+
shift: false,
|
|
223
|
+
eventType: "press",
|
|
224
|
+
repeated: false,
|
|
225
|
+
defaultPrevented: false,
|
|
226
|
+
propagationStopped: false,
|
|
227
|
+
preventDefault() {
|
|
228
|
+
this.defaultPrevented = true;
|
|
229
|
+
},
|
|
230
|
+
stopPropagation() {
|
|
231
|
+
this.propagationStopped = true;
|
|
232
|
+
},
|
|
233
|
+
...event,
|
|
234
|
+
};
|
|
235
|
+
testSetup!.renderer.keyInput.emit("keypress", keyEvent as any);
|
|
236
|
+
await testSetup!.renderOnce();
|
|
237
|
+
await testSetup!.renderOnce();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
214
241
|
function HeaderHarness({
|
|
215
242
|
updateAvailable = null,
|
|
216
243
|
updateProgress = null,
|
|
@@ -935,6 +962,177 @@ describe("Shell", () => {
|
|
|
935
962
|
expect(updateLayout?.layout.dockRoot).toBeNull();
|
|
936
963
|
});
|
|
937
964
|
|
|
965
|
+
test("closes the focused pane after double Escape", async () => {
|
|
966
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
967
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
968
|
+
if (!mainPane) throw new Error("missing default portfolio pane");
|
|
969
|
+
|
|
970
|
+
const singlePaneLayout = {
|
|
971
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
972
|
+
instances: [{ ...mainPane }],
|
|
973
|
+
floating: [],
|
|
974
|
+
};
|
|
975
|
+
const state = {
|
|
976
|
+
...createInitialState({
|
|
977
|
+
...config,
|
|
978
|
+
layout: cloneLayout(singlePaneLayout),
|
|
979
|
+
layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
|
|
980
|
+
}),
|
|
981
|
+
focusedPaneId: "portfolio-list:main",
|
|
982
|
+
};
|
|
983
|
+
const actions: Array<any> = [];
|
|
984
|
+
|
|
985
|
+
testSetup = await testRender(
|
|
986
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
987
|
+
<TestDialogProvider>
|
|
988
|
+
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
989
|
+
</TestDialogProvider>
|
|
990
|
+
</AppContext>,
|
|
991
|
+
{ width: 40, height: 10 },
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
await testSetup.renderOnce();
|
|
995
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
996
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
997
|
+
|
|
998
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
999
|
+
|
|
1000
|
+
const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
1001
|
+
expect(actions).toContainEqual({ type: "PUSH_LAYOUT_HISTORY" });
|
|
1002
|
+
expect(updateLayout?.layout.instances).toEqual([]);
|
|
1003
|
+
expect(updateLayout?.layout.floating).toEqual([]);
|
|
1004
|
+
expect(updateLayout?.layout.dockRoot).toBeNull();
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test("closes on double Escape even when the focused pane handles the first Escape", async () => {
|
|
1008
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
1009
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
1010
|
+
if (!mainPane) throw new Error("missing default portfolio pane");
|
|
1011
|
+
|
|
1012
|
+
let consumedEscapes = 0;
|
|
1013
|
+
function EscapeConsumerPane() {
|
|
1014
|
+
useShortcut((event) => {
|
|
1015
|
+
if (event.name !== "escape") return;
|
|
1016
|
+
consumedEscapes += 1;
|
|
1017
|
+
event.preventDefault();
|
|
1018
|
+
event.stopPropagation();
|
|
1019
|
+
});
|
|
1020
|
+
return <text>Escape Consumer</text>;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const singlePaneLayout = {
|
|
1024
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
1025
|
+
instances: [{ ...mainPane }],
|
|
1026
|
+
floating: [],
|
|
1027
|
+
};
|
|
1028
|
+
const state = {
|
|
1029
|
+
...createInitialState({
|
|
1030
|
+
...config,
|
|
1031
|
+
layout: cloneLayout(singlePaneLayout),
|
|
1032
|
+
layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
|
|
1033
|
+
}),
|
|
1034
|
+
focusedPaneId: "portfolio-list:main",
|
|
1035
|
+
};
|
|
1036
|
+
const actions: Array<any> = [];
|
|
1037
|
+
|
|
1038
|
+
testSetup = await testRender(
|
|
1039
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
1040
|
+
<TestDialogProvider>
|
|
1041
|
+
<Shell
|
|
1042
|
+
pluginRegistry={createShellPluginRegistry({
|
|
1043
|
+
portfolioListComponent: EscapeConsumerPane,
|
|
1044
|
+
})}
|
|
1045
|
+
/>
|
|
1046
|
+
</TestDialogProvider>
|
|
1047
|
+
</AppContext>,
|
|
1048
|
+
{ width: 40, height: 10 },
|
|
1049
|
+
);
|
|
1050
|
+
|
|
1051
|
+
await testSetup.renderOnce();
|
|
1052
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1053
|
+
expect(consumedEscapes).toBe(1);
|
|
1054
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
1055
|
+
|
|
1056
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1057
|
+
const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
1058
|
+
expect(consumedEscapes).toBe(1);
|
|
1059
|
+
expect(updateLayout?.layout.dockRoot).toBeNull();
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test("closes on double Escape while pane input is captured", async () => {
|
|
1063
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
1064
|
+
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
1065
|
+
if (!mainPane) throw new Error("missing default portfolio pane");
|
|
1066
|
+
|
|
1067
|
+
const singlePaneLayout = {
|
|
1068
|
+
dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" },
|
|
1069
|
+
instances: [{ ...mainPane }],
|
|
1070
|
+
floating: [],
|
|
1071
|
+
};
|
|
1072
|
+
const state = {
|
|
1073
|
+
...createInitialState({
|
|
1074
|
+
...config,
|
|
1075
|
+
layout: cloneLayout(singlePaneLayout),
|
|
1076
|
+
layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }],
|
|
1077
|
+
}),
|
|
1078
|
+
focusedPaneId: "portfolio-list:main",
|
|
1079
|
+
inputCaptured: true,
|
|
1080
|
+
};
|
|
1081
|
+
const actions: Array<any> = [];
|
|
1082
|
+
|
|
1083
|
+
testSetup = await testRender(
|
|
1084
|
+
<AppContext value={{ state, dispatch: (action) => actions.push(action) }}>
|
|
1085
|
+
<TestDialogProvider>
|
|
1086
|
+
<Shell pluginRegistry={createShellPluginRegistry()} />
|
|
1087
|
+
</TestDialogProvider>
|
|
1088
|
+
</AppContext>,
|
|
1089
|
+
{ width: 40, height: 10 },
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
await testSetup.renderOnce();
|
|
1093
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1094
|
+
expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false);
|
|
1095
|
+
|
|
1096
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1097
|
+
const updateLayout = actions.find((action) => action.type === "UPDATE_LAYOUT");
|
|
1098
|
+
expect(updateLayout?.layout.dockRoot).toBeNull();
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("closes a detached pane window after double Escape", async () => {
|
|
1102
|
+
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
1103
|
+
const state = {
|
|
1104
|
+
...createInitialState(config),
|
|
1105
|
+
focusedPaneId: "portfolio-list:main",
|
|
1106
|
+
};
|
|
1107
|
+
const closedPaneIds: string[] = [];
|
|
1108
|
+
|
|
1109
|
+
testSetup = await testRender(
|
|
1110
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
1111
|
+
<TestDialogProvider>
|
|
1112
|
+
<DetachedPaneShell
|
|
1113
|
+
pluginRegistry={createShellPluginRegistry()}
|
|
1114
|
+
desktopWindowBridge={{
|
|
1115
|
+
kind: "detached",
|
|
1116
|
+
paneId: "portfolio-list:main",
|
|
1117
|
+
subscribeState: () => () => {},
|
|
1118
|
+
closeDetachedPane: async (paneId) => {
|
|
1119
|
+
closedPaneIds.push(paneId);
|
|
1120
|
+
},
|
|
1121
|
+
}}
|
|
1122
|
+
/>
|
|
1123
|
+
</TestDialogProvider>
|
|
1124
|
+
</AppContext>,
|
|
1125
|
+
{ width: 40, height: 10 },
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
await testSetup.renderOnce();
|
|
1129
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1130
|
+
expect(closedPaneIds).toEqual([]);
|
|
1131
|
+
|
|
1132
|
+
await emitKeypress({ name: "escape", sequence: "\u001b" });
|
|
1133
|
+
expect(closedPaneIds).toEqual(["portfolio-list:main"]);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
938
1136
|
test("closes the focused floating pane with Ctrl+W", async () => {
|
|
939
1137
|
const config = createDefaultConfig("/tmp/gloomberb-shell-test");
|
|
940
1138
|
const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main");
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
selectStatusBarVisible,
|
|
44
44
|
} from "../../state/selectors-ui";
|
|
45
45
|
import { colors } from "../../theme/colors";
|
|
46
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
46
47
|
import { PANE_HEADER_ACTION, PANE_HEADER_CLOSE } from "./pane-header";
|
|
47
48
|
import { getNativeSurfaceManager, type NativeOccluder, type NativePaneLayer } from "../chart/native/surface-manager";
|
|
48
49
|
import { FloatingPaneWrapper } from "./floating-pane";
|
|
@@ -58,6 +59,11 @@ import {
|
|
|
58
59
|
getShortcutDisplayMode,
|
|
59
60
|
type ShortcutDisplayMode,
|
|
60
61
|
} from "../../utils/shortcut-labels";
|
|
62
|
+
import {
|
|
63
|
+
createDoubleEscapeCloseState,
|
|
64
|
+
recordDoubleEscapeClose,
|
|
65
|
+
resetDoubleEscapeClose,
|
|
66
|
+
} from "../../utils/double-escape-close";
|
|
61
67
|
|
|
62
68
|
interface ShellProps {
|
|
63
69
|
pluginRegistry: PluginRegistry;
|
|
@@ -777,6 +783,7 @@ function resolveExternalDockPreview(
|
|
|
777
783
|
}
|
|
778
784
|
|
|
779
785
|
export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview }: ShellProps) {
|
|
786
|
+
useThemeColors();
|
|
780
787
|
const dispatch = useAppDispatch();
|
|
781
788
|
const config = useAppSelector((state) => state.config);
|
|
782
789
|
const paneState = useAppSelector((state) => state.paneState);
|
|
@@ -808,6 +815,7 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
808
815
|
const overlayOpen = commandBarOpen || dialogOpen || !!menuState;
|
|
809
816
|
|
|
810
817
|
const dragRef = useRef<DragMode | null>(null);
|
|
818
|
+
const doubleEscapeCloseRef = useRef(createDoubleEscapeCloseState());
|
|
811
819
|
const [dragFloatingRect, setDragFloatingRect] = useState<{ paneId: string; rect: FloatingRect } | null>(null);
|
|
812
820
|
const [dragCursor, setDragCursor] = useState<{ x: number; y: number } | null>(null);
|
|
813
821
|
const [dividerPreview, setDividerPreview] = useState<DividerPreviewState | null>(null);
|
|
@@ -964,13 +972,36 @@ export function Shell({ pluginRegistry, desktopWindowBridge, desktopDockPreview
|
|
|
964
972
|
return true;
|
|
965
973
|
}, [contentHeight, persistLayout, visibleLayout, width]);
|
|
966
974
|
|
|
975
|
+
useEffect(() => {
|
|
976
|
+
if (overlayOpen) {
|
|
977
|
+
resetDoubleEscapeClose(doubleEscapeCloseRef.current);
|
|
978
|
+
}
|
|
979
|
+
}, [overlayOpen]);
|
|
980
|
+
|
|
967
981
|
useShortcut((event) => {
|
|
968
|
-
|
|
982
|
+
const isEscape = event.name === "escape" || event.name === "esc";
|
|
983
|
+
if (isEscape) {
|
|
984
|
+
const doubleEscapeState = doubleEscapeCloseRef.current;
|
|
985
|
+
if (!dragRef.current && !overlayOpen) {
|
|
986
|
+
if (recordDoubleEscapeClose(doubleEscapeState, focusedPaneId, Date.now()) && closeFocusedPane()) {
|
|
987
|
+
event.preventDefault();
|
|
988
|
+
event.stopPropagation();
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
} else {
|
|
992
|
+
resetDoubleEscapeClose(doubleEscapeState);
|
|
993
|
+
}
|
|
994
|
+
|
|
969
995
|
if (!dragRef.current) return;
|
|
970
996
|
cancelActiveDrag();
|
|
971
|
-
|
|
997
|
+
event.preventDefault();
|
|
998
|
+
event.stopPropagation();
|
|
999
|
+
} else {
|
|
1000
|
+
resetDoubleEscapeClose(doubleEscapeCloseRef.current);
|
|
972
1001
|
}
|
|
1002
|
+
}, { phase: "before" });
|
|
973
1003
|
|
|
1004
|
+
useShortcut((event) => {
|
|
974
1005
|
const shortcut = resolvePaneManagementShortcut(event);
|
|
975
1006
|
if (!shortcut || dragRef.current || overlayOpen) return;
|
|
976
1007
|
if (inputCaptured && !inputCaptureAllowsPaneManagementShortcut(shortcut, event)) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box, Span, Text, TextAttributes, contextMenuDivider, useContextMenu, useUiCapabilities } from "../../ui";
|
|
2
2
|
import { useCallback, useEffect, useState } from "react";
|
|
3
3
|
import { colors, hoverBg } from "../../theme/colors";
|
|
4
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
4
5
|
import { useAppDispatch, useAppSelector, useFocusedTicker } from "../../state/app-context";
|
|
5
6
|
import {
|
|
6
7
|
selectActiveLayoutIndex,
|
|
@@ -57,6 +58,7 @@ function truncate(text: string, width: number): string {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export function StatusBar() {
|
|
61
|
+
useThemeColors();
|
|
60
62
|
const { nativePaneChrome, nativeContextMenu } = useUiCapabilities();
|
|
61
63
|
const { showContextMenu } = useContextMenu();
|
|
62
64
|
const registry = getSharedRegistry();
|
|
@@ -2,6 +2,7 @@ import { createElement, useMemo, type SVGProps } from "react";
|
|
|
2
2
|
import { Box, ChartSurface, Span, Text, TextAttributes, useNativeRenderer, useUiCapabilities, useUiHost } from "../ui";
|
|
3
3
|
import { computeBitmapSize, type NativeChartBitmap } from "./chart/native/chart-rasterizer";
|
|
4
4
|
import { colors } from "../theme/colors";
|
|
5
|
+
import { useThemeColors } from "../theme/theme-context";
|
|
5
6
|
|
|
6
7
|
export interface SpeedometerSegment {
|
|
7
8
|
from: number;
|
|
@@ -20,6 +21,7 @@ export interface SpeedometerGaugeProps {
|
|
|
20
21
|
currentLabel?: string;
|
|
21
22
|
minWidth?: number;
|
|
22
23
|
maxWidth?: number;
|
|
24
|
+
compact?: boolean;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
interface GaugeCell {
|
|
@@ -43,23 +45,17 @@ const DEFAULT_MIN_WIDTH = 34;
|
|
|
43
45
|
const DEFAULT_MAX_WIDTH = 50;
|
|
44
46
|
const DESKTOP_VIEWBOX_WIDTH = 520;
|
|
45
47
|
const DESKTOP_VIEWBOX_HEIGHT = 232;
|
|
48
|
+
const DESKTOP_COMPACT_VIEWBOX_HEIGHT = 214;
|
|
46
49
|
const DESKTOP_CENTER_X = 260;
|
|
47
50
|
const DESKTOP_CENTER_Y = 182;
|
|
48
51
|
const DESKTOP_ARC_RADIUS = 138;
|
|
49
52
|
const DESKTOP_NEEDLE_RADIUS = 104;
|
|
50
53
|
const DESKTOP_LABEL_POSITIONS = [
|
|
51
|
-
{ x:
|
|
52
|
-
{ x:
|
|
53
|
-
{ x: 260, y:
|
|
54
|
-
{ x:
|
|
55
|
-
{ x:
|
|
56
|
-
];
|
|
57
|
-
const DESKTOP_TICK_LABEL_POSITIONS = [
|
|
58
|
-
{ x: 78, y: 224 },
|
|
59
|
-
{ x: 170, y: 224 },
|
|
60
|
-
{ x: 260, y: 224 },
|
|
61
|
-
{ x: 350, y: 224 },
|
|
62
|
-
{ x: 442, y: 224 },
|
|
54
|
+
{ x: 76, y: 84 },
|
|
55
|
+
{ x: 154, y: 44 },
|
|
56
|
+
{ x: 260, y: 18 },
|
|
57
|
+
{ x: 366, y: 44 },
|
|
58
|
+
{ x: 444, y: 84 },
|
|
63
59
|
];
|
|
64
60
|
|
|
65
61
|
function SvgText(props: SVGProps<SVGTextElement>) {
|
|
@@ -358,21 +354,24 @@ function DesktopGauge({
|
|
|
358
354
|
currentLabel,
|
|
359
355
|
minWidth,
|
|
360
356
|
maxWidth,
|
|
357
|
+
compact,
|
|
361
358
|
}: Required<SpeedometerGaugeProps>) {
|
|
362
359
|
const gaugeWidth = Math.min(Math.max(width - 2, minWidth), maxWidth);
|
|
360
|
+
const gaugeHeight = compact ? 9 : 12;
|
|
361
|
+
const viewBoxHeight = compact ? DESKTOP_COMPACT_VIEWBOX_HEIGHT : DESKTOP_VIEWBOX_HEIGHT;
|
|
363
362
|
const needleAngle = valueToDegrees(value, min, max);
|
|
364
363
|
const needleEnd = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_NEEDLE_RADIUS, needleAngle);
|
|
365
364
|
|
|
366
365
|
return (
|
|
367
366
|
<Box
|
|
368
367
|
width={gaugeWidth}
|
|
369
|
-
height={
|
|
370
|
-
marginTop={1}
|
|
368
|
+
height={gaugeHeight}
|
|
369
|
+
marginTop={compact ? 0 : 1}
|
|
371
370
|
overflow="hidden"
|
|
372
371
|
style={{ alignSelf: "center", maxWidth: 420 }}
|
|
373
372
|
>
|
|
374
373
|
<svg
|
|
375
|
-
viewBox={`0 0 ${DESKTOP_VIEWBOX_WIDTH} ${
|
|
374
|
+
viewBox={`0 0 ${DESKTOP_VIEWBOX_WIDTH} ${viewBoxHeight}`}
|
|
376
375
|
width="100%"
|
|
377
376
|
height="100%"
|
|
378
377
|
role="img"
|
|
@@ -418,19 +417,12 @@ function DesktopGauge({
|
|
|
418
417
|
</SvgText>
|
|
419
418
|
);
|
|
420
419
|
})}
|
|
421
|
-
{[min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max].map((tick
|
|
420
|
+
{[min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max].map((tick) => {
|
|
422
421
|
const angle = valueToDegrees(tick, min, max);
|
|
423
422
|
const outer = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS + 16, angle);
|
|
424
423
|
const inner = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS - 10, angle);
|
|
425
|
-
const label = DESKTOP_TICK_LABEL_POSITIONS[index]
|
|
426
|
-
?? polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS + 34, angle);
|
|
427
424
|
return (
|
|
428
|
-
<
|
|
429
|
-
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke={colors.textDim} strokeWidth="2" />
|
|
430
|
-
<SvgText x={label.x} y={label.y} fill={colors.textDim} textAnchor="middle" fontFamily="inherit" fontSize="13">
|
|
431
|
-
{formatGaugeValue(tick)}
|
|
432
|
-
</SvgText>
|
|
433
|
-
</g>
|
|
425
|
+
<line key={tick} x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke={colors.textDim} strokeWidth="2" />
|
|
434
426
|
);
|
|
435
427
|
})}
|
|
436
428
|
<line
|
|
@@ -443,7 +435,16 @@ function DesktopGauge({
|
|
|
443
435
|
strokeLinecap="round"
|
|
444
436
|
/>
|
|
445
437
|
<circle cx={DESKTOP_CENTER_X} cy={DESKTOP_CENTER_Y} r="27" fill={colors.bg} />
|
|
446
|
-
<SvgText
|
|
438
|
+
<SvgText
|
|
439
|
+
x={DESKTOP_CENTER_X}
|
|
440
|
+
y={DESKTOP_CENTER_Y + 2}
|
|
441
|
+
fill={colors.textBright}
|
|
442
|
+
textAnchor="middle"
|
|
443
|
+
dominantBaseline="central"
|
|
444
|
+
fontFamily="inherit"
|
|
445
|
+
fontSize="31"
|
|
446
|
+
fontWeight="800"
|
|
447
|
+
>
|
|
447
448
|
{formatGaugeValue(value)}
|
|
448
449
|
</SvgText>
|
|
449
450
|
</svg>
|
|
@@ -540,7 +541,9 @@ export function SpeedometerGauge({
|
|
|
540
541
|
currentLabel = "Current reading",
|
|
541
542
|
minWidth = DEFAULT_MIN_WIDTH,
|
|
542
543
|
maxWidth = DEFAULT_MAX_WIDTH,
|
|
544
|
+
compact = false,
|
|
543
545
|
}: SpeedometerGaugeProps) {
|
|
546
|
+
useThemeColors();
|
|
544
547
|
const props = {
|
|
545
548
|
value,
|
|
546
549
|
valueLabel,
|
|
@@ -551,6 +554,7 @@ export function SpeedometerGauge({
|
|
|
551
554
|
currentLabel,
|
|
552
555
|
minWidth,
|
|
553
556
|
maxWidth,
|
|
557
|
+
compact,
|
|
554
558
|
};
|
|
555
559
|
return useUiHost().kind === "desktop-web"
|
|
556
560
|
? <DesktopGauge {...props} />
|
|
@@ -129,6 +129,60 @@ export function useTableBodyScrollActivity({
|
|
|
129
129
|
}, [afterScroll, onBodyScrollActivity, syncHeaderScroll]);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
export function useScrollBoxScrollActivity({
|
|
133
|
+
scrollRef,
|
|
134
|
+
onVerticalScroll,
|
|
135
|
+
onHorizontalScroll,
|
|
136
|
+
}: {
|
|
137
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
138
|
+
onVerticalScroll?: () => void;
|
|
139
|
+
onHorizontalScroll?: () => void;
|
|
140
|
+
}) {
|
|
141
|
+
const onVerticalScrollRef = useRef(onVerticalScroll);
|
|
142
|
+
const onHorizontalScrollRef = useRef(onHorizontalScroll);
|
|
143
|
+
const verticalScrollScheduledRef = useRef(false);
|
|
144
|
+
const horizontalScrollScheduledRef = useRef(false);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
onVerticalScrollRef.current = onVerticalScroll;
|
|
148
|
+
onHorizontalScrollRef.current = onHorizontalScroll;
|
|
149
|
+
}, [onHorizontalScroll, onVerticalScroll]);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
const scrollBox = scrollRef.current;
|
|
153
|
+
if (!scrollBox) return;
|
|
154
|
+
let active = true;
|
|
155
|
+
|
|
156
|
+
const handleVerticalChange = () => {
|
|
157
|
+
if (verticalScrollScheduledRef.current) return;
|
|
158
|
+
verticalScrollScheduledRef.current = true;
|
|
159
|
+
queueMicrotask(() => {
|
|
160
|
+
verticalScrollScheduledRef.current = false;
|
|
161
|
+
if (!active) return;
|
|
162
|
+
onVerticalScrollRef.current?.();
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
const handleHorizontalChange = () => {
|
|
166
|
+
if (horizontalScrollScheduledRef.current) return;
|
|
167
|
+
horizontalScrollScheduledRef.current = true;
|
|
168
|
+
queueMicrotask(() => {
|
|
169
|
+
horizontalScrollScheduledRef.current = false;
|
|
170
|
+
if (!active) return;
|
|
171
|
+
onHorizontalScrollRef.current?.();
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
scrollBox.verticalScrollBar.on("change", handleVerticalChange);
|
|
176
|
+
scrollBox.horizontalScrollBar.on("change", handleHorizontalChange);
|
|
177
|
+
|
|
178
|
+
return () => {
|
|
179
|
+
active = false;
|
|
180
|
+
scrollBox.verticalScrollBar.off("change", handleVerticalChange);
|
|
181
|
+
scrollBox.horizontalScrollBar.off("change", handleHorizontalChange);
|
|
182
|
+
};
|
|
183
|
+
}, [scrollRef]);
|
|
184
|
+
}
|
|
185
|
+
|
|
132
186
|
export function useResetTableScroll({
|
|
133
187
|
headerScrollRef,
|
|
134
188
|
scrollRef,
|
|
@@ -140,6 +194,12 @@ export function useResetTableScroll({
|
|
|
140
194
|
resetScrollKey?: unknown;
|
|
141
195
|
afterReset?: () => void;
|
|
142
196
|
}) {
|
|
197
|
+
const afterResetRef = useRef(afterReset);
|
|
198
|
+
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
afterResetRef.current = afterReset;
|
|
201
|
+
}, [afterReset]);
|
|
202
|
+
|
|
143
203
|
useEffect(() => {
|
|
144
204
|
if (resetScrollKey === undefined) return;
|
|
145
205
|
const body = scrollRef.current;
|
|
@@ -151,8 +211,8 @@ export function useResetTableScroll({
|
|
|
151
211
|
if (header) {
|
|
152
212
|
header.scrollLeft = 0;
|
|
153
213
|
}
|
|
154
|
-
if (
|
|
155
|
-
queueMicrotask(
|
|
214
|
+
if (afterResetRef.current) {
|
|
215
|
+
queueMicrotask(afterResetRef.current);
|
|
156
216
|
}
|
|
157
|
-
}, [
|
|
217
|
+
}, [headerScrollRef, resetScrollKey, scrollRef]);
|
|
158
218
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
useCallback,
|
|
3
3
|
useEffect,
|
|
4
|
+
useRef,
|
|
4
5
|
type ReactNode,
|
|
5
6
|
type RefObject,
|
|
6
7
|
} from "react";
|
|
@@ -93,6 +94,7 @@ export function TickerListTableView({
|
|
|
93
94
|
const selectedIndex = tableProps.tickers.findIndex(
|
|
94
95
|
(ticker) => ticker.metadata.ticker === tableProps.cursorSymbol,
|
|
95
96
|
);
|
|
97
|
+
const lastScrollCursorSymbolRef = useRef<string | null | undefined>(undefined);
|
|
96
98
|
|
|
97
99
|
const emitVisibleRange = useCallback(() => {
|
|
98
100
|
if (!onVisibleRangeChange) return;
|
|
@@ -185,7 +187,19 @@ export function TickerListTableView({
|
|
|
185
187
|
|
|
186
188
|
useEffect(() => {
|
|
187
189
|
const scrollBox = effectiveScrollRef.current;
|
|
188
|
-
if (!scrollBox?.viewport || selectedIndex < 0)
|
|
190
|
+
if (!scrollBox?.viewport || selectedIndex < 0) {
|
|
191
|
+
queueMicrotask(emitVisibleRange);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const shouldScrollToCursor =
|
|
196
|
+
lastScrollCursorSymbolRef.current !== tableProps.cursorSymbol;
|
|
197
|
+
lastScrollCursorSymbolRef.current = tableProps.cursorSymbol;
|
|
198
|
+
|
|
199
|
+
if (!shouldScrollToCursor) {
|
|
200
|
+
queueMicrotask(emitVisibleRange);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
189
203
|
|
|
190
204
|
const viewportHeight = Math.max(scrollBox.viewport.height, 1);
|
|
191
205
|
if (selectedIndex < scrollBox.scrollTop) {
|
|
@@ -194,7 +208,7 @@ export function TickerListTableView({
|
|
|
194
208
|
scrollBox.scrollTo(selectedIndex - viewportHeight + 1);
|
|
195
209
|
}
|
|
196
210
|
queueMicrotask(emitVisibleRange);
|
|
197
|
-
}, [effectiveScrollRef, emitVisibleRange, selectedIndex]);
|
|
211
|
+
}, [effectiveScrollRef, emitVisibleRange, selectedIndex, tableProps.cursorSymbol]);
|
|
198
212
|
|
|
199
213
|
return (
|
|
200
214
|
<TableViewFrame
|