gloomberb 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type DataTableColumn,
|
|
8
8
|
type DataTableKeyEvent,
|
|
9
9
|
} from "../../../components";
|
|
10
|
-
import type { AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
|
|
11
|
-
import type {
|
|
10
|
+
import type { AnalystRatingRecord, AnalystResearchData, CorporateActionsData, TickerFinancials } from "../../../types/financials";
|
|
11
|
+
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
12
12
|
import { usePaneInstance, usePaneTicker } from "../../../state/app-context";
|
|
13
13
|
import { blendHex, colors, priceColor } from "../../../theme/colors";
|
|
14
14
|
import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
|
|
@@ -110,6 +110,32 @@ function ratingActionColor(action: string | undefined): string {
|
|
|
110
110
|
return colors.textDim;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function formatPriceTarget(value: number | undefined, currency: string): string {
|
|
114
|
+
if (value == null) return "-";
|
|
115
|
+
return formatCurrency(value, currency)
|
|
116
|
+
.replace(/\.00\b/, "")
|
|
117
|
+
.replace(/(\.\d)0\b/, "$1");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function formatRatingTarget(row: AnalystResearchData["ratings"][number], currency: string): string {
|
|
121
|
+
const current = row.currentPriceTarget;
|
|
122
|
+
const prior = row.priorPriceTarget;
|
|
123
|
+
if (current == null && prior == null) return "-";
|
|
124
|
+
if (current == null) return ` ${formatPriceTarget(prior, currency)}`;
|
|
125
|
+
if (prior == null) return ` ${formatPriceTarget(current, currency)}`;
|
|
126
|
+
return ` ${formatPriceTarget(prior, currency)} → ${formatPriceTarget(current, currency)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function ratingTargetDelta(row: AnalystResearchData["ratings"][number]): number | null {
|
|
130
|
+
if (row.currentPriceTarget == null || row.priorPriceTarget == null) return null;
|
|
131
|
+
return row.currentPriceTarget - row.priorPriceTarget;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ratingTargetBackground(delta: number | null): string | undefined {
|
|
135
|
+
if (delta == null || delta === 0) return undefined;
|
|
136
|
+
return blendHex(colors.bg, delta > 0 ? colors.positive : colors.negative, 0.42);
|
|
137
|
+
}
|
|
138
|
+
|
|
113
139
|
function AnalystSummary({ data, width }: { data: AnalystResearchData | null; width: number }) {
|
|
114
140
|
const target = data?.priceTarget;
|
|
115
141
|
const upside = targetUpside(target);
|
|
@@ -170,26 +196,135 @@ function AnalystSummary({ data, width }: { data: AnalystResearchData | null; wid
|
|
|
170
196
|
);
|
|
171
197
|
}
|
|
172
198
|
|
|
173
|
-
type RatingColumnId = "date" | "firm" | "action" | "current" | "prior";
|
|
199
|
+
export type RatingColumnId = "date" | "firm" | "action" | "current" | "target" | "prior";
|
|
174
200
|
type RatingColumn = DataTableColumn & { id: RatingColumnId };
|
|
201
|
+
export type SortDirection = "asc" | "desc";
|
|
202
|
+
|
|
203
|
+
export interface RatingSortPreference {
|
|
204
|
+
columnId: RatingColumnId;
|
|
205
|
+
direction: SortDirection;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const DEFAULT_RATING_SORT: RatingSortPreference = {
|
|
209
|
+
columnId: "date",
|
|
210
|
+
direction: "desc",
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const DEFAULT_RATING_SORT_DIRECTIONS: Record<RatingColumnId, SortDirection> = {
|
|
214
|
+
date: "desc",
|
|
215
|
+
firm: "asc",
|
|
216
|
+
action: "asc",
|
|
217
|
+
current: "asc",
|
|
218
|
+
target: "desc",
|
|
219
|
+
prior: "asc",
|
|
220
|
+
};
|
|
175
221
|
|
|
176
222
|
const RATING_COLUMNS: RatingColumn[] = [
|
|
177
223
|
{ id: "date", label: "DATE", width: 10, align: "left" },
|
|
178
|
-
{ id: "firm", label: "FIRM", width:
|
|
179
|
-
{ id: "action", label: "ACTION", width:
|
|
180
|
-
{ id: "current", label: "RATING", width:
|
|
181
|
-
{ id: "
|
|
224
|
+
{ id: "firm", label: "FIRM", width: 20, align: "left" },
|
|
225
|
+
{ id: "action", label: "ACTION", width: 10, align: "left" },
|
|
226
|
+
{ id: "current", label: "RATING", width: 13, align: "left" },
|
|
227
|
+
{ id: "target", label: "TARGET", width: 13, align: "left" },
|
|
228
|
+
{ id: "prior", label: "PRIOR", width: 13, align: "left" },
|
|
182
229
|
];
|
|
183
230
|
|
|
231
|
+
function normalizedText(value: string | undefined): string | null {
|
|
232
|
+
const trimmed = value?.trim();
|
|
233
|
+
return trimmed ? trimmed.toLocaleLowerCase() : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ratingTargetSortValue(row: AnalystRatingRecord): number | null {
|
|
237
|
+
return row.currentPriceTarget ?? row.priorPriceTarget ?? null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function ratingSortValue(row: AnalystRatingRecord, columnId: RatingColumnId): string | number | null {
|
|
241
|
+
switch (columnId) {
|
|
242
|
+
case "date":
|
|
243
|
+
return normalizedText(row.date);
|
|
244
|
+
case "firm":
|
|
245
|
+
return normalizedText(row.firm);
|
|
246
|
+
case "action":
|
|
247
|
+
return normalizedText(row.action);
|
|
248
|
+
case "current":
|
|
249
|
+
return normalizedText(row.current);
|
|
250
|
+
case "target":
|
|
251
|
+
return ratingTargetSortValue(row);
|
|
252
|
+
case "prior":
|
|
253
|
+
return normalizedText(row.prior);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function compareSortValues(
|
|
258
|
+
left: string | number | null,
|
|
259
|
+
right: string | number | null,
|
|
260
|
+
direction: SortDirection,
|
|
261
|
+
): number {
|
|
262
|
+
if (left == null && right == null) return 0;
|
|
263
|
+
if (left == null) return 1;
|
|
264
|
+
if (right == null) return -1;
|
|
265
|
+
|
|
266
|
+
const comparison = typeof left === "string" && typeof right === "string"
|
|
267
|
+
? left.localeCompare(right)
|
|
268
|
+
: Number(left) - Number(right);
|
|
269
|
+
return direction === "asc" ? comparison : -comparison;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function sortRatingRows<T extends AnalystRatingRecord>(
|
|
273
|
+
rows: readonly T[],
|
|
274
|
+
preference: RatingSortPreference,
|
|
275
|
+
): T[] {
|
|
276
|
+
return rows
|
|
277
|
+
.map((row, index) => ({ row, index }))
|
|
278
|
+
.sort((left, right) => {
|
|
279
|
+
const primary = compareSortValues(
|
|
280
|
+
ratingSortValue(left.row, preference.columnId),
|
|
281
|
+
ratingSortValue(right.row, preference.columnId),
|
|
282
|
+
preference.direction,
|
|
283
|
+
);
|
|
284
|
+
if (primary !== 0) return primary;
|
|
285
|
+
|
|
286
|
+
const dateTieBreak = preference.columnId === "date"
|
|
287
|
+
? 0
|
|
288
|
+
: compareSortValues(
|
|
289
|
+
ratingSortValue(left.row, "date"),
|
|
290
|
+
ratingSortValue(right.row, "date"),
|
|
291
|
+
"desc",
|
|
292
|
+
);
|
|
293
|
+
if (dateTieBreak !== 0) return dateTieBreak;
|
|
294
|
+
|
|
295
|
+
return left.index - right.index;
|
|
296
|
+
})
|
|
297
|
+
.map((entry) => entry.row);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function nextRatingSortPreference(
|
|
301
|
+
current: RatingSortPreference,
|
|
302
|
+
columnId: string,
|
|
303
|
+
): RatingSortPreference {
|
|
304
|
+
const typedColumnId = columnId as RatingColumnId;
|
|
305
|
+
if (current.columnId !== typedColumnId) {
|
|
306
|
+
return {
|
|
307
|
+
columnId: typedColumnId,
|
|
308
|
+
direction: DEFAULT_RATING_SORT_DIRECTIONS[typedColumnId] ?? "asc",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
columnId: typedColumnId,
|
|
313
|
+
direction: current.direction === "asc" ? "desc" : "asc",
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
184
317
|
function AnalystResearchView({ focused, width, height }: { focused: boolean; width: number; height: number }) {
|
|
185
318
|
const dataProvider = useAssetData();
|
|
186
319
|
const { symbol, exchange } = useSymbolBinding();
|
|
320
|
+
const [sortPreference, setSortPreference] = useState<RatingSortPreference>(DEFAULT_RATING_SORT);
|
|
187
321
|
const loader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
|
|
188
322
|
if (!dataProvider?.getAnalystResearch) throw new Error("Analyst data unavailable");
|
|
189
323
|
return dataProvider.getAnalystResearch(nextSymbol, nextExchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
|
|
190
324
|
}, [dataProvider]);
|
|
191
325
|
const { data, loading, error, reload } = useTickerRequest<AnalystResearchData>(loader, symbol, exchange);
|
|
192
|
-
const rows = data?.ratings ?? [];
|
|
326
|
+
const rows = useMemo(() => sortRatingRows(data?.ratings ?? [], sortPreference), [data?.ratings, sortPreference]);
|
|
327
|
+
const ratingCurrency = data?.priceTarget?.currency ?? data?.currency ?? "USD";
|
|
193
328
|
|
|
194
329
|
const renderCell = useCallback((
|
|
195
330
|
row: AnalystResearchData["ratings"][number],
|
|
@@ -207,10 +342,20 @@ function AnalystResearchView({ focused, width, height }: { focused: boolean; wid
|
|
|
207
342
|
return { text: row.action ?? "-", color: selectedColor ?? ratingActionColor(row.action) };
|
|
208
343
|
case "current":
|
|
209
344
|
return { text: row.current ?? "-", color: selectedColor ?? colors.text };
|
|
345
|
+
case "target": {
|
|
346
|
+
const delta = ratingTargetDelta(row);
|
|
347
|
+
const hasTarget = row.currentPriceTarget != null || row.priorPriceTarget != null;
|
|
348
|
+
return {
|
|
349
|
+
text: formatRatingTarget(row, ratingCurrency),
|
|
350
|
+
color: selectedColor ?? (hasTarget ? colors.textBright : colors.textDim),
|
|
351
|
+
backgroundColor: rowState.selected ? undefined : ratingTargetBackground(delta),
|
|
352
|
+
attributes: hasTarget ? TextAttributes.BOLD : undefined,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
210
355
|
case "prior":
|
|
211
356
|
return { text: row.prior ?? "-", color: selectedColor ?? colors.textDim };
|
|
212
357
|
}
|
|
213
|
-
}, []);
|
|
358
|
+
}, [ratingCurrency]);
|
|
214
359
|
|
|
215
360
|
const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
216
361
|
if (event.name !== "r") return false;
|
|
@@ -219,6 +364,9 @@ function AnalystResearchView({ focused, width, height }: { focused: boolean; wid
|
|
|
219
364
|
reload();
|
|
220
365
|
return true;
|
|
221
366
|
}, [reload]);
|
|
367
|
+
const handleHeaderClick = useCallback((columnId: string) => {
|
|
368
|
+
setSortPreference((current) => nextRatingSortPreference(current, columnId));
|
|
369
|
+
}, []);
|
|
222
370
|
|
|
223
371
|
usePaneFooter("analyst-research", () => ({
|
|
224
372
|
info: [
|
|
@@ -237,15 +385,14 @@ function AnalystResearchView({ focused, width, height }: { focused: boolean; wid
|
|
|
237
385
|
onRootKeyDown={handleKeyDown}
|
|
238
386
|
columns={RATING_COLUMNS}
|
|
239
387
|
items={rows}
|
|
240
|
-
sortColumnId={
|
|
241
|
-
sortDirection=
|
|
242
|
-
onHeaderClick={
|
|
388
|
+
sortColumnId={sortPreference.columnId}
|
|
389
|
+
sortDirection={sortPreference.direction}
|
|
390
|
+
onHeaderClick={handleHeaderClick}
|
|
243
391
|
getItemKey={(row, index) => `${row.date}:${row.firm}:${index}`}
|
|
244
392
|
isSelected={() => false}
|
|
245
393
|
onSelect={() => {}}
|
|
246
394
|
renderCell={renderCell}
|
|
247
395
|
emptyStateTitle={loading ? "Loading analyst data..." : error ?? "No analyst data"}
|
|
248
|
-
showHorizontalScrollbar={false}
|
|
249
396
|
/>
|
|
250
397
|
);
|
|
251
398
|
}
|
|
@@ -609,22 +756,6 @@ function RelativeValuationPane({ focused, width, height }: PaneProps) {
|
|
|
609
756
|
);
|
|
610
757
|
}
|
|
611
758
|
|
|
612
|
-
function ResearchPane({ focused, width, height }: PaneProps) {
|
|
613
|
-
return <AnalystResearchView focused={focused} width={width} height={height} />;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function EventsPane({ focused, width, height }: PaneProps) {
|
|
617
|
-
return <CorporateActionsView focused={focused} width={width} height={height} />;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
function AnalystTab({ focused, width, height }: DetailTabProps) {
|
|
621
|
-
return <AnalystResearchView focused={focused} width={width} height={height} />;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function EventsTab({ focused, width, height }: DetailTabProps) {
|
|
625
|
-
return <CorporateActionsView focused={focused} width={width} height={height} />;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
759
|
export const researchPlugin: GloomPlugin = {
|
|
629
760
|
id: "research",
|
|
630
761
|
name: "Research",
|
|
@@ -637,14 +768,14 @@ export const researchPlugin: GloomPlugin = {
|
|
|
637
768
|
id: "analyst-research",
|
|
638
769
|
name: "Analyst",
|
|
639
770
|
order: 32,
|
|
640
|
-
component:
|
|
771
|
+
component: AnalystResearchView,
|
|
641
772
|
isVisible: ({ ticker }) => !!ticker,
|
|
642
773
|
});
|
|
643
774
|
ctx.registerDetailTab({
|
|
644
775
|
id: "corporate-actions",
|
|
645
776
|
name: "Events",
|
|
646
777
|
order: 34,
|
|
647
|
-
component:
|
|
778
|
+
component: CorporateActionsView,
|
|
648
779
|
isVisible: ({ ticker }) => !!ticker,
|
|
649
780
|
});
|
|
650
781
|
},
|
|
@@ -654,7 +785,7 @@ export const researchPlugin: GloomPlugin = {
|
|
|
654
785
|
id: "analyst-research",
|
|
655
786
|
name: "Analyst Research",
|
|
656
787
|
icon: "A",
|
|
657
|
-
component:
|
|
788
|
+
component: AnalystResearchView,
|
|
658
789
|
defaultPosition: "right",
|
|
659
790
|
defaultMode: "floating",
|
|
660
791
|
defaultFloatingSize: { width: 90, height: 28 },
|
|
@@ -663,7 +794,7 @@ export const researchPlugin: GloomPlugin = {
|
|
|
663
794
|
id: "corporate-actions",
|
|
664
795
|
name: "Corporate Actions",
|
|
665
796
|
icon: "E",
|
|
666
|
-
component:
|
|
797
|
+
component: CorporateActionsView,
|
|
667
798
|
defaultPosition: "right",
|
|
668
799
|
defaultMode: "floating",
|
|
669
800
|
defaultFloatingSize: { width: 82, height: 24 },
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Box, Text } from "../../ui";
|
|
2
|
+
import { colors } from "../../theme/colors";
|
|
3
|
+
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
4
|
+
import { transactionTypeLabel, type InsiderTransaction } from "./insider/insider-data";
|
|
5
|
+
|
|
6
|
+
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
7
|
+
|
|
8
|
+
export function formatFilingShortDate(value: Date | string | number): string {
|
|
9
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
10
|
+
if (isNaN(date.getTime())) return "—";
|
|
11
|
+
return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatFilingMetaDate(value: Date): string {
|
|
15
|
+
return value.toLocaleString("en-US", {
|
|
16
|
+
month: "short",
|
|
17
|
+
day: "numeric",
|
|
18
|
+
year: "numeric",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatFilingFormLabel(form: string, fallback = "FORM 4"): string {
|
|
23
|
+
const value = form.trim();
|
|
24
|
+
return value ? `FORM ${value}` : fallback;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function renderFilingNotice(message: string, width: number) {
|
|
28
|
+
const lines = wrapNoticeLines(message, width - 4);
|
|
29
|
+
return (
|
|
30
|
+
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
31
|
+
{lines.map((line, index) => (
|
|
32
|
+
<Box key={index} height={1}>
|
|
33
|
+
<Text fg={colors.textDim}>{line}</Text>
|
|
34
|
+
</Box>
|
|
35
|
+
))}
|
|
36
|
+
</Box>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildInsiderTransactionTitle(transaction: InsiderTransaction): string {
|
|
41
|
+
const type = transactionTypeLabel(transaction.transactionType);
|
|
42
|
+
const price = transaction.pricePerShare != null
|
|
43
|
+
? ` @ ${formatCurrency(transaction.pricePerShare)}`
|
|
44
|
+
: "";
|
|
45
|
+
const value = transaction.totalValue != null
|
|
46
|
+
? ` | ${formatCurrency(transaction.totalValue)}`
|
|
47
|
+
: "";
|
|
48
|
+
return `${type} ${formatCompact(transaction.shares)} shares${price}${value}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildInsiderTransactionDetailBody(transaction: InsiderTransaction): string {
|
|
52
|
+
const lines = [
|
|
53
|
+
`Transaction: ${transactionTypeLabel(transaction.transactionType)}`,
|
|
54
|
+
`Date: ${formatFilingShortDate(transaction.filingDate)}`,
|
|
55
|
+
`Shares: ${formatCompact(transaction.shares)}`,
|
|
56
|
+
`Price/Share: ${transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}`,
|
|
57
|
+
`Total Value: ${transaction.totalValue != null ? formatCurrency(transaction.totalValue) : "—"}`,
|
|
58
|
+
`Shares Owned After: ${transaction.sharesOwned != null ? formatCompact(transaction.sharesOwned) : "—"}`,
|
|
59
|
+
];
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function wrapNoticeLines(text: string, width: number): string[] {
|
|
64
|
+
const maxWidth = Math.max(width, 12);
|
|
65
|
+
const words = text.trim().split(/\s+/);
|
|
66
|
+
const lines: string[] = [];
|
|
67
|
+
let current = "";
|
|
68
|
+
|
|
69
|
+
for (const word of words) {
|
|
70
|
+
if (!current) {
|
|
71
|
+
current = word;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (current.length + 1 + word.length <= maxWidth) {
|
|
75
|
+
current = `${current} ${word}`;
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(current);
|
|
78
|
+
current = word;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (current) lines.push(current);
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Text } from "../../ui";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GloomPlugin } from "../../types/plugin";
|
|
4
4
|
import type { SecFilingItem } from "../../types/data-provider";
|
|
5
5
|
import { useResolvedEntryValue, useSecFilingContent, useSecFilingsQuery } from "../../market-data/hooks";
|
|
6
6
|
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
@@ -13,6 +13,10 @@ import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
|
13
13
|
import { parseForm4Xml, transactionTypeLabel } from "./insider/insider-data";
|
|
14
14
|
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
15
15
|
import { createTickerSurfacePaneTemplate } from "./ticker-surface";
|
|
16
|
+
import {
|
|
17
|
+
formatFilingMetaDate,
|
|
18
|
+
renderFilingNotice,
|
|
19
|
+
} from "./sec-filing-display";
|
|
16
20
|
|
|
17
21
|
const SEC_FILING_LIMIT = 50;
|
|
18
22
|
const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
|
|
@@ -60,48 +64,8 @@ function getFilingDisplayTitle(filing: SecFilingItem): string {
|
|
|
60
64
|
return description ? `${formLabel} | ${description}` : formLabel;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
|
-
function wrapMessageLines(text: string, width: number): string[] {
|
|
64
|
-
const maxWidth = Math.max(width, 12);
|
|
65
|
-
const words = text.trim().split(/\s+/);
|
|
66
|
-
const lines: string[] = [];
|
|
67
|
-
let current = "";
|
|
68
|
-
|
|
69
|
-
for (const word of words) {
|
|
70
|
-
if (!current) {
|
|
71
|
-
current = word;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if ((current.length + 1 + word.length) <= maxWidth) {
|
|
75
|
-
current = `${current} ${word}`;
|
|
76
|
-
} else {
|
|
77
|
-
lines.push(current);
|
|
78
|
-
current = word;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (current) lines.push(current);
|
|
83
|
-
return lines;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function renderNotice(message: string, width: number) {
|
|
87
|
-
const lines = wrapMessageLines(message, width - 4);
|
|
88
|
-
return (
|
|
89
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
90
|
-
{lines.map((line, index) => (
|
|
91
|
-
<Box key={index} height={1}>
|
|
92
|
-
<Text fg={colors.textDim}>{line}</Text>
|
|
93
|
-
</Box>
|
|
94
|
-
))}
|
|
95
|
-
</Box>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
67
|
function formatFiledAt(filing: SecFilingItem): string {
|
|
100
|
-
return filing.filingDate
|
|
101
|
-
month: "short",
|
|
102
|
-
day: "numeric",
|
|
103
|
-
year: "numeric",
|
|
104
|
-
});
|
|
68
|
+
return formatFilingMetaDate(filing.filingDate);
|
|
105
69
|
}
|
|
106
70
|
|
|
107
71
|
function buildDetailBody(filing: SecFilingItem): string {
|
|
@@ -142,15 +106,6 @@ function buildForm4Detail(content: string | null, filing: SecFilingItem): string
|
|
|
142
106
|
return lines.join("\n");
|
|
143
107
|
}
|
|
144
108
|
|
|
145
|
-
function getFormCategory(form: string): "annual" | "quarterly" | "current" | "ownership" | "other" {
|
|
146
|
-
const f = form.trim().toUpperCase();
|
|
147
|
-
if (f === "10-K" || f === "10-K/A" || f === "20-F") return "annual";
|
|
148
|
-
if (f === "10-Q" || f === "10-Q/A") return "quarterly";
|
|
149
|
-
if (f.startsWith("8-K")) return "current";
|
|
150
|
-
if (OWNERSHIP_FORMS.has(f) || f === "SC 13G" || f === "SC 13D" || f === "SC 13G/A" || f === "SC 13D/A") return "ownership";
|
|
151
|
-
return "other";
|
|
152
|
-
}
|
|
153
|
-
|
|
154
109
|
function getFormDescription(form: string): string {
|
|
155
110
|
const f = form.trim().toUpperCase();
|
|
156
111
|
switch (f) {
|
|
@@ -226,7 +181,7 @@ function toFeedItems(
|
|
|
226
181
|
});
|
|
227
182
|
}
|
|
228
183
|
|
|
229
|
-
|
|
184
|
+
function SecView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
|
|
230
185
|
const { ticker } = usePaneTicker();
|
|
231
186
|
const selectionKey = `selectedIdx:${ticker?.metadata.ticker ?? "none"}`;
|
|
232
187
|
const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>(selectionKey, 0);
|
|
@@ -323,10 +278,10 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
|
|
|
323
278
|
});
|
|
324
279
|
|
|
325
280
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view SEC filings.</Text>;
|
|
326
|
-
if (!eligibleTicker) return
|
|
281
|
+
if (!eligibleTicker) return renderFilingNotice("SEC filings are only shown for US equities.", width);
|
|
327
282
|
if (loading && filings.length === 0) return <Spinner label="Loading SEC filings..." />;
|
|
328
|
-
if (error) return
|
|
329
|
-
if (filings.length === 0) return
|
|
283
|
+
if (error) return renderFilingNotice(`Error: ${error}`, width);
|
|
284
|
+
if (filings.length === 0) return renderFilingNotice(`No recent SEC filings for ${ticker.metadata.ticker}.`, width);
|
|
330
285
|
|
|
331
286
|
return (
|
|
332
287
|
<FeedDataTableStackView
|
|
@@ -344,14 +299,6 @@ export function SecView({ width, height, focused }: Pick<DetailTabProps, "width"
|
|
|
344
299
|
);
|
|
345
300
|
}
|
|
346
301
|
|
|
347
|
-
function SecTab(props: DetailTabProps) {
|
|
348
|
-
return <SecView {...props} />;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function SecPane({ focused, width, height }: PaneProps) {
|
|
352
|
-
return <SecView focused={focused} width={width} height={height} />;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
302
|
export const secPlugin: GloomPlugin = {
|
|
356
303
|
id: "sec",
|
|
357
304
|
name: "SEC",
|
|
@@ -364,7 +311,7 @@ export const secPlugin: GloomPlugin = {
|
|
|
364
311
|
id: "sec",
|
|
365
312
|
name: "SEC",
|
|
366
313
|
icon: "S",
|
|
367
|
-
component:
|
|
314
|
+
component: SecView,
|
|
368
315
|
defaultPosition: "right",
|
|
369
316
|
defaultMode: "floating",
|
|
370
317
|
defaultFloatingSize: { width: 100, height: 32 },
|
|
@@ -388,7 +335,7 @@ export const secPlugin: GloomPlugin = {
|
|
|
388
335
|
id: "sec",
|
|
389
336
|
name: "SEC",
|
|
390
337
|
order: 45,
|
|
391
|
-
component:
|
|
338
|
+
component: SecView,
|
|
392
339
|
isVisible: ({ ticker }) => isUsEquityTicker(ticker),
|
|
393
340
|
});
|
|
394
341
|
},
|
|
@@ -109,7 +109,7 @@ function nextSortPreference(current: SectorSortPreference, columnId: string): Se
|
|
|
109
109
|
return DEFAULT_SORT_PREFERENCE;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
113
113
|
const dataProvider = useAssetData();
|
|
114
114
|
const { navigateTicker } = usePluginTickerActions();
|
|
115
115
|
const [rows, setRows] = useState<SectorRow[]>(
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
2
2
|
import { normalizeTickerInput } from "../../../utils/ticker-search";
|
|
3
3
|
import { TickerDetailPane } from "./pane";
|
|
4
|
-
import { FinancialsTab } from "./financials-tab";
|
|
5
4
|
import { QuoteMonitorPane } from "./quote-monitor";
|
|
6
5
|
import {
|
|
7
6
|
buildQuoteMonitorSettingsDef,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box } from "../../../ui";
|
|
2
2
|
import { useShortcut } from "../../../react/input";
|
|
3
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
4
|
import type { DetailTabDef, PaneProps } from "../../../types/plugin";
|
|
5
5
|
import { quoteSubscriptionTargetFromTicker } from "../../../market-data/request-types";
|
|
6
6
|
import {
|
|
@@ -33,6 +33,29 @@ function sameStringSet(left: Set<string>, right: Set<string>): boolean {
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function registryDetailTabsSnapshot(registry: ReturnType<typeof getSharedRegistry>): string {
|
|
37
|
+
if (!registry) return "";
|
|
38
|
+
return [...registry.detailTabs.values()]
|
|
39
|
+
.map((tab) => `${tab.id}:${tab.name}:${tab.order}:${registry.getDetailTabPluginId?.(tab.id) ?? ""}`)
|
|
40
|
+
.join("\0");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function useRegistryDetailTabsSnapshot(registry: ReturnType<typeof getSharedRegistry>): string {
|
|
44
|
+
const subscribe = useCallback((onStoreChange: () => void) => {
|
|
45
|
+
const events = registry?.events;
|
|
46
|
+
if (!events) return () => {};
|
|
47
|
+
const unregisterRegistered = events.on("plugin:registered", onStoreChange);
|
|
48
|
+
const unregisterUnregistered = events.on("plugin:unregistered", onStoreChange);
|
|
49
|
+
return () => {
|
|
50
|
+
unregisterRegistered();
|
|
51
|
+
unregisterUnregistered();
|
|
52
|
+
};
|
|
53
|
+
}, [registry]);
|
|
54
|
+
|
|
55
|
+
const getSnapshot = useCallback(() => registryDetailTabsSnapshot(registry), [registry]);
|
|
56
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
37
60
|
const dispatch = useAppDispatch();
|
|
38
61
|
const config = useAppSelector((state) => state.config);
|
|
@@ -54,6 +77,7 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
54
77
|
|
|
55
78
|
const disabledPlugins = config.disabledPlugins;
|
|
56
79
|
const registry = getSharedRegistry();
|
|
80
|
+
const detailTabsSnapshot = useRegistryDetailTabsSnapshot(registry);
|
|
57
81
|
const pluginTabs = useMemo<DetailTabDef[]>(() => (
|
|
58
82
|
registry
|
|
59
83
|
? [...registry.detailTabs.values()].filter((tab) => {
|
|
@@ -61,7 +85,7 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
61
85
|
return !ownerId || !disabledPlugins.includes(ownerId);
|
|
62
86
|
})
|
|
63
87
|
: []
|
|
64
|
-
), [disabledPlugins, registry]);
|
|
88
|
+
), [disabledPlugins, registry, detailTabsSnapshot]);
|
|
65
89
|
const allTabs = buildVisibleDetailTabs(pluginTabs, ticker, financials, {
|
|
66
90
|
config,
|
|
67
91
|
hasOptionsChain,
|
|
@@ -84,26 +108,17 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
84
108
|
return next;
|
|
85
109
|
}, [mountedTabIds, resolvedTabId, visibleTabIds]);
|
|
86
110
|
|
|
87
|
-
const allTabsRef = useRef(allTabs);
|
|
88
|
-
allTabsRef.current = allTabs;
|
|
89
|
-
|
|
90
111
|
const stateRef = useRef({
|
|
91
112
|
focused,
|
|
92
113
|
chartInteractive,
|
|
93
114
|
pluginCaptured,
|
|
94
115
|
activeTabId: resolvedTabId,
|
|
95
|
-
tabIdx: Math.max(0, allTabs.findIndex((tab) => tab.id === resolvedTabId)),
|
|
96
|
-
allTabCount: allTabs.length,
|
|
97
|
-
hideTabs: paneSettings.hideTabs,
|
|
98
116
|
});
|
|
99
117
|
stateRef.current = {
|
|
100
118
|
focused,
|
|
101
119
|
chartInteractive,
|
|
102
120
|
pluginCaptured,
|
|
103
121
|
activeTabId: resolvedTabId,
|
|
104
|
-
tabIdx: Math.max(0, allTabs.findIndex((tab) => tab.id === resolvedTabId)),
|
|
105
|
-
allTabCount: allTabs.length,
|
|
106
|
-
hideTabs: paneSettings.hideTabs,
|
|
107
122
|
};
|
|
108
123
|
|
|
109
124
|
const setChartInteractiveEager = useCallback((value: boolean) => {
|
|
@@ -162,17 +177,7 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
162
177
|
if (currentState.chartInteractive) return;
|
|
163
178
|
}
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const tabs = allTabsRef.current;
|
|
168
|
-
if (event.name === "h" || event.name === "left") {
|
|
169
|
-
const nextIndex = Math.max(currentState.tabIdx - 1, 0);
|
|
170
|
-
setActiveTabId(tabs[nextIndex]!.id);
|
|
171
|
-
} else if (event.name === "l" || event.name === "right") {
|
|
172
|
-
const nextIndex = Math.min(currentState.tabIdx + 1, currentState.allTabCount - 1);
|
|
173
|
-
setActiveTabId(tabs[nextIndex]!.id);
|
|
174
|
-
}
|
|
175
|
-
}, [setActiveTabId, setChartInteractiveEager]);
|
|
180
|
+
}, [setChartInteractiveEager]);
|
|
176
181
|
|
|
177
182
|
useShortcut(handleKeyboard);
|
|
178
183
|
|
|
@@ -196,6 +201,7 @@ export function TickerDetailPane({ focused, width, height }: PaneProps) {
|
|
|
196
201
|
tabs={allTabs.map((tab) => ({ label: tab.name, value: tab.id }))}
|
|
197
202
|
activeValue={resolvedTabId}
|
|
198
203
|
onSelect={setActiveTabId}
|
|
204
|
+
focused={focused && !pluginCaptured && !(resolvedTabId === "chart" && chartInteractive)}
|
|
199
205
|
/>
|
|
200
206
|
)}
|
|
201
207
|
|