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,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box, Text } from "../../../ui";
|
|
2
|
+
import { Box, Text, useUiHost } from "../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
ChoiceDialog,
|
|
5
5
|
Tabs,
|
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
import { CompositeChart } from "../../../components/chart/composite";
|
|
14
14
|
import type { PaneProps, TickerResearchTabProps } from "../../../types/plugin";
|
|
15
15
|
import type { ChartResolution, TimeRange } from "../../../components/chart/core/types";
|
|
16
|
-
import type { ChartSpec, SeriesStyle } from "../../../time-series/types";
|
|
16
|
+
import type { ChartSpec, ResolvedSeries, SeriesStyle } from "../../../time-series/types";
|
|
17
|
+
import { getSupportedChartResolutionsForViewport } from "../../../time-series/resolution";
|
|
17
18
|
import { useResolvedChartSpec } from "../../../time-series/hooks";
|
|
18
19
|
import { useShortcut } from "../../../react/input";
|
|
19
20
|
import { useDialog, useDialogState, type PromptContext } from "../../../ui/dialog";
|
|
@@ -60,13 +61,9 @@ import {
|
|
|
60
61
|
} from "./settings";
|
|
61
62
|
import { resolveChartComposerShortcut } from "./shortcuts";
|
|
62
63
|
import { ChartSeriesQuickAdd } from "./quick-add";
|
|
64
|
+
import { useLiveStreamingSetting } from "../shared/live-streaming";
|
|
63
65
|
|
|
64
66
|
const RANGE_TABS = RANGES.map((range, index) => ({ label: `${index + 1}:${range}`, value: range }));
|
|
65
|
-
const RESOLUTION_TABS = RESOLUTIONS.map((value) => ({ label: value.toUpperCase(), value }));
|
|
66
|
-
const RESOLUTION_TABS_WIDTH = RESOLUTION_TABS.reduce(
|
|
67
|
-
(width, tab) => width + [...tab.label].length + 1,
|
|
68
|
-
0,
|
|
69
|
-
);
|
|
70
67
|
const AUTO_VIEWPORT_DEBOUNCE_MS = 350;
|
|
71
68
|
|
|
72
69
|
interface RuntimeChartViewport {
|
|
@@ -92,6 +89,8 @@ interface ChartComposerSurfaceProps {
|
|
|
92
89
|
onCapture?: (capturing: boolean) => void;
|
|
93
90
|
}
|
|
94
91
|
|
|
92
|
+
const QUICK_ADD_CAPTURE = "quick-add";
|
|
93
|
+
|
|
95
94
|
function isPriceStudyTarget(spec: ChartSpec): boolean {
|
|
96
95
|
return spec.series.some((series) => (
|
|
97
96
|
series.source.kind === "security"
|
|
@@ -110,7 +109,9 @@ function ChartComposerSurface({
|
|
|
110
109
|
}: ChartComposerSurfaceProps) {
|
|
111
110
|
const dialog = useDialog();
|
|
112
111
|
const dispatch = useAppDispatch();
|
|
112
|
+
const isDesktopWeb = useUiHost().kind === "desktop-web";
|
|
113
113
|
const paneId = usePaneInstanceId();
|
|
114
|
+
const liveStreaming = useLiveStreamingSetting();
|
|
114
115
|
const dialogOpen = useDialogState((state) => state.isOpen);
|
|
115
116
|
const authoredViewportKey = useMemo(() => JSON.stringify({
|
|
116
117
|
range: spec.viewport.range,
|
|
@@ -149,19 +150,73 @@ function ChartComposerSurface({
|
|
|
149
150
|
: null,
|
|
150
151
|
requestViewport: activeRuntimeViewport?.requestViewport,
|
|
151
152
|
targetPointCount,
|
|
153
|
+
liveStreaming,
|
|
152
154
|
});
|
|
155
|
+
const availableResolutions = useMemo<ChartResolution[]>(() => {
|
|
156
|
+
if (!resolution.resolutionSupport) {
|
|
157
|
+
if (!resolution.loading) return RESOLUTIONS;
|
|
158
|
+
return spec.viewport.resolution === "auto"
|
|
159
|
+
? ["auto"]
|
|
160
|
+
: ["auto", spec.viewport.resolution];
|
|
161
|
+
}
|
|
162
|
+
const supported = new Set(getSupportedChartResolutionsForViewport(
|
|
163
|
+
spec.viewport.range,
|
|
164
|
+
resolution.resolutionSupport,
|
|
165
|
+
spec.viewport.dateWindow,
|
|
166
|
+
));
|
|
167
|
+
return RESOLUTIONS.filter((value) => value === "auto" || supported.has(value));
|
|
168
|
+
}, [
|
|
169
|
+
resolution.loading,
|
|
170
|
+
resolution.resolutionSupport,
|
|
171
|
+
spec.viewport.dateWindow,
|
|
172
|
+
spec.viewport.range,
|
|
173
|
+
spec.viewport.resolution,
|
|
174
|
+
]);
|
|
175
|
+
const resolutionTabs = useMemo(
|
|
176
|
+
() => availableResolutions.map((value) => ({ label: value.toUpperCase(), value })),
|
|
177
|
+
[availableResolutions],
|
|
178
|
+
);
|
|
179
|
+
const resolutionTabsWidth = useMemo(
|
|
180
|
+
() => resolutionTabs.reduce((total, tab) => total + [...tab.label].length + 1, 0),
|
|
181
|
+
[resolutionTabs],
|
|
182
|
+
);
|
|
153
183
|
const selectedStudies = getSelectedBuiltinStudies(spec);
|
|
154
184
|
const selectedPairStudies = getSelectedPairStudies(spec);
|
|
155
185
|
const inlineStyleTarget = useMemo(() => getChartInlineStyleTarget(spec), [spec]);
|
|
156
186
|
const styles = useMemo(() => getChartInlineStyles(spec), [spec]);
|
|
157
|
-
const styleTabs = useMemo(
|
|
158
|
-
() => styles.map((value) => ({ label: value.toUpperCase(), value })),
|
|
159
|
-
[styles],
|
|
160
|
-
);
|
|
161
187
|
const inlineStyle = inlineStyleTarget?.style ?? "line";
|
|
162
188
|
const inlineStyleLabel = inlineStyleTarget ? chartSeriesLabel(inlineStyleTarget) : "";
|
|
163
189
|
const viewport = resolution.viewport;
|
|
164
190
|
const baseSeriesIds = useMemo(() => new Set(spec.series.map((series) => series.id)), [spec.series]);
|
|
191
|
+
// Hidden series are never loaded, so the resolver has nothing to report for
|
|
192
|
+
// them. Without a placeholder they vanish from the legend entirely and the
|
|
193
|
+
// only way back is the series dialog.
|
|
194
|
+
const legendSeries = useMemo(() => {
|
|
195
|
+
const resolved = resolution.legendSeries ?? [];
|
|
196
|
+
const resolvedIds = new Set(resolved.map((entry) => entry.id));
|
|
197
|
+
const missing = spec.series.filter((entry) => !resolvedIds.has(entry.id));
|
|
198
|
+
if (missing.length === 0) return resolution.legendSeries;
|
|
199
|
+
const bySpecOrder = new Map(spec.series.map((entry, index) => [entry.id, index] as const));
|
|
200
|
+
return [
|
|
201
|
+
...resolved,
|
|
202
|
+
...missing.map((entry): ResolvedSeries => ({
|
|
203
|
+
id: entry.id,
|
|
204
|
+
label: chartSeriesLabel(entry),
|
|
205
|
+
color: entry.color ?? colors.textDim,
|
|
206
|
+
unit: "",
|
|
207
|
+
unitGroup: "unknown",
|
|
208
|
+
nativeFrequency: "daily",
|
|
209
|
+
dataShape: "scalar",
|
|
210
|
+
style: entry.style,
|
|
211
|
+
transform: entry.transform,
|
|
212
|
+
axis: entry.axis === "right" ? "right" : "left",
|
|
213
|
+
panelId: entry.panelId,
|
|
214
|
+
interpolation: entry.interpolation,
|
|
215
|
+
hidden: true,
|
|
216
|
+
points: [],
|
|
217
|
+
})),
|
|
218
|
+
].sort((a, b) => (bySpecOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER) - (bySpecOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER));
|
|
219
|
+
}, [resolution.legendSeries, spec.series]);
|
|
165
220
|
const plottedSeries = useMemo(
|
|
166
221
|
() => projectVisibleChartSeries(
|
|
167
222
|
spec,
|
|
@@ -171,6 +226,8 @@ function ChartComposerSurface({
|
|
|
171
226
|
[resolution.bufferedSeries, resolution.legendSeries, resolution.series, spec],
|
|
172
227
|
);
|
|
173
228
|
const [interactionCaptured, setInteractionCapturedState] = useState(false);
|
|
229
|
+
// Typing in quick-add must not freeze the plot: it only takes the keyboard.
|
|
230
|
+
const [modalCaptured, setModalCaptured] = useState(false);
|
|
174
231
|
const [quickAddWidth, setQuickAddWidth] = useState(14);
|
|
175
232
|
const interactionCaptureRef = useRef(false);
|
|
176
233
|
const interactionCaptureSourcesRef = useRef(new Set<string>());
|
|
@@ -183,6 +240,7 @@ function ChartComposerSurface({
|
|
|
183
240
|
if (captured) sources.add(source);
|
|
184
241
|
else sources.delete(source);
|
|
185
242
|
const next = sources.size > 0;
|
|
243
|
+
setModalCaptured([...sources].some((entry) => entry !== QUICK_ADD_CAPTURE));
|
|
186
244
|
if (interactionCaptureRef.current === next) return;
|
|
187
245
|
interactionCaptureRef.current = next;
|
|
188
246
|
setInteractionCapturedState(next);
|
|
@@ -197,6 +255,8 @@ function ChartComposerSurface({
|
|
|
197
255
|
[setInteractionCaptured],
|
|
198
256
|
);
|
|
199
257
|
const surfaceInteractive = !dialogOpen && !interactionCaptured;
|
|
258
|
+
/** The plot keeps its pointer unless something modal is actually covering it. */
|
|
259
|
+
const surfacePointerInteractive = !dialogOpen && !modalCaptured;
|
|
200
260
|
const shortcutActive = focused && surfaceInteractive;
|
|
201
261
|
const activatePane = useCallback(() => {
|
|
202
262
|
if (!focused) dispatch({ type: "FOCUS_PANE", paneId });
|
|
@@ -257,7 +317,11 @@ function ChartComposerSurface({
|
|
|
257
317
|
useRemoteUiNode({
|
|
258
318
|
role: "chart-data",
|
|
259
319
|
label: "Rendered chart composer data",
|
|
260
|
-
metadata: chartComposerSemanticMetadata(
|
|
320
|
+
metadata: chartComposerSemanticMetadata(
|
|
321
|
+
spec,
|
|
322
|
+
resolution,
|
|
323
|
+
activeRuntimeViewport?.requestViewport,
|
|
324
|
+
),
|
|
261
325
|
});
|
|
262
326
|
|
|
263
327
|
const openSeriesEditor = useCallback(async () => {
|
|
@@ -310,6 +374,15 @@ function ChartComposerSurface({
|
|
|
310
374
|
const setResolution = useCallback((next: ChartResolution) => {
|
|
311
375
|
setSpec({ ...spec, viewport: { ...spec.viewport, resolution: next } });
|
|
312
376
|
}, [setSpec, spec]);
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
if (
|
|
379
|
+
spec.viewport.resolution === "auto"
|
|
380
|
+
|| availableResolutions.includes(spec.viewport.resolution)
|
|
381
|
+
) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
setResolution("auto");
|
|
385
|
+
}, [availableResolutions, setResolution, spec.viewport.resolution]);
|
|
313
386
|
const setInlineStyle = useCallback((style: SeriesStyle) => {
|
|
314
387
|
if (!inlineStyleTarget || !styles.includes(style)) return;
|
|
315
388
|
setSpec({
|
|
@@ -352,7 +425,7 @@ function ChartComposerSurface({
|
|
|
352
425
|
{...context}
|
|
353
426
|
title="Chart Resolution"
|
|
354
427
|
selectedChoiceId={spec.viewport.resolution}
|
|
355
|
-
choices={
|
|
428
|
+
choices={availableResolutions.map((value) => ({
|
|
356
429
|
id: value,
|
|
357
430
|
label: value.toUpperCase(),
|
|
358
431
|
description: value === "auto"
|
|
@@ -362,11 +435,17 @@ function ChartComposerSurface({
|
|
|
362
435
|
/>
|
|
363
436
|
),
|
|
364
437
|
}).catch(() => "");
|
|
365
|
-
if (
|
|
438
|
+
if (availableResolutions.includes(next as ChartResolution)) setResolution(next as ChartResolution);
|
|
366
439
|
} finally {
|
|
367
440
|
setInteractionCaptured("prompt", false);
|
|
368
441
|
}
|
|
369
|
-
}, [
|
|
442
|
+
}, [
|
|
443
|
+
availableResolutions,
|
|
444
|
+
dialog,
|
|
445
|
+
setInteractionCaptured,
|
|
446
|
+
setResolution,
|
|
447
|
+
spec.viewport.resolution,
|
|
448
|
+
]);
|
|
370
449
|
const openModePicker = useCallback(async () => {
|
|
371
450
|
if (styles.length === 0) return;
|
|
372
451
|
setInteractionCaptured("prompt", true);
|
|
@@ -400,7 +479,7 @@ function ChartComposerSurface({
|
|
|
400
479
|
[baseSeriesIds, spec],
|
|
401
480
|
);
|
|
402
481
|
const handleQuickAddActiveChange = useCallback(
|
|
403
|
-
(active: boolean) => setInteractionCaptured(
|
|
482
|
+
(active: boolean) => setInteractionCaptured(QUICK_ADD_CAPTURE, active),
|
|
404
483
|
[setInteractionCaptured],
|
|
405
484
|
);
|
|
406
485
|
const openIndicators = useCallback((event?: PaneFooterPressEvent) => {
|
|
@@ -505,7 +584,15 @@ function ChartComposerSurface({
|
|
|
505
584
|
return (
|
|
506
585
|
<Box flexDirection="column" width={width} height={height} backgroundColor={colors.panel}>
|
|
507
586
|
<Box flexDirection="row" height={1} paddingX={1} gap={0} overflow="hidden">
|
|
508
|
-
<Box
|
|
587
|
+
<Box
|
|
588
|
+
flexShrink={0}
|
|
589
|
+
height={1}
|
|
590
|
+
maxWidth={52}
|
|
591
|
+
overflow="hidden"
|
|
592
|
+
// Desktop tabs are laid out in pixels, so a cell budget clips them
|
|
593
|
+
// while the row still has room to spare.
|
|
594
|
+
style={isDesktopWeb ? { maxWidth: "none", width: "auto" } : undefined}
|
|
595
|
+
>
|
|
509
596
|
<Tabs
|
|
510
597
|
tabs={RANGE_TABS}
|
|
511
598
|
activeValue={spec.viewport.dateWindow ? null : spec.viewport.range}
|
|
@@ -522,13 +609,14 @@ function ChartComposerSurface({
|
|
|
522
609
|
<Box
|
|
523
610
|
flexShrink={1}
|
|
524
611
|
minWidth={0}
|
|
525
|
-
width={
|
|
612
|
+
width={resolutionTabsWidth}
|
|
526
613
|
height={1}
|
|
527
614
|
overflow="hidden"
|
|
615
|
+
style={isDesktopWeb ? { width: "auto", flexShrink: 0 } : undefined}
|
|
528
616
|
data-gloom-role="chart-resolution-control"
|
|
529
617
|
>
|
|
530
618
|
<Tabs
|
|
531
|
-
tabs={
|
|
619
|
+
tabs={resolutionTabs}
|
|
532
620
|
activeValue={spec.viewport.resolution}
|
|
533
621
|
onSelect={(value) => setResolution(value as ChartResolution)}
|
|
534
622
|
compact
|
|
@@ -538,29 +626,6 @@ function ChartComposerSurface({
|
|
|
538
626
|
keyboardNavigation={false}
|
|
539
627
|
/>
|
|
540
628
|
</Box>
|
|
541
|
-
{width >= 132 && inlineStyleTarget && (
|
|
542
|
-
<Box
|
|
543
|
-
flexDirection="row"
|
|
544
|
-
flexShrink={0}
|
|
545
|
-
maxWidth={64}
|
|
546
|
-
height={1}
|
|
547
|
-
overflow="hidden"
|
|
548
|
-
data-gloom-role="chart-inline-style"
|
|
549
|
-
data-gloom-label={`${inlineStyleLabel} style`}
|
|
550
|
-
>
|
|
551
|
-
<Text fg={colors.textDim}>{`${inlineStyleLabel}: `}</Text>
|
|
552
|
-
<Tabs
|
|
553
|
-
tabs={styleTabs}
|
|
554
|
-
activeValue={inlineStyle}
|
|
555
|
-
onSelect={(value) => setInlineStyle(value as SeriesStyle)}
|
|
556
|
-
compact
|
|
557
|
-
dense
|
|
558
|
-
variant="bare"
|
|
559
|
-
focused={focused}
|
|
560
|
-
keyboardNavigation={false}
|
|
561
|
-
/>
|
|
562
|
-
</Box>
|
|
563
|
-
)}
|
|
564
629
|
</Box>
|
|
565
630
|
<MultiSelectDialogButton
|
|
566
631
|
ref={indicatorsDialogRef}
|
|
@@ -594,7 +659,7 @@ function ChartComposerSurface({
|
|
|
594
659
|
<Box flexGrow={1} minHeight={4}>
|
|
595
660
|
<CompositeChart
|
|
596
661
|
series={plottedSeries}
|
|
597
|
-
legendSeries={
|
|
662
|
+
legendSeries={legendSeries}
|
|
598
663
|
timelineSeries={resolution.timelineSeries}
|
|
599
664
|
panels={spec.panels}
|
|
600
665
|
viewport={viewport}
|
|
@@ -602,7 +667,9 @@ function ChartComposerSurface({
|
|
|
602
667
|
width={Math.max(1, width)}
|
|
603
668
|
height={Math.max(4, height - 1)}
|
|
604
669
|
focused={focused}
|
|
605
|
-
interactive={
|
|
670
|
+
interactive={surfacePointerInteractive}
|
|
671
|
+
allowHistoricalBackfill
|
|
672
|
+
showLatestChangePercent={!spec.viewport.dateWindow && spec.viewport.range === "1D"}
|
|
606
673
|
onViewportChange={handleChartViewportChange}
|
|
607
674
|
onActivate={activatePane}
|
|
608
675
|
onToggleSeries={toggleSeries}
|
|
@@ -34,6 +34,19 @@ interface ClosestElementLike {
|
|
|
34
34
|
} | null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Blurs the field itself rather than going through the imperative handle, which
|
|
39
|
+
* only reaches the element it still owns. A no-op unless this widget truly holds
|
|
40
|
+
* the document focus, so a click inside a dialog leaves that dialog alone.
|
|
41
|
+
*/
|
|
42
|
+
export function releaseQuickAddFocus(quickAddId: string): void {
|
|
43
|
+
const active = (globalThis as {
|
|
44
|
+
document?: { activeElement?: { blur?: () => void } | null };
|
|
45
|
+
}).document?.activeElement;
|
|
46
|
+
if (!active || !isChartQuickAddMouseTarget(active, quickAddId)) return;
|
|
47
|
+
active.blur?.();
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
export function isChartQuickAddMouseTarget(target: unknown, quickAddId: string): boolean {
|
|
38
51
|
if (!target || typeof target !== "object") return false;
|
|
39
52
|
const candidate = target as Partial<ClosestElementLike> & {
|
|
@@ -197,16 +210,18 @@ export function ChartSeriesQuickAdd({
|
|
|
197
210
|
}, [active, focused]);
|
|
198
211
|
|
|
199
212
|
useEffect(() => {
|
|
200
|
-
if (ui.kind !== "desktop-web"
|
|
213
|
+
if (ui.kind !== "desktop-web") return;
|
|
201
214
|
|
|
215
|
+
// Watch regardless of what state believes: the field can hold the document
|
|
216
|
+
// focus after the state that tracks it has already moved on.
|
|
202
217
|
const handleOutsideMouseDown = (event: globalThis.MouseEvent) => {
|
|
203
218
|
if (isChartQuickAddMouseTarget(event.target, quickAddId)) return;
|
|
204
|
-
|
|
219
|
+
releaseQuickAddFocus(quickAddId);
|
|
205
220
|
};
|
|
206
221
|
|
|
207
222
|
document.addEventListener("mousedown", handleOutsideMouseDown, true);
|
|
208
223
|
return () => document.removeEventListener("mousedown", handleOutsideMouseDown, true);
|
|
209
|
-
}, [
|
|
224
|
+
}, [quickAddId, ui.kind]);
|
|
210
225
|
|
|
211
226
|
const cancelPendingBlur = useCallback(() => {
|
|
212
227
|
if (blurTimerRef.current === null) return;
|
|
@@ -216,10 +231,17 @@ export function ChartSeriesQuickAdd({
|
|
|
216
231
|
|
|
217
232
|
useEffect(() => cancelPendingBlur, [cancelPendingBlur]);
|
|
218
233
|
|
|
234
|
+
// Writing the field programmatically comes back through onChange, which is
|
|
235
|
+
// otherwise indistinguishable from a keystroke and re-arms what just closed.
|
|
236
|
+
const clearingRef = useRef(false);
|
|
219
237
|
const clearInput = useCallback(() => {
|
|
238
|
+
clearingRef.current = true;
|
|
220
239
|
setQuery("");
|
|
221
240
|
inputRef.current?.editBuffer.setText?.("");
|
|
222
241
|
inputRef.current?.setCursorOffset?.(0);
|
|
242
|
+
queueMicrotask(() => {
|
|
243
|
+
clearingRef.current = false;
|
|
244
|
+
});
|
|
223
245
|
}, []);
|
|
224
246
|
|
|
225
247
|
const focusInput = useCallback(() => {
|
|
@@ -320,6 +342,7 @@ export function ChartSeriesQuickAdd({
|
|
|
320
342
|
zIndex={30}
|
|
321
343
|
data-gloom-role="chart-series-quick-add"
|
|
322
344
|
data-gloom-chart-quick-add={quickAddId}
|
|
345
|
+
data-gloom-focus-scope="chart-series-quick-add"
|
|
323
346
|
>
|
|
324
347
|
<InlineQuickAddRow
|
|
325
348
|
value={query}
|
|
@@ -335,6 +358,7 @@ export function ChartSeriesQuickAdd({
|
|
|
335
358
|
onChange={(value) => {
|
|
336
359
|
setQuery(value);
|
|
337
360
|
setError(null);
|
|
361
|
+
if (clearingRef.current) return;
|
|
338
362
|
if (!active) setActive(true);
|
|
339
363
|
if (!inputFocused) setInputFocused(true);
|
|
340
364
|
}}
|
|
@@ -15,6 +15,15 @@ function pointEvidence(point: TimeSeriesPoint | undefined) {
|
|
|
15
15
|
: null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function viewportEvidence(viewport: UseChartResolutionResult["viewport"]) {
|
|
19
|
+
if (!viewport) return null;
|
|
20
|
+
return {
|
|
21
|
+
start: viewport.start.toISOString(),
|
|
22
|
+
end: viewport.end.toISOString(),
|
|
23
|
+
spanMs: viewport.end.getTime() - viewport.start.getTime(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
function sourceEvidence(series: ChartSeriesSpec) {
|
|
19
28
|
return series.source.kind === "security"
|
|
20
29
|
? {
|
|
@@ -36,6 +45,7 @@ function sourceEvidence(series: ChartSeriesSpec) {
|
|
|
36
45
|
export function chartComposerSemanticMetadata(
|
|
37
46
|
spec: ChartSpec,
|
|
38
47
|
resolution: UseChartResolutionResult,
|
|
48
|
+
runtimeViewport?: { start: Date; end: Date } | null,
|
|
39
49
|
): Record<string, unknown> {
|
|
40
50
|
const resolvedById = new Map(resolution.series.map((series) => [series.id, series] as const));
|
|
41
51
|
const symbols = [...new Set(spec.series.flatMap((series) => (
|
|
@@ -54,6 +64,7 @@ export function chartComposerSemanticMetadata(
|
|
|
54
64
|
panelId: series.panelId,
|
|
55
65
|
visible: series.visible !== false,
|
|
56
66
|
pointCount: resolved?.points.length ?? 0,
|
|
67
|
+
latestChangePercent: resolved?.latestChangePercent ?? null,
|
|
57
68
|
first: pointEvidence(resolved?.points[0]),
|
|
58
69
|
last: pointEvidence(resolved?.points.at(-1)),
|
|
59
70
|
};
|
|
@@ -69,7 +80,16 @@ export function chartComposerSemanticMetadata(
|
|
|
69
80
|
loading: resolution.loading,
|
|
70
81
|
errors: resolution.errors,
|
|
71
82
|
warnings: resolution.warnings,
|
|
83
|
+
resolutionSupport: resolution.resolutionSupport ?? null,
|
|
84
|
+
viewport: viewportEvidence(runtimeViewport ?? resolution.viewport),
|
|
85
|
+
authoredViewport: viewportEvidence(resolution.viewport),
|
|
72
86
|
baseSeries,
|
|
87
|
+
bufferedSeries: (resolution.bufferedSeries ?? []).map((series) => ({
|
|
88
|
+
id: series.id,
|
|
89
|
+
pointCount: series.points.length,
|
|
90
|
+
first: pointEvidence(series.points[0]),
|
|
91
|
+
last: pointEvidence(series.points.at(-1)),
|
|
92
|
+
})),
|
|
73
93
|
resolvedSeries: resolution.series.map((series) => ({
|
|
74
94
|
id: series.id,
|
|
75
95
|
label: series.label,
|
|
@@ -77,6 +97,7 @@ export function chartComposerSemanticMetadata(
|
|
|
77
97
|
transform: series.transform,
|
|
78
98
|
axis: series.axis,
|
|
79
99
|
panelId: series.panelId,
|
|
100
|
+
timeBasis: series.timeBasis ?? null,
|
|
80
101
|
pointCount: series.points.length,
|
|
81
102
|
first: pointEvidence(series.points[0]),
|
|
82
103
|
last: pointEvidence(series.points.at(-1)),
|
|
@@ -22,6 +22,9 @@ export interface ChatSessionUser {
|
|
|
22
22
|
username: string;
|
|
23
23
|
emailVerified: boolean;
|
|
24
24
|
plan?: "free" | "pro";
|
|
25
|
+
/** Cached so a cold start renders the right entitlement before /auth/get-session lands. */
|
|
26
|
+
trialEndsAt?: string | null;
|
|
27
|
+
effectivePlan?: "free" | "pro";
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export function readChatSessionState(
|
|
@@ -42,6 +45,8 @@ export function normalizeSessionUser(user: PersistedAuthUser | null | undefined)
|
|
|
42
45
|
username: user.username ?? user.name ?? "account",
|
|
43
46
|
emailVerified: user.emailVerified === true,
|
|
44
47
|
plan: user.plan,
|
|
48
|
+
trialEndsAt: user.trialEndsAt ?? undefined,
|
|
49
|
+
effectivePlan: user.effectivePlan,
|
|
45
50
|
}
|
|
46
51
|
: null;
|
|
47
52
|
}
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
import { registerTwitterFeedFeature } from "../cloud-tweets/registration";
|
|
22
22
|
import { composeBuiltinPlugin, type PluginModule } from "../plugin-module";
|
|
23
23
|
import { registerCloudAuthCommands } from "./auth-commands";
|
|
24
|
+
import { registerCloudUpgradeCommand } from "./upgrade-command";
|
|
25
|
+
import { CloudUpgradeStatusWidget } from "./upgrade-status-widget";
|
|
24
26
|
|
|
25
27
|
interface GloomberbCloudPluginComponents {
|
|
26
28
|
ChatPane: (props: PaneProps) => ReactNode;
|
|
@@ -137,7 +139,13 @@ const accountModule: PluginModule = {
|
|
|
137
139
|
shortcut: { prefix: "ACM" },
|
|
138
140
|
createInstance: () => ({ placement: "floating" }),
|
|
139
141
|
}],
|
|
140
|
-
|
|
142
|
+
slots: {
|
|
143
|
+
"status:widget": () => <CloudUpgradeStatusWidget />,
|
|
144
|
+
},
|
|
145
|
+
setup: (ctx) => {
|
|
146
|
+
registerCloudAuthCommands(ctx);
|
|
147
|
+
registerCloudUpgradeCommand(ctx);
|
|
148
|
+
},
|
|
141
149
|
};
|
|
142
150
|
|
|
143
151
|
const buildoutModule: PluginModule = {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import type { GloomPluginContext } from "../../../types/plugin";
|
|
3
|
+
import { requestAccountManagementTab } from "../account-management/navigation";
|
|
4
|
+
import { openCloudUpgradeUrl } from "../shared/cloud-upgrade";
|
|
5
|
+
import { resolvePlanAccess } from "../shared/plan-access";
|
|
6
|
+
|
|
7
|
+
export function registerCloudUpgradeCommand(ctx: GloomPluginContext): void {
|
|
8
|
+
ctx.registerCommand({
|
|
9
|
+
id: "cloud-upgrade",
|
|
10
|
+
label: "Upgrade to Pro",
|
|
11
|
+
description: "Real-time market data, real-time news, and X data",
|
|
12
|
+
keywords: ["upgrade", "pro", "pricing", "plan", "billing", "subscribe", "trial", "real-time"],
|
|
13
|
+
category: "config",
|
|
14
|
+
shortcut: "UPGRADE",
|
|
15
|
+
// Paying subscribers have nothing to upgrade to; trial accounts still do.
|
|
16
|
+
hidden: () => resolvePlanAccess(apiClient.getCurrentUser()).isPayingPro,
|
|
17
|
+
execute: () => {
|
|
18
|
+
if (!apiClient.getSessionToken()) {
|
|
19
|
+
ctx.openCommandBar("Sign Up");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (openCloudUpgradeUrl()) return;
|
|
23
|
+
// No renderer-backed opener yet: fall back to the in-app billing surface.
|
|
24
|
+
requestAccountManagementTab("pro");
|
|
25
|
+
ctx.showPane("account-management");
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { apiClient } from "../../../api-client";
|
|
3
|
+
import { t, tf } from "../../../i18n";
|
|
4
|
+
import { useAppLanguage } from "../../../i18n/react";
|
|
5
|
+
import { useAppSelector } from "../../../state/app/context";
|
|
6
|
+
import { colors, hoverBg } from "../../../theme/colors";
|
|
7
|
+
import { Box, Text } from "../../../ui";
|
|
8
|
+
import { chatController, type ChatController } from "../chat/controller";
|
|
9
|
+
import { useCloudPlanAction, useCloudUpgradeAction } from "../shared/cloud-upgrade";
|
|
10
|
+
import { resolvePlanAccess } from "../shared/plan-access";
|
|
11
|
+
|
|
12
|
+
interface CloudUpgradeStatusWidgetProps {
|
|
13
|
+
controller?: Pick<ChatController, "getSnapshot" | "subscribe">;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Global entitlement status: the trial countdown while Pro is on loan, or the
|
|
18
|
+
* delay free accounts get on cloud data. Signed-out users already have sign-in
|
|
19
|
+
* affordances next to it, and paying subscribers have nothing to report.
|
|
20
|
+
*/
|
|
21
|
+
export function CloudUpgradeStatusWidget({ controller = chatController }: CloudUpgradeStatusWidgetProps) {
|
|
22
|
+
useAppLanguage();
|
|
23
|
+
const cloudPluginDisabled = useAppSelector((state) => state.config.disabledPlugins).includes("gloomberb-cloud");
|
|
24
|
+
const openUpgrade = useCloudUpgradeAction();
|
|
25
|
+
const openPlan = useCloudPlanAction();
|
|
26
|
+
const [access, setAccess] = useState(() => resolvePlanAccess(apiClient.getCurrentUser()));
|
|
27
|
+
const [hovered, setHovered] = useState(false);
|
|
28
|
+
|
|
29
|
+
useEffect(
|
|
30
|
+
// The chat widget already drives session refreshes; this only follows them.
|
|
31
|
+
() => controller.subscribe(() => setAccess(resolvePlanAccess(apiClient.getCurrentUser()))),
|
|
32
|
+
[controller],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (cloudPluginDisabled || !access.signedIn || access.isPayingPro) return null;
|
|
36
|
+
|
|
37
|
+
const trial = access.isTrialActive;
|
|
38
|
+
const label = trial
|
|
39
|
+
? tf("Pro trial {days}d", { days: access.trialDaysLeft })
|
|
40
|
+
: t("delayed data · upgrade");
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Box
|
|
44
|
+
flexDirection="row"
|
|
45
|
+
paddingRight={1}
|
|
46
|
+
backgroundColor={hovered ? hoverBg() : undefined}
|
|
47
|
+
onMouseOver={() => setHovered((current) => (current ? current : true))}
|
|
48
|
+
onMouseOut={() => setHovered((current) => (current ? false : current))}
|
|
49
|
+
onMouseDown={trial ? openPlan : openUpgrade}
|
|
50
|
+
>
|
|
51
|
+
<Text fg={trial ? colors.positive : colors.warning}>{label}</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -153,6 +153,40 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
153
153
|
/>
|
|
154
154
|
</HelpSection>
|
|
155
155
|
|
|
156
|
+
<HelpSection title="Charts">
|
|
157
|
+
<ShortcutRow
|
|
158
|
+
badges={["Drag", "Scroll"]}
|
|
159
|
+
description="Pan the chart through time."
|
|
160
|
+
/>
|
|
161
|
+
<ShortcutRow
|
|
162
|
+
badges={["Shift+Z", "Shift+Drag"]}
|
|
163
|
+
description="Pick the zoom tool, then drag a time range to zoom into."
|
|
164
|
+
/>
|
|
165
|
+
<ShortcutRow
|
|
166
|
+
badges={["Shift+M", "Alt+Drag"]}
|
|
167
|
+
description="Pick the ruler, then drag to measure change, percent, bars, and elapsed time."
|
|
168
|
+
/>
|
|
169
|
+
<ShortcutRow
|
|
170
|
+
badges={["Shift+D", "Shift+P"]}
|
|
171
|
+
description="Draw a trend line or a freehand shape. Grab a line's end to reshape it, drag its middle to move it."
|
|
172
|
+
/>
|
|
173
|
+
<ShortcutRow
|
|
174
|
+
badges={["c", "Backspace"]}
|
|
175
|
+
description="Cycle the drawing colour, or delete the selected drawing."
|
|
176
|
+
/>
|
|
177
|
+
<ShortcutRow
|
|
178
|
+
badges={["Ctrl+Scroll"]}
|
|
179
|
+
description="Zoom around the pointer."
|
|
180
|
+
/>
|
|
181
|
+
<ShortcutRow
|
|
182
|
+
badges={["+/-", "0"]}
|
|
183
|
+
description="Zoom the focused chart in or out, or reset it."
|
|
184
|
+
/>
|
|
185
|
+
<Text fg={colors.textDim} wrapText>
|
|
186
|
+
{t("The tool icons sit over the top-left corner of the chart. Most terminals keep shift-drag and option-drag for their own text selection, so pick the tool there instead.")}
|
|
187
|
+
</Text>
|
|
188
|
+
</HelpSection>
|
|
189
|
+
|
|
156
190
|
<HelpSection title="Global Keys">
|
|
157
191
|
<ShortcutRow
|
|
158
192
|
badges={["Tab", "Shift+Tab"]}
|
|
@@ -327,6 +361,10 @@ function HelpPane({ focused, width, height }: PaneProps) {
|
|
|
327
361
|
badges={["DES", "<ticker>"]}
|
|
328
362
|
description="Open security details for a specific ticker."
|
|
329
363
|
/>
|
|
364
|
+
<ShortcutRow
|
|
365
|
+
badges={["UPGRADE"]}
|
|
366
|
+
description="Go Pro for real-time data at gloom.sh/cloud, free for 7 days."
|
|
367
|
+
/>
|
|
330
368
|
<ShortcutRow
|
|
331
369
|
badges={["Up/Down", "Ctrl+P/N"]}
|
|
332
370
|
description="Move through command bar results."
|