@zendir/ui 0.2.21 → 0.3.1
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 +183 -1
- package/README.md +70 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +73 -64
- package/dist/index.js.map +1 -1
- package/dist/react/3d/CesiumCaptureSource.d.ts +1 -1
- package/dist/react/3d/CesiumCaptureSource.js +1 -1
- package/dist/react/3d/CesiumCaptureSource.js.map +1 -1
- package/dist/react/3d/ZenSpace3D.js +1254 -0
- package/dist/react/3d/ZenSpace3D.js.map +1 -0
- package/dist/react/3d/ZenSpace3DCesium.js +579 -0
- package/dist/react/3d/ZenSpace3DCesium.js.map +1 -0
- package/dist/react/3d/ZenSpace3DShaders.js +478 -0
- package/dist/react/3d/ZenSpace3DShaders.js.map +1 -0
- package/dist/react/3d/ZenSpace3DTypes.d.ts +28 -1
- package/dist/react/3d/ZenSpace3DUtils.d.ts +17 -173
- package/dist/react/3d/ZenSpace3DUtils.js +20 -1
- package/dist/react/3d/ZenSpace3DUtils.js.map +1 -1
- package/dist/react/3d/capturePngAnalysis.js +42 -0
- package/dist/react/3d/capturePngAnalysis.js.map +1 -0
- package/dist/react/3d/index.d.ts +6 -12
- package/dist/react/3d/index.js +57 -0
- package/dist/react/3d/index.js.map +1 -0
- package/dist/react/3d/threeLoader.js +22 -0
- package/dist/react/3d/threeLoader.js.map +1 -0
- 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 +2 -2
- package/dist/react/astro/MonitoringIcon.js.map +1 -1
- package/dist/react/astro/Notification.js +1 -1
- package/dist/react/astro/Progress.js +1 -1
- package/dist/react/astro/SimulationControls.js +3 -3
- package/dist/react/astro/SimulationControls.js.map +1 -1
- package/dist/react/astro/StatusIndicator.js +1 -1
- package/dist/react/astro/UnifiedTimeline.js +5 -5
- package/dist/react/astro/UnifiedTimeline.js.map +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.d.ts +2 -15
- package/dist/react/charts/GroundTrackMap.js +1 -1
- package/dist/react/charts/GroundTrackMap.js.map +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 +35 -14
- package/dist/react/charts/unified/AstroChart.js.map +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/AppCard.d.ts +0 -4
- package/dist/react/chatgpt/index.d.ts +0 -19
- package/dist/react/chatgpt/index.js +163 -0
- package/dist/react/chatgpt/index.js.map +1 -0
- package/dist/react/context/SpatialSelectionContext.d.ts +40 -0
- package/dist/react/context/SpatialSelectionContext.js +10 -0
- package/dist/react/context/SpatialSelectionContext.js.map +1 -0
- package/dist/react/context/index.d.ts +2 -0
- package/dist/react/core/{DataTable.d.ts → data/DataTable.d.ts} +1 -1
- package/dist/react/core/{DataTable.js → data/DataTable.js} +4 -4
- package/dist/react/core/data/DataTable.js.map +1 -0
- package/dist/react/core/{DataValue.d.ts → data/DataValue.d.ts} +2 -2
- package/dist/react/core/{DataValue.js → data/DataValue.js} +2 -2
- package/dist/react/core/data/DataValue.js.map +1 -0
- package/dist/react/core/{propertyConfig.d.ts → data/propertyConfig.d.ts} +2 -2
- package/dist/react/core/data/propertyConfig.js.map +1 -0
- package/dist/react/core/{AstroIcon.js → display/AstroIcon.js} +1 -1
- package/dist/react/core/display/AstroIcon.js.map +1 -0
- package/dist/react/core/{Badge.d.ts → display/Badge.d.ts} +1 -1
- package/dist/react/core/{Badge.js → display/Badge.js} +2 -2
- package/dist/react/core/display/Badge.js.map +1 -0
- package/dist/react/core/{CardHeader.d.ts → display/CardHeader.d.ts} +1 -1
- package/dist/react/core/{CardHeader.js → display/CardHeader.js} +2 -2
- package/dist/react/core/display/CardHeader.js.map +1 -0
- package/dist/react/core/{Container.d.ts → display/Container.d.ts} +1 -1
- package/dist/react/core/{Container.js → display/Container.js} +3 -3
- package/dist/react/core/display/Container.js.map +1 -0
- package/dist/react/core/{CopyButton.js → display/CopyButton.js} +1 -1
- package/dist/react/core/display/CopyButton.js.map +1 -0
- package/dist/react/core/{GlassCard.d.ts → display/GlassCard.d.ts} +1 -1
- package/dist/react/core/{GlassCard.js → display/GlassCard.js} +2 -2
- package/dist/react/core/display/GlassCard.js.map +1 -0
- package/dist/react/core/{HeaderIconWithStatus.d.ts → display/HeaderIconWithStatus.d.ts} +1 -1
- package/dist/react/core/{HeaderIconWithStatus.js → display/HeaderIconWithStatus.js} +1 -1
- package/dist/react/core/display/HeaderIconWithStatus.js.map +1 -0
- package/dist/react/core/{Icon.d.ts → display/Icon.d.ts} +1 -1
- package/dist/react/core/{Icon.js → display/Icon.js} +1 -1
- package/dist/react/core/display/Icon.js.map +1 -0
- package/dist/react/core/{Typography.d.ts → display/Typography.d.ts} +13 -4
- package/dist/react/core/{Typography.js → display/Typography.js} +1 -1
- package/dist/react/core/display/Typography.js.map +1 -0
- package/dist/react/core/{ConfirmDialog.js → feedback/ConfirmDialog.js} +1 -1
- package/dist/react/core/feedback/ConfirmDialog.js.map +1 -0
- package/dist/react/core/{Dialog.js → feedback/Dialog.js} +2 -2
- package/dist/react/core/feedback/Dialog.js.map +1 -0
- package/dist/react/core/{Toast.js → feedback/Toast.js} +3 -3
- package/dist/react/core/feedback/Toast.js.map +1 -0
- package/dist/react/core/index.d.ts +85 -85
- package/dist/react/core/index.js +161 -0
- package/dist/react/core/index.js.map +1 -0
- package/dist/react/core/{Button.js → inputs/Button.js} +2 -2
- package/dist/react/core/inputs/Button.js.map +1 -0
- package/dist/react/core/{Checkbox.js → inputs/Checkbox.js} +2 -2
- package/dist/react/core/inputs/Checkbox.js.map +1 -0
- package/dist/react/core/{Input.d.ts → inputs/Input.d.ts} +1 -1
- package/dist/react/core/{Input.js → inputs/Input.js} +3 -3
- package/dist/react/core/inputs/Input.js.map +1 -0
- package/dist/react/core/{LimitsBar.js → inputs/LimitsBar.js} +1 -1
- package/dist/react/core/inputs/LimitsBar.js.map +1 -0
- package/dist/react/core/{NumberInput.d.ts → inputs/NumberInput.d.ts} +2 -2
- package/dist/react/core/{NumberInput.js → inputs/NumberInput.js} +3 -3
- package/dist/react/core/inputs/NumberInput.js.map +1 -0
- package/dist/react/core/{PinInput.js → inputs/PinInput.js} +2 -2
- package/dist/react/core/inputs/PinInput.js.map +1 -0
- package/dist/react/core/{Select.js → inputs/Select.js} +3 -3
- package/dist/react/core/inputs/Select.js.map +1 -0
- package/dist/react/core/{Toggle.js → inputs/Toggle.js} +2 -2
- package/dist/react/core/inputs/Toggle.js.map +1 -0
- 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/{AppBar.d.ts → navigation/AppBar.d.ts} +1 -1
- package/dist/react/core/{AppBar.js → navigation/AppBar.js} +7 -7
- package/dist/react/core/navigation/AppBar.js.map +1 -0
- package/dist/react/core/{Pagination.js → navigation/Pagination.js} +2 -2
- package/dist/react/core/navigation/Pagination.js.map +1 -0
- package/dist/react/core/{SideNav.d.ts → navigation/SideNav.d.ts} +1 -1
- package/dist/react/core/{SideNav.js → navigation/SideNav.js} +3 -3
- package/dist/react/core/navigation/SideNav.js.map +1 -0
- package/dist/react/core/{Tabs.js → navigation/Tabs.js} +2 -2
- package/dist/react/core/navigation/Tabs.js.map +1 -0
- package/dist/react/core/{Popover.js → overlays/Popover.js} +1 -1
- package/dist/react/core/overlays/Popover.js.map +1 -0
- package/dist/react/core/{SidePanel.js → overlays/SidePanel.js} +3 -3
- package/dist/react/core/overlays/SidePanel.js.map +1 -0
- package/dist/react/core/{Tooltip.js → overlays/Tooltip.js} +2 -2
- package/dist/react/core/overlays/Tooltip.js.map +1 -0
- package/dist/react/core/{ActivityPlanner.js → widgets/ActivityPlanner.js} +1 -1
- package/dist/react/core/widgets/ActivityPlanner.js.map +1 -0
- package/dist/react/core/{Capture.js → widgets/Capture.js} +3 -3
- package/dist/react/core/widgets/Capture.js.map +1 -0
- package/dist/react/core/{ChatPanel.d.ts → widgets/ChatPanel.d.ts} +1 -1
- package/dist/react/core/{ChatPanel.js → widgets/ChatPanel.js} +2 -2
- package/dist/react/core/widgets/ChatPanel.js.map +1 -0
- package/dist/react/core/{ColorPickerPanel.d.ts → widgets/ColorPickerPanel.d.ts} +1 -1
- package/dist/react/core/{ColorPickerPanel.js → widgets/ColorPickerPanel.js} +3 -3
- package/dist/react/core/widgets/ColorPickerPanel.js.map +1 -0
- package/dist/react/core/{CommandBuilder.js → widgets/CommandBuilder.js} +1 -1
- package/dist/react/core/widgets/CommandBuilder.js.map +1 -0
- package/dist/react/core/{ConnectionForm.d.ts → widgets/ConnectionForm.d.ts} +1 -1
- package/dist/react/core/{ConnectionForm.js → widgets/ConnectionForm.js} +2 -2
- package/dist/react/core/widgets/ConnectionForm.js.map +1 -0
- package/dist/react/core/{FileExplorer.js → widgets/FileExplorer.js} +2 -2
- package/dist/react/core/widgets/FileExplorer.js.map +1 -0
- package/dist/react/core/{HexViewer.js → widgets/HexViewer.js} +1 -1
- package/dist/react/core/widgets/HexViewer.js.map +1 -0
- package/dist/react/core/{ImageGallery.d.ts → widgets/ImageGallery.d.ts} +1 -1
- package/dist/react/core/{ImageGallery.js → widgets/ImageGallery.js} +3 -3
- package/dist/react/core/widgets/ImageGallery.js.map +1 -0
- package/dist/react/core/{LogViewer.d.ts → widgets/LogViewer.d.ts} +13 -3
- package/dist/react/core/{LogViewer.js → widgets/LogViewer.js} +28 -8
- package/dist/react/core/widgets/LogViewer.js.map +1 -0
- package/dist/react/core/{MessageStream.d.ts → widgets/MessageStream.d.ts} +2 -2
- package/dist/react/core/{MessageStream.js → widgets/MessageStream.js} +4 -4
- package/dist/react/core/widgets/MessageStream.js.map +1 -0
- package/dist/react/core/{MissionCalendar.js → widgets/MissionCalendar.js} +2 -2
- package/dist/react/core/widgets/MissionCalendar.js.map +1 -0
- package/dist/react/core/{PacketViewer.js → widgets/PacketViewer.js} +1 -1
- package/dist/react/core/widgets/PacketViewer.js.map +1 -0
- package/dist/react/core/widgets/capture-placeholder.png.js.map +1 -0
- package/dist/react/hooks/index.d.ts +9 -11
- package/dist/react/hooks/useAccessWindows.d.ts +15 -19
- package/dist/react/hooks/useCompactMode.js +1 -1
- package/dist/react/hooks/useGroundTrackHistory.d.ts +34 -0
- package/dist/react/hooks/useSimulationScene.d.ts +141 -0
- package/dist/react/hooks/useSimulationScene.js +401 -0
- package/dist/react/hooks/useSimulationScene.js.map +1 -0
- package/dist/react/hooks/useZendirSession.d.ts +44 -69
- package/dist/react/index.d.ts +7 -3
- package/dist/react/panels/LayerControlPanel.d.ts +54 -0
- package/dist/react/panels/LayerControlPanel.js +184 -0
- package/dist/react/panels/LayerControlPanel.js.map +1 -0
- package/dist/react/panels/ObjectInventoryPanel.d.ts +57 -0
- package/dist/react/panels/ObjectInventoryPanel.js +261 -0
- package/dist/react/panels/ObjectInventoryPanel.js.map +1 -0
- package/dist/react/panels/index.d.ts +15 -0
- package/dist/react/panels/index.js +8 -0
- package/dist/react/panels/index.js.map +1 -0
- package/dist/react/theme/ThemeProvider.d.ts +2 -0
- package/dist/react/theme/ThemeProvider.js +50 -72
- package/dist/react/theme/ThemeProvider.js.map +1 -1
- 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/types.d.ts +32 -3
- package/dist/react/types.js.map +1 -1
- package/dist/react/utils/index.js +8 -0
- package/dist/react/utils/index.js.map +1 -1
- package/dist/react.js +73 -64
- package/dist/react.js.map +1 -1
- package/dist/shaders/atmosphere.frag.js +5 -0
- package/dist/shaders/atmosphere.frag.js.map +1 -0
- package/dist/shaders/atmosphere.vert.js +5 -0
- package/dist/shaders/atmosphere.vert.js.map +1 -0
- package/dist/shaders/stars.frag.js +5 -0
- package/dist/shaders/stars.frag.js.map +1 -0
- package/dist/shaders/stars.vert.js +5 -0
- package/dist/shaders/stars.vert.js.map +1 -0
- package/dist/style.css +6 -4
- package/dist/tokens/css-vars.d.ts +91 -0
- package/dist/tokens/css-vars.js +228 -0
- package/dist/tokens/css-vars.js.map +1 -0
- package/dist/tokens/index.d.ts +71 -18
- package/dist/tokens/index.js +206 -97
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/tokens.css +50 -50
- package/package.json +70 -58
- package/sdk-stub.js +10 -5
- package/dist/react/3d/EarthViewer.d.ts +0 -46
- package/dist/react/3d/SolarSystemViewer.d.ts +0 -43
- package/dist/react/chatgpt/ChatGPTCard.d.ts +0 -6
- package/dist/react/core/ActivityPlanner.js.map +0 -1
- package/dist/react/core/AppBar.js.map +0 -1
- package/dist/react/core/AstroIcon.js.map +0 -1
- package/dist/react/core/Badge.js.map +0 -1
- package/dist/react/core/Button.js.map +0 -1
- package/dist/react/core/Capture.js.map +0 -1
- package/dist/react/core/CardHeader.js.map +0 -1
- package/dist/react/core/ChatPanel.js.map +0 -1
- package/dist/react/core/Checkbox.js.map +0 -1
- package/dist/react/core/ColorPickerPanel.js.map +0 -1
- package/dist/react/core/CommandBuilder.js.map +0 -1
- package/dist/react/core/ConfirmDialog.js.map +0 -1
- package/dist/react/core/ConnectionForm.js.map +0 -1
- package/dist/react/core/Container.js.map +0 -1
- package/dist/react/core/CopyButton.js.map +0 -1
- package/dist/react/core/DataTable.js.map +0 -1
- package/dist/react/core/DataValue.js.map +0 -1
- package/dist/react/core/Dialog.js.map +0 -1
- package/dist/react/core/FileExplorer.js.map +0 -1
- package/dist/react/core/GlassCard.js.map +0 -1
- package/dist/react/core/HeaderIconWithStatus.js.map +0 -1
- package/dist/react/core/HexViewer.js.map +0 -1
- package/dist/react/core/Icon.js.map +0 -1
- package/dist/react/core/ImageGallery.js.map +0 -1
- package/dist/react/core/Input.js.map +0 -1
- package/dist/react/core/LimitsBar.js.map +0 -1
- package/dist/react/core/LogViewer.js.map +0 -1
- package/dist/react/core/MessageStream.js.map +0 -1
- package/dist/react/core/MissionCalendar.js.map +0 -1
- package/dist/react/core/NumberInput.js.map +0 -1
- package/dist/react/core/PacketViewer.js.map +0 -1
- package/dist/react/core/Pagination.js.map +0 -1
- package/dist/react/core/PinInput.js.map +0 -1
- package/dist/react/core/Popover.js.map +0 -1
- package/dist/react/core/Select.js.map +0 -1
- package/dist/react/core/SideNav.js.map +0 -1
- package/dist/react/core/SidePanel.js.map +0 -1
- package/dist/react/core/Tabs.js.map +0 -1
- package/dist/react/core/Toast.js.map +0 -1
- package/dist/react/core/Toggle.js.map +0 -1
- package/dist/react/core/Tooltip.js.map +0 -1
- package/dist/react/core/Typography.js.map +0 -1
- package/dist/react/core/capture-placeholder.png.js.map +0 -1
- package/dist/react/core/propertyConfig.js.map +0 -1
- package/dist/react/hooks/useSimulationTime.d.ts +0 -61
- package/dist/react/hooks/useSpacecraftPosition.d.ts +0 -50
- package/dist/react/hooks/useTelemetry.d.ts +0 -55
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- /package/dist/react/core/{propertyConfig.js → data/propertyConfig.js} +0 -0
- /package/dist/react/core/{AstroIcon.d.ts → display/AstroIcon.d.ts} +0 -0
- /package/dist/react/core/{CopyButton.d.ts → display/CopyButton.d.ts} +0 -0
- /package/dist/react/core/{ConfirmDialog.d.ts → feedback/ConfirmDialog.d.ts} +0 -0
- /package/dist/react/core/{Dialog.d.ts → feedback/Dialog.d.ts} +0 -0
- /package/dist/react/core/{Toast.d.ts → feedback/Toast.d.ts} +0 -0
- /package/dist/react/core/{Button.d.ts → inputs/Button.d.ts} +0 -0
- /package/dist/react/core/{Checkbox.d.ts → inputs/Checkbox.d.ts} +0 -0
- /package/dist/react/core/{LimitsBar.d.ts → inputs/LimitsBar.d.ts} +0 -0
- /package/dist/react/core/{PinInput.d.ts → inputs/PinInput.d.ts} +0 -0
- /package/dist/react/core/{Select.d.ts → inputs/Select.d.ts} +0 -0
- /package/dist/react/core/{Toggle.d.ts → inputs/Toggle.d.ts} +0 -0
- /package/dist/react/core/{Pagination.d.ts → navigation/Pagination.d.ts} +0 -0
- /package/dist/react/core/{Tabs.d.ts → navigation/Tabs.d.ts} +0 -0
- /package/dist/react/core/{Popover.d.ts → overlays/Popover.d.ts} +0 -0
- /package/dist/react/core/{SidePanel.d.ts → overlays/SidePanel.d.ts} +0 -0
- /package/dist/react/core/{Tooltip.d.ts → overlays/Tooltip.d.ts} +0 -0
- /package/dist/react/core/{ActivityPlanner.d.ts → widgets/ActivityPlanner.d.ts} +0 -0
- /package/dist/react/core/{Capture.d.ts → widgets/Capture.d.ts} +0 -0
- /package/dist/react/core/{CommandBuilder.d.ts → widgets/CommandBuilder.d.ts} +0 -0
- /package/dist/react/core/{FileExplorer.d.ts → widgets/FileExplorer.d.ts} +0 -0
- /package/dist/react/core/{HexViewer.d.ts → widgets/HexViewer.d.ts} +0 -0
- /package/dist/react/core/{MissionCalendar.d.ts → widgets/MissionCalendar.d.ts} +0 -0
- /package/dist/react/core/{PacketViewer.d.ts → widgets/PacketViewer.d.ts} +0 -0
- /package/dist/react/core/{capture-placeholder.png.js → widgets/capture-placeholder.png.js} +0 -0
|
@@ -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":";AAmDO,SAAS,eACd,UACA,UAAyB,IACP;AAClB,QAAM;AAAA,IACJ,WAAW,aAAa;AAAA,IACxB,YAAY;AAAA,IACZ,aAAa;AAAA;AAAA,IACb,aAAa;AAAA,IACb,WAAW;AAAA,IACX,oBAAoB;AAAA,EAAA,IAClB;AAEJ,QAAM,YAAY,OAAqD,oBAAI,KAAK;AAChF,QAAM,eAAe,OAAe,CAAC;AACrC,QAAM,gBAAgB,OAAe,CAAC;AACtC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAS,KAAK;AAC9C,QAAM,oBAAoB,OAAA;AAG1B,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAM,QAAQ,SAAS;AACvB,QAAI,CAAC,SAAS,SAAU;AAExB,UAAM,MAAM,KAAK,IAAA;AACjB,QAAI,MAAM,aAAa,UAAU,YAAY;AAE3C,wBAAkB,UAAU,sBAAsB,KAAK;AACvD;AAAA,IACF;AAEA,cAAU,QAAQ,QAAQ,CAAC,QAAQ,gBAAgB;AACjD,UAAI,OAAO,WAAW,EAAG;AAGzB,UAAI,kBAAkB;AACtB,UAAI,YAAY,cAAc,UAAU,mBAAmB;AACzD,0BAAkB,aAAa,QAAQ,KAAK,KAAK,OAAO,SAAS,CAAC,CAAC;AAAA,MACrE;AAGA,sBAAgB,QAAQ,CAAA,UAAS;AAC/B,cAAM,WAAW,aAAa,KAAK;AAAA,MACrC,CAAC;AAED,oBAAc,WAAW,gBAAgB;AAAA,IAC3C,CAAC;AAGD,cAAU,QAAQ,MAAA;AAClB,iBAAa,UAAU;AAAA,EACzB,GAAG,CAAC,UAAU,UAAU,YAAY,UAAU,iBAAiB,CAAC;AAGhE,QAAM,OAAO,YAAY,CAAC,aAAqB,UAAuC;AACpF,QAAI,SAAU;AAEd,QAAI,SAAS,UAAU,QAAQ,IAAI,WAAW;AAC9C,QAAI,CAAC,QAAQ;AACX,eAAS,CAAA;AACT,gBAAU,QAAQ,IAAI,aAAa,MAAM;AAAA,IAC3C;AAEA,WAAO,KAAK,KAAK;AAGjB,QAAI,OAAO,UAAU,YAAY;AAC/B,YAAA;AAAA,IACF,WAAW,CAAC,kBAAkB,SAAS;AAErC,wBAAkB,UAAU,sBAAsB,MAAM;AACtD,0BAAkB,UAAU;AAC5B,cAAA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,UAAU,YAAY,KAAK,CAAC;AAGhC,QAAM,YAAY,YAAY,CAAC,aAAqB,WAA4C;AAC9F,QAAI,YAAY,OAAO,WAAW,EAAG;AAErC,QAAI,SAAS,UAAU,QAAQ,IAAI,WAAW;AAC9C,QAAI,CAAC,QAAQ;AACX,eAAS,CAAA;AACT,gBAAU,QAAQ,IAAI,aAAa,MAAM;AAAA,IAC3C;AAEA,WAAO,KAAK,GAAG,MAAM;AACrB,UAAA;AAAA,EACF,GAAG,CAAC,UAAU,KAAK,CAAC;AAGpB,QAAM,QAAQ,YAAY,MAAM;;AAC9B,cAAU,QAAQ,MAAA;AAClB,kBAAc,UAAU;AACxB,mBAAS,YAAT,mBAAkB;AAAA,EACpB,GAAG,CAAC,QAAQ,CAAC;AAGb,QAAM,QAAQ,YAAY,MAAM,YAAY,IAAI,GAAG,CAAA,CAAE;AACrD,QAAM,SAAS,YAAY,MAAM,YAAY,KAAK,GAAG,CAAA,CAAE;AAGvD,YAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,kBAAkB,SAAS;AAC7B,6BAAqB,kBAAkB,OAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,YAAY,cAAc;AAAA,EAAA;AAE9B;AAUA,SAAS,aACP,MACA,WACK;AACL,MAAI,KAAK,UAAU,UAAW,QAAO;AAErC,QAAM,SAAc,CAAA;AACpB,QAAM,cAAc,KAAK,SAAS,MAAM,YAAY;AAGpD,SAAO,KAAK,KAAK,CAAC,CAAC;AAEnB,MAAI,IAAI;AAER,WAAS,IAAI,GAAG,IAAI,YAAY,GAAG,KAAK;AAEtC,UAAM,cAAc,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;AACvD,UAAM,YAAY,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;AACrD,UAAM,eAAe,KAAK,IAAI,WAAW,KAAK,MAAM,IAAI;AAGxD,QAAI,OAAO;AACX,QAAI,OAAO;AACX,UAAM,kBAAkB,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;AAC3D,UAAM,gBAAgB,KAAK,IAAI,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI,GAAG,KAAK,MAAM;AAChF,UAAM,mBAAmB,gBAAgB;AAEzC,aAAS,IAAI,iBAAiB,IAAI,eAAe,KAAK;AACpD,YAAM,QAAQ,KAAK,CAAC;AACpB,YAAM,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK;AAC/B,cAAQ;AACR,cAAQ;AAAA,IACV;AACA,YAAQ,oBAAoB;AAC5B,YAAQ,oBAAoB;AAG5B,UAAM,CAAC,IAAI,EAAE,IAAI,WAAW,KAAK,CAAC,CAAC;AACnC,QAAI,UAAU;AACd,QAAI,SAAS;AAEb,aAAS,IAAI,aAAa,IAAI,cAAc,cAAc,KAAK;AAC7D,YAAM,QAAQ,KAAK,CAAC;AACpB,YAAM,CAAC,GAAG,CAAC,IAAI,WAAW,KAAK;AAG/B,YAAM,OAAO,KAAK;AAAA,SACf,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,OAAO;AAAA,MAAA;AAG9C,UAAI,OAAO,SAAS;AAClB,kBAAU;AACV,iBAAS;AAAA,MACX;AAAA,IACF;AAEA,WAAO,KAAK,KAAK,MAAM,CAAC;AACxB,QAAI;AAAA,EACN;AAGA,SAAO,KAAK,KAAK,KAAK,SAAS,CAAC,CAAC;AAEjC,SAAO;AACT;AAEA,SAAS,WAAW,OAAsD;AACxE,MAAI,UAAU,OAAO;AACnB,WAAO;AAAA,MACL,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO,IAAI,KAAK,MAAM,IAAI,EAAE,QAAA;AAAA,MACnE,MAAM;AAAA,IAAA;AAAA,EAEV;AACA,SAAO;AAAA,IACL,OAAO,MAAM,MAAM,WAAW,MAAM,IAAI;AAAA,IACxC,MAAM;AAAA,EAAA;AAEV;AA4BO,SAAS,mBACd,UACA,SAC2B;AAC3B,QAAM;AAAA,IACJ;AAAA,IACA,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,gBAAgB;AAAA,IAChB;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AAEJ,QAAM,QAAQ,OAAyB,IAAI;AAC3C,QAAM,oBAAoB,OAAO,CAAC;AAClC,QAAM,oBAAoB,OAAA;AAC1B,QAAM,CAAC,iBAAiB,kBAAkB,IAAI,SAAuD,cAAc;AAEnH,QAAM,SAAS,eAAe,UAAU,aAAa;AAErD,QAAM,UAAU,YAAY,MAAM;;AAChC,UAAI,WAAM,YAAN,mBAAe,gBAAe,UAAU,KAAM;AAElD,uBAAmB,YAAY;AAC/B,UAAM,KAAK,IAAI,UAAU,GAAG;AAE5B,OAAG,SAAS,MAAM;AAChB,yBAAmB,WAAW;AAC9B,wBAAkB,UAAU;AAAA,IAC9B;AAEA,OAAG,YAAY,CAAC,UAAU;AACxB,UAAI;AACF,cAAM,OAAO,KAAK,MAAM,MAAM,IAAI;AAClC,cAAM,SAAS,eACX,aAAa,IAAI,IACjB,oBAAoB,IAAI;AAE5B,YAAI,QAAQ;AACV,iBAAO,KAAK,OAAO,aAAa,OAAO,KAAK;AAAA,QAC9C;AAAA,MACF,SAAS,GAAG;AAAA,MAIZ;AAAA,IACF;AAEA,OAAG,UAAU,MAAM;AACjB,yBAAmB,OAAO;AAAA,IAC5B;AAEA,OAAG,UAAU,MAAM;AACjB,yBAAmB,cAAc;AAEjC,UAAI,aAAa,kBAAkB,UAAU,eAAe;AAC1D,0BAAkB;AAClB,0BAAkB,UAAU,WAAW,SAAS,iBAAiB,kBAAkB,OAAO;AAAA,MAC5F;AAAA,IACF;AAEA,UAAM,UAAU;AAAA,EAClB,GAAG,CAAC,KAAK,WAAW,gBAAgB,eAAe,cAAc,MAAM,CAAC;AAExE,QAAM,aAAa,YAAY,MAAM;;AACnC,iBAAa,kBAAkB,OAAO;AACtC,gBAAM,YAAN,mBAAe;AACf,UAAM,UAAU;AAAA,EAClB,GAAG,CAAA,CAAE;AAGL,YAAU,MAAM;AACd,YAAA;AACA,WAAO;AAAA,EACT,GAAG,CAAC,SAAS,UAAU,CAAC;AAExB,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;AAEA,SAAS,oBAAoB,MAAuE;AAClG,MAAI,OAAO,SAAS,YAAY,SAAS,KAAM,QAAO;AAEtD,QAAM,MAAM;AAEZ,MAAI,UAAU,OAAO,WAAW,KAAK;AACnC,WAAO;AAAA,MACL,aAAa,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;AAAA,MACrE,OAAO;AAAA,QACL,MAAM,IAAI;AAAA,QACV,OAAO,IAAI;AAAA,MAAA;AAAA,IACb;AAAA,EAEJ;AAEA,SAAO;AACT;"}
|
|
@@ -52,8 +52,4 @@ export interface AppCardProps {
|
|
|
52
52
|
* crash the host application — critical for sandboxed iframes.
|
|
53
53
|
*/
|
|
54
54
|
export declare const AppCard: React.FC<AppCardProps>;
|
|
55
|
-
/** @deprecated Use `AppCard` instead. Alias kept for backward compatibility. */
|
|
56
|
-
export declare const ChatGPTCard: React.FC<AppCardProps>;
|
|
57
|
-
/** @deprecated Use `AppCardProps` instead. */
|
|
58
|
-
export type ChatGPTCardProps = AppCardProps;
|
|
59
55
|
export default AppCard;
|
|
@@ -130,25 +130,6 @@ export declare function useCallTool(): {
|
|
|
130
130
|
* Send a follow-up message as if the user asked it
|
|
131
131
|
*/
|
|
132
132
|
export declare function useSendMessage(): (prompt: string) => Promise<void>;
|
|
133
|
-
/**
|
|
134
|
-
* DEPRECATED: Do not use notifyIntrinsicHeight!
|
|
135
|
-
*
|
|
136
|
-
* According to OpenAI Apps SDK examples (via DeepWiki), the correct pattern is:
|
|
137
|
-
* - The HOST provides maxHeight via window.openai.maxHeight
|
|
138
|
-
* - Widgets READ this and constrain themselves
|
|
139
|
-
* - Widgets NEVER report height back to the host
|
|
140
|
-
*
|
|
141
|
-
* Calling notifyIntrinsicHeight causes infinite resize loops in MCPJam sandbox
|
|
142
|
-
* because the sandbox responds by resizing the container, which triggers another
|
|
143
|
-
* resize event.
|
|
144
|
-
*
|
|
145
|
-
* Use useMaxHeight() instead to get the host's height constraint.
|
|
146
|
-
*
|
|
147
|
-
* @deprecated Use useMaxHeight() instead
|
|
148
|
-
*/
|
|
149
|
-
export declare function useIntrinsicHeight(_ref: {
|
|
150
|
-
current: HTMLElement | null;
|
|
151
|
-
}): void;
|
|
152
133
|
/**
|
|
153
134
|
* Get the maximum height available for the widget from the host environment.
|
|
154
135
|
* This is the CORRECT pattern per OpenAI Apps SDK examples.
|
|
@@ -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":";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;AAeO,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;"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SpatialSelectionContextValue {
|
|
4
|
+
/** ID of the currently selected object, or null if nothing is selected. */
|
|
5
|
+
selectedId: string | null;
|
|
6
|
+
/** Set the selection (or clear with `null`). */
|
|
7
|
+
setSelectedId: (id: string | null) => void;
|
|
8
|
+
/** Toggle selection: select the id if it isn't selected, clear otherwise. */
|
|
9
|
+
toggleSelection: (id: string) => void;
|
|
10
|
+
/** Convenience: clear any current selection. */
|
|
11
|
+
clearSelection: () => void;
|
|
12
|
+
}
|
|
13
|
+
export interface SpatialSelectionProviderProps {
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** Initial selected id. Default null (nothing selected). */
|
|
16
|
+
defaultSelectedId?: string | null;
|
|
17
|
+
/** Controlled mode: when set, the provider mirrors this value instead
|
|
18
|
+
* of owning state. Pair with `onSelectionChange`. */
|
|
19
|
+
selectedId?: string | null;
|
|
20
|
+
/** Called whenever the selection changes (uncontrolled or controlled). */
|
|
21
|
+
onSelectionChange?: (id: string | null) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function SpatialSelectionProvider({ children, defaultSelectedId, selectedId: controlledId, onSelectionChange, }: SpatialSelectionProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* Read the spatial-selection context. Returns `null` when used outside
|
|
26
|
+
* a `<SpatialSelectionProvider>` — components MUST handle that case so
|
|
27
|
+
* they stay standalone-usable. Treat the provider as a progressive
|
|
28
|
+
* enhancement, never a hard dependency.
|
|
29
|
+
*
|
|
30
|
+
* const ctx = useSpatialSelection();
|
|
31
|
+
* const selectedId = ctx?.selectedId ?? null; // safe outside provider
|
|
32
|
+
* const onSelect = ctx?.setSelectedId ?? noop; // safe outside provider
|
|
33
|
+
*/
|
|
34
|
+
export declare function useSpatialSelection(): SpatialSelectionContextValue | null;
|
|
35
|
+
/**
|
|
36
|
+
* Strict variant — throws if used outside a provider. Useful in the
|
|
37
|
+
* provider's own children when the dependency is intentional.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useSpatialSelectionStrict(): SpatialSelectionContextValue;
|
|
40
|
+
export default SpatialSelectionProvider;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "react/jsx-runtime";
|
|
2
|
+
import { useContext, createContext } from "react";
|
|
3
|
+
const SpatialSelectionContextInternal = createContext(null);
|
|
4
|
+
function useSpatialSelection() {
|
|
5
|
+
return useContext(SpatialSelectionContextInternal);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
useSpatialSelection
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=SpatialSelectionContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SpatialSelectionContext.js","sources":["../../../src/react/context/SpatialSelectionContext.tsx"],"sourcesContent":["/**\n * @zendir/ui - SpatialSelectionContext\n *\n * Optional shared selection state for spatial dashboards. Wrap a tree\n * containing `<ZenSpace3D />`, `<GroundTrackMap />`,\n * `<ObjectInventoryPanel />`, etc. — they cross-talk via the context\n * and stay synchronised without prop-drilling:\n *\n * <SpatialSelectionProvider>\n * <ObjectInventoryPanel objects={scene.objects} /> // writes selection\n * <ZenSpace3D satellites={...} /> // reads selection\n * <GroundTrackMap satellites={tracks} /> // reads selection\n * </SpatialSelectionProvider>\n *\n * The provider is **opt-in**. Components that consume the context use\n * `useSpatialSelection()` which returns `null` outside a provider —\n * that's how every component stays standalone-usable. No coupling.\n */\n\nimport { createContext, useCallback, useContext, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface SpatialSelectionContextValue {\n /** ID of the currently selected object, or null if nothing is selected. */\n selectedId: string | null;\n /** Set the selection (or clear with `null`). */\n setSelectedId: (id: string | null) => void;\n /** Toggle selection: select the id if it isn't selected, clear otherwise. */\n toggleSelection: (id: string) => void;\n /** Convenience: clear any current selection. */\n clearSelection: () => void;\n}\n\nconst SpatialSelectionContextInternal = createContext<SpatialSelectionContextValue | null>(null);\n\n// ─── Provider ─────────────────────────────────────────────────────────────────\n\nexport interface SpatialSelectionProviderProps {\n children: ReactNode;\n /** Initial selected id. Default null (nothing selected). */\n defaultSelectedId?: string | null;\n /** Controlled mode: when set, the provider mirrors this value instead\n * of owning state. Pair with `onSelectionChange`. */\n selectedId?: string | null;\n /** Called whenever the selection changes (uncontrolled or controlled). */\n onSelectionChange?: (id: string | null) => void;\n}\n\nexport function SpatialSelectionProvider({\n children,\n defaultSelectedId = null,\n selectedId: controlledId,\n onSelectionChange,\n}: SpatialSelectionProviderProps) {\n const [internalId, setInternalId] = useState<string | null>(defaultSelectedId);\n const isControlled = controlledId !== undefined;\n const selectedId = isControlled ? controlledId : internalId;\n\n const setSelectedId = useCallback(\n (id: string | null) => {\n if (!isControlled) setInternalId(id);\n onSelectionChange?.(id);\n },\n [isControlled, onSelectionChange],\n );\n\n const toggleSelection = useCallback(\n (id: string) => {\n const next = selectedId === id ? null : id;\n if (!isControlled) setInternalId(next);\n onSelectionChange?.(next);\n },\n [selectedId, isControlled, onSelectionChange],\n );\n\n const clearSelection = useCallback(() => {\n if (!isControlled) setInternalId(null);\n onSelectionChange?.(null);\n }, [isControlled, onSelectionChange]);\n\n const value = useMemo<SpatialSelectionContextValue>(\n () => ({ selectedId, setSelectedId, toggleSelection, clearSelection }),\n [selectedId, setSelectedId, toggleSelection, clearSelection],\n );\n\n return (\n <SpatialSelectionContextInternal.Provider value={value}>\n {children}\n </SpatialSelectionContextInternal.Provider>\n );\n}\n\n// ─── Hooks ────────────────────────────────────────────────────────────────────\n\n/**\n * Read the spatial-selection context. Returns `null` when used outside\n * a `<SpatialSelectionProvider>` — components MUST handle that case so\n * they stay standalone-usable. Treat the provider as a progressive\n * enhancement, never a hard dependency.\n *\n * const ctx = useSpatialSelection();\n * const selectedId = ctx?.selectedId ?? null; // safe outside provider\n * const onSelect = ctx?.setSelectedId ?? noop; // safe outside provider\n */\nexport function useSpatialSelection(): SpatialSelectionContextValue | null {\n return useContext(SpatialSelectionContextInternal);\n}\n\n/**\n * Strict variant — throws if used outside a provider. Useful in the\n * provider's own children when the dependency is intentional.\n */\nexport function useSpatialSelectionStrict(): SpatialSelectionContextValue {\n const ctx = useContext(SpatialSelectionContextInternal);\n if (!ctx) {\n throw new Error(\n 'useSpatialSelectionStrict() called outside <SpatialSelectionProvider>. ' +\n 'Wrap the tree, or use useSpatialSelection() (returns null) for opt-in usage.',\n );\n }\n return ctx;\n}\n\nexport default SpatialSelectionProvider;\n"],"names":[],"mappings":";;AAmCA,MAAM,kCAAkC,cAAmD,IAAI;AAuExF,SAAS,sBAA2D;AACzE,SAAO,WAAW,+BAA+B;AACnD;"}
|
|
@@ -5,3 +5,5 @@ export { DisplaySettingsProvider, useDisplaySettings, useDisplaySettingsOptional
|
|
|
5
5
|
export type { DisplaySettings, DisplaySettingsContextValue, DisplaySettingsProviderProps, ColorTarget, PresetColorKey, GlassTintKey, } from './DisplaySettingsContext';
|
|
6
6
|
export { CategoryProvider, useCategoryPalette, useCategoryPaletteRequired, } from './CategoryContext';
|
|
7
7
|
export type { CategoryContextValue, CategoryProviderProps, } from './CategoryContext';
|
|
8
|
+
export { SpatialSelectionProvider, useSpatialSelection, useSpatialSelectionStrict, } from './SpatialSelectionContext';
|
|
9
|
+
export type { SpatialSelectionContextValue, SpatialSelectionProviderProps, } from './SpatialSelectionContext';
|
|
@@ -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 {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { useTheme } from "../../theme/ThemeProvider.js";
|
|
4
|
+
import { useBreakpoint } from "../layout/useBreakpoint.js";
|
|
5
|
+
import { Tooltip } from "../overlays/Tooltip.js";
|
|
6
|
+
import { addAlpha } from "../../utils/index.js";
|
|
7
7
|
const STATUS_DOT_COLORS = {
|
|
8
8
|
normal: "#56F000",
|
|
9
9
|
caution: "#FCE83A",
|