gloomberb 0.8.2 → 0.9.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/README.md +9 -7
- package/package.json +18 -13
- package/patches/{@opentui%2Fcore@0.1.90.patch → @opentui%2Fcore@0.3.2.patch} +175 -90
- package/patches/electrobun@1.18.1.patch +85 -0
- package/src/app/global-shortcuts.test.tsx +66 -4
- package/src/app/global-shortcuts.ts +12 -6
- 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/command-bar/action-runtime.ts +1 -0
- package/src/components/command-bar/layout-items/current-layout.test.ts +115 -0
- package/src/components/command-bar/layout-items/current-layout.ts +33 -0
- package/src/components/command-bar/list/view.tsx +3 -1
- package/src/components/command-bar/pane-templates/items.ts +4 -1
- package/src/components/command-bar/panel/body.tsx +1 -0
- package/src/components/command-bar/routes/root/results.ts +0 -30
- package/src/components/command-bar/routes/root/runtime.ts +2 -20
- package/src/components/command-bar/routes/ticker-search/actions.ts +42 -4
- package/src/components/command-bar/routes/ticker-search/route.ts +4 -1
- package/src/components/command-bar/routing/launch-request.ts +14 -7
- package/src/components/command-bar/routing/list-state.ts +1 -3
- package/src/components/command-bar/surface/index.test.tsx +76 -4
- package/src/components/command-bar/surface/index.tsx +0 -2
- package/src/components/command-bar/surface/pane.test.tsx +1 -0
- package/src/components/command-bar/surface/test-harness.tsx +1 -0
- package/src/components/command-bar/view-model.ts +3 -3
- package/src/components/layout/detached-pane-shell.tsx +8 -4
- package/src/components/layout/header.tsx +13 -6
- package/src/components/layout/shell/index.test.tsx +120 -2
- package/src/components/layout/shell/index.tsx +2 -0
- package/src/components/layout/shell/menu.ts +1 -7
- package/src/components/layout/shell/pane/actions.ts +8 -0
- package/src/components/layout/shell/pane/management-shortcuts.ts +12 -2
- package/src/components/layout/shell/shortcuts.ts +14 -6
- package/src/components/layout/shell/window-mode/index.ts +8 -3
- package/src/components/layout/titlebar-overlay.ts +16 -0
- package/src/components/layout/window-controls.tsx +90 -0
- package/src/components/layout/window-edit/presentation.ts +4 -4
- package/src/components/onboarding/onboarding-steps.tsx +3 -2
- package/src/components/ticker/list-table-view.tsx +6 -0
- package/src/components/ui/remote-image.tsx +1 -14
- package/src/components/ui/tabs.tsx +95 -80
- package/src/components/ui/ui.test.tsx +54 -0
- package/src/core/state/app/types.ts +12 -4
- package/src/plugins/builtin/chat/content/channel-navigation.ts +44 -11
- package/src/plugins/builtin/chat/content/index.tsx +5 -4
- package/src/plugins/builtin/chat/content/shortcuts.ts +7 -0
- package/src/plugins/builtin/chat/sidebar.test.tsx +71 -0
- package/src/plugins/builtin/help/components.tsx +1 -1
- package/src/plugins/builtin/help/index.test.tsx +105 -30
- package/src/plugins/builtin/help/index.tsx +324 -168
- package/src/plugins/builtin/help/shortcut-model.ts +5 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +304 -1
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +93 -0
- package/src/plugins/builtin/portfolio-list/pane/data.ts +7 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +25 -2
- package/src/plugins/builtin/portfolio-list/pane/supplemental.ts +281 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +27 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +10 -68
- package/src/plugins/builtin/substack/api/auth.ts +185 -0
- package/src/plugins/builtin/substack/api/cache.ts +78 -0
- package/src/plugins/builtin/substack/api/loaders.ts +146 -0
- package/src/plugins/builtin/substack/api/store.ts +186 -0
- package/src/plugins/builtin/substack/api/types.ts +40 -0
- package/src/plugins/builtin/substack/api.test.ts +298 -0
- package/src/plugins/builtin/substack/article-detail.tsx +359 -0
- package/src/plugins/builtin/substack/article-stack.tsx +129 -0
- package/src/plugins/builtin/substack/content.ts +478 -0
- package/src/plugins/builtin/substack/feed-tabs.tsx +41 -0
- package/src/plugins/builtin/substack/index.tsx +49 -0
- package/src/plugins/builtin/substack/login-view.tsx +126 -0
- package/src/plugins/builtin/substack/model.test.ts +155 -0
- package/src/plugins/builtin/substack/normalize.ts +343 -0
- package/src/plugins/builtin/substack/pane-footer.ts +71 -0
- package/src/plugins/builtin/substack/pane-state.ts +170 -0
- package/src/plugins/builtin/substack/pane.tsx +500 -0
- package/src/plugins/builtin/substack/table.ts +111 -0
- package/src/plugins/builtin/substack/types.ts +71 -0
- package/src/plugins/builtin/substack/utils.ts +131 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +16 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +13 -1
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/pane-manager/layout-state.ts +11 -0
- package/src/plugins/pane-manager.ts +1 -1
- package/src/react/use-throttled-commit-value.ts +116 -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 +43 -2
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +31 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.test.ts +42 -0
- package/src/renderers/electrobun/bun/desktop/http-fetch.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.test.ts +53 -0
- package/src/renderers/electrobun/bun/desktop/window-controls.ts +64 -0
- package/src/renderers/electrobun/bun/desktop/window-style.ts +5 -0
- package/src/renderers/electrobun/bun/desktop/windows-icons.ts +123 -0
- package/src/renderers/electrobun/bun/index.ts +28 -6
- 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/view/build-assets.ts +2 -1
- package/src/renderers/electrobun/view/host/text.tsx +29 -9
- package/src/renderers/electrobun/view/http-fetch.ts +10 -3
- package/src/renderers/electrobun/view/styles.css +20 -0
- package/src/renderers/electrobun/view/ui-host.tsx +14 -0
- package/src/state/app/context/index.test.tsx +16 -0
- package/src/tickers/search/index.ts +1 -0
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import { useShortcut } from "../../../../react/input";
|
|
3
|
+
import type { WindowEditMode } from "../../../../plugins/registry";
|
|
3
4
|
import {
|
|
4
5
|
createDoubleEscapeCloseState,
|
|
5
6
|
recordDoubleEscapeClose,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
|
|
13
14
|
interface ShellPaneManagementShortcutOptions {
|
|
14
15
|
cancelActiveDrag(): void;
|
|
16
|
+
closeAllFloatingPanes(): boolean;
|
|
15
17
|
closeFocusedPane(): boolean;
|
|
16
18
|
copyFocusedPaneScreenshot(): boolean;
|
|
17
19
|
focusedPaneId: string | null;
|
|
@@ -22,12 +24,13 @@ interface ShellPaneManagementShortcutOptions {
|
|
|
22
24
|
openLayoutMenu(): void;
|
|
23
25
|
overlayOpen: boolean;
|
|
24
26
|
popOutFocusedPane(): boolean;
|
|
25
|
-
startWindowMode(): void;
|
|
27
|
+
startWindowMode(paneId?: string, mode?: WindowEditMode): void;
|
|
26
28
|
toggleFocusedPaneFloating(): boolean;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function useShellPaneManagementShortcuts({
|
|
30
32
|
cancelActiveDrag,
|
|
33
|
+
closeAllFloatingPanes,
|
|
31
34
|
closeFocusedPane,
|
|
32
35
|
copyFocusedPaneScreenshot,
|
|
33
36
|
focusedPaneId,
|
|
@@ -82,6 +85,9 @@ export function useShellPaneManagementShortcuts({
|
|
|
82
85
|
case "close":
|
|
83
86
|
handled = closeFocusedPane();
|
|
84
87
|
break;
|
|
88
|
+
case "close-all-floating":
|
|
89
|
+
handled = closeAllFloatingPanes();
|
|
90
|
+
break;
|
|
85
91
|
case "settings":
|
|
86
92
|
handled = openFocusedPaneSettings();
|
|
87
93
|
break;
|
|
@@ -102,7 +108,11 @@ export function useShellPaneManagementShortcuts({
|
|
|
102
108
|
handled = gridlockVisiblePanes();
|
|
103
109
|
break;
|
|
104
110
|
case "window-mode":
|
|
105
|
-
startWindowMode();
|
|
111
|
+
startWindowMode(undefined, "move");
|
|
112
|
+
handled = true;
|
|
113
|
+
break;
|
|
114
|
+
case "window-resize-mode":
|
|
115
|
+
startWindowMode(undefined, "resize");
|
|
106
116
|
handled = true;
|
|
107
117
|
break;
|
|
108
118
|
}
|
|
@@ -6,9 +6,11 @@ export const PANE_MANAGEMENT_ACCELERATORS = {
|
|
|
6
6
|
popOut: "CmdOrCtrl+Shift+O",
|
|
7
7
|
copyScreenshot: "CmdOrCtrl+Shift+C",
|
|
8
8
|
close: "CmdOrCtrl+W",
|
|
9
|
+
closeAllFloating: "CmdOrCtrl+Alt+W",
|
|
9
10
|
layoutActions: "CmdOrCtrl+Shift+L",
|
|
10
11
|
gridlockAll: "CmdOrCtrl+Shift+G",
|
|
11
12
|
windowMode: "CmdOrCtrl+Shift+M",
|
|
13
|
+
windowResizeMode: "CmdOrCtrl+Shift+R",
|
|
12
14
|
} as const;
|
|
13
15
|
|
|
14
16
|
export type PaneManagementShortcut =
|
|
@@ -17,24 +19,30 @@ export type PaneManagementShortcut =
|
|
|
17
19
|
| "pop-out"
|
|
18
20
|
| "copy-screenshot"
|
|
19
21
|
| "close"
|
|
22
|
+
| "close-all-floating"
|
|
20
23
|
| "layout-actions"
|
|
21
24
|
| "gridlock-all"
|
|
22
|
-
| "window-mode"
|
|
25
|
+
| "window-mode"
|
|
26
|
+
| "window-resize-mode";
|
|
23
27
|
|
|
24
28
|
export function resolvePaneManagementShortcut(
|
|
25
29
|
event: Pick<KeyEventLike, "name" | "key" | "ctrl" | "meta" | "super" | "shift" | "alt">,
|
|
26
30
|
): PaneManagementShortcut | null {
|
|
27
31
|
if (!event.ctrl && !event.meta && !event.super) return null;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (!
|
|
32
|
+
const rawName = event.name ?? event.key ?? "";
|
|
33
|
+
const name = rawName.toLowerCase();
|
|
34
|
+
const shifted = event.shift || rawName !== name;
|
|
35
|
+
if (!shifted && event.alt && name === "w") return "close-all-floating";
|
|
36
|
+
if (!shifted && name === "w") return "close";
|
|
37
|
+
if (!shifted && name === ",") return "settings";
|
|
38
|
+
if (!shifted || event.alt) return null;
|
|
32
39
|
if (name === "c") return "copy-screenshot";
|
|
33
40
|
if (name === "d") return "toggle-floating";
|
|
34
41
|
if (name === "o") return "pop-out";
|
|
35
42
|
if (name === "l") return "layout-actions";
|
|
36
43
|
if (name === "g") return "gridlock-all";
|
|
37
44
|
if (name === "m") return "window-mode";
|
|
45
|
+
if (name === "r") return "window-resize-mode";
|
|
38
46
|
return null;
|
|
39
47
|
}
|
|
40
48
|
|
|
@@ -42,6 +50,6 @@ export function inputCaptureAllowsPaneManagementShortcut(
|
|
|
42
50
|
shortcut: PaneManagementShortcut,
|
|
43
51
|
event: Pick<KeyEventLike, "meta" | "super" | "targetEditable">,
|
|
44
52
|
): boolean {
|
|
45
|
-
if (shortcut !== "close") return false;
|
|
53
|
+
if (shortcut !== "close" && shortcut !== "close-all-floating") return false;
|
|
46
54
|
return event.meta || event.super || event.targetEditable !== true;
|
|
47
55
|
}
|
|
@@ -128,9 +128,13 @@ export function useShellWindowMode({
|
|
|
128
128
|
if (!windowMode) return;
|
|
129
129
|
const committedLayout = resolveWindowEditCommitLayout(windowMode, bounds, dockGeometryOptions);
|
|
130
130
|
const hasPendingCommit = windowEditHasPendingCommit(windowMode, bounds, dockGeometryOptions);
|
|
131
|
-
if (hasPendingCommit) {
|
|
132
|
-
|
|
131
|
+
if (!hasPendingCommit) {
|
|
132
|
+
focusPane(windowMode.paneId);
|
|
133
|
+
setWindowMode(null);
|
|
134
|
+
return;
|
|
133
135
|
}
|
|
136
|
+
|
|
137
|
+
persistLayout(committedLayout);
|
|
134
138
|
focusPane(windowMode.paneId);
|
|
135
139
|
setWindowMode({
|
|
136
140
|
paneId: windowMode.paneId,
|
|
@@ -138,7 +142,7 @@ export function useShellWindowMode({
|
|
|
138
142
|
mode: "move",
|
|
139
143
|
focus: normalizeWindowEditFocus({ kind: "move" }, committedLayout, windowMode.paneId, "move", bounds, dockGeometryOptions),
|
|
140
144
|
dirty: false,
|
|
141
|
-
notice:
|
|
145
|
+
notice: "Committed",
|
|
142
146
|
});
|
|
143
147
|
}, [bounds, dockGeometryOptions, focusPane, persistLayout, windowMode]);
|
|
144
148
|
|
|
@@ -166,6 +170,7 @@ export function useShellWindowMode({
|
|
|
166
170
|
|
|
167
171
|
useShortcut((event) => {
|
|
168
172
|
if (!windowMode) return;
|
|
173
|
+
if (event.ctrl || event.meta || event.super || event.alt) return;
|
|
169
174
|
const name = (event.name ?? event.key ?? "").toLowerCase();
|
|
170
175
|
let handled = true;
|
|
171
176
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Box, useRendererHost } from "../../ui";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
const WINDOWS_CONTROL_WIDTH_PX = 30;
|
|
5
|
+
const WINDOWS_CONTROL_TRAILING_PADDING_PX = 10;
|
|
6
|
+
|
|
7
|
+
type WindowControlAction = "minimize" | "toggle-maximize" | "close";
|
|
8
|
+
|
|
9
|
+
const WINDOWS_CONTROLS: Array<{
|
|
10
|
+
action: WindowControlAction;
|
|
11
|
+
label: string;
|
|
12
|
+
}> = [
|
|
13
|
+
{ action: "minimize", label: "Minimize" },
|
|
14
|
+
{ action: "toggle-maximize", label: "Maximize" },
|
|
15
|
+
{ action: "close", label: "Close" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function stopMouse(event: { stopPropagation?: () => void; preventDefault?: () => void }) {
|
|
19
|
+
event.stopPropagation?.();
|
|
20
|
+
event.preventDefault?.();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function WindowControlIcon({ action }: { action: WindowControlAction }) {
|
|
24
|
+
if (action === "minimize") {
|
|
25
|
+
return (
|
|
26
|
+
<svg viewBox="0 0 12 12" width="10" height="10" fill="none" aria-hidden="true">
|
|
27
|
+
<path d="M2.5 6.5H9.5" stroke="currentColor" strokeWidth="1.25" strokeLinecap="square" />
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (action === "toggle-maximize") {
|
|
33
|
+
return (
|
|
34
|
+
<svg viewBox="0 0 12 12" width="10" height="10" fill="none" aria-hidden="true">
|
|
35
|
+
<rect x="3" y="3" width="6" height="6" stroke="currentColor" strokeWidth="1.25" />
|
|
36
|
+
</svg>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<svg viewBox="0 0 12 12" width="10" height="10" fill="none" aria-hidden="true">
|
|
42
|
+
<path d="M3 3L9 9M9 3L3 9" stroke="currentColor" strokeWidth="1.35" strokeLinecap="round" />
|
|
43
|
+
</svg>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function WindowControls() {
|
|
48
|
+
const rendererHost = useRendererHost();
|
|
49
|
+
|
|
50
|
+
const controlWindow = useCallback((action: WindowControlAction, event: { stopPropagation?: () => void; preventDefault?: () => void }) => {
|
|
51
|
+
stopMouse(event);
|
|
52
|
+
void rendererHost.controlWindow?.(action);
|
|
53
|
+
}, [rendererHost]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Box
|
|
57
|
+
flexDirection="row"
|
|
58
|
+
alignItems="stretch"
|
|
59
|
+
flexShrink={0}
|
|
60
|
+
className="electrobun-webkit-app-region-no-drag"
|
|
61
|
+
data-gloom-role="window-controls"
|
|
62
|
+
aria-hidden={false}
|
|
63
|
+
style={{
|
|
64
|
+
paddingRight: WINDOWS_CONTROL_TRAILING_PADDING_PX,
|
|
65
|
+
}}
|
|
66
|
+
>
|
|
67
|
+
{WINDOWS_CONTROLS.map((control) => (
|
|
68
|
+
<Box
|
|
69
|
+
key={control.action}
|
|
70
|
+
alignItems="center"
|
|
71
|
+
justifyContent="center"
|
|
72
|
+
data-gloom-role="window-control"
|
|
73
|
+
data-window-control-action={control.action}
|
|
74
|
+
data-gloom-interactive="true"
|
|
75
|
+
role="button"
|
|
76
|
+
aria-label={control.label}
|
|
77
|
+
title={control.label}
|
|
78
|
+
className="electrobun-webkit-app-region-no-drag"
|
|
79
|
+
onMouseDown={(event: { stopPropagation?: () => void; preventDefault?: () => void }) => controlWindow(control.action, event)}
|
|
80
|
+
style={{
|
|
81
|
+
width: WINDOWS_CONTROL_WIDTH_PX,
|
|
82
|
+
height: "100%",
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
<WindowControlIcon action={control.action} />
|
|
86
|
+
</Box>
|
|
87
|
+
))}
|
|
88
|
+
</Box>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -73,14 +73,14 @@ export function windowEditStatusLine(
|
|
|
73
73
|
export function windowEditHelpText(state: WindowEditState): string {
|
|
74
74
|
if (state.mode === "move") {
|
|
75
75
|
if (state.focus.kind === "dock-move") {
|
|
76
|
-
return "arrows/hjkl choose side m retarget Tab/w window d float r resize Enter commit Esc exit/cancel";
|
|
76
|
+
return "arrows/hjkl choose side m retarget Tab/w window d float r resize Enter commit/exit Esc exit/cancel";
|
|
77
77
|
}
|
|
78
|
-
return "Tab/w window d dock/float r resize arrows/hjkl target/move Shift fast Enter commit Esc exit/cancel";
|
|
78
|
+
return "Tab/w window d dock/float r resize arrows/hjkl target/move Shift fast Enter commit/exit Esc exit/cancel";
|
|
79
79
|
}
|
|
80
80
|
if (state.focus.kind === "floating-resize") {
|
|
81
|
-
return "Tab corner w window m move arrows/hjkl resize Shift fast Enter commit Esc exit/cancel";
|
|
81
|
+
return "Tab corner w window m move arrows/hjkl resize Shift fast Enter commit/exit Esc exit/cancel";
|
|
82
82
|
}
|
|
83
|
-
return "arrows/hjkl move divider Tab divider w window m move Shift fast Enter commit Esc exit/cancel";
|
|
83
|
+
return "arrows/hjkl move divider Tab divider w window m move Shift fast Enter commit/exit Esc exit/cancel";
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function resolveWindowEditDockMovePreview(
|
|
@@ -101,7 +101,8 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
|
|
|
101
101
|
const shortcutDisplayMode = getShortcutDisplayMode(uiHost.kind);
|
|
102
102
|
const platformShortcut = (keys: string | readonly string[]) => formatPrimaryShortcut(keys, shortcutPlatform, shortcutDisplayMode);
|
|
103
103
|
const keyboardShortcuts = [
|
|
104
|
-
{ key: "Ctrl+P
|
|
104
|
+
{ key: "Ctrl+P", desc: "Open command mode" },
|
|
105
|
+
{ key: "`", desc: "Open ticker search" },
|
|
105
106
|
{ key: "Tab", desc: "Switch between panels" },
|
|
106
107
|
{ key: platformShortcut("W"), desc: "Close the focused pane" },
|
|
107
108
|
{ key: platformShortcut(["Shift", "D"]), desc: "Dock or float the focused pane" },
|
|
@@ -155,7 +156,7 @@ export function ShortcutsStep({ pluginRegistry }: { pluginRegistry: PluginRegist
|
|
|
155
156
|
</Box>
|
|
156
157
|
<Box height={1} />
|
|
157
158
|
<Box height={1}>
|
|
158
|
-
<Text fg={colors.textDim}>{"Type these in the command bar ("}<Span fg={colors.text}><Strong>{"Ctrl+P"}</Strong></Span>{"
|
|
159
|
+
<Text fg={colors.textDim}>{"Type these in the command bar ("}<Span fg={colors.text}><Strong>{"Ctrl+P"}</Strong></Span>{"):"}</Text>
|
|
159
160
|
</Box>
|
|
160
161
|
<Box height={1} />
|
|
161
162
|
|
|
@@ -81,12 +81,18 @@ const FLASHABLE_QUOTE_COLUMN_IDS = new Set([
|
|
|
81
81
|
"bid",
|
|
82
82
|
"ask",
|
|
83
83
|
"spread",
|
|
84
|
+
"spread_pct",
|
|
84
85
|
"ext_hours",
|
|
85
86
|
"market_cap",
|
|
87
|
+
"volume",
|
|
88
|
+
"dollar_volume",
|
|
89
|
+
"range_52w",
|
|
86
90
|
"mkt_value",
|
|
91
|
+
"weight",
|
|
87
92
|
"day_pnl",
|
|
88
93
|
"pnl",
|
|
89
94
|
"pnl_pct",
|
|
95
|
+
"mark_delta",
|
|
90
96
|
]);
|
|
91
97
|
|
|
92
98
|
const EMPTY_FLASH_SYMBOLS = new Map<string, QuoteFlashDirection>();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Box, ImageSurface, Text } from "../../ui";
|
|
2
2
|
import { colors } from "../../theme/colors";
|
|
3
|
-
import { ExternalLinkText } from "./external-link";
|
|
4
3
|
import { normalizedHttpUrl } from "../../utils/url";
|
|
5
4
|
|
|
6
5
|
export interface RemoteImageProps {
|
|
@@ -11,13 +10,6 @@ export interface RemoteImageProps {
|
|
|
11
10
|
label?: string;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
function truncate(value: string, width: number): string {
|
|
15
|
-
if (width <= 0) return "";
|
|
16
|
-
if (value.length <= width) return value;
|
|
17
|
-
if (width <= 3) return value.slice(0, width);
|
|
18
|
-
return `${value.slice(0, width - 3)}...`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
export function RemoteImage({
|
|
22
14
|
src,
|
|
23
15
|
alt = "Image",
|
|
@@ -37,13 +29,8 @@ export function RemoteImage({
|
|
|
37
29
|
height={Math.max(4, height)}
|
|
38
30
|
objectFit="contain"
|
|
39
31
|
>
|
|
40
|
-
<Box flexDirection="column"
|
|
32
|
+
<Box flexDirection="column">
|
|
41
33
|
<Text fg={colors.textDim}>{label}</Text>
|
|
42
|
-
<ExternalLinkText
|
|
43
|
-
url={imageUrl}
|
|
44
|
-
label={truncate(imageUrl, resolvedWidth)}
|
|
45
|
-
color={colors.textBright}
|
|
46
|
-
/>
|
|
47
34
|
</Box>
|
|
48
35
|
</ImageSurface>
|
|
49
36
|
);
|
|
@@ -30,6 +30,7 @@ export interface TabsProps {
|
|
|
30
30
|
onAdd?: () => void;
|
|
31
31
|
focused?: boolean;
|
|
32
32
|
keyboardNavigation?: boolean;
|
|
33
|
+
scrollable?: boolean;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const WHEEL_DELTA_PER_CELL = 8;
|
|
@@ -45,6 +46,7 @@ export function Tabs({
|
|
|
45
46
|
onAdd,
|
|
46
47
|
focused = false,
|
|
47
48
|
keyboardNavigation = true,
|
|
49
|
+
scrollable = true,
|
|
48
50
|
}: TabsProps) {
|
|
49
51
|
const ui = useUiHost();
|
|
50
52
|
const NativeTabs = ui.Tabs;
|
|
@@ -131,6 +133,7 @@ export function Tabs({
|
|
|
131
133
|
closeMode={closeMode}
|
|
132
134
|
addLabel={addLabel}
|
|
133
135
|
onAdd={onAdd}
|
|
136
|
+
scrollable={scrollable}
|
|
134
137
|
palette={palette}
|
|
135
138
|
/>
|
|
136
139
|
);
|
|
@@ -154,6 +157,7 @@ function OpenTuiTabs({
|
|
|
154
157
|
closeMode = "always",
|
|
155
158
|
addLabel = "+",
|
|
156
159
|
onAdd,
|
|
160
|
+
scrollable = true,
|
|
157
161
|
palette,
|
|
158
162
|
}: TabsProps & {
|
|
159
163
|
palette: {
|
|
@@ -227,6 +231,96 @@ function OpenTuiTabs({
|
|
|
227
231
|
scrollBox.scrollTo({ x: nextLeft, y: scrollBox.scrollTop });
|
|
228
232
|
};
|
|
229
233
|
|
|
234
|
+
const tabRow = (
|
|
235
|
+
<Box flexDirection="row" width={totalWidth} height={1}>
|
|
236
|
+
{tabs.map((tab, index) => {
|
|
237
|
+
const active = tab.value === activeValue;
|
|
238
|
+
const hovered = hoveredValue === tab.value && !tab.disabled;
|
|
239
|
+
const tabWidth = tabWidths[index] ?? tab.label.length + 2;
|
|
240
|
+
const showClose = !!tab.onClose && (closeMode === "always" || active);
|
|
241
|
+
const attributes = (active ? TextAttributes.BOLD : 0)
|
|
242
|
+
| (variant === "underline" && !compact && (active || hovered) ? TextAttributes.UNDERLINE : 0);
|
|
243
|
+
const startHover = tab.disabled
|
|
244
|
+
? undefined
|
|
245
|
+
: () => {
|
|
246
|
+
setHoveredValue((current) => (current === tab.value ? current : tab.value));
|
|
247
|
+
};
|
|
248
|
+
const endHover = tab.disabled
|
|
249
|
+
? undefined
|
|
250
|
+
: () => {
|
|
251
|
+
setHoveredValue((current) => (current === tab.value ? null : current));
|
|
252
|
+
};
|
|
253
|
+
const selectTab = tab.disabled
|
|
254
|
+
? undefined
|
|
255
|
+
: (event: TabPointerEvent) => {
|
|
256
|
+
if (event.button === 2 && tab.onContextMenu) {
|
|
257
|
+
event.preventDefault?.();
|
|
258
|
+
event.stopPropagation?.();
|
|
259
|
+
tab.onContextMenu(tab.value, event);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
event.stopPropagation?.();
|
|
264
|
+
onSelect(tab.value);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Box
|
|
269
|
+
key={tab.value}
|
|
270
|
+
width={tabWidth}
|
|
271
|
+
height={1}
|
|
272
|
+
flexDirection="row"
|
|
273
|
+
backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
|
|
274
|
+
onMouseOver={startHover}
|
|
275
|
+
onMouseMove={startHover}
|
|
276
|
+
onMouseOut={endHover}
|
|
277
|
+
onMouseDown={selectTab}
|
|
278
|
+
onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
|
|
279
|
+
>
|
|
280
|
+
<Text
|
|
281
|
+
fg={tab.disabled ? palette.disabledFg : active && variant === "pill" ? palette.activePillFg : active ? palette.activeFg : hovered ? palette.hoverFg : palette.inactiveFg}
|
|
282
|
+
attributes={attributes}
|
|
283
|
+
onMouseDown={selectTab}
|
|
284
|
+
>
|
|
285
|
+
{` ${tab.label} `}
|
|
286
|
+
</Text>
|
|
287
|
+
{showClose && (
|
|
288
|
+
<Text
|
|
289
|
+
fg={active && variant === "pill" ? palette.activePillFg : palette.closeFg}
|
|
290
|
+
onMouseDown={(event: any) => {
|
|
291
|
+
event.preventDefault?.();
|
|
292
|
+
event.stopPropagation?.();
|
|
293
|
+
tab.onClose?.(tab.value);
|
|
294
|
+
}}
|
|
295
|
+
>
|
|
296
|
+
{"x "}
|
|
297
|
+
</Text>
|
|
298
|
+
)}
|
|
299
|
+
</Box>
|
|
300
|
+
);
|
|
301
|
+
})}
|
|
302
|
+
{onAdd && (
|
|
303
|
+
<Box
|
|
304
|
+
width={addWidth}
|
|
305
|
+
height={1}
|
|
306
|
+
backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
|
|
307
|
+
onMouseMove={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
|
|
308
|
+
onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
|
|
309
|
+
onMouseDown={(event: TabPointerEvent) => {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
onAdd();
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
<Text fg={hoveredValue === "__add__" ? palette.hoverFg : palette.addFg}>
|
|
315
|
+
{` ${addLabel} `}
|
|
316
|
+
</Text>
|
|
317
|
+
</Box>
|
|
318
|
+
)}
|
|
319
|
+
</Box>
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (!scrollable) return tabRow;
|
|
323
|
+
|
|
230
324
|
return (
|
|
231
325
|
<ScrollBox
|
|
232
326
|
ref={scrollRef}
|
|
@@ -237,86 +331,7 @@ function OpenTuiTabs({
|
|
|
237
331
|
horizontalScrollbarOptions={{ visible: false }}
|
|
238
332
|
onMouseScroll={handleMouseScroll}
|
|
239
333
|
>
|
|
240
|
-
|
|
241
|
-
{tabs.map((tab, index) => {
|
|
242
|
-
const active = tab.value === activeValue;
|
|
243
|
-
const hovered = hoveredValue === tab.value && !tab.disabled;
|
|
244
|
-
const tabWidth = tabWidths[index] ?? tab.label.length + 2;
|
|
245
|
-
const showClose = !!tab.onClose && (closeMode === "always" || active);
|
|
246
|
-
const attributes = (active ? TextAttributes.BOLD : 0)
|
|
247
|
-
| (variant === "underline" && !compact && (active || hovered) ? TextAttributes.UNDERLINE : 0);
|
|
248
|
-
const startHover = tab.disabled
|
|
249
|
-
? undefined
|
|
250
|
-
: () => {
|
|
251
|
-
setHoveredValue((current) => (current === tab.value ? current : tab.value));
|
|
252
|
-
};
|
|
253
|
-
const endHover = tab.disabled
|
|
254
|
-
? undefined
|
|
255
|
-
: () => {
|
|
256
|
-
setHoveredValue((current) => (current === tab.value ? null : current));
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<Box
|
|
261
|
-
key={tab.value}
|
|
262
|
-
width={tabWidth}
|
|
263
|
-
height={1}
|
|
264
|
-
flexDirection="row"
|
|
265
|
-
backgroundColor={active && variant === "pill" ? palette.activeBg : hovered ? palette.hoverBg : undefined}
|
|
266
|
-
onMouseOver={startHover}
|
|
267
|
-
onMouseMove={startHover}
|
|
268
|
-
onMouseOut={endHover}
|
|
269
|
-
onMouseDown={tab.disabled ? undefined : (event: TabPointerEvent) => {
|
|
270
|
-
if (event.button === 2 && tab.onContextMenu) {
|
|
271
|
-
event.preventDefault?.();
|
|
272
|
-
event.stopPropagation?.();
|
|
273
|
-
tab.onContextMenu(tab.value, event);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
event.preventDefault();
|
|
277
|
-
onSelect(tab.value);
|
|
278
|
-
}}
|
|
279
|
-
onDoubleClick={tab.disabled || !tab.onDoubleClick ? undefined : () => tab.onDoubleClick?.(tab.value)}
|
|
280
|
-
>
|
|
281
|
-
<Text
|
|
282
|
-
fg={tab.disabled ? palette.disabledFg : active && variant === "pill" ? palette.activePillFg : active ? palette.activeFg : hovered ? palette.hoverFg : palette.inactiveFg}
|
|
283
|
-
attributes={attributes}
|
|
284
|
-
>
|
|
285
|
-
{` ${tab.label} `}
|
|
286
|
-
</Text>
|
|
287
|
-
{showClose && (
|
|
288
|
-
<Text
|
|
289
|
-
fg={active && variant === "pill" ? palette.activePillFg : palette.closeFg}
|
|
290
|
-
onMouseDown={(event: any) => {
|
|
291
|
-
event.preventDefault?.();
|
|
292
|
-
event.stopPropagation?.();
|
|
293
|
-
tab.onClose?.(tab.value);
|
|
294
|
-
}}
|
|
295
|
-
>
|
|
296
|
-
{"x "}
|
|
297
|
-
</Text>
|
|
298
|
-
)}
|
|
299
|
-
</Box>
|
|
300
|
-
);
|
|
301
|
-
})}
|
|
302
|
-
{onAdd && (
|
|
303
|
-
<Box
|
|
304
|
-
width={addWidth}
|
|
305
|
-
height={1}
|
|
306
|
-
backgroundColor={hoveredValue === "__add__" ? palette.hoverBg : undefined}
|
|
307
|
-
onMouseMove={() => setHoveredValue((current) => (current === "__add__" ? current : "__add__"))}
|
|
308
|
-
onMouseOut={() => setHoveredValue((current) => (current === "__add__" ? null : current))}
|
|
309
|
-
onMouseDown={(event: TabPointerEvent) => {
|
|
310
|
-
event.preventDefault();
|
|
311
|
-
onAdd();
|
|
312
|
-
}}
|
|
313
|
-
>
|
|
314
|
-
<Text fg={hoveredValue === "__add__" ? palette.hoverFg : palette.addFg}>
|
|
315
|
-
{` ${addLabel} `}
|
|
316
|
-
</Text>
|
|
317
|
-
</Box>
|
|
318
|
-
)}
|
|
319
|
-
</Box>
|
|
334
|
+
{tabRow}
|
|
320
335
|
</ScrollBox>
|
|
321
336
|
);
|
|
322
337
|
}
|
|
@@ -360,6 +360,60 @@ describe("shared UI kit", () => {
|
|
|
360
360
|
expect(lastSelected).toBe("chart");
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
+
test("selects tabs by clicking their text labels", async () => {
|
|
364
|
+
let lastSelected = "overview";
|
|
365
|
+
const selectedValues: string[] = [];
|
|
366
|
+
|
|
367
|
+
function PointerTabsHarness() {
|
|
368
|
+
const [activeValue, setActiveValue] = useState("overview");
|
|
369
|
+
return (
|
|
370
|
+
<Tabs
|
|
371
|
+
tabs={[
|
|
372
|
+
{ label: "Overview", value: "overview" },
|
|
373
|
+
{ label: "News", value: "news" },
|
|
374
|
+
{ label: "Chart", value: "chart" },
|
|
375
|
+
]}
|
|
376
|
+
activeValue={activeValue}
|
|
377
|
+
onSelect={(value) => {
|
|
378
|
+
lastSelected = value;
|
|
379
|
+
selectedValues.push(value);
|
|
380
|
+
setActiveValue(value);
|
|
381
|
+
}}
|
|
382
|
+
scrollable={false}
|
|
383
|
+
/>
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
testSetup = await testRender(<PointerTabsHarness />, { width: 40, height: 4 });
|
|
388
|
+
|
|
389
|
+
await act(async () => {
|
|
390
|
+
await testSetup!.renderOnce();
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
let frame = testSetup.captureCharFrame();
|
|
394
|
+
const newsCol = frame.split("\n")[0]!.indexOf("News");
|
|
395
|
+
expect(newsCol).toBeGreaterThanOrEqual(0);
|
|
396
|
+
|
|
397
|
+
await act(async () => {
|
|
398
|
+
await testSetup!.mockMouse.click(newsCol + 1, 0);
|
|
399
|
+
await testSetup!.renderOnce();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(lastSelected).toBe("news");
|
|
403
|
+
|
|
404
|
+
frame = testSetup.captureCharFrame();
|
|
405
|
+
const chartCol = frame.split("\n")[0]!.indexOf("Chart");
|
|
406
|
+
expect(chartCol).toBeGreaterThanOrEqual(0);
|
|
407
|
+
|
|
408
|
+
await act(async () => {
|
|
409
|
+
await testSetup!.mockMouse.click(chartCol + 1, 0);
|
|
410
|
+
await testSetup!.renderOnce();
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
expect(selectedValues).toEqual(["news", "chart"]);
|
|
414
|
+
expect(lastSelected).toBe("chart");
|
|
415
|
+
});
|
|
416
|
+
|
|
363
417
|
test("renders tab actions for editable tab sets", async () => {
|
|
364
418
|
testSetup = await testRender(
|
|
365
419
|
<Tabs
|
|
@@ -26,10 +26,18 @@ export interface CollectionSortPreference {
|
|
|
26
26
|
direction: SortDirection;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
interface
|
|
29
|
+
interface CommandBarLaunchRequestBase {
|
|
30
|
+
sequence: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface CommandBarPluginLaunchRequest extends CommandBarLaunchRequestBase {
|
|
30
34
|
kind: "plugin-command";
|
|
31
35
|
commandId: string;
|
|
32
|
-
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CommandBarTickerSearchLaunchRequest extends CommandBarLaunchRequestBase {
|
|
39
|
+
kind: "ticker-search";
|
|
40
|
+
query?: string;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
export interface AppState {
|
|
@@ -44,7 +52,7 @@ export interface AppState {
|
|
|
44
52
|
recentTickers: string[];
|
|
45
53
|
commandBarOpen: boolean;
|
|
46
54
|
commandBarQuery: string;
|
|
47
|
-
commandBarLaunchRequest:
|
|
55
|
+
commandBarLaunchRequest: CommandBarPluginLaunchRequest | CommandBarTickerSearchLaunchRequest | null;
|
|
48
56
|
themePreview: string | null;
|
|
49
57
|
refreshing: Set<string>;
|
|
50
58
|
initialized: boolean;
|
|
@@ -74,7 +82,7 @@ export type AppAction =
|
|
|
74
82
|
type: "SET_COMMAND_BAR";
|
|
75
83
|
open: boolean;
|
|
76
84
|
query?: string;
|
|
77
|
-
launch?: { kind: "plugin-command"; commandId: string } | null;
|
|
85
|
+
launch?: { kind: "plugin-command"; commandId: string } | { kind: "ticker-search"; query?: string } | null;
|
|
78
86
|
}
|
|
79
87
|
| { type: "SET_COMMAND_BAR_QUERY"; query: string }
|
|
80
88
|
| { type: "SET_REFRESHING"; symbol: string; refreshing: boolean }
|