@zendir/ui 0.1.9 → 0.1.11

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.
@@ -1,306 +0,0 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
- import { Component, useRef } from "react";
3
- import { useChatGPTTheme, useDisplayMode, useMaxHeight, isInChatGPT } from "./index.js";
4
- import { FONT_FAMILY_PRIMARY } from "../core/Typography.js";
5
- const STATUS_COLORS_INTERNAL = {
6
- off: "#a4abb6",
7
- standby: "#2dccff",
8
- normal: "#56f000",
9
- caution: "#fce83a",
10
- serious: "#ffb302",
11
- critical: "#ff3838"
12
- };
13
- function StatusShape({ status, size = 10 }) {
14
- const color = STATUS_COLORS_INTERNAL[status] ?? STATUS_COLORS_INTERNAL.off;
15
- const glow = `${color}50`;
16
- switch (status) {
17
- case "caution":
18
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("rect", { x: "1", y: "1", width: "10", height: "10", fill: color }) });
19
- case "serious":
20
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("polygon", { points: "6,1 11,6 6,11 1,6", fill: color }) });
21
- case "critical":
22
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("polygon", { points: "6,11 1,2 11,2", fill: color }) });
23
- case "standby":
24
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "3.5", fill: "none", stroke: color, strokeWidth: "2" }) });
25
- case "off":
26
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "3", fill: color }) });
27
- default:
28
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 12 12", width: size, height: size, style: { filter: `drop-shadow(0 0 3px ${glow})` }, "aria-hidden": "true", children: /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "5", fill: color }) });
29
- }
30
- }
31
- const LEGACY_VARIANT_MAP = {
32
- success: "normal",
33
- warning: "caution",
34
- error: "critical",
35
- info: "standby",
36
- neutral: "off"
37
- };
38
- function resolveStatusLevel(input) {
39
- if (["off", "standby", "normal", "caution", "serious", "critical"].includes(input)) {
40
- return input;
41
- }
42
- return LEGACY_VARIANT_MAP[input] ?? "off";
43
- }
44
- class CardErrorBoundary extends Component {
45
- constructor(props) {
46
- super(props);
47
- this.state = { hasError: false };
48
- }
49
- static getDerivedStateFromError(error) {
50
- return { hasError: true, error };
51
- }
52
- render() {
53
- var _a;
54
- if (this.state.hasError) {
55
- return /* @__PURE__ */ jsxs(
56
- "div",
57
- {
58
- style: {
59
- padding: "16px",
60
- borderRadius: "12px",
61
- background: "rgba(255, 56, 56, 0.08)",
62
- border: "1px solid rgba(255, 56, 56, 0.3)",
63
- fontFamily: FONT_FAMILY_PRIMARY,
64
- color: "#ff8a8a"
65
- // WCAG AA: lighter red for readable text on dark error background
66
- },
67
- role: "alert",
68
- children: [
69
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", marginBottom: "8px" }, children: [
70
- /* @__PURE__ */ jsx(StatusShape, { status: "critical", size: 12 }),
71
- /* @__PURE__ */ jsxs("strong", { style: { fontSize: "0.875rem" }, children: [
72
- this.props.componentName ?? "Widget",
73
- " Error"
74
- ] })
75
- ] }),
76
- /* @__PURE__ */ jsx("p", { style: { margin: 0, fontSize: "0.75rem", opacity: 0.8 }, children: ((_a = this.state.error) == null ? void 0 : _a.message) ?? "An unexpected error occurred." })
77
- ]
78
- }
79
- );
80
- }
81
- return this.props.children;
82
- }
83
- }
84
- function LoadingSkeleton({ compact, isDark }) {
85
- const bar = (w, h = 12) => ({
86
- width: w,
87
- height: h,
88
- borderRadius: 4,
89
- background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)",
90
- animation: "chatgpt-card-pulse 1.5s ease-in-out infinite"
91
- });
92
- return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: compact ? 8 : 12 }, "aria-label": "Loading...", role: "status", children: [
93
- /* @__PURE__ */ jsx("div", { style: bar("70%", 14) }),
94
- /* @__PURE__ */ jsx("div", { style: bar("100%") }),
95
- /* @__PURE__ */ jsx("div", { style: bar("85%") }),
96
- /* @__PURE__ */ jsx("div", { style: bar("60%") }),
97
- /* @__PURE__ */ jsx("style", { children: `@keyframes chatgpt-card-pulse { 0%,100% { opacity: 0.4 } 50% { opacity: 1 } }` })
98
- ] });
99
- }
100
- function ErrorDisplay({ error, onRetry, isDark }) {
101
- const message = typeof error === "string" ? error : error.message;
102
- return /* @__PURE__ */ jsxs(
103
- "div",
104
- {
105
- role: "alert",
106
- style: {
107
- display: "flex",
108
- flexDirection: "column",
109
- gap: 8,
110
- padding: "12px",
111
- borderRadius: 8,
112
- background: "rgba(255, 56, 56, 0.08)",
113
- border: "1px solid rgba(255, 56, 56, 0.2)"
114
- },
115
- children: [
116
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
117
- /* @__PURE__ */ jsx(StatusShape, { status: "critical", size: 12 }),
118
- /* @__PURE__ */ jsx("span", { style: { fontSize: "0.75rem", fontWeight: 500, color: "#ff8a8a" }, children: message })
119
- ] }),
120
- onRetry && /* @__PURE__ */ jsx(
121
- "button",
122
- {
123
- onClick: onRetry,
124
- style: {
125
- alignSelf: "flex-start",
126
- padding: "4px 12px",
127
- borderRadius: 4,
128
- border: `1px solid ${isDark ? "rgba(255,255,255,0.15)" : "rgba(0,0,0,0.15)"}`,
129
- background: "transparent",
130
- color: isDark ? "#a1a1aa" : "#636370",
131
- // WCAG AA compliant on light backgrounds
132
- cursor: "pointer",
133
- fontSize: "0.6875rem",
134
- fontFamily: FONT_FAMILY_PRIMARY
135
- },
136
- "aria-label": "Retry loading widget",
137
- children: "Retry"
138
- }
139
- )
140
- ]
141
- }
142
- );
143
- }
144
- const AppCardInner = ({
145
- children,
146
- title,
147
- subtitle,
148
- status,
149
- allowFullscreen = false,
150
- onFullscreenChange,
151
- className = "",
152
- compact = false,
153
- icon,
154
- footer,
155
- actions,
156
- loading = false,
157
- error,
158
- onRetry,
159
- style
160
- }) => {
161
- const containerRef = useRef(null);
162
- const { isDark, colors } = useChatGPTTheme();
163
- const { mode, requestMode } = useDisplayMode();
164
- const maxHeight = useMaxHeight();
165
- const resolvedStatus = status ? resolveStatusLevel(status.level ?? status.variant ?? "off") : void 0;
166
- const statusColor = resolvedStatus ? STATUS_COLORS_INTERNAL[resolvedStatus] : void 0;
167
- const toggleFullscreen = async () => {
168
- const newMode = mode === "fullscreen" ? "inline" : "fullscreen";
169
- await requestMode(newMode);
170
- onFullscreenChange == null ? void 0 : onFullscreenChange(newMode === "fullscreen");
171
- };
172
- return /* @__PURE__ */ jsxs(
173
- "div",
174
- {
175
- ref: containerRef,
176
- className: `chatgpt-card ${className}`,
177
- role: "region",
178
- "aria-label": title ?? "Widget",
179
- style: {
180
- background: colors.surface,
181
- border: `1px solid ${colors.border}`,
182
- borderRadius: "12px",
183
- padding: compact ? "12px" : "16px",
184
- fontFamily: FONT_FAMILY_PRIMARY,
185
- color: colors.text,
186
- transition: "all 0.2s ease",
187
- ...maxHeight ? { maxHeight, overflow: "auto" } : {},
188
- ...style
189
- },
190
- children: [
191
- (title || status || allowFullscreen || actions) && /* @__PURE__ */ jsxs(
192
- "div",
193
- {
194
- style: {
195
- display: "flex",
196
- alignItems: "flex-start",
197
- justifyContent: "space-between",
198
- marginBottom: compact ? "8px" : "12px"
199
- },
200
- children: [
201
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", minWidth: 0 }, children: [
202
- icon && /* @__PURE__ */ jsx("span", { style: { fontSize: compact ? "16px" : "20px", flexShrink: 0 }, children: icon }),
203
- /* @__PURE__ */ jsxs("div", { style: { minWidth: 0 }, children: [
204
- title && /* @__PURE__ */ jsx(
205
- "h3",
206
- {
207
- style: {
208
- margin: 0,
209
- fontSize: compact ? "0.8125rem" : "0.875rem",
210
- fontWeight: 500,
211
- color: colors.text,
212
- whiteSpace: "nowrap",
213
- overflow: "hidden",
214
- textOverflow: "ellipsis"
215
- },
216
- children: title
217
- }
218
- ),
219
- subtitle && /* @__PURE__ */ jsx(
220
- "p",
221
- {
222
- style: {
223
- margin: "2px 0 0 0",
224
- fontSize: "0.6875rem",
225
- color: colors.textMuted,
226
- whiteSpace: "nowrap",
227
- overflow: "hidden",
228
- textOverflow: "ellipsis"
229
- },
230
- children: subtitle
231
- }
232
- )
233
- ] })
234
- ] }),
235
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 }, children: [
236
- actions,
237
- status && resolvedStatus && statusColor && /* @__PURE__ */ jsxs(
238
- "span",
239
- {
240
- role: "status",
241
- "aria-label": `Status: ${status.label} (${resolvedStatus})`,
242
- style: {
243
- display: "inline-flex",
244
- alignItems: "center",
245
- gap: "4px",
246
- fontSize: "0.5625rem",
247
- fontWeight: 500,
248
- letterSpacing: "0.5px",
249
- padding: "2px 8px 2px 4px",
250
- borderRadius: "4px",
251
- background: `${statusColor}18`,
252
- color: statusColor,
253
- whiteSpace: "nowrap"
254
- },
255
- children: [
256
- /* @__PURE__ */ jsx(StatusShape, { status: resolvedStatus, size: 8 }),
257
- status.label
258
- ]
259
- }
260
- ),
261
- allowFullscreen && isInChatGPT() && /* @__PURE__ */ jsx(
262
- "button",
263
- {
264
- onClick: toggleFullscreen,
265
- "aria-label": mode === "fullscreen" ? "Exit fullscreen" : "Enter fullscreen",
266
- style: {
267
- background: "transparent",
268
- border: "none",
269
- cursor: "pointer",
270
- fontSize: "14px",
271
- opacity: 0.6,
272
- padding: "4px",
273
- color: colors.text,
274
- borderRadius: "4px",
275
- lineHeight: 1
276
- },
277
- children: mode === "fullscreen" ? "⤓" : "⤢"
278
- }
279
- )
280
- ] })
281
- ]
282
- }
283
- ),
284
- /* @__PURE__ */ jsx("div", { children: loading ? /* @__PURE__ */ jsx(LoadingSkeleton, { compact, isDark }) : error ? /* @__PURE__ */ jsx(ErrorDisplay, { error, onRetry, isDark }) : children }),
285
- footer && !loading && !error && /* @__PURE__ */ jsx(
286
- "div",
287
- {
288
- style: {
289
- marginTop: compact ? "8px" : "12px",
290
- paddingTop: compact ? "8px" : "12px",
291
- borderTop: `1px solid ${colors.border}`
292
- },
293
- children: footer
294
- }
295
- )
296
- ]
297
- }
298
- );
299
- };
300
- const AppCard = (props) => /* @__PURE__ */ jsx(CardErrorBoundary, { componentName: props.title ?? "AppCard", children: /* @__PURE__ */ jsx(AppCardInner, { ...props }) });
301
- const ChatGPTCard = AppCard;
302
- export {
303
- AppCard,
304
- ChatGPTCard
305
- };
306
- //# sourceMappingURL=AppCard.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AppCard.js","sources":["../../../src/react/chatgpt/AppCard.tsx"],"sourcesContent":["/**\n * AppCard — Universal widget wrapper for AI host environments.\n *\n * Works with: ChatGPT Apps, Anthropic MCP Apps, Google Gemini, any MCP-compatible host.\n *\n * Features:\n * - Theme-aware styling (light/dark via host environment)\n * - Astro UX 6-level status system with dual-coded indicators (color + shape)\n * - Error boundary protection with graceful fallback UI\n * - Loading and error state management\n * - Display mode support (inline/PiP/fullscreen)\n * - Widget state persistence\n * - WCAG 2.1 AA accessibility (ARIA labels, keyboard nav, screen readers)\n *\n * @example\n * ```tsx\n * import { AppCard, useToolOutput } from '@zendir/ui/react';\n *\n * function SatelliteWidget() {\n * const data = useToolOutput<SatelliteHealth>();\n * return (\n * <AppCard\n * title=\"SAT-001\"\n * subtitle=\"Health Monitor\"\n * status={{ level: 'caution', label: 'Battery Low' }}\n * allowFullscreen\n * >\n * <TelemetryCard data={data} />\n * </AppCard>\n * );\n * }\n * ```\n */\n\nimport React, { useRef, ReactNode, Component } from 'react';\nimport { useChatGPTTheme, useMaxHeight, useDisplayMode, isInChatGPT } from './index';\nimport { FONT_FAMILY_PRIMARY } from '../core/Typography';\nimport type { StatusLevel } from '../utils';\n\n// ─── Status shape helper (matches ChatPanel & Astro UX components) ───────────\n\nconst STATUS_COLORS_INTERNAL: Record<StatusLevel, string> = {\n off: '#a4abb6',\n standby: '#2dccff',\n normal: '#56f000',\n caution: '#fce83a',\n serious: '#ffb302',\n critical: '#ff3838',\n};\n\nfunction StatusShape({ status, size = 10 }: { status: StatusLevel; size?: number }) {\n const color = STATUS_COLORS_INTERNAL[status] ?? STATUS_COLORS_INTERNAL.off;\n const glow = `${color}50`;\n switch (status) {\n case 'caution':\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill={color} /></svg>;\n case 'serious':\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><polygon points=\"6,1 11,6 6,11 1,6\" fill={color} /></svg>;\n case 'critical':\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><polygon points=\"6,11 1,2 11,2\" fill={color} /></svg>;\n case 'standby':\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"3.5\" fill=\"none\" stroke={color} strokeWidth=\"2\" /></svg>;\n case 'off':\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"3\" fill={color} /></svg>;\n default: // normal\n return <svg viewBox=\"0 0 12 12\" width={size} height={size} style={{ filter: `drop-shadow(0 0 3px ${glow})` }} aria-hidden=\"true\"><circle cx=\"6\" cy=\"6\" r=\"5\" fill={color} /></svg>;\n }\n}\n\n// ─── Legacy status mapping (for backward compatibility) ──────────────────────\n\n/**\n * Maps legacy variant strings to Astro UX StatusLevel.\n * Supports both new `StatusLevel` and old `success/warning/error/info/neutral` variants.\n */\nconst LEGACY_VARIANT_MAP: Record<string, StatusLevel> = {\n success: 'normal',\n warning: 'caution',\n error: 'critical',\n info: 'standby',\n neutral: 'off',\n};\n\nfunction resolveStatusLevel(input: string): StatusLevel {\n if (['off', 'standby', 'normal', 'caution', 'serious', 'critical'].includes(input)) {\n return input as StatusLevel;\n }\n return LEGACY_VARIANT_MAP[input] ?? 'off';\n}\n\n// ─── Error Boundary ──────────────────────────────────────────────────────────\n\ninterface CardErrorBoundaryState {\n hasError: boolean;\n error?: Error;\n}\n\nclass CardErrorBoundary extends Component<\n { children: ReactNode; componentName?: string },\n CardErrorBoundaryState\n> {\n constructor(props: { children: ReactNode; componentName?: string }) {\n super(props);\n this.state = { hasError: false };\n }\n\n static getDerivedStateFromError(error: Error): CardErrorBoundaryState {\n return { hasError: true, error };\n }\n\n render() {\n if (this.state.hasError) {\n return (\n <div\n style={{\n padding: '16px',\n borderRadius: '12px',\n background: 'rgba(255, 56, 56, 0.08)',\n border: '1px solid rgba(255, 56, 56, 0.3)',\n fontFamily: FONT_FAMILY_PRIMARY,\n color: '#ff8a8a', // WCAG AA: lighter red for readable text on dark error background\n }}\n role=\"alert\"\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>\n <StatusShape status=\"critical\" size={12} />\n <strong style={{ fontSize: '0.875rem' }}>\n {this.props.componentName ?? 'Widget'} Error\n </strong>\n </div>\n <p style={{ margin: 0, fontSize: '0.75rem', opacity: 0.8 }}>\n {this.state.error?.message ?? 'An unexpected error occurred.'}\n </p>\n </div>\n );\n }\n return this.props.children;\n }\n}\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface AppCardProps {\n /** Card content */\n children: ReactNode;\n /** Card title */\n title?: string;\n /** Subtitle or description */\n subtitle?: string;\n /**\n * Status badge with Astro UX dual-coded indicator (color + shape).\n *\n * Supports both the Astro UX `StatusLevel` and legacy variants:\n * - Astro UX: `{ level: 'normal' | 'standby' | 'caution' | 'serious' | 'critical' | 'off', label: '...' }`\n * - Legacy: `{ variant: 'success' | 'warning' | 'error' | 'info' | 'neutral', label: '...' }`\n */\n status?: {\n label: string;\n level?: StatusLevel;\n /** @deprecated Use `level` (Astro UX StatusLevel) instead. Still supported for backward compatibility. */\n variant?: 'success' | 'warning' | 'error' | 'info' | 'neutral';\n };\n /** Show fullscreen button (only rendered inside ChatGPT host) */\n allowFullscreen?: boolean;\n /** Callback when fullscreen is toggled */\n onFullscreenChange?: (isFullscreen: boolean) => void;\n /** Additional CSS classes */\n className?: string;\n /** Enable compact mode (reduced padding and font sizes) */\n compact?: boolean;\n /** Header icon (ReactNode, rendered before title) */\n icon?: ReactNode;\n /** Footer content (rendered below content with a divider) */\n footer?: ReactNode;\n /** Actions (buttons) rendered in the header, right-aligned */\n actions?: ReactNode;\n /** Show a loading skeleton over the content area */\n loading?: boolean;\n /** Error state — renders an error message with a critical alert */\n error?: string | Error;\n /** Retry callback (shown when `error` is set) */\n onRetry?: () => void;\n /** Custom inline styles for the root container */\n style?: React.CSSProperties;\n}\n\n// ─── Loading Skeleton ────────────────────────────────────────────────────────\n\nfunction LoadingSkeleton({ compact, isDark }: { compact: boolean; isDark: boolean }) {\n const bar = (w: string, h = 12) => ({\n width: w,\n height: h,\n borderRadius: 4,\n background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)',\n animation: 'chatgpt-card-pulse 1.5s ease-in-out infinite',\n });\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: compact ? 8 : 12 }} aria-label=\"Loading...\" role=\"status\">\n <div style={bar('70%', 14)} />\n <div style={bar('100%')} />\n <div style={bar('85%')} />\n <div style={bar('60%')} />\n <style>{`@keyframes chatgpt-card-pulse { 0%,100% { opacity: 0.4 } 50% { opacity: 1 } }`}</style>\n </div>\n );\n}\n\n// ─── Error Display ───────────────────────────────────────────────────────────\n\nfunction ErrorDisplay({ error, onRetry, isDark }: { error: string | Error; onRetry?: () => void; isDark: boolean }) {\n const message = typeof error === 'string' ? error : error.message;\n return (\n <div\n role=\"alert\"\n style={{\n display: 'flex',\n flexDirection: 'column',\n gap: 8,\n padding: '12px',\n borderRadius: 8,\n background: 'rgba(255, 56, 56, 0.08)',\n border: '1px solid rgba(255, 56, 56, 0.2)',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>\n <StatusShape status=\"critical\" size={12} />\n <span style={{ fontSize: '0.75rem', fontWeight: 500, color: '#ff8a8a' }}>{message}</span>\n </div>\n {onRetry && (\n <button\n onClick={onRetry}\n style={{\n alignSelf: 'flex-start',\n padding: '4px 12px',\n borderRadius: 4,\n border: `1px solid ${isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.15)'}`,\n background: 'transparent',\n color: isDark ? '#a1a1aa' : '#636370', // WCAG AA compliant on light backgrounds\n cursor: 'pointer',\n fontSize: '0.6875rem',\n fontFamily: FONT_FAMILY_PRIMARY,\n }}\n aria-label=\"Retry loading widget\"\n >\n Retry\n </button>\n )}\n </div>\n );\n}\n\n// ─── Main Component ──────────────────────────────────────────────────────────\n\nconst AppCardInner: React.FC<AppCardProps> = ({\n children,\n title,\n subtitle,\n status,\n allowFullscreen = false,\n onFullscreenChange,\n className = '',\n compact = false,\n icon,\n footer,\n actions,\n loading = false,\n error,\n onRetry,\n style,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const { isDark, colors } = useChatGPTTheme();\n const { mode, requestMode } = useDisplayMode();\n const maxHeight = useMaxHeight();\n\n // Resolve status level (supports both Astro UX and legacy variants)\n const resolvedStatus = status\n ? resolveStatusLevel(status.level ?? status.variant ?? 'off')\n : undefined;\n const statusColor = resolvedStatus ? STATUS_COLORS_INTERNAL[resolvedStatus] : undefined;\n\n // Handle fullscreen toggle\n const toggleFullscreen = async () => {\n const newMode = mode === 'fullscreen' ? 'inline' : 'fullscreen';\n await requestMode(newMode);\n onFullscreenChange?.(newMode === 'fullscreen');\n };\n\n return (\n <div\n ref={containerRef}\n className={`chatgpt-card ${className}`}\n role=\"region\"\n aria-label={title ?? 'Widget'}\n style={{\n background: colors.surface,\n border: `1px solid ${colors.border}`,\n borderRadius: '12px',\n padding: compact ? '12px' : '16px',\n fontFamily: FONT_FAMILY_PRIMARY,\n color: colors.text,\n transition: 'all 0.2s ease',\n ...(maxHeight ? { maxHeight, overflow: 'auto' } : {}),\n ...style,\n }}\n >\n {/* Header */}\n {(title || status || allowFullscreen || actions) && (\n <div\n style={{\n display: 'flex',\n alignItems: 'flex-start',\n justifyContent: 'space-between',\n marginBottom: compact ? '8px' : '12px',\n }}\n >\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', minWidth: 0 }}>\n {icon && <span style={{ fontSize: compact ? '16px' : '20px', flexShrink: 0 }}>{icon}</span>}\n <div style={{ minWidth: 0 }}>\n {title && (\n <h3\n style={{\n margin: 0,\n fontSize: compact ? '0.8125rem' : '0.875rem',\n fontWeight: 500,\n color: colors.text,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {title}\n </h3>\n )}\n {subtitle && (\n <p\n style={{\n margin: '2px 0 0 0',\n fontSize: '0.6875rem',\n color: colors.textMuted,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n }}\n >\n {subtitle}\n </p>\n )}\n </div>\n </div>\n\n <div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexShrink: 0 }}>\n {actions}\n\n {status && resolvedStatus && statusColor && (\n <span\n role=\"status\"\n aria-label={`Status: ${status.label} (${resolvedStatus})`}\n style={{\n display: 'inline-flex',\n alignItems: 'center',\n gap: '4px',\n fontSize: '0.5625rem',\n fontWeight: 500,\n letterSpacing: '0.5px',\n padding: '2px 8px 2px 4px',\n borderRadius: '4px',\n background: `${statusColor}18`,\n color: statusColor,\n whiteSpace: 'nowrap',\n }}\n >\n <StatusShape status={resolvedStatus} size={8} />\n {status.label}\n </span>\n )}\n\n {allowFullscreen && isInChatGPT() && (\n <button\n onClick={toggleFullscreen}\n aria-label={mode === 'fullscreen' ? 'Exit fullscreen' : 'Enter fullscreen'}\n style={{\n background: 'transparent',\n border: 'none',\n cursor: 'pointer',\n fontSize: '14px',\n opacity: 0.6,\n padding: '4px',\n color: colors.text,\n borderRadius: '4px',\n lineHeight: 1,\n }}\n >\n {mode === 'fullscreen' ? '⤓' : '⤢'}\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Content: loading → error → children */}\n <div>\n {loading ? (\n <LoadingSkeleton compact={compact} isDark={isDark} />\n ) : error ? (\n <ErrorDisplay error={error} onRetry={onRetry} isDark={isDark} />\n ) : (\n children\n )}\n </div>\n\n {/* Footer */}\n {footer && !loading && !error && (\n <div\n style={{\n marginTop: compact ? '8px' : '12px',\n paddingTop: compact ? '8px' : '12px',\n borderTop: `1px solid ${colors.border}`,\n }}\n >\n {footer}\n </div>\n )}\n </div>\n );\n};\n\n/**\n * AppCard — enterprise-ready widget wrapper for AI host environments.\n *\n * Works with ChatGPT Apps, Anthropic MCP Apps, Google Gemini, or any MCP host.\n * Wrapped in an error boundary so rendering failures in child components never\n * crash the host application — critical for sandboxed iframes.\n */\nexport const AppCard: React.FC<AppCardProps> = (props) => (\n <CardErrorBoundary componentName={props.title ?? 'AppCard'}>\n <AppCardInner {...props} />\n </CardErrorBoundary>\n);\n\n/** @deprecated Use `AppCard` instead. Alias kept for backward compatibility. */\nexport const ChatGPTCard = AppCard;\n/** @deprecated Use `AppCardProps` instead. */\nexport type ChatGPTCardProps = AppCardProps;\n\nexport default AppCard;\n"],"names":[],"mappings":";;;;AAyCA,MAAM,yBAAsD;AAAA,EAC1D,KAAK;AAAA,EACL,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AAAA,EACT,UAAU;AACZ;AAEA,SAAS,YAAY,EAAE,QAAQ,OAAO,MAA8C;AAClF,QAAM,QAAQ,uBAAuB,MAAM,KAAK,uBAAuB;AACvE,QAAM,OAAO,GAAG,KAAK;AACrB,UAAQ,QAAA;AAAA,IACN,KAAK;AACH,aAAO,oBAAC,OAAA,EAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,QAAA,EAAK,GAAE,KAAI,GAAE,KAAI,OAAM,MAAK,QAAO,MAAK,MAAM,OAAO,GAAE;AAAA,IAC3L,KAAK;AACH,aAAO,oBAAC,SAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,aAAQ,QAAO,qBAAoB,MAAM,MAAA,CAAO,EAAA,CAAE;AAAA,IACtL,KAAK;AACH,aAAO,oBAAC,SAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,aAAQ,QAAO,iBAAgB,MAAM,MAAA,CAAO,EAAA,CAAE;AAAA,IAClL,KAAK;AACH,aAAO,oBAAC,OAAA,EAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,UAAA,EAAO,IAAG,KAAI,IAAG,KAAI,GAAE,OAAM,MAAK,QAAO,QAAQ,OAAO,aAAY,KAAI,GAAE;AAAA,IAC9M,KAAK;AACH,aAAO,oBAAC,OAAA,EAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,UAAA,EAAO,IAAG,KAAI,IAAG,KAAI,GAAE,KAAI,MAAM,MAAA,CAAO,GAAE;AAAA,IAC9K;AACE,aAAO,oBAAC,OAAA,EAAI,SAAQ,aAAY,OAAO,MAAM,QAAQ,MAAM,OAAO,EAAE,QAAQ,uBAAuB,IAAI,IAAA,GAAO,eAAY,QAAO,UAAA,oBAAC,UAAA,EAAO,IAAG,KAAI,IAAG,KAAI,GAAE,KAAI,MAAM,MAAA,CAAO,GAAE;AAAA,EAAA;AAElL;AAQA,MAAM,qBAAkD;AAAA,EACtD,SAAS;AAAA,EACT,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,SAAS;AACX;AAEA,SAAS,mBAAmB,OAA4B;AACtD,MAAI,CAAC,OAAO,WAAW,UAAU,WAAW,WAAW,UAAU,EAAE,SAAS,KAAK,GAAG;AAClF,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,KAAK,KAAK;AACtC;AASA,MAAM,0BAA0B,UAG9B;AAAA,EACA,YAAY,OAAwD;AAClE,UAAM,KAAK;AACX,SAAK,QAAQ,EAAE,UAAU,MAAA;AAAA,EAC3B;AAAA,EAEA,OAAO,yBAAyB,OAAsC;AACpE,WAAO,EAAE,UAAU,MAAM,MAAA;AAAA,EAC3B;AAAA,EAEA,SAAS;;AACP,QAAI,KAAK,MAAM,UAAU;AACvB,aACE;AAAA,QAAC;AAAA,QAAA;AAAA,UACC,OAAO;AAAA,YACL,SAAS;AAAA,YACT,cAAc;AAAA,YACd,YAAY;AAAA,YACZ,QAAQ;AAAA,YACR,YAAY;AAAA,YACZ,OAAO;AAAA;AAAA,UAAA;AAAA,UAET,MAAK;AAAA,UAEL,UAAA;AAAA,YAAA,qBAAC,OAAA,EAAI,OAAO,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,OAAO,cAAc,MAAA,GAC7E,UAAA;AAAA,cAAA,oBAAC,aAAA,EAAY,QAAO,YAAW,MAAM,IAAI;AAAA,mCACxC,UAAA,EAAO,OAAO,EAAE,UAAU,cACxB,UAAA;AAAA,gBAAA,KAAK,MAAM,iBAAiB;AAAA,gBAAS;AAAA,cAAA,EAAA,CACxC;AAAA,YAAA,GACF;AAAA,YACA,oBAAC,KAAA,EAAE,OAAO,EAAE,QAAQ,GAAG,UAAU,WAAW,SAAS,OAClD,YAAA,UAAK,MAAM,UAAX,mBAAkB,YAAW,gCAAA,CAChC;AAAA,UAAA;AAAA,QAAA;AAAA,MAAA;AAAA,IAGN;AACA,WAAO,KAAK,MAAM;AAAA,EACpB;AACF;AAkDA,SAAS,gBAAgB,EAAE,SAAS,UAAiD;AACnF,QAAM,MAAM,CAAC,GAAW,IAAI,QAAQ;AAAA,IAClC,OAAO;AAAA,IACP,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,YAAY,SAAS,2BAA2B;AAAA,IAChD,WAAW;AAAA,EAAA;AAGb,8BACG,OAAA,EAAI,OAAO,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,UAAU,IAAI,MAAM,cAAW,cAAa,MAAK,UAC5G,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,OAAO,IAAI,OAAO,EAAE,GAAG;AAAA,IAC5B,oBAAC,OAAA,EAAI,OAAO,IAAI,MAAM,EAAA,CAAG;AAAA,IACzB,oBAAC,OAAA,EAAI,OAAO,IAAI,KAAK,EAAA,CAAG;AAAA,IACxB,oBAAC,OAAA,EAAI,OAAO,IAAI,KAAK,EAAA,CAAG;AAAA,IACxB,oBAAC,WAAO,UAAA,gFAAA,CAAgF;AAAA,EAAA,GAC1F;AAEJ;AAIA,SAAS,aAAa,EAAE,OAAO,SAAS,UAA4E;AAClH,QAAM,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM;AAC1D,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,OAAO;AAAA,QACL,SAAS;AAAA,QACT,eAAe;AAAA,QACf,KAAK;AAAA,QACL,SAAS;AAAA,QACT,cAAc;AAAA,QACd,YAAY;AAAA,QACZ,QAAQ;AAAA,MAAA;AAAA,MAGV,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,OAAO,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,EAAA,GACxD,UAAA;AAAA,UAAA,oBAAC,aAAA,EAAY,QAAO,YAAW,MAAM,IAAI;AAAA,UACzC,oBAAC,QAAA,EAAK,OAAO,EAAE,UAAU,WAAW,YAAY,KAAK,OAAO,aAAc,UAAA,QAAA,CAAQ;AAAA,QAAA,GACpF;AAAA,QACC,WACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,SAAS;AAAA,YACT,OAAO;AAAA,cACL,WAAW;AAAA,cACX,SAAS;AAAA,cACT,cAAc;AAAA,cACd,QAAQ,aAAa,SAAS,2BAA2B,kBAAkB;AAAA,cAC3E,YAAY;AAAA,cACZ,OAAO,SAAS,YAAY;AAAA;AAAA,cAC5B,QAAQ;AAAA,cACR,UAAU;AAAA,cACV,YAAY;AAAA,YAAA;AAAA,YAEd,cAAW;AAAA,YACZ,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MAED;AAAA,IAAA;AAAA,EAAA;AAIR;AAIA,MAAM,eAAuC,CAAC;AAAA,EAC5C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AAAA,EAClB;AAAA,EACA,YAAY;AAAA,EACZ,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AACF,MAAM;AACJ,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,EAAE,QAAQ,OAAA,IAAW,gBAAA;AAC3B,QAAM,EAAE,MAAM,YAAA,IAAgB,eAAA;AAC9B,QAAM,YAAY,aAAA;AAGlB,QAAM,iBAAiB,SACnB,mBAAmB,OAAO,SAAS,OAAO,WAAW,KAAK,IAC1D;AACJ,QAAM,cAAc,iBAAiB,uBAAuB,cAAc,IAAI;AAG9E,QAAM,mBAAmB,YAAY;AACnC,UAAM,UAAU,SAAS,eAAe,WAAW;AACnD,UAAM,YAAY,OAAO;AACzB,6DAAqB,YAAY;AAAA,EACnC;AAEA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,gBAAgB,SAAS;AAAA,MACpC,MAAK;AAAA,MACL,cAAY,SAAS;AAAA,MACrB,OAAO;AAAA,QACL,YAAY,OAAO;AAAA,QACnB,QAAQ,aAAa,OAAO,MAAM;AAAA,QAClC,cAAc;AAAA,QACd,SAAS,UAAU,SAAS;AAAA,QAC5B,YAAY;AAAA,QACZ,OAAO,OAAO;AAAA,QACd,YAAY;AAAA,QACZ,GAAI,YAAY,EAAE,WAAW,UAAU,OAAA,IAAW,CAAA;AAAA,QAClD,GAAG;AAAA,MAAA;AAAA,MAIH,UAAA;AAAA,SAAA,SAAS,UAAU,mBAAmB,YACtC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO;AAAA,cACL,SAAS;AAAA,cACT,YAAY;AAAA,cACZ,gBAAgB;AAAA,cAChB,cAAc,UAAU,QAAQ;AAAA,YAAA;AAAA,YAGlC,UAAA;AAAA,cAAA,qBAAC,OAAA,EAAI,OAAO,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,OAAO,UAAU,EAAA,GACxE,UAAA;AAAA,gBAAA,QAAQ,oBAAC,QAAA,EAAK,OAAO,EAAE,UAAU,UAAU,SAAS,QAAQ,YAAY,EAAA,GAAM,UAAA,KAAA,CAAK;AAAA,qCACnF,OAAA,EAAI,OAAO,EAAE,UAAU,KACrB,UAAA;AAAA,kBAAA,SACC;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,OAAO;AAAA,wBACL,QAAQ;AAAA,wBACR,UAAU,UAAU,cAAc;AAAA,wBAClC,YAAY;AAAA,wBACZ,OAAO,OAAO;AAAA,wBACd,YAAY;AAAA,wBACZ,UAAU;AAAA,wBACV,cAAc;AAAA,sBAAA;AAAA,sBAGf,UAAA;AAAA,oBAAA;AAAA,kBAAA;AAAA,kBAGJ,YACC;AAAA,oBAAC;AAAA,oBAAA;AAAA,sBACC,OAAO;AAAA,wBACL,QAAQ;AAAA,wBACR,UAAU;AAAA,wBACV,OAAO,OAAO;AAAA,wBACd,YAAY;AAAA,wBACZ,UAAU;AAAA,wBACV,cAAc;AAAA,sBAAA;AAAA,sBAGf,UAAA;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBACH,EAAA,CAEJ;AAAA,cAAA,GACF;AAAA,cAEA,qBAAC,OAAA,EAAI,OAAO,EAAE,SAAS,QAAQ,YAAY,UAAU,KAAK,OAAO,YAAY,EAAA,GAC1E,UAAA;AAAA,gBAAA;AAAA,gBAEA,UAAU,kBAAkB,eAC3B;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,MAAK;AAAA,oBACL,cAAY,WAAW,OAAO,KAAK,KAAK,cAAc;AAAA,oBACtD,OAAO;AAAA,sBACL,SAAS;AAAA,sBACT,YAAY;AAAA,sBACZ,KAAK;AAAA,sBACL,UAAU;AAAA,sBACV,YAAY;AAAA,sBACZ,eAAe;AAAA,sBACf,SAAS;AAAA,sBACT,cAAc;AAAA,sBACd,YAAY,GAAG,WAAW;AAAA,sBAC1B,OAAO;AAAA,sBACP,YAAY;AAAA,oBAAA;AAAA,oBAGd,UAAA;AAAA,sBAAA,oBAAC,aAAA,EAAY,QAAQ,gBAAgB,MAAM,GAAG;AAAA,sBAC7C,OAAO;AAAA,oBAAA;AAAA,kBAAA;AAAA,gBAAA;AAAA,gBAIX,mBAAmB,iBAClB;AAAA,kBAAC;AAAA,kBAAA;AAAA,oBACC,SAAS;AAAA,oBACT,cAAY,SAAS,eAAe,oBAAoB;AAAA,oBACxD,OAAO;AAAA,sBACL,YAAY;AAAA,sBACZ,QAAQ;AAAA,sBACR,QAAQ;AAAA,sBACR,UAAU;AAAA,sBACV,SAAS;AAAA,sBACT,SAAS;AAAA,sBACT,OAAO,OAAO;AAAA,sBACd,cAAc;AAAA,sBACd,YAAY;AAAA,oBAAA;AAAA,oBAGb,UAAA,SAAS,eAAe,MAAM;AAAA,kBAAA;AAAA,gBAAA;AAAA,cACjC,EAAA,CAEJ;AAAA,YAAA;AAAA,UAAA;AAAA,QAAA;AAAA,QAKJ,oBAAC,OAAA,EACE,UAAA,UACC,oBAAC,mBAAgB,SAAkB,OAAA,CAAgB,IACjD,4BACD,cAAA,EAAa,OAAc,SAAkB,OAAA,CAAgB,IAE9D,UAEJ;AAAA,QAGC,UAAU,CAAC,WAAW,CAAC,SACtB;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,OAAO;AAAA,cACL,WAAW,UAAU,QAAQ;AAAA,cAC7B,YAAY,UAAU,QAAQ;AAAA,cAC9B,WAAW,aAAa,OAAO,MAAM;AAAA,YAAA;AAAA,YAGtC,UAAA;AAAA,UAAA;AAAA,QAAA;AAAA,MACH;AAAA,IAAA;AAAA,EAAA;AAIR;AASO,MAAM,UAAkC,CAAC,UAC9C,oBAAC,mBAAA,EAAkB,eAAe,MAAM,SAAS,WAC/C,UAAA,oBAAC,cAAA,EAAc,GAAG,OAAO,EAAA,CAC3B;AAIK,MAAM,cAAc;"}
@@ -1,166 +0,0 @@
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 useIntrinsicHeight(_ref) {
122
- }
123
- function useMaxHeight() {
124
- const [maxHeight, setMaxHeight] = useState(
125
- () => {
126
- var _a;
127
- return (_a = window.openai) == null ? void 0 : _a.maxHeight;
128
- }
129
- );
130
- useEffect(() => {
131
- const handler = (event) => {
132
- var _a;
133
- if (((_a = event.detail) == null ? void 0 : _a.globals) && "maxHeight" in event.detail.globals) {
134
- setMaxHeight(event.detail.globals.maxHeight);
135
- }
136
- };
137
- window.addEventListener("openai:set_globals", handler);
138
- return () => window.removeEventListener("openai:set_globals", handler);
139
- }, []);
140
- return maxHeight;
141
- }
142
- function useOpenExternal() {
143
- return useCallback((href) => {
144
- var _a, _b;
145
- (_b = (_a = window.openai) == null ? void 0 : _a.openExternal) == null ? void 0 : _b.call(_a, { href });
146
- }, []);
147
- }
148
- function isInChatGPT() {
149
- return typeof window !== "undefined" && window.openai !== void 0;
150
- }
151
- export {
152
- isInChatGPT,
153
- useCallTool,
154
- useChatGPTTheme,
155
- useDisplayMode,
156
- useIntrinsicHeight,
157
- useLocale,
158
- useMaxHeight,
159
- useOpenAiGlobal,
160
- useOpenExternal,
161
- useSendMessage,
162
- useToolInput,
163
- useToolOutput,
164
- useWidgetState
165
- };
166
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
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 * DEPRECATED: Do not use notifyIntrinsicHeight!\n * \n * According to OpenAI Apps SDK examples (via DeepWiki), the correct pattern is:\n * - The HOST provides maxHeight via window.openai.maxHeight\n * - Widgets READ this and constrain themselves\n * - Widgets NEVER report height back to the host\n * \n * Calling notifyIntrinsicHeight causes infinite resize loops in MCPJam sandbox\n * because the sandbox responds by resizing the container, which triggers another\n * resize event.\n * \n * Use useMaxHeight() instead to get the host's height constraint.\n * \n * @deprecated Use useMaxHeight() instead\n */\nexport function useIntrinsicHeight(_ref: { current: HTMLElement | null }): void {\n // NO-OP: Do not call notifyIntrinsicHeight - it causes infinite loops\n // The host provides maxHeight, widgets should not report height back\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":";AAsEA,MAAM,yBAAyB;AASxB,SAAS,gBAA+C,KAA0B;AACvF,QAAM,cAAc,YAAY,MAAM;;AACpC,YAAO,YAAO,WAAP,mBAAgB;AAAA,EACzB,GAAG,CAAC,GAAG,CAAC;AAER,QAAM,YAAY,YAAY,CAAC,aAAyB;AACtD,UAAM,kBAAkB,CAAC,UAAiB;;AACxC,YAAM,cAAc;AACpB,YAAI,uBAAY,WAAZ,mBAAoB,YAApB,mBAA8B,UAAS,QAAW;AACpD,iBAAA;AAAA,MACF;AAAA,IACF;AAEA,WAAO,iBAAiB,wBAAwB,iBAAiB,EAAE,SAAS,MAAM;AAClF,WAAO,MAAM,OAAO,oBAAoB,wBAAwB,eAAe;AAAA,EACjF,GAAG,CAAC,GAAG,CAAC;AAER,SAAO,qBAAqB,WAAW,aAAa,WAAW;AACjE;AAWO,SAAS,eACd,cACkD;AAClD,QAAM,wBAAwB,gBAAgB,aAAa;AAE3D,QAAM,CAAC,aAAa,eAAe,IAAI,SAAY,MAAM;AACvD,QAAI,yBAAyB,MAAM;AACjC,aAAO;AAAA,IACT;AACA,WAAO,OAAO,iBAAiB,aAAc,aAAA,IAA6B;AAAA,EAC5E,CAAC;AAED,YAAU,MAAM;AACd,QAAI,yBAAyB,MAAM;AACjC,sBAAgB,qBAAqB;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,qBAAqB,CAAC;AAE1B,QAAM,iBAAiB,YAAY,CAAC,UAA6B;AAC/D,oBAAgB,CAAC,cAAiB;;AAChC,YAAM,WAAW,OAAO,UAAU,aAAc,MAAsB,SAAS,IAAI;AACnF,yBAAO,WAAP,mBAAe,mBAAf,4BAAgC;AAChC,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAA,CAAE;AAEL,SAAO,CAAC,aAAa,cAAc;AACrC;AASO,SAAS,gBAA4D;AAC1E,SAAO,gBAAgB,YAAY;AACrC;AAKO,SAAS,eAA2D;AACzE,SAAO,gBAAgB,WAAW;AACpC;AAYO,SAAS,kBAYd;AACA,QAAM,QAAQ,gBAAgB,OAAO,KAAK;AAC1C,QAAM,SAAS,UAAU;AAEzB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,SAAS;AAAA,MACf,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,cAAc;AAAA,MACd,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA,MACX,QAAQ;AAAA,IAAA,IACN;AAAA,MACF,YAAY;AAAA,MACZ,SAAS;AAAA,MACT,cAAc;AAAA,MACd,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,WAAW;AAAA;AAAA,MACX,QAAQ;AAAA,IAAA;AAAA,EACV;AAEJ;AASO,SAAS,YAAoB;AAClC,SAAO,gBAAgB,QAAQ,KAAK;AACtC;AASO,SAAS,iBAKd;AACA,QAAM,OAAO,gBAAgB,aAAa,KAAK;AAC/C,QAAM,YAAY,gBAAgB,WAAW;AAE7C,QAAM,cAAc,YAAY,OAAO,YAA6C;;AAClF,YAAM,kBAAO,WAAP,mBAAe,uBAAf,4BAAoC,EAAE,MAAM;EACpD,GAAG,CAAA,CAAE;AAEL,QAAM,QAAQ,YAAY,MAAM;;AAC9B,uBAAO,WAAP,mBAAe,iBAAf;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO,EAAE,MAAM,WAAW,aAAa,MAAA;AACzC;AASO,SAAS,cAId;AACA,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,KAAK;AAChD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAErD,QAAM,WAAW,YAAY,OAC3B,MACA,SACe;;AACf,QAAI,GAAC,YAAO,WAAP,mBAAe,WAAU;AAC5B,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAEA,iBAAa,IAAI;AACjB,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,SAAS,MAAM,OAAO,OAAO,SAAS,MAAM,IAAI;AACtD,aAAO;AAAA,IACT,SAAS,KAAK;AACZ,YAAMA,SAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,eAASA,MAAK;AACd,YAAMA;AAAAA,IACR,UAAA;AACE,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO,EAAE,UAAU,WAAW,MAAA;AAChC;AASO,SAAS,iBAAoD;AAClE,SAAO,YAAY,OAAO,WAAmB;;AAC3C,YAAM,kBAAO,WAAP,mBAAe,wBAAf,4BAAqC,EAAE;EAC/C,GAAG,CAAA,CAAE;AACP;AAsBO,SAAS,mBAAmB,MAA6C;AAGhF;AAWO,SAAS,eAAmC;AACjD,QAAM,CAAC,WAAW,YAAY,IAAI;AAAA,IAChC,MAAA;;AAAM,0BAAO,WAAP,mBAAe;AAAA;AAAA,EAAA;AAGvB,YAAU,MAAM;AACd,UAAM,UAAU,CAAC,UAAuB;;AACtC,YAAI,WAAM,WAAN,mBAAc,YAAW,eAAe,MAAM,OAAO,SAAS;AAChE,qBAAa,MAAM,OAAO,QAAQ,SAAS;AAAA,MAC7C;AAAA,IACF;AAEA,WAAO,iBAAiB,sBAAsB,OAAwB;AACtE,WAAO,MAAM,OAAO,oBAAoB,sBAAsB,OAAwB;AAAA,EACxF,GAAG,CAAA,CAAE;AAEL,SAAO;AACT;AASO,SAAS,kBAA0C;AACxD,SAAO,YAAY,CAAC,SAAiB;;AACnC,uBAAO,WAAP,mBAAe,iBAAf,4BAA8B,EAAE,KAAA;AAAA,EAClC,GAAG,CAAA,CAAE;AACP;AAMO,SAAS,cAAuB;AACrC,SAAO,OAAO,WAAW,eAAe,OAAO,WAAW;AAC5D;"}
@@ -1,89 +0,0 @@
1
- import { useState, useRef, useCallback, useEffect } from "react";
2
- const DEFAULT_MAX_HISTORY = 500;
3
- function useSpacecraftPosition(options) {
4
- const {
5
- client,
6
- interval = 1e3,
7
- enabled = true,
8
- trackHistory = true,
9
- maxHistory = DEFAULT_MAX_HISTORY
10
- } = options;
11
- const [spacecraft, setSpacecraft] = useState([]);
12
- const [groundTracks, setGroundTracks] = useState(/* @__PURE__ */ new Map());
13
- const [isLoading, setIsLoading] = useState(false);
14
- const [error, setError] = useState(null);
15
- const [lastUpdate, setLastUpdate] = useState(null);
16
- const intervalRef = useRef(null);
17
- const startTimeRef = useRef(Date.now());
18
- const fetchPositions = useCallback(async () => {
19
- setIsLoading(true);
20
- setError(null);
21
- try {
22
- const positions = await client.fetchSpacecraftPositions();
23
- setSpacecraft(positions);
24
- setLastUpdate(Date.now());
25
- if (trackHistory) {
26
- const elapsed = (Date.now() - startTimeRef.current) / 1e3;
27
- setGroundTracks((prev) => {
28
- const newTracks = new Map(prev);
29
- for (const sc of positions) {
30
- if (!sc.id) continue;
31
- const point = {
32
- lat: sc.latitude,
33
- lon: sc.longitude,
34
- t: elapsed
35
- };
36
- const existing = newTracks.get(sc.id) || [];
37
- const updated = [...existing, point];
38
- if (updated.length > maxHistory) {
39
- newTracks.set(sc.id, updated.slice(-maxHistory));
40
- } else {
41
- newTracks.set(sc.id, updated);
42
- }
43
- }
44
- return newTracks;
45
- });
46
- }
47
- } catch (err) {
48
- const message = err instanceof Error ? err.message : "Failed to fetch positions";
49
- setError(message);
50
- } finally {
51
- setIsLoading(false);
52
- }
53
- }, [client, trackHistory, maxHistory]);
54
- useEffect(() => {
55
- if (!enabled) {
56
- if (intervalRef.current) {
57
- clearInterval(intervalRef.current);
58
- intervalRef.current = null;
59
- }
60
- return;
61
- }
62
- startTimeRef.current = Date.now();
63
- fetchPositions();
64
- intervalRef.current = setInterval(fetchPositions, interval);
65
- return () => {
66
- if (intervalRef.current) {
67
- clearInterval(intervalRef.current);
68
- intervalRef.current = null;
69
- }
70
- };
71
- }, [enabled, interval, fetchPositions]);
72
- const getSpacecraft = useCallback(
73
- (id) => spacecraft.find((sc) => sc.id === id),
74
- [spacecraft]
75
- );
76
- return {
77
- spacecraft,
78
- groundTracks,
79
- isLoading,
80
- error,
81
- lastUpdate,
82
- getSpacecraft,
83
- refresh: fetchPositions
84
- };
85
- }
86
- export {
87
- useSpacecraftPosition
88
- };
89
- //# sourceMappingURL=useSpacecraftPosition.js.map