gloomberb 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -2
- package/bin/gloomberb +1 -1
- package/package.json +1 -1
- package/src/app/runtime/desktop-menu.ts +5 -1
- package/src/app/runtime/ticker-refresh.ts +4 -1
- package/src/app.tsx +20 -8
- package/src/brokers/account-cache.ts +1 -1
- package/src/brokers/sync-broker-instance.test.ts +107 -0
- package/src/brokers/sync-broker-instance.ts +22 -10
- package/src/capabilities/registry.ts +1 -0
- package/src/capabilities/types.ts +23 -0
- package/src/cli/commands/api.ts +119 -0
- package/src/cli/commands/automation.ts +173 -0
- package/src/cli/commands/command-utils.ts +53 -0
- package/src/cli/commands/market.ts +527 -0
- package/src/cli/commands/overview.ts +222 -0
- package/src/cli/commands/search.ts +39 -0
- package/src/cli/commands/system.ts +553 -0
- package/src/cli/commands/ticker.ts +104 -12
- package/src/cli/context.ts +35 -2
- package/src/cli/desktop-pane-shot.ts +2 -1
- package/src/cli/entry.ts +51 -0
- package/src/cli/errors.ts +56 -5
- package/src/cli/index.ts +73 -11
- package/src/cli/options.test.ts +38 -0
- package/src/cli/options.ts +95 -0
- package/src/cli/pane-functions/index.ts +10 -1
- package/src/cli/pane-functions/screenshot.ts +13 -1
- package/src/cli/registry.test.ts +40 -0
- package/src/cli/registry.ts +46 -5
- package/src/cli/result.test.ts +82 -0
- package/src/cli/result.ts +149 -0
- package/src/cli/types.ts +11 -0
- package/src/cli.test.ts +11 -11
- package/src/components/chart/bar-chart-renderer.test.ts +108 -3
- package/src/components/chart/bar-chart-renderer.ts +226 -27
- package/src/components/chart/comparison-stock-chart/interactions.ts +7 -1
- package/src/components/chart/comparison-stock-chart/legend.tsx +4 -0
- package/src/components/chart/comparison-stock-chart/render-data.ts +37 -8
- package/src/components/chart/comparison-stock-chart/selection-runtime.ts +19 -2
- package/src/components/chart/comparison-stock-chart/sources.ts +8 -4
- package/src/components/chart/comparison-stock-chart/toolbar.tsx +13 -3
- package/src/components/chart/comparison-stock-chart/view.tsx +12 -2
- package/src/components/chart/core/pane-settings.test.ts +27 -0
- package/src/components/chart/core/pane-settings.ts +60 -1
- package/src/components/chart/core/pointer.ts +34 -6
- package/src/components/chart/price-axis-labels.tsx +3 -1
- package/src/components/chart/static/bar-chart-surface.test.tsx +97 -0
- package/src/components/chart/static/bar-chart-surface.tsx +151 -28
- package/src/components/chart/static/chart/axis-overlays.tsx +243 -0
- package/src/components/chart/static/chart/surface.test.tsx +92 -0
- package/src/components/chart/static/chart/surface.tsx +167 -24
- package/src/components/chart/stock-chart/index.test.ts +25 -0
- package/src/components/chart/stock-chart/resolved/runtime.ts +1 -1
- package/src/components/chart/stock-chart/settings.ts +7 -3
- package/src/components/chart/stock-chart/viewport/runtime.ts +5 -5
- package/src/components/command-bar/layout-items/current-layout.ts +5 -1
- package/src/components/command-bar/pane-templates/workflow-route.ts +10 -0
- package/src/components/command-bar/surface/pane.test.tsx +76 -0
- package/src/components/data-table/view.tsx +13 -2
- package/src/components/feed-data-table/stack-view.tsx +9 -1
- package/src/components/index.ts +1 -1
- package/src/components/input-search-bar.test.tsx +103 -0
- package/src/components/input-search-bar.tsx +16 -0
- package/src/components/layout/detached-pane-shell.tsx +6 -3
- package/src/components/layout/floating-pane.tsx +3 -2
- package/src/components/layout/pane/content.tsx +20 -8
- package/src/components/layout/pane/footer/registration.tsx +6 -2
- package/src/components/layout/pane/frame.tsx +4 -4
- package/src/components/layout/pane/header.tsx +40 -4
- package/src/components/layout/pane/index.tsx +3 -1
- package/src/components/layout/pane/sizing.test.ts +43 -0
- package/src/components/layout/pane/sizing.ts +13 -4
- package/src/components/layout/shell/active-drag.ts +5 -1
- package/src/components/layout/shell/cursor-occlusion.ts +196 -0
- package/src/components/layout/shell/drag/runtime.ts +28 -3
- package/src/components/layout/shell/fullscreen.ts +10 -0
- package/src/components/layout/shell/index.test.tsx +487 -4
- package/src/components/layout/shell/index.tsx +165 -4
- package/src/components/layout/shell/layout-state.test.ts +33 -0
- package/src/components/layout/shell/layout-state.ts +4 -9
- package/src/components/layout/shell/native/pointer-runtime.ts +10 -4
- package/src/components/layout/shell/pane/layers.tsx +34 -16
- package/src/components/layout/shell/pane/management-shortcuts.ts +19 -0
- package/src/components/layout/shell/shortcuts.ts +4 -0
- package/src/components/layout/shell/terminal-pointer-runtime.ts +29 -19
- package/src/components/layout/shell/visible-layout.ts +17 -0
- package/src/components/layout/shell/window-mode/index.ts +11 -3
- package/src/components/layout/shell/window-mode/overlays.tsx +8 -1
- package/src/components/layout/status-bar.test.tsx +111 -1
- package/src/components/layout/status-bar.tsx +37 -7
- package/src/components/layout/transient-layout.tsx +57 -0
- package/src/components/layout/window-edit/mode.test.ts +76 -5
- package/src/components/layout/window-edit/mode.ts +47 -2
- package/src/components/layout/window-edit/presentation.ts +2 -1
- package/src/components/onboarding/onboarding-steps.tsx +8 -28
- package/src/components/onboarding/onboarding-wizard.test.tsx +13 -12
- package/src/components/onboarding/onboarding-wizard.tsx +2 -4
- package/src/components/onboarding/portfolio-step/index.tsx +0 -12
- package/src/components/onboarding/portfolio-step/types.ts +1 -3
- package/src/components/onboarding/wizard-keyboard.ts +2 -5
- package/src/components/price-performance.tsx +9 -1
- package/src/components/table-view-shared.tsx +12 -4
- package/src/components/ui/data-table/opentui/index.tsx +25 -12
- package/src/components/ui/data-table/types.ts +4 -0
- package/src/components/ui/fields.tsx +10 -3
- package/src/components/ui/page-stack-view.tsx +6 -2
- package/src/components/ui/table-layout.test.ts +25 -0
- package/src/components/ui/table-layout.ts +22 -6
- package/src/components/ui/tabs.tsx +26 -10
- package/src/components/ui/ui.test.tsx +87 -0
- package/src/core/app-services.ts +7 -4
- package/src/core/state/app/layout-reducer.ts +2 -2
- package/src/data/config/store/index.ts +1 -2
- package/src/plugins/builtin/analytics/index.test.tsx +6 -2
- package/src/plugins/builtin/analytics/pane-model.ts +43 -8
- package/src/plugins/builtin/cloud-tweets/pane.tsx +2 -0
- package/src/plugins/builtin/cloud-tweets/search-bar.tsx +3 -0
- package/src/plugins/builtin/cloud-tweets/table.tsx +14 -2
- package/src/plugins/builtin/comparison-chart/index.test.tsx +130 -6
- package/src/plugins/builtin/kelly-sizer/constants.ts +3 -0
- package/src/plugins/builtin/kelly-sizer/fields.ts +214 -0
- package/src/plugins/builtin/kelly-sizer/index.test.tsx +197 -0
- package/src/plugins/builtin/kelly-sizer/index.tsx +47 -0
- package/src/plugins/builtin/kelly-sizer/model.test.ts +152 -0
- package/src/plugins/builtin/kelly-sizer/model.ts +580 -0
- package/src/plugins/builtin/kelly-sizer/pane.tsx +558 -0
- package/src/plugins/builtin/kelly-sizer/portfolio.ts +71 -0
- package/src/plugins/builtin/kelly-sizer/sections.tsx +142 -0
- package/src/plugins/builtin/kelly-sizer/state.ts +67 -0
- package/src/plugins/builtin/kelly-sizer/test-support.tsx +174 -0
- package/src/plugins/builtin/kelly-sizer/types.ts +156 -0
- package/src/plugins/builtin/kelly-sizer/view.test.tsx +77 -0
- package/src/plugins/builtin/kelly-sizer/view.tsx +375 -0
- package/src/plugins/builtin/layout-manager/index.tsx +5 -1
- package/src/plugins/builtin/news/wire/read-state.ts +9 -32
- package/src/plugins/builtin/options/view.test.tsx +141 -4
- package/src/plugins/builtin/options/view.tsx +76 -15
- package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +26 -1
- package/src/plugins/builtin/portfolio-list/account-metrics.ts +14 -0
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +23 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +19 -2
- package/src/plugins/builtin/portfolio-list/index.test.ts +23 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +3 -1
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +2 -7
- package/src/plugins/builtin/portfolio-list/summary/index.test.ts +59 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +36 -5
- package/src/plugins/builtin/shared/read-state.ts +43 -0
- package/src/plugins/builtin/substack/article-detail.test.tsx +10 -0
- package/src/plugins/builtin/substack/article-detail.tsx +39 -15
- package/src/plugins/builtin/substack/article-stack.tsx +6 -2
- package/src/plugins/builtin/substack/feed-tabs.tsx +12 -9
- package/src/plugins/builtin/substack/pane.tsx +8 -3
- package/src/plugins/builtin/substack/read-state.ts +55 -0
- package/src/plugins/builtin/thirteenf/pane.test.tsx +26 -0
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -1
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/content.tsx +149 -48
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph/model.ts +25 -2
- package/src/plugins/builtin/ticker-detail/data-panes/fundamental-graph.tsx +5 -4
- package/src/plugins/builtin/ticker-detail/data-panes.test.ts +16 -0
- package/src/plugins/builtin/ticker-detail/index.test.tsx +143 -0
- package/src/plugins/catalog-ui.ts +2 -0
- package/src/plugins/ibkr/broker-adapter.ts +19 -0
- package/src/plugins/ibkr/config.test.ts +31 -2
- package/src/plugins/ibkr/config.ts +9 -2
- package/src/plugins/ibkr/flex/index.test.ts +18 -9
- package/src/plugins/ibkr/flex/index.ts +30 -0
- package/src/plugins/ibkr/flex/statement-client.ts +68 -14
- package/src/plugins/ibkr/gateway/account-loaders.ts +1 -1
- package/src/plugins/ibkr/gateway/account-summary.ts +1 -0
- package/src/plugins/ibkr/gateway/service/index.test.ts +8 -0
- package/src/plugins/pane-manager/docking.ts +9 -2
- package/src/plugins/pane-manager/gridlock.ts +13 -5
- package/src/plugins/pane-manager/layout-state.ts +25 -0
- package/src/plugins/pane-manager/queries.ts +8 -2
- package/src/plugins/pane-manager.test.ts +33 -0
- package/src/plugins/pane-manager.ts +6 -1
- package/src/plugins/prediction-markets/chart.tsx +10 -4
- package/src/plugins/prediction-markets/controller/index.ts +2 -0
- package/src/plugins/prediction-markets/controller/keyboard.ts +34 -5
- package/src/plugins/prediction-markets/detail/book.tsx +141 -96
- package/src/plugins/prediction-markets/detail/overview.tsx +3 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +71 -64
- package/src/plugins/prediction-markets/detail/shared.tsx +19 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +64 -19
- package/src/plugins/prediction-markets/detail.test.tsx +50 -0
- package/src/plugins/prediction-markets/pane.test.tsx +75 -0
- package/src/plugins/prediction-markets/pane.tsx +71 -23
- package/src/plugins/prediction-markets/test-helpers.tsx +42 -23
- package/src/renderers/electrobun/bun/application-menu/click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/click.ts +1 -0
- package/src/renderers/electrobun/bun/application-menu/index.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
- package/src/renderers/electrobun/bun/index.ts +13 -0
- package/src/renderers/electrobun/bun/window/focus.test.ts +35 -0
- package/src/renderers/electrobun/bun/window/focus.ts +5 -0
- package/src/renderers/electrobun/view/app-services.ts +6 -4
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +13 -2
- package/src/renderers/electrobun/view/data-table/index.tsx +19 -4
- package/src/renderers/electrobun/view/data-table/row.tsx +30 -6
- package/src/renderers/electrobun/view/desktop/controls.tsx +11 -5
- package/src/renderers/electrobun/view/host/ascii-text.ts +50 -0
- package/src/renderers/electrobun/view/host/input.tsx +115 -18
- package/src/renderers/electrobun/view/host/tabs.tsx +7 -2
- package/src/renderers/electrobun/view/host/text.test.ts +22 -0
- package/src/renderers/electrobun/view/host/text.tsx +24 -20
- package/src/renderers/electrobun/view/styles.css +11 -0
- package/src/renderers/electrobun/view/ui-host.tsx +1 -1
- package/src/renderers/opentui/host.tsx +17 -1
- package/src/renderers/opentui/start.tsx +13 -5
- package/src/renderers/opentui/ui-host.tsx +57 -25
- package/src/sources/provider-router/batches.ts +16 -2
- package/src/sources/provider-router/financial-routes.ts +5 -0
- package/src/sources/provider-router/financials.ts +11 -23
- package/src/sources/provider-router/index.test.ts +189 -0
- package/src/sources/provider-router/primary.ts +3 -2
- package/src/sources/sec-edgar.test.ts +83 -0
- package/src/sources/sec-edgar.ts +231 -0
- package/src/sources/yahoo-finance.test.ts +51 -0
- package/src/sources/yahoo-finance.ts +49 -1
- package/src/state/app/context/index.test.tsx +5 -8
- package/src/state/app/input-capture.ts +12 -0
- package/src/types/broker.ts +1 -0
- package/src/types/config.ts +10 -0
- package/src/types/plugin.ts +28 -0
- package/src/types/trading.ts +2 -0
- package/src/ui/host.tsx +16 -0
- package/src/ui/index.tsx +2 -0
- package/src/utils/cli-output.ts +6 -0
- package/src/utils/financial-statements.ts +23 -0
- package/src/utils/search-focus-navigation.ts +24 -0
- package/src/version.ts +1 -1
- package/src/components/onboarding/portfolio-step/manual-name-panel.tsx +0 -62
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, type RefObject } from "react";
|
|
1
|
+
import { memo, useCallback, useMemo, type RefObject } from "react";
|
|
2
2
|
import { Box, ScrollBox, Text, TextAttributes, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
|
|
3
3
|
import { Spinner } from "../../../components";
|
|
4
4
|
import { RemoteImage } from "../../../components/ui";
|
|
@@ -38,6 +38,25 @@ function wrappedTextProps(width: number) {
|
|
|
38
38
|
} as const;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
const ARTICLE_IMAGE_MAX_WIDTH = 152;
|
|
42
|
+
const ARTICLE_IMAGE_MAX_HEIGHT = 26;
|
|
43
|
+
const ARTICLE_IMAGE_MIN_HEIGHT = 6;
|
|
44
|
+
const ARTICLE_IMAGE_LEGACY_HEIGHT_CAP = 14;
|
|
45
|
+
|
|
46
|
+
export function resolveArticleImageSize(lineWidth: number) {
|
|
47
|
+
const safeLineWidth = Math.max(1, Math.floor(lineWidth));
|
|
48
|
+
const width = Math.min(safeLineWidth, ARTICLE_IMAGE_MAX_WIDTH);
|
|
49
|
+
const legacyHeight = Math.min(ARTICLE_IMAGE_LEGACY_HEIGHT_CAP, Math.floor(width * 0.32));
|
|
50
|
+
const expandedHeight = Math.floor(width * 0.18);
|
|
51
|
+
return {
|
|
52
|
+
width,
|
|
53
|
+
height: Math.max(ARTICLE_IMAGE_MIN_HEIGHT, Math.min(
|
|
54
|
+
ARTICLE_IMAGE_MAX_HEIGHT,
|
|
55
|
+
Math.max(legacyHeight, expandedHeight),
|
|
56
|
+
)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
function normalizedTwitterUsername(username: string | null | undefined): string | null {
|
|
42
61
|
const normalized = username?.trim().replace(/^@/, "") ?? "";
|
|
43
62
|
return /^[A-Za-z0-9_]{1,15}$/.test(normalized) ? normalized : null;
|
|
@@ -251,7 +270,7 @@ function ArticleBlockView({
|
|
|
251
270
|
}
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
function ArticleRichContent({
|
|
273
|
+
const ArticleRichContent = memo(function ArticleRichContent({
|
|
255
274
|
blocks,
|
|
256
275
|
fallbackText,
|
|
257
276
|
fallbackImageUrls,
|
|
@@ -269,14 +288,20 @@ function ArticleRichContent({
|
|
|
269
288
|
imageHeight: number;
|
|
270
289
|
}) {
|
|
271
290
|
const rendererHost = useRendererHost();
|
|
272
|
-
const resolvedBlocks =
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
291
|
+
const resolvedBlocks = useMemo(() => (
|
|
292
|
+
blocks.length > 0
|
|
293
|
+
? blocks
|
|
294
|
+
: [
|
|
295
|
+
...(fallbackText ? [{ type: "paragraph" as const, text: fallbackText }] : []),
|
|
296
|
+
...fallbackImageUrls.map((url): SubstackContentBlock => ({ type: "image", url, alt: articleTitle })),
|
|
297
|
+
]
|
|
298
|
+
), [articleTitle, blocks, fallbackImageUrls, fallbackText]);
|
|
299
|
+
const tickerText = useMemo(
|
|
300
|
+
() => resolvedBlocks.map(articleBlockText).filter(Boolean).join("\n"),
|
|
301
|
+
[resolvedBlocks],
|
|
302
|
+
);
|
|
303
|
+
const tickerTexts = useMemo(() => [tickerText], [tickerText]);
|
|
304
|
+
const { catalog, openTicker } = useInlineTickers(tickerTexts, { liveQuotes: false });
|
|
280
305
|
const openLink = useCallback((url: string) => {
|
|
281
306
|
void rendererHost.openExternal(url);
|
|
282
307
|
}, [rendererHost]);
|
|
@@ -307,9 +332,9 @@ function ArticleRichContent({
|
|
|
307
332
|
))}
|
|
308
333
|
</Box>
|
|
309
334
|
);
|
|
310
|
-
}
|
|
335
|
+
});
|
|
311
336
|
|
|
312
|
-
export function ArticleDetail({
|
|
337
|
+
export const ArticleDetail = memo(function ArticleDetail({
|
|
313
338
|
article,
|
|
314
339
|
detail,
|
|
315
340
|
width,
|
|
@@ -331,8 +356,7 @@ export function ArticleDetail({
|
|
|
331
356
|
const blocks = resolved?.contentBlocks ?? [];
|
|
332
357
|
const fallbackImageUrls = resolved?.imageUrls.length ? resolved.imageUrls : article.imageUrls;
|
|
333
358
|
const lineWidth = Math.max(1, width - 2);
|
|
334
|
-
const imageWidth =
|
|
335
|
-
const imageHeight = Math.max(6, Math.min(14, Math.floor(imageWidth * 0.32)));
|
|
359
|
+
const { width: imageWidth, height: imageHeight } = resolveArticleImageSize(lineWidth);
|
|
336
360
|
|
|
337
361
|
return (
|
|
338
362
|
<ScrollBox ref={scrollRef} scrollY focusable={false} flexGrow={1} paddingX={1}>
|
|
@@ -356,4 +380,4 @@ export function ArticleDetail({
|
|
|
356
380
|
</Box>
|
|
357
381
|
</ScrollBox>
|
|
358
382
|
);
|
|
359
|
-
}
|
|
383
|
+
});
|
|
@@ -24,6 +24,7 @@ export function SubstackArticleStack({
|
|
|
24
24
|
detailOpen,
|
|
25
25
|
selectedArticle,
|
|
26
26
|
selectedArticleId,
|
|
27
|
+
readArticleIds,
|
|
27
28
|
detailContent,
|
|
28
29
|
activePublication,
|
|
29
30
|
activeFeedState,
|
|
@@ -45,6 +46,7 @@ export function SubstackArticleStack({
|
|
|
45
46
|
detailOpen: boolean;
|
|
46
47
|
selectedArticle: SubstackArticleSummary | null;
|
|
47
48
|
selectedArticleId: string | null;
|
|
49
|
+
readArticleIds: ReadonlySet<string>;
|
|
48
50
|
detailContent: ReactNode;
|
|
49
51
|
activePublication: SubstackPublication | null;
|
|
50
52
|
activeFeedState: ActiveFeedState;
|
|
@@ -78,7 +80,9 @@ export function SubstackArticleStack({
|
|
|
78
80
|
return {
|
|
79
81
|
text: article.title,
|
|
80
82
|
color: selectedColor ?? colors.text,
|
|
81
|
-
attributes:
|
|
83
|
+
attributes: readArticleIds.has(article.id)
|
|
84
|
+
? TextAttributes.NONE
|
|
85
|
+
: TextAttributes.BOLD,
|
|
82
86
|
};
|
|
83
87
|
case "read":
|
|
84
88
|
return {
|
|
@@ -86,7 +90,7 @@ export function SubstackArticleStack({
|
|
|
86
90
|
color: selectedColor ?? colors.textDim,
|
|
87
91
|
};
|
|
88
92
|
}
|
|
89
|
-
}, []);
|
|
93
|
+
}, [readArticleIds]);
|
|
90
94
|
|
|
91
95
|
const bodyAfter = activePublication && sortedRows.length > 0 && (activeFeedState.loading || activeFeedState.loadingMore) ? (
|
|
92
96
|
<Box height={1} paddingX={1}>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memo, useMemo } from "react";
|
|
1
2
|
import { Box } from "../../../ui";
|
|
2
3
|
import { Tabs } from "../../../components";
|
|
3
4
|
import { tabIdForPublication } from "./table";
|
|
@@ -7,7 +8,7 @@ import {
|
|
|
7
8
|
} from "./types";
|
|
8
9
|
import { tabLabel } from "./pane-state";
|
|
9
10
|
|
|
10
|
-
export function SubstackFeedTabs({
|
|
11
|
+
export const SubstackFeedTabs = memo(function SubstackFeedTabs({
|
|
11
12
|
subscriptions,
|
|
12
13
|
activeTab,
|
|
13
14
|
focused,
|
|
@@ -20,16 +21,18 @@ export function SubstackFeedTabs({
|
|
|
20
21
|
detailOpen: boolean;
|
|
21
22
|
onSelect: (tabId: string) => void;
|
|
22
23
|
}) {
|
|
24
|
+
const tabs = useMemo(() => [
|
|
25
|
+
{ label: "Feed", value: SUBSTACK_FEED_TAB_ID },
|
|
26
|
+
...subscriptions.map((publication) => ({
|
|
27
|
+
label: tabLabel(publication.name),
|
|
28
|
+
value: tabIdForPublication(publication),
|
|
29
|
+
})),
|
|
30
|
+
], [subscriptions]);
|
|
31
|
+
|
|
23
32
|
return (
|
|
24
33
|
<Box height={1}>
|
|
25
34
|
<Tabs
|
|
26
|
-
tabs={
|
|
27
|
-
{ label: "Feed", value: SUBSTACK_FEED_TAB_ID },
|
|
28
|
-
...subscriptions.map((publication) => ({
|
|
29
|
-
label: tabLabel(publication.name),
|
|
30
|
-
value: tabIdForPublication(publication),
|
|
31
|
-
})),
|
|
32
|
-
]}
|
|
35
|
+
tabs={tabs}
|
|
33
36
|
activeValue={activeTab}
|
|
34
37
|
onSelect={onSelect}
|
|
35
38
|
compact
|
|
@@ -38,4 +41,4 @@ export function SubstackFeedTabs({
|
|
|
38
41
|
/>
|
|
39
42
|
</Box>
|
|
40
43
|
);
|
|
41
|
-
}
|
|
44
|
+
});
|
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
type LoadState,
|
|
59
59
|
type PublicationFeedState,
|
|
60
60
|
} from "./pane-state";
|
|
61
|
+
import { useSubstackReadState } from "./read-state";
|
|
61
62
|
|
|
62
63
|
const PUBLICATION_LOAD_MORE_THRESHOLD_ROWS = 8;
|
|
63
64
|
|
|
@@ -74,6 +75,7 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
74
75
|
columnId: "published",
|
|
75
76
|
direction: "desc",
|
|
76
77
|
});
|
|
78
|
+
const { readArticleIds, markArticleRead } = useSubstackReadState();
|
|
77
79
|
const homeFetchGenRef = useRef(0);
|
|
78
80
|
const publicationFetchGenRef = useRef<Record<string, number>>({});
|
|
79
81
|
const detailFetchGenRef = useRef(0);
|
|
@@ -206,12 +208,12 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
206
208
|
loadPublication(activePublication, false);
|
|
207
209
|
}, [activePublication, activePublicationTabId, loadPublication, publicationFeeds]);
|
|
208
210
|
|
|
209
|
-
const activeFeedState = activeFeedStateFromSources({
|
|
211
|
+
const activeFeedState = useMemo(() => activeFeedStateFromSources({
|
|
210
212
|
activePublication,
|
|
211
213
|
activePublicationTabId,
|
|
212
214
|
publicationFeeds,
|
|
213
215
|
home,
|
|
214
|
-
});
|
|
216
|
+
}), [activePublication, activePublicationTabId, home, publicationFeeds]);
|
|
215
217
|
|
|
216
218
|
const loadMoreActivePublicationRows = useCallback(() => {
|
|
217
219
|
if (!activePublication || !activePublicationTabId || detailOpen) return;
|
|
@@ -335,8 +337,9 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
335
337
|
|
|
336
338
|
const openSelectedArticle = useCallback(() => {
|
|
337
339
|
if (!selectedArticle?.url) return;
|
|
340
|
+
markArticleRead(selectedArticle.id);
|
|
338
341
|
void rendererHost.openExternal(selectedArticle.url);
|
|
339
|
-
}, [rendererHost, selectedArticle]);
|
|
342
|
+
}, [markArticleRead, rendererHost, selectedArticle]);
|
|
340
343
|
|
|
341
344
|
const handleLogin = useCallback((nextAuth: SubstackAuthState) => {
|
|
342
345
|
setAuth(nextAuth);
|
|
@@ -477,7 +480,9 @@ export function SubstackPane({ focused, width, height }: PaneProps) {
|
|
|
477
480
|
selectedArticle={selectedArticle}
|
|
478
481
|
detailContent={detailContent}
|
|
479
482
|
selectedArticleId={selectedArticleId}
|
|
483
|
+
readArticleIds={readArticleIds}
|
|
480
484
|
onActivate={(article) => {
|
|
485
|
+
markArticleRead(article.id);
|
|
481
486
|
setSelectedArticleId(article.id, { immediate: true });
|
|
482
487
|
setDetailOpen(true);
|
|
483
488
|
}}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { usePluginState } from "../../runtime";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_MAX_READ_IDS,
|
|
5
|
+
markReadId,
|
|
6
|
+
normalizeReadIds,
|
|
7
|
+
sameStringArray,
|
|
8
|
+
} from "../shared/read-state";
|
|
9
|
+
|
|
10
|
+
export interface SubstackReadState {
|
|
11
|
+
articleIds: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SUBSTACK_READ_STATE_SCHEMA_VERSION = 1;
|
|
15
|
+
const READ_STATE_KEY = "read-articles";
|
|
16
|
+
const EMPTY_READ_STATE: SubstackReadState = { articleIds: [] };
|
|
17
|
+
|
|
18
|
+
export const MAX_READ_SUBSTACK_ARTICLE_IDS = DEFAULT_MAX_READ_IDS;
|
|
19
|
+
|
|
20
|
+
export function normalizeSubstackReadState(state: SubstackReadState): SubstackReadState {
|
|
21
|
+
return {
|
|
22
|
+
articleIds: normalizeReadIds(state.articleIds, MAX_READ_SUBSTACK_ARTICLE_IDS),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function markSubstackArticleRead(
|
|
27
|
+
state: SubstackReadState,
|
|
28
|
+
articleId: string,
|
|
29
|
+
): SubstackReadState {
|
|
30
|
+
const current = normalizeSubstackReadState(state).articleIds;
|
|
31
|
+
const articleIds = markReadId(current, articleId, MAX_READ_SUBSTACK_ARTICLE_IDS);
|
|
32
|
+
|
|
33
|
+
return sameStringArray(articleIds, state.articleIds) ? state : { articleIds };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useSubstackReadState() {
|
|
37
|
+
const [readState, setReadState] = usePluginState<SubstackReadState>(
|
|
38
|
+
READ_STATE_KEY,
|
|
39
|
+
EMPTY_READ_STATE,
|
|
40
|
+
{ schemaVersion: SUBSTACK_READ_STATE_SCHEMA_VERSION },
|
|
41
|
+
);
|
|
42
|
+
const normalizedReadState = useMemo(
|
|
43
|
+
() => normalizeSubstackReadState(readState),
|
|
44
|
+
[readState],
|
|
45
|
+
);
|
|
46
|
+
const readArticleIds = useMemo(
|
|
47
|
+
() => new Set(normalizedReadState.articleIds),
|
|
48
|
+
[normalizedReadState],
|
|
49
|
+
);
|
|
50
|
+
const markArticleRead = useCallback((articleId: string) => {
|
|
51
|
+
setReadState((current) => markSubstackArticleRead(current, articleId));
|
|
52
|
+
}, [setReadState]);
|
|
53
|
+
|
|
54
|
+
return { readArticleIds, markArticleRead };
|
|
55
|
+
}
|
|
@@ -224,6 +224,32 @@ describe("ThirteenFPane", () => {
|
|
|
224
224
|
).toBeUndefined();
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
+
test("moves from the first fund row to search with Up", async () => {
|
|
228
|
+
setHttpFetchTransport(async (url) => {
|
|
229
|
+
const path = new URL(String(url)).pathname;
|
|
230
|
+
if (path.endsWith("/topfunds")) {
|
|
231
|
+
return json([
|
|
232
|
+
{ cik: "1", name: "Alpha Capital", period_of_report: "2026-03-31", pnl: null },
|
|
233
|
+
]);
|
|
234
|
+
}
|
|
235
|
+
return json([]);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await act(async () => {
|
|
239
|
+
testSetup = await testRender(<Harness />, { width: 100, height: 20 });
|
|
240
|
+
});
|
|
241
|
+
await renderFrames();
|
|
242
|
+
|
|
243
|
+
await emitKeypress({ name: "up", sequence: "\u001B[A" });
|
|
244
|
+
await act(async () => {
|
|
245
|
+
await testSetup!.mockInput.typeText("BRK");
|
|
246
|
+
await testSetup!.renderOnce();
|
|
247
|
+
});
|
|
248
|
+
await renderFrames(2);
|
|
249
|
+
|
|
250
|
+
expect(testSetup!.captureCharFrame()).toContain("BRK");
|
|
251
|
+
});
|
|
252
|
+
|
|
227
253
|
test("rapid keyboard navigation activates the current rendered row", async () => {
|
|
228
254
|
setHttpFetchTransport(async (url) => {
|
|
229
255
|
const path = new URL(String(url)).pathname;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Tabs,
|
|
16
16
|
usePaneFooter,
|
|
17
17
|
type DataTableKeyEvent,
|
|
18
|
+
type DataTableRootKeyContext,
|
|
18
19
|
} from "../../../components";
|
|
19
20
|
import { useShortcut } from "../../../react/input";
|
|
20
21
|
import { usePaneSettingValue } from "../../../state/app/context";
|
|
@@ -23,6 +24,7 @@ import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
|
|
|
23
24
|
import type { PaneProps } from "../../../types/plugin";
|
|
24
25
|
import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
|
|
25
26
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
27
|
+
import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
|
|
26
28
|
import { truncateWithEllipsis } from "../../../utils/text-wrap";
|
|
27
29
|
import { usePluginPaneState, usePluginTickerActions } from "../../runtime";
|
|
28
30
|
import { loadBrowserRows, loadFilingPositions, loadFundDetail } from "./data";
|
|
@@ -303,6 +305,7 @@ export function ThirteenFPane({ focused, width, height }: PaneProps) {
|
|
|
303
305
|
normalizeValue={trimSearchValue}
|
|
304
306
|
onFocus={focusSearch}
|
|
305
307
|
onBlur={blurSearch}
|
|
308
|
+
onNavigateDown={blurSearch}
|
|
306
309
|
onQueryChange={updateQuery}
|
|
307
310
|
/>
|
|
308
311
|
</Box>
|
|
@@ -312,7 +315,15 @@ export function ThirteenFPane({ focused, width, height }: PaneProps) {
|
|
|
312
315
|
? "Loading 13F funds..."
|
|
313
316
|
: error ?? warning ?? "No 13F funds found.";
|
|
314
317
|
|
|
315
|
-
const handleRootKeyDown = useCallback((
|
|
318
|
+
const handleRootKeyDown = useCallback((
|
|
319
|
+
event: DataTableKeyEvent,
|
|
320
|
+
context: DataTableRootKeyContext,
|
|
321
|
+
) => {
|
|
322
|
+
if (context.selectedIndex <= 0 && isPlainArrowUp(event)) {
|
|
323
|
+
stopSearchFocusNavigation(event);
|
|
324
|
+
focusSearch();
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
316
327
|
if (event.name === "r") {
|
|
317
328
|
event.preventDefault?.();
|
|
318
329
|
event.stopPropagation?.();
|
|
@@ -1,23 +1,25 @@
|
|
|
1
|
-
import { useCallback, useMemo } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { Box, TextAttributes } from "../../../../../ui";
|
|
3
3
|
import {
|
|
4
4
|
DataTableView,
|
|
5
|
+
Tabs,
|
|
5
6
|
usePaneFooter,
|
|
6
7
|
type DataTableCell,
|
|
7
8
|
type DataTableKeyEvent,
|
|
8
9
|
} from "../../../../../components";
|
|
10
|
+
import { useShortcut } from "../../../../../react/input";
|
|
11
|
+
import type { BarChartHover } from "../../../../../components/chart/bar-chart-renderer";
|
|
9
12
|
import { StaticBarChartSurface } from "../../../../../components/chart/static/bar-chart-surface";
|
|
10
13
|
import { colors, priceColor } from "../../../../../theme/colors";
|
|
11
14
|
import { usePluginPaneState } from "../../../../runtime";
|
|
12
15
|
import { loadingErrorFooterInfo, refreshFooterHint, useClampSelectedIndex } from "../../../shared/table-pane";
|
|
13
16
|
import {
|
|
17
|
+
allMetricDefs,
|
|
14
18
|
buildFundamentalColumns,
|
|
15
19
|
buildGraphBarSeries,
|
|
16
|
-
defaultMetric,
|
|
17
20
|
formatMaybePercent,
|
|
18
|
-
isMetricForKind,
|
|
19
21
|
metricDef,
|
|
20
|
-
|
|
22
|
+
metricKind,
|
|
21
23
|
} from "./model";
|
|
22
24
|
import type {
|
|
23
25
|
FundamentalColumn,
|
|
@@ -41,6 +43,7 @@ export function FundamentalGraphContent({
|
|
|
41
43
|
setMetric,
|
|
42
44
|
chartKind,
|
|
43
45
|
setChartKind,
|
|
46
|
+
onCapture,
|
|
44
47
|
}: {
|
|
45
48
|
focused: boolean;
|
|
46
49
|
width: number;
|
|
@@ -55,39 +58,106 @@ export function FundamentalGraphContent({
|
|
|
55
58
|
setMetric: (updater: (current: GraphMetricKey) => GraphMetricKey) => void;
|
|
56
59
|
chartKind: GraphKind;
|
|
57
60
|
setChartKind: (updater: (current: GraphKind) => GraphKind) => void;
|
|
61
|
+
onCapture?: (capturing: boolean) => void;
|
|
58
62
|
}) {
|
|
59
63
|
const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>("selectedIdx", 0);
|
|
64
|
+
const [hoveredBar, setHoveredBar] = useState<BarChartHover | null>(null);
|
|
65
|
+
const [metricTabsFocused, setMetricTabsFocused] = useState(false);
|
|
60
66
|
const definition = metricDef(chartKind, metric);
|
|
67
|
+
const metricOptions = useMemo(() => allMetricDefs(), []);
|
|
68
|
+
const metricTabs = useMemo(() => metricOptions.map(({ definition }) => ({
|
|
69
|
+
label: definition.label,
|
|
70
|
+
value: definition.key,
|
|
71
|
+
})), [metricOptions]);
|
|
61
72
|
const multiSymbol = new Set(rows.map((row) => row.symbol)).size > 1;
|
|
62
73
|
const columns = useMemo(() => buildFundamentalColumns(width, multiSymbol), [multiSymbol, width]);
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
const tableRows = useMemo(() => [...rows].sort((left, right) => (
|
|
75
|
+
right.date.localeCompare(left.date) || left.symbol.localeCompare(right.symbol)
|
|
76
|
+
)), [rows]);
|
|
77
|
+
const boundedSelectedIdx = tableRows.length > 0 ? Math.min(selectedIdx, tableRows.length - 1) : -1;
|
|
78
|
+
const minTableHeight = tableRows.length > 0 ? 4 : 3;
|
|
79
|
+
const desiredChartHeight = height >= 14 ? Math.min(16, Math.max(9, Math.floor(height * 0.58))) : Math.max(4, Math.floor(height / 2));
|
|
80
|
+
const chartHeight = Math.max(1, Math.min(desiredChartHeight, Math.max(1, height - minTableHeight)));
|
|
81
|
+
const tableHeight = Math.max(0, height - chartHeight);
|
|
66
82
|
const chartSeries = useMemo(() => buildGraphBarSeries(rows), [rows]);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
const selectMetric = useCallback((value: string) => {
|
|
84
|
+
const nextMetric = value as GraphMetricKey;
|
|
85
|
+
const nextKind = metricKind(nextMetric);
|
|
86
|
+
if (!nextKind) return;
|
|
87
|
+
setHoveredBar(null);
|
|
71
88
|
setChartKind(() => nextKind);
|
|
72
|
-
setMetric((
|
|
73
|
-
}, [
|
|
89
|
+
setMetric(() => nextMetric);
|
|
90
|
+
}, [setChartKind, setMetric]);
|
|
91
|
+
const focusMetricTabs = useCallback(() => {
|
|
92
|
+
if (metricTabsFocused) return;
|
|
93
|
+
setMetricTabsFocused(true);
|
|
94
|
+
onCapture?.(true);
|
|
95
|
+
}, [metricTabsFocused, onCapture]);
|
|
96
|
+
const releaseMetricTabs = useCallback(() => {
|
|
97
|
+
if (!metricTabsFocused) return;
|
|
98
|
+
setMetricTabsFocused(false);
|
|
99
|
+
onCapture?.(false);
|
|
100
|
+
}, [metricTabsFocused, onCapture]);
|
|
101
|
+
const selectMetricFromTabs = useCallback((value: string) => {
|
|
102
|
+
focusMetricTabs();
|
|
103
|
+
selectMetric(value);
|
|
104
|
+
}, [focusMetricTabs, selectMetric]);
|
|
105
|
+
const togglePeriod = useCallback(() => setPeriod((current) => current === "annual" ? "quarterly" : "annual"), [setPeriod]);
|
|
106
|
+
const hoveredValue = useMemo(() => {
|
|
107
|
+
if (!hoveredBar) return null;
|
|
108
|
+
const label = chartSeries.length > 1 ? `${hoveredBar.category} ${hoveredBar.seriesLabel}` : hoveredBar.category;
|
|
109
|
+
return `${label} ${definition.format(hoveredBar.value)}`;
|
|
110
|
+
}, [chartSeries.length, definition, hoveredBar]);
|
|
111
|
+
|
|
112
|
+
useClampSelectedIndex(tableRows.length, selectedIdx, setSelectedIdx);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!focused && metricTabsFocused) {
|
|
116
|
+
setMetricTabsFocused(false);
|
|
117
|
+
onCapture?.(false);
|
|
118
|
+
}
|
|
119
|
+
}, [focused, metricTabsFocused, onCapture]);
|
|
120
|
+
|
|
121
|
+
useEffect(() => () => {
|
|
122
|
+
onCapture?.(false);
|
|
123
|
+
}, [onCapture]);
|
|
74
124
|
|
|
75
|
-
|
|
125
|
+
useShortcut((event) => {
|
|
126
|
+
if (event.defaultPrevented || event.propagationStopped || event.targetEditable) return;
|
|
127
|
+
if (event.ctrl || event.meta || event.alt || event.shift) return;
|
|
128
|
+
|
|
129
|
+
const isEnter = event.name === "enter" || event.name === "return";
|
|
130
|
+
const isEscape = event.name === "escape" || event.name === "esc";
|
|
131
|
+
if (isEnter) {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
focusMetricTabs();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (isEscape && metricTabsFocused) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
event.stopPropagation();
|
|
140
|
+
releaseMetricTabs();
|
|
141
|
+
}
|
|
142
|
+
}, { enabled: focused, phase: "before" });
|
|
76
143
|
|
|
77
144
|
const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
78
|
-
|
|
145
|
+
const isEnter = event.name === "enter" || event.name === "return";
|
|
146
|
+
if (isEnter) {
|
|
79
147
|
event.preventDefault?.();
|
|
80
|
-
|
|
148
|
+
event.stopPropagation?.();
|
|
149
|
+
focusMetricTabs();
|
|
81
150
|
return true;
|
|
82
151
|
}
|
|
83
|
-
if (event.name === "
|
|
152
|
+
if ((event.name === "escape" || event.name === "esc") && metricTabsFocused) {
|
|
84
153
|
event.preventDefault?.();
|
|
85
|
-
|
|
154
|
+
event.stopPropagation?.();
|
|
155
|
+
releaseMetricTabs();
|
|
86
156
|
return true;
|
|
87
157
|
}
|
|
88
|
-
if (event.name === "
|
|
158
|
+
if (event.name === "r") {
|
|
89
159
|
event.preventDefault?.();
|
|
90
|
-
|
|
160
|
+
reload();
|
|
91
161
|
return true;
|
|
92
162
|
}
|
|
93
163
|
if (event.name === "p") {
|
|
@@ -96,7 +166,7 @@ export function FundamentalGraphContent({
|
|
|
96
166
|
return true;
|
|
97
167
|
}
|
|
98
168
|
return false;
|
|
99
|
-
}, [
|
|
169
|
+
}, [focusMetricTabs, metricTabsFocused, releaseMetricTabs, reload, togglePeriod]);
|
|
100
170
|
|
|
101
171
|
const renderCell = useCallback((
|
|
102
172
|
row: FundamentalGraphRow,
|
|
@@ -111,7 +181,11 @@ export function FundamentalGraphContent({
|
|
|
111
181
|
case "date":
|
|
112
182
|
return { text: row.date, color: selectedColor ?? colors.textDim };
|
|
113
183
|
case "value":
|
|
114
|
-
return {
|
|
184
|
+
return {
|
|
185
|
+
text: definition.format(row.value),
|
|
186
|
+
color: row.value < 0 ? colors.negative : selectedColor ?? colors.textBright,
|
|
187
|
+
attributes: TextAttributes.BOLD,
|
|
188
|
+
};
|
|
115
189
|
case "growth":
|
|
116
190
|
return { text: formatMaybePercent(row.growth), color: selectedColor ?? priceColor(row.growth ?? 0) };
|
|
117
191
|
}
|
|
@@ -119,41 +193,68 @@ export function FundamentalGraphContent({
|
|
|
119
193
|
|
|
120
194
|
usePaneFooter("fundamental-graph", () => ({
|
|
121
195
|
info: [
|
|
122
|
-
{ id: "
|
|
123
|
-
{ id: "
|
|
124
|
-
{ id: "period", parts: [{ text: period, tone: "muted" as const }] },
|
|
196
|
+
{ id: "period", parts: [{ text: "period", tone: "label" as const }, { text: period, tone: "value" as const }] },
|
|
197
|
+
...(hoveredValue ? [{ id: "hover", parts: [{ text: hoveredValue, tone: "value" as const }] }] : []),
|
|
125
198
|
...loadingErrorFooterInfo(loading, error),
|
|
126
199
|
],
|
|
127
200
|
hints: [
|
|
128
|
-
{ id: "metric", key: "m", label: "etric", onPress: cycleMetric },
|
|
129
|
-
{ id: "kind", key: "g", label: "raph", onPress: toggleGraphKind },
|
|
130
201
|
{ id: "period", key: "p", label: "eriod", onPress: togglePeriod },
|
|
131
202
|
refreshFooterHint(reload),
|
|
132
203
|
],
|
|
133
|
-
}), [
|
|
204
|
+
}), [error, hoveredValue, loading, period, reload, togglePeriod]);
|
|
205
|
+
|
|
206
|
+
const metricHeader = (
|
|
207
|
+
<Tabs
|
|
208
|
+
tabs={metricTabs}
|
|
209
|
+
activeValue={metric}
|
|
210
|
+
onSelect={selectMetricFromTabs}
|
|
211
|
+
compact
|
|
212
|
+
focused={focused && metricTabsFocused}
|
|
213
|
+
keyboardNavigation={metricTabsFocused}
|
|
214
|
+
scrollable
|
|
215
|
+
variant="underline"
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
134
218
|
|
|
135
219
|
return (
|
|
136
|
-
<Box
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
sortDirection="asc"
|
|
152
|
-
onHeaderClick={() => {}}
|
|
153
|
-
getItemKey={(row) => row.key}
|
|
154
|
-
renderCell={renderCell}
|
|
155
|
-
emptyStateTitle={loading ? "Loading fundamentals..." : "No graph data"}
|
|
220
|
+
<Box
|
|
221
|
+
flexDirection="column"
|
|
222
|
+
width={width}
|
|
223
|
+
height={height}
|
|
224
|
+
overflow="hidden"
|
|
225
|
+
onMouseDown={focusMetricTabs}
|
|
226
|
+
>
|
|
227
|
+
<StaticBarChartSurface
|
|
228
|
+
width={width}
|
|
229
|
+
height={chartHeight}
|
|
230
|
+
series={chartSeries}
|
|
231
|
+
header={metricHeader}
|
|
232
|
+
formatValue={definition.format}
|
|
233
|
+
onHoverChange={setHoveredBar}
|
|
234
|
+
onMouseDown={focusMetricTabs}
|
|
156
235
|
/>
|
|
236
|
+
{tableHeight > 0 ? (
|
|
237
|
+
<DataTableView<FundamentalGraphRow, FundamentalColumn>
|
|
238
|
+
focused={focused}
|
|
239
|
+
selection={{
|
|
240
|
+
kind: "index",
|
|
241
|
+
selectedIndex: boundedSelectedIdx,
|
|
242
|
+
onChange: (index) => setSelectedIdx(index),
|
|
243
|
+
}}
|
|
244
|
+
onRootKeyDown={handleKeyDown}
|
|
245
|
+
rootWidth={width}
|
|
246
|
+
rootHeight={tableHeight}
|
|
247
|
+
columns={columns}
|
|
248
|
+
items={tableRows}
|
|
249
|
+
sortColumnId="date"
|
|
250
|
+
sortDirection="desc"
|
|
251
|
+
onHeaderClick={() => {}}
|
|
252
|
+
onTableMouseDown={focusMetricTabs}
|
|
253
|
+
getItemKey={(row) => row.key}
|
|
254
|
+
renderCell={renderCell}
|
|
255
|
+
emptyStateTitle={loading ? "Loading fundamentals..." : "No graph data"}
|
|
256
|
+
/>
|
|
257
|
+
) : null}
|
|
157
258
|
</Box>
|
|
158
259
|
);
|
|
159
260
|
}
|