gloomberb 0.10.2 → 0.10.4
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 +3 -3
- package/README.zh-CN.md +2 -2
- package/package.json +2 -1
- package/src/api-client/auth.ts +8 -0
- package/src/api-client/index.ts +70 -0
- package/src/api-client/types.ts +49 -0
- package/src/app/runtime/desktop-deeplink.test.ts +8 -0
- package/src/app/runtime/desktop-deeplink.ts +14 -1
- package/src/components/chart/composite/composite-chart.test.tsx +340 -17
- package/src/components/chart/composite/composite-chart.tsx +859 -68
- package/src/components/chart/composite/interactions.test.ts +78 -22
- package/src/components/chart/composite/interactions.ts +45 -17
- package/src/components/chart/composite/rasterizer.ts +8 -2
- package/src/components/chart/composite/renderers.test.ts +61 -1
- package/src/components/chart/composite/scene.ts +14 -4
- package/src/components/chart/composite/tools.test.ts +371 -0
- package/src/components/chart/composite/tools.ts +508 -0
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/chart/core/pointer.ts +2 -0
- package/src/components/chart/native/raster/primitives.ts +18 -0
- package/src/components/chart/price-axis-labels.tsx +42 -2
- package/src/components/chart/static/chart/axis-overlays.test.tsx +2 -0
- package/src/components/chart/static/chart/axis-overlays.tsx +46 -0
- package/src/components/command-bar/action-runtime.ts +3 -0
- package/src/components/command-bar/assist/inventory.test.ts +93 -0
- package/src/components/command-bar/assist/inventory.ts +92 -0
- package/src/components/command-bar/assist/model.test.ts +104 -0
- package/src/components/command-bar/assist/model.ts +169 -0
- package/src/components/command-bar/assist/runtime.ts +185 -0
- package/src/components/command-bar/commands/direct/index.ts +1 -1
- package/src/components/command-bar/keyboard-shortcuts.ts +5 -0
- package/src/components/command-bar/list/model.ts +15 -2
- package/src/components/command-bar/list/navigation.ts +6 -0
- package/src/components/command-bar/list/view.tsx +16 -3
- package/src/components/command-bar/pane-templates/workflow.ts +1 -0
- package/src/components/command-bar/panel/body.tsx +2 -0
- package/src/components/command-bar/panel/index.tsx +3 -0
- package/src/components/command-bar/panel/runtime.ts +6 -0
- package/src/components/command-bar/panel/types.ts +1 -0
- package/src/components/command-bar/routes/root/results.ts +28 -2
- package/src/components/command-bar/routes/root/runtime.ts +66 -36
- package/src/components/command-bar/routes/root/shortcuts.ts +8 -4
- package/src/components/command-bar/routing/navigation-state.ts +17 -0
- package/src/components/command-bar/selection-runtime.ts +26 -0
- package/src/components/command-bar/surface/assist.test.tsx +276 -0
- package/src/components/command-bar/surface/index.tsx +93 -2
- package/src/components/command-bar/surface/portfolio.test.tsx +2 -1
- package/src/components/command-bar/view-model.ts +7 -1
- package/src/components/command-bar/workflow/broker.test.ts +22 -0
- package/src/components/command-bar/workflow/broker.ts +1 -0
- package/src/components/index.ts +1 -1
- package/src/components/layout/detached-pane-shell.tsx +40 -2
- package/src/components/layout/floating-pane.tsx +4 -1
- package/src/components/layout/pane/header.tsx +64 -6
- package/src/components/layout/pane/index.tsx +4 -1
- package/src/components/layout/shell/index.tsx +23 -0
- package/src/components/layout/shell/pane/layers.tsx +5 -0
- package/src/components/onboarding/account-step/chooser-panel.tsx +74 -0
- package/src/components/onboarding/account-step/form-panel.tsx +157 -0
- package/src/components/onboarding/account-step/index.tsx +58 -0
- package/src/components/onboarding/account-step/model.test.ts +67 -0
- package/src/components/onboarding/account-step/model.ts +126 -0
- package/src/components/onboarding/account-step/signed-in-panel.tsx +31 -0
- package/src/components/onboarding/onboarding-steps.tsx +91 -55
- package/src/components/onboarding/onboarding-wizard.test.tsx +73 -6
- package/src/components/onboarding/onboarding-wizard.tsx +93 -9
- package/src/components/onboarding/portfolio-step/broker-fields-panel.tsx +3 -3
- package/src/components/onboarding/wizard-account.ts +235 -0
- package/src/components/onboarding/wizard-keyboard.ts +67 -1
- package/src/components/onboarding/wizard-model.ts +2 -2
- package/src/components/ui/data-table/index.tsx +1 -0
- package/src/components/ui/data-table/opentui/index.tsx +44 -2
- package/src/components/ui/data-table/types.ts +10 -0
- package/src/components/ui/data-table/visible-range.test.ts +29 -0
- package/src/components/ui/data-table/visible-range.ts +21 -0
- package/src/components/ui/ui.test.tsx +23 -1
- package/src/i18n/es.ts +1093 -0
- package/src/i18n/index.test.ts +5 -2
- package/src/i18n/index.ts +3 -0
- package/src/i18n/ja.ts +66 -3
- package/src/i18n/ko.ts +66 -3
- package/src/i18n/languages.test.ts +4 -1
- package/src/i18n/languages.ts +12 -1
- package/src/i18n/zh-cn.ts +66 -3
- package/src/i18n/zh-tw.ts +66 -3
- package/src/market-data/coordinator/index.ts +1 -1
- package/src/market-data/coordinator/quotes.ts +64 -1
- package/src/market-data/coordinator/subscriptions.test.ts +72 -0
- package/src/market-data/quotes/cadence.ts +2 -0
- package/src/plugins/builtin/account-management/footer.ts +9 -3
- package/src/plugins/builtin/account-management/model.ts +53 -1
- package/src/plugins/builtin/account-management/pane.tsx +107 -24
- package/src/plugins/builtin/ai/index.tsx +7 -2
- package/src/plugins/builtin/ai/screener/market-runtime.ts +4 -2
- package/src/plugins/builtin/ai/screener/pane.tsx +3 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +0 -1
- package/src/plugins/builtin/chart-composer/index.tsx +10 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +111 -44
- package/src/plugins/builtin/chart-composer/quick-add.tsx +27 -3
- package/src/plugins/builtin/chart-composer/semantic.ts +21 -0
- package/src/plugins/builtin/chat/controller/persistence.ts +5 -0
- package/src/plugins/builtin/cloud/plugin.tsx +9 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +28 -0
- package/src/plugins/builtin/cloud/upgrade-status-widget.tsx +54 -0
- package/src/plugins/builtin/help/index.tsx +38 -0
- package/src/plugins/builtin/news/wire/news/footer.ts +18 -38
- package/src/plugins/builtin/options/footer.test.ts +20 -112
- package/src/plugins/builtin/options/footer.ts +35 -78
- package/src/plugins/builtin/options/index.tsx +6 -0
- package/src/plugins/builtin/options/live-quotes.test.ts +117 -14
- package/src/plugins/builtin/options/live-quotes.ts +93 -39
- package/src/plugins/builtin/options/view.test.tsx +5 -2
- package/src/plugins/builtin/options/view.tsx +49 -9
- package/src/plugins/builtin/portfolio-list/index.tsx +12 -4
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +22 -1
- package/src/plugins/builtin/portfolio-list/pane/streaming.ts +17 -13
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.test.tsx +76 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-ticker-order.ts +78 -0
- package/src/plugins/builtin/shared/cloud-upgrade.ts +131 -0
- package/src/plugins/builtin/shared/live-streaming.ts +60 -0
- package/src/plugins/builtin/shared/plan-access.test.ts +69 -0
- package/src/plugins/builtin/shared/plan-access.ts +109 -0
- package/src/plugins/builtin/ticker-detail/chart-tab-switch.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +11 -2
- package/src/plugins/builtin/ticker-detail/pane.tsx +21 -3
- package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +4 -2
- package/src/plugins/registry/index.test.ts +51 -0
- package/src/plugins/registry/index.ts +12 -0
- package/src/plugins/registry/pane-settings.ts +29 -0
- package/src/renderers/electrobun/view/data-table/index.tsx +44 -3
- package/src/renderers/electrobun/view/host/box.test.tsx +97 -0
- package/src/renderers/electrobun/view/host/box.tsx +3 -0
- package/src/renderers/electrobun/view/host/chart-surface.tsx +85 -1
- package/src/renderers/electrobun/view/host/focus-scope.test.ts +37 -0
- package/src/renderers/electrobun/view/host/focus-scope.ts +56 -0
- package/src/renderers/electrobun/view/host/input.tsx +16 -1
- package/src/renderers/electrobun/view/host/mouse.ts +1 -0
- package/src/renderers/electrobun/view/host/style.ts +1 -1
- package/src/renderers/electrobun/view/host/tabs.tsx +4 -2
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/opentui/chart-surface.tsx +2 -0
- package/src/renderers/opentui/host.tsx +3 -0
- package/src/sources/gloomberb-cloud/index.test.ts +8 -0
- package/src/sources/gloomberb-cloud/index.ts +4 -4
- package/src/sources/provider-router/history.test.ts +3 -3
- package/src/sources/provider-router/history.ts +15 -4
- package/src/sources/yahoo-finance/history.test.ts +47 -0
- package/src/sources/yahoo-finance/history.ts +5 -1
- package/src/state/hooks/quote-streaming.test.tsx +181 -1
- package/src/state/hooks/quote-streaming.ts +93 -7
- package/src/theme/colors.ts +20 -1
- package/src/theme/themes.test.ts +3 -0
- package/src/{sources/gloomberb-cloud → time-series}/history-quality.ts +27 -3
- package/src/time-series/resolution.test.ts +51 -0
- package/src/time-series/resolution.ts +41 -0
- package/src/time-series/resolve.test.ts +207 -1
- package/src/time-series/resolve.ts +196 -29
- package/src/time-series/spec-studies.test.ts +4 -0
- package/src/time-series/spec.ts +15 -1
- package/src/time-series/types.ts +7 -0
- package/src/time-series/use-chart-resolution.test.tsx +37 -0
- package/src/time-series/use-chart-resolution.ts +17 -1
- package/src/types/plugin.ts +9 -0
- package/src/ui/host.tsx +17 -0
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
1
2
|
import { AsciiText, Box, Span, Strong, Text, TextAttributes, useUiHost } from "../../ui";
|
|
2
3
|
import { colors } from "../../theme/colors";
|
|
3
4
|
import { t, tf } from "../../i18n";
|
|
4
5
|
import { themes } from "../../theme/themes";
|
|
5
6
|
import { detectShortcutPlatform, formatPrimaryShortcut, getShortcutDisplayMode } from "../../utils/shortcut-labels";
|
|
7
|
+
import { ListView, type ListViewItem } from "../ui";
|
|
8
|
+
import type { AccountOutcome } from "./account-step/model";
|
|
6
9
|
export { PortfolioStep, type PortfolioSub } from "./portfolio-step";
|
|
10
|
+
export { AccountStep } from "./account-step";
|
|
7
11
|
|
|
8
12
|
export interface BrokerSyncSummary {
|
|
9
13
|
portfolioId: string | null;
|
|
@@ -30,12 +34,24 @@ export function WelcomeStep() {
|
|
|
30
34
|
);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Rows the theme step spends on everything that is not the theme list itself.
|
|
38
|
+
const THEME_CHROME_ROWS = 8;
|
|
39
|
+
|
|
40
|
+
export function ThemeStep({
|
|
41
|
+
themeIds,
|
|
42
|
+
selectedIdx,
|
|
43
|
+
onSelect,
|
|
44
|
+
height,
|
|
45
|
+
}: {
|
|
46
|
+
themeIds: string[];
|
|
47
|
+
selectedIdx: number;
|
|
48
|
+
onSelect: (index: number) => void;
|
|
49
|
+
height: number;
|
|
50
|
+
}) {
|
|
51
|
+
const items = useMemo<ListViewItem[]>(
|
|
52
|
+
() => themeIds.map((id) => ({ id, label: themes[id]!.name })),
|
|
53
|
+
[themeIds],
|
|
54
|
+
);
|
|
39
55
|
|
|
40
56
|
return (
|
|
41
57
|
<Box flexDirection="column" paddingX={2}>
|
|
@@ -48,7 +64,7 @@ export function ThemeStep({ themeIds, selectedIdx, height }: { themeIds: string[
|
|
|
48
64
|
<Text fg={colors.text} attributes={TextAttributes.BOLD}>{"TH"}</Text>
|
|
49
65
|
</Box>
|
|
50
66
|
<Box height={1} />
|
|
51
|
-
<Box height={1}>
|
|
67
|
+
<Box height={1} flexDirection="row">
|
|
52
68
|
<Text fg={colors.positive}>{" \u2588\u2588 "}</Text>
|
|
53
69
|
<Text fg={colors.negative}>{" \u2588\u2588 "}</Text>
|
|
54
70
|
<Text fg={colors.text}>{" \u2588\u2588 "}</Text>
|
|
@@ -58,33 +74,13 @@ export function ThemeStep({ themeIds, selectedIdx, height }: { themeIds: string[
|
|
|
58
74
|
</Box>
|
|
59
75
|
<Box height={1} />
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const theme = themes[id]!;
|
|
69
|
-
const globalIdx = windowStart + index;
|
|
70
|
-
const isSelected = globalIdx === selectedIdx;
|
|
71
|
-
return (
|
|
72
|
-
<Box key={id} height={1} backgroundColor={isSelected ? colors.selected : colors.bg}>
|
|
73
|
-
<Text fg={isSelected ? colors.selectedText : colors.textDim}>
|
|
74
|
-
{isSelected ? "\u25b8 " : " "}
|
|
75
|
-
</Text>
|
|
76
|
-
<Text fg={isSelected ? colors.text : colors.textDim} attributes={isSelected ? TextAttributes.BOLD : 0}>
|
|
77
|
-
{theme.name}
|
|
78
|
-
</Text>
|
|
79
|
-
</Box>
|
|
80
|
-
);
|
|
81
|
-
})}
|
|
82
|
-
|
|
83
|
-
{windowEnd < themeIds.length && (
|
|
84
|
-
<Box height={1}>
|
|
85
|
-
<Text fg={colors.textMuted}>{"\u2193 more"}</Text>
|
|
86
|
-
</Box>
|
|
87
|
-
)}
|
|
77
|
+
<ListView
|
|
78
|
+
items={items}
|
|
79
|
+
selectedIndex={selectedIdx}
|
|
80
|
+
onSelect={onSelect}
|
|
81
|
+
scrollable
|
|
82
|
+
height={Math.max(3, height - THEME_CHROME_ROWS)}
|
|
83
|
+
/>
|
|
88
84
|
|
|
89
85
|
<Box height={1} />
|
|
90
86
|
<Box height={1}>
|
|
@@ -94,7 +90,7 @@ export function ThemeStep({ themeIds, selectedIdx, height }: { themeIds: string[
|
|
|
94
90
|
);
|
|
95
91
|
}
|
|
96
92
|
|
|
97
|
-
export function ShortcutsStep() {
|
|
93
|
+
export function ShortcutsStep({ height }: { height: number }) {
|
|
98
94
|
const uiHost = useUiHost();
|
|
99
95
|
const shortcutPlatform = detectShortcutPlatform();
|
|
100
96
|
const shortcutDisplayMode = getShortcutDisplayMode(uiHost.kind);
|
|
@@ -117,6 +113,11 @@ export function ShortcutsStep() {
|
|
|
117
113
|
|
|
118
114
|
const COL = 20;
|
|
119
115
|
|
|
116
|
+
// Shed the least useful rows first so the wizard footer stays on screen in an 80x24 terminal.
|
|
117
|
+
const gap = height < 21 ? 1 : 2;
|
|
118
|
+
const showTagline = height >= 19;
|
|
119
|
+
const showPrefixHint = height >= 17;
|
|
120
|
+
|
|
120
121
|
return (
|
|
121
122
|
<Box flexDirection="column" paddingX={2}>
|
|
122
123
|
<Box height={1}>
|
|
@@ -131,15 +132,19 @@ export function ShortcutsStep() {
|
|
|
131
132
|
</Box>
|
|
132
133
|
))}
|
|
133
134
|
|
|
134
|
-
<Box height={
|
|
135
|
+
<Box height={gap} />
|
|
135
136
|
<Box height={1}>
|
|
136
137
|
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{t("Basic command prefixes")}</Text>
|
|
137
138
|
</Box>
|
|
138
139
|
<Box height={1} />
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
{showPrefixHint && (
|
|
141
|
+
<>
|
|
142
|
+
<Box height={1}>
|
|
143
|
+
<Text fg={colors.textDim}>{t("Type these in the command bar (")}<Span fg={colors.text}><Strong>{"Ctrl+P"}</Strong></Span>{"):"}</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
<Box height={1} />
|
|
146
|
+
</>
|
|
147
|
+
)}
|
|
143
148
|
|
|
144
149
|
{commandPrefixes.map((shortcut) => (
|
|
145
150
|
<Box key={shortcut.key} height={1} flexDirection="row">
|
|
@@ -148,10 +153,14 @@ export function ShortcutsStep() {
|
|
|
148
153
|
</Box>
|
|
149
154
|
))}
|
|
150
155
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
{showTagline && (
|
|
157
|
+
<>
|
|
158
|
+
<Box height={gap} />
|
|
159
|
+
<Box height={1}>
|
|
160
|
+
<Text fg={colors.textDim}>{t("Everything is searchable, just type what you want.")}</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
155
164
|
</Box>
|
|
156
165
|
);
|
|
157
166
|
}
|
|
@@ -160,17 +169,20 @@ export function ReadyStep({
|
|
|
160
169
|
brokerName,
|
|
161
170
|
portfolioName,
|
|
162
171
|
brokerSyncSummary,
|
|
172
|
+
accountOutcome,
|
|
163
173
|
isFinishing,
|
|
164
174
|
error,
|
|
165
175
|
}: {
|
|
166
176
|
brokerName: string | null;
|
|
167
177
|
portfolioName: string;
|
|
168
178
|
brokerSyncSummary: BrokerSyncSummary | null;
|
|
179
|
+
accountOutcome: AccountOutcome | null;
|
|
169
180
|
isFinishing: boolean;
|
|
170
181
|
error: string | null;
|
|
171
182
|
}) {
|
|
172
183
|
const positionsImported = brokerSyncSummary?.positionsImported ?? 0;
|
|
173
184
|
const positionLabel = positionsImported === 1 ? "position" : "positions";
|
|
185
|
+
const accountEmail = accountOutcome?.email ?? "";
|
|
174
186
|
|
|
175
187
|
return (
|
|
176
188
|
<Box flexDirection="column" paddingX={2}>
|
|
@@ -179,23 +191,47 @@ export function ReadyStep({
|
|
|
179
191
|
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{t("You're all set")}</Text>
|
|
180
192
|
</Box>
|
|
181
193
|
<Box height={2} />
|
|
182
|
-
<Box height={1}>
|
|
183
|
-
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</Text>
|
|
184
|
-
<Text fg={colors.text}>{
|
|
194
|
+
<Box height={1} flexDirection="row">
|
|
195
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713 "}</Text>
|
|
196
|
+
<Text fg={colors.text}>{t("Theme configured")}</Text>
|
|
185
197
|
</Box>
|
|
186
|
-
<Box height={1}>
|
|
187
|
-
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</Text>
|
|
198
|
+
<Box height={1} flexDirection="row">
|
|
199
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713 "}</Text>
|
|
188
200
|
<Text fg={colors.text}>
|
|
189
201
|
{brokerName
|
|
190
|
-
?
|
|
191
|
-
:
|
|
202
|
+
? tf("{broker} connected. Imported {count} {label}", { broker: brokerName, count: positionsImported, label: t(positionLabel) })
|
|
203
|
+
: tf('Portfolio "{name}" created', { name: portfolioName })}
|
|
192
204
|
</Text>
|
|
193
205
|
</Box>
|
|
194
|
-
<Box height={1}>
|
|
195
|
-
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713"}</Text>
|
|
196
|
-
<Text fg={colors.text}>{
|
|
206
|
+
<Box height={1} flexDirection="row">
|
|
207
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713 "}</Text>
|
|
208
|
+
<Text fg={colors.text}>{t("Plugins enabled")}</Text>
|
|
197
209
|
</Box>
|
|
198
|
-
|
|
210
|
+
{accountOutcome && (
|
|
211
|
+
<Box height={1} flexDirection="row">
|
|
212
|
+
<Text fg={colors.positive} attributes={TextAttributes.BOLD}>{"\u2713 "}</Text>
|
|
213
|
+
<Text fg={colors.text}>
|
|
214
|
+
{!accountEmail
|
|
215
|
+
? t("Signed in to Gloom Cloud")
|
|
216
|
+
: accountOutcome.mode === "signup"
|
|
217
|
+
? tf("Account created for {email}", { email: accountEmail })
|
|
218
|
+
: tf("Signed in as {email}", { email: accountEmail })}
|
|
219
|
+
</Text>
|
|
220
|
+
</Box>
|
|
221
|
+
)}
|
|
222
|
+
<Box height={1} />
|
|
223
|
+
{accountOutcome?.mode === "signup" ? (
|
|
224
|
+
<Box height={2}>
|
|
225
|
+
<Text fg={colors.text}>
|
|
226
|
+
{t("Check your inbox to verify your email. Then start your free 7-day Pro trial anytime \u2014 type UPGRADE.")}
|
|
227
|
+
</Text>
|
|
228
|
+
</Box>
|
|
229
|
+
) : accountOutcome ? null : (
|
|
230
|
+
<Box height={1}>
|
|
231
|
+
<Text fg={colors.textMuted}>{t("Create an account anytime: Ctrl+P \u2192 Sign Up.")}</Text>
|
|
232
|
+
</Box>
|
|
233
|
+
)}
|
|
234
|
+
<Box height={1} />
|
|
199
235
|
{brokerName ? (
|
|
200
236
|
<Box height={1}>
|
|
201
237
|
<Text fg={isFinishing ? colors.text : colors.textDim}>
|
|
@@ -3,6 +3,7 @@ import { mkdtemp, rm } from "fs/promises";
|
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
5
5
|
import { act } from "react";
|
|
6
|
+
import { setCloudApiFetchTransport } from "../../api-client";
|
|
6
7
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
7
8
|
import { OnboardingWizard } from "./onboarding-wizard";
|
|
8
9
|
import { createDefaultConfig, type AppConfig } from "../../types/config";
|
|
@@ -80,6 +81,7 @@ async function waitForFrameToContain(text: string, attempts = 8): Promise<string
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
afterEach(async () => {
|
|
84
|
+
setCloudApiFetchTransport(null);
|
|
83
85
|
if (testSetup) {
|
|
84
86
|
await act(async () => {
|
|
85
87
|
testSetup!.renderer.destroy();
|
|
@@ -223,12 +225,10 @@ describe("OnboardingWizard", () => {
|
|
|
223
225
|
frame = testSetup.captureCharFrame();
|
|
224
226
|
expect(frame).toContain("After launch shortcuts");
|
|
225
227
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
expect(frame).toContain("Imported 1 position");
|
|
228
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
229
|
+
await waitForFrameToContain("Create your free Gloom Cloud account");
|
|
230
|
+
await emitKeypress(testSetup, { name: "right", sequence: "[C" });
|
|
231
|
+
frame = await waitForFrameToContain("Imported 1 position");
|
|
232
232
|
expect(completedConfig).toBeNull();
|
|
233
233
|
|
|
234
234
|
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
@@ -252,4 +252,71 @@ describe("OnboardingWizard", () => {
|
|
|
252
252
|
.toBe("broker:demo-demo-broker:ACC-1");
|
|
253
253
|
expect((await tickerRepository.loadAllTickers()).map((ticker) => ticker.metadata.ticker)).toContain("AAPL");
|
|
254
254
|
});
|
|
255
|
+
|
|
256
|
+
test("account step validates locally and stays skippable all the way to launch", async () => {
|
|
257
|
+
tempDataDir = await mkdtemp(join(tmpdir(), "gloomberb-onboarding-account-"));
|
|
258
|
+
const requestedUrls: string[] = [];
|
|
259
|
+
setCloudApiFetchTransport(async (url) => {
|
|
260
|
+
requestedUrls.push(url);
|
|
261
|
+
throw new Error("network is disabled in tests");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const pluginRegistry = {
|
|
265
|
+
allPlugins: new Map(),
|
|
266
|
+
brokers: new Map(),
|
|
267
|
+
paneTemplates: new Map(),
|
|
268
|
+
getPaneTemplatePluginId: () => undefined,
|
|
269
|
+
tickerRepository: createTickerRepository(),
|
|
270
|
+
persistence: { resources: undefined },
|
|
271
|
+
} as unknown as PluginRegistry;
|
|
272
|
+
|
|
273
|
+
let completedConfig: AppConfig | null = null;
|
|
274
|
+
testSetup = await testRender(
|
|
275
|
+
<OnboardingWizard
|
|
276
|
+
config={createDefaultConfig(tempDataDir)}
|
|
277
|
+
pluginRegistry={pluginRegistry}
|
|
278
|
+
onComplete={(nextConfig) => {
|
|
279
|
+
completedConfig = nextConfig;
|
|
280
|
+
}}
|
|
281
|
+
/>,
|
|
282
|
+
{ width: 90, height: 28 },
|
|
283
|
+
);
|
|
284
|
+
await testSetup.renderOnce();
|
|
285
|
+
|
|
286
|
+
// welcome -> theme -> portfolio -> shortcuts -> account
|
|
287
|
+
for (let index = 0; index < 4; index += 1) {
|
|
288
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
289
|
+
}
|
|
290
|
+
let frame = await waitForFrameToContain("Create your free Gloom Cloud account");
|
|
291
|
+
expect(frame).toContain("Try Pro free for 7 days");
|
|
292
|
+
|
|
293
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
294
|
+
await waitForFrameToContain("Create your free account");
|
|
295
|
+
|
|
296
|
+
// Submitting an empty form must be caught locally instead of hitting the API.
|
|
297
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
298
|
+
frame = await waitForFrameToContain("Enter your email address.");
|
|
299
|
+
expect(requestedUrls).toEqual([]);
|
|
300
|
+
|
|
301
|
+
await emitKeypress(testSetup, { name: "escape", sequence: "\u001b" });
|
|
302
|
+
await waitForFrameToContain("Skip for now");
|
|
303
|
+
|
|
304
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
305
|
+
await emitKeypress(testSetup, { name: "down", sequence: "\u001b[B" });
|
|
306
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
307
|
+
frame = await waitForFrameToContain("Create an account anytime");
|
|
308
|
+
expect(frame).not.toContain("Signed in as");
|
|
309
|
+
|
|
310
|
+
await emitKeypress(testSetup, { name: "return", sequence: "\r" });
|
|
311
|
+
for (let index = 0; index < 20 && !completedConfig; index += 1) {
|
|
312
|
+
await act(async () => {
|
|
313
|
+
await Promise.resolve();
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
315
|
+
await testSetup!.renderOnce();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
expect(completedConfig).not.toBeNull();
|
|
320
|
+
expect(requestedUrls).toEqual([]);
|
|
321
|
+
});
|
|
255
322
|
});
|
|
@@ -10,6 +10,7 @@ import type { PluginRegistry } from "../../plugins/registry";
|
|
|
10
10
|
import { resolveBrokerConfigFields, type BrokerConfigField } from "../../types/broker";
|
|
11
11
|
import type { ListViewItem } from "../ui";
|
|
12
12
|
import {
|
|
13
|
+
AccountStep,
|
|
13
14
|
PortfolioStep,
|
|
14
15
|
ReadyStep,
|
|
15
16
|
ShortcutsStep,
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
WelcomeStep,
|
|
18
19
|
type PortfolioSub,
|
|
19
20
|
} from "./onboarding-steps";
|
|
21
|
+
import { ACCOUNT_CHOICE_IDS } from "./account-step/model";
|
|
22
|
+
import { useOnboardingAccount } from "./wizard-account";
|
|
20
23
|
import { finishOnboarding, summarizeOnboardingError, useOnboardingBrokerSync } from "./wizard-broker-sync";
|
|
21
24
|
import { useOnboardingKeyboard } from "./wizard-keyboard";
|
|
22
25
|
import {
|
|
@@ -26,6 +29,9 @@ import {
|
|
|
26
29
|
type OnboardingStep,
|
|
27
30
|
} from "./wizard-model";
|
|
28
31
|
|
|
32
|
+
// Blank spacer + progress dots + hint line.
|
|
33
|
+
const FOOTER_ROWS = 3;
|
|
34
|
+
|
|
29
35
|
interface OnboardingWizardProps {
|
|
30
36
|
config: AppConfig;
|
|
31
37
|
pluginRegistry: PluginRegistry;
|
|
@@ -83,12 +89,6 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
83
89
|
applyTheme(themeIds[themeIdx]!);
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
useEffect(() => {
|
|
87
|
-
if (!editingField) return;
|
|
88
|
-
const focusTimer = setTimeout(() => inputRef.current?.focus?.(), 10);
|
|
89
|
-
return () => clearTimeout(focusTimer);
|
|
90
|
-
}, [editingField, portfolioSub, brokerFieldIdx]);
|
|
91
|
-
|
|
92
92
|
useEffect(() => {
|
|
93
93
|
if (!selectedBrokerId) return;
|
|
94
94
|
const field = activeBrokerFields[brokerFieldIdx];
|
|
@@ -119,6 +119,32 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
119
119
|
}
|
|
120
120
|
}, [step]);
|
|
121
121
|
|
|
122
|
+
const account = useOnboardingAccount({ nextStep, setEditingField });
|
|
123
|
+
|
|
124
|
+
// Re-focus whenever the active field changes so enter keeps moving through
|
|
125
|
+
// the broker and account forms without a mouse.
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
if (!editingField) return;
|
|
128
|
+
const focusTimer = setTimeout(() => inputRef.current?.focus?.(), 10);
|
|
129
|
+
return () => clearTimeout(focusTimer);
|
|
130
|
+
}, [editingField, portfolioSub, brokerFieldIdx, account.accountSub, account.accountFieldIdx]);
|
|
131
|
+
|
|
132
|
+
const { beginAccountMode, syncExistingAccountSession } = account;
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (step !== "account") return;
|
|
135
|
+
syncExistingAccountSession();
|
|
136
|
+
}, [step, syncExistingAccountSession]);
|
|
137
|
+
|
|
138
|
+
// Clicking a chooser row has to do exactly what enter does on it.
|
|
139
|
+
const activateAccountChoice = useCallback((index: number) => {
|
|
140
|
+
const choice = ACCOUNT_CHOICE_IDS[index];
|
|
141
|
+
if (!choice || choice === "skip") {
|
|
142
|
+
nextStep();
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
beginAccountMode(choice);
|
|
146
|
+
}, [beginAccountMode, nextStep]);
|
|
147
|
+
|
|
122
148
|
const {
|
|
123
149
|
isBrokerSyncing,
|
|
124
150
|
brokerSyncError,
|
|
@@ -202,6 +228,16 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
202
228
|
setEditingField,
|
|
203
229
|
isBrokerSyncing,
|
|
204
230
|
brokerSyncError,
|
|
231
|
+
accountSub: account.accountSub,
|
|
232
|
+
accountChoiceIdx: account.accountChoiceIdx,
|
|
233
|
+
setAccountChoiceIdx: account.setAccountChoiceIdx,
|
|
234
|
+
accountSubmitting: account.accountSubmitting,
|
|
235
|
+
accountSubmitError: account.accountSubmitError,
|
|
236
|
+
beginAccountMode: account.beginAccountMode,
|
|
237
|
+
returnToAccountChooser: account.returnToAccountChooser,
|
|
238
|
+
switchToAccountLogin: account.switchToAccountLogin,
|
|
239
|
+
submitAccountField: account.submitAccountField,
|
|
240
|
+
submitAccount: account.submitAccount,
|
|
205
241
|
isFinishing,
|
|
206
242
|
nextStep,
|
|
207
243
|
prevStep,
|
|
@@ -213,7 +249,9 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
213
249
|
|
|
214
250
|
const contentWidth = Math.min(60, termWidth - 4);
|
|
215
251
|
const contentLeft = Math.floor((termWidth - contentWidth) / 2);
|
|
216
|
-
|
|
252
|
+
// The footer is pinned to the last FOOTER_ROWS rows, so steps get whatever is left.
|
|
253
|
+
const contentTop = Math.max(0, Math.min(Math.floor((termHeight - 24) / 2), termHeight - FOOTER_ROWS - 1));
|
|
254
|
+
const contentHeight = Math.max(1, termHeight - contentTop - FOOTER_ROWS);
|
|
217
255
|
const progressDots = ONBOARDING_STEPS.map((_, index) => {
|
|
218
256
|
if (index < stepIdx) return "\u2501";
|
|
219
257
|
if (index === stepIdx) return "\u25cf";
|
|
@@ -227,6 +265,15 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
227
265
|
if (step === "ready") {
|
|
228
266
|
hintText = t("enter to launch");
|
|
229
267
|
}
|
|
268
|
+
if (step === "account") {
|
|
269
|
+
if (account.accountSubmitting) {
|
|
270
|
+
hintText = account.accountSub === "login" ? t("signing in...") : t("creating account...");
|
|
271
|
+
} else if (account.accountSubmitError?.kind === "switch-to-login") {
|
|
272
|
+
hintText = t("enter to log in");
|
|
273
|
+
} else if (account.accountSubmitError) {
|
|
274
|
+
hintText = t("enter to retry");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
230
277
|
if (step === "portfolio" && portfolioSub === "broker-sync") {
|
|
231
278
|
hintText = isBrokerSyncing ? "syncing broker..." : "enter to retry";
|
|
232
279
|
} else if (isFinishing) {
|
|
@@ -248,10 +295,18 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
248
295
|
top={contentTop}
|
|
249
296
|
left={contentLeft}
|
|
250
297
|
width={contentWidth}
|
|
298
|
+
height={contentHeight}
|
|
251
299
|
flexDirection="column"
|
|
252
300
|
>
|
|
253
301
|
{step === "welcome" && <WelcomeStep />}
|
|
254
|
-
{step === "theme" &&
|
|
302
|
+
{step === "theme" && (
|
|
303
|
+
<ThemeStep
|
|
304
|
+
themeIds={themeIds}
|
|
305
|
+
selectedIdx={themeIdx}
|
|
306
|
+
onSelect={setThemeIdx}
|
|
307
|
+
height={contentHeight}
|
|
308
|
+
/>
|
|
309
|
+
)}
|
|
255
310
|
{step === "portfolio" && (
|
|
256
311
|
<PortfolioStep
|
|
257
312
|
sub={portfolioSub}
|
|
@@ -271,18 +326,47 @@ export function OnboardingWizard({ config, pluginRegistry, onComplete }: Onboard
|
|
|
271
326
|
/>
|
|
272
327
|
)}
|
|
273
328
|
{step === "shortcuts" && (
|
|
274
|
-
<ShortcutsStep />
|
|
329
|
+
<ShortcutsStep height={contentHeight} />
|
|
330
|
+
)}
|
|
331
|
+
{step === "account" && (
|
|
332
|
+
<AccountStep
|
|
333
|
+
sub={account.accountSub}
|
|
334
|
+
choiceIdx={account.accountChoiceIdx}
|
|
335
|
+
onChoiceSelect={account.setAccountChoiceIdx}
|
|
336
|
+
onChoiceActivate={activateAccountChoice}
|
|
337
|
+
email={account.accountEmail}
|
|
338
|
+
password={account.accountPassword}
|
|
339
|
+
fieldIdx={account.accountFieldIdx}
|
|
340
|
+
editing={editingField}
|
|
341
|
+
inputRef={inputRef}
|
|
342
|
+
submitting={account.accountSubmitting}
|
|
343
|
+
submitError={account.accountSubmitError}
|
|
344
|
+
validationError={account.accountValidationError}
|
|
345
|
+
outcome={account.accountOutcome}
|
|
346
|
+
onEmailChange={account.setAccountEmail}
|
|
347
|
+
onPasswordChange={account.setAccountPassword}
|
|
348
|
+
height={contentHeight}
|
|
349
|
+
/>
|
|
275
350
|
)}
|
|
276
351
|
{step === "ready" && (
|
|
277
352
|
<ReadyStep
|
|
278
353
|
brokerName={connectedBrokerName ?? null}
|
|
279
354
|
portfolioName={portfolioName}
|
|
280
355
|
brokerSyncSummary={brokerSyncSummary}
|
|
356
|
+
accountOutcome={account.accountOutcome}
|
|
281
357
|
isFinishing={isFinishing}
|
|
282
358
|
error={finishError}
|
|
283
359
|
/>
|
|
284
360
|
)}
|
|
361
|
+
</Box>
|
|
285
362
|
|
|
363
|
+
<Box
|
|
364
|
+
position="absolute"
|
|
365
|
+
top={termHeight - FOOTER_ROWS}
|
|
366
|
+
left={contentLeft}
|
|
367
|
+
width={contentWidth}
|
|
368
|
+
flexDirection="column"
|
|
369
|
+
>
|
|
286
370
|
<Box height={1} />
|
|
287
371
|
<Box height={1} flexDirection="row" width={contentWidth}>
|
|
288
372
|
<Box flexGrow={1}>
|
|
@@ -45,7 +45,7 @@ export function BrokerFieldsPanel({
|
|
|
45
45
|
|
|
46
46
|
if (!isActive && value) {
|
|
47
47
|
return (
|
|
48
|
-
<Box key={field.key} height={1}>
|
|
48
|
+
<Box key={field.key} height={1} flexDirection="row">
|
|
49
49
|
<Text fg={colors.positive}>{"\u2713 "}</Text>
|
|
50
50
|
<Text fg={colors.text}>{`${field.label}: ${formatBrokerFieldValue(field, value)}`}</Text>
|
|
51
51
|
</Box>
|
|
@@ -58,7 +58,7 @@ export function BrokerFieldsPanel({
|
|
|
58
58
|
return (
|
|
59
59
|
<Box key={field.key} flexDirection="column">
|
|
60
60
|
{index > 0 && <Box height={1} />}
|
|
61
|
-
<Box height={1}>
|
|
61
|
+
<Box height={1} flexDirection="row">
|
|
62
62
|
<Text fg={colors.text} attributes={TextAttributes.BOLD}>
|
|
63
63
|
{tf("Step {step}: ", { step: index + 1 })}
|
|
64
64
|
</Text>
|
|
@@ -97,7 +97,7 @@ export function BrokerFieldsPanel({
|
|
|
97
97
|
const selected = optionIdx === brokerSelectIdx;
|
|
98
98
|
return (
|
|
99
99
|
<Box key={option.value} flexDirection="column" backgroundColor={selected ? colors.selected : colors.bg}>
|
|
100
|
-
<Box height={1}>
|
|
100
|
+
<Box height={1} flexDirection="row">
|
|
101
101
|
<Text fg={selected ? colors.selectedText : colors.textDim}>{selected ? "\u25b8 " : " "}</Text>
|
|
102
102
|
<Text
|
|
103
103
|
fg={selected ? colors.text : colors.textDim}
|