gloomberb 0.17.2 → 0.17.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/package.json +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/index.ts +1 -1
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +19 -2
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +24 -5
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
|
@@ -7,6 +7,7 @@ import type { InlineTickerCatalogEntry } from "../state/hooks/inline-tickers";
|
|
|
7
7
|
import { colors } from "../theme/colors";
|
|
8
8
|
import { displayWidth } from "../utils/format";
|
|
9
9
|
import { useUiCapabilities } from "../ui";
|
|
10
|
+
import { MARKDOWN_HEADING_RE, markdownListItem, parseInlineMarkdown, type MarkdownSegment } from "../utils/markdown";
|
|
10
11
|
|
|
11
12
|
export interface MarkdownTextProps {
|
|
12
13
|
text: string;
|
|
@@ -30,15 +31,9 @@ export interface MarkdownTextProps {
|
|
|
30
31
|
onOpenLink?: (href: string) => void;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
bold?: boolean;
|
|
36
|
-
italic?: boolean;
|
|
37
|
-
dim?: boolean;
|
|
38
|
-
code?: boolean;
|
|
34
|
+
/** A segment as drawn here: a heading or a table header has its own colour. */
|
|
35
|
+
interface StyledSegment extends MarkdownSegment {
|
|
39
36
|
color?: string;
|
|
40
|
-
/** Where the segment links to, when links are read. */
|
|
41
|
-
link?: string;
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
interface ParsedLine {
|
|
@@ -47,52 +42,12 @@ interface ParsedLine {
|
|
|
47
42
|
indent?: number;
|
|
48
43
|
}
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const INLINE_RE =
|
|
53
|
-
/(\\(?<escaped>[\\`*_~#>[\]()-])|\[(?<linkLabel>[^\]]+)\]\((?<linkHref>[^)\s]+)\)|\*\*(?<bold>[^*]+)\*\*|(?<!\*)\*(?!\*)(?<italic>(?:[^*]|\*\*[^*]+\*\*)+?)\*(?!\*)|`(?<code>[^`]+)`|~~(?<strike>[^~]+)~~)/g;
|
|
54
|
-
|
|
55
|
-
function parseInlineMarkdown(text: string, links = false): StyledSegment[] {
|
|
56
|
-
const segments: StyledSegment[] = [];
|
|
57
|
-
let cursor = 0;
|
|
58
|
-
|
|
59
|
-
const pattern = new RegExp(INLINE_RE.source, "g");
|
|
60
|
-
let match: RegExpExecArray | null;
|
|
61
|
-
while ((match = pattern.exec(text)) !== null) {
|
|
62
|
-
if (match.groups?.linkHref != null && !links) {
|
|
63
|
-
// Not reading links: the bracket is text, and what is inside it still styles.
|
|
64
|
-
pattern.lastIndex = match.index + 1;
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (match.index > cursor) {
|
|
68
|
-
segments.push({ text: text.slice(cursor, match.index) });
|
|
69
|
-
}
|
|
70
|
-
if (match.groups?.escaped != null) {
|
|
71
|
-
segments.push({ text: match.groups.escaped });
|
|
72
|
-
} else if (match.groups?.linkHref != null) {
|
|
73
|
-
for (const inner of parseInlineMarkdown(match.groups.linkLabel ?? "", false)) segments.push({ ...inner, link: match.groups.linkHref });
|
|
74
|
-
} else if (match.groups?.bold != null) {
|
|
75
|
-
segments.push({ text: match.groups.bold, bold: true });
|
|
76
|
-
} else if (match.groups?.italic != null) {
|
|
77
|
-
for (const inner of parseInlineMarkdown(match.groups.italic, links)) segments.push({ ...inner, italic: true });
|
|
78
|
-
} else if (match.groups?.code != null) {
|
|
79
|
-
segments.push({ text: match.groups.code, code: true });
|
|
80
|
-
} else if (match.groups?.strike != null) {
|
|
81
|
-
segments.push({ text: match.groups.strike, dim: true });
|
|
82
|
-
}
|
|
83
|
-
cursor = match.index + match[0].length;
|
|
84
|
-
}
|
|
85
|
-
if (cursor < text.length) {
|
|
86
|
-
segments.push({ text: text.slice(cursor) });
|
|
87
|
-
}
|
|
88
|
-
return segments;
|
|
45
|
+
function parseInline(text: string, links = false): StyledSegment[] {
|
|
46
|
+
return parseInlineMarkdown(text, { links });
|
|
89
47
|
}
|
|
90
48
|
|
|
91
|
-
const HEADING_RE = /^(#{1,3})\s+(.+)$/;
|
|
92
|
-
|
|
93
49
|
function parseLine(line: string, links: boolean): ParsedLine {
|
|
94
|
-
|
|
95
|
-
const headingMatch = line.match(HEADING_RE);
|
|
50
|
+
const headingMatch = line.match(MARKDOWN_HEADING_RE);
|
|
96
51
|
if (headingMatch) {
|
|
97
52
|
return {
|
|
98
53
|
heading: true,
|
|
@@ -100,22 +55,18 @@ function parseLine(line: string, links: boolean): ParsedLine {
|
|
|
100
55
|
};
|
|
101
56
|
}
|
|
102
57
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (listMatch) {
|
|
106
|
-
const indent = listMatch[1]!.length;
|
|
107
|
-
const marker = listMatch[2]!;
|
|
108
|
-
const content = listMatch[3]!;
|
|
58
|
+
const item = markdownListItem(line);
|
|
59
|
+
if (item) {
|
|
109
60
|
return {
|
|
110
|
-
indent,
|
|
61
|
+
indent: item.indent,
|
|
111
62
|
segments: [
|
|
112
|
-
{ text: `${marker} `, bold: true, color: colors.borderFocused },
|
|
113
|
-
...
|
|
63
|
+
{ text: `${item.marker} `, bold: true, color: colors.borderFocused },
|
|
64
|
+
...parseInline(item.text, links),
|
|
114
65
|
],
|
|
115
66
|
};
|
|
116
67
|
}
|
|
117
68
|
|
|
118
|
-
return { segments:
|
|
69
|
+
return { segments: parseInline(line, links) };
|
|
119
70
|
}
|
|
120
71
|
|
|
121
72
|
type TableAlign = "left" | "center" | "right";
|
|
@@ -176,7 +127,7 @@ function columnAlign(table: MarkdownTable, column: number): TableAlign {
|
|
|
176
127
|
const explicit = table.align[column];
|
|
177
128
|
if (explicit) return explicit;
|
|
178
129
|
const values = table.rows
|
|
179
|
-
.map((row) =>
|
|
130
|
+
.map((row) => parseInline(row[column] ?? "").map((segment) => segment.text).join("").trim())
|
|
180
131
|
.filter((value) => value !== "" && value !== "-" && value !== "\u2014");
|
|
181
132
|
return values.length > 0 && values.every((value) => NUMERIC_CELL_RE.test(value)) ? "right" : "left";
|
|
182
133
|
}
|
|
@@ -214,6 +165,28 @@ function parseMarkdownBlocks(text: string): MarkdownBlock[] {
|
|
|
214
165
|
return blocks;
|
|
215
166
|
}
|
|
216
167
|
|
|
168
|
+
/**
|
|
169
|
+
* The links a text reads as, in reading order, for a reader's pane menu: the
|
|
170
|
+
* terminal cannot press part of a line.
|
|
171
|
+
*/
|
|
172
|
+
export function markdownLinks(text: string): Array<{ label: string; href: string }> {
|
|
173
|
+
const links: Array<{ label: string; href: string }> = [];
|
|
174
|
+
// A label with bold in it is several segments of one link.
|
|
175
|
+
const read = (segments: StyledSegment[]) => {
|
|
176
|
+
let previous: string | undefined;
|
|
177
|
+
for (const { text: part, link } of segments) {
|
|
178
|
+
if (link && link === previous) links[links.length - 1]!.label += part;
|
|
179
|
+
else if (link) links.push({ label: part, href: link });
|
|
180
|
+
previous = link;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
for (const block of parseMarkdownBlocks(text)) {
|
|
184
|
+
if (block.kind === "line") read(parseLine(block.text, true).segments);
|
|
185
|
+
else for (const cell of [block.table.header, ...block.table.rows].flat()) read(parseInline(cell, true));
|
|
186
|
+
}
|
|
187
|
+
return links;
|
|
188
|
+
}
|
|
189
|
+
|
|
217
190
|
function segmentsWidth(segments: StyledSegment[]): number {
|
|
218
191
|
return segments.reduce((total, segment) => total + displayWidth(segment.text), 0);
|
|
219
192
|
}
|
|
@@ -374,7 +347,7 @@ function layoutTableRows(
|
|
|
374
347
|
}
|
|
375
348
|
|
|
376
349
|
function headerCell(text: string, links: boolean): StyledSegment[] {
|
|
377
|
-
return
|
|
350
|
+
return parseInline(text, links).map((segment) => ({ ...segment, bold: true, color: colors.textDim }));
|
|
378
351
|
}
|
|
379
352
|
|
|
380
353
|
/**
|
|
@@ -384,7 +357,7 @@ function headerCell(text: string, links: boolean): StyledSegment[] {
|
|
|
384
357
|
*/
|
|
385
358
|
function layoutMarkdownTable(table: MarkdownTable, lineWidth?: number, links = false): TableLine[] {
|
|
386
359
|
const header = table.header.map((cell) => headerCell(cell, links));
|
|
387
|
-
const body = table.rows.map((row) => row.map((cell) =>
|
|
360
|
+
const body = table.rows.map((row) => row.map((cell) => parseInline(cell, links)));
|
|
388
361
|
const align = table.header.map((_, column) => columnAlign(table, column));
|
|
389
362
|
const widths = tableColumnWidths([header, ...body], lineWidth);
|
|
390
363
|
if (widths) return layoutTableRows([header, ...body], widths, align);
|
|
@@ -460,7 +433,8 @@ function SegmentSpan({ segment, wrap = false, onOpenLink }: {
|
|
|
460
433
|
const attrs =
|
|
461
434
|
(segment.bold ? TextAttributes.BOLD : 0) |
|
|
462
435
|
(segment.italic ? TextAttributes.ITALIC : 0) |
|
|
463
|
-
|
|
436
|
+
// Struck text reads dim here.
|
|
437
|
+
(segment.strike ? TextAttributes.DIM : 0) |
|
|
464
438
|
(segment.link ? TextAttributes.UNDERLINE : 0);
|
|
465
439
|
const href = segment.link;
|
|
466
440
|
// The terminal cannot press part of a line; there the pane menu lists links.
|
|
@@ -627,7 +601,7 @@ export function MarkdownText({
|
|
|
627
601
|
if (line.trim() === "") {
|
|
628
602
|
// Under a heading set in its own style, the heading's margin is the space: it sits with what it names.
|
|
629
603
|
const previous = blocks[index - 1];
|
|
630
|
-
if (nativePaneChrome && headingStyle && previous?.kind === "line" &&
|
|
604
|
+
if (nativePaneChrome && headingStyle && previous?.kind === "line" && MARKDOWN_HEADING_RE.test(previous.text)) return null;
|
|
631
605
|
return nativePaneChrome && paragraphGap
|
|
632
606
|
? <Box key={index} flexShrink={0} style={{ height: paragraphGap }} />
|
|
633
607
|
: <Text key={index}>{" "}</Text>;
|
|
@@ -84,13 +84,15 @@ export interface MarketBoardStackProps<T extends MarketBoardRow> {
|
|
|
84
84
|
const FIT_STEPS = ["labelDetail", "history", "shortAsOf", "changeAsOf", "percentile"] as const;
|
|
85
85
|
/** An as-of date without its year ("09-17"). */
|
|
86
86
|
const SHORT_AS_OF_WIDTH = 5;
|
|
87
|
+
/** The word a stale row's date carries, with its space: "2026-09-17 stale". */
|
|
88
|
+
const STALE_WORD = " stale";
|
|
87
89
|
|
|
88
90
|
function shortIsoDate(text: string): string {
|
|
89
91
|
return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text.slice(5) : text;
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
/** Fit the board to the pane rather than cut its right edge mid-value. */
|
|
93
|
-
function fitBoardColumns(candidates: DataTableColumn[], width: number): { columns: DataTableColumn[]; shortAsOf: boolean } {
|
|
95
|
+
function fitBoardColumns(candidates: DataTableColumn[], width: number, wordWidth: number): { columns: DataTableColumn[]; shortAsOf: boolean } {
|
|
94
96
|
const withAsOfWidth = (list: DataTableColumn[], asOfWidth: number | null) => asOfWidth === null
|
|
95
97
|
? list
|
|
96
98
|
: list.map((column) => column.id === "asOf" ? { ...column, width: asOfWidth } : column);
|
|
@@ -100,7 +102,7 @@ function fitBoardColumns(candidates: DataTableColumn[], width: number): { column
|
|
|
100
102
|
if (getTableWidth(columns) <= width) break;
|
|
101
103
|
if (step === "shortAsOf") {
|
|
102
104
|
shortAsOf = true;
|
|
103
|
-
columns = withAsOfWidth(columns, SHORT_AS_OF_WIDTH);
|
|
105
|
+
columns = withAsOfWidth(columns, SHORT_AS_OF_WIDTH + wordWidth);
|
|
104
106
|
} else {
|
|
105
107
|
columns = columns.filter((column) => column.id !== step);
|
|
106
108
|
}
|
|
@@ -134,6 +136,8 @@ export function MarketBoardStack<T extends MarketBoardRow>({ rows, width, height
|
|
|
134
136
|
return compareSortValues(left, right, sort.direction);
|
|
135
137
|
}) : rows, [rows, sort, extraColumns]);
|
|
136
138
|
const open = rows.find((row) => row.id === openId);
|
|
139
|
+
// A stale row's date carries a word, so the column has room for both and the date is never cut.
|
|
140
|
+
const wordWidth = rows.some((row) => row.status === "stale") ? STALE_WORD.length : 0;
|
|
137
141
|
const { columns, shortAsOf } = fitBoardColumns([
|
|
138
142
|
{ id: "label", label: labelHeader, width: labelWidth, align: "left" },
|
|
139
143
|
...(rows.some((row) => row.labelDetail !== undefined)
|
|
@@ -144,8 +148,8 @@ export function MarketBoardStack<T extends MarketBoardRow>({ rows, width, height
|
|
|
144
148
|
...(extraColumns?.map((item) => item.column) ?? []),
|
|
145
149
|
{ id: "percentile", label: percentileLabel, width: 8, align: "right" },
|
|
146
150
|
{ id: "history", label: "1Y", width: 14, align: "left" },
|
|
147
|
-
{ id: "asOf", label: "AS OF", width: asOfWidth, align: "left" },
|
|
148
|
-
], width);
|
|
151
|
+
{ id: "asOf", label: "AS OF", width: asOfWidth + wordWidth, align: "left" },
|
|
152
|
+
], width, wordWidth);
|
|
149
153
|
const renderCell = (row: T, column: DataTableColumn): DataTableCell => {
|
|
150
154
|
const extra = extraColumns?.find((item) => item.column.id === column.id);
|
|
151
155
|
if (extra) return extra.renderCell(row);
|
|
@@ -159,8 +163,10 @@ export function MarketBoardStack<T extends MarketBoardRow>({ rows, width, height
|
|
|
159
163
|
if (column.id === "percentile") return { text: row.percentileText ?? row.percentile?.toFixed(0) ?? "--", color: row.percentile != null && (row.percentile <= 10 || row.percentile >= 90) ? colors.warning : colors.textMuted };
|
|
160
164
|
if (column.id === "history") return { text: "", content: <PriceSparkline priceHistory={row.history} width={column.width} period="1Y" trend="neutral" /> };
|
|
161
165
|
const asOf = row.asOfText ?? row.asOf ?? "--";
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
const asOfText = shortAsOf ? shortIsoDate(asOf) : asOf;
|
|
167
|
+
// A narrow board drops the year; the export keeps the full date. The word
|
|
168
|
+
// stays with the color, so a stale row is not color alone.
|
|
169
|
+
return { text: row.status === "stale" ? `${asOfText}${STALE_WORD}` : asOfText, ...(row.asOf ? { value: row.asOf } : {}),
|
|
164
170
|
color: row.status === "stale" ? colors.warning : colors.textDim };
|
|
165
171
|
};
|
|
166
172
|
return <DataTableStackView columns={columns} items={items} getItemKey={(row) => row.id} renderCell={renderCell}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { memo, useEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
2
2
|
import { Box, Text, TextAttributes, useUiCapabilities, type BoxRenderable } from "../../ui";
|
|
3
3
|
import { useLiveStretch } from "../../ui/live-stretch";
|
|
4
|
+
import { useSettledLayoutSize } from "../../ui/resize-settle";
|
|
4
5
|
import { blendHex } from "../../theme/colors";
|
|
5
6
|
import {
|
|
6
7
|
heatmapColorDistance,
|
|
@@ -91,9 +92,57 @@ export function buildHeatTreemapScene<T>(
|
|
|
91
92
|
return buildMetricTreemapScene(items, canvas.width, canvas.height, canvas.options, previous);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
export interface HeatTreemapLayout<T> {
|
|
96
|
+
scene: MetricTreemapScene<T>;
|
|
97
|
+
/** The box is being resized (`useSettledLayoutSize`): hand it to the surface, which then holds still. */
|
|
98
|
+
resizing: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface ShownScene<T> {
|
|
102
|
+
key: string;
|
|
103
|
+
canvas: HeatTreemapCanvas;
|
|
104
|
+
items: readonly MetricTreemapItem<T>[];
|
|
105
|
+
scene: MetricTreemapScene<T>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The scene a heat map shows in a `width` by `height` box (cells). Each
|
|
110
|
+
* layout is seeded with the scene on screen under the same `key` (a tab), so
|
|
111
|
+
* a refresh nudges tiles instead of reshuffling the board. On the desktop,
|
|
112
|
+
* while the box is being resized the scene on screen stretches with it and a
|
|
113
|
+
* refresh's new sizes wait: the board is laid out once, at the size the box
|
|
114
|
+
* settles on.
|
|
115
|
+
*/
|
|
116
|
+
export function useHeatTreemapScene<T>(
|
|
117
|
+
items: readonly MetricTreemapItem<T>[],
|
|
118
|
+
width: number,
|
|
119
|
+
height: number,
|
|
120
|
+
key = "",
|
|
121
|
+
): HeatTreemapLayout<T> {
|
|
122
|
+
const { nativePaneChrome, cellWidthPx, cellHeightPx } = useUiCapabilities();
|
|
123
|
+
const layout = useSettledLayoutSize(width, height, { enabled: nativePaneChrome === true });
|
|
124
|
+
const canvas = useMemo(
|
|
125
|
+
() => heatTreemapCanvas(layout.width, layout.height, { nativePaneChrome, cellWidthPx, cellHeightPx }),
|
|
126
|
+
[cellHeightPx, cellWidthPx, layout.height, layout.width, nativePaneChrome],
|
|
127
|
+
);
|
|
128
|
+
const shownRef = useRef<ShownScene<T> | null>(null);
|
|
129
|
+
const scene = useMemo(() => {
|
|
130
|
+
const shown = shownRef.current?.key === key ? shownRef.current : null;
|
|
131
|
+
// While the box resizes, the scene on screen holds until it is laid out
|
|
132
|
+
// again, a refresh's new sizes with it: the board relays out once.
|
|
133
|
+
if (shown && shown.canvas === canvas && (layout.resizing || shown.items === items)) return shown.scene;
|
|
134
|
+
const next = buildHeatTreemapScene(items, canvas, shown?.scene);
|
|
135
|
+
shownRef.current = { key, canvas, items, scene: next };
|
|
136
|
+
return next;
|
|
137
|
+
}, [canvas, items, key, layout.resizing]);
|
|
138
|
+
return { scene, resizing: layout.resizing };
|
|
139
|
+
}
|
|
140
|
+
|
|
94
141
|
export interface HeatTreemapSurfaceProps<T> {
|
|
95
|
-
/** Geometry, from `
|
|
142
|
+
/** Geometry, from `useHeatTreemapScene`; rebuilt only when sizes change. */
|
|
96
143
|
scene: MetricTreemapScene<T>;
|
|
144
|
+
/** From `useHeatTreemapScene`: while the box resizes, no tile glides, fades or pulses. */
|
|
145
|
+
resizing: boolean;
|
|
97
146
|
/** Current colours and text, by id; they change with every tick. */
|
|
98
147
|
items: readonly MetricTreemapItem<T>[];
|
|
99
148
|
width: number;
|
|
@@ -223,7 +272,7 @@ const VERTICAL_LABEL_STYLE: CSSProperties = {
|
|
|
223
272
|
|
|
224
273
|
const DomHeatTile = memo(function DomHeatTile({
|
|
225
274
|
id, rect, canvasWidth, canvasHeight, gap, label, value, valueSuffix, background, foreground,
|
|
226
|
-
selected, reducedMotion, glide, pulseCount, events,
|
|
275
|
+
selected, reducedMotion, resizing, glide, pulseCount, events,
|
|
227
276
|
}: {
|
|
228
277
|
id: string;
|
|
229
278
|
rect: TreemapRect;
|
|
@@ -237,6 +286,8 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
237
286
|
foreground: string;
|
|
238
287
|
selected: boolean;
|
|
239
288
|
reducedMotion: boolean;
|
|
289
|
+
resizing: boolean;
|
|
290
|
+
/** A relayout this render may slide the tile: false for one that came with a new size. */
|
|
240
291
|
glide: boolean;
|
|
241
292
|
pulseCount: number;
|
|
242
293
|
events: SurfaceEvents;
|
|
@@ -248,16 +299,22 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
248
299
|
});
|
|
249
300
|
// Where this tile sat before the last relayout, kept to slide it from there.
|
|
250
301
|
const placedRef = useRef(rect);
|
|
251
|
-
const glideRef = useRef<{ dx: number; dy: number; count: number }>({ dx: 0, dy: 0, count: 0 });
|
|
302
|
+
const glideRef = useRef<{ dx: number; dy: number; count: number; playing: boolean }>({ dx: 0, dy: 0, count: 0, playing: false });
|
|
303
|
+
const allowGlide = glide && !resizing;
|
|
252
304
|
if (placedRef.current !== rect) {
|
|
253
305
|
const dx = placedRef.current.x - rect.x;
|
|
254
306
|
const dy = placedRef.current.y - rect.y;
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
307
|
+
// A slide belongs to the relayout that moved the tile: a later render
|
|
308
|
+
// (a tick, a hover, the end of a resize) never plays it again.
|
|
309
|
+
glideRef.current = allowGlide && (Math.abs(dx) >= 0.5 || Math.abs(dy) >= 0.5)
|
|
310
|
+
? { dx, dy, count: glideRef.current.count + 1, playing: true }
|
|
311
|
+
: { ...glideRef.current, playing: false };
|
|
258
312
|
placedRef.current = rect;
|
|
313
|
+
} else if (resizing && glideRef.current.playing) {
|
|
314
|
+
// A resize stops a slide under way: the tile sits where its layout has it.
|
|
315
|
+
glideRef.current = { ...glideRef.current, playing: false };
|
|
259
316
|
}
|
|
260
|
-
const motion = heatTileMotion({ reducedMotion, glide, glideCount: glideRef.current.count, pulseCount });
|
|
317
|
+
const motion = heatTileMotion({ reducedMotion, resizing, glide: glideRef.current.playing, glideCount: glideRef.current.count, pulseCount });
|
|
261
318
|
const colors = useThemeColors();
|
|
262
319
|
const ring = resolveHeatmapSelectionRing(colors);
|
|
263
320
|
// The colour this tile showed before, kept to fade out over the new one.
|
|
@@ -274,10 +331,13 @@ const DomHeatTile = memo(function DomHeatTile({
|
|
|
274
331
|
else if (visible) fadeRef.current = { from: shownRef.current, key: (fadeRef.current?.key ?? 0) + 1 };
|
|
275
332
|
shownRef.current = background;
|
|
276
333
|
}
|
|
334
|
+
// A resize ends a fade under way, and the pulses due meanwhile are dropped, not saved for later.
|
|
335
|
+
if (resizing) fadeRef.current = null;
|
|
277
336
|
const fade = fadeRef.current;
|
|
278
337
|
// A finished overlay leaves the DOM, so idle tiles carry no extra nodes.
|
|
279
338
|
const [, setOverlayVersion] = useState(0);
|
|
280
339
|
const pulseDoneRef = useRef(0);
|
|
340
|
+
if (resizing) pulseDoneRef.current = Math.max(pulseDoneRef.current, pulseCount);
|
|
281
341
|
const pulsing = motion.pulseAnimation != null && pulseCount > pulseDoneRef.current;
|
|
282
342
|
const style: CSSProperties = {
|
|
283
343
|
position: "absolute",
|
|
@@ -491,7 +551,7 @@ function DomTooltip<T>({ state, itemsById, otherLines }: {
|
|
|
491
551
|
const EMPTY_PULSES: ReadonlyMap<string, number> = new Map();
|
|
492
552
|
|
|
493
553
|
function DomHeatTreemap<T>({
|
|
494
|
-
scene, items, width, height, selectedId, onSelect, onActivate, otherTooltip, otherSummary: summarizeOther,
|
|
554
|
+
scene, resizing, items, width, height, selectedId, onSelect, onActivate, otherTooltip, otherSummary: summarizeOther,
|
|
495
555
|
}: HeatTreemapSurfaceProps<T>) {
|
|
496
556
|
const reducedMotion = usePrefersReducedMotion();
|
|
497
557
|
const colors = useThemeColors();
|
|
@@ -519,13 +579,14 @@ function DomHeatTreemap<T>({
|
|
|
519
579
|
);
|
|
520
580
|
}, [itemsById, reducedMotion, scene]);
|
|
521
581
|
|
|
522
|
-
//
|
|
582
|
+
// The layout a resize settles on comes in at once, as a chart or a table
|
|
583
|
+
// redraws at its new size: tiles slide only when sizes move at rest.
|
|
523
584
|
const sizeKey = `${scene.width}x${scene.height}`;
|
|
524
|
-
const
|
|
525
|
-
const glide =
|
|
585
|
+
const restedRef = useRef({ sizeKey, resizing });
|
|
586
|
+
const glide = !resizing && !restedRef.current.resizing && restedRef.current.sizeKey === sizeKey;
|
|
526
587
|
useEffect(() => {
|
|
527
|
-
|
|
528
|
-
}, [sizeKey]);
|
|
588
|
+
restedRef.current = { sizeKey, resizing };
|
|
589
|
+
}, [resizing, sizeKey]);
|
|
529
590
|
|
|
530
591
|
const gap = scene.width < NARROW_CANVAS_PX ? 0.5 : 1;
|
|
531
592
|
const otherLines = useMemo(
|
|
@@ -603,6 +664,7 @@ function DomHeatTreemap<T>({
|
|
|
603
664
|
foreground={tileColors.foreground}
|
|
604
665
|
selected={tile.item.id === selectedId}
|
|
605
666
|
reducedMotion={reducedMotion}
|
|
667
|
+
resizing={resizing}
|
|
606
668
|
glide={glide}
|
|
607
669
|
pulseCount={pulses.get(tile.item.id) ?? 0}
|
|
608
670
|
events={events}
|
|
@@ -755,7 +817,7 @@ function TerminalHeatTreemap<T>({
|
|
|
755
817
|
* calm tiles (ticker, then the move, as space allows), a continuous scale
|
|
756
818
|
* from the theme's down colour to its up colour, and on the desktop a hover
|
|
757
819
|
* tooltip, colour fades, a short pulse on a tile whose move jumps and a glide
|
|
758
|
-
* when sizes change.
|
|
820
|
+
* when sizes change, none of them while the box is being resized.
|
|
759
821
|
*/
|
|
760
822
|
export function HeatTreemapSurface<T>(props: HeatTreemapSurfaceProps<T>) {
|
|
761
823
|
const { nativePaneChrome } = useUiCapabilities();
|
|
@@ -240,15 +240,17 @@ export interface HeatTileMotion {
|
|
|
240
240
|
* tile from its old place (its new size applies at once), a colour change
|
|
241
241
|
* fades the old colour out over the new one, and a jump in the move flashes
|
|
242
242
|
* once. `glideCount` alternates two keyframe names, which restarts the slide.
|
|
243
|
-
* Reduced motion keeps every change instant
|
|
243
|
+
* Reduced motion keeps every change instant, and so does a resize: while the
|
|
244
|
+
* box is changing size nothing on the board moves on its own.
|
|
244
245
|
*/
|
|
245
|
-
export function heatTileMotion({ reducedMotion, glide, glideCount, pulseCount }: {
|
|
246
|
+
export function heatTileMotion({ reducedMotion, resizing, glide, glideCount, pulseCount }: {
|
|
246
247
|
reducedMotion: boolean;
|
|
248
|
+
resizing: boolean;
|
|
247
249
|
glide: boolean;
|
|
248
250
|
glideCount: number;
|
|
249
251
|
pulseCount: number;
|
|
250
252
|
}): HeatTileMotion {
|
|
251
|
-
if (reducedMotion) return {};
|
|
253
|
+
if (reducedMotion || resizing) return {};
|
|
252
254
|
return {
|
|
253
255
|
...(glide && glideCount > 0
|
|
254
256
|
? { glideAnimation: `gloom-heat-glide-${glideCount % 2 === 0 ? "b" : "a"} ${GLIDE_MS}ms ease` }
|
|
@@ -18,7 +18,7 @@ export {
|
|
|
18
18
|
type MetricTreemapDirection,
|
|
19
19
|
type MetricTreemapItem,
|
|
20
20
|
} from "./layout";
|
|
21
|
-
export { buildHeatTreemapScene, heatTreemapCanvas, HeatTreemapSurface } from "./heat-surface";
|
|
21
|
+
export { buildHeatTreemapScene, heatTreemapCanvas, HeatTreemapSurface, useHeatTreemapScene } from "./heat-surface";
|
|
22
22
|
export { formatHeatTileMove, settleTreemapLayoutItems } from "./heat-tiles";
|
|
23
23
|
|
|
24
24
|
type PreventableMouseEvent = { preventDefault(): void };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { forwardRef, useMemo } from "react";
|
|
2
|
-
import { t } from "../../i18n";
|
|
1
|
+
import { forwardRef, useCallback, useMemo } from "react";
|
|
2
|
+
import { t, tf } from "../../i18n";
|
|
3
3
|
import { hoverBg } from "../../theme/colors";
|
|
4
4
|
import { useThemeColors } from "../../theme/theme-context";
|
|
5
|
-
import { Box, Text, useUiCapabilities, type ScrollBoxRenderable } from "../../ui";
|
|
5
|
+
import { Box, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../ui";
|
|
6
6
|
import { wrapTextLines } from "../../utils/text-wrap";
|
|
7
|
-
import { MarkdownText } from "../markdown-text";
|
|
7
|
+
import { MarkdownText, markdownLinks, type MarkdownTextProps } from "../markdown-text";
|
|
8
8
|
import { DetailScrollBody } from "../ui/detail-scroll-body";
|
|
9
9
|
import { KeyValueRow } from "../ui/display";
|
|
10
10
|
import { ExternalLink, usePaneLinkMenuEntry } from "../ui/external-link";
|
|
@@ -121,7 +121,7 @@ function NativeDetail({ meta, facts, body, bodyFormat, note }: DetailContentProp
|
|
|
121
121
|
) : null}
|
|
122
122
|
{bodyFormat === "markdown" && body ? (
|
|
123
123
|
<Box flexDirection="column" width="100%" style={MARKDOWN_STYLE}>
|
|
124
|
-
<
|
|
124
|
+
<MarkdownBody text={body} paragraphGap={PARAGRAPH_GAP} headingStyle={READER_HEADING_STYLE} />
|
|
125
125
|
</Box>
|
|
126
126
|
) : blocks.map((block, index) => (
|
|
127
127
|
<Box key={index} flexDirection="column">
|
|
@@ -140,6 +140,28 @@ function NativeDetail({ meta, facts, body, bodyFormat, note }: DetailContentProp
|
|
|
140
140
|
);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Markdown whose `[label](url)` links open outside the app, as the reader's
|
|
145
|
+
* other links do. The terminal cannot press part of a line, so the pane menu
|
|
146
|
+
* lists each link there (inside a `PaneLinkMenu`).
|
|
147
|
+
*/
|
|
148
|
+
function MarkdownBody({ text, ...style }: Pick<MarkdownTextProps, "text" | "lineWidth" | "paragraphGap" | "headingStyle">) {
|
|
149
|
+
const rendererHost = useRendererHost();
|
|
150
|
+
const links = useMemo(() => markdownLinks(text), [text]);
|
|
151
|
+
const open = useCallback((href: string) => { void rendererHost.openExternal(href); }, [rendererHost]);
|
|
152
|
+
return (
|
|
153
|
+
<>
|
|
154
|
+
<MarkdownText text={text} onOpenLink={open} {...style} />
|
|
155
|
+
{links.map((link, index) => <LinkMenuEntry key={index} {...link} onOpen={open} />)}
|
|
156
|
+
</>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function LinkMenuEntry({ label, href, onOpen }: { label: string; href: string; onOpen: (href: string) => void }) {
|
|
161
|
+
usePaneLinkMenuEntry(`link:${href}`, tf("Open {label}", { label }), () => onOpen(href));
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
143
165
|
function FactValue({ fact }: { fact: DetailFact }) {
|
|
144
166
|
const colors = useThemeColors();
|
|
145
167
|
const { link } = fact;
|
|
@@ -167,7 +189,7 @@ function TerminalDetail({ width, meta, facts, body, bodyFormat, note }: DetailCo
|
|
|
167
189
|
return (
|
|
168
190
|
<Box flexDirection="column" width={lineWidth}>
|
|
169
191
|
{metaLines.map((line, index) => <Text key={index} fg={colors.textMuted}>{line}</Text>)}
|
|
170
|
-
<
|
|
192
|
+
<MarkdownBody text={body ?? ""} lineWidth={lineWidth} />
|
|
171
193
|
</Box>
|
|
172
194
|
);
|
|
173
195
|
}
|
|
@@ -68,6 +68,8 @@ export function TickerBadge({
|
|
|
68
68
|
// badges; the desktop leaves a hairline of row above and below.
|
|
69
69
|
justifyContent={nativePaneChrome ? "center" : undefined}
|
|
70
70
|
style={nativePaneChrome ? NATIVE_CHIP_STYLE : undefined}
|
|
71
|
+
role="button"
|
|
72
|
+
aria-label={symbol}
|
|
71
73
|
data-gloom-context-menu-surface="true"
|
|
72
74
|
onMouseOver={() => {
|
|
73
75
|
onHoverStart?.();
|
|
@@ -26,6 +26,14 @@ export interface ButtonProps {
|
|
|
26
26
|
onPress?: () => void;
|
|
27
27
|
variant?: ButtonVariant;
|
|
28
28
|
disabled?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* The action it started is still running (a retry waiting for its answer).
|
|
31
|
+
* It reads as disabled and ignores presses, but keeps its key, so it does not
|
|
32
|
+
* change size and the key does not fall through to something else.
|
|
33
|
+
*/
|
|
34
|
+
pending?: boolean;
|
|
35
|
+
/** What it says while pending, such as "Retrying...". Without it the label stays. */
|
|
36
|
+
pendingLabel?: string;
|
|
29
37
|
active?: boolean;
|
|
30
38
|
shortcut?: string;
|
|
31
39
|
/**
|
|
@@ -86,9 +94,11 @@ export function Button({
|
|
|
86
94
|
displayLabel,
|
|
87
95
|
children,
|
|
88
96
|
expanded,
|
|
89
|
-
onPress,
|
|
97
|
+
onPress: pressAction,
|
|
90
98
|
variant = "secondary",
|
|
91
99
|
disabled = false,
|
|
100
|
+
pending = false,
|
|
101
|
+
pendingLabel,
|
|
92
102
|
active = false,
|
|
93
103
|
shortcut,
|
|
94
104
|
desktopShortcut,
|
|
@@ -100,17 +110,21 @@ export function Button({
|
|
|
100
110
|
title,
|
|
101
111
|
}: ButtonProps) {
|
|
102
112
|
const colors = useThemeColors();
|
|
103
|
-
const label = t(rawLabel);
|
|
113
|
+
const label = t(pending && pendingLabel ? pendingLabel : rawLabel);
|
|
114
|
+
if (pending && pendingLabel) displayLabel = undefined;
|
|
115
|
+
// A pending press is ignored, but the button stays its scope's primary action.
|
|
116
|
+
const onPress = pressAction && pending ? () => {} : pressAction;
|
|
104
117
|
const scopedShortcut = useScopedButtonAction(label, onPress, disabled);
|
|
105
118
|
shortcut = shortcut ?? scopedShortcut;
|
|
119
|
+
const inert = disabled || pending;
|
|
106
120
|
const remoteScope = useRemoteUiScope();
|
|
107
121
|
useRemoteUiNode({
|
|
108
122
|
role: "button",
|
|
109
123
|
label,
|
|
110
|
-
disabled,
|
|
124
|
+
disabled: inert,
|
|
111
125
|
actions: {
|
|
112
126
|
press: () => {
|
|
113
|
-
if (!
|
|
127
|
+
if (!inert) onPress?.();
|
|
114
128
|
},
|
|
115
129
|
},
|
|
116
130
|
metadata: { variant, active, shortcut, expanded, ...(remoteScope ? { scope: remoteScope } : {}) },
|
|
@@ -124,7 +138,8 @@ export function Button({
|
|
|
124
138
|
expanded={expanded}
|
|
125
139
|
onPress={onPress}
|
|
126
140
|
variant={variant}
|
|
127
|
-
disabled={
|
|
141
|
+
disabled={inert}
|
|
142
|
+
pending={pending}
|
|
128
143
|
active={active}
|
|
129
144
|
shortcut={shortcut ?? desktopShortcut}
|
|
130
145
|
title={title}
|
|
@@ -137,7 +152,7 @@ export function Button({
|
|
|
137
152
|
);
|
|
138
153
|
}
|
|
139
154
|
|
|
140
|
-
const palette = resolveButtonColors(variant, active,
|
|
155
|
+
const palette = resolveButtonColors(variant, active, inert, colors);
|
|
141
156
|
|
|
142
157
|
return (
|
|
143
158
|
<Box
|
|
@@ -145,20 +160,20 @@ export function Button({
|
|
|
145
160
|
height={height ?? 1}
|
|
146
161
|
flexDirection="row"
|
|
147
162
|
backgroundColor={palette.bg}
|
|
148
|
-
cursor={
|
|
163
|
+
cursor={inert ? "default" : "pointer"}
|
|
149
164
|
onMouseDown={(event: { preventDefault?: () => void; stopPropagation?: () => void }) => {
|
|
150
165
|
if (stopPropagation) {
|
|
151
166
|
event.preventDefault?.();
|
|
152
167
|
event.stopPropagation?.();
|
|
153
168
|
}
|
|
154
|
-
if (!
|
|
169
|
+
if (!inert) onPress?.();
|
|
155
170
|
}}
|
|
156
171
|
>
|
|
157
172
|
{children ?? <Text fg={palette.fg} attributes={active ? TextAttributes.BOLD : 0}>
|
|
158
173
|
{compact ? displayLabel ?? label : ` ${displayLabel ?? label} `}
|
|
159
174
|
</Text>}
|
|
160
175
|
{shortcut && (
|
|
161
|
-
<Text fg={
|
|
176
|
+
<Text fg={inert ? colors.textMuted : colors.textDim}>
|
|
162
177
|
{` ${shortcut}`}
|
|
163
178
|
</Text>
|
|
164
179
|
)}
|
|
@@ -226,7 +226,12 @@ export function Divider({ width, height, orientation = "horizontal" }: DividerPr
|
|
|
226
226
|
flexShrink={0}
|
|
227
227
|
border={vertical ? ["left"] : ["top"]}
|
|
228
228
|
borderColor={colors.border}
|
|
229
|
-
style={{
|
|
229
|
+
style={{
|
|
230
|
+
border: 0,
|
|
231
|
+
[vertical ? "borderLeft" : "borderTop"]: `1px solid ${colors.border}`,
|
|
232
|
+
// A vertical rule given no height runs the height of the row it stands in.
|
|
233
|
+
...(vertical && height === undefined ? { height: "auto", alignSelf: "stretch" } : {}),
|
|
234
|
+
}}
|
|
230
235
|
role="separator"
|
|
231
236
|
data-gloom-ui="divider"
|
|
232
237
|
/>
|
|
@@ -98,11 +98,18 @@ function handleOpen(
|
|
|
98
98
|
onOpen(url);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
/** Desktop and web: a link inside a line of text runs and wraps with the line, rather than standing as a box of its own. */
|
|
102
|
+
const INLINE_LINK_STYLE = { display: "inline", whiteSpace: "pre-wrap", overflowWrap: "anywhere" } as const;
|
|
103
|
+
|
|
101
104
|
export function ExternalLinkText(
|
|
102
|
-
{ url, label, color, onOpen = openUrl }: {
|
|
105
|
+
{ url, label, color, attributes = 0, inline = false, onOpen = openUrl }: {
|
|
103
106
|
url: string;
|
|
104
107
|
label?: string;
|
|
105
108
|
color?: string;
|
|
109
|
+
/** Marks drawn with the underline, such as the bold of a bold link. */
|
|
110
|
+
attributes?: number;
|
|
111
|
+
/** Flows inside a line of text on the desktop and the web. */
|
|
112
|
+
inline?: boolean;
|
|
106
113
|
onOpen?: (url: string) => void;
|
|
107
114
|
},
|
|
108
115
|
) {
|
|
@@ -128,8 +135,9 @@ export function ExternalLinkText(
|
|
|
128
135
|
return (
|
|
129
136
|
<Text
|
|
130
137
|
fg={color ?? colors.textBright}
|
|
131
|
-
attributes={TextAttributes.UNDERLINE}
|
|
138
|
+
attributes={TextAttributes.UNDERLINE | attributes}
|
|
132
139
|
cursor="pointer"
|
|
140
|
+
style={inline ? INLINE_LINK_STYLE : undefined}
|
|
133
141
|
role="link"
|
|
134
142
|
tabIndex={0}
|
|
135
143
|
wrapText
|