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.
Files changed (129) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +49 -15
  7. package/src/components/chart/chart-types.ts +2 -2
  8. package/src/components/chart/chart.test.ts +27 -8
  9. package/src/components/chart/comparison-stock-chart.tsx +84 -121
  10. package/src/components/chart/native/chart-rasterizer.ts +43 -17
  11. package/src/components/chart/price-axis-labels.tsx +25 -9
  12. package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
  13. package/src/components/chart/stock-chart.tsx +59 -41
  14. package/src/components/command-bar/command-bar.test.tsx +446 -120
  15. package/src/components/command-bar/command-bar.tsx +528 -383
  16. package/src/components/command-bar/command-registry.ts +16 -6
  17. package/src/components/command-bar/root-shortcuts.ts +4 -5
  18. package/src/components/command-bar/view-model.test.ts +1 -0
  19. package/src/components/command-bar/view-model.ts +2 -1
  20. package/src/components/command-bar/workflow-ops.ts +11 -32
  21. package/src/components/data-table-view.test.tsx +24 -1
  22. package/src/components/data-table-view.tsx +3 -2
  23. package/src/components/feed-data-table-stack-view.tsx +3 -2
  24. package/src/components/layout/detached-pane-shell.tsx +2 -2
  25. package/src/components/layout/floating-pane.tsx +9 -2
  26. package/src/components/layout/pane-footer.tsx +1 -1
  27. package/src/components/layout/pane-header.tsx +4 -2
  28. package/src/components/layout/pane-sizing.ts +12 -0
  29. package/src/components/layout/pane.tsx +24 -2
  30. package/src/components/layout/shell.tsx +24 -10
  31. package/src/components/layout/status-bar.test.tsx +4 -1
  32. package/src/components/layout/status-bar.tsx +4 -24
  33. package/src/components/markdown-text.tsx +53 -8
  34. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  35. package/src/components/pane-settings-dialog.tsx +9 -6
  36. package/src/components/pane-template-wizard.tsx +3 -2
  37. package/src/components/price-selector-dialog.tsx +3 -2
  38. package/src/components/price-sparkline-view.tsx +463 -0
  39. package/src/components/price-sparkline.ts +46 -0
  40. package/src/components/table-view-shared.tsx +21 -3
  41. package/src/components/ticker-list-table-view.tsx +3 -2
  42. package/src/components/ticker-list-table.tsx +11 -51
  43. package/src/components/toggle-list.tsx +3 -3
  44. package/src/components/ui/choice-dialog.tsx +3 -2
  45. package/src/components/ui/multi-select-dialog.tsx +3 -2
  46. package/src/components/ui/native-select.tsx +3 -3
  47. package/src/core/state/app-state.ts +210 -80
  48. package/src/data/config-store-node.ts +113 -25
  49. package/src/data/config-store.test.ts +65 -3
  50. package/src/data/plugin-state-store.ts +39 -13
  51. package/src/data/sqlite-cache.test.ts +13 -0
  52. package/src/market-data/coordinator.ts +8 -1
  53. package/src/news/aggregator.test.ts +41 -0
  54. package/src/news/aggregator.ts +61 -9
  55. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  56. package/src/plugins/builtin/ai/settings.ts +3 -0
  57. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  58. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  59. package/src/plugins/builtin/chat-controller.ts +258 -44
  60. package/src/plugins/builtin/chat.test.tsx +257 -6
  61. package/src/plugins/builtin/chat.tsx +225 -60
  62. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  63. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  64. package/src/plugins/builtin/correlation/colors.ts +87 -0
  65. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  66. package/src/plugins/builtin/correlation/index.tsx +92 -21
  67. package/src/plugins/builtin/debug.tsx +4 -3
  68. package/src/plugins/builtin/econ/index.tsx +3 -2
  69. package/src/plugins/builtin/help.test.tsx +2 -2
  70. package/src/plugins/builtin/holders/index.tsx +25 -13
  71. package/src/plugins/builtin/layout-manager.tsx +1 -7
  72. package/src/plugins/builtin/news-wire/index.tsx +1 -1
  73. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  74. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  75. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  76. package/src/plugins/builtin/options.tsx +9 -17
  77. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  78. package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
  79. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  80. package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
  81. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  82. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  83. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  84. package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
  85. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  86. package/src/plugins/builtin/sectors/index.tsx +252 -41
  87. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  88. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  89. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  90. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  91. package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
  92. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
  93. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  94. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  95. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  96. package/src/plugins/plugin-runtime.tsx +2 -2
  97. package/src/plugins/prediction-markets/settings.ts +4 -0
  98. package/src/plugins/registry.test.ts +55 -0
  99. package/src/plugins/registry.ts +11 -4
  100. package/src/plugins/ticker-navigation.test.ts +35 -5
  101. package/src/plugins/ticker-navigation.ts +16 -22
  102. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  103. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  104. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  105. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  106. package/src/renderers/electrobun/bun/index.ts +52 -13
  107. package/src/renderers/electrobun/view/app-services.ts +51 -9
  108. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  109. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  110. package/src/renderers/electrobun/view/main.tsx +0 -16
  111. package/src/renderers/electrobun/view/styles.css +23 -21
  112. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  113. package/src/renderers/opentui/host.tsx +2 -1
  114. package/src/renderers/opentui/start.tsx +1 -1
  115. package/src/renderers/opentui/test-utils.tsx +6 -5
  116. package/src/state/app-bootstrap.ts +3 -3
  117. package/src/state/app-context.test.ts +113 -0
  118. package/src/state/app-context.tsx +8 -4
  119. package/src/theme/colors.ts +13 -14
  120. package/src/types/config.ts +9 -4
  121. package/src/types/plugin.ts +8 -1
  122. package/src/ui/context-menu.tsx +4 -4
  123. package/src/utils/api-client.test.ts +128 -0
  124. package/src/utils/api-client.ts +112 -7
  125. package/src/utils/fuzzy-search.ts +12 -1
  126. package/src/utils/keyboard.ts +31 -0
  127. package/src/utils/ticker-search.test.ts +2 -2
  128. package/src/utils/ticker-search.ts +48 -1
  129. 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
- <Text fg={colors.textDim}>{padTo(label, labelWidth)}</Text>
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
- <Text fg={colors.textDim}>
79
- {padTo(lowText, endpointWidth)}
80
- </Text>
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
- <Text fg={colors.textDim}>
87
- {padTo(highText, endpointWidth, "right")}
88
- </Text>
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 colWidth = Math.floor((width - STAT_COLUMN_GAP * (columnCount - 1)) / columnCount);
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
- <Text fg={colors.textDim}>{padTo(field.label, labelWidth)}</Text>
193
- <Text fg={field.valueColor ?? colors.text}>{padTo(field.value, valueWidth, "right")}</Text>
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.min(RANGE_MAX_WIDTH, Math.floor((contentWidth - 2) / 2))
351
- : Math.min(RANGE_MAX_WIDTH, contentWidth);
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 { Box, Text } from "../../../ui";
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 { usePaneTicker } from "../../../state/app-context";
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, usePaneFooter } from "../../../components";
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
- export function QuoteMonitorPane({ focused, width }: PaneProps) {
19
- const { ticker, financials } = usePaneTicker();
20
- const streamingTarget = quoteSubscriptionTargetFromTicker(ticker, ticker?.metadata.ticker, "provider");
21
- useQuoteStreaming(streamingTarget ? [streamingTarget] : []);
22
- const quote = financials?.quote;
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
- usePaneFooter("quote-monitor", () => ({
25
- info: [
26
- ...(ticker ? [{ id: "ticker", parts: [{ text: ticker.metadata.ticker, tone: "value" as const, bold: true }] }] : []),
27
- ...(quote?.lastUpdated ? [{
28
- id: "updated",
29
- parts: [{ text: `quote ${new Date(quote.lastUpdated).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`, tone: "muted" as const }],
30
- }] : []),
31
- ...(quote?.providerId || quote?.dataSource ? [{
32
- id: "source",
33
- parts: [{ text: quote.providerId ?? quote.dataSource ?? "", tone: "muted" as const }],
34
- }] : []),
35
- ],
36
- }), [quote?.dataSource, quote?.lastUpdated, quote?.providerId, ticker?.metadata.ticker]);
37
-
38
- if (!ticker) {
39
- return (
40
- <Box flexDirection="column" flexGrow={1} paddingX={1}>
41
- <EmptyState title="No ticker selected." />
42
- </Box>
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
- const display = getQuoteMonitorDisplay(financials?.quote);
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 compact = width < 56;
49
- const currency = financials?.quote?.currency ?? ticker.metadata.currency ?? "USD";
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 flexDirection="column" flexGrow={1} backgroundColor={colors.panel} padding={1}>
53
- <Box
54
- flexGrow={1}
55
- border
56
- borderColor={focused ? colors.borderFocused : colors.border}
57
- backgroundColor={colors.bg}
58
- paddingX={compact ? 2 : 4}
59
- paddingY={1}
60
- justifyContent="center"
61
- >
62
- {!display ? (
63
- <Box flexDirection="column" alignItems="center" justifyContent="center" gap={1} flexGrow={1}>
64
- <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
65
- {ticker.metadata.ticker}
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
- <Text fg={colors.textDim}>Waiting for quote...</Text>
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={compact ? "column" : "row"}
72
- alignItems={compact ? "flex-start" : "center"}
73
- justifyContent="space-between"
74
- gap={compact ? 1 : 2}
75
- flexGrow={1}
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
- <Box flexGrow={1} justifyContent="center">
78
- <Text attributes={TextAttributes.BOLD} fg={colors.textBright}>
79
- {ticker.metadata.ticker}
80
- </Text>
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
- <Box flexDirection="column" alignItems={compact ? "flex-start" : "flex-end"}>
84
- <Text attributes={TextAttributes.BOLD} fg={changeColor}>
85
- {formatMarketPriceWithCurrency(display.price, currency, { assetCategory: ticker.metadata.assetCategory })}
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="row" gap={1}>
88
- <Text fg={changeColor}>{formatPercentRaw(display.changePercent)}</Text>
89
- <Text fg={changeColor}>{formatSignedMarketPrice(display.change, { assetCategory: ticker.metadata.assetCategory })}</Text>
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
  }