gloomberb 0.7.2 → 0.7.3
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, ScrollBox, Text } from "../../../ui";
|
|
1
|
+
import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
|
|
2
2
|
import { TextAttributes } from "../../../ui";
|
|
3
3
|
import { useViewport } from "../../../react/input";
|
|
4
4
|
import { useFxRatesMap } from "../../../market-data/hooks";
|
|
@@ -32,7 +32,6 @@ const STAT_COLUMN_GAP = 2;
|
|
|
32
32
|
const STAT_LABEL_WIDTH = 12;
|
|
33
33
|
const BOOK_LABEL_WIDTH = 4;
|
|
34
34
|
const RANGE_ENDPOINT_WIDTH = 11;
|
|
35
|
-
const RANGE_MAX_WIDTH = 42;
|
|
36
35
|
const POSITION_COLUMN_GAP = 1;
|
|
37
36
|
|
|
38
37
|
function CompactRangeBar({
|
|
@@ -71,21 +70,23 @@ function CompactRangeBar({
|
|
|
71
70
|
return (
|
|
72
71
|
<Box flexDirection="column" width={width} flexShrink={0}>
|
|
73
72
|
<Box flexDirection="row" height={1}>
|
|
74
|
-
<
|
|
73
|
+
<Box width={labelWidth} overflow="hidden">
|
|
74
|
+
<Text fg={colors.textDim}>{label}</Text>
|
|
75
|
+
</Box>
|
|
75
76
|
<Text fg={markerColor}>{pctLabel}</Text>
|
|
76
77
|
</Box>
|
|
77
78
|
<Box flexDirection="row" height={1}>
|
|
78
|
-
<
|
|
79
|
-
{
|
|
80
|
-
</
|
|
79
|
+
<Box width={endpointWidth} overflow="hidden">
|
|
80
|
+
<Text fg={colors.textDim}>{lowText}</Text>
|
|
81
|
+
</Box>
|
|
81
82
|
<Box marginLeft={1} marginRight={1} width={barWidth} flexDirection="row">
|
|
82
83
|
<Text fg={colors.border}>{"─".repeat(markerIndex)}</Text>
|
|
83
84
|
<Text fg={markerColor}>{"●"}</Text>
|
|
84
85
|
<Text fg={colors.border}>{"─".repeat(Math.max(0, barWidth - markerIndex - 1))}</Text>
|
|
85
86
|
</Box>
|
|
86
|
-
<
|
|
87
|
-
{
|
|
88
|
-
</
|
|
87
|
+
<Box flexDirection="row" width={endpointWidth} justifyContent="flex-end" overflow="hidden">
|
|
88
|
+
<Text fg={colors.textDim}>{highText}</Text>
|
|
89
|
+
</Box>
|
|
89
90
|
</Box>
|
|
90
91
|
</Box>
|
|
91
92
|
);
|
|
@@ -164,7 +165,10 @@ interface PositionColumn {
|
|
|
164
165
|
|
|
165
166
|
function StatGrid({ fields, width }: { fields: StatField[]; width: number }) {
|
|
166
167
|
const columnCount = width >= 58 ? 2 : 1;
|
|
167
|
-
const
|
|
168
|
+
const availableWidth = width - STAT_COLUMN_GAP * (columnCount - 1);
|
|
169
|
+
const baseColWidth = Math.floor(availableWidth / columnCount);
|
|
170
|
+
const extraWidth = availableWidth - baseColWidth * columnCount;
|
|
171
|
+
const colWidths = Array.from({ length: columnCount }, (_, index) => baseColWidth + (index === columnCount - 1 ? extraWidth : 0));
|
|
168
172
|
const rows: Array<Array<StatField | null>> = [];
|
|
169
173
|
for (let i = 0; i < fields.length; i += columnCount) {
|
|
170
174
|
rows.push(Array.from({ length: columnCount }, (_, offset) => fields[i + offset] ?? null));
|
|
@@ -175,6 +179,7 @@ function StatGrid({ fields, width }: { fields: StatField[]; width: number }) {
|
|
|
175
179
|
{rows.map((row, i) => (
|
|
176
180
|
<Box key={i} flexDirection="row" height={1}>
|
|
177
181
|
{row.map((field, j) => {
|
|
182
|
+
const colWidth = colWidths[j] ?? baseColWidth;
|
|
178
183
|
if (!field) {
|
|
179
184
|
return (
|
|
180
185
|
<Box key={j} flexDirection="row">
|
|
@@ -189,8 +194,12 @@ function StatGrid({ fields, width }: { fields: StatField[]; width: number }) {
|
|
|
189
194
|
<Box key={j} flexDirection="row">
|
|
190
195
|
{j > 0 && <Box width={STAT_COLUMN_GAP} />}
|
|
191
196
|
<Box width={colWidth} flexDirection="row">
|
|
192
|
-
<
|
|
193
|
-
|
|
197
|
+
<Box width={labelWidth} overflow="hidden">
|
|
198
|
+
<Text fg={colors.textDim}>{field.label}</Text>
|
|
199
|
+
</Box>
|
|
200
|
+
<Box flexDirection="row" width={valueWidth} justifyContent="flex-end" overflow="hidden">
|
|
201
|
+
<Text fg={field.valueColor ?? colors.text}>{field.value}</Text>
|
|
202
|
+
</Box>
|
|
194
203
|
</Box>
|
|
195
204
|
</Box>
|
|
196
205
|
);
|
|
@@ -300,6 +309,7 @@ export function OverviewTab({
|
|
|
300
309
|
const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
|
|
301
310
|
const exchangeRatesState = useAppSelector((state) => state.exchangeRates);
|
|
302
311
|
const { width: termWidth } = useViewport();
|
|
312
|
+
const { fractionalViewport = false } = useUiCapabilities();
|
|
303
313
|
|
|
304
314
|
if (!ticker) return <EmptyState title="No ticker selected." />;
|
|
305
315
|
|
|
@@ -336,7 +346,7 @@ export function OverviewTab({
|
|
|
336
346
|
routingVenue && routingVenue !== listingVenue ? `route ${routingVenue}` : "",
|
|
337
347
|
].filter((part) => part.length > 0);
|
|
338
348
|
|
|
339
|
-
const contentWidth = Math.max((width || Math.floor(termWidth * 0.5)) - 4, 20);
|
|
349
|
+
const contentWidth = Math.max((width || Math.floor(termWidth * 0.5)) - (fractionalViewport ? 2 : 4), 20);
|
|
340
350
|
const chartWidth = contentWidth;
|
|
341
351
|
const hasHistory = (financials?.priceHistory?.length ?? 0) > 2;
|
|
342
352
|
const hasBidAsk = quote?.bid != null || quote?.ask != null;
|
|
@@ -347,8 +357,8 @@ export function OverviewTab({
|
|
|
347
357
|
const hasYearRange = quote?.low52w != null && quote?.high52w != null && quote.high52w > quote.low52w;
|
|
348
358
|
const rangeInline = contentWidth >= 70 && hasDayRange && hasYearRange;
|
|
349
359
|
const rangeWidth = rangeInline
|
|
350
|
-
? Math.
|
|
351
|
-
:
|
|
360
|
+
? Math.floor((contentWidth - 2) / 2)
|
|
361
|
+
: contentWidth;
|
|
352
362
|
const rangeMarkerColor = quote ? priceColor(quote.change) : colors.textDim;
|
|
353
363
|
|
|
354
364
|
const stats: StatField[] = [];
|
|
@@ -154,12 +154,6 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
154
154
|
});
|
|
155
155
|
}, [resolvedTabId, visibleTabIds]);
|
|
156
156
|
|
|
157
|
-
useEffect(() => {
|
|
158
|
-
if (resolvedTabId !== activeTabId) {
|
|
159
|
-
setActiveTabId(resolvedTabId);
|
|
160
|
-
}
|
|
161
|
-
}, [activeTabId, resolvedTabId, setActiveTabId]);
|
|
162
|
-
|
|
163
157
|
const handleKeyboard = useCallback((event: { name?: string }) => {
|
|
164
158
|
const currentState = stateRef.current;
|
|
165
159
|
if (!currentState.focused || currentState.pluginCaptured) return;
|
|
@@ -1,97 +1,476 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { Box, Text, useUiCapabilities } from "../../../ui";
|
|
2
3
|
import { TextAttributes } from "../../../ui";
|
|
3
4
|
import type { PaneProps } from "../../../types/plugin";
|
|
4
5
|
import type { Quote } from "../../../types/financials";
|
|
6
|
+
import type { TickerFinancials } from "../../../types/financials";
|
|
7
|
+
import type { TickerRecord } from "../../../types/ticker";
|
|
8
|
+
import type { QuoteSubscriptionTarget } from "../../../types/data-provider";
|
|
5
9
|
import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
|
|
6
|
-
import {
|
|
10
|
+
import { useTickerFinancials } from "../../../market-data/hooks";
|
|
11
|
+
import { useAppSelector, usePaneInstance, usePaneTicker } from "../../../state/app-context";
|
|
7
12
|
import { useQuoteStreaming } from "../../../state/use-quote-streaming";
|
|
13
|
+
import { usePluginAppActions, usePluginTickerActions } from "../../plugin-runtime";
|
|
14
|
+
import { useDoubleClickActivation } from "../../../components/use-double-click-activation";
|
|
8
15
|
import { colors, priceColor } from "../../../theme/colors";
|
|
9
16
|
import { formatPercentRaw } from "../../../utils/format";
|
|
10
17
|
import { formatMarketPriceWithCurrency, formatSignedMarketPrice } from "../../../utils/market-format";
|
|
11
18
|
import { getActiveQuoteDisplay } from "../../../utils/market-status";
|
|
12
|
-
import { EmptyState
|
|
19
|
+
import { EmptyState } from "../../../components";
|
|
20
|
+
import {
|
|
21
|
+
PriceAreaSparklineBackground,
|
|
22
|
+
PriceSparkline,
|
|
23
|
+
resolvePriceSparklineRange,
|
|
24
|
+
type PriceSparklinePeriod,
|
|
25
|
+
type PriceSparklineTrend,
|
|
26
|
+
} from "../../../components/price-sparkline-view";
|
|
27
|
+
import { getQuoteMonitorPaneSettings } from "./settings";
|
|
28
|
+
import { useShortcut } from "../../../react/input";
|
|
13
29
|
|
|
14
30
|
function getQuoteMonitorDisplay(quote: Quote | null | undefined) {
|
|
15
31
|
return getActiveQuoteDisplay(quote);
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
function chunkSymbols(symbols: string[], columns: number): string[][] {
|
|
35
|
+
const rows: string[][] = [];
|
|
36
|
+
for (let index = 0; index < symbols.length; index += columns) {
|
|
37
|
+
rows.push(symbols.slice(index, index + columns));
|
|
38
|
+
}
|
|
39
|
+
return rows;
|
|
40
|
+
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
42
|
+
function resolveGridColumnCount(symbolCount: number, width: number, height: number, nativePaneChrome: boolean): number {
|
|
43
|
+
if (symbolCount <= 1) return 1;
|
|
44
|
+
|
|
45
|
+
const contentWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
46
|
+
const contentHeight = Math.max(1, height);
|
|
47
|
+
const minimumCellWidth = nativePaneChrome ? 34 : 28;
|
|
48
|
+
const minimumCellHeight = nativePaneChrome ? 4 : 3;
|
|
49
|
+
const maxColumns = Math.max(1, Math.min(symbolCount, Math.floor(contentWidth / minimumCellWidth)));
|
|
50
|
+
const targetAspect = nativePaneChrome ? 5.5 : 4.5;
|
|
51
|
+
let bestColumns = 1;
|
|
52
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
53
|
+
|
|
54
|
+
for (let columns = 1; columns <= maxColumns; columns++) {
|
|
55
|
+
const rows = Math.ceil(symbolCount / columns);
|
|
56
|
+
const cellWidth = Math.floor(contentWidth / columns);
|
|
57
|
+
const cellHeight = Math.floor(contentHeight / rows);
|
|
58
|
+
const aspect = cellWidth / Math.max(1, cellHeight);
|
|
59
|
+
const emptySlots = columns * rows - symbolCount;
|
|
60
|
+
const widthPenalty = cellWidth < minimumCellWidth ? (minimumCellWidth - cellWidth) * 0.5 : 0;
|
|
61
|
+
const heightPenalty = cellHeight < minimumCellHeight ? (minimumCellHeight - cellHeight) * 1.4 : 0;
|
|
62
|
+
const score = Math.abs(Math.log(aspect / targetAspect))
|
|
63
|
+
+ emptySlots * 0.35
|
|
64
|
+
+ widthPenalty
|
|
65
|
+
+ heightPenalty
|
|
66
|
+
- columns * 0.03;
|
|
67
|
+
if (score < bestScore) {
|
|
68
|
+
bestScore = score;
|
|
69
|
+
bestColumns = columns;
|
|
70
|
+
}
|
|
44
71
|
}
|
|
45
72
|
|
|
46
|
-
|
|
73
|
+
return bestColumns;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function quoteTrend(value: number | null | undefined): PriceSparklineTrend {
|
|
77
|
+
if (value == null || value === 0) return "neutral";
|
|
78
|
+
return value > 0 ? "positive" : "negative";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function QuoteMonitorCard({
|
|
82
|
+
symbol,
|
|
83
|
+
ticker,
|
|
84
|
+
cachedFinancials,
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
showRightDivider,
|
|
88
|
+
showBottomDivider,
|
|
89
|
+
chartPeriod,
|
|
90
|
+
onOpen,
|
|
91
|
+
}: {
|
|
92
|
+
symbol: string;
|
|
93
|
+
ticker: TickerRecord | null;
|
|
94
|
+
cachedFinancials: TickerFinancials | null;
|
|
95
|
+
width: number;
|
|
96
|
+
height: number;
|
|
97
|
+
showRightDivider: boolean;
|
|
98
|
+
showBottomDivider: boolean;
|
|
99
|
+
chartPeriod: PriceSparklinePeriod;
|
|
100
|
+
onOpen: (symbol: string) => void;
|
|
101
|
+
}) {
|
|
102
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
103
|
+
const marketFinancials = useTickerFinancials(symbol, ticker);
|
|
104
|
+
const financials = marketFinancials ?? cachedFinancials;
|
|
105
|
+
const quote = financials?.quote;
|
|
106
|
+
const display = getQuoteMonitorDisplay(quote);
|
|
47
107
|
const changeColor = priceColor(display?.change ?? 0);
|
|
48
|
-
const
|
|
49
|
-
const
|
|
108
|
+
const currency = quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
109
|
+
const stacked = width < 31;
|
|
110
|
+
const priceText = display
|
|
111
|
+
? formatMarketPriceWithCurrency(display.price, currency, { assetCategory: ticker?.metadata.assetCategory })
|
|
112
|
+
: "";
|
|
113
|
+
const changePercentText = display ? formatPercentRaw(display.changePercent) : "";
|
|
114
|
+
const changeValueText = display ? formatSignedMarketPrice(display.change, { assetCategory: ticker?.metadata.assetCategory }) : "";
|
|
115
|
+
const priceColumnWidth = Math.max(priceText.length, changePercentText.length + changeValueText.length + 1);
|
|
116
|
+
const nameMaxWidth = Math.max(10, width - priceColumnWidth - (nativePaneChrome ? 5 : 3));
|
|
117
|
+
const sparklineRange = resolvePriceSparklineRange(financials?.priceHistory, chartPeriod);
|
|
118
|
+
const rangeLabel = sparklineRange
|
|
119
|
+
? `${chartPeriod} ${formatMarketPriceWithCurrency(sparklineRange.min, currency, { assetCategory: ticker?.metadata.assetCategory })}-${formatMarketPriceWithCurrency(sparklineRange.max, currency, { assetCategory: ticker?.metadata.assetCategory })}`
|
|
120
|
+
: "";
|
|
121
|
+
const sparklineWidth = Math.max(8, width - (nativePaneChrome ? rangeLabel.length + 5 : 2));
|
|
122
|
+
const trend = quoteTrend(display?.change);
|
|
123
|
+
const terminalSparklineHeight = !nativePaneChrome && !stacked && height >= 4 ? 2 : 1;
|
|
124
|
+
const desktopPriceStyle = nativePaneChrome
|
|
125
|
+
? {
|
|
126
|
+
fontSize: "22px",
|
|
127
|
+
lineHeight: "1.05",
|
|
128
|
+
fontWeight: 720,
|
|
129
|
+
textShadow: `0 1px 2px ${colors.bg}`,
|
|
130
|
+
}
|
|
131
|
+
: undefined;
|
|
132
|
+
const desktopChangeStyle = nativePaneChrome
|
|
133
|
+
? {
|
|
134
|
+
fontSize: "12px",
|
|
135
|
+
lineHeight: "1.1",
|
|
136
|
+
fontWeight: 540,
|
|
137
|
+
textShadow: `0 1px 2px ${colors.bg}`,
|
|
138
|
+
}
|
|
139
|
+
: undefined;
|
|
140
|
+
const desktopSymbolStyle = nativePaneChrome
|
|
141
|
+
? {
|
|
142
|
+
fontSize: "15px",
|
|
143
|
+
lineHeight: "18px",
|
|
144
|
+
}
|
|
145
|
+
: undefined;
|
|
146
|
+
|
|
147
|
+
const handleMouseDown = useDoubleClickActivation<string>({
|
|
148
|
+
onActivate: onOpen,
|
|
149
|
+
});
|
|
50
150
|
|
|
51
151
|
return (
|
|
52
|
-
<Box
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
152
|
+
<Box
|
|
153
|
+
flexDirection="column"
|
|
154
|
+
width={nativePaneChrome ? undefined : width}
|
|
155
|
+
height={nativePaneChrome ? undefined : height}
|
|
156
|
+
backgroundColor={colors.bg}
|
|
157
|
+
paddingX={nativePaneChrome ? 0 : 1}
|
|
158
|
+
paddingY={0}
|
|
159
|
+
overflow="hidden"
|
|
160
|
+
onMouseDown={(event: any) => {
|
|
161
|
+
event.preventDefault?.();
|
|
162
|
+
handleMouseDown(symbol, symbol, event);
|
|
163
|
+
}}
|
|
164
|
+
data-gloom-role="quote-monitor-card"
|
|
165
|
+
style={nativePaneChrome ? {
|
|
166
|
+
cursor: "pointer",
|
|
167
|
+
position: "relative",
|
|
168
|
+
width: "100%",
|
|
169
|
+
height: "100%",
|
|
170
|
+
paddingLeft: 10,
|
|
171
|
+
paddingRight: 10,
|
|
172
|
+
paddingTop: 6,
|
|
173
|
+
paddingBottom: 4,
|
|
174
|
+
borderRight: showRightDivider ? `1px solid ${colors.border}` : undefined,
|
|
175
|
+
borderBottom: showBottomDivider ? `1px solid ${colors.border}` : undefined,
|
|
176
|
+
} : undefined}
|
|
177
|
+
>
|
|
178
|
+
{nativePaneChrome && display && (
|
|
179
|
+
<PriceAreaSparklineBackground priceHistory={financials?.priceHistory} trend={trend} period={chartPeriod} />
|
|
180
|
+
)}
|
|
181
|
+
{!display ? (
|
|
182
|
+
<Box flexDirection="column" flexGrow={1} justifyContent="center">
|
|
183
|
+
<Text attributes={TextAttributes.BOLD} fg={colors.textBright} style={desktopSymbolStyle}>
|
|
184
|
+
{symbol}
|
|
185
|
+
</Text>
|
|
186
|
+
<Text fg={colors.textDim}>Waiting for quote...</Text>
|
|
187
|
+
</Box>
|
|
188
|
+
) : nativePaneChrome ? (
|
|
189
|
+
<Box
|
|
190
|
+
flexGrow={1}
|
|
191
|
+
style={{
|
|
192
|
+
position: "relative",
|
|
193
|
+
zIndex: 1,
|
|
194
|
+
display: "grid",
|
|
195
|
+
gridTemplateColumns: "minmax(130px, 1fr) minmax(88px, auto)",
|
|
196
|
+
gridTemplateRows: "auto 1fr auto",
|
|
197
|
+
columnGap: 12,
|
|
198
|
+
width: "100%",
|
|
199
|
+
height: "100%",
|
|
200
|
+
}}
|
|
201
|
+
>
|
|
202
|
+
<Box
|
|
203
|
+
flexDirection="column"
|
|
204
|
+
minWidth={0}
|
|
205
|
+
style={{
|
|
206
|
+
gridColumn: "1",
|
|
207
|
+
gridRow: "1 / span 2",
|
|
208
|
+
minWidth: 0,
|
|
209
|
+
overflow: "hidden",
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<Text attributes={TextAttributes.BOLD} fg={colors.textBright} style={desktopSymbolStyle}>
|
|
213
|
+
{symbol}
|
|
66
214
|
</Text>
|
|
67
|
-
|
|
215
|
+
{ticker?.metadata.name && (
|
|
216
|
+
<Text
|
|
217
|
+
fg={colors.textDim}
|
|
218
|
+
style={{
|
|
219
|
+
fontSize: "12px",
|
|
220
|
+
lineHeight: "14px",
|
|
221
|
+
maxWidth: "100%",
|
|
222
|
+
overflow: "hidden",
|
|
223
|
+
textOverflow: "ellipsis",
|
|
224
|
+
whiteSpace: "nowrap",
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{ticker.metadata.name}
|
|
228
|
+
</Text>
|
|
229
|
+
)}
|
|
68
230
|
</Box>
|
|
69
|
-
|
|
231
|
+
|
|
70
232
|
<Box
|
|
71
|
-
flexDirection=
|
|
72
|
-
alignItems=
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
233
|
+
flexDirection="column"
|
|
234
|
+
alignItems="flex-end"
|
|
235
|
+
style={{
|
|
236
|
+
gridColumn: "2",
|
|
237
|
+
gridRow: "1",
|
|
238
|
+
justifySelf: "end",
|
|
239
|
+
backgroundColor: colors.bg,
|
|
240
|
+
borderRadius: 4,
|
|
241
|
+
boxShadow: `0 0 0 2px ${colors.bg}`,
|
|
242
|
+
paddingLeft: 6,
|
|
243
|
+
paddingBottom: 1,
|
|
244
|
+
}}
|
|
76
245
|
>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
246
|
+
<Text
|
|
247
|
+
attributes={TextAttributes.BOLD}
|
|
248
|
+
fg={changeColor}
|
|
249
|
+
style={desktopPriceStyle}
|
|
250
|
+
>
|
|
251
|
+
{priceText}
|
|
252
|
+
</Text>
|
|
253
|
+
<Box flexDirection="row" gap={1} justifyContent="flex-end">
|
|
254
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changePercentText}</Text>
|
|
255
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changeValueText}</Text>
|
|
81
256
|
</Box>
|
|
257
|
+
</Box>
|
|
82
258
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
259
|
+
{rangeLabel && (
|
|
260
|
+
<Text
|
|
261
|
+
fg={colors.textDim}
|
|
262
|
+
style={{
|
|
263
|
+
gridColumn: "2",
|
|
264
|
+
gridRow: "3",
|
|
265
|
+
justifySelf: "end",
|
|
266
|
+
alignSelf: "end",
|
|
267
|
+
fontSize: "11px",
|
|
268
|
+
lineHeight: "13px",
|
|
269
|
+
backgroundColor: colors.bg,
|
|
270
|
+
paddingLeft: 6,
|
|
271
|
+
paddingRight: 2,
|
|
272
|
+
boxShadow: `0 0 0 2px ${colors.bg}`,
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{rangeLabel}
|
|
276
|
+
</Text>
|
|
277
|
+
)}
|
|
278
|
+
</Box>
|
|
279
|
+
) : (
|
|
280
|
+
<Box flexDirection="column" flexGrow={1} justifyContent="flex-start">
|
|
281
|
+
{stacked ? (
|
|
282
|
+
<Box flexDirection="column">
|
|
283
|
+
<Text attributes={TextAttributes.BOLD} fg={colors.textBright} style={desktopSymbolStyle}>
|
|
284
|
+
{symbol}
|
|
86
285
|
</Text>
|
|
87
|
-
<Box flexDirection="
|
|
88
|
-
<Text fg={changeColor}
|
|
89
|
-
|
|
286
|
+
<Box flexDirection="column">
|
|
287
|
+
<Text attributes={TextAttributes.BOLD} fg={changeColor} style={desktopPriceStyle}>
|
|
288
|
+
{priceText}
|
|
289
|
+
</Text>
|
|
290
|
+
<Box flexDirection="row" gap={1}>
|
|
291
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changePercentText}</Text>
|
|
292
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changeValueText}</Text>
|
|
293
|
+
</Box>
|
|
90
294
|
</Box>
|
|
91
295
|
</Box>
|
|
296
|
+
) : (
|
|
297
|
+
<Box
|
|
298
|
+
flexDirection="row"
|
|
299
|
+
alignItems="flex-start"
|
|
300
|
+
justifyContent="space-between"
|
|
301
|
+
gap={1}
|
|
302
|
+
>
|
|
303
|
+
<Box flexDirection="column" flexGrow={1} minWidth={0}>
|
|
304
|
+
<Text attributes={TextAttributes.BOLD} fg={colors.textBright} style={desktopSymbolStyle}>
|
|
305
|
+
{symbol}
|
|
306
|
+
</Text>
|
|
307
|
+
{nativePaneChrome && ticker?.metadata.name && width >= 36 && (
|
|
308
|
+
<Text
|
|
309
|
+
fg={colors.textDim}
|
|
310
|
+
style={{
|
|
311
|
+
fontSize: "12px",
|
|
312
|
+
lineHeight: "16px",
|
|
313
|
+
overflow: "hidden",
|
|
314
|
+
textOverflow: "ellipsis",
|
|
315
|
+
whiteSpace: "nowrap",
|
|
316
|
+
}}
|
|
317
|
+
maxWidth={nameMaxWidth}
|
|
318
|
+
>
|
|
319
|
+
{ticker.metadata.name}
|
|
320
|
+
</Text>
|
|
321
|
+
)}
|
|
322
|
+
</Box>
|
|
323
|
+
<Box flexDirection="column" alignItems="flex-end">
|
|
324
|
+
<Text
|
|
325
|
+
attributes={TextAttributes.BOLD}
|
|
326
|
+
fg={changeColor}
|
|
327
|
+
style={desktopPriceStyle}
|
|
328
|
+
>
|
|
329
|
+
{priceText}
|
|
330
|
+
</Text>
|
|
331
|
+
<Box flexDirection="row" gap={1} justifyContent="flex-end">
|
|
332
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changePercentText}</Text>
|
|
333
|
+
<Text fg={changeColor} style={desktopChangeStyle}>{changeValueText}</Text>
|
|
334
|
+
</Box>
|
|
335
|
+
</Box>
|
|
336
|
+
</Box>
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
<Box height={terminalSparklineHeight} flexDirection="row" alignItems="center" gap={1}>
|
|
340
|
+
<PriceSparkline
|
|
341
|
+
priceHistory={financials?.priceHistory}
|
|
342
|
+
width={sparklineWidth}
|
|
343
|
+
height={terminalSparklineHeight}
|
|
344
|
+
trend={trend}
|
|
345
|
+
period={chartPeriod}
|
|
346
|
+
area={!nativePaneChrome}
|
|
347
|
+
/>
|
|
348
|
+
{nativePaneChrome && rangeLabel && (
|
|
349
|
+
<Text fg={colors.textDim} style={{ fontSize: "11px", lineHeight: "14px" }}>
|
|
350
|
+
{rangeLabel}
|
|
351
|
+
</Text>
|
|
352
|
+
)}
|
|
92
353
|
</Box>
|
|
93
|
-
|
|
354
|
+
</Box>
|
|
355
|
+
)}
|
|
356
|
+
</Box>
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function QuoteMonitorPane({ paneId, focused, width, height }: PaneProps) {
|
|
361
|
+
const pane = usePaneInstance();
|
|
362
|
+
const { symbol: fallbackSymbol, ticker: fallbackTicker } = usePaneTicker();
|
|
363
|
+
const settings = useMemo(
|
|
364
|
+
() => getQuoteMonitorPaneSettings(pane?.settings, fallbackSymbol),
|
|
365
|
+
[fallbackSymbol, pane?.settings],
|
|
366
|
+
);
|
|
367
|
+
const symbols = settings.symbols;
|
|
368
|
+
const financialsBySymbol = useAppSelector((state) => state.financials);
|
|
369
|
+
const tickersBySymbol = useAppSelector((state) => state.tickers);
|
|
370
|
+
const streamingTargets = useMemo(() => (
|
|
371
|
+
symbols
|
|
372
|
+
.map((symbol) => {
|
|
373
|
+
const ticker = tickersBySymbol.get(symbol) ?? (fallbackTicker?.metadata.ticker === symbol ? fallbackTicker : null);
|
|
374
|
+
return quoteSubscriptionTargetFromTicker(ticker, symbol, "provider");
|
|
375
|
+
})
|
|
376
|
+
.filter((target): target is QuoteSubscriptionTarget => target != null)
|
|
377
|
+
), [fallbackTicker, symbols, tickersBySymbol]);
|
|
378
|
+
useQuoteStreaming(streamingTargets);
|
|
379
|
+
const { pinTicker } = usePluginTickerActions();
|
|
380
|
+
const { openPaneSettings } = usePluginAppActions();
|
|
381
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
382
|
+
const openTicker = useCallback((nextSymbol: string) => {
|
|
383
|
+
pinTicker(nextSymbol, { paneType: "ticker-detail", floating: true });
|
|
384
|
+
}, [pinTicker]);
|
|
385
|
+
useShortcut((event) => {
|
|
386
|
+
if (!focused || event.name !== "t") return;
|
|
387
|
+
event.preventDefault?.();
|
|
388
|
+
event.stopPropagation?.();
|
|
389
|
+
openPaneSettings(paneId);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (symbols.length === 0) {
|
|
393
|
+
return (
|
|
394
|
+
<Box flexDirection="column" flexGrow={1} paddingX={1}>
|
|
395
|
+
<EmptyState title="No tickers selected." />
|
|
94
396
|
</Box>
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const columns = resolveGridColumnCount(symbols.length, width, height, nativePaneChrome);
|
|
401
|
+
const rows = chunkSymbols(symbols, columns);
|
|
402
|
+
const contentWidth = Math.max(1, width - (nativePaneChrome ? 0 : 2));
|
|
403
|
+
const contentHeight = Math.max(3, height);
|
|
404
|
+
const cardHeight = Math.max(nativePaneChrome ? 4 : 3, Math.floor(contentHeight / rows.length));
|
|
405
|
+
const cardWidth = Math.max(1, Math.floor(contentWidth / columns));
|
|
406
|
+
|
|
407
|
+
if (nativePaneChrome) {
|
|
408
|
+
return (
|
|
409
|
+
<Box
|
|
410
|
+
flexGrow={1}
|
|
411
|
+
backgroundColor={colors.bg}
|
|
412
|
+
overflow="hidden"
|
|
413
|
+
style={{
|
|
414
|
+
display: "grid",
|
|
415
|
+
gridTemplateColumns: "repeat(auto-fit, minmax(min(240px, 100%), 1fr))",
|
|
416
|
+
gridAutoRows: "minmax(66px, 1fr)",
|
|
417
|
+
alignItems: "stretch",
|
|
418
|
+
alignContent: "stretch",
|
|
419
|
+
width: "100%",
|
|
420
|
+
height: "100%",
|
|
421
|
+
}}
|
|
422
|
+
>
|
|
423
|
+
{symbols.map((symbol, index) => {
|
|
424
|
+
const ticker = tickersBySymbol.get(symbol) ?? (fallbackTicker?.metadata.ticker === symbol ? fallbackTicker : null);
|
|
425
|
+
return (
|
|
426
|
+
<QuoteMonitorCard
|
|
427
|
+
key={symbol}
|
|
428
|
+
symbol={symbol}
|
|
429
|
+
ticker={ticker}
|
|
430
|
+
cachedFinancials={financialsBySymbol.get(symbol) ?? null}
|
|
431
|
+
width={width}
|
|
432
|
+
height={height}
|
|
433
|
+
showRightDivider={false}
|
|
434
|
+
showBottomDivider
|
|
435
|
+
chartPeriod={settings.chartPeriod}
|
|
436
|
+
onOpen={openTicker}
|
|
437
|
+
/>
|
|
438
|
+
);
|
|
439
|
+
})}
|
|
440
|
+
</Box>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<Box
|
|
446
|
+
flexDirection="column"
|
|
447
|
+
flexGrow={1}
|
|
448
|
+
backgroundColor={colors.bg}
|
|
449
|
+
paddingX={nativePaneChrome ? 0 : 1}
|
|
450
|
+
paddingY={0}
|
|
451
|
+
overflow="hidden"
|
|
452
|
+
>
|
|
453
|
+
{rows.map((row, rowIndex) => (
|
|
454
|
+
<Box key={`${rowIndex}:${row.join(",")}`} flexDirection="row" height={cardHeight}>
|
|
455
|
+
{row.map((symbol, columnIndex) => {
|
|
456
|
+
const ticker = tickersBySymbol.get(symbol) ?? (fallbackTicker?.metadata.ticker === symbol ? fallbackTicker : null);
|
|
457
|
+
return (
|
|
458
|
+
<QuoteMonitorCard
|
|
459
|
+
key={symbol}
|
|
460
|
+
symbol={symbol}
|
|
461
|
+
ticker={ticker}
|
|
462
|
+
cachedFinancials={financialsBySymbol.get(symbol) ?? null}
|
|
463
|
+
width={cardWidth}
|
|
464
|
+
height={cardHeight}
|
|
465
|
+
showRightDivider={columnIndex < row.length - 1}
|
|
466
|
+
showBottomDivider={rowIndex < rows.length - 1}
|
|
467
|
+
chartPeriod={settings.chartPeriod}
|
|
468
|
+
onOpen={openTicker}
|
|
469
|
+
/>
|
|
470
|
+
);
|
|
471
|
+
})}
|
|
472
|
+
</Box>
|
|
473
|
+
))}
|
|
95
474
|
</Box>
|
|
96
475
|
);
|
|
97
476
|
}
|