@zendir/ui 0.3.0 → 0.4.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/CHANGELOG.md +15 -0
- package/dist/index.js +67 -62
- package/dist/index.js.map +1 -1
- package/dist/react/3d/ZenSpace3D.js +3 -2
- package/dist/react/3d/ZenSpace3DShaders.js +478 -0
- package/dist/react/3d/ZenSpace3DShaders.js.map +1 -0
- package/dist/react/3d/capturePngAnalysis.js +42 -0
- package/dist/react/3d/capturePngAnalysis.js.map +1 -0
- package/dist/react/3d/index.js +57 -0
- package/dist/react/3d/index.js.map +1 -0
- package/dist/react/3d/threeLoader.js +4 -0
- package/dist/react/3d/threeLoader.js.map +1 -1
- package/dist/react/astro/ClassificationBanner.js +1 -1
- package/dist/react/astro/GlobalStatusBar.js +1 -1
- package/dist/react/astro/MissionClock.js +8 -5
- package/dist/react/astro/MissionClock.js.map +1 -1
- package/dist/react/astro/MonitoringIcon.js +1 -1
- package/dist/react/astro/Notification.js +1 -1
- package/dist/react/astro/Progress.js +1 -1
- package/dist/react/astro/SimulationControls.js +1 -1
- package/dist/react/astro/StatusIndicator.js +1 -1
- package/dist/react/astro/UnifiedTimeline.js +1 -1
- package/dist/react/astro/index.js +34 -0
- package/dist/react/astro/index.js.map +1 -0
- package/dist/react/cards/AccessCard.js +410 -0
- package/dist/react/cards/AccessCard.js.map +1 -0
- package/dist/react/cards/OrbitCard.js +372 -0
- package/dist/react/cards/OrbitCard.js.map +1 -0
- package/dist/react/cards/SpacecraftCard.js +941 -0
- package/dist/react/cards/SpacecraftCard.js.map +1 -0
- package/dist/react/cards/TelemetryCard.js +742 -0
- package/dist/react/cards/TelemetryCard.js.map +1 -0
- package/dist/react/cards/TelemetryStreamCard.js +309 -0
- package/dist/react/cards/TelemetryStreamCard.js.map +1 -0
- package/dist/react/cards/index.js +13 -0
- package/dist/react/cards/index.js.map +1 -0
- package/dist/react/charts/GroundTrackMap.js +1 -1
- package/dist/react/charts/GroundTrackMapLeaflet.js +1 -1
- package/dist/react/charts/index.js +104 -0
- package/dist/react/charts/index.js.map +1 -0
- package/dist/react/charts/unified/AstroChart.js +1 -1
- package/dist/react/charts/unified/PowerOverviewChart.js +488 -0
- package/dist/react/charts/unified/PowerOverviewChart.js.map +1 -0
- package/dist/react/charts/unified/domain.js +3168 -0
- package/dist/react/charts/unified/domain.js.map +1 -0
- package/dist/react/charts/unified/generators.js +518 -0
- package/dist/react/charts/unified/generators.js.map +1 -0
- package/dist/react/charts/unified/presets.js +1000 -0
- package/dist/react/charts/unified/presets.js.map +1 -0
- package/dist/react/charts/unified/sync.js +219 -0
- package/dist/react/charts/unified/sync.js.map +1 -0
- package/dist/react/charts/unified/theme.js +331 -4
- package/dist/react/charts/unified/theme.js.map +1 -1
- package/dist/react/charts/unified/useChartStream.js +226 -0
- package/dist/react/charts/unified/useChartStream.js.map +1 -0
- package/dist/react/chatgpt/index.js +163 -0
- package/dist/react/chatgpt/index.js.map +1 -0
- package/dist/react/core/data/DataTable.js +1 -1
- package/dist/react/core/data/DataValue.js +1 -1
- package/dist/react/core/display/AstroIcon.js +1 -1
- package/dist/react/core/display/Badge.js +1 -1
- package/dist/react/core/display/CardHeader.js +1 -1
- package/dist/react/core/display/Container.js +1 -1
- package/dist/react/core/display/GlassCard.js +1 -1
- package/dist/react/core/display/HeaderIconWithStatus.js +1 -1
- package/dist/react/core/feedback/Dialog.js +24 -21
- package/dist/react/core/feedback/Dialog.js.map +1 -1
- package/dist/react/core/feedback/Toast.js +1 -1
- package/dist/react/core/index.d.ts +4 -0
- package/dist/react/core/index.js +166 -0
- package/dist/react/core/index.js.map +1 -0
- package/dist/react/core/inputs/Button.js +1 -1
- package/dist/react/core/inputs/Checkbox.js +1 -1
- package/dist/react/core/inputs/Input.js +1 -1
- package/dist/react/core/inputs/NumberInput.js +1 -1
- package/dist/react/core/inputs/PinInput.js +1 -1
- package/dist/react/core/inputs/Select.js +1 -1
- package/dist/react/core/inputs/Toggle.js +1 -1
- package/dist/react/core/layout/Box.js +1 -1
- package/dist/react/core/layout/Flex.js +1 -1
- package/dist/react/core/layout/Grid.js +1 -1
- package/dist/react/core/layout/Stack.js +1 -1
- package/dist/react/core/navigation/AppBar.js +1 -1
- package/dist/react/core/navigation/Pagination.js +1 -1
- package/dist/react/core/navigation/SideNav.js +1 -1
- package/dist/react/core/navigation/SideNav.js.map +1 -1
- package/dist/react/core/navigation/Tabs.js +1 -1
- package/dist/react/core/overlays/SidePanel.js +1 -1
- package/dist/react/core/overlays/Tooltip.js +1 -1
- package/dist/react/core/widgets/Capture.js +1 -1
- package/dist/react/core/widgets/ChatPanel.js +1 -1
- package/dist/react/core/widgets/ColorPickerPanel.js +1 -1
- package/dist/react/core/widgets/ConnectionForm.js +1 -1
- package/dist/react/core/widgets/FileExplorer.js +1 -1
- package/dist/react/core/widgets/ImageFrame.d.ts +51 -0
- package/dist/react/core/widgets/ImageFrame.js +201 -0
- package/dist/react/core/widgets/ImageFrame.js.map +1 -0
- package/dist/react/core/widgets/ImageGallery.js +1 -1
- package/dist/react/core/widgets/ImagePanel.d.ts +43 -0
- package/dist/react/core/widgets/ImagePanel.js +105 -0
- package/dist/react/core/widgets/ImagePanel.js.map +1 -0
- package/dist/react/core/widgets/MessageStream.js +1 -1
- package/dist/react/core/widgets/MissionCalendar.js +1 -1
- package/dist/react/hooks/useCompactMode.js +1 -1
- package/dist/react/index.d.ts +2 -2
- package/dist/react/panels/index.js +8 -0
- package/dist/react/panels/index.js.map +1 -0
- package/dist/react/theme/config.js +30 -0
- package/dist/react/theme/config.js.map +1 -0
- package/dist/react/theme/index.js +19 -0
- package/dist/react/theme/index.js.map +1 -0
- package/dist/react/utils/index.js +8 -0
- package/dist/react/utils/index.js.map +1 -1
- package/dist/react.js +67 -62
- package/dist/react.js.map +1 -1
- package/package.json +46 -38
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { useRef, useState, useCallback, useEffect } from "react";
|
|
2
|
+
function useChartStream(chartRef, options = {}) {
|
|
3
|
+
const {
|
|
4
|
+
maxPoints: _maxPoints = 1e3,
|
|
5
|
+
timeWindow: _timeWindow,
|
|
6
|
+
throttleMs = 16,
|
|
7
|
+
// ~60fps
|
|
8
|
+
bufferSize = 10,
|
|
9
|
+
decimate = true,
|
|
10
|
+
decimateThreshold = 5e3
|
|
11
|
+
} = options;
|
|
12
|
+
const bufferRef = useRef(/* @__PURE__ */ new Map());
|
|
13
|
+
const lastFlushRef = useRef(0);
|
|
14
|
+
const pointCountRef = useRef(0);
|
|
15
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
16
|
+
const animationFrameRef = useRef();
|
|
17
|
+
const flush = useCallback(() => {
|
|
18
|
+
const chart = chartRef.current;
|
|
19
|
+
if (!chart || isPaused) return;
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (now - lastFlushRef.current < throttleMs) {
|
|
22
|
+
animationFrameRef.current = requestAnimationFrame(flush);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
bufferRef.current.forEach((points, seriesIndex) => {
|
|
26
|
+
if (points.length === 0) return;
|
|
27
|
+
let processedPoints = points;
|
|
28
|
+
if (decimate && pointCountRef.current > decimateThreshold) {
|
|
29
|
+
processedPoints = decimateData(points, Math.ceil(points.length / 2));
|
|
30
|
+
}
|
|
31
|
+
processedPoints.forEach((point) => {
|
|
32
|
+
chart.appendData(seriesIndex, point);
|
|
33
|
+
});
|
|
34
|
+
pointCountRef.current += processedPoints.length;
|
|
35
|
+
});
|
|
36
|
+
bufferRef.current.clear();
|
|
37
|
+
lastFlushRef.current = now;
|
|
38
|
+
}, [chartRef, isPaused, throttleMs, decimate, decimateThreshold]);
|
|
39
|
+
const push = useCallback((seriesIndex, point) => {
|
|
40
|
+
if (isPaused) return;
|
|
41
|
+
let buffer = bufferRef.current.get(seriesIndex);
|
|
42
|
+
if (!buffer) {
|
|
43
|
+
buffer = [];
|
|
44
|
+
bufferRef.current.set(seriesIndex, buffer);
|
|
45
|
+
}
|
|
46
|
+
buffer.push(point);
|
|
47
|
+
if (buffer.length >= bufferSize) {
|
|
48
|
+
flush();
|
|
49
|
+
} else if (!animationFrameRef.current) {
|
|
50
|
+
animationFrameRef.current = requestAnimationFrame(() => {
|
|
51
|
+
animationFrameRef.current = void 0;
|
|
52
|
+
flush();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}, [isPaused, bufferSize, flush]);
|
|
56
|
+
const pushBatch = useCallback((seriesIndex, points) => {
|
|
57
|
+
if (isPaused || points.length === 0) return;
|
|
58
|
+
let buffer = bufferRef.current.get(seriesIndex);
|
|
59
|
+
if (!buffer) {
|
|
60
|
+
buffer = [];
|
|
61
|
+
bufferRef.current.set(seriesIndex, buffer);
|
|
62
|
+
}
|
|
63
|
+
buffer.push(...points);
|
|
64
|
+
flush();
|
|
65
|
+
}, [isPaused, flush]);
|
|
66
|
+
const clear = useCallback(() => {
|
|
67
|
+
var _a;
|
|
68
|
+
bufferRef.current.clear();
|
|
69
|
+
pointCountRef.current = 0;
|
|
70
|
+
(_a = chartRef.current) == null ? void 0 : _a.clearData();
|
|
71
|
+
}, [chartRef]);
|
|
72
|
+
const pause = useCallback(() => setIsPaused(true), []);
|
|
73
|
+
const resume = useCallback(() => setIsPaused(false), []);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
return () => {
|
|
76
|
+
if (animationFrameRef.current) {
|
|
77
|
+
cancelAnimationFrame(animationFrameRef.current);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
return {
|
|
82
|
+
push,
|
|
83
|
+
pushBatch,
|
|
84
|
+
clear,
|
|
85
|
+
pause,
|
|
86
|
+
resume,
|
|
87
|
+
isPaused,
|
|
88
|
+
pointCount: pointCountRef.current
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function decimateData(data, threshold) {
|
|
92
|
+
if (data.length <= threshold) return data;
|
|
93
|
+
const result = [];
|
|
94
|
+
const bucketSize = (data.length - 2) / (threshold - 2);
|
|
95
|
+
result.push(data[0]);
|
|
96
|
+
let a = 0;
|
|
97
|
+
for (let i = 0; i < threshold - 2; i++) {
|
|
98
|
+
const bucketStart = Math.floor((i + 1) * bucketSize) + 1;
|
|
99
|
+
const bucketEnd = Math.floor((i + 2) * bucketSize) + 1;
|
|
100
|
+
const bucketLength = Math.min(bucketEnd, data.length) - bucketStart;
|
|
101
|
+
let avgX = 0;
|
|
102
|
+
let avgY = 0;
|
|
103
|
+
const nextBucketStart = Math.floor((i + 2) * bucketSize) + 1;
|
|
104
|
+
const nextBucketEnd = Math.min(Math.floor((i + 3) * bucketSize) + 1, data.length);
|
|
105
|
+
const nextBucketLength = nextBucketEnd - nextBucketStart;
|
|
106
|
+
for (let j = nextBucketStart; j < nextBucketEnd; j++) {
|
|
107
|
+
const point = data[j];
|
|
108
|
+
const [x, y] = getPointXY(point);
|
|
109
|
+
avgX += x;
|
|
110
|
+
avgY += y;
|
|
111
|
+
}
|
|
112
|
+
avgX /= nextBucketLength || 1;
|
|
113
|
+
avgY /= nextBucketLength || 1;
|
|
114
|
+
const [aX, aY] = getPointXY(data[a]);
|
|
115
|
+
let maxArea = -1;
|
|
116
|
+
let maxIdx = bucketStart;
|
|
117
|
+
for (let j = bucketStart; j < bucketStart + bucketLength; j++) {
|
|
118
|
+
const point = data[j];
|
|
119
|
+
const [x, y] = getPointXY(point);
|
|
120
|
+
const area = Math.abs(
|
|
121
|
+
(aX - avgX) * (y - aY) - (aX - x) * (avgY - aY)
|
|
122
|
+
);
|
|
123
|
+
if (area > maxArea) {
|
|
124
|
+
maxArea = area;
|
|
125
|
+
maxIdx = j;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
result.push(data[maxIdx]);
|
|
129
|
+
a = maxIdx;
|
|
130
|
+
}
|
|
131
|
+
result.push(data[data.length - 1]);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function getPointXY(point) {
|
|
135
|
+
if ("time" in point) {
|
|
136
|
+
return [
|
|
137
|
+
typeof point.time === "number" ? point.time : new Date(point.time).getTime(),
|
|
138
|
+
point.value
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
return [
|
|
142
|
+
typeof point.x === "number" ? point.x : 0,
|
|
143
|
+
point.y
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function useWebSocketStream(chartRef, options) {
|
|
147
|
+
const {
|
|
148
|
+
url,
|
|
149
|
+
reconnect = true,
|
|
150
|
+
reconnectDelay = 1e3,
|
|
151
|
+
maxReconnects = 10,
|
|
152
|
+
parseMessage,
|
|
153
|
+
...streamOptions
|
|
154
|
+
} = options;
|
|
155
|
+
const wsRef = useRef(null);
|
|
156
|
+
const reconnectCountRef = useRef(0);
|
|
157
|
+
const reconnectTimerRef = useRef();
|
|
158
|
+
const [connectionState, setConnectionState] = useState("disconnected");
|
|
159
|
+
const stream = useChartStream(chartRef, streamOptions);
|
|
160
|
+
const connect = useCallback(() => {
|
|
161
|
+
var _a;
|
|
162
|
+
if (((_a = wsRef.current) == null ? void 0 : _a.readyState) === WebSocket.OPEN) return;
|
|
163
|
+
setConnectionState("connecting");
|
|
164
|
+
const ws = new WebSocket(url);
|
|
165
|
+
ws.onopen = () => {
|
|
166
|
+
setConnectionState("connected");
|
|
167
|
+
reconnectCountRef.current = 0;
|
|
168
|
+
};
|
|
169
|
+
ws.onmessage = (event) => {
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(event.data);
|
|
172
|
+
const parsed = parseMessage ? parseMessage(data) : defaultParseMessage(data);
|
|
173
|
+
if (parsed) {
|
|
174
|
+
stream.push(parsed.seriesIndex, parsed.point);
|
|
175
|
+
}
|
|
176
|
+
} catch (e) {
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
ws.onerror = () => {
|
|
180
|
+
setConnectionState("error");
|
|
181
|
+
};
|
|
182
|
+
ws.onclose = () => {
|
|
183
|
+
setConnectionState("disconnected");
|
|
184
|
+
if (reconnect && reconnectCountRef.current < maxReconnects) {
|
|
185
|
+
reconnectCountRef.current++;
|
|
186
|
+
reconnectTimerRef.current = setTimeout(connect, reconnectDelay * reconnectCountRef.current);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
wsRef.current = ws;
|
|
190
|
+
}, [url, reconnect, reconnectDelay, maxReconnects, parseMessage, stream]);
|
|
191
|
+
const disconnect = useCallback(() => {
|
|
192
|
+
var _a;
|
|
193
|
+
clearTimeout(reconnectTimerRef.current);
|
|
194
|
+
(_a = wsRef.current) == null ? void 0 : _a.close();
|
|
195
|
+
wsRef.current = null;
|
|
196
|
+
}, []);
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
connect();
|
|
199
|
+
return disconnect;
|
|
200
|
+
}, [connect, disconnect]);
|
|
201
|
+
return {
|
|
202
|
+
...stream,
|
|
203
|
+
connectionState,
|
|
204
|
+
connect,
|
|
205
|
+
disconnect
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function defaultParseMessage(data) {
|
|
209
|
+
if (typeof data !== "object" || data === null) return null;
|
|
210
|
+
const obj = data;
|
|
211
|
+
if ("time" in obj && "value" in obj) {
|
|
212
|
+
return {
|
|
213
|
+
seriesIndex: typeof obj.seriesIndex === "number" ? obj.seriesIndex : 0,
|
|
214
|
+
point: {
|
|
215
|
+
time: obj.time,
|
|
216
|
+
value: obj.value
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
export {
|
|
223
|
+
useChartStream,
|
|
224
|
+
useWebSocketStream
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=useChartStream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useChartStream.js","sources":["../../../../src/react/charts/unified/useChartStream.ts"],"sourcesContent":["/**\n * @zendir/ui - Real-time Chart Streaming Hook\n * \n * Provides efficient real-time data streaming for charts.\n * Handles buffering, throttling, and memory management.\n */\n\nimport { useRef, useCallback, useEffect, useState } from 'react';\nimport type { AstroChartHandle } from './AstroChart';\nimport type { TimeSeriesPoint, DataPoint } from './types';\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface StreamOptions {\n /** Maximum data points to keep */\n maxPoints?: number;\n /** Time window in milliseconds (alternative to maxPoints) */\n timeWindow?: number;\n /** Throttle interval in milliseconds */\n throttleMs?: number;\n /** Buffer size before flushing */\n bufferSize?: number;\n /** Enable data decimation for large datasets */\n decimate?: boolean;\n /** Decimation threshold (start decimating above this count) */\n decimateThreshold?: number;\n}\n\nexport interface StreamController {\n /** Push a single data point */\n push: (seriesIndex: number, point: TimeSeriesPoint | DataPoint) => void;\n /** Push multiple data points */\n pushBatch: (seriesIndex: number, points: (TimeSeriesPoint | DataPoint)[]) => void;\n /** Clear all data */\n clear: () => void;\n /** Pause streaming */\n pause: () => void;\n /** Resume streaming */\n resume: () => void;\n /** Get current state */\n isPaused: boolean;\n /** Get current point count */\n pointCount: number;\n}\n\n// =============================================================================\n// Hook Implementation\n// =============================================================================\n\nexport function useChartStream(\n chartRef: React.RefObject<AstroChartHandle>,\n options: StreamOptions = {}\n): StreamController {\n const {\n maxPoints: _maxPoints = 1000,\n timeWindow: _timeWindow,\n throttleMs = 16, // ~60fps\n bufferSize = 10,\n decimate = true,\n decimateThreshold = 5000,\n } = options;\n\n const bufferRef = useRef<Map<number, (TimeSeriesPoint | DataPoint)[]>>(new Map());\n const lastFlushRef = useRef<number>(0);\n const pointCountRef = useRef<number>(0);\n const [isPaused, setIsPaused] = useState(false);\n const animationFrameRef = useRef<number>();\n\n // Flush buffer to chart\n const flush = useCallback(() => {\n const chart = chartRef.current;\n if (!chart || isPaused) return;\n\n const now = Date.now();\n if (now - lastFlushRef.current < throttleMs) {\n // Schedule next flush\n animationFrameRef.current = requestAnimationFrame(flush);\n return;\n }\n\n bufferRef.current.forEach((points, seriesIndex) => {\n if (points.length === 0) return;\n\n // Apply decimation if needed\n let processedPoints = points;\n if (decimate && pointCountRef.current > decimateThreshold) {\n processedPoints = decimateData(points, Math.ceil(points.length / 2));\n }\n\n // Append each point\n processedPoints.forEach(point => {\n chart.appendData(seriesIndex, point);\n });\n\n pointCountRef.current += processedPoints.length;\n });\n\n // Clear buffer\n bufferRef.current.clear();\n lastFlushRef.current = now;\n }, [chartRef, isPaused, throttleMs, decimate, decimateThreshold]);\n\n // Push single point\n const push = useCallback((seriesIndex: number, point: TimeSeriesPoint | DataPoint) => {\n if (isPaused) return;\n\n let buffer = bufferRef.current.get(seriesIndex);\n if (!buffer) {\n buffer = [];\n bufferRef.current.set(seriesIndex, buffer);\n }\n\n buffer.push(point);\n\n // Flush if buffer is full\n if (buffer.length >= bufferSize) {\n flush();\n } else if (!animationFrameRef.current) {\n // Schedule flush\n animationFrameRef.current = requestAnimationFrame(() => {\n animationFrameRef.current = undefined;\n flush();\n });\n }\n }, [isPaused, bufferSize, flush]);\n\n // Push batch\n const pushBatch = useCallback((seriesIndex: number, points: (TimeSeriesPoint | DataPoint)[]) => {\n if (isPaused || points.length === 0) return;\n\n let buffer = bufferRef.current.get(seriesIndex);\n if (!buffer) {\n buffer = [];\n bufferRef.current.set(seriesIndex, buffer);\n }\n\n buffer.push(...points);\n flush();\n }, [isPaused, flush]);\n\n // Clear all data\n const clear = useCallback(() => {\n bufferRef.current.clear();\n pointCountRef.current = 0;\n chartRef.current?.clearData();\n }, [chartRef]);\n\n // Pause/resume\n const pause = useCallback(() => setIsPaused(true), []);\n const resume = useCallback(() => setIsPaused(false), []);\n\n // Cleanup\n useEffect(() => {\n return () => {\n if (animationFrameRef.current) {\n cancelAnimationFrame(animationFrameRef.current);\n }\n };\n }, []);\n\n return {\n push,\n pushBatch,\n clear,\n pause,\n resume,\n isPaused,\n pointCount: pointCountRef.current,\n };\n}\n\n// =============================================================================\n// Data Decimation\n// =============================================================================\n\n/**\n * Largest Triangle Three Buckets (LTTB) algorithm for data decimation\n * Preserves visual shape while reducing point count\n */\nfunction decimateData<T extends TimeSeriesPoint | DataPoint>(\n data: T[],\n threshold: number\n): T[] {\n if (data.length <= threshold) return data;\n\n const result: T[] = [];\n const bucketSize = (data.length - 2) / (threshold - 2);\n\n // Always include first point\n result.push(data[0]);\n\n let a = 0; // Previous selected point\n\n for (let i = 0; i < threshold - 2; i++) {\n // Calculate bucket range\n const bucketStart = Math.floor((i + 1) * bucketSize) + 1;\n const bucketEnd = Math.floor((i + 2) * bucketSize) + 1;\n const bucketLength = Math.min(bucketEnd, data.length) - bucketStart;\n\n // Calculate average point in next bucket\n let avgX = 0;\n let avgY = 0;\n const nextBucketStart = Math.floor((i + 2) * bucketSize) + 1;\n const nextBucketEnd = Math.min(Math.floor((i + 3) * bucketSize) + 1, data.length);\n const nextBucketLength = nextBucketEnd - nextBucketStart;\n\n for (let j = nextBucketStart; j < nextBucketEnd; j++) {\n const point = data[j];\n const [x, y] = getPointXY(point);\n avgX += x;\n avgY += y;\n }\n avgX /= nextBucketLength || 1;\n avgY /= nextBucketLength || 1;\n\n // Find point with largest triangle area\n const [aX, aY] = getPointXY(data[a]);\n let maxArea = -1;\n let maxIdx = bucketStart;\n\n for (let j = bucketStart; j < bucketStart + bucketLength; j++) {\n const point = data[j];\n const [x, y] = getPointXY(point);\n \n // Triangle area\n const area = Math.abs(\n (aX - avgX) * (y - aY) - (aX - x) * (avgY - aY)\n );\n\n if (area > maxArea) {\n maxArea = area;\n maxIdx = j;\n }\n }\n\n result.push(data[maxIdx]);\n a = maxIdx;\n }\n\n // Always include last point\n result.push(data[data.length - 1]);\n\n return result;\n}\n\nfunction getPointXY(point: TimeSeriesPoint | DataPoint): [number, number] {\n if ('time' in point) {\n return [\n typeof point.time === 'number' ? point.time : new Date(point.time).getTime(),\n point.value,\n ];\n }\n return [\n typeof point.x === 'number' ? point.x : 0,\n point.y,\n ];\n}\n\n// =============================================================================\n// WebSocket Stream Hook\n// =============================================================================\n\nexport interface WebSocketStreamOptions extends StreamOptions {\n /** WebSocket URL */\n url: string;\n /** Reconnect on disconnect */\n reconnect?: boolean;\n /** Reconnect delay in milliseconds */\n reconnectDelay?: number;\n /** Maximum reconnect attempts */\n maxReconnects?: number;\n /** Message parser */\n parseMessage?: (data: unknown) => { seriesIndex: number; point: TimeSeriesPoint | DataPoint } | null;\n}\n\nexport interface WebSocketStreamController extends StreamController {\n /** WebSocket connection state */\n connectionState: 'connecting' | 'connected' | 'disconnected' | 'error';\n /** Manually connect */\n connect: () => void;\n /** Manually disconnect */\n disconnect: () => void;\n}\n\nexport function useWebSocketStream(\n chartRef: React.RefObject<AstroChartHandle>,\n options: WebSocketStreamOptions\n): WebSocketStreamController {\n const {\n url,\n reconnect = true,\n reconnectDelay = 1000,\n maxReconnects = 10,\n parseMessage,\n ...streamOptions\n } = options;\n\n const wsRef = useRef<WebSocket | null>(null);\n const reconnectCountRef = useRef(0);\n const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>();\n const [connectionState, setConnectionState] = useState<WebSocketStreamController['connectionState']>('disconnected');\n \n const stream = useChartStream(chartRef, streamOptions);\n\n const connect = useCallback(() => {\n if (wsRef.current?.readyState === WebSocket.OPEN) return;\n\n setConnectionState('connecting');\n const ws = new WebSocket(url);\n\n ws.onopen = () => {\n setConnectionState('connected');\n reconnectCountRef.current = 0;\n };\n\n ws.onmessage = (event) => {\n try {\n const data = JSON.parse(event.data);\n const parsed = parseMessage \n ? parseMessage(data) \n : defaultParseMessage(data);\n \n if (parsed) {\n stream.push(parsed.seriesIndex, parsed.point);\n }\n } catch (e) {\n if (import.meta.env.DEV) {\n console.error('Failed to parse WebSocket message:', e);\n }\n }\n };\n\n ws.onerror = () => {\n setConnectionState('error');\n };\n\n ws.onclose = () => {\n setConnectionState('disconnected');\n \n if (reconnect && reconnectCountRef.current < maxReconnects) {\n reconnectCountRef.current++;\n reconnectTimerRef.current = setTimeout(connect, reconnectDelay * reconnectCountRef.current);\n }\n };\n\n wsRef.current = ws;\n }, [url, reconnect, reconnectDelay, maxReconnects, parseMessage, stream]);\n\n const disconnect = useCallback(() => {\n clearTimeout(reconnectTimerRef.current);\n wsRef.current?.close();\n wsRef.current = null;\n }, []);\n\n // Auto-connect on mount\n useEffect(() => {\n connect();\n return disconnect;\n }, [connect, disconnect]);\n\n return {\n ...stream,\n connectionState,\n connect,\n disconnect,\n };\n}\n\nfunction defaultParseMessage(data: unknown): { seriesIndex: number; point: TimeSeriesPoint } | null {\n if (typeof data !== 'object' || data === null) return null;\n \n const obj = data as Record<string, unknown>;\n \n if ('time' in obj && 'value' in obj) {\n return {\n seriesIndex: typeof obj.seriesIndex === 'number' ? obj.seriesIndex : 0,\n point: {\n time: obj.time as number,\n value: obj.value as number,\n },\n };\n }\n \n return null;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
const SET_GLOBALS_EVENT_TYPE = "openai:set_globals";
|
|
3
|
+
function useOpenAiGlobal(key) {
|
|
4
|
+
const getSnapshot = useCallback(() => {
|
|
5
|
+
var _a;
|
|
6
|
+
return (_a = window.openai) == null ? void 0 : _a[key];
|
|
7
|
+
}, [key]);
|
|
8
|
+
const subscribe = useCallback((onChange) => {
|
|
9
|
+
const handleSetGlobal = (event) => {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
const customEvent = event;
|
|
12
|
+
if (((_b = (_a = customEvent.detail) == null ? void 0 : _a.globals) == null ? void 0 : _b[key]) !== void 0) {
|
|
13
|
+
onChange();
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, { passive: true });
|
|
17
|
+
return () => window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);
|
|
18
|
+
}, [key]);
|
|
19
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
20
|
+
}
|
|
21
|
+
function useWidgetState(defaultState) {
|
|
22
|
+
const widgetStateFromWindow = useOpenAiGlobal("widgetState");
|
|
23
|
+
const [widgetState, _setWidgetState] = useState(() => {
|
|
24
|
+
if (widgetStateFromWindow != null) {
|
|
25
|
+
return widgetStateFromWindow;
|
|
26
|
+
}
|
|
27
|
+
return typeof defaultState === "function" ? defaultState() : defaultState;
|
|
28
|
+
});
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (widgetStateFromWindow != null) {
|
|
31
|
+
_setWidgetState(widgetStateFromWindow);
|
|
32
|
+
}
|
|
33
|
+
}, [widgetStateFromWindow]);
|
|
34
|
+
const setWidgetState = useCallback((state) => {
|
|
35
|
+
_setWidgetState((prevState) => {
|
|
36
|
+
var _a, _b;
|
|
37
|
+
const newState = typeof state === "function" ? state(prevState) : state;
|
|
38
|
+
(_b = (_a = window.openai) == null ? void 0 : _a.setWidgetState) == null ? void 0 : _b.call(_a, newState);
|
|
39
|
+
return newState;
|
|
40
|
+
});
|
|
41
|
+
}, []);
|
|
42
|
+
return [widgetState, setWidgetState];
|
|
43
|
+
}
|
|
44
|
+
function useToolOutput() {
|
|
45
|
+
return useOpenAiGlobal("toolOutput");
|
|
46
|
+
}
|
|
47
|
+
function useToolInput() {
|
|
48
|
+
return useOpenAiGlobal("toolInput");
|
|
49
|
+
}
|
|
50
|
+
function useChatGPTTheme() {
|
|
51
|
+
const theme = useOpenAiGlobal("theme") ?? "dark";
|
|
52
|
+
const isDark = theme === "dark";
|
|
53
|
+
return {
|
|
54
|
+
theme,
|
|
55
|
+
isDark,
|
|
56
|
+
colors: isDark ? {
|
|
57
|
+
background: "#09090b",
|
|
58
|
+
surface: "#18181b",
|
|
59
|
+
surfaceHover: "#27272a",
|
|
60
|
+
border: "#3f3f46",
|
|
61
|
+
text: "#fafafa",
|
|
62
|
+
textMuted: "#a1a1aa",
|
|
63
|
+
accent: "#22d3ee"
|
|
64
|
+
} : {
|
|
65
|
+
background: "#ffffff",
|
|
66
|
+
surface: "#f4f4f5",
|
|
67
|
+
surfaceHover: "#e4e4e7",
|
|
68
|
+
border: "#d4d4d8",
|
|
69
|
+
text: "#18181b",
|
|
70
|
+
textMuted: "#636370",
|
|
71
|
+
// WCAG AA: 5.90:1 on #fff, 5.39:1 on #f4f4f5, 4.66:1 on #e4e4e7 (was #71717a → 4.49:1 FAIL on surface)
|
|
72
|
+
accent: "#0ea5e9"
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function useLocale() {
|
|
77
|
+
return useOpenAiGlobal("locale") ?? "en-US";
|
|
78
|
+
}
|
|
79
|
+
function useDisplayMode() {
|
|
80
|
+
const mode = useOpenAiGlobal("displayMode") ?? "inline";
|
|
81
|
+
const maxHeight = useOpenAiGlobal("maxHeight");
|
|
82
|
+
const requestMode = useCallback(async (newMode) => {
|
|
83
|
+
var _a, _b;
|
|
84
|
+
await ((_b = (_a = window.openai) == null ? void 0 : _a.requestDisplayMode) == null ? void 0 : _b.call(_a, { mode: newMode }));
|
|
85
|
+
}, []);
|
|
86
|
+
const close = useCallback(() => {
|
|
87
|
+
var _a, _b;
|
|
88
|
+
(_b = (_a = window.openai) == null ? void 0 : _a.requestClose) == null ? void 0 : _b.call(_a);
|
|
89
|
+
}, []);
|
|
90
|
+
return { mode, maxHeight, requestMode, close };
|
|
91
|
+
}
|
|
92
|
+
function useCallTool() {
|
|
93
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
94
|
+
const [error, setError] = useState(null);
|
|
95
|
+
const callTool = useCallback(async (name, args) => {
|
|
96
|
+
var _a;
|
|
97
|
+
if (!((_a = window.openai) == null ? void 0 : _a.callTool)) {
|
|
98
|
+
throw new Error("window.openai.callTool not available");
|
|
99
|
+
}
|
|
100
|
+
setIsLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
try {
|
|
103
|
+
const result = await window.openai.callTool(name, args);
|
|
104
|
+
return result;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
107
|
+
setError(error2);
|
|
108
|
+
throw error2;
|
|
109
|
+
} finally {
|
|
110
|
+
setIsLoading(false);
|
|
111
|
+
}
|
|
112
|
+
}, []);
|
|
113
|
+
return { callTool, isLoading, error };
|
|
114
|
+
}
|
|
115
|
+
function useSendMessage() {
|
|
116
|
+
return useCallback(async (prompt) => {
|
|
117
|
+
var _a, _b;
|
|
118
|
+
await ((_b = (_a = window.openai) == null ? void 0 : _a.sendFollowUpMessage) == null ? void 0 : _b.call(_a, { prompt }));
|
|
119
|
+
}, []);
|
|
120
|
+
}
|
|
121
|
+
function useMaxHeight() {
|
|
122
|
+
const [maxHeight, setMaxHeight] = useState(
|
|
123
|
+
() => {
|
|
124
|
+
var _a;
|
|
125
|
+
return (_a = window.openai) == null ? void 0 : _a.maxHeight;
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const handler = (event) => {
|
|
130
|
+
var _a;
|
|
131
|
+
if (((_a = event.detail) == null ? void 0 : _a.globals) && "maxHeight" in event.detail.globals) {
|
|
132
|
+
setMaxHeight(event.detail.globals.maxHeight);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener("openai:set_globals", handler);
|
|
136
|
+
return () => window.removeEventListener("openai:set_globals", handler);
|
|
137
|
+
}, []);
|
|
138
|
+
return maxHeight;
|
|
139
|
+
}
|
|
140
|
+
function useOpenExternal() {
|
|
141
|
+
return useCallback((href) => {
|
|
142
|
+
var _a, _b;
|
|
143
|
+
(_b = (_a = window.openai) == null ? void 0 : _a.openExternal) == null ? void 0 : _b.call(_a, { href });
|
|
144
|
+
}, []);
|
|
145
|
+
}
|
|
146
|
+
function isInChatGPT() {
|
|
147
|
+
return typeof window !== "undefined" && window.openai !== void 0;
|
|
148
|
+
}
|
|
149
|
+
export {
|
|
150
|
+
isInChatGPT,
|
|
151
|
+
useCallTool,
|
|
152
|
+
useChatGPTTheme,
|
|
153
|
+
useDisplayMode,
|
|
154
|
+
useLocale,
|
|
155
|
+
useMaxHeight,
|
|
156
|
+
useOpenAiGlobal,
|
|
157
|
+
useOpenExternal,
|
|
158
|
+
useSendMessage,
|
|
159
|
+
useToolInput,
|
|
160
|
+
useToolOutput,
|
|
161
|
+
useWidgetState
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/react/chatgpt/index.ts"],"sourcesContent":["/**\n * ChatGPT Apps SDK Integration\n * \n * Hooks and utilities for integrating Zendir UI components with ChatGPT's\n * window.openai API. Based on: https://developers.openai.com/apps-sdk/build/chatgpt-ui/\n * \n * Key capabilities:\n * - window.openai.toolInput / toolOutput - Tool data from MCP server\n * - window.openai.widgetState / setWidgetState - Persistent UI state\n * - window.openai.callTool - Invoke other MCP tools from widget\n * - window.openai.sendFollowUpMessage - Insert user messages\n * - window.openai.theme - Light/dark theme detection\n * - window.openai.locale - Localization\n * - window.openai.requestDisplayMode - Fullscreen/PiP modes\n * - window.openai.notifyIntrinsicHeight - Dynamic height reporting\n */\n\nimport { useState, useEffect, useCallback, useSyncExternalStore } from 'react';\n\n// ============================================================================\n// Type Definitions\n// ============================================================================\n\nexport interface OpenAiGlobals {\n /** Arguments supplied when the tool was invoked */\n toolInput?: Record<string, unknown>;\n /** Your structuredContent - the model reads this */\n toolOutput?: Record<string, unknown>;\n /** Metadata payload - only widget sees it */\n toolResponseMetadata?: Record<string, unknown>;\n /** Snapshot of UI state persisted between renders */\n widgetState?: Record<string, unknown>;\n /** Current theme */\n theme?: 'light' | 'dark';\n /** Current locale (e.g., 'en-US') */\n locale?: string;\n /** Display mode */\n displayMode?: 'inline' | 'pip' | 'fullscreen';\n /** Maximum allowed height */\n maxHeight?: number;\n /** View type */\n view?: 'conversation' | 'modal';\n /** User agent info */\n userAgent?: string;\n /** Safe area insets */\n safeArea?: { top: number; bottom: number; left: number; right: number };\n \n // API Methods\n setWidgetState?: (state: Record<string, unknown>) => void;\n callTool?: (name: string, args: Record<string, unknown>) => Promise<unknown>;\n sendFollowUpMessage?: (options: { prompt: string }) => Promise<void>;\n uploadFile?: (file: File) => Promise<{ fileId: string }>;\n getFileDownloadUrl?: (options: { fileId: string }) => Promise<{ downloadUrl: string }>;\n requestDisplayMode?: (options: { mode: 'inline' | 'pip' | 'fullscreen' }) => Promise<void>;\n requestModal?: (options: unknown) => Promise<void>;\n notifyIntrinsicHeight?: (height: number) => void;\n openExternal?: (options: { href: string }) => void;\n requestClose?: () => void;\n}\n\ndeclare global {\n interface Window {\n openai?: OpenAiGlobals;\n }\n}\n\n// ============================================================================\n// Core Hook: useOpenAiGlobal\n// ============================================================================\n\nconst SET_GLOBALS_EVENT_TYPE = 'openai:set_globals';\n\ninterface SetGlobalsEvent extends CustomEvent {\n detail: { globals: Partial<OpenAiGlobals> };\n}\n\n/**\n * Subscribe to a specific window.openai global value\n */\nexport function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGlobals[K] {\n const getSnapshot = useCallback(() => {\n return window.openai?.[key];\n }, [key]);\n\n const subscribe = useCallback((onChange: () => void) => {\n const handleSetGlobal = (event: Event) => {\n const customEvent = event as SetGlobalsEvent;\n if (customEvent.detail?.globals?.[key] !== undefined) {\n onChange();\n }\n };\n\n window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, { passive: true });\n return () => window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);\n }, [key]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n// ============================================================================\n// Widget State Hook\n// ============================================================================\n\ntype SetStateAction<T> = T | ((prevState: T) => T);\n\n/**\n * Manage widget state that persists across renders via window.openai.setWidgetState\n */\nexport function useWidgetState<T extends Record<string, unknown>>(\n defaultState: T | (() => T)\n): readonly [T, (state: SetStateAction<T>) => void] {\n const widgetStateFromWindow = useOpenAiGlobal('widgetState') as T | undefined;\n\n const [widgetState, _setWidgetState] = useState<T>(() => {\n if (widgetStateFromWindow != null) {\n return widgetStateFromWindow;\n }\n return typeof defaultState === 'function' ? (defaultState as () => T)() : defaultState;\n });\n\n useEffect(() => {\n if (widgetStateFromWindow != null) {\n _setWidgetState(widgetStateFromWindow);\n }\n }, [widgetStateFromWindow]);\n\n const setWidgetState = useCallback((state: SetStateAction<T>) => {\n _setWidgetState((prevState: T) => {\n const newState = typeof state === 'function' ? (state as (p: T) => T)(prevState) : state;\n window.openai?.setWidgetState?.(newState);\n return newState;\n });\n }, []);\n\n return [widgetState, setWidgetState] as const;\n}\n\n// ============================================================================\n// Tool Output Hook\n// ============================================================================\n\n/**\n * Read the tool output (structuredContent) from the MCP server response\n */\nexport function useToolOutput<T = Record<string, unknown>>(): T | undefined {\n return useOpenAiGlobal('toolOutput') as T | undefined;\n}\n\n/**\n * Read the tool input (arguments) from the tool invocation\n */\nexport function useToolInput<T = Record<string, unknown>>(): T | undefined {\n return useOpenAiGlobal('toolInput') as T | undefined;\n}\n\n// ============================================================================\n// Theme Hook\n// ============================================================================\n\nexport type ThemeMode = 'light' | 'dark';\n\n/**\n * Get current ChatGPT theme and provide CSS-friendly values\n * Named useChatGPTTheme to avoid conflict with the core useTheme from ThemeProvider\n */\nexport function useChatGPTTheme(): {\n theme: ThemeMode;\n isDark: boolean;\n colors: {\n background: string;\n surface: string;\n surfaceHover: string;\n border: string;\n text: string;\n textMuted: string;\n accent: string;\n };\n} {\n const theme = useOpenAiGlobal('theme') ?? 'dark';\n const isDark = theme === 'dark';\n\n return {\n theme,\n isDark,\n colors: isDark ? {\n background: '#09090b',\n surface: '#18181b',\n surfaceHover: '#27272a',\n border: '#3f3f46',\n text: '#fafafa',\n textMuted: '#a1a1aa',\n accent: '#22d3ee',\n } : {\n background: '#ffffff',\n surface: '#f4f4f5',\n surfaceHover: '#e4e4e7',\n border: '#d4d4d8',\n text: '#18181b',\n textMuted: '#636370', // WCAG AA: 5.90:1 on #fff, 5.39:1 on #f4f4f5, 4.66:1 on #e4e4e7 (was #71717a → 4.49:1 FAIL on surface)\n accent: '#0ea5e9',\n },\n };\n}\n\n// ============================================================================\n// Locale Hook\n// ============================================================================\n\n/**\n * Get current locale for internationalization\n */\nexport function useLocale(): string {\n return useOpenAiGlobal('locale') ?? 'en-US';\n}\n\n// ============================================================================\n// Display Mode Hook\n// ============================================================================\n\n/**\n * Manage widget display mode (inline, PiP, fullscreen)\n */\nexport function useDisplayMode(): {\n mode: 'inline' | 'pip' | 'fullscreen';\n maxHeight?: number;\n requestMode: (mode: 'inline' | 'pip' | 'fullscreen') => Promise<void>;\n close: () => void;\n} {\n const mode = useOpenAiGlobal('displayMode') ?? 'inline';\n const maxHeight = useOpenAiGlobal('maxHeight');\n\n const requestMode = useCallback(async (newMode: 'inline' | 'pip' | 'fullscreen') => {\n await window.openai?.requestDisplayMode?.({ mode: newMode });\n }, []);\n\n const close = useCallback(() => {\n window.openai?.requestClose?.();\n }, []);\n\n return { mode, maxHeight, requestMode, close };\n}\n\n// ============================================================================\n// Tool Calling Hook\n// ============================================================================\n\n/**\n * Call another MCP tool from within a widget\n */\nexport function useCallTool(): {\n callTool: <T = unknown>(name: string, args: Record<string, unknown>) => Promise<T>;\n isLoading: boolean;\n error: Error | null;\n} {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const callTool = useCallback(async <T = unknown>(\n name: string,\n args: Record<string, unknown>\n ): Promise<T> => {\n if (!window.openai?.callTool) {\n throw new Error('window.openai.callTool not available');\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n const result = await window.openai.callTool(name, args);\n return result as T;\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n setError(error);\n throw error;\n } finally {\n setIsLoading(false);\n }\n }, []);\n\n return { callTool, isLoading, error };\n}\n\n// ============================================================================\n// Follow-up Message Hook\n// ============================================================================\n\n/**\n * Send a follow-up message as if the user asked it\n */\nexport function useSendMessage(): (prompt: string) => Promise<void> {\n return useCallback(async (prompt: string) => {\n await window.openai?.sendFollowUpMessage?.({ prompt });\n }, []);\n}\n\n// ============================================================================\n// Dynamic Height Hook\n// ============================================================================\n\n/**\n * Get the maximum height available for the widget from the host environment.\n * This is the CORRECT pattern per OpenAI Apps SDK examples.\n * \n * The host (ChatGPT/MCPJam) provides maxHeight, and widgets should constrain\n * themselves to this height. Widgets should NOT report their height back.\n * \n * @returns The maximum height in pixels, or undefined if not available\n */\nexport function useMaxHeight(): number | undefined {\n const [maxHeight, setMaxHeight] = useState<number | undefined>(\n () => window.openai?.maxHeight\n );\n \n useEffect(() => {\n const handler = (event: CustomEvent) => {\n if (event.detail?.globals && 'maxHeight' in event.detail.globals) {\n setMaxHeight(event.detail.globals.maxHeight);\n }\n };\n \n window.addEventListener('openai:set_globals', handler as EventListener);\n return () => window.removeEventListener('openai:set_globals', handler as EventListener);\n }, []);\n \n return maxHeight;\n}\n\n// ============================================================================\n// External Link Hook\n// ============================================================================\n\n/**\n * Open vetted external links in the user's browser\n */\nexport function useOpenExternal(): (href: string) => void {\n return useCallback((href: string) => {\n window.openai?.openExternal?.({ href });\n }, []);\n}\n\n// ============================================================================\n// Utility: Check if running in ChatGPT\n// ============================================================================\n\nexport function isInChatGPT(): boolean {\n return typeof window !== 'undefined' && window.openai !== undefined;\n}\n\n// ============================================================================\n// Re-exports (types are already exported above)\n// ============================================================================\n\n"],"names":["error"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { memo, useMemo, useState, useCallback } from "react";
|
|
3
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
3
4
|
import { useBreakpoint } from "../layout/useBreakpoint.js";
|
|
4
5
|
import { Tooltip } from "../overlays/Tooltip.js";
|
|
5
6
|
import { addAlpha } from "../../utils/index.js";
|
|
6
|
-
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
7
7
|
const STATUS_DOT_COLORS = {
|
|
8
8
|
normal: "#56F000",
|
|
9
9
|
caution: "#FCE83A",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useMemo } from "react";
|
|
3
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
3
4
|
import { AstroIcon } from "../display/AstroIcon.js";
|
|
4
5
|
import { getPropertyConfig, deriveStatus, formatPropertyValue } from "./propertyConfig.js";
|
|
5
|
-
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
6
6
|
const STATUS_BORDER_COLORS = {
|
|
7
7
|
off: "#3c3e42",
|
|
8
8
|
standby: "#285766",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useState, useRef, useEffect } from "react";
|
|
3
|
-
import { isValidIconName, Icon } from "./Icon.js";
|
|
4
3
|
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
4
|
+
import { isValidIconName, Icon } from "./Icon.js";
|
|
5
5
|
const SIZE_MAP = {
|
|
6
6
|
"extra-small": 16,
|
|
7
7
|
"small": 24,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo } from "react";
|
|
3
|
-
import { classNames } from "../../utils/index.js";
|
|
4
3
|
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
4
|
+
import { classNames } from "../../utils/index.js";
|
|
5
5
|
const Badge = memo(function Badge2({
|
|
6
6
|
variant = "filled",
|
|
7
7
|
size = "small",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useMemo } from "react";
|
|
3
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
3
4
|
import { HeaderIconWithStatus } from "./HeaderIconWithStatus.js";
|
|
4
5
|
import { AstroIcon } from "./AstroIcon.js";
|
|
5
6
|
import { Icon } from "./Icon.js";
|
|
6
7
|
import { safeAccentText, getStatusColor } from "../../utils/index.js";
|
|
7
|
-
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
8
8
|
const STATUS_COLORS = {
|
|
9
9
|
off: "#a4abb6",
|
|
10
10
|
standby: "#2dccff",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import React, { memo, useCallback } from "react";
|
|
3
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
3
4
|
import { addAlpha, classNames } from "../../utils/index.js";
|
|
4
5
|
import { AstroIcon } from "./AstroIcon.js";
|
|
5
6
|
import { HeaderIconWithStatus } from "./HeaderIconWithStatus.js";
|
|
6
7
|
import { CardHeader } from "./CardHeader.js";
|
|
7
8
|
import { useBreakpoint } from "../layout/useBreakpoint.js";
|
|
8
|
-
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
9
9
|
const STATUS_COLORS = {
|
|
10
10
|
off: "#a4abb6",
|
|
11
11
|
standby: "#2dccff",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo } from "react";
|
|
3
|
-
import { classNames } from "../../utils/index.js";
|
|
4
3
|
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
4
|
+
import { classNames } from "../../utils/index.js";
|
|
5
5
|
const GLASS_COLOR_OVERLAYS = {
|
|
6
6
|
// Core brand colors
|
|
7
7
|
electric: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo } from "react";
|
|
3
|
-
import { AstroIcon } from "./AstroIcon.js";
|
|
4
3
|
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
4
|
+
import { AstroIcon } from "./AstroIcon.js";
|
|
5
5
|
const STATUS_BORDER_COLORS = {
|
|
6
6
|
off: "#3c3e42",
|
|
7
7
|
standby: "#285766",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
2
|
import { memo, useRef, useId, useCallback, useEffect } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import { classNames } from "../../utils/index.js";
|
|
5
4
|
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
5
|
+
import { classNames } from "../../utils/index.js";
|
|
6
6
|
const DialogActions = memo(function DialogActions2({
|
|
7
7
|
children,
|
|
8
8
|
align = "right"
|
|
@@ -52,27 +52,30 @@ const Dialog = memo(function Dialog2({
|
|
|
52
52
|
onClose();
|
|
53
53
|
}
|
|
54
54
|
}, [closeOnEscape, onClose]);
|
|
55
|
+
const handleKeyDownRef = useRef(handleKeyDown);
|
|
56
|
+
handleKeyDownRef.current = handleKeyDown;
|
|
55
57
|
useEffect(() => {
|
|
56
|
-
if (open)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
58
|
+
if (!open) return void 0;
|
|
59
|
+
previousActiveElement.current = document.activeElement;
|
|
60
|
+
document.body.style.overflow = "hidden";
|
|
61
|
+
const onKeyDown = (e) => handleKeyDownRef.current(e);
|
|
62
|
+
document.addEventListener("keydown", onKeyDown);
|
|
63
|
+
const focusTimer = setTimeout(() => {
|
|
64
|
+
var _a, _b;
|
|
65
|
+
if ((_a = dialogRef.current) == null ? void 0 : _a.contains(document.activeElement)) return;
|
|
66
|
+
const focusable = (_b = dialogRef.current) == null ? void 0 : _b.querySelector(
|
|
67
|
+
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
|
68
|
+
);
|
|
69
|
+
focusable == null ? void 0 : focusable.focus();
|
|
70
|
+
}, 0);
|
|
71
|
+
return () => {
|
|
72
|
+
var _a;
|
|
73
|
+
clearTimeout(focusTimer);
|
|
74
|
+
document.body.style.overflow = "";
|
|
75
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
76
|
+
(_a = previousActiveElement.current) == null ? void 0 : _a.focus();
|
|
77
|
+
};
|
|
78
|
+
}, [open]);
|
|
76
79
|
if (!open) return null;
|
|
77
80
|
if (typeof document === "undefined") return null;
|
|
78
81
|
return createPortal(/* @__PURE__ */ jsxs(
|