gloomberb 0.6.0 → 0.7.0
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/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +70 -0
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +17 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +0 -3
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +478 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { createElement, useMemo, type SVGProps } from "react";
|
|
2
|
+
import { Box, ChartSurface, Span, Text, TextAttributes, useNativeRenderer, useUiCapabilities, useUiHost } from "../ui";
|
|
3
|
+
import { computeBitmapSize, type NativeChartBitmap } from "./chart/native/chart-rasterizer";
|
|
4
|
+
import { colors } from "../theme/colors";
|
|
5
|
+
|
|
6
|
+
export interface SpeedometerSegment {
|
|
7
|
+
from: number;
|
|
8
|
+
to: number;
|
|
9
|
+
label: string;
|
|
10
|
+
color: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SpeedometerGaugeProps {
|
|
14
|
+
value: number;
|
|
15
|
+
valueLabel: string;
|
|
16
|
+
width: number;
|
|
17
|
+
segments: SpeedometerSegment[];
|
|
18
|
+
min?: number;
|
|
19
|
+
max?: number;
|
|
20
|
+
currentLabel?: string;
|
|
21
|
+
minWidth?: number;
|
|
22
|
+
maxWidth?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface GaugeCell {
|
|
26
|
+
char: string;
|
|
27
|
+
color?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface GaugeChunk {
|
|
31
|
+
text: string;
|
|
32
|
+
color?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface RgbaColor {
|
|
36
|
+
r: number;
|
|
37
|
+
g: number;
|
|
38
|
+
b: number;
|
|
39
|
+
a: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_MIN_WIDTH = 34;
|
|
43
|
+
const DEFAULT_MAX_WIDTH = 50;
|
|
44
|
+
const DESKTOP_VIEWBOX_WIDTH = 520;
|
|
45
|
+
const DESKTOP_VIEWBOX_HEIGHT = 232;
|
|
46
|
+
const DESKTOP_CENTER_X = 260;
|
|
47
|
+
const DESKTOP_CENTER_Y = 182;
|
|
48
|
+
const DESKTOP_ARC_RADIUS = 138;
|
|
49
|
+
const DESKTOP_NEEDLE_RADIUS = 104;
|
|
50
|
+
const DESKTOP_LABEL_POSITIONS = [
|
|
51
|
+
{ x: 90, y: 84 },
|
|
52
|
+
{ x: 172, y: 44 },
|
|
53
|
+
{ x: 260, y: 30 },
|
|
54
|
+
{ x: 348, y: 44 },
|
|
55
|
+
{ x: 430, y: 84 },
|
|
56
|
+
];
|
|
57
|
+
const DESKTOP_TICK_LABEL_POSITIONS = [
|
|
58
|
+
{ x: 78, y: 224 },
|
|
59
|
+
{ x: 170, y: 224 },
|
|
60
|
+
{ x: 260, y: 224 },
|
|
61
|
+
{ x: 350, y: 224 },
|
|
62
|
+
{ x: 442, y: 224 },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function SvgText(props: SVGProps<SVGTextElement>) {
|
|
66
|
+
return createElement("text", props);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function clamp(value: number, min: number, max: number): number {
|
|
70
|
+
return Math.max(min, Math.min(max, value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeValue(value: number, min: number, max: number): number {
|
|
74
|
+
if (max <= min) return 0;
|
|
75
|
+
return clamp((value - min) / (max - min), 0, 1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function valueToAngle(value: number, min: number, max: number): number {
|
|
79
|
+
return Math.PI - normalizeValue(value, min, max) * Math.PI;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function valueToDegrees(value: number, min: number, max: number): number {
|
|
83
|
+
return -90 + normalizeValue(value, min, max) * 180;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatGaugeValue(value: number): string {
|
|
87
|
+
if (!Number.isFinite(value)) return "--";
|
|
88
|
+
return String(Math.round(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function segmentForValue(value: number, segments: SpeedometerSegment[]): SpeedometerSegment | null {
|
|
92
|
+
return segments.find((segment) => value >= segment.from && value <= segment.to)
|
|
93
|
+
?? segments[segments.length - 1]
|
|
94
|
+
?? null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function blankGaugeLine(width: number): GaugeCell[] {
|
|
98
|
+
return Array.from({ length: width }, () => ({ char: " " }));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function placeText(cells: GaugeCell[], label: string, center: number, color?: string): void {
|
|
102
|
+
const width = cells.length;
|
|
103
|
+
if (width <= 0) return;
|
|
104
|
+
const text = label.length > width ? label.slice(0, width) : label;
|
|
105
|
+
const start = Math.max(0, Math.min(width - text.length, Math.round(center - text.length / 2)));
|
|
106
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
107
|
+
cells[start + index] = { char: text[index]!, color };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function cellsToChunks(cells: GaugeCell[]): GaugeChunk[] {
|
|
112
|
+
const chunks: GaugeChunk[] = [];
|
|
113
|
+
for (const cell of cells) {
|
|
114
|
+
const last = chunks[chunks.length - 1];
|
|
115
|
+
if (last && last.color === cell.color) {
|
|
116
|
+
last.text += cell.char;
|
|
117
|
+
} else {
|
|
118
|
+
chunks.push({ text: cell.char, color: cell.color });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return chunks;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function segmentColorForScore(score: number, segments: SpeedometerSegment[]): string {
|
|
125
|
+
return segmentForValue(score, segments)?.color ?? colors.textDim;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function labelForSegment(segment: SpeedometerSegment, gaugeWidth: number): string {
|
|
129
|
+
if (gaugeWidth >= 64) return segment.label;
|
|
130
|
+
const compact = compactSegmentLabel(segment);
|
|
131
|
+
if (gaugeWidth >= 44) {
|
|
132
|
+
return compact === "NEUTRAL" ? "NEUT" : compact;
|
|
133
|
+
}
|
|
134
|
+
if (compact === "EXT FEAR") return "XF";
|
|
135
|
+
if (compact === "EXT GREED") return "XG";
|
|
136
|
+
if (compact === "NEUTRAL") return "NEUT";
|
|
137
|
+
return compact;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function compactSegmentLabel(segment: SpeedometerSegment): string {
|
|
141
|
+
return segment.label.replace(/^EXTREME /, "EXT ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function renderLabelRow(width: number, min: number, max: number, segments: SpeedometerSegment[]): GaugeChunk[] {
|
|
145
|
+
const cells = blankGaugeLine(width);
|
|
146
|
+
for (const segment of segments) {
|
|
147
|
+
const centerValue = (segment.from + segment.to) / 2;
|
|
148
|
+
const center = normalizeValue(centerValue, min, max) * (width - 1);
|
|
149
|
+
placeText(cells, labelForSegment(segment, width), center, segment.color);
|
|
150
|
+
}
|
|
151
|
+
return cellsToChunks(cells);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderTickRow(width: number, min: number, max: number): GaugeChunk[] {
|
|
155
|
+
const cells = blankGaugeLine(width);
|
|
156
|
+
for (const tick of [min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max]) {
|
|
157
|
+
placeText(cells, formatGaugeValue(tick), normalizeValue(tick, min, max) * (width - 1), colors.textDim);
|
|
158
|
+
}
|
|
159
|
+
return cellsToChunks(cells);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function renderArcRows(value: number, min: number, max: number, width: number, segments: SpeedometerSegment[]): GaugeChunk[][] {
|
|
163
|
+
const dialHeight = 7;
|
|
164
|
+
const dial = Array.from({ length: dialHeight }, () => blankGaugeLine(width));
|
|
165
|
+
const centerX = Math.floor((width - 1) / 2);
|
|
166
|
+
const centerY = dialHeight - 1;
|
|
167
|
+
const radiusX = Math.max(8, (width - 4) / 2);
|
|
168
|
+
const radiusY = dialHeight - 1;
|
|
169
|
+
|
|
170
|
+
for (let step = 0; step <= 220; step += 1) {
|
|
171
|
+
const normalized = step / 220;
|
|
172
|
+
const score = min + normalized * (max - min);
|
|
173
|
+
const angle = Math.PI - normalized * Math.PI;
|
|
174
|
+
const x = Math.round(centerX + Math.cos(angle) * radiusX);
|
|
175
|
+
const y = Math.round(centerY - Math.sin(angle) * radiusY);
|
|
176
|
+
if (dial[y]?.[x]) {
|
|
177
|
+
dial[y]![x] = { char: "●", color: segmentColorForScore(score, segments) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const tick of [min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max]) {
|
|
182
|
+
const angle = valueToAngle(tick, min, max);
|
|
183
|
+
const x = Math.round(centerX + Math.cos(angle) * radiusX);
|
|
184
|
+
const y = Math.round(centerY - Math.sin(angle) * radiusY);
|
|
185
|
+
if (dial[y]?.[x]) {
|
|
186
|
+
dial[y]![x] = { char: "│", color: colors.textDim };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const pointerAngle = valueToAngle(value, min, max);
|
|
191
|
+
const pointerChar = Math.abs(Math.cos(pointerAngle)) < 0.22
|
|
192
|
+
? "│"
|
|
193
|
+
: Math.cos(pointerAngle) > 0
|
|
194
|
+
? "/"
|
|
195
|
+
: "\\";
|
|
196
|
+
for (let t = 0.14; t <= 0.75; t += 0.06) {
|
|
197
|
+
const x = Math.round(centerX + Math.cos(pointerAngle) * radiusX * t);
|
|
198
|
+
const y = Math.round(centerY - Math.sin(pointerAngle) * radiusY * t);
|
|
199
|
+
if (dial[y]?.[x]) {
|
|
200
|
+
dial[y]![x] = { char: pointerChar, color: colors.textBright };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
placeText(dial[centerY]!, formatGaugeValue(value), centerX, colors.textBright);
|
|
205
|
+
return dial.map(cellsToChunks);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function GaugeLine({ chunks }: { chunks: GaugeChunk[] }) {
|
|
209
|
+
return (
|
|
210
|
+
<Text>
|
|
211
|
+
{chunks.map((chunk, index) => (
|
|
212
|
+
<Span key={`${index}-${chunk.text}`} fg={chunk.color}>{chunk.text}</Span>
|
|
213
|
+
))}
|
|
214
|
+
</Text>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseHex(hex: string, alpha = 1): RgbaColor {
|
|
219
|
+
const normalized = hex.replace("#", "");
|
|
220
|
+
return {
|
|
221
|
+
r: parseInt(normalized.slice(0, 2), 16),
|
|
222
|
+
g: parseInt(normalized.slice(2, 4), 16),
|
|
223
|
+
b: parseInt(normalized.slice(4, 6), 16),
|
|
224
|
+
a: Math.round(clamp(alpha, 0, 1) * 255),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function blendPixel(
|
|
229
|
+
data: Uint8Array,
|
|
230
|
+
width: number,
|
|
231
|
+
height: number,
|
|
232
|
+
x: number,
|
|
233
|
+
y: number,
|
|
234
|
+
color: RgbaColor,
|
|
235
|
+
opacity = 1,
|
|
236
|
+
) {
|
|
237
|
+
if (x < 0 || y < 0 || x >= width || y >= height) return;
|
|
238
|
+
|
|
239
|
+
const alpha = clamp((color.a / 255) * opacity, 0, 1);
|
|
240
|
+
if (alpha <= 0) return;
|
|
241
|
+
|
|
242
|
+
const index = (y * width + x) * 4;
|
|
243
|
+
const dstAlpha = data[index + 3]! / 255;
|
|
244
|
+
const outAlpha = alpha + dstAlpha * (1 - alpha);
|
|
245
|
+
if (outAlpha <= 0) return;
|
|
246
|
+
|
|
247
|
+
const dstFactor = dstAlpha * (1 - alpha);
|
|
248
|
+
data[index] = Math.round((color.r * alpha + data[index]! * dstFactor) / outAlpha);
|
|
249
|
+
data[index + 1] = Math.round((color.g * alpha + data[index + 1]! * dstFactor) / outAlpha);
|
|
250
|
+
data[index + 2] = Math.round((color.b * alpha + data[index + 2]! * dstFactor) / outAlpha);
|
|
251
|
+
data[index + 3] = Math.round(outAlpha * 255);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function drawCircle(data: Uint8Array, width: number, height: number, centerX: number, centerY: number, radius: number, color: RgbaColor) {
|
|
255
|
+
const minX = Math.floor(centerX - radius - 1);
|
|
256
|
+
const maxX = Math.ceil(centerX + radius + 1);
|
|
257
|
+
const minY = Math.floor(centerY - radius - 1);
|
|
258
|
+
const maxY = Math.ceil(centerY + radius + 1);
|
|
259
|
+
|
|
260
|
+
for (let y = minY; y <= maxY; y += 1) {
|
|
261
|
+
for (let x = minX; x <= maxX; x += 1) {
|
|
262
|
+
const distance = Math.hypot(x + 0.5 - centerX, y + 0.5 - centerY);
|
|
263
|
+
const coverage = 1 - clamp((distance - (radius - 0.75)) / 1.5, 0, 1);
|
|
264
|
+
if (coverage > 0) blendPixel(data, width, height, x, y, color, coverage);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function drawLine(data: Uint8Array, width: number, height: number, x0: number, y0: number, x1: number, y1: number, color: RgbaColor, thickness: number) {
|
|
270
|
+
const half = thickness / 2;
|
|
271
|
+
const minX = Math.floor(Math.min(x0, x1) - half - 1);
|
|
272
|
+
const maxX = Math.ceil(Math.max(x0, x1) + half + 1);
|
|
273
|
+
const minY = Math.floor(Math.min(y0, y1) - half - 1);
|
|
274
|
+
const maxY = Math.ceil(Math.max(y0, y1) + half + 1);
|
|
275
|
+
const dx = x1 - x0;
|
|
276
|
+
const dy = y1 - y0;
|
|
277
|
+
const segmentLengthSq = dx * dx + dy * dy || 1;
|
|
278
|
+
|
|
279
|
+
for (let y = minY; y <= maxY; y += 1) {
|
|
280
|
+
for (let x = minX; x <= maxX; x += 1) {
|
|
281
|
+
const projection = clamp((((x + 0.5) - x0) * dx + ((y + 0.5) - y0) * dy) / segmentLengthSq, 0, 1);
|
|
282
|
+
const nearestX = x0 + dx * projection;
|
|
283
|
+
const nearestY = y0 + dy * projection;
|
|
284
|
+
const distance = Math.hypot(x + 0.5 - nearestX, y + 0.5 - nearestY);
|
|
285
|
+
const coverage = 1 - clamp((distance - half) / 1.2, 0, 1);
|
|
286
|
+
if (coverage > 0) blendPixel(data, width, height, x, y, color, coverage);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function renderGaugeBitmap(
|
|
292
|
+
value: number,
|
|
293
|
+
min: number,
|
|
294
|
+
max: number,
|
|
295
|
+
segments: SpeedometerSegment[],
|
|
296
|
+
pixelWidth: number,
|
|
297
|
+
pixelHeight: number,
|
|
298
|
+
): NativeChartBitmap {
|
|
299
|
+
const pixels = new Uint8Array(pixelWidth * pixelHeight * 4);
|
|
300
|
+
const centerX = pixelWidth / 2;
|
|
301
|
+
const centerY = pixelHeight * 0.84;
|
|
302
|
+
const radius = Math.min(pixelWidth * 0.42, pixelHeight * 0.72);
|
|
303
|
+
const arcThickness = Math.max(4, Math.min(pixelWidth, pixelHeight) * 0.038);
|
|
304
|
+
|
|
305
|
+
for (const segment of segments) {
|
|
306
|
+
const start = valueToAngle(segment.from, min, max);
|
|
307
|
+
const end = valueToAngle(segment.to, min, max);
|
|
308
|
+
const steps = Math.max(8, Math.ceil(Math.abs(end - start) * radius * 0.36));
|
|
309
|
+
const color = parseHex(segment.color, 0.9);
|
|
310
|
+
for (let index = 0; index <= steps; index += 1) {
|
|
311
|
+
const t = index / steps;
|
|
312
|
+
const angle = start + (end - start) * t;
|
|
313
|
+
const x = centerX + Math.cos(angle) * radius;
|
|
314
|
+
const y = centerY - Math.sin(angle) * radius;
|
|
315
|
+
drawCircle(pixels, pixelWidth, pixelHeight, x, y, arcThickness, color);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
for (const tick of [min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max]) {
|
|
320
|
+
const angle = valueToAngle(tick, min, max);
|
|
321
|
+
const innerX = centerX + Math.cos(angle) * (radius - arcThickness * 1.45);
|
|
322
|
+
const innerY = centerY - Math.sin(angle) * (radius - arcThickness * 1.45);
|
|
323
|
+
const outerX = centerX + Math.cos(angle) * (radius + arcThickness * 1.25);
|
|
324
|
+
const outerY = centerY - Math.sin(angle) * (radius + arcThickness * 1.25);
|
|
325
|
+
drawLine(pixels, pixelWidth, pixelHeight, innerX, innerY, outerX, outerY, parseHex(colors.textDim, 0.8), Math.max(1, arcThickness * 0.22));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const angle = valueToAngle(value, min, max);
|
|
329
|
+
const needleEndX = centerX + Math.cos(angle) * (radius * 0.76);
|
|
330
|
+
const needleEndY = centerY - Math.sin(angle) * (radius * 0.76);
|
|
331
|
+
drawLine(pixels, pixelWidth, pixelHeight, centerX, centerY, needleEndX, needleEndY, parseHex(colors.textBright, 0.98), Math.max(3, arcThickness * 0.48));
|
|
332
|
+
|
|
333
|
+
return { width: pixelWidth, height: pixelHeight, pixels };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function polarToCartesian(centerX: number, centerY: number, radius: number, angleDegrees: number) {
|
|
337
|
+
const angleRadians = (angleDegrees - 90) * Math.PI / 180;
|
|
338
|
+
return {
|
|
339
|
+
x: centerX + radius * Math.cos(angleRadians),
|
|
340
|
+
y: centerY + radius * Math.sin(angleRadians),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function describeArc(centerX: number, centerY: number, radius: number, startAngle: number, endAngle: number): string {
|
|
345
|
+
const start = polarToCartesian(centerX, centerY, radius, endAngle);
|
|
346
|
+
const end = polarToCartesian(centerX, centerY, radius, startAngle);
|
|
347
|
+
const largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
|
|
348
|
+
return `M ${start.x.toFixed(2)} ${start.y.toFixed(2)} A ${radius} ${radius} 0 ${largeArcFlag} 0 ${end.x.toFixed(2)} ${end.y.toFixed(2)}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function DesktopGauge({
|
|
352
|
+
value,
|
|
353
|
+
valueLabel,
|
|
354
|
+
width,
|
|
355
|
+
segments,
|
|
356
|
+
min,
|
|
357
|
+
max,
|
|
358
|
+
currentLabel,
|
|
359
|
+
minWidth,
|
|
360
|
+
maxWidth,
|
|
361
|
+
}: Required<SpeedometerGaugeProps>) {
|
|
362
|
+
const gaugeWidth = Math.min(Math.max(width - 2, minWidth), maxWidth);
|
|
363
|
+
const needleAngle = valueToDegrees(value, min, max);
|
|
364
|
+
const needleEnd = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_NEEDLE_RADIUS, needleAngle);
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<Box
|
|
368
|
+
width={gaugeWidth}
|
|
369
|
+
height={12}
|
|
370
|
+
marginTop={1}
|
|
371
|
+
overflow="hidden"
|
|
372
|
+
style={{ alignSelf: "center", maxWidth: 420 }}
|
|
373
|
+
>
|
|
374
|
+
<svg
|
|
375
|
+
viewBox={`0 0 ${DESKTOP_VIEWBOX_WIDTH} ${DESKTOP_VIEWBOX_HEIGHT}`}
|
|
376
|
+
width="100%"
|
|
377
|
+
height="100%"
|
|
378
|
+
role="img"
|
|
379
|
+
aria-label={`${currentLabel} ${formatGaugeValue(value)} ${valueLabel}`}
|
|
380
|
+
style={{ display: "block" }}
|
|
381
|
+
>
|
|
382
|
+
{segments.map((segment) => (
|
|
383
|
+
<path
|
|
384
|
+
key={segment.label}
|
|
385
|
+
d={describeArc(
|
|
386
|
+
DESKTOP_CENTER_X,
|
|
387
|
+
DESKTOP_CENTER_Y,
|
|
388
|
+
DESKTOP_ARC_RADIUS,
|
|
389
|
+
valueToDegrees(segment.from, min, max),
|
|
390
|
+
valueToDegrees(segment.to, min, max),
|
|
391
|
+
)}
|
|
392
|
+
fill="none"
|
|
393
|
+
stroke={segment.color}
|
|
394
|
+
strokeWidth="22"
|
|
395
|
+
strokeLinecap="butt"
|
|
396
|
+
opacity={value >= segment.from && value <= segment.to ? 0.96 : 0.42}
|
|
397
|
+
/>
|
|
398
|
+
))}
|
|
399
|
+
{segments.map((segment, index) => {
|
|
400
|
+
const fixedPoint = segments.length === DESKTOP_LABEL_POSITIONS.length
|
|
401
|
+
? DESKTOP_LABEL_POSITIONS[index]
|
|
402
|
+
: null;
|
|
403
|
+
const midpoint = (valueToDegrees(segment.from, min, max) + valueToDegrees(segment.to, min, max)) / 2;
|
|
404
|
+
const point = fixedPoint ?? polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS + 36, midpoint);
|
|
405
|
+
return (
|
|
406
|
+
<SvgText
|
|
407
|
+
key={`label:${segment.label}`}
|
|
408
|
+
x={point.x}
|
|
409
|
+
y={point.y}
|
|
410
|
+
fill={segment.color}
|
|
411
|
+
textAnchor="middle"
|
|
412
|
+
dominantBaseline="middle"
|
|
413
|
+
fontFamily="inherit"
|
|
414
|
+
fontSize="14"
|
|
415
|
+
fontWeight="700"
|
|
416
|
+
>
|
|
417
|
+
{compactSegmentLabel(segment)}
|
|
418
|
+
</SvgText>
|
|
419
|
+
);
|
|
420
|
+
})}
|
|
421
|
+
{[min, min + (max - min) * 0.25, min + (max - min) * 0.5, min + (max - min) * 0.75, max].map((tick, index) => {
|
|
422
|
+
const angle = valueToDegrees(tick, min, max);
|
|
423
|
+
const outer = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS + 16, angle);
|
|
424
|
+
const inner = polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS - 10, angle);
|
|
425
|
+
const label = DESKTOP_TICK_LABEL_POSITIONS[index]
|
|
426
|
+
?? polarToCartesian(DESKTOP_CENTER_X, DESKTOP_CENTER_Y, DESKTOP_ARC_RADIUS + 34, angle);
|
|
427
|
+
return (
|
|
428
|
+
<g key={tick}>
|
|
429
|
+
<line x1={inner.x} y1={inner.y} x2={outer.x} y2={outer.y} stroke={colors.textDim} strokeWidth="2" />
|
|
430
|
+
<SvgText x={label.x} y={label.y} fill={colors.textDim} textAnchor="middle" fontFamily="inherit" fontSize="13">
|
|
431
|
+
{formatGaugeValue(tick)}
|
|
432
|
+
</SvgText>
|
|
433
|
+
</g>
|
|
434
|
+
);
|
|
435
|
+
})}
|
|
436
|
+
<line
|
|
437
|
+
x1={DESKTOP_CENTER_X}
|
|
438
|
+
y1={DESKTOP_CENTER_Y}
|
|
439
|
+
x2={needleEnd.x}
|
|
440
|
+
y2={needleEnd.y}
|
|
441
|
+
stroke={colors.textBright}
|
|
442
|
+
strokeWidth="7"
|
|
443
|
+
strokeLinecap="round"
|
|
444
|
+
/>
|
|
445
|
+
<circle cx={DESKTOP_CENTER_X} cy={DESKTOP_CENTER_Y} r="27" fill={colors.bg} />
|
|
446
|
+
<SvgText x={DESKTOP_CENTER_X} y={DESKTOP_CENTER_Y + 18} fill={colors.textBright} textAnchor="middle" fontFamily="inherit" fontSize="31" fontWeight="800">
|
|
447
|
+
{formatGaugeValue(value)}
|
|
448
|
+
</SvgText>
|
|
449
|
+
</svg>
|
|
450
|
+
</Box>
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function TerminalGauge({
|
|
455
|
+
value,
|
|
456
|
+
valueLabel,
|
|
457
|
+
width,
|
|
458
|
+
segments,
|
|
459
|
+
min,
|
|
460
|
+
max,
|
|
461
|
+
currentLabel,
|
|
462
|
+
minWidth,
|
|
463
|
+
maxWidth,
|
|
464
|
+
}: Required<SpeedometerGaugeProps>) {
|
|
465
|
+
const {
|
|
466
|
+
nativeCharts,
|
|
467
|
+
cellWidthPx = 8,
|
|
468
|
+
cellHeightPx = 18,
|
|
469
|
+
pixelRatio = 1,
|
|
470
|
+
} = useUiCapabilities();
|
|
471
|
+
const nativeRenderer = useNativeRenderer();
|
|
472
|
+
const gaugeWidth = Math.max(minWidth, Math.min(maxWidth, Math.floor(width - 4)));
|
|
473
|
+
const labelRow = useMemo(() => renderLabelRow(gaugeWidth, min, max, segments), [gaugeWidth, max, min, segments]);
|
|
474
|
+
const tickRow = useMemo(() => renderTickRow(gaugeWidth, min, max), [gaugeWidth, max, min]);
|
|
475
|
+
const arcRows = useMemo(() => renderArcRows(value, min, max, gaugeWidth, segments), [gaugeWidth, max, min, segments, value]);
|
|
476
|
+
const rendererResolution = nativeRenderer.resolution;
|
|
477
|
+
const rendererTerminalWidth = nativeRenderer.terminalWidth;
|
|
478
|
+
const rendererTerminalHeight = nativeRenderer.terminalHeight;
|
|
479
|
+
const bitmap = useMemo<NativeChartBitmap | null>(() => {
|
|
480
|
+
if (!nativeCharts) return null;
|
|
481
|
+
const bitmapSize = rendererResolution && rendererTerminalWidth > 0 && rendererTerminalHeight > 0
|
|
482
|
+
? computeBitmapSize(
|
|
483
|
+
{ x: 0, y: 0, width: gaugeWidth, height: arcRows.length },
|
|
484
|
+
rendererResolution,
|
|
485
|
+
rendererTerminalWidth,
|
|
486
|
+
rendererTerminalHeight,
|
|
487
|
+
)
|
|
488
|
+
: null;
|
|
489
|
+
const scale = Math.max(1, pixelRatio);
|
|
490
|
+
const pixelWidth = bitmapSize?.pixelWidth ?? Math.max(1, Math.round(gaugeWidth * cellWidthPx * scale));
|
|
491
|
+
const pixelHeight = bitmapSize?.pixelHeight ?? Math.max(1, Math.round(arcRows.length * cellHeightPx * scale));
|
|
492
|
+
return renderGaugeBitmap(
|
|
493
|
+
value,
|
|
494
|
+
min,
|
|
495
|
+
max,
|
|
496
|
+
segments,
|
|
497
|
+
pixelWidth,
|
|
498
|
+
pixelHeight,
|
|
499
|
+
);
|
|
500
|
+
}, [
|
|
501
|
+
arcRows.length,
|
|
502
|
+
cellHeightPx,
|
|
503
|
+
cellWidthPx,
|
|
504
|
+
gaugeWidth,
|
|
505
|
+
max,
|
|
506
|
+
min,
|
|
507
|
+
nativeCharts,
|
|
508
|
+
pixelRatio,
|
|
509
|
+
rendererResolution,
|
|
510
|
+
rendererTerminalHeight,
|
|
511
|
+
rendererTerminalWidth,
|
|
512
|
+
segments,
|
|
513
|
+
value,
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<Box flexDirection="column" alignItems="center" marginTop={1}>
|
|
518
|
+
<GaugeLine chunks={labelRow} />
|
|
519
|
+
<ChartSurface width={gaugeWidth} height={arcRows.length} flexDirection="column" bitmaps={bitmap ? [bitmap] : null}>
|
|
520
|
+
{arcRows.map((chunks, index) => (
|
|
521
|
+
<GaugeLine key={index} chunks={chunks} />
|
|
522
|
+
))}
|
|
523
|
+
</ChartSurface>
|
|
524
|
+
<GaugeLine chunks={tickRow} />
|
|
525
|
+
<Box flexDirection="row" marginTop={1}>
|
|
526
|
+
<Text fg={colors.textDim}>{currentLabel} </Text>
|
|
527
|
+
<Text fg={segmentColorForScore(value, segments)} attributes={TextAttributes.BOLD}>{valueLabel}</Text>
|
|
528
|
+
</Box>
|
|
529
|
+
</Box>
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function SpeedometerGauge({
|
|
534
|
+
value,
|
|
535
|
+
valueLabel,
|
|
536
|
+
width,
|
|
537
|
+
segments,
|
|
538
|
+
min = 0,
|
|
539
|
+
max = 100,
|
|
540
|
+
currentLabel = "Current reading",
|
|
541
|
+
minWidth = DEFAULT_MIN_WIDTH,
|
|
542
|
+
maxWidth = DEFAULT_MAX_WIDTH,
|
|
543
|
+
}: SpeedometerGaugeProps) {
|
|
544
|
+
const props = {
|
|
545
|
+
value,
|
|
546
|
+
valueLabel,
|
|
547
|
+
width,
|
|
548
|
+
segments,
|
|
549
|
+
min,
|
|
550
|
+
max,
|
|
551
|
+
currentLabel,
|
|
552
|
+
minWidth,
|
|
553
|
+
maxWidth,
|
|
554
|
+
};
|
|
555
|
+
return useUiHost().kind === "desktop-web"
|
|
556
|
+
? <DesktopGauge {...props} />
|
|
557
|
+
: <TerminalGauge {...props} />;
|
|
558
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type RefObject,
|
|
8
|
+
} from "react";
|
|
9
|
+
import { Box, type ScrollBoxRenderable } from "../ui";
|
|
10
|
+
|
|
11
|
+
export interface TableViewKeyEvent {
|
|
12
|
+
name?: string;
|
|
13
|
+
ctrl?: boolean;
|
|
14
|
+
meta?: boolean;
|
|
15
|
+
option?: boolean;
|
|
16
|
+
shift?: boolean;
|
|
17
|
+
preventDefault?: () => void;
|
|
18
|
+
stopPropagation?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TableViewFrameProps {
|
|
22
|
+
width?: number;
|
|
23
|
+
height?: number;
|
|
24
|
+
backgroundColor?: string;
|
|
25
|
+
before?: ReactNode;
|
|
26
|
+
after?: ReactNode;
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function TableViewFrame({
|
|
31
|
+
width,
|
|
32
|
+
height,
|
|
33
|
+
backgroundColor,
|
|
34
|
+
before,
|
|
35
|
+
after,
|
|
36
|
+
children,
|
|
37
|
+
}: TableViewFrameProps) {
|
|
38
|
+
return (
|
|
39
|
+
<Box
|
|
40
|
+
flexDirection="column"
|
|
41
|
+
flexGrow={1}
|
|
42
|
+
width={width}
|
|
43
|
+
height={height}
|
|
44
|
+
backgroundColor={backgroundColor}
|
|
45
|
+
overflow="hidden"
|
|
46
|
+
>
|
|
47
|
+
{before}
|
|
48
|
+
{children}
|
|
49
|
+
{after}
|
|
50
|
+
</Box>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isTableActivationKey(name: string | undefined): boolean {
|
|
55
|
+
return name === "enter" || name === "return";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isNextTableRowKey(name: string | undefined): boolean {
|
|
59
|
+
return name === "j" || name === "down";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isPreviousTableRowKey(name: string | undefined): boolean {
|
|
63
|
+
return name === "k" || name === "up";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function stopTableKey(event: TableViewKeyEvent) {
|
|
67
|
+
event.stopPropagation?.();
|
|
68
|
+
event.preventDefault?.();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function useTableViewState({
|
|
72
|
+
headerScrollRef,
|
|
73
|
+
scrollRef,
|
|
74
|
+
syncHeaderScroll,
|
|
75
|
+
hoveredIdx,
|
|
76
|
+
setHoveredIdx,
|
|
77
|
+
}: {
|
|
78
|
+
headerScrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
79
|
+
scrollRef?: RefObject<ScrollBoxRenderable | null>;
|
|
80
|
+
syncHeaderScroll?: () => void;
|
|
81
|
+
hoveredIdx?: number | null;
|
|
82
|
+
setHoveredIdx?: (index: number | null) => void;
|
|
83
|
+
}) {
|
|
84
|
+
const internalHeaderScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
85
|
+
const internalScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
86
|
+
const [internalHoveredIdx, setInternalHoveredIdx] = useState<number | null>(null);
|
|
87
|
+
|
|
88
|
+
const effectiveHeaderScrollRef = headerScrollRef ?? internalHeaderScrollRef;
|
|
89
|
+
const effectiveScrollRef = scrollRef ?? internalScrollRef;
|
|
90
|
+
const effectiveHoveredIdx = hoveredIdx !== undefined ? hoveredIdx : internalHoveredIdx;
|
|
91
|
+
const effectiveSetHoveredIdx = setHoveredIdx ?? setInternalHoveredIdx;
|
|
92
|
+
|
|
93
|
+
const defaultSyncHeaderScroll = useCallback(() => {
|
|
94
|
+
const body = effectiveScrollRef.current;
|
|
95
|
+
const header = effectiveHeaderScrollRef.current;
|
|
96
|
+
if (!body || !header) return;
|
|
97
|
+
if (header.scrollLeft !== body.scrollLeft) {
|
|
98
|
+
header.scrollLeft = body.scrollLeft;
|
|
99
|
+
}
|
|
100
|
+
}, [effectiveHeaderScrollRef, effectiveScrollRef]);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
effectiveHeaderScrollRef,
|
|
104
|
+
effectiveScrollRef,
|
|
105
|
+
effectiveHoveredIdx,
|
|
106
|
+
effectiveSetHoveredIdx,
|
|
107
|
+
effectiveSyncHeaderScroll: syncHeaderScroll ?? defaultSyncHeaderScroll,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useTableBodyScrollActivity({
|
|
112
|
+
onBodyScrollActivity,
|
|
113
|
+
syncHeaderScroll,
|
|
114
|
+
afterScroll,
|
|
115
|
+
}: {
|
|
116
|
+
onBodyScrollActivity?: () => void;
|
|
117
|
+
syncHeaderScroll: () => void;
|
|
118
|
+
afterScroll?: () => void;
|
|
119
|
+
}) {
|
|
120
|
+
return useCallback(() => {
|
|
121
|
+
if (onBodyScrollActivity) {
|
|
122
|
+
onBodyScrollActivity();
|
|
123
|
+
} else {
|
|
124
|
+
queueMicrotask(syncHeaderScroll);
|
|
125
|
+
}
|
|
126
|
+
if (afterScroll) {
|
|
127
|
+
queueMicrotask(afterScroll);
|
|
128
|
+
}
|
|
129
|
+
}, [afterScroll, onBodyScrollActivity, syncHeaderScroll]);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useResetTableScroll({
|
|
133
|
+
headerScrollRef,
|
|
134
|
+
scrollRef,
|
|
135
|
+
resetScrollKey,
|
|
136
|
+
afterReset,
|
|
137
|
+
}: {
|
|
138
|
+
headerScrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
139
|
+
scrollRef: RefObject<ScrollBoxRenderable | null>;
|
|
140
|
+
resetScrollKey?: unknown;
|
|
141
|
+
afterReset?: () => void;
|
|
142
|
+
}) {
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (resetScrollKey === undefined) return;
|
|
145
|
+
const body = scrollRef.current;
|
|
146
|
+
if (body) {
|
|
147
|
+
body.scrollTop = 0;
|
|
148
|
+
body.scrollLeft = 0;
|
|
149
|
+
}
|
|
150
|
+
const header = headerScrollRef.current;
|
|
151
|
+
if (header) {
|
|
152
|
+
header.scrollLeft = 0;
|
|
153
|
+
}
|
|
154
|
+
if (afterReset) {
|
|
155
|
+
queueMicrotask(afterReset);
|
|
156
|
+
}
|
|
157
|
+
}, [afterReset, headerScrollRef, resetScrollKey, scrollRef]);
|
|
158
|
+
}
|