gloomberb 0.17.0 → 0.17.2
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/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { Box, Text, useUiCapabilities } from "../../ui";
|
|
3
|
+
import { useThemeColors } from "../../theme/theme-context";
|
|
4
|
+
import { displayWidth, padTo, truncateToDisplayWidth } from "../../utils/format";
|
|
5
|
+
import { ExternalLinkText } from "./external-link";
|
|
6
|
+
|
|
7
|
+
/** One column of a ColumnGrid. */
|
|
8
|
+
export interface ColumnGridColumn {
|
|
9
|
+
/** Cells the terminal gives the column, from the caller's own cell math. */
|
|
10
|
+
width: number;
|
|
11
|
+
/**
|
|
12
|
+
* On the desktop and the web the column takes an equal share of the room the
|
|
13
|
+
* fixed columns leave, and never less than this many characters. Without it
|
|
14
|
+
* the column holds `width` characters there too.
|
|
15
|
+
*/
|
|
16
|
+
growFrom?: number;
|
|
17
|
+
/** Where the column's text sits; a number column is right-aligned. */
|
|
18
|
+
align?: "left" | "right";
|
|
19
|
+
/** Space before the column: cells on the terminal, characters on the desktop. */
|
|
20
|
+
gapBefore?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Text the grid fits to its column (padded on the terminal, cut by CSS on the desktop), or content the column holds. */
|
|
24
|
+
export type ColumnGridCell =
|
|
25
|
+
| { text: string; color?: string }
|
|
26
|
+
| { content: ReactNode }
|
|
27
|
+
| null;
|
|
28
|
+
|
|
29
|
+
export interface ColumnGridProps {
|
|
30
|
+
columns: readonly ColumnGridColumn[];
|
|
31
|
+
/** Rows of cells, one per column. A header is the first row. */
|
|
32
|
+
rows: ReadonlyArray<ReadonlyArray<ColumnGridCell>>;
|
|
33
|
+
/** The terminal's width for the grid, in cells. The desktop fills its box. */
|
|
34
|
+
width?: number;
|
|
35
|
+
/** Names the grid for the pane chrome checks (`data-gloom-role`). */
|
|
36
|
+
role?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Desktop text, in a box that takes what its track gives it. Whether its track
|
|
41
|
+
* can be narrower than the text is `clipOf`'s to say; where it can, the text
|
|
42
|
+
* carries `data-gloom-clip` and styles.css clips it, without a scroll container
|
|
43
|
+
* where the engine has `overflow: clip`, and ends it in an ellipsis.
|
|
44
|
+
*/
|
|
45
|
+
const TEXT_BOX: CSSProperties = { display: "block", minWidth: 0 };
|
|
46
|
+
const CLIPPED = { "data-gloom-clip": "text" } as const;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The clip a cell's text gets: a fixed column is as wide as its characters at
|
|
50
|
+
* every frame, but a growing column can fall to `growFrom` characters at any
|
|
51
|
+
* frame of a resize, between two catch-ups, however far the pointer moved. Text
|
|
52
|
+
* longer than that is clipped, so a cell never draws outside its track.
|
|
53
|
+
*/
|
|
54
|
+
function clipOf(text: string, column: ColumnGridColumn): typeof CLIPPED | undefined {
|
|
55
|
+
return displayWidth(text) > (column.growFrom ?? column.width) ? CLIPPED : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The grid's tracks as CSS: characters for fixed columns, an equal share for
|
|
60
|
+
* growing ones. Gaps that are all alike are the grid's column gap; otherwise
|
|
61
|
+
* each gap is a track of its own, which no cell sits in.
|
|
62
|
+
*/
|
|
63
|
+
function gridTemplate(columns: readonly ColumnGridColumn[]): { template: string; gap: string | undefined; lines: number[] } {
|
|
64
|
+
const track = (column: ColumnGridColumn) => column.growFrom != null ? `minmax(${column.growFrom}ch, 1fr)` : `${column.width}ch`;
|
|
65
|
+
const gaps = columns.slice(1).map((column) => column.gapBefore ?? 0);
|
|
66
|
+
if ((columns[0]?.gapBefore ?? 0) === 0 && gaps.every((gap) => gap === gaps[0])) {
|
|
67
|
+
return { template: columns.map(track).join(" "), gap: gaps[0] ? `${gaps[0]}ch` : undefined, lines: columns.map((_, index) => index + 1) };
|
|
68
|
+
}
|
|
69
|
+
const tracks: string[] = [];
|
|
70
|
+
const lines: number[] = [];
|
|
71
|
+
for (const column of columns) {
|
|
72
|
+
if (column.gapBefore) tracks.push(`${column.gapBefore}ch`);
|
|
73
|
+
tracks.push(track(column));
|
|
74
|
+
lines.push(tracks.length);
|
|
75
|
+
}
|
|
76
|
+
return { template: tracks.join(" "), gap: undefined, lines };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Columns of single-line text that line up from row to row: a table of
|
|
81
|
+
* figures under its header, or label and value pairs set in columns. The terminal lays the
|
|
82
|
+
* columns out at the whole cells the caller worked out, as text padded to
|
|
83
|
+
* them. The desktop and the web lay out one CSS grid whose tracks are counted
|
|
84
|
+
* in characters, so the columns follow a resize to the pixel, every row and
|
|
85
|
+
* the header share the same tracks, and text that does not fit ends in an
|
|
86
|
+
* ellipsis instead of being cut to a count of cells.
|
|
87
|
+
*/
|
|
88
|
+
export function ColumnGrid({ columns, rows, width, role }: ColumnGridProps) {
|
|
89
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
90
|
+
if (!nativePaneChrome) {
|
|
91
|
+
return (
|
|
92
|
+
<Box flexDirection="column" width={width} data-gloom-role={role}>
|
|
93
|
+
{rows.map((row, rowIndex) => (
|
|
94
|
+
<Box key={rowIndex} flexDirection="row" height={1}>
|
|
95
|
+
{columns.map((column, index) => {
|
|
96
|
+
const cell = row[index] ?? null;
|
|
97
|
+
return (
|
|
98
|
+
<Box key={index} flexDirection="row">
|
|
99
|
+
{column.gapBefore ? <Box width={column.gapBefore} /> : null}
|
|
100
|
+
{cell && "text" in cell
|
|
101
|
+
? <Text fg={cell.color}>{padTo(cell.text, column.width, column.align)}</Text>
|
|
102
|
+
: <Box width={column.width} flexDirection="row">{cell?.content}</Box>}
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
})}
|
|
106
|
+
</Box>
|
|
107
|
+
))}
|
|
108
|
+
</Box>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { template, gap, lines } = gridTemplate(columns);
|
|
113
|
+
return (
|
|
114
|
+
<Box
|
|
115
|
+
data-gloom-ui="column-grid"
|
|
116
|
+
data-gloom-role={role}
|
|
117
|
+
// A row is one line of text, as tall as the text's own line: rows of a set
|
|
118
|
+
// height spare the browser sizing them again on every frame of a resize.
|
|
119
|
+
// The grid clips what its tracks put past its edge (columns that cannot shrink under a pane that does).
|
|
120
|
+
data-gloom-clip="box"
|
|
121
|
+
style={{ display: "grid", gridTemplateColumns: template, gridAutoRows: "var(--cell-h)", ...(gap ? { columnGap: gap } : {}) }}
|
|
122
|
+
>
|
|
123
|
+
{rows.map((row, rowIndex) => (
|
|
124
|
+
<Box key={rowIndex} data-gloom-ui="column-grid-row" style={{ display: "contents" }}>
|
|
125
|
+
{columns.map((column, index) => {
|
|
126
|
+
const cell = row[index] ?? null;
|
|
127
|
+
if (!cell) return null;
|
|
128
|
+
const place: CSSProperties = { gridColumn: lines[index], gridRow: rowIndex + 1 };
|
|
129
|
+
return "text" in cell
|
|
130
|
+
? (
|
|
131
|
+
<Text
|
|
132
|
+
key={index}
|
|
133
|
+
fg={cell.color}
|
|
134
|
+
data-gloom-ui="column-grid-text"
|
|
135
|
+
{...clipOf(cell.text, column)}
|
|
136
|
+
style={{ ...place, ...TEXT_BOX, textAlign: column.align ?? "left" }}
|
|
137
|
+
>
|
|
138
|
+
{cell.text}
|
|
139
|
+
</Text>
|
|
140
|
+
)
|
|
141
|
+
: <Box key={index} flexDirection="row" data-gloom-ui="column-grid-cell" style={{ ...place, minWidth: 0 }}>{cell.content}</Box>;
|
|
142
|
+
})}
|
|
143
|
+
</Box>
|
|
144
|
+
))}
|
|
145
|
+
</Box>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ColumnGridPairProps {
|
|
150
|
+
label: string;
|
|
151
|
+
/** Cells, and on the desktop characters, the label takes. */
|
|
152
|
+
labelWidth: number;
|
|
153
|
+
/** The terminal's cells for label and value together. */
|
|
154
|
+
width: number;
|
|
155
|
+
value: string;
|
|
156
|
+
valueColor?: string;
|
|
157
|
+
/** Muted context after the value, already chosen to fit. */
|
|
158
|
+
detail?: string;
|
|
159
|
+
/** The value opens this address. */
|
|
160
|
+
url?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* A label and its value in one ColumnGrid cell: the label at a fixed width,
|
|
165
|
+
* the value right-aligned in the rest and cut from its end when it is too
|
|
166
|
+
* long, and its muted detail after it. The detail gives way before the value.
|
|
167
|
+
*/
|
|
168
|
+
export function ColumnGridPair({ label, labelWidth, width, value, valueColor, detail, url }: ColumnGridPairProps) {
|
|
169
|
+
const colors = useThemeColors();
|
|
170
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
171
|
+
const color = valueColor ?? colors.text;
|
|
172
|
+
if (!nativePaneChrome) {
|
|
173
|
+
const valueWidth = Math.max(1, width - labelWidth);
|
|
174
|
+
const shown = truncateToDisplayWidth(value, valueWidth);
|
|
175
|
+
return (
|
|
176
|
+
<>
|
|
177
|
+
<Box width={labelWidth} overflow="hidden">
|
|
178
|
+
<Text fg={colors.textDim}>{label}</Text>
|
|
179
|
+
</Box>
|
|
180
|
+
<Box flexDirection="row" width={valueWidth} justifyContent="flex-end" overflow="hidden">
|
|
181
|
+
{url ? <ExternalLinkText url={url} label={shown} color={color} /> : <Text fg={color}>{shown}</Text>}
|
|
182
|
+
{detail ? <Text fg={colors.textDim}>{` ${detail}`}</Text> : null}
|
|
183
|
+
</Box>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
// The value's box can fall to nothing at any frame of a resize (the columns hold their labels, not their
|
|
188
|
+
// values), so its text and detail are clipped, the detail first. A link is clipped by its box, from its end.
|
|
189
|
+
return (
|
|
190
|
+
<>
|
|
191
|
+
<Text
|
|
192
|
+
fg={colors.textDim}
|
|
193
|
+
{...(displayWidth(label) > labelWidth ? { "data-gloom-clip": "box" } : {})}
|
|
194
|
+
style={{ flexGrow: 0, flexShrink: 0, flexBasis: `${labelWidth}ch`, display: "block" }}
|
|
195
|
+
>
|
|
196
|
+
{label}
|
|
197
|
+
</Text>
|
|
198
|
+
<Box flexDirection="row" justifyContent="flex-end" flexGrow={1} flexShrink={1} flexBasis={0} {...(url ? { "data-gloom-clip": "box" } : {})}>
|
|
199
|
+
{url
|
|
200
|
+
? <ExternalLinkText url={url} label={value} color={color} />
|
|
201
|
+
: <Text fg={color} {...CLIPPED} style={{ ...TEXT_BOX, flexShrink: 1 }}>{value}</Text>}
|
|
202
|
+
{detail ? <Text fg={colors.textDim} {...CLIPPED} style={{ ...TEXT_BOX, flexShrink: 1000 }}>{` ${detail}`}</Text> : null}
|
|
203
|
+
</Box>
|
|
204
|
+
</>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
@@ -20,14 +20,20 @@ export interface SectionHeadingProps {
|
|
|
20
20
|
|
|
21
21
|
export function SectionHeading({ title, marginTop = 0, width, wrap = false }: SectionHeadingProps) {
|
|
22
22
|
const colors = useThemeColors();
|
|
23
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
23
24
|
// English reads in title case; a translation keeps its own casing.
|
|
24
25
|
const translated = t(title);
|
|
25
26
|
const text = translated === title ? titleCase(title) : translated;
|
|
26
27
|
return (
|
|
27
28
|
<Box height={wrap ? undefined : 1} marginTop={marginTop} width={width} overflow="hidden" data-gloom-ui="section-heading">
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
{nativePaneChrome && !wrap ? (
|
|
30
|
+
// The desktop cuts a long heading where its box ends, not at a count of columns.
|
|
31
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD} style={{ ...NATIVE_CLIP_STYLE, flex: "1 1 auto" }}>{text}</Text>
|
|
32
|
+
) : (
|
|
33
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD} wrapText={wrap}>
|
|
34
|
+
{wrap || width === undefined ? text : truncateToDisplayWidth(text, width)}
|
|
35
|
+
</Text>
|
|
36
|
+
)}
|
|
31
37
|
</Box>
|
|
32
38
|
);
|
|
33
39
|
}
|
|
@@ -70,6 +76,37 @@ export interface KeyValueRowProps {
|
|
|
70
76
|
*/
|
|
71
77
|
export const NATIVE_CLIP_STYLE = { display: "block", minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "pre" } as const;
|
|
72
78
|
|
|
79
|
+
export interface ClippedTextProps {
|
|
80
|
+
text: string;
|
|
81
|
+
/** The terminal's room for the text, in cells. */
|
|
82
|
+
width: number;
|
|
83
|
+
fg?: string;
|
|
84
|
+
attributes?: number;
|
|
85
|
+
/** How the text sits in a row on the desktop: by default it gives way to its siblings. */
|
|
86
|
+
flex?: string;
|
|
87
|
+
/** Kept whole before the text in both renderers (an indent, a separator). */
|
|
88
|
+
lead?: string;
|
|
89
|
+
/** How the terminal cuts it; the caller's own cut keeps its frames as they were. */
|
|
90
|
+
cut?: (text: string, width: number) => string;
|
|
91
|
+
/** The desktop wraps it onto more lines instead (a message, a description); the terminal still cuts it. */
|
|
92
|
+
wrap?: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* One line of text cut where its room ends: in the terminal at `width` cells,
|
|
97
|
+
* on the desktop and the web by CSS at its box's edge, or wrapped there with
|
|
98
|
+
* `wrap`. Its box (or its row, with the siblings that keep their room) has to
|
|
99
|
+
* bound it there.
|
|
100
|
+
*/
|
|
101
|
+
export function ClippedText({ text, width, fg, attributes, flex = "0 1 auto", lead = "", cut = truncateToDisplayWidth, wrap = false }: ClippedTextProps) {
|
|
102
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
103
|
+
const ink = { ...(fg === undefined ? {} : { fg }), ...(attributes === undefined ? {} : { attributes }) };
|
|
104
|
+
if (!nativePaneChrome) return <Text {...ink}>{`${lead}${cut(text, width)}`}</Text>;
|
|
105
|
+
return wrap
|
|
106
|
+
? <Text {...ink} wrapText style={{ minWidth: 0, flex }}>{`${lead}${text}`}</Text>
|
|
107
|
+
: <Text {...ink} style={{ ...NATIVE_CLIP_STYLE, flex }}>{`${lead}${text}`}</Text>;
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
/** Aligned labels and values; a bounded row reserves room for an optional detail. */
|
|
74
111
|
export function KeyValueRow(props: KeyValueRowProps) {
|
|
75
112
|
const { nativePaneChrome } = useUiCapabilities();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box props for what sits under a pane's header zone (tabs, query bar,
|
|
3
|
+
* figures) on the desktop and the web: it takes the space left in its column
|
|
4
|
+
* by flex and ends at the pane footer. The terminal counts the rows left
|
|
5
|
+
* instead; there a bar is one row, but on the desktop a chrome bar is taller
|
|
6
|
+
* than a row (`--chrome-h`), so a body given those rows overruns the pane.
|
|
7
|
+
*/
|
|
8
|
+
export const FILL_REST = { flexGrow: 1, flexShrink: 1, flexBasis: 0, minHeight: 0 } as const;
|
|
@@ -35,6 +35,8 @@ export { QueryBar, useQueryBarSearch } from "./query-bar";
|
|
|
35
35
|
export { FieldGrid, GridFieldView, fieldGridColumns, fieldGridRows } from "./field-grid";
|
|
36
36
|
export type { FieldGridProps, GridField } from "./field-grid";
|
|
37
37
|
export { StatGrid, statGridColumns, statGridRows } from "./stat-grid";
|
|
38
|
+
export { ColumnGrid, ColumnGridPair } from "./column-grid";
|
|
39
|
+
export type { ColumnGridCell, ColumnGridColumn, ColumnGridPairProps, ColumnGridProps } from "./column-grid";
|
|
38
40
|
export type { StatGridProps, StatItem } from "./stat-grid";
|
|
39
41
|
export { RatioBar } from "./ratio-bar";
|
|
40
42
|
export type { RatioBarProps } from "./ratio-bar";
|
|
@@ -11,8 +11,8 @@ import { useAppDispatch, useAppSelector } from "../state/app/context";
|
|
|
11
11
|
import { useDialog, useDialogState, type PromptContext } from "../ui/dialog";
|
|
12
12
|
import { useRendererHost, useUiCapabilities } from "../ui";
|
|
13
13
|
import { apiClient, type TeamSummary } from "../api-client";
|
|
14
|
-
import { requestAuthDialog } from "../
|
|
15
|
-
import { teamStore } from "../
|
|
14
|
+
import { requestAuthDialog } from "../account/auth-dialog";
|
|
15
|
+
import { teamStore } from "../team/store";
|
|
16
16
|
import { usePlanAccess } from "../api-client/plan-access";
|
|
17
17
|
import { getMarketplaceHost } from "../plugins/marketplace/store";
|
|
18
18
|
import { rememberLayoutRequirements } from "../components/layout/missing-pane";
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef } from "react";
|
|
|
2
2
|
import { apiClient } from "../api-client";
|
|
3
3
|
import { rememberLayoutRequirements } from "../components/layout/missing-pane";
|
|
4
4
|
import type { PluginRegistry } from "../plugins/registry";
|
|
5
|
-
import { teamStore } from "../
|
|
5
|
+
import { teamStore } from "../team/store";
|
|
6
6
|
import { useAppDispatch, useAppSelector } from "../state/app/context";
|
|
7
7
|
import { decideLinkedAction, linkedLayoutStatus, linkedLayoutUpdates, originFromEntry } from "./linked";
|
|
8
8
|
import { materializeMarketplaceLayout } from "../shares/portable-layout";
|
|
@@ -50,6 +50,7 @@ const REGULAR_OPEN_MINUTES: Record<string, number> = {
|
|
|
50
50
|
FWB: 8 * 60,
|
|
51
51
|
SWX: 9 * 60,
|
|
52
52
|
VIE: 9 * 60,
|
|
53
|
+
WBDM: 9 * 60,
|
|
53
54
|
OSL: 9 * 60,
|
|
54
55
|
ICEX: 9 * 60 + 30,
|
|
55
56
|
WSE: 9 * 60,
|
|
@@ -89,6 +90,8 @@ const REGULAR_CLOSE_MINUTES: Record<string, number> = {
|
|
|
89
90
|
JPX: 15 * 60 + 30, HKEX: 16 * 60 + 10, TWSE: 14 * 60 + 30, TPEX: 14 * 60 + 30,
|
|
90
91
|
NSE: 16 * 60, BSE: 16 * 60, ASX: 16 * 60 + 15, SGX: 17 * 60 + 20, KRX: 16 * 60, KOSDAQ: 16 * 60,
|
|
91
92
|
NZX: 17 * 60, SSE: 15 * 60 + 30, SZSE: 15 * 60 + 30,
|
|
93
|
+
// The Vienna MTF's ETPs and notes trade in continuous auctions to 17:30, with no closing auction.
|
|
94
|
+
WBDM: 17 * 60 + 30,
|
|
92
95
|
BMV: 15 * 60 + 10, B3: 18 * 60 + 30, BYMA: 17 * 60 + 10, JSE: 17 * 60 + 15,
|
|
93
96
|
// Monday to Thursday: trading at last ends 17:30, after the closing auction.
|
|
94
97
|
// Not later: a complete 5-minute copy ends with its 17:09 bar, which must
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Published full-day closures (weekend dates omitted) for Asia-Pacific venues and
|
|
2
|
-
// not a holiday-rule engine. A year a venue has not published is unknown, not
|
|
1
|
+
// Published full-day closures (weekend dates omitted) for Asia-Pacific venues, EGX and Wiener
|
|
2
|
+
// Boerse, not a holiday-rule engine. A year a venue has not published is unknown, not
|
|
3
3
|
// open: every weekday of it reads as a session. Half-day sessions (HKEX and
|
|
4
4
|
// SGX eves, ASX's 14:10 closes) are not modelled. Checked 2026-10-09:
|
|
5
5
|
// KRX, also KOSDAQ: http://open.krx.co.kr/contents/MKD/01/0110/01100305/MKD01100305.jsp
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
// (some moved, as its list warns), and on National Day, 16 and 17 Dec. Its week is Sunday to Thursday.
|
|
10
10
|
// HKEX: https://www.hkex.com.hk/-/media/HKEX-Market/Services/Circulars-and-Notices/Participant-and-Members-Circulars/SEHK/2025/ce_SEHK_CT_075_2025.pdf
|
|
11
11
|
// and https://www.hkex.com.hk/-/media/HKEX-Market/Services/Circulars-and-Notices/Participant-and-Members-Circulars/SEHK/2026/ce_SEHK_CT_077_2026.pdf
|
|
12
|
+
// Wiener Boerse closes its main market (VIE) and the Vienna MTF (WBDM) on its stock exchange holidays,
|
|
13
|
+
// and trades on Austria's other public holidays:
|
|
14
|
+
// https://www.wienerborse.at/uploads/u/cms/files/trading/stock-exchange-holidays-2026-en.pdf (and -2027-en.pdf)
|
|
12
15
|
// SGX closes on Singapore's gazetted public holidays, one on a Sunday moving to the Monday:
|
|
13
16
|
// https://www.sgx.com/stock-exchange/trading and https://www.mom.gov.sg/employment-practices/public-holidays
|
|
14
17
|
// MSX closes on the holidays its site lists (msx.om exchange holidays, read 10 Oct 2026).
|
|
@@ -28,6 +31,11 @@ const KRX_CLOSURES: Record<number, readonly string[]> = {
|
|
|
28
31
|
],
|
|
29
32
|
};
|
|
30
33
|
|
|
34
|
+
const WIENER_BOERSE_CLOSURES: Record<number, readonly string[]> = {
|
|
35
|
+
2026: ["01-01", "04-03", "04-06", "05-01", "10-26", "12-24", "12-25", "12-31"],
|
|
36
|
+
2027: ["01-01", "03-26", "03-29", "10-26", "12-24", "12-31"],
|
|
37
|
+
};
|
|
38
|
+
|
|
31
39
|
const TWSE_CLOSURES: Record<number, readonly string[]> = {
|
|
32
40
|
2026: [
|
|
33
41
|
"01-01", "02-12", "02-13", "02-16", "02-17", "02-18", "02-19", "02-20", "02-27",
|
|
@@ -61,6 +69,8 @@ export const PUBLISHED_APAC_CLOSURES: Readonly<Record<string, Record<number, rea
|
|
|
61
69
|
2026: ["01-01", "02-17", "02-18", "04-03", "05-01", "05-27", "06-01", "08-10", "11-09", "12-25"],
|
|
62
70
|
2027: ["01-01", "02-08", "03-10", "03-26", "05-17", "05-20", "08-09", "10-28"],
|
|
63
71
|
},
|
|
72
|
+
VIE: WIENER_BOERSE_CLOSURES,
|
|
73
|
+
WBDM: WIENER_BOERSE_CLOSURES,
|
|
64
74
|
MSX: {
|
|
65
75
|
2026: ["01-15", "01-18", "03-19", "03-22", "03-23", "05-26", "05-27", "05-28", "06-18", "08-27", "11-25", "11-26"],
|
|
66
76
|
},
|
|
@@ -5,7 +5,7 @@ import { buildQuoteKey, resolveEntryData } from "../selectors";
|
|
|
5
5
|
import type { QueryEntry } from "../result-types";
|
|
6
6
|
import { resolveCurrencyUnit } from "../../utils/currency-units";
|
|
7
7
|
import { isFiniteNumber } from "../../utils/guards";
|
|
8
|
-
import { getCompletedRegularSessionDisplay, getTradedSessionMoveDisplay, type ExtendedSession } from "../market/status";
|
|
8
|
+
import { getCompletedRegularSessionDisplay, getTradedSessionMoveDisplay, type ActiveQuoteDisplay, type ExtendedSession } from "../market/status";
|
|
9
9
|
import { quoteTradingDay } from "./day-range";
|
|
10
10
|
|
|
11
11
|
const STREAM_FRESHNESS_MS = 2 * 60_000;
|
|
@@ -172,15 +172,32 @@ function overlaySessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote):
|
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
/** The regular session as a board reads it from one quote (getRegularSessionQuoteDisplay). */
|
|
176
|
+
export type RegularSessionQuoteDisplay = ActiveQuoteDisplay & (
|
|
177
|
+
| { completed: false }
|
|
178
|
+
/** The completed session's close and move, and its date when the quote can say it. */
|
|
179
|
+
| { completed: true; closedSession: string | null }
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* What a board that headlines only the regular session reads from a quote
|
|
184
|
+
* (the `regularSession` overlay): the live quote while the session trades,
|
|
185
|
+
* then the completed session's close and its move. Null when the quote
|
|
186
|
+
* leaves the row as it was: outside the session without a close it can name,
|
|
187
|
+
* or, on a board of `completedSessionDate`, a quote that is no news for it.
|
|
188
|
+
*/
|
|
189
|
+
export function getRegularSessionQuoteDisplay(
|
|
190
|
+
quote: Quote,
|
|
191
|
+
completedSessionDate: string | null = null,
|
|
192
|
+
): RegularSessionQuoteDisplay | null {
|
|
176
193
|
const state = quote.marketState;
|
|
177
194
|
if (state == null || state === "REGULAR") {
|
|
178
195
|
// On a board of a completed session, only a session that trades after it is news.
|
|
179
196
|
if (completedSessionDate) {
|
|
180
197
|
const day = state === "REGULAR" ? quoteTradingDay(quote) : null;
|
|
181
|
-
if (day == null || day <= completedSessionDate) return
|
|
198
|
+
if (day == null || day <= completedSessionDate) return null;
|
|
182
199
|
}
|
|
183
|
-
return
|
|
200
|
+
return { price: quote.price, change: quote.change, changePercent: quote.changePercent, completed: false };
|
|
184
201
|
}
|
|
185
202
|
const display = getCompletedRegularSessionDisplay(quote);
|
|
186
203
|
const closeDate = quote.regularCloseSessionDate;
|
|
@@ -190,14 +207,21 @@ function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Q
|
|
|
190
207
|
|| !isFiniteNumber(display.changePercent)
|
|
191
208
|
|| (completedSessionDate != null && closeDate != null && closeDate < completedSessionDate)
|
|
192
209
|
) {
|
|
193
|
-
return
|
|
210
|
+
return null;
|
|
194
211
|
}
|
|
212
|
+
return { ...display, completed: true, closedSession: closeDate ?? quoteTradingDay(quote) };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function overlayRegularSessionQuote<T extends ScreenerQuoteRow>(row: T, quote: Quote, completedSessionDate: string | null): T {
|
|
216
|
+
const display = getRegularSessionQuoteDisplay(quote, completedSessionDate);
|
|
217
|
+
if (!display) return row;
|
|
218
|
+
if (!display.completed) return overlayQuote(row, quote);
|
|
195
219
|
return {
|
|
196
220
|
...overlayQuote(row, quote),
|
|
197
221
|
price: display.price,
|
|
198
222
|
change: isFiniteNumber(display.change) ? display.change : null,
|
|
199
|
-
changePercent: display.changePercent,
|
|
200
|
-
regularChangePercent: display.changePercent,
|
|
223
|
+
changePercent: display.changePercent ?? null,
|
|
224
|
+
regularChangePercent: display.changePercent ?? null,
|
|
201
225
|
};
|
|
202
226
|
}
|
|
203
227
|
|
|
@@ -15,6 +15,7 @@ import { truncateWithEllipsis } from "../../../utils/text-wrap";
|
|
|
15
15
|
import type { SelectControl } from "../../../components/ui/select-button";
|
|
16
16
|
import type { ProfileAnalyticsPreview } from "./model";
|
|
17
17
|
import { t } from "../../../i18n";
|
|
18
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
18
19
|
|
|
19
20
|
export function accountFieldLabelWidth(width: number) {
|
|
20
21
|
const preferred = width >= 28 ? 16 : Math.max(10, Math.floor(width * 0.42));
|
|
@@ -132,16 +133,12 @@ export function PublicAnalyticsGroup({
|
|
|
132
133
|
const valueWidth = Math.max(1, metricWidth - labelTextWidth - 1);
|
|
133
134
|
return (
|
|
134
135
|
<Box key={metric.id} width={metricWidth} height={1} flexDirection="row" gap={1}>
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
{truncateWithEllipsis(metric.value, valueWidth)}
|
|
138
|
-
</Text>
|
|
136
|
+
<ClippedText fg={colors.textDim} text={metric.label} width={labelTextWidth} cut={truncateWithEllipsis} />
|
|
137
|
+
<ClippedText fg={metricColor(metric.tone)} attributes={TextAttributes.BOLD} text={metric.value} width={valueWidth} cut={truncateWithEllipsis} />
|
|
139
138
|
</Box>
|
|
140
139
|
);
|
|
141
140
|
}) : detail ? (
|
|
142
|
-
<
|
|
143
|
-
{truncateWithEllipsis(detail, detailWidth)}
|
|
144
|
-
</Text>
|
|
141
|
+
<ClippedText fg={colors.textMuted} text={detail} width={detailWidth} cut={truncateWithEllipsis} />
|
|
145
142
|
) : null}
|
|
146
143
|
</Box>
|
|
147
144
|
{metrics.length === 0 && displayPreview.subtitle ? (
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { useCallback, useState } from "react";
|
|
2
2
|
import { Button, TextField } from "../../../components";
|
|
3
3
|
import { DialogFrame } from "../../../components/ui/frame";
|
|
4
|
-
import { Box
|
|
4
|
+
import { Box } from "../../../ui";
|
|
5
5
|
import { useDialogKeyboard, type AlertContext } from "../../../ui/dialog";
|
|
6
6
|
import { colors } from "../../../theme/colors";
|
|
7
7
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
8
8
|
import { truncateWithEllipsis } from "../../../utils/text-wrap";
|
|
9
9
|
import { t } from "../../../i18n";
|
|
10
10
|
import { MIN_ACCOUNT_PASSWORD_LENGTH } from "../../../account/auth-model";
|
|
11
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
11
12
|
|
|
12
13
|
type PasswordDialogField = "current" | "new" | "confirm";
|
|
13
14
|
|
|
@@ -111,7 +112,7 @@ export function PasswordChangeDialog({
|
|
|
111
112
|
onChange={setConfirmPassword}
|
|
112
113
|
onSubmit={() => { void submit(); }}
|
|
113
114
|
/>
|
|
114
|
-
{error ? <
|
|
115
|
+
{error ? <ClippedText fg={colors.negative} text={error} width={fieldWidth} cut={truncateWithEllipsis} wrap /> : null}
|
|
115
116
|
<Box flexDirection="row" justifyContent="flex-end">
|
|
116
117
|
<Button
|
|
117
118
|
label={submitting ? t("Changing...") : t("Update Password")}
|
|
@@ -22,7 +22,8 @@ import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
|
22
22
|
import { usePaneInstanceId, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
|
|
23
23
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
24
24
|
import type { PaneProps } from "../../../types/plugin";
|
|
25
|
-
import { Box } from "../../../ui";
|
|
25
|
+
import { Box, useUiCapabilities } from "../../../ui";
|
|
26
|
+
import { FILL_REST } from "../../../components/ui/fill";
|
|
26
27
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
27
28
|
import { loadBacktestHistory } from "./client";
|
|
28
29
|
import { runBacktest, type BacktestResult, type BacktestTrade } from "./engine";
|
|
@@ -146,6 +147,7 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
|
|
|
146
147
|
}), [history.loading, result, view, paneId, symbol]);
|
|
147
148
|
|
|
148
149
|
const { strip: tabStrip, rows: tabRows } = usePaneTabs(symbol ? { tabs: TABS, activeValue: view, onSelect: setView, focused, dense: true } : null);
|
|
150
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
149
151
|
if (!symbol) return <EmptyState title="Select a ticker." hint="Open BT with a symbol, for example BT AAPL." />;
|
|
150
152
|
const bodyHeight = Math.max(4, height - 1 - tabRows);
|
|
151
153
|
const wide = width - SUMMARY_WIDTH - 1 >= MIN_WIDE_CHART_COLS;
|
|
@@ -187,7 +189,9 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
|
|
|
187
189
|
/>
|
|
188
190
|
);
|
|
189
191
|
return (
|
|
190
|
-
|
|
192
|
+
// On the desktop the pane's box fills the pane by flex, so what fills it
|
|
193
|
+
// follows the pane's frame by CSS, the chart beside the metrics too.
|
|
194
|
+
<Box width={width} flexDirection="column" {...(nativePaneChrome ? FILL_REST : { height })}>
|
|
191
195
|
{tabStrip}
|
|
192
196
|
<QueryBar
|
|
193
197
|
width={width}
|
|
@@ -242,7 +246,9 @@ export function BacktestPane({ width, height, focused }: PaneProps) {
|
|
|
242
246
|
emptyStateTitle="No trades in this window."
|
|
243
247
|
/>
|
|
244
248
|
) : wide ? (
|
|
245
|
-
|
|
249
|
+
// The desktop's query bar is taller than a row: the chart and the
|
|
250
|
+
// metrics take what is left under it, down to the pane footer.
|
|
251
|
+
<Box flexDirection="row" width={width} {...(nativePaneChrome ? FILL_REST : { height: bodyHeight })}>
|
|
246
252
|
{chart(width - SUMMARY_WIDTH - 1, bodyHeight)}
|
|
247
253
|
<Box width={1} />
|
|
248
254
|
{summaryTable(SUMMARY_WIDTH, bodyHeight)}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Section, TickerBadgeList } from "../../../../components";
|
|
2
|
+
import { ClippedText } from "../../../../components/ui/display";
|
|
2
3
|
import { formatMarketPriceWithCurrency, liveQuoteFormatOptions } from "../../../../market-data/market/format";
|
|
3
4
|
import { useInlineTickerQuote } from "../../../../state/hooks/inline-tickers";
|
|
4
5
|
import { useTickerQuoteStream } from "../../../../state/hooks/live-ticker-financials";
|
|
5
6
|
import { colors } from "../../../../theme/colors";
|
|
6
7
|
import type { Quote } from "../../../../types/financials";
|
|
7
8
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
8
|
-
import { Box
|
|
9
|
+
import { Box } from "../../../../ui";
|
|
9
10
|
import { formatCompact } from "../../../../utils/format";
|
|
10
11
|
import { truncateWithEllipsis } from "../../../../utils/text-wrap";
|
|
11
12
|
import { activityLabel, criticalityColor, dateShort, metricColor, metricNumber, textOrNull, tickerSymbol } from "../format";
|
|
@@ -249,7 +250,7 @@ export function CompanyDetail({
|
|
|
249
250
|
].filter(Boolean);
|
|
250
251
|
return (
|
|
251
252
|
<Box key={`${site.name}-${index}`} flexDirection="column" marginTop={index === 0 ? 0 : 1}>
|
|
252
|
-
<
|
|
253
|
+
<ClippedText fg={colors.textMuted} text={parts.join(" - ")} width={bodyWidth} cut={truncateWithEllipsis} />
|
|
253
254
|
{site.involvementSummary ? (
|
|
254
255
|
<MarkdownBlock text={site.involvementSummary} width={bodyWidth} catalog={catalog} openTicker={openTicker} marginTop={0} />
|
|
255
256
|
) : null}
|
|
@@ -262,7 +263,7 @@ export function CompanyDetail({
|
|
|
262
263
|
<Section title="Recent Intel" width={bodyWidth}>
|
|
263
264
|
{company.intelligence!.slice(0, 5).map((item, index) => (
|
|
264
265
|
<Box key={`${item.headline ?? "intel"}:${index}`} flexDirection="column" marginTop={index === 0 ? 0 : 1}>
|
|
265
|
-
<
|
|
266
|
+
<ClippedText fg={colors.textDim} text={`${dateShort(item.publishedAt)} ${item.headline ?? ""}`} width={bodyWidth} cut={truncateWithEllipsis} />
|
|
266
267
|
<MarkdownBlock text={item.content} width={bodyWidth} catalog={catalog} openTicker={openTicker} marginTop={0} />
|
|
267
268
|
</Box>
|
|
268
269
|
))}
|