@zendir/ui 0.6.0 → 0.6.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.
Files changed (116) hide show
  1. package/CHANGELOG.md +419 -406
  2. package/dist/react/3d/CesiumCaptureSource.js.map +1 -1
  3. package/dist/react/3d/ZenSpace3D.js.map +1 -1
  4. package/dist/react/3d/ZenSpace3DCesium.js.map +1 -1
  5. package/dist/react/3d/ZenSpace3DShaders.js.map +1 -1
  6. package/dist/react/3d/ZenSpace3DUtils.js.map +1 -1
  7. package/dist/react/3d/capturePngAnalysis.js.map +1 -1
  8. package/dist/react/3d/threeLoader.js.map +1 -1
  9. package/dist/react/astro/ClassificationBanner.js.map +1 -1
  10. package/dist/react/astro/GlobalStatusBar.js.map +1 -1
  11. package/dist/react/astro/MissionClock.js.map +1 -1
  12. package/dist/react/astro/MonitoringIcon.js.map +1 -1
  13. package/dist/react/astro/Notification.js.map +1 -1
  14. package/dist/react/astro/Progress.js.map +1 -1
  15. package/dist/react/astro/SimulationControls.js.map +1 -1
  16. package/dist/react/astro/StatusIndicator.js.map +1 -1
  17. package/dist/react/astro/UnifiedTimeline.js.map +1 -1
  18. package/dist/react/cards/AccessCard.js.map +1 -1
  19. package/dist/react/cards/OrbitCard.js.map +1 -1
  20. package/dist/react/cards/SpacecraftCard.js.map +1 -1
  21. package/dist/react/cards/TelemetryCard.js.map +1 -1
  22. package/dist/react/cards/TelemetryStreamCard.js.map +1 -1
  23. package/dist/react/charts/GroundTrackMap.js.map +1 -1
  24. package/dist/react/charts/GroundTrackMapLeaflet.js.map +1 -1
  25. package/dist/react/charts/groundTrackMapLeafletTiles.js.map +1 -1
  26. package/dist/react/charts/groundTrackMapLeafletUtils.js.map +1 -1
  27. package/dist/react/charts/unified/AstroChart.js.map +1 -1
  28. package/dist/react/charts/unified/PowerOverviewChart.js.map +1 -1
  29. package/dist/react/charts/unified/domain.js.map +1 -1
  30. package/dist/react/charts/unified/generators.js.map +1 -1
  31. package/dist/react/charts/unified/presets.js.map +1 -1
  32. package/dist/react/charts/unified/sync.js.map +1 -1
  33. package/dist/react/charts/unified/theme.js.map +1 -1
  34. package/dist/react/charts/unified/useChartStream.js.map +1 -1
  35. package/dist/react/chatgpt/index.js.map +1 -1
  36. package/dist/react/context/CategoryContext.js.map +1 -1
  37. package/dist/react/context/DisplaySettingsContext.js.map +1 -1
  38. package/dist/react/context/SpatialSelectionContext.js.map +1 -1
  39. package/dist/react/core/data/DataTable.js.map +1 -1
  40. package/dist/react/core/data/DataValue.js.map +1 -1
  41. package/dist/react/core/data/propertyConfig.js.map +1 -1
  42. package/dist/react/core/display/AstroIcon.js.map +1 -1
  43. package/dist/react/core/display/Badge.js.map +1 -1
  44. package/dist/react/core/display/CardHeader.js.map +1 -1
  45. package/dist/react/core/display/Container.js.map +1 -1
  46. package/dist/react/core/display/CopyButton.js.map +1 -1
  47. package/dist/react/core/display/GlassCard.js.map +1 -1
  48. package/dist/react/core/display/HeaderIconWithStatus.js.map +1 -1
  49. package/dist/react/core/display/Icon.js.map +1 -1
  50. package/dist/react/core/display/Typography.js.map +1 -1
  51. package/dist/react/core/feedback/ConfirmDialog.js.map +1 -1
  52. package/dist/react/core/feedback/Dialog.js.map +1 -1
  53. package/dist/react/core/feedback/Toast.js.map +1 -1
  54. package/dist/react/core/inputs/Button.js.map +1 -1
  55. package/dist/react/core/inputs/Checkbox.js.map +1 -1
  56. package/dist/react/core/inputs/Input.js.map +1 -1
  57. package/dist/react/core/inputs/LimitsBar.js.map +1 -1
  58. package/dist/react/core/inputs/NumberInput.js.map +1 -1
  59. package/dist/react/core/inputs/PinInput.js.map +1 -1
  60. package/dist/react/core/inputs/Select.js.map +1 -1
  61. package/dist/react/core/inputs/Toggle.js.map +1 -1
  62. package/dist/react/core/layout/Box.js.map +1 -1
  63. package/dist/react/core/layout/Center.js.map +1 -1
  64. package/dist/react/core/layout/Divider.js.map +1 -1
  65. package/dist/react/core/layout/Flex.js.map +1 -1
  66. package/dist/react/core/layout/Grid.js.map +1 -1
  67. package/dist/react/core/layout/Spacer.js.map +1 -1
  68. package/dist/react/core/layout/Stack.js.map +1 -1
  69. package/dist/react/core/layout/responsive.js.map +1 -1
  70. package/dist/react/core/layout/useBreakpoint.js.map +1 -1
  71. package/dist/react/core/navigation/AppBar.js.map +1 -1
  72. package/dist/react/core/navigation/Pagination.js.map +1 -1
  73. package/dist/react/core/navigation/SideNav.js.map +1 -1
  74. package/dist/react/core/navigation/Tabs.js.map +1 -1
  75. package/dist/react/core/overlays/Popover.js.map +1 -1
  76. package/dist/react/core/overlays/SidePanel.js.map +1 -1
  77. package/dist/react/core/overlays/Tooltip.js.map +1 -1
  78. package/dist/react/core/widgets/ActivityPlanner.js.map +1 -1
  79. package/dist/react/core/widgets/Capture.js.map +1 -1
  80. package/dist/react/core/widgets/ChatPanel.js.map +1 -1
  81. package/dist/react/core/widgets/ColorPickerPanel.js.map +1 -1
  82. package/dist/react/core/widgets/CommandBuilder.js.map +1 -1
  83. package/dist/react/core/widgets/ConnectionForm.js.map +1 -1
  84. package/dist/react/core/widgets/FileExplorer.js.map +1 -1
  85. package/dist/react/core/widgets/HexViewer.js.map +1 -1
  86. package/dist/react/core/widgets/ImageFrame.js.map +1 -1
  87. package/dist/react/core/widgets/ImageGallery.js.map +1 -1
  88. package/dist/react/core/widgets/ImagePanel.js.map +1 -1
  89. package/dist/react/core/widgets/LogViewer.js.map +1 -1
  90. package/dist/react/core/widgets/MessageStream.js.map +1 -1
  91. package/dist/react/core/widgets/MissionCalendar.js.map +1 -1
  92. package/dist/react/core/widgets/PacketViewer.js.map +1 -1
  93. package/dist/react/hooks/useCompactMode.js.map +1 -1
  94. package/dist/react/hooks/useSimulationScene.js.map +1 -1
  95. package/dist/react/panels/LayerControlPanel.js.map +1 -1
  96. package/dist/react/panels/ObjectInventoryPanel.js.map +1 -1
  97. package/dist/react/shared/ErrorBoundary.js.map +1 -1
  98. package/dist/react/shared/Skeleton.js.map +1 -1
  99. package/dist/react/theme/ThemeProvider.js +2 -1
  100. package/dist/react/theme/ThemeProvider.js.map +1 -1
  101. package/dist/react/theme/cardAccent.js.map +1 -1
  102. package/dist/react/theme/config.js.map +1 -1
  103. package/dist/react/theme/types.js.map +1 -1
  104. package/dist/react/types.js.map +1 -1
  105. package/dist/react/utils/categoryPalette.js.map +1 -1
  106. package/dist/react/utils/index.js.map +1 -1
  107. package/dist/style.css +216 -216
  108. package/dist/tokens/core.js.map +1 -1
  109. package/dist/tokens/index.js.map +1 -1
  110. package/dist/tokens/palettes/astro.js.map +1 -1
  111. package/dist/tokens/palettes/index.js.map +1 -1
  112. package/dist/tokens/palettes/tailwind.js.map +1 -1
  113. package/dist/tokens/palettes/zendir.js.map +1 -1
  114. package/dist/tokens/themes.js.map +1 -1
  115. package/dist/tokens/tokens.css +533 -533
  116. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../../../../src/react/charts/unified/sync.ts"],"sourcesContent":["/**\n * @zendir/ui - Chart Synchronization Utilities\n *\n * Enables crosshair, tooltip, and zoom synchronization across multiple charts.\n * Useful for dashboard views with related time-series data.\n */\n\nimport { useEffect, useRef } from \"react\";\nimport type { ECharts } from \"echarts\";\nimport type { AstroChartHandle } from \"./AstroChart\";\n\n// =============================================================================\n// Sync Group Manager\n// =============================================================================\n\ninterface SyncGroup {\n id: string;\n charts: Set<ECharts>;\n options: SyncGroupOptions;\n}\n\ninterface SyncGroupOptions {\n /** Sync tooltip position */\n tooltip?: boolean;\n /** Sync crosshair */\n crosshair?: boolean;\n /** Sync zoom/pan */\n zoom?: boolean;\n /** Sync legend selection */\n legend?: boolean;\n}\n\n// Global registry of sync groups\nconst syncGroups = new Map<string, SyncGroup>();\n\n/**\n * Create or get a sync group\n */\nfunction getOrCreateGroup(\n groupId: string,\n options: SyncGroupOptions = {},\n): SyncGroup {\n let group = syncGroups.get(groupId);\n if (!group) {\n group = {\n id: groupId,\n charts: new Set(),\n options: {\n tooltip: true,\n crosshair: true,\n zoom: true,\n legend: true,\n ...options,\n },\n };\n syncGroups.set(groupId, group);\n }\n return group;\n}\n\n/**\n * Register a chart with a sync group\n */\nfunction registerChart(groupId: string, chart: ECharts): () => void {\n const group = getOrCreateGroup(groupId);\n group.charts.add(chart);\n\n // Setup event handlers\n const handlers = setupSyncHandlers(chart, group);\n\n // Return cleanup function\n return () => {\n handlers.forEach((cleanup) => cleanup());\n group.charts.delete(chart);\n if (group.charts.size === 0) {\n syncGroups.delete(groupId);\n }\n };\n}\n\n/**\n * Setup synchronization event handlers\n */\nfunction setupSyncHandlers(\n sourceChart: ECharts,\n group: SyncGroup,\n): (() => void)[] {\n const cleanups: (() => void)[] = [];\n\n // Tooltip/Crosshair sync\n if (group.options.tooltip || group.options.crosshair) {\n const handleAxisPointer = (params: unknown) => {\n const p = params as {\n axesInfo?: { axisDim: string; value: unknown }[];\n seriesIndex?: number;\n dataIndex?: number;\n };\n\n group.charts.forEach((chart) => {\n if (chart === sourceChart) return;\n\n try {\n if (p.axesInfo && p.axesInfo.length > 0) {\n const axisInfo = p.axesInfo[0];\n chart.dispatchAction({\n type: \"showTip\",\n x: axisInfo.value as number,\n seriesIndex: 0,\n });\n } else if (p.dataIndex !== undefined) {\n chart.dispatchAction({\n type: \"showTip\",\n seriesIndex: p.seriesIndex || 0,\n dataIndex: p.dataIndex,\n });\n }\n } catch (e) {\n // Ignore errors from disposed charts\n }\n });\n };\n\n const handleMouseOut = () => {\n group.charts.forEach((chart) => {\n if (chart === sourceChart) return;\n try {\n chart.dispatchAction({ type: \"hideTip\" });\n } catch (e) {\n // Ignore\n }\n });\n };\n\n sourceChart.on(\"updateAxisPointer\", handleAxisPointer);\n sourceChart.getZr().on(\"mouseout\", handleMouseOut);\n\n cleanups.push(() => {\n sourceChart.off(\"updateAxisPointer\", handleAxisPointer);\n sourceChart.getZr()?.off(\"mouseout\", handleMouseOut);\n });\n }\n\n // Zoom sync\n if (group.options.zoom) {\n const handleDataZoom = (params: unknown) => {\n const p = params as {\n batch?: { start: number; end: number }[];\n start?: number;\n end?: number;\n };\n\n const start = p.batch?.[0]?.start ?? p.start;\n const end = p.batch?.[0]?.end ?? p.end;\n\n if (start === undefined || end === undefined) return;\n\n group.charts.forEach((chart) => {\n if (chart === sourceChart) return;\n try {\n chart.dispatchAction({\n type: \"dataZoom\",\n start,\n end,\n });\n } catch (e) {\n // Ignore\n }\n });\n };\n\n sourceChart.on(\"datazoom\", handleDataZoom);\n cleanups.push(() => sourceChart.off(\"datazoom\", handleDataZoom));\n }\n\n // Legend sync\n if (group.options.legend) {\n const handleLegendSelect = (params: unknown) => {\n const p = params as { name: string; selected: Record<string, boolean> };\n\n group.charts.forEach((chart) => {\n if (chart === sourceChart) return;\n try {\n chart.dispatchAction({\n type: \"legendToggleSelect\",\n name: p.name,\n });\n } catch (e) {\n // Ignore\n }\n });\n };\n\n sourceChart.on(\"legendselectchanged\", handleLegendSelect);\n cleanups.push(() =>\n sourceChart.off(\"legendselectchanged\", handleLegendSelect),\n );\n }\n\n return cleanups;\n}\n\n// =============================================================================\n// React Hook\n// =============================================================================\n\nexport interface UseChartSyncOptions extends SyncGroupOptions {\n /** Sync group ID */\n groupId: string;\n /** Whether sync is enabled */\n enabled?: boolean;\n}\n\n/**\n * Hook to synchronize a chart with other charts in the same group\n *\n * @example\n * ```tsx\n * const chartRef = useRef<AstroChartHandle>(null);\n * useChartSync(chartRef, { groupId: 'dashboard-charts' });\n * ```\n */\nexport function useChartSync(\n chartRef: React.RefObject<AstroChartHandle>,\n options: UseChartSyncOptions,\n): void {\n const { groupId, enabled = true, ..._syncOptions } = options;\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n if (!enabled) return;\n\n // Get chart instance\n const chart = chartRef.current?.getChart() as ECharts | undefined;\n if (!chart) return;\n\n // Register with group\n cleanupRef.current = registerChart(groupId, chart);\n\n return () => {\n cleanupRef.current?.();\n cleanupRef.current = null;\n };\n }, [chartRef, groupId, enabled]);\n}\n\n// =============================================================================\n// Programmatic API\n// =============================================================================\n\n/**\n * Dispatch action to all charts in a group\n */\nexport function dispatchToGroup(\n groupId: string,\n action: Parameters<ECharts[\"dispatchAction\"]>[0],\n): void {\n const group = syncGroups.get(groupId);\n if (!group) return;\n\n group.charts.forEach((chart) => {\n try {\n chart.dispatchAction(action);\n } catch (e) {\n // Ignore disposed charts\n }\n });\n}\n\n/**\n * Sync zoom range across all charts in a group\n */\nexport function syncZoomRange(\n groupId: string,\n start: number,\n end: number,\n): void {\n dispatchToGroup(groupId, { type: \"dataZoom\", start, end });\n}\n\n/**\n * Reset zoom for all charts in a group\n */\nexport function resetGroupZoom(groupId: string): void {\n dispatchToGroup(groupId, { type: \"dataZoom\", start: 0, end: 100 });\n}\n\n/**\n * Show tooltip at specific data index for all charts\n */\nexport function showGroupTooltip(\n groupId: string,\n seriesIndex: number,\n dataIndex: number,\n): void {\n dispatchToGroup(groupId, { type: \"showTip\", seriesIndex, dataIndex });\n}\n\n/**\n * Hide tooltip for all charts in a group\n */\nexport function hideGroupTooltip(groupId: string): void {\n dispatchToGroup(groupId, { type: \"hideTip\" });\n}\n\n/**\n * Get all chart instances in a group\n */\nexport function getGroupCharts(groupId: string): ECharts[] {\n const group = syncGroups.get(groupId);\n return group ? Array.from(group.charts) : [];\n}\n\n// =============================================================================\n// Export Utilities\n// =============================================================================\n\n/**\n * Export all charts in a group as images\n */\nexport function exportGroupAsImages(\n groupId: string,\n options: {\n type?: \"png\" | \"svg\";\n pixelRatio?: number;\n fileName?: string;\n } = {},\n): string[] {\n const {\n type = \"png\",\n pixelRatio = 2,\n fileName: _fileName = \"chart\",\n } = options;\n const charts = getGroupCharts(groupId);\n\n return charts.map((chart, _index) => {\n return chart.getDataURL({\n type,\n pixelRatio,\n backgroundColor: \"transparent\",\n });\n });\n}\n\n/**\n * Export all charts in a group as a combined CSV\n */\nexport function exportGroupAsCSV(\n groupId: string,\n options: {\n fileName?: string;\n includeHeaders?: boolean;\n } = {},\n): string {\n const { includeHeaders = true } = options;\n const charts = getGroupCharts(groupId);\n const rows: string[][] = [];\n\n charts.forEach((chart, chartIndex) => {\n const option = chart.getOption();\n const series = option.series as Array<{ name: string; data: unknown[] }>;\n\n if (!series || series.length === 0) return;\n\n // Add chart header\n if (includeHeaders) {\n rows.push([`Chart ${chartIndex + 1}`]);\n rows.push([\n \"Index\",\n ...series.map((s) => s.name || `Series ${chartIndex}`),\n ]);\n }\n\n // Find max data length\n const maxLen = Math.max(...series.map((s) => s.data?.length || 0));\n\n // Add data rows\n for (let i = 0; i < maxLen; i++) {\n const row = [i.toString()];\n series.forEach((s) => {\n const value = s.data?.[i];\n if (typeof value === \"number\") {\n row.push(value.toString());\n } else if (Array.isArray(value)) {\n row.push(value[1]?.toString() || \"\");\n } else if (value && typeof value === \"object\") {\n const v = value as { value?: number; y?: number };\n row.push((v.value ?? v.y)?.toString() || \"\");\n } else {\n row.push(\"\");\n }\n });\n rows.push(row);\n }\n\n rows.push([]); // Empty row between charts\n });\n\n return rows.map((row) => row.join(\",\")).join(\"\\n\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../../../../src/react/charts/unified/sync.ts"],"sourcesContent":["/**\r\n * @zendir/ui - Chart Synchronization Utilities\r\n *\r\n * Enables crosshair, tooltip, and zoom synchronization across multiple charts.\r\n * Useful for dashboard views with related time-series data.\r\n */\r\n\r\nimport { useEffect, useRef } from \"react\";\r\nimport type { ECharts } from \"echarts\";\r\nimport type { AstroChartHandle } from \"./AstroChart\";\r\n\r\n// =============================================================================\r\n// Sync Group Manager\r\n// =============================================================================\r\n\r\ninterface SyncGroup {\r\n id: string;\r\n charts: Set<ECharts>;\r\n options: SyncGroupOptions;\r\n}\r\n\r\ninterface SyncGroupOptions {\r\n /** Sync tooltip position */\r\n tooltip?: boolean;\r\n /** Sync crosshair */\r\n crosshair?: boolean;\r\n /** Sync zoom/pan */\r\n zoom?: boolean;\r\n /** Sync legend selection */\r\n legend?: boolean;\r\n}\r\n\r\n// Global registry of sync groups\r\nconst syncGroups = new Map<string, SyncGroup>();\r\n\r\n/**\r\n * Create or get a sync group\r\n */\r\nfunction getOrCreateGroup(\r\n groupId: string,\r\n options: SyncGroupOptions = {},\r\n): SyncGroup {\r\n let group = syncGroups.get(groupId);\r\n if (!group) {\r\n group = {\r\n id: groupId,\r\n charts: new Set(),\r\n options: {\r\n tooltip: true,\r\n crosshair: true,\r\n zoom: true,\r\n legend: true,\r\n ...options,\r\n },\r\n };\r\n syncGroups.set(groupId, group);\r\n }\r\n return group;\r\n}\r\n\r\n/**\r\n * Register a chart with a sync group\r\n */\r\nfunction registerChart(groupId: string, chart: ECharts): () => void {\r\n const group = getOrCreateGroup(groupId);\r\n group.charts.add(chart);\r\n\r\n // Setup event handlers\r\n const handlers = setupSyncHandlers(chart, group);\r\n\r\n // Return cleanup function\r\n return () => {\r\n handlers.forEach((cleanup) => cleanup());\r\n group.charts.delete(chart);\r\n if (group.charts.size === 0) {\r\n syncGroups.delete(groupId);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Setup synchronization event handlers\r\n */\r\nfunction setupSyncHandlers(\r\n sourceChart: ECharts,\r\n group: SyncGroup,\r\n): (() => void)[] {\r\n const cleanups: (() => void)[] = [];\r\n\r\n // Tooltip/Crosshair sync\r\n if (group.options.tooltip || group.options.crosshair) {\r\n const handleAxisPointer = (params: unknown) => {\r\n const p = params as {\r\n axesInfo?: { axisDim: string; value: unknown }[];\r\n seriesIndex?: number;\r\n dataIndex?: number;\r\n };\r\n\r\n group.charts.forEach((chart) => {\r\n if (chart === sourceChart) return;\r\n\r\n try {\r\n if (p.axesInfo && p.axesInfo.length > 0) {\r\n const axisInfo = p.axesInfo[0];\r\n chart.dispatchAction({\r\n type: \"showTip\",\r\n x: axisInfo.value as number,\r\n seriesIndex: 0,\r\n });\r\n } else if (p.dataIndex !== undefined) {\r\n chart.dispatchAction({\r\n type: \"showTip\",\r\n seriesIndex: p.seriesIndex || 0,\r\n dataIndex: p.dataIndex,\r\n });\r\n }\r\n } catch (e) {\r\n // Ignore errors from disposed charts\r\n }\r\n });\r\n };\r\n\r\n const handleMouseOut = () => {\r\n group.charts.forEach((chart) => {\r\n if (chart === sourceChart) return;\r\n try {\r\n chart.dispatchAction({ type: \"hideTip\" });\r\n } catch (e) {\r\n // Ignore\r\n }\r\n });\r\n };\r\n\r\n sourceChart.on(\"updateAxisPointer\", handleAxisPointer);\r\n sourceChart.getZr().on(\"mouseout\", handleMouseOut);\r\n\r\n cleanups.push(() => {\r\n sourceChart.off(\"updateAxisPointer\", handleAxisPointer);\r\n sourceChart.getZr()?.off(\"mouseout\", handleMouseOut);\r\n });\r\n }\r\n\r\n // Zoom sync\r\n if (group.options.zoom) {\r\n const handleDataZoom = (params: unknown) => {\r\n const p = params as {\r\n batch?: { start: number; end: number }[];\r\n start?: number;\r\n end?: number;\r\n };\r\n\r\n const start = p.batch?.[0]?.start ?? p.start;\r\n const end = p.batch?.[0]?.end ?? p.end;\r\n\r\n if (start === undefined || end === undefined) return;\r\n\r\n group.charts.forEach((chart) => {\r\n if (chart === sourceChart) return;\r\n try {\r\n chart.dispatchAction({\r\n type: \"dataZoom\",\r\n start,\r\n end,\r\n });\r\n } catch (e) {\r\n // Ignore\r\n }\r\n });\r\n };\r\n\r\n sourceChart.on(\"datazoom\", handleDataZoom);\r\n cleanups.push(() => sourceChart.off(\"datazoom\", handleDataZoom));\r\n }\r\n\r\n // Legend sync\r\n if (group.options.legend) {\r\n const handleLegendSelect = (params: unknown) => {\r\n const p = params as { name: string; selected: Record<string, boolean> };\r\n\r\n group.charts.forEach((chart) => {\r\n if (chart === sourceChart) return;\r\n try {\r\n chart.dispatchAction({\r\n type: \"legendToggleSelect\",\r\n name: p.name,\r\n });\r\n } catch (e) {\r\n // Ignore\r\n }\r\n });\r\n };\r\n\r\n sourceChart.on(\"legendselectchanged\", handleLegendSelect);\r\n cleanups.push(() =>\r\n sourceChart.off(\"legendselectchanged\", handleLegendSelect),\r\n );\r\n }\r\n\r\n return cleanups;\r\n}\r\n\r\n// =============================================================================\r\n// React Hook\r\n// =============================================================================\r\n\r\nexport interface UseChartSyncOptions extends SyncGroupOptions {\r\n /** Sync group ID */\r\n groupId: string;\r\n /** Whether sync is enabled */\r\n enabled?: boolean;\r\n}\r\n\r\n/**\r\n * Hook to synchronize a chart with other charts in the same group\r\n *\r\n * @example\r\n * ```tsx\r\n * const chartRef = useRef<AstroChartHandle>(null);\r\n * useChartSync(chartRef, { groupId: 'dashboard-charts' });\r\n * ```\r\n */\r\nexport function useChartSync(\r\n chartRef: React.RefObject<AstroChartHandle>,\r\n options: UseChartSyncOptions,\r\n): void {\r\n const { groupId, enabled = true, ..._syncOptions } = options;\r\n const cleanupRef = useRef<(() => void) | null>(null);\r\n\r\n useEffect(() => {\r\n if (!enabled) return;\r\n\r\n // Get chart instance\r\n const chart = chartRef.current?.getChart() as ECharts | undefined;\r\n if (!chart) return;\r\n\r\n // Register with group\r\n cleanupRef.current = registerChart(groupId, chart);\r\n\r\n return () => {\r\n cleanupRef.current?.();\r\n cleanupRef.current = null;\r\n };\r\n }, [chartRef, groupId, enabled]);\r\n}\r\n\r\n// =============================================================================\r\n// Programmatic API\r\n// =============================================================================\r\n\r\n/**\r\n * Dispatch action to all charts in a group\r\n */\r\nexport function dispatchToGroup(\r\n groupId: string,\r\n action: Parameters<ECharts[\"dispatchAction\"]>[0],\r\n): void {\r\n const group = syncGroups.get(groupId);\r\n if (!group) return;\r\n\r\n group.charts.forEach((chart) => {\r\n try {\r\n chart.dispatchAction(action);\r\n } catch (e) {\r\n // Ignore disposed charts\r\n }\r\n });\r\n}\r\n\r\n/**\r\n * Sync zoom range across all charts in a group\r\n */\r\nexport function syncZoomRange(\r\n groupId: string,\r\n start: number,\r\n end: number,\r\n): void {\r\n dispatchToGroup(groupId, { type: \"dataZoom\", start, end });\r\n}\r\n\r\n/**\r\n * Reset zoom for all charts in a group\r\n */\r\nexport function resetGroupZoom(groupId: string): void {\r\n dispatchToGroup(groupId, { type: \"dataZoom\", start: 0, end: 100 });\r\n}\r\n\r\n/**\r\n * Show tooltip at specific data index for all charts\r\n */\r\nexport function showGroupTooltip(\r\n groupId: string,\r\n seriesIndex: number,\r\n dataIndex: number,\r\n): void {\r\n dispatchToGroup(groupId, { type: \"showTip\", seriesIndex, dataIndex });\r\n}\r\n\r\n/**\r\n * Hide tooltip for all charts in a group\r\n */\r\nexport function hideGroupTooltip(groupId: string): void {\r\n dispatchToGroup(groupId, { type: \"hideTip\" });\r\n}\r\n\r\n/**\r\n * Get all chart instances in a group\r\n */\r\nexport function getGroupCharts(groupId: string): ECharts[] {\r\n const group = syncGroups.get(groupId);\r\n return group ? Array.from(group.charts) : [];\r\n}\r\n\r\n// =============================================================================\r\n// Export Utilities\r\n// =============================================================================\r\n\r\n/**\r\n * Export all charts in a group as images\r\n */\r\nexport function exportGroupAsImages(\r\n groupId: string,\r\n options: {\r\n type?: \"png\" | \"svg\";\r\n pixelRatio?: number;\r\n fileName?: string;\r\n } = {},\r\n): string[] {\r\n const {\r\n type = \"png\",\r\n pixelRatio = 2,\r\n fileName: _fileName = \"chart\",\r\n } = options;\r\n const charts = getGroupCharts(groupId);\r\n\r\n return charts.map((chart, _index) => {\r\n return chart.getDataURL({\r\n type,\r\n pixelRatio,\r\n backgroundColor: \"transparent\",\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * Export all charts in a group as a combined CSV\r\n */\r\nexport function exportGroupAsCSV(\r\n groupId: string,\r\n options: {\r\n fileName?: string;\r\n includeHeaders?: boolean;\r\n } = {},\r\n): string {\r\n const { includeHeaders = true } = options;\r\n const charts = getGroupCharts(groupId);\r\n const rows: string[][] = [];\r\n\r\n charts.forEach((chart, chartIndex) => {\r\n const option = chart.getOption();\r\n const series = option.series as Array<{ name: string; data: unknown[] }>;\r\n\r\n if (!series || series.length === 0) return;\r\n\r\n // Add chart header\r\n if (includeHeaders) {\r\n rows.push([`Chart ${chartIndex + 1}`]);\r\n rows.push([\r\n \"Index\",\r\n ...series.map((s) => s.name || `Series ${chartIndex}`),\r\n ]);\r\n }\r\n\r\n // Find max data length\r\n const maxLen = Math.max(...series.map((s) => s.data?.length || 0));\r\n\r\n // Add data rows\r\n for (let i = 0; i < maxLen; i++) {\r\n const row = [i.toString()];\r\n series.forEach((s) => {\r\n const value = s.data?.[i];\r\n if (typeof value === \"number\") {\r\n row.push(value.toString());\r\n } else if (Array.isArray(value)) {\r\n row.push(value[1]?.toString() || \"\");\r\n } else if (value && typeof value === \"object\") {\r\n const v = value as { value?: number; y?: number };\r\n row.push((v.value ?? v.y)?.toString() || \"\");\r\n } else {\r\n row.push(\"\");\r\n }\r\n });\r\n rows.push(row);\r\n }\r\n\r\n rows.push([]); // Empty row between charts\r\n });\r\n\r\n return rows.map((row) => row.join(\",\")).join(\"\\n\");\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"theme.js","names":[],"sources":["../../../../src/react/charts/unified/theme.ts"],"sourcesContent":["/**\n * @zendir/ui - ECharts Theme Adapter\n *\n * Converts Astro UX Design System tokens to ECharts theme configuration.\n * Supports dark/light modes and colorblind-accessible palettes.\n *\n * AstroUXD Compliance:\n * - Max 11 colors per dataset\n * - Colorblind-accessible palette\n * - Sentence-case labels\n * - 14pt minimum axis text\n * - WCAG contrast compliance\n *\n * @version 2.0.0 - Full Zendir Brand Color Migration\n */\n\nimport type { ThemeTokens, ThemeMode, ThemeVariant } from \"../../theme\";\n\n// =============================================================================\n// AstroUXD Data Visualization Color Palettes\n// =============================================================================\n\n/**\n * Zendir Brand Colors (from Brand Guide v2026)\n * Core: Electric (#3E3CFF), Purple (#9D70FF), Prussian Blue (#1B2DA0)\n * Shades derived using 10%, 20%, 35% white/black overlays per brand guide\n */\nexport const ZENDIR_BRAND_COLORS = {\n // === Core Brand Colors ===\n /** Electric - Primary accent (#3E3CFF) */\n electric: \"#3E3CFF\",\n /** Purple - Secondary accent (#9D70FF) */\n purple: \"#9D70FF\",\n /** Prussian Blue - Subsections (#1B2DA0) */\n prussianBlue: \"#1B2DA0\",\n /** Black */\n black: \"#000000\",\n /** White */\n white: \"#FFFFFF\",\n\n // === Electric Shades (10%, 20%, 35% overlays) ===\n electricLighter: \"#9997FF\", // 35% white overlay\n electricLight: \"#6B69FF\", // 20% white overlay\n electricSoft: \"#5250FF\", // 10% white overlay\n electricDark: \"#3836E6\", // 10% black overlay\n electricDarker: \"#3230CC\", // 20% black overlay\n electricDeep: \"#2926A3\", // 35% black overlay\n\n // === Purple Shades ===\n purpleLighter: \"#C9A8FF\", // 35% white overlay\n purpleLight: \"#B48DFF\", // 20% white overlay\n purpleSoft: \"#A97FFF\", // 10% white overlay\n purpleDark: \"#8E64E6\", // 10% black overlay\n purpleDarker: \"#7E5ACC\", // 20% black overlay\n purpleDeep: \"#5F4499\", // 35% black overlay\n\n // === Prussian Blue Shades ===\n prussianBlueLighter: \"#4A5FD6\", // 35% white overlay\n prussianBlueLight: \"#3548C0\", // 20% white overlay\n prussianBlueSoft: \"#2838B0\", // 10% white overlay\n prussianBlueDark: \"#182890\", // 10% black overlay\n prussianBlueDarker: \"#142380\", // 20% black overlay\n prussianBlueDeep: \"#0F1A60\", // 35% black overlay\n} as const;\n\n/**\n * Official AstroUXDS Data Visualization Color Palette\n * Per https://astrouxds.com/design-tokens/system/#data-viz\n *\n * These are the OFFICIAL AstroUXDS data visualization colors from the design system.\n * Use these for pure AstroUXDS compliance (e.g., 'astro' theme).\n *\n * @see https://astrouxds.com/design-tokens/system/\n */\nexport const OFFICIAL_ASTRO_DATA_VIZ_COLORS = {\n /** Official AstroUXDS data visualization palette (8 colors) */\n primary: [\n \"#00c7cb\", // color-data-visualization-1 - Teal 500\n \"#938bdb\", // color-data-visualization-2 - Purple 400\n \"#4dacff\", // color-data-visualization-3 - Brightblue 500\n \"#70dde0\", // color-data-visualization-4 - Teal 300\n \"#c9c5ed\", // color-data-visualization-5 - Purple 200\n \"#92cbff\", // color-data-visualization-6 - Brightblue 400\n \"#a1e9eb\", // color-data-visualization-7 - Teal 200\n \"#b7dcff\", // color-data-visualization-8 - Brightblue 300\n ],\n\n /** Extended palette - primary + additional derived colors for 11 max */\n extended: [\n \"#00c7cb\", // Teal 500 - Primary\n \"#938bdb\", // Purple 400 - Secondary\n \"#4dacff\", // Brightblue 500\n \"#70dde0\", // Teal 300\n \"#c9c5ed\", // Purple 200\n \"#92cbff\", // Brightblue 400\n \"#a1e9eb\", // Teal 200\n \"#b7dcff\", // Brightblue 300\n \"#009a9d\", // Teal 600 (darker)\n \"#756bb8\", // Purple 500 (darker)\n \"#2b659b\", // Brightblue 700 (darker)\n ],\n\n /** Sequential palettes for heatmaps using AstroUXDS colors */\n sequential: {\n /** Teal sequential */\n teal: [\"#e5f9f9\", \"#a1e9eb\", \"#70dde0\", \"#00c7cb\", \"#009a9d\", \"#006e70\"],\n /** Purple sequential */\n purple: [\"#eceaf7\", \"#c9c5ed\", \"#a9a3dc\", \"#938bdb\", \"#756bb8\", \"#5b5299\"],\n /** Brightblue sequential */\n brightblue: [\n \"#e8f4ff\",\n \"#b7dcff\",\n \"#92cbff\",\n \"#4dacff\",\n \"#2b659b\",\n \"#1c3f5e\",\n ],\n /** Thermal - Status colors for temperature */\n thermal: [\"#00c7cb\", \"#56f000\", \"#fce83a\", \"#ffb302\", \"#ff6b35\", \"#ff3838\"],\n },\n} as const;\n\n/**\n * Zendir-branded Data Visualization Color Palette\n * Uses Zendir brand colors (Electric, Purple, Prussian Blue)\n * while maintaining AstroUXDS compliance (max 11 colors, colorblind-accessible)\n *\n * @see https://www.astrouxds.com/patterns/data-visualization/\n */\nexport const ASTRO_DATA_VIZ_COLORS = {\n /**\n * Primary data visualization palette - Pure Zendir Brand\n * Uses only Zendir brand colors and their derived shades\n * Max 11 colors per AstroUXDS compliance requirement 3.2.1\n *\n * NOTE: Status colors (green, yellow, red) reserved for status indication only\n */\n mixed: [\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF - Primary\n ZENDIR_BRAND_COLORS.purple, // #9D70FF - Secondary\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF - Electric 20% lighter\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF - Purple 20% lighter\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0 - Prussian Blue lighter\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF - Electric 35% lighter\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF - Purple 35% lighter\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0 - Prussian Blue\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC - Electric 20% darker\n ZENDIR_BRAND_COLORS.purpleDarker, // #7E5ACC - Purple 20% darker\n ZENDIR_BRAND_COLORS.prussianBlueLighter, // #4A5FD6 - Prussian Blue lightest\n ],\n\n /**\n * Zendir Brand palette - Full brand spectrum\n * Complete set of Zendir brand colors for maximum brand expression\n */\n zendir: [\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF - Primary\n ZENDIR_BRAND_COLORS.purple, // #9D70FF - Secondary\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\n ],\n\n /**\n * Extended Zendir palette - All shades for complex visualizations\n * Includes dark and light variants for maximum differentiation\n */\n zendirExtended: [\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC\n ZENDIR_BRAND_COLORS.purpleDarker, // #7E5ACC\n ZENDIR_BRAND_COLORS.prussianBlueDarker, // #142380\n ],\n\n /**\n * Cool palette - Blues and purples\n * Professional Zendir-branded cool tones\n */\n cool: [\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\n ZENDIR_BRAND_COLORS.prussianBlueLighter, // #4A5FD6\n ],\n\n /**\n * Warm accent palette - For charts needing warm tones\n * Use sparingly - not part of Zendir brand\n */\n warm: [\n \"#ffb302\", // Amber\n \"#ff9966\", // Coral\n \"#fce83a\", // Yellow\n \"#ff6b6b\", // Salmon\n \"#f0a000\", // Orange\n \"#ffcc80\", // Peach\n \"#e0a86c\", // Tan\n \"#d4b896\", // Sand\n ],\n\n /**\n * Electric-Purple gradient palette (Zendir brand gradient)\n * Matches the official brand gradient from Electric to Purple\n */\n electricPurple: [\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\n ZENDIR_BRAND_COLORS.electricSoft, // #5250FF\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n \"#8070FF\", // Blend E→P\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\n \"#DCC4FF\", // Lightest\n ],\n\n /**\n * Prussian-Electric gradient palette\n * Deep to vibrant brand progression\n */\n prussianElectric: [\n ZENDIR_BRAND_COLORS.prussianBlueDarker, // #142380\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n ],\n\n /**\n * Full Zendir spectrum gradient\n * Prussian Blue → Electric → Purple\n */\n zendirSpectrum: [\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\n ],\n\n /**\n * Sequential palettes for heatmaps and gradual data\n * Zendir brand-forward gradients\n */\n sequential: {\n /** Zendir Electric sequential - Primary brand gradient */\n electric: [\n \"#E8E8FF\",\n \"#C9C8FF\",\n \"#9997FF\",\n \"#6B69FF\",\n \"#3E3CFF\",\n \"#3230CC\",\n \"#262499\",\n ],\n /** Zendir Purple sequential - Secondary brand gradient */\n purple: [\n \"#F0E8FF\",\n \"#DCC4FF\",\n \"#C9A8FF\",\n \"#B48DFF\",\n \"#9D70FF\",\n \"#7E5ACC\",\n \"#5F4499\",\n ],\n /** Zendir Prussian Blue sequential - Deep brand gradient */\n prussianBlue: [\n \"#E8ECFF\",\n \"#C5CFF0\",\n \"#8A9DD6\",\n \"#4A5FD6\",\n \"#3548C0\",\n \"#1B2DA0\",\n \"#0F1A60\",\n ],\n /** Zendir gradient - Electric → Purple blend */\n brand: [\n \"#3E3CFF\",\n \"#5250FF\",\n \"#6B69FF\",\n \"#7E70FF\",\n \"#9D70FF\",\n \"#B48DFF\",\n \"#C9A8FF\",\n ],\n /** Thermal - Status colors for temperature (AstroUXDS compliant) */\n thermal: [\"#00C7CB\", \"#56F000\", \"#FCE83A\", \"#FFB302\", \"#FF6B35\", \"#FF3838\"],\n },\n};\n\n/**\n * AstroUXDS Status Colors (DO NOT MODIFY)\n * Per https://www.astrouxds.com/patterns/status-system/\n *\n * These colors are RESERVED for status indication only.\n * Per compliance requirement, do not use status colors for\n * non-status data visualization.\n *\n * @see https://www.astrouxds.com/compliance/astro-design-compliance/\n * Rule 3.1.2: Elements indicating state shall use only specified Status Colors\n */\nexport const STATUS_COLORS = {\n normal: \"#56f000\", // Green - System operating normally\n standby: \"#2dccff\", // Cyan - Awaiting/standby state\n caution: \"#fce83a\", // Yellow - Requires attention\n serious: \"#ffb302\", // Orange - Significant issue\n critical: \"#ff3838\", // Red - Immediate action required\n off: \"#a4abb6\", // Grey - Inactive/off state\n} as const;\n\n/**\n * Zendir Accent Colors for UI elements (non-status)\n * Use these for interactive elements, highlights, and branding\n */\nexport const ZENDIR_ACCENT_COLORS = {\n /** Primary accent - Zendir Electric */\n primary: ZENDIR_BRAND_COLORS.electric,\n /** Secondary accent - Zendir Purple */\n secondary: ZENDIR_BRAND_COLORS.purple,\n /** Tertiary accent - Zendir Prussian Blue */\n tertiary: ZENDIR_BRAND_COLORS.prussianBlue,\n /** Info accent - Prussian Blue Light */\n info: ZENDIR_BRAND_COLORS.prussianBlueLight,\n /** Highlight - Electric Light */\n highlight: ZENDIR_BRAND_COLORS.electricLight,\n} as const;\n\n// =============================================================================\n// Theme Generator\n// =============================================================================\n\nexport interface EChartsTheme {\n color: string[];\n backgroundColor: string;\n textStyle: {\n color: string;\n fontFamily: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n title: {\n textStyle: {\n color: string;\n fontSize: number;\n fontWeight: string;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n subtextStyle: {\n color: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n };\n legend: {\n textStyle: {\n color: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n inactiveColor: string;\n };\n tooltip: {\n backgroundColor: string;\n borderColor: string;\n borderWidth: number;\n textStyle: {\n color: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n extraCssText: string;\n };\n axisPointer: {\n lineStyle: { color: string; width: number };\n crossStyle: { color: string; width: number };\n };\n xAxis: {\n axisLine: { lineStyle: { color: string } };\n axisTick: { lineStyle: { color: string } };\n axisLabel: {\n color: string;\n fontSize: number;\n fontFamily: string;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n splitLine: { lineStyle: { color: string; type: string } };\n nameTextStyle: {\n color: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n };\n yAxis: {\n axisLine: { lineStyle: { color: string } };\n axisTick: { lineStyle: { color: string } };\n axisLabel: {\n color: string;\n fontSize: number;\n fontFamily: string;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n splitLine: { lineStyle: { color: string; type: string } };\n nameTextStyle: {\n color: string;\n fontSize: number;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n };\n series: {\n line: { lineStyle: { width: number }; symbolSize: number };\n bar: { barMaxWidth: number; itemStyle: { borderRadius: number[] } };\n pie: {\n itemStyle: { borderColor: string; borderWidth: number };\n label?: {\n color: string;\n textBorderWidth: number;\n textBorderColor: string;\n fontSize: number;\n };\n };\n scatter: { symbolSize: number };\n gauge: { axisLine: { lineStyle: { width: number } } };\n };\n dataZoom: {\n backgroundColor: string;\n borderColor: string;\n fillerColor: string;\n handleColor: string;\n textStyle: {\n color: string;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n };\n visualMap: {\n textStyle: {\n color: string;\n textBorderWidth?: number;\n textBorderColor?: string;\n };\n inRange: { color: string[] };\n };\n grid: {\n borderColor: string;\n };\n}\n\n/**\n * Generate ECharts theme from Astro tokens\n *\n * Creates a theme that:\n * - Uses official AstroUXDS colors for 'astro' theme variant\n * - Uses Zendir brand colors for all other theme variants\n * - Maintains AstroUXDS compliance (max 11 colors, 14pt min font)\n * - Preserves status colors for their reserved purposes\n * - Supports both dark and light modes\n *\n * @param tokens - Theme tokens from ThemeProvider\n * @param mode - Current theme mode ('dark' | 'light')\n * @param themeVariant - Optional theme variant to determine color palette\n * @returns ECharts theme configuration\n */\nexport function createAstroEChartsTheme(\n tokens: ThemeTokens,\n mode: ThemeMode,\n themeVariant?: ThemeVariant,\n): EChartsTheme {\n const isDark = mode === \"dark\";\n\n // Use official AstroUXDS colors for 'astro' theme, Zendir colors for others\n const isAstroTheme = themeVariant === \"astro\";\n const dataVizColors = isAstroTheme\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended]\n : ASTRO_DATA_VIZ_COLORS.mixed;\n\n return {\n // Data visualization color palette\n // - 'astro' theme: Official AstroUXDS colors (teal, purple, brightblue)\n // - Other themes: Zendir brand colors (electric, purple, prussian blue)\n color: dataVizColors,\n\n // Background\n backgroundColor: \"transparent\",\n\n // Global text style - 14pt minimum per AstroUXDS; no white border on labels\n textStyle: {\n color: tokens.colors.text.primary,\n fontFamily: tokens.typography.fontFamily.primary,\n fontSize: 14, // AstroUXD minimum requirement\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n\n // Title - Grey for readability, AstroUXDS compliant\n title: {\n textStyle: {\n color: tokens.colors.text.secondary, // Grey for headings\n fontSize: 14,\n fontWeight: \"bold\",\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n subtextStyle: {\n color: tokens.colors.text.tertiary,\n fontSize: 12,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n },\n\n // Legend\n legend: {\n textStyle: {\n color: tokens.colors.text.secondary,\n fontSize: 12,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n inactiveColor: tokens.colors.text.tertiary,\n },\n\n // Tooltip\n tooltip: {\n backgroundColor: isDark\n ? \"rgba(27, 45, 62, 0.95)\"\n : \"rgba(255, 255, 255, 0.95)\",\n borderColor: tokens.colors.border.default,\n borderWidth: 1,\n textStyle: {\n color: tokens.colors.text.primary,\n fontSize: 12,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n extraCssText: `\n border-radius: ${tokens.borderRadius.md};\n box-shadow: ${tokens.shadows.lg};\n backdrop-filter: blur(8px);\n `,\n },\n\n // Axis pointer / crosshair - Subtle grey for clarity\n axisPointer: {\n lineStyle: {\n color: tokens.colors.text.tertiary, // Subtle grey\n width: 1,\n },\n crossStyle: {\n color: tokens.colors.text.tertiary, // Subtle grey\n width: 1,\n },\n },\n\n // X Axis\n xAxis: {\n axisLine: {\n lineStyle: {\n color: tokens.colors.border.default,\n },\n },\n axisTick: {\n lineStyle: {\n color: tokens.colors.border.muted,\n },\n },\n axisLabel: {\n color: tokens.colors.text.secondary,\n fontSize: 12,\n fontFamily: tokens.typography.fontFamily.mono, // Tabular numbers\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n splitLine: {\n lineStyle: {\n color: tokens.colors.border.muted,\n type: \"dashed\",\n },\n },\n nameTextStyle: {\n color: tokens.colors.text.secondary,\n fontSize: 12,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n },\n\n // Y Axis\n yAxis: {\n axisLine: {\n lineStyle: {\n color: tokens.colors.border.default,\n },\n },\n axisTick: {\n lineStyle: {\n color: tokens.colors.border.muted,\n },\n },\n axisLabel: {\n color: tokens.colors.text.secondary,\n fontSize: 12,\n fontFamily: tokens.typography.fontFamily.mono, // Tabular numbers\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n splitLine: {\n lineStyle: {\n color: tokens.colors.border.muted,\n type: \"dashed\",\n },\n },\n nameTextStyle: {\n color: tokens.colors.text.secondary,\n fontSize: 12,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n },\n\n // Series defaults\n series: {\n line: {\n lineStyle: {\n width: 2,\n },\n symbolSize: 6,\n },\n bar: {\n barMaxWidth: 40,\n itemStyle: {\n borderRadius: [4, 4, 0, 0],\n },\n },\n pie: {\n itemStyle: {\n borderColor: tokens.colors.background.surface,\n borderWidth: 2,\n },\n label: {\n color: tokens.colors.text.primary,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n fontSize: 12,\n },\n },\n scatter: {\n symbolSize: 10,\n },\n gauge: {\n axisLine: {\n lineStyle: {\n width: 10,\n },\n },\n },\n },\n\n // Data zoom - Theme-aware interactive controls\n dataZoom: {\n backgroundColor: isDark\n ? \"rgba(27, 45, 62, 0.5)\"\n : \"rgba(234, 238, 244, 0.5)\",\n borderColor: tokens.colors.border.muted,\n // Astro theme uses brightblue, others use Zendir Electric\n fillerColor: isAstroTheme\n ? \"rgba(77, 172, 255, 0.15)\" // AstroUXDS Brightblue\n : \"rgba(62, 60, 255, 0.15)\", // Zendir Electric\n handleColor: isAstroTheme\n ? \"#4dacff\" // AstroUXDS Brightblue 500\n : ZENDIR_BRAND_COLORS.electric, // Zendir Electric\n textStyle: {\n color: tokens.colors.text.secondary,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n },\n\n // Visual map (for heatmaps) - Theme-aware sequential palette\n visualMap: {\n textStyle: {\n color: tokens.colors.text.secondary,\n textBorderWidth: 0,\n textBorderColor: \"transparent\",\n },\n inRange: {\n // Astro theme uses official teal gradient, others use Zendir Electric\n color: isAstroTheme\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.sequential.teal]\n : ASTRO_DATA_VIZ_COLORS.sequential.electric,\n },\n },\n\n // Grid\n grid: {\n borderColor: tokens.colors.border.muted,\n },\n };\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Get status color from value and thresholds\n */\nexport function getStatusColor(\n value: number,\n thresholds: {\n critical?: number;\n serious?: number;\n caution?: number;\n normal?: number;\n },\n): string {\n if (thresholds.critical !== undefined && value >= thresholds.critical) {\n return STATUS_COLORS.critical;\n }\n if (thresholds.serious !== undefined && value >= thresholds.serious) {\n return STATUS_COLORS.serious;\n }\n if (thresholds.caution !== undefined && value >= thresholds.caution) {\n return STATUS_COLORS.caution;\n }\n return STATUS_COLORS.normal;\n}\n\n/**\n * Get color for series by index (wraps around palette)\n *\n * @param index - Series index\n * @param palette - Color palette to use (defaults to Zendir-Astro mixed)\n * @returns Hex color string\n *\n * @example\n * ```tsx\n * // Uses Zendir-Astro fusion palette\n * const color = getSeriesColor(0); // #3E3CFF (Zendir Electric)\n *\n * // Use Zendir-only palette\n * const brandColor = getSeriesColor(0, ASTRO_DATA_VIZ_COLORS.zendir);\n * ```\n */\nexport function getSeriesColor(\n index: number,\n palette = ASTRO_DATA_VIZ_COLORS.mixed,\n): string {\n return palette[index % palette.length];\n}\n\n/**\n * Get Zendir brand color by role\n * Use for consistent branding across the application\n */\nexport function getZendirAccentColor(\n role: keyof typeof ZENDIR_ACCENT_COLORS,\n): string {\n return ZENDIR_ACCENT_COLORS[role];\n}\n\n/**\n * Get appropriate palette based on data type and optional theme\n *\n * @param dataType - Type of data being visualized\n * @param useAstroTheme - If true, uses official AstroUXDS colors; otherwise Zendir colors\n * @returns Appropriate color palette\n *\n * @example\n * ```tsx\n * // For general multi-series data (Zendir brand - default)\n * const colors = getPaletteForDataType('categorical');\n *\n * // For official AstroUXDS colors\n * const astroColors = getPaletteForDataType('categorical', true);\n *\n * // For Zendir brand-forward charts\n * const brandColors = getPaletteForDataType('zendir');\n *\n * // For official AstroUXDS palette explicitly\n * const officialColors = getPaletteForDataType('astro-official');\n *\n * // For status-based data (uses reserved AstroUXDS status colors)\n * const statusColors = getPaletteForDataType('status');\n * ```\n */\nexport function getPaletteForDataType(\n dataType:\n | \"categorical\"\n | \"sequential\"\n | \"diverging\"\n | \"status\"\n | \"zendir\"\n | \"zendirExtended\"\n | \"cool\"\n | \"warm\"\n | \"astro-official\",\n useAstroTheme = false,\n): string[] {\n // If useAstroTheme is true, default to official AstroUXDS colors\n if (\n useAstroTheme &&\n (dataType === \"categorical\" || dataType === \"sequential\")\n ) {\n return dataType === \"sequential\"\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.sequential.teal]\n : [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended];\n }\n\n switch (dataType) {\n case \"astro-official\":\n return [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended]; // Official AstroUXDS colors\n case \"categorical\":\n return ASTRO_DATA_VIZ_COLORS.mixed; // Pure Zendir brand palette\n case \"zendir\":\n return ASTRO_DATA_VIZ_COLORS.zendir; // Core Zendir brand colors\n case \"zendirExtended\":\n return ASTRO_DATA_VIZ_COLORS.zendirExtended; // All Zendir shades\n case \"sequential\":\n return ASTRO_DATA_VIZ_COLORS.sequential.electric; // Zendir Electric gradient\n case \"diverging\":\n return [...ASTRO_DATA_VIZ_COLORS.sequential.electric]\n .reverse()\n .concat(ASTRO_DATA_VIZ_COLORS.sequential.purple);\n case \"status\":\n // AstroUXDS status colors - RESERVED for status indication only\n return [\n STATUS_COLORS.normal,\n STATUS_COLORS.standby,\n STATUS_COLORS.caution,\n STATUS_COLORS.serious,\n STATUS_COLORS.critical,\n ];\n case \"cool\":\n return ASTRO_DATA_VIZ_COLORS.cool;\n case \"warm\":\n return ASTRO_DATA_VIZ_COLORS.warm;\n default:\n return ASTRO_DATA_VIZ_COLORS.mixed;\n }\n}\n\n/**\n * Create gradient for area charts\n */\nexport function createAreaGradient(\n color: string,\n direction: \"vertical\" | \"horizontal\" = \"vertical\",\n): {\n type: \"linear\";\n x: number;\n y: number;\n x2: number;\n y2: number;\n colorStops: Array<{ offset: number; color: string }>;\n} {\n return {\n type: \"linear\",\n x: 0,\n y: direction === \"vertical\" ? 0 : 0,\n x2: direction === \"horizontal\" ? 1 : 0,\n y2: direction === \"vertical\" ? 1 : 0,\n colorStops: [\n { offset: 0, color: `${color}40` },\n { offset: 1, color: `${color}05` },\n ],\n };\n}\n\n/**\n * Format time for axis labels\n */\nexport function formatTimeLabel(\n timestamp: number,\n format: \"short\" | \"medium\" | \"long\" = \"short\",\n): string {\n const date = new Date(timestamp);\n\n switch (format) {\n case \"short\":\n return date.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n case \"medium\":\n return date.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n hour12: false,\n });\n case \"long\":\n return date.toLocaleString([], {\n month: \"short\",\n day: \"numeric\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n });\n }\n}\n\n/**\n * Format number with unit suffix\n */\nexport function formatValueWithUnit(\n value: number,\n unit?: string,\n precision = 2,\n): string {\n const formatted = Number.isInteger(value)\n ? value.toString()\n : value.toFixed(precision);\n return unit ? `${formatted}${unit}` : formatted;\n}\n\n/**\n * Calculate nice axis bounds\n */\nexport function calculateAxisBounds(\n values: number[],\n padding = 0.1,\n): { min: number; max: number } {\n if (values.length === 0) return { min: 0, max: 100 };\n\n const min = Math.min(...values);\n const max = Math.max(...values);\n const range = max - min || 1;\n\n return {\n min: min - range * padding,\n max: max + range * padding,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"theme.js","names":[],"sources":["../../../../src/react/charts/unified/theme.ts"],"sourcesContent":["/**\r\n * @zendir/ui - ECharts Theme Adapter\r\n *\r\n * Converts Astro UX Design System tokens to ECharts theme configuration.\r\n * Supports dark/light modes and colorblind-accessible palettes.\r\n *\r\n * AstroUXD Compliance:\r\n * - Max 11 colors per dataset\r\n * - Colorblind-accessible palette\r\n * - Sentence-case labels\r\n * - 14pt minimum axis text\r\n * - WCAG contrast compliance\r\n *\r\n * @version 2.0.0 - Full Zendir Brand Color Migration\r\n */\r\n\r\nimport type { ThemeTokens, ThemeMode, ThemeVariant } from \"../../theme\";\r\n\r\n// =============================================================================\r\n// AstroUXD Data Visualization Color Palettes\r\n// =============================================================================\r\n\r\n/**\r\n * Zendir Brand Colors (from Brand Guide v2026)\r\n * Core: Electric (#3E3CFF), Purple (#9D70FF), Prussian Blue (#1B2DA0)\r\n * Shades derived using 10%, 20%, 35% white/black overlays per brand guide\r\n */\r\nexport const ZENDIR_BRAND_COLORS = {\r\n // === Core Brand Colors ===\r\n /** Electric - Primary accent (#3E3CFF) */\r\n electric: \"#3E3CFF\",\r\n /** Purple - Secondary accent (#9D70FF) */\r\n purple: \"#9D70FF\",\r\n /** Prussian Blue - Subsections (#1B2DA0) */\r\n prussianBlue: \"#1B2DA0\",\r\n /** Black */\r\n black: \"#000000\",\r\n /** White */\r\n white: \"#FFFFFF\",\r\n\r\n // === Electric Shades (10%, 20%, 35% overlays) ===\r\n electricLighter: \"#9997FF\", // 35% white overlay\r\n electricLight: \"#6B69FF\", // 20% white overlay\r\n electricSoft: \"#5250FF\", // 10% white overlay\r\n electricDark: \"#3836E6\", // 10% black overlay\r\n electricDarker: \"#3230CC\", // 20% black overlay\r\n electricDeep: \"#2926A3\", // 35% black overlay\r\n\r\n // === Purple Shades ===\r\n purpleLighter: \"#C9A8FF\", // 35% white overlay\r\n purpleLight: \"#B48DFF\", // 20% white overlay\r\n purpleSoft: \"#A97FFF\", // 10% white overlay\r\n purpleDark: \"#8E64E6\", // 10% black overlay\r\n purpleDarker: \"#7E5ACC\", // 20% black overlay\r\n purpleDeep: \"#5F4499\", // 35% black overlay\r\n\r\n // === Prussian Blue Shades ===\r\n prussianBlueLighter: \"#4A5FD6\", // 35% white overlay\r\n prussianBlueLight: \"#3548C0\", // 20% white overlay\r\n prussianBlueSoft: \"#2838B0\", // 10% white overlay\r\n prussianBlueDark: \"#182890\", // 10% black overlay\r\n prussianBlueDarker: \"#142380\", // 20% black overlay\r\n prussianBlueDeep: \"#0F1A60\", // 35% black overlay\r\n} as const;\r\n\r\n/**\r\n * Official AstroUXDS Data Visualization Color Palette\r\n * Per https://astrouxds.com/design-tokens/system/#data-viz\r\n *\r\n * These are the OFFICIAL AstroUXDS data visualization colors from the design system.\r\n * Use these for pure AstroUXDS compliance (e.g., 'astro' theme).\r\n *\r\n * @see https://astrouxds.com/design-tokens/system/\r\n */\r\nexport const OFFICIAL_ASTRO_DATA_VIZ_COLORS = {\r\n /** Official AstroUXDS data visualization palette (8 colors) */\r\n primary: [\r\n \"#00c7cb\", // color-data-visualization-1 - Teal 500\r\n \"#938bdb\", // color-data-visualization-2 - Purple 400\r\n \"#4dacff\", // color-data-visualization-3 - Brightblue 500\r\n \"#70dde0\", // color-data-visualization-4 - Teal 300\r\n \"#c9c5ed\", // color-data-visualization-5 - Purple 200\r\n \"#92cbff\", // color-data-visualization-6 - Brightblue 400\r\n \"#a1e9eb\", // color-data-visualization-7 - Teal 200\r\n \"#b7dcff\", // color-data-visualization-8 - Brightblue 300\r\n ],\r\n\r\n /** Extended palette - primary + additional derived colors for 11 max */\r\n extended: [\r\n \"#00c7cb\", // Teal 500 - Primary\r\n \"#938bdb\", // Purple 400 - Secondary\r\n \"#4dacff\", // Brightblue 500\r\n \"#70dde0\", // Teal 300\r\n \"#c9c5ed\", // Purple 200\r\n \"#92cbff\", // Brightblue 400\r\n \"#a1e9eb\", // Teal 200\r\n \"#b7dcff\", // Brightblue 300\r\n \"#009a9d\", // Teal 600 (darker)\r\n \"#756bb8\", // Purple 500 (darker)\r\n \"#2b659b\", // Brightblue 700 (darker)\r\n ],\r\n\r\n /** Sequential palettes for heatmaps using AstroUXDS colors */\r\n sequential: {\r\n /** Teal sequential */\r\n teal: [\"#e5f9f9\", \"#a1e9eb\", \"#70dde0\", \"#00c7cb\", \"#009a9d\", \"#006e70\"],\r\n /** Purple sequential */\r\n purple: [\"#eceaf7\", \"#c9c5ed\", \"#a9a3dc\", \"#938bdb\", \"#756bb8\", \"#5b5299\"],\r\n /** Brightblue sequential */\r\n brightblue: [\r\n \"#e8f4ff\",\r\n \"#b7dcff\",\r\n \"#92cbff\",\r\n \"#4dacff\",\r\n \"#2b659b\",\r\n \"#1c3f5e\",\r\n ],\r\n /** Thermal - Status colors for temperature */\r\n thermal: [\"#00c7cb\", \"#56f000\", \"#fce83a\", \"#ffb302\", \"#ff6b35\", \"#ff3838\"],\r\n },\r\n} as const;\r\n\r\n/**\r\n * Zendir-branded Data Visualization Color Palette\r\n * Uses Zendir brand colors (Electric, Purple, Prussian Blue)\r\n * while maintaining AstroUXDS compliance (max 11 colors, colorblind-accessible)\r\n *\r\n * @see https://www.astrouxds.com/patterns/data-visualization/\r\n */\r\nexport const ASTRO_DATA_VIZ_COLORS = {\r\n /**\r\n * Primary data visualization palette - Pure Zendir Brand\r\n * Uses only Zendir brand colors and their derived shades\r\n * Max 11 colors per AstroUXDS compliance requirement 3.2.1\r\n *\r\n * NOTE: Status colors (green, yellow, red) reserved for status indication only\r\n */\r\n mixed: [\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF - Primary\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF - Secondary\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF - Electric 20% lighter\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF - Purple 20% lighter\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0 - Prussian Blue lighter\r\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF - Electric 35% lighter\r\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF - Purple 35% lighter\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0 - Prussian Blue\r\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC - Electric 20% darker\r\n ZENDIR_BRAND_COLORS.purpleDarker, // #7E5ACC - Purple 20% darker\r\n ZENDIR_BRAND_COLORS.prussianBlueLighter, // #4A5FD6 - Prussian Blue lightest\r\n ],\r\n\r\n /**\r\n * Zendir Brand palette - Full brand spectrum\r\n * Complete set of Zendir brand colors for maximum brand expression\r\n */\r\n zendir: [\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF - Primary\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF - Secondary\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\r\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\r\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\r\n ],\r\n\r\n /**\r\n * Extended Zendir palette - All shades for complex visualizations\r\n * Includes dark and light variants for maximum differentiation\r\n */\r\n zendirExtended: [\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\r\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\r\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\r\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC\r\n ZENDIR_BRAND_COLORS.purpleDarker, // #7E5ACC\r\n ZENDIR_BRAND_COLORS.prussianBlueDarker, // #142380\r\n ],\r\n\r\n /**\r\n * Cool palette - Blues and purples\r\n * Professional Zendir-branded cool tones\r\n */\r\n cool: [\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\r\n ZENDIR_BRAND_COLORS.electricLighter, // #9997FF\r\n ZENDIR_BRAND_COLORS.prussianBlueLighter, // #4A5FD6\r\n ],\r\n\r\n /**\r\n * Warm accent palette - For charts needing warm tones\r\n * Use sparingly - not part of Zendir brand\r\n */\r\n warm: [\r\n \"#ffb302\", // Amber\r\n \"#ff9966\", // Coral\r\n \"#fce83a\", // Yellow\r\n \"#ff6b6b\", // Salmon\r\n \"#f0a000\", // Orange\r\n \"#ffcc80\", // Peach\r\n \"#e0a86c\", // Tan\r\n \"#d4b896\", // Sand\r\n ],\r\n\r\n /**\r\n * Electric-Purple gradient palette (Zendir brand gradient)\r\n * Matches the official brand gradient from Electric to Purple\r\n */\r\n electricPurple: [\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\r\n ZENDIR_BRAND_COLORS.electricSoft, // #5250FF\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n \"#8070FF\", // Blend E→P\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\r\n ZENDIR_BRAND_COLORS.purpleLighter, // #C9A8FF\r\n \"#DCC4FF\", // Lightest\r\n ],\r\n\r\n /**\r\n * Prussian-Electric gradient palette\r\n * Deep to vibrant brand progression\r\n */\r\n prussianElectric: [\r\n ZENDIR_BRAND_COLORS.prussianBlueDarker, // #142380\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\r\n ZENDIR_BRAND_COLORS.electricDarker, // #3230CC\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n ],\r\n\r\n /**\r\n * Full Zendir spectrum gradient\r\n * Prussian Blue → Electric → Purple\r\n */\r\n zendirSpectrum: [\r\n ZENDIR_BRAND_COLORS.prussianBlue, // #1B2DA0\r\n ZENDIR_BRAND_COLORS.prussianBlueLight, // #3548C0\r\n ZENDIR_BRAND_COLORS.electric, // #3E3CFF\r\n ZENDIR_BRAND_COLORS.electricLight, // #6B69FF\r\n ZENDIR_BRAND_COLORS.purple, // #9D70FF\r\n ZENDIR_BRAND_COLORS.purpleLight, // #B48DFF\r\n ],\r\n\r\n /**\r\n * Sequential palettes for heatmaps and gradual data\r\n * Zendir brand-forward gradients\r\n */\r\n sequential: {\r\n /** Zendir Electric sequential - Primary brand gradient */\r\n electric: [\r\n \"#E8E8FF\",\r\n \"#C9C8FF\",\r\n \"#9997FF\",\r\n \"#6B69FF\",\r\n \"#3E3CFF\",\r\n \"#3230CC\",\r\n \"#262499\",\r\n ],\r\n /** Zendir Purple sequential - Secondary brand gradient */\r\n purple: [\r\n \"#F0E8FF\",\r\n \"#DCC4FF\",\r\n \"#C9A8FF\",\r\n \"#B48DFF\",\r\n \"#9D70FF\",\r\n \"#7E5ACC\",\r\n \"#5F4499\",\r\n ],\r\n /** Zendir Prussian Blue sequential - Deep brand gradient */\r\n prussianBlue: [\r\n \"#E8ECFF\",\r\n \"#C5CFF0\",\r\n \"#8A9DD6\",\r\n \"#4A5FD6\",\r\n \"#3548C0\",\r\n \"#1B2DA0\",\r\n \"#0F1A60\",\r\n ],\r\n /** Zendir gradient - Electric → Purple blend */\r\n brand: [\r\n \"#3E3CFF\",\r\n \"#5250FF\",\r\n \"#6B69FF\",\r\n \"#7E70FF\",\r\n \"#9D70FF\",\r\n \"#B48DFF\",\r\n \"#C9A8FF\",\r\n ],\r\n /** Thermal - Status colors for temperature (AstroUXDS compliant) */\r\n thermal: [\"#00C7CB\", \"#56F000\", \"#FCE83A\", \"#FFB302\", \"#FF6B35\", \"#FF3838\"],\r\n },\r\n};\r\n\r\n/**\r\n * AstroUXDS Status Colors (DO NOT MODIFY)\r\n * Per https://www.astrouxds.com/patterns/status-system/\r\n *\r\n * These colors are RESERVED for status indication only.\r\n * Per compliance requirement, do not use status colors for\r\n * non-status data visualization.\r\n *\r\n * @see https://www.astrouxds.com/compliance/astro-design-compliance/\r\n * Rule 3.1.2: Elements indicating state shall use only specified Status Colors\r\n */\r\nexport const STATUS_COLORS = {\r\n normal: \"#56f000\", // Green - System operating normally\r\n standby: \"#2dccff\", // Cyan - Awaiting/standby state\r\n caution: \"#fce83a\", // Yellow - Requires attention\r\n serious: \"#ffb302\", // Orange - Significant issue\r\n critical: \"#ff3838\", // Red - Immediate action required\r\n off: \"#a4abb6\", // Grey - Inactive/off state\r\n} as const;\r\n\r\n/**\r\n * Zendir Accent Colors for UI elements (non-status)\r\n * Use these for interactive elements, highlights, and branding\r\n */\r\nexport const ZENDIR_ACCENT_COLORS = {\r\n /** Primary accent - Zendir Electric */\r\n primary: ZENDIR_BRAND_COLORS.electric,\r\n /** Secondary accent - Zendir Purple */\r\n secondary: ZENDIR_BRAND_COLORS.purple,\r\n /** Tertiary accent - Zendir Prussian Blue */\r\n tertiary: ZENDIR_BRAND_COLORS.prussianBlue,\r\n /** Info accent - Prussian Blue Light */\r\n info: ZENDIR_BRAND_COLORS.prussianBlueLight,\r\n /** Highlight - Electric Light */\r\n highlight: ZENDIR_BRAND_COLORS.electricLight,\r\n} as const;\r\n\r\n// =============================================================================\r\n// Theme Generator\r\n// =============================================================================\r\n\r\nexport interface EChartsTheme {\r\n color: string[];\r\n backgroundColor: string;\r\n textStyle: {\r\n color: string;\r\n fontFamily: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n title: {\r\n textStyle: {\r\n color: string;\r\n fontSize: number;\r\n fontWeight: string;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n subtextStyle: {\r\n color: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n };\r\n legend: {\r\n textStyle: {\r\n color: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n inactiveColor: string;\r\n };\r\n tooltip: {\r\n backgroundColor: string;\r\n borderColor: string;\r\n borderWidth: number;\r\n textStyle: {\r\n color: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n extraCssText: string;\r\n };\r\n axisPointer: {\r\n lineStyle: { color: string; width: number };\r\n crossStyle: { color: string; width: number };\r\n };\r\n xAxis: {\r\n axisLine: { lineStyle: { color: string } };\r\n axisTick: { lineStyle: { color: string } };\r\n axisLabel: {\r\n color: string;\r\n fontSize: number;\r\n fontFamily: string;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n splitLine: { lineStyle: { color: string; type: string } };\r\n nameTextStyle: {\r\n color: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n };\r\n yAxis: {\r\n axisLine: { lineStyle: { color: string } };\r\n axisTick: { lineStyle: { color: string } };\r\n axisLabel: {\r\n color: string;\r\n fontSize: number;\r\n fontFamily: string;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n splitLine: { lineStyle: { color: string; type: string } };\r\n nameTextStyle: {\r\n color: string;\r\n fontSize: number;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n };\r\n series: {\r\n line: { lineStyle: { width: number }; symbolSize: number };\r\n bar: { barMaxWidth: number; itemStyle: { borderRadius: number[] } };\r\n pie: {\r\n itemStyle: { borderColor: string; borderWidth: number };\r\n label?: {\r\n color: string;\r\n textBorderWidth: number;\r\n textBorderColor: string;\r\n fontSize: number;\r\n };\r\n };\r\n scatter: { symbolSize: number };\r\n gauge: { axisLine: { lineStyle: { width: number } } };\r\n };\r\n dataZoom: {\r\n backgroundColor: string;\r\n borderColor: string;\r\n fillerColor: string;\r\n handleColor: string;\r\n textStyle: {\r\n color: string;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n };\r\n visualMap: {\r\n textStyle: {\r\n color: string;\r\n textBorderWidth?: number;\r\n textBorderColor?: string;\r\n };\r\n inRange: { color: string[] };\r\n };\r\n grid: {\r\n borderColor: string;\r\n };\r\n}\r\n\r\n/**\r\n * Generate ECharts theme from Astro tokens\r\n *\r\n * Creates a theme that:\r\n * - Uses official AstroUXDS colors for 'astro' theme variant\r\n * - Uses Zendir brand colors for all other theme variants\r\n * - Maintains AstroUXDS compliance (max 11 colors, 14pt min font)\r\n * - Preserves status colors for their reserved purposes\r\n * - Supports both dark and light modes\r\n *\r\n * @param tokens - Theme tokens from ThemeProvider\r\n * @param mode - Current theme mode ('dark' | 'light')\r\n * @param themeVariant - Optional theme variant to determine color palette\r\n * @returns ECharts theme configuration\r\n */\r\nexport function createAstroEChartsTheme(\r\n tokens: ThemeTokens,\r\n mode: ThemeMode,\r\n themeVariant?: ThemeVariant,\r\n): EChartsTheme {\r\n const isDark = mode === \"dark\";\r\n\r\n // Use official AstroUXDS colors for 'astro' theme, Zendir colors for others\r\n const isAstroTheme = themeVariant === \"astro\";\r\n const dataVizColors = isAstroTheme\r\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended]\r\n : ASTRO_DATA_VIZ_COLORS.mixed;\r\n\r\n return {\r\n // Data visualization color palette\r\n // - 'astro' theme: Official AstroUXDS colors (teal, purple, brightblue)\r\n // - Other themes: Zendir brand colors (electric, purple, prussian blue)\r\n color: dataVizColors,\r\n\r\n // Background\r\n backgroundColor: \"transparent\",\r\n\r\n // Global text style - 14pt minimum per AstroUXDS; no white border on labels\r\n textStyle: {\r\n color: tokens.colors.text.primary,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n fontSize: 14, // AstroUXD minimum requirement\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n\r\n // Title - Grey for readability, AstroUXDS compliant\r\n title: {\r\n textStyle: {\r\n color: tokens.colors.text.secondary, // Grey for headings\r\n fontSize: 14,\r\n fontWeight: \"bold\",\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n subtextStyle: {\r\n color: tokens.colors.text.tertiary,\r\n fontSize: 12,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n },\r\n\r\n // Legend\r\n legend: {\r\n textStyle: {\r\n color: tokens.colors.text.secondary,\r\n fontSize: 12,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n inactiveColor: tokens.colors.text.tertiary,\r\n },\r\n\r\n // Tooltip\r\n tooltip: {\r\n backgroundColor: isDark\r\n ? \"rgba(27, 45, 62, 0.95)\"\r\n : \"rgba(255, 255, 255, 0.95)\",\r\n borderColor: tokens.colors.border.default,\r\n borderWidth: 1,\r\n textStyle: {\r\n color: tokens.colors.text.primary,\r\n fontSize: 12,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n extraCssText: `\r\n border-radius: ${tokens.borderRadius.md};\r\n box-shadow: ${tokens.shadows.lg};\r\n backdrop-filter: blur(8px);\r\n `,\r\n },\r\n\r\n // Axis pointer / crosshair - Subtle grey for clarity\r\n axisPointer: {\r\n lineStyle: {\r\n color: tokens.colors.text.tertiary, // Subtle grey\r\n width: 1,\r\n },\r\n crossStyle: {\r\n color: tokens.colors.text.tertiary, // Subtle grey\r\n width: 1,\r\n },\r\n },\r\n\r\n // X Axis\r\n xAxis: {\r\n axisLine: {\r\n lineStyle: {\r\n color: tokens.colors.border.default,\r\n },\r\n },\r\n axisTick: {\r\n lineStyle: {\r\n color: tokens.colors.border.muted,\r\n },\r\n },\r\n axisLabel: {\r\n color: tokens.colors.text.secondary,\r\n fontSize: 12,\r\n fontFamily: tokens.typography.fontFamily.mono, // Tabular numbers\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n splitLine: {\r\n lineStyle: {\r\n color: tokens.colors.border.muted,\r\n type: \"dashed\",\r\n },\r\n },\r\n nameTextStyle: {\r\n color: tokens.colors.text.secondary,\r\n fontSize: 12,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n },\r\n\r\n // Y Axis\r\n yAxis: {\r\n axisLine: {\r\n lineStyle: {\r\n color: tokens.colors.border.default,\r\n },\r\n },\r\n axisTick: {\r\n lineStyle: {\r\n color: tokens.colors.border.muted,\r\n },\r\n },\r\n axisLabel: {\r\n color: tokens.colors.text.secondary,\r\n fontSize: 12,\r\n fontFamily: tokens.typography.fontFamily.mono, // Tabular numbers\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n splitLine: {\r\n lineStyle: {\r\n color: tokens.colors.border.muted,\r\n type: \"dashed\",\r\n },\r\n },\r\n nameTextStyle: {\r\n color: tokens.colors.text.secondary,\r\n fontSize: 12,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n },\r\n\r\n // Series defaults\r\n series: {\r\n line: {\r\n lineStyle: {\r\n width: 2,\r\n },\r\n symbolSize: 6,\r\n },\r\n bar: {\r\n barMaxWidth: 40,\r\n itemStyle: {\r\n borderRadius: [4, 4, 0, 0],\r\n },\r\n },\r\n pie: {\r\n itemStyle: {\r\n borderColor: tokens.colors.background.surface,\r\n borderWidth: 2,\r\n },\r\n label: {\r\n color: tokens.colors.text.primary,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n fontSize: 12,\r\n },\r\n },\r\n scatter: {\r\n symbolSize: 10,\r\n },\r\n gauge: {\r\n axisLine: {\r\n lineStyle: {\r\n width: 10,\r\n },\r\n },\r\n },\r\n },\r\n\r\n // Data zoom - Theme-aware interactive controls\r\n dataZoom: {\r\n backgroundColor: isDark\r\n ? \"rgba(27, 45, 62, 0.5)\"\r\n : \"rgba(234, 238, 244, 0.5)\",\r\n borderColor: tokens.colors.border.muted,\r\n // Astro theme uses brightblue, others use Zendir Electric\r\n fillerColor: isAstroTheme\r\n ? \"rgba(77, 172, 255, 0.15)\" // AstroUXDS Brightblue\r\n : \"rgba(62, 60, 255, 0.15)\", // Zendir Electric\r\n handleColor: isAstroTheme\r\n ? \"#4dacff\" // AstroUXDS Brightblue 500\r\n : ZENDIR_BRAND_COLORS.electric, // Zendir Electric\r\n textStyle: {\r\n color: tokens.colors.text.secondary,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n },\r\n\r\n // Visual map (for heatmaps) - Theme-aware sequential palette\r\n visualMap: {\r\n textStyle: {\r\n color: tokens.colors.text.secondary,\r\n textBorderWidth: 0,\r\n textBorderColor: \"transparent\",\r\n },\r\n inRange: {\r\n // Astro theme uses official teal gradient, others use Zendir Electric\r\n color: isAstroTheme\r\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.sequential.teal]\r\n : ASTRO_DATA_VIZ_COLORS.sequential.electric,\r\n },\r\n },\r\n\r\n // Grid\r\n grid: {\r\n borderColor: tokens.colors.border.muted,\r\n },\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Utility Functions\r\n// =============================================================================\r\n\r\n/**\r\n * Get status color from value and thresholds\r\n */\r\nexport function getStatusColor(\r\n value: number,\r\n thresholds: {\r\n critical?: number;\r\n serious?: number;\r\n caution?: number;\r\n normal?: number;\r\n },\r\n): string {\r\n if (thresholds.critical !== undefined && value >= thresholds.critical) {\r\n return STATUS_COLORS.critical;\r\n }\r\n if (thresholds.serious !== undefined && value >= thresholds.serious) {\r\n return STATUS_COLORS.serious;\r\n }\r\n if (thresholds.caution !== undefined && value >= thresholds.caution) {\r\n return STATUS_COLORS.caution;\r\n }\r\n return STATUS_COLORS.normal;\r\n}\r\n\r\n/**\r\n * Get color for series by index (wraps around palette)\r\n *\r\n * @param index - Series index\r\n * @param palette - Color palette to use (defaults to Zendir-Astro mixed)\r\n * @returns Hex color string\r\n *\r\n * @example\r\n * ```tsx\r\n * // Uses Zendir-Astro fusion palette\r\n * const color = getSeriesColor(0); // #3E3CFF (Zendir Electric)\r\n *\r\n * // Use Zendir-only palette\r\n * const brandColor = getSeriesColor(0, ASTRO_DATA_VIZ_COLORS.zendir);\r\n * ```\r\n */\r\nexport function getSeriesColor(\r\n index: number,\r\n palette = ASTRO_DATA_VIZ_COLORS.mixed,\r\n): string {\r\n return palette[index % palette.length];\r\n}\r\n\r\n/**\r\n * Get Zendir brand color by role\r\n * Use for consistent branding across the application\r\n */\r\nexport function getZendirAccentColor(\r\n role: keyof typeof ZENDIR_ACCENT_COLORS,\r\n): string {\r\n return ZENDIR_ACCENT_COLORS[role];\r\n}\r\n\r\n/**\r\n * Get appropriate palette based on data type and optional theme\r\n *\r\n * @param dataType - Type of data being visualized\r\n * @param useAstroTheme - If true, uses official AstroUXDS colors; otherwise Zendir colors\r\n * @returns Appropriate color palette\r\n *\r\n * @example\r\n * ```tsx\r\n * // For general multi-series data (Zendir brand - default)\r\n * const colors = getPaletteForDataType('categorical');\r\n *\r\n * // For official AstroUXDS colors\r\n * const astroColors = getPaletteForDataType('categorical', true);\r\n *\r\n * // For Zendir brand-forward charts\r\n * const brandColors = getPaletteForDataType('zendir');\r\n *\r\n * // For official AstroUXDS palette explicitly\r\n * const officialColors = getPaletteForDataType('astro-official');\r\n *\r\n * // For status-based data (uses reserved AstroUXDS status colors)\r\n * const statusColors = getPaletteForDataType('status');\r\n * ```\r\n */\r\nexport function getPaletteForDataType(\r\n dataType:\r\n | \"categorical\"\r\n | \"sequential\"\r\n | \"diverging\"\r\n | \"status\"\r\n | \"zendir\"\r\n | \"zendirExtended\"\r\n | \"cool\"\r\n | \"warm\"\r\n | \"astro-official\",\r\n useAstroTheme = false,\r\n): string[] {\r\n // If useAstroTheme is true, default to official AstroUXDS colors\r\n if (\r\n useAstroTheme &&\r\n (dataType === \"categorical\" || dataType === \"sequential\")\r\n ) {\r\n return dataType === \"sequential\"\r\n ? [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.sequential.teal]\r\n : [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended];\r\n }\r\n\r\n switch (dataType) {\r\n case \"astro-official\":\r\n return [...OFFICIAL_ASTRO_DATA_VIZ_COLORS.extended]; // Official AstroUXDS colors\r\n case \"categorical\":\r\n return ASTRO_DATA_VIZ_COLORS.mixed; // Pure Zendir brand palette\r\n case \"zendir\":\r\n return ASTRO_DATA_VIZ_COLORS.zendir; // Core Zendir brand colors\r\n case \"zendirExtended\":\r\n return ASTRO_DATA_VIZ_COLORS.zendirExtended; // All Zendir shades\r\n case \"sequential\":\r\n return ASTRO_DATA_VIZ_COLORS.sequential.electric; // Zendir Electric gradient\r\n case \"diverging\":\r\n return [...ASTRO_DATA_VIZ_COLORS.sequential.electric]\r\n .reverse()\r\n .concat(ASTRO_DATA_VIZ_COLORS.sequential.purple);\r\n case \"status\":\r\n // AstroUXDS status colors - RESERVED for status indication only\r\n return [\r\n STATUS_COLORS.normal,\r\n STATUS_COLORS.standby,\r\n STATUS_COLORS.caution,\r\n STATUS_COLORS.serious,\r\n STATUS_COLORS.critical,\r\n ];\r\n case \"cool\":\r\n return ASTRO_DATA_VIZ_COLORS.cool;\r\n case \"warm\":\r\n return ASTRO_DATA_VIZ_COLORS.warm;\r\n default:\r\n return ASTRO_DATA_VIZ_COLORS.mixed;\r\n }\r\n}\r\n\r\n/**\r\n * Create gradient for area charts\r\n */\r\nexport function createAreaGradient(\r\n color: string,\r\n direction: \"vertical\" | \"horizontal\" = \"vertical\",\r\n): {\r\n type: \"linear\";\r\n x: number;\r\n y: number;\r\n x2: number;\r\n y2: number;\r\n colorStops: Array<{ offset: number; color: string }>;\r\n} {\r\n return {\r\n type: \"linear\",\r\n x: 0,\r\n y: direction === \"vertical\" ? 0 : 0,\r\n x2: direction === \"horizontal\" ? 1 : 0,\r\n y2: direction === \"vertical\" ? 1 : 0,\r\n colorStops: [\r\n { offset: 0, color: `${color}40` },\r\n { offset: 1, color: `${color}05` },\r\n ],\r\n };\r\n}\r\n\r\n/**\r\n * Format time for axis labels\r\n */\r\nexport function formatTimeLabel(\r\n timestamp: number,\r\n format: \"short\" | \"medium\" | \"long\" = \"short\",\r\n): string {\r\n const date = new Date(timestamp);\r\n\r\n switch (format) {\r\n case \"short\":\r\n return date.toLocaleTimeString([], {\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n hour12: false,\r\n });\r\n case \"medium\":\r\n return date.toLocaleTimeString([], {\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n hour12: false,\r\n });\r\n case \"long\":\r\n return date.toLocaleString([], {\r\n month: \"short\",\r\n day: \"numeric\",\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n hour12: false,\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Format number with unit suffix\r\n */\r\nexport function formatValueWithUnit(\r\n value: number,\r\n unit?: string,\r\n precision = 2,\r\n): string {\r\n const formatted = Number.isInteger(value)\r\n ? value.toString()\r\n : value.toFixed(precision);\r\n return unit ? `${formatted}${unit}` : formatted;\r\n}\r\n\r\n/**\r\n * Calculate nice axis bounds\r\n */\r\nexport function calculateAxisBounds(\r\n values: number[],\r\n padding = 0.1,\r\n): { min: number; max: number } {\r\n if (values.length === 0) return { min: 0, max: 100 };\r\n\r\n const min = Math.min(...values);\r\n const max = Math.max(...values);\r\n const range = max - min || 1;\r\n\r\n return {\r\n min: min - range * padding,\r\n max: max + range * padding,\r\n };\r\n}\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useChartStream.js","names":[],"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: (\n seriesIndex: number,\n points: (TimeSeriesPoint | DataPoint)[],\n ) => 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)[]>>(\n new Map(),\n );\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(\n (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 },\n [isPaused, bufferSize, flush],\n );\n\n // Push batch\n const pushBatch = useCallback(\n (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 },\n [isPaused, flush],\n );\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(\n Math.floor((i + 3) * bucketSize) + 1,\n data.length,\n );\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((aX - avgX) * (y - aY) - (aX - x) * (avgY - aY));\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\"\n ? point.time\n : new Date(point.time).getTime(),\n point.value,\n ];\n }\n return [typeof point.x === \"number\" ? point.x : 0, point.y];\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?: (\n data: unknown,\n ) => { 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] =\n 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(\n connect,\n reconnectDelay * reconnectCountRef.current,\n );\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(\n data: unknown,\n): { 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"],"mappings":"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"}
1
+ {"version":3,"file":"useChartStream.js","names":[],"sources":["../../../../src/react/charts/unified/useChartStream.ts"],"sourcesContent":["/**\r\n * @zendir/ui - Real-time Chart Streaming Hook\r\n *\r\n * Provides efficient real-time data streaming for charts.\r\n * Handles buffering, throttling, and memory management.\r\n */\r\n\r\nimport { useRef, useCallback, useEffect, useState } from \"react\";\r\nimport type { AstroChartHandle } from \"./AstroChart\";\r\nimport type { TimeSeriesPoint, DataPoint } from \"./types\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport interface StreamOptions {\r\n /** Maximum data points to keep */\r\n maxPoints?: number;\r\n /** Time window in milliseconds (alternative to maxPoints) */\r\n timeWindow?: number;\r\n /** Throttle interval in milliseconds */\r\n throttleMs?: number;\r\n /** Buffer size before flushing */\r\n bufferSize?: number;\r\n /** Enable data decimation for large datasets */\r\n decimate?: boolean;\r\n /** Decimation threshold (start decimating above this count) */\r\n decimateThreshold?: number;\r\n}\r\n\r\nexport interface StreamController {\r\n /** Push a single data point */\r\n push: (seriesIndex: number, point: TimeSeriesPoint | DataPoint) => void;\r\n /** Push multiple data points */\r\n pushBatch: (\r\n seriesIndex: number,\r\n points: (TimeSeriesPoint | DataPoint)[],\r\n ) => void;\r\n /** Clear all data */\r\n clear: () => void;\r\n /** Pause streaming */\r\n pause: () => void;\r\n /** Resume streaming */\r\n resume: () => void;\r\n /** Get current state */\r\n isPaused: boolean;\r\n /** Get current point count */\r\n pointCount: number;\r\n}\r\n\r\n// =============================================================================\r\n// Hook Implementation\r\n// =============================================================================\r\n\r\nexport function useChartStream(\r\n chartRef: React.RefObject<AstroChartHandle>,\r\n options: StreamOptions = {},\r\n): StreamController {\r\n const {\r\n maxPoints: _maxPoints = 1000,\r\n timeWindow: _timeWindow,\r\n throttleMs = 16, // ~60fps\r\n bufferSize = 10,\r\n decimate = true,\r\n decimateThreshold = 5000,\r\n } = options;\r\n\r\n const bufferRef = useRef<Map<number, (TimeSeriesPoint | DataPoint)[]>>(\r\n new Map(),\r\n );\r\n const lastFlushRef = useRef<number>(0);\r\n const pointCountRef = useRef<number>(0);\r\n const [isPaused, setIsPaused] = useState(false);\r\n const animationFrameRef = useRef<number>();\r\n\r\n // Flush buffer to chart\r\n const flush = useCallback(() => {\r\n const chart = chartRef.current;\r\n if (!chart || isPaused) return;\r\n\r\n const now = Date.now();\r\n if (now - lastFlushRef.current < throttleMs) {\r\n // Schedule next flush\r\n animationFrameRef.current = requestAnimationFrame(flush);\r\n return;\r\n }\r\n\r\n bufferRef.current.forEach((points, seriesIndex) => {\r\n if (points.length === 0) return;\r\n\r\n // Apply decimation if needed\r\n let processedPoints = points;\r\n if (decimate && pointCountRef.current > decimateThreshold) {\r\n processedPoints = decimateData(points, Math.ceil(points.length / 2));\r\n }\r\n\r\n // Append each point\r\n processedPoints.forEach((point) => {\r\n chart.appendData(seriesIndex, point);\r\n });\r\n\r\n pointCountRef.current += processedPoints.length;\r\n });\r\n\r\n // Clear buffer\r\n bufferRef.current.clear();\r\n lastFlushRef.current = now;\r\n }, [chartRef, isPaused, throttleMs, decimate, decimateThreshold]);\r\n\r\n // Push single point\r\n const push = useCallback(\r\n (seriesIndex: number, point: TimeSeriesPoint | DataPoint) => {\r\n if (isPaused) return;\r\n\r\n let buffer = bufferRef.current.get(seriesIndex);\r\n if (!buffer) {\r\n buffer = [];\r\n bufferRef.current.set(seriesIndex, buffer);\r\n }\r\n\r\n buffer.push(point);\r\n\r\n // Flush if buffer is full\r\n if (buffer.length >= bufferSize) {\r\n flush();\r\n } else if (!animationFrameRef.current) {\r\n // Schedule flush\r\n animationFrameRef.current = requestAnimationFrame(() => {\r\n animationFrameRef.current = undefined;\r\n flush();\r\n });\r\n }\r\n },\r\n [isPaused, bufferSize, flush],\r\n );\r\n\r\n // Push batch\r\n const pushBatch = useCallback(\r\n (seriesIndex: number, points: (TimeSeriesPoint | DataPoint)[]) => {\r\n if (isPaused || points.length === 0) return;\r\n\r\n let buffer = bufferRef.current.get(seriesIndex);\r\n if (!buffer) {\r\n buffer = [];\r\n bufferRef.current.set(seriesIndex, buffer);\r\n }\r\n\r\n buffer.push(...points);\r\n flush();\r\n },\r\n [isPaused, flush],\r\n );\r\n\r\n // Clear all data\r\n const clear = useCallback(() => {\r\n bufferRef.current.clear();\r\n pointCountRef.current = 0;\r\n chartRef.current?.clearData();\r\n }, [chartRef]);\r\n\r\n // Pause/resume\r\n const pause = useCallback(() => setIsPaused(true), []);\r\n const resume = useCallback(() => setIsPaused(false), []);\r\n\r\n // Cleanup\r\n useEffect(() => {\r\n return () => {\r\n if (animationFrameRef.current) {\r\n cancelAnimationFrame(animationFrameRef.current);\r\n }\r\n };\r\n }, []);\r\n\r\n return {\r\n push,\r\n pushBatch,\r\n clear,\r\n pause,\r\n resume,\r\n isPaused,\r\n pointCount: pointCountRef.current,\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Data Decimation\r\n// =============================================================================\r\n\r\n/**\r\n * Largest Triangle Three Buckets (LTTB) algorithm for data decimation\r\n * Preserves visual shape while reducing point count\r\n */\r\nfunction decimateData<T extends TimeSeriesPoint | DataPoint>(\r\n data: T[],\r\n threshold: number,\r\n): T[] {\r\n if (data.length <= threshold) return data;\r\n\r\n const result: T[] = [];\r\n const bucketSize = (data.length - 2) / (threshold - 2);\r\n\r\n // Always include first point\r\n result.push(data[0]);\r\n\r\n let a = 0; // Previous selected point\r\n\r\n for (let i = 0; i < threshold - 2; i++) {\r\n // Calculate bucket range\r\n const bucketStart = Math.floor((i + 1) * bucketSize) + 1;\r\n const bucketEnd = Math.floor((i + 2) * bucketSize) + 1;\r\n const bucketLength = Math.min(bucketEnd, data.length) - bucketStart;\r\n\r\n // Calculate average point in next bucket\r\n let avgX = 0;\r\n let avgY = 0;\r\n const nextBucketStart = Math.floor((i + 2) * bucketSize) + 1;\r\n const nextBucketEnd = Math.min(\r\n Math.floor((i + 3) * bucketSize) + 1,\r\n data.length,\r\n );\r\n const nextBucketLength = nextBucketEnd - nextBucketStart;\r\n\r\n for (let j = nextBucketStart; j < nextBucketEnd; j++) {\r\n const point = data[j];\r\n const [x, y] = getPointXY(point);\r\n avgX += x;\r\n avgY += y;\r\n }\r\n avgX /= nextBucketLength || 1;\r\n avgY /= nextBucketLength || 1;\r\n\r\n // Find point with largest triangle area\r\n const [aX, aY] = getPointXY(data[a]);\r\n let maxArea = -1;\r\n let maxIdx = bucketStart;\r\n\r\n for (let j = bucketStart; j < bucketStart + bucketLength; j++) {\r\n const point = data[j];\r\n const [x, y] = getPointXY(point);\r\n\r\n // Triangle area\r\n const area = Math.abs((aX - avgX) * (y - aY) - (aX - x) * (avgY - aY));\r\n\r\n if (area > maxArea) {\r\n maxArea = area;\r\n maxIdx = j;\r\n }\r\n }\r\n\r\n result.push(data[maxIdx]);\r\n a = maxIdx;\r\n }\r\n\r\n // Always include last point\r\n result.push(data[data.length - 1]);\r\n\r\n return result;\r\n}\r\n\r\nfunction getPointXY(point: TimeSeriesPoint | DataPoint): [number, number] {\r\n if (\"time\" in point) {\r\n return [\r\n typeof point.time === \"number\"\r\n ? point.time\r\n : new Date(point.time).getTime(),\r\n point.value,\r\n ];\r\n }\r\n return [typeof point.x === \"number\" ? point.x : 0, point.y];\r\n}\r\n\r\n// =============================================================================\r\n// WebSocket Stream Hook\r\n// =============================================================================\r\n\r\nexport interface WebSocketStreamOptions extends StreamOptions {\r\n /** WebSocket URL */\r\n url: string;\r\n /** Reconnect on disconnect */\r\n reconnect?: boolean;\r\n /** Reconnect delay in milliseconds */\r\n reconnectDelay?: number;\r\n /** Maximum reconnect attempts */\r\n maxReconnects?: number;\r\n /** Message parser */\r\n parseMessage?: (\r\n data: unknown,\r\n ) => { seriesIndex: number; point: TimeSeriesPoint | DataPoint } | null;\r\n}\r\n\r\nexport interface WebSocketStreamController extends StreamController {\r\n /** WebSocket connection state */\r\n connectionState: \"connecting\" | \"connected\" | \"disconnected\" | \"error\";\r\n /** Manually connect */\r\n connect: () => void;\r\n /** Manually disconnect */\r\n disconnect: () => void;\r\n}\r\n\r\nexport function useWebSocketStream(\r\n chartRef: React.RefObject<AstroChartHandle>,\r\n options: WebSocketStreamOptions,\r\n): WebSocketStreamController {\r\n const {\r\n url,\r\n reconnect = true,\r\n reconnectDelay = 1000,\r\n maxReconnects = 10,\r\n parseMessage,\r\n ...streamOptions\r\n } = options;\r\n\r\n const wsRef = useRef<WebSocket | null>(null);\r\n const reconnectCountRef = useRef(0);\r\n const reconnectTimerRef = useRef<ReturnType<typeof setTimeout>>();\r\n const [connectionState, setConnectionState] =\r\n useState<WebSocketStreamController[\"connectionState\"]>(\"disconnected\");\r\n\r\n const stream = useChartStream(chartRef, streamOptions);\r\n\r\n const connect = useCallback(() => {\r\n if (wsRef.current?.readyState === WebSocket.OPEN) return;\r\n\r\n setConnectionState(\"connecting\");\r\n const ws = new WebSocket(url);\r\n\r\n ws.onopen = () => {\r\n setConnectionState(\"connected\");\r\n reconnectCountRef.current = 0;\r\n };\r\n\r\n ws.onmessage = (event) => {\r\n try {\r\n const data = JSON.parse(event.data);\r\n const parsed = parseMessage\r\n ? parseMessage(data)\r\n : defaultParseMessage(data);\r\n\r\n if (parsed) {\r\n stream.push(parsed.seriesIndex, parsed.point);\r\n }\r\n } catch (e) {\r\n if (import.meta.env.DEV) {\r\n console.error(\"Failed to parse WebSocket message:\", e);\r\n }\r\n }\r\n };\r\n\r\n ws.onerror = () => {\r\n setConnectionState(\"error\");\r\n };\r\n\r\n ws.onclose = () => {\r\n setConnectionState(\"disconnected\");\r\n\r\n if (reconnect && reconnectCountRef.current < maxReconnects) {\r\n reconnectCountRef.current++;\r\n reconnectTimerRef.current = setTimeout(\r\n connect,\r\n reconnectDelay * reconnectCountRef.current,\r\n );\r\n }\r\n };\r\n\r\n wsRef.current = ws;\r\n }, [url, reconnect, reconnectDelay, maxReconnects, parseMessage, stream]);\r\n\r\n const disconnect = useCallback(() => {\r\n clearTimeout(reconnectTimerRef.current);\r\n wsRef.current?.close();\r\n wsRef.current = null;\r\n }, []);\r\n\r\n // Auto-connect on mount\r\n useEffect(() => {\r\n connect();\r\n return disconnect;\r\n }, [connect, disconnect]);\r\n\r\n return {\r\n ...stream,\r\n connectionState,\r\n connect,\r\n disconnect,\r\n };\r\n}\r\n\r\nfunction defaultParseMessage(\r\n data: unknown,\r\n): { seriesIndex: number; point: TimeSeriesPoint } | null {\r\n if (typeof data !== \"object\" || data === null) return null;\r\n\r\n const obj = data as Record<string, unknown>;\r\n\r\n if (\"time\" in obj && \"value\" in obj) {\r\n return {\r\n seriesIndex: typeof obj.seriesIndex === \"number\" ? obj.seriesIndex : 0,\r\n point: {\r\n time: obj.time as number,\r\n value: obj.value as number,\r\n },\r\n };\r\n }\r\n\r\n return null;\r\n}\r\n"],"mappings":";;;;;;;;AAsDA,SAAgB,eACd,UACA,UAAyB,CAAC,GACR;CAClB,MAAM,EACJ,WAAW,aAAa,KACxB,YAAY,aACZ,aAAa,IACb,aAAa,IACb,WAAW,MACX,oBAAoB,QAClB;CAEJ,MAAM,YAAY,uBAChB,IAAI,IAAI,CACV;CACA,MAAM,eAAe,OAAe,CAAC;CACrC,MAAM,gBAAgB,OAAe,CAAC;CACtC,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAC9C,MAAM,oBAAoB,OAAe;CAGzC,MAAM,QAAQ,kBAAkB;EAC9B,MAAM,QAAQ,SAAS;EACvB,IAAI,CAAC,SAAS,UAAU;EAExB,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,MAAM,aAAa,UAAU,YAAY;GAE3C,kBAAkB,UAAU,sBAAsB,KAAK;GACvD;EACF;EAEA,UAAU,QAAQ,SAAS,QAAQ,gBAAgB;GACjD,IAAI,OAAO,WAAW,GAAG;GAGzB,IAAI,kBAAkB;GACtB,IAAI,YAAY,cAAc,UAAU,mBACtC,kBAAkB,aAAa,QAAQ,KAAK,KAAK,OAAO,SAAS,CAAC,CAAC;GAIrE,gBAAgB,SAAS,UAAU;IACjC,MAAM,WAAW,aAAa,KAAK;GACrC,CAAC;GAED,cAAc,WAAW,gBAAgB;EAC3C,CAAC;EAGD,UAAU,QAAQ,MAAM;EACxB,aAAa,UAAU;CACzB,GAAG;EAAC;EAAU;EAAU;EAAY;EAAU;CAAiB,CAAC;CAGhE,MAAM,OAAO,aACV,aAAqB,UAAuC;EAC3D,IAAI,UAAU;EAEd,IAAI,SAAS,UAAU,QAAQ,IAAI,WAAW;EAC9C,IAAI,CAAC,QAAQ;GACX,SAAS,CAAC;GACV,UAAU,QAAQ,IAAI,aAAa,MAAM;EAC3C;EAEA,OAAO,KAAK,KAAK;EAGjB,IAAI,OAAO,UAAU,YACnB,MAAM;OACD,IAAI,CAAC,kBAAkB,SAE5B,kBAAkB,UAAU,4BAA4B;GACtD,kBAAkB,UAAU,KAAA;GAC5B,MAAM;EACR,CAAC;CAEL,GACA;EAAC;EAAU;EAAY;CAAK,CAC9B;CAGA,MAAM,YAAY,aACf,aAAqB,WAA4C;EAChE,IAAI,YAAY,OAAO,WAAW,GAAG;EAErC,IAAI,SAAS,UAAU,QAAQ,IAAI,WAAW;EAC9C,IAAI,CAAC,QAAQ;GACX,SAAS,CAAC;GACV,UAAU,QAAQ,IAAI,aAAa,MAAM;EAC3C;EAEA,OAAO,KAAK,GAAG,MAAM;EACrB,MAAM;CACR,GACA,CAAC,UAAU,KAAK,CAClB;CAGA,MAAM,QAAQ,kBAAkB;EAC9B,UAAU,QAAQ,MAAM;EACxB,cAAc,UAAU;EACxB,SAAS,SAAS,UAAU;CAC9B,GAAG,CAAC,QAAQ,CAAC;CAGb,MAAM,QAAQ,kBAAkB,YAAY,IAAI,GAAG,CAAC,CAAC;CACrD,MAAM,SAAS,kBAAkB,YAAY,KAAK,GAAG,CAAC,CAAC;CAGvD,gBAAgB;EACd,aAAa;GACX,IAAI,kBAAkB,SACpB,qBAAqB,kBAAkB,OAAO;EAElD;CACF,GAAG,CAAC,CAAC;CAEL,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,YAAY,cAAc;CAC5B;AACF;;;;;AAUA,SAAS,aACP,MACA,WACK;CACL,IAAI,KAAK,UAAU,WAAW,OAAO;CAErC,MAAM,SAAc,CAAC;CACrB,MAAM,cAAc,KAAK,SAAS,MAAM,YAAY;CAGpD,OAAO,KAAK,KAAK,EAAE;CAEnB,IAAI,IAAI;CAER,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,GAAG,KAAK;EAEtC,MAAM,cAAc,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;EACvD,MAAM,YAAY,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;EACrD,MAAM,eAAe,KAAK,IAAI,WAAW,KAAK,MAAM,IAAI;EAGxD,IAAI,OAAO;EACX,IAAI,OAAO;EACX,MAAM,kBAAkB,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI;EAC3D,MAAM,gBAAgB,KAAK,IACzB,KAAK,OAAO,IAAI,KAAK,UAAU,IAAI,GACnC,KAAK,MACP;EACA,MAAM,mBAAmB,gBAAgB;EAEzC,KAAK,IAAI,IAAI,iBAAiB,IAAI,eAAe,KAAK;GACpD,MAAM,QAAQ,KAAK;GACnB,MAAM,CAAC,GAAG,KAAK,WAAW,KAAK;GAC/B,QAAQ;GACR,QAAQ;EACV;EACA,QAAQ,oBAAoB;EAC5B,QAAQ,oBAAoB;EAG5B,MAAM,CAAC,IAAI,MAAM,WAAW,KAAK,EAAE;EACnC,IAAI,UAAU;EACd,IAAI,SAAS;EAEb,KAAK,IAAI,IAAI,aAAa,IAAI,cAAc,cAAc,KAAK;GAC7D,MAAM,QAAQ,KAAK;GACnB,MAAM,CAAC,GAAG,KAAK,WAAW,KAAK;GAG/B,MAAM,OAAO,KAAK,KAAK,KAAK,SAAS,IAAI,OAAO,KAAK,MAAM,OAAO,GAAG;GAErE,IAAI,OAAO,SAAS;IAClB,UAAU;IACV,SAAS;GACX;EACF;EAEA,OAAO,KAAK,KAAK,OAAO;EACxB,IAAI;CACN;CAGA,OAAO,KAAK,KAAK,KAAK,SAAS,EAAE;CAEjC,OAAO;AACT;AAEA,SAAS,WAAW,OAAsD;CACxE,IAAI,UAAU,OACZ,OAAO,CACL,OAAO,MAAM,SAAS,WAClB,MAAM,OACN,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,QAAQ,GACjC,MAAM,KACR;CAEF,OAAO,CAAC,OAAO,MAAM,MAAM,WAAW,MAAM,IAAI,GAAG,MAAM,CAAC;AAC5D;AA8BA,SAAgB,mBACd,UACA,SAC2B;CAC3B,MAAM,EACJ,KACA,YAAY,MACZ,iBAAiB,KACjB,gBAAgB,IAChB,cACA,GAAG,kBACD;CAEJ,MAAM,QAAQ,OAAyB,IAAI;CAC3C,MAAM,oBAAoB,OAAO,CAAC;CAClC,MAAM,oBAAoB,OAAsC;CAChE,MAAM,CAAC,iBAAiB,sBACtB,SAAuD,cAAc;CAEvE,MAAM,SAAS,eAAe,UAAU,aAAa;CAErD,MAAM,UAAU,kBAAkB;EAChC,IAAI,MAAM,SAAS,eAAe,UAAU,MAAM;EAElD,mBAAmB,YAAY;EAC/B,MAAM,KAAK,IAAI,UAAU,GAAG;EAE5B,GAAG,eAAe;GAChB,mBAAmB,WAAW;GAC9B,kBAAkB,UAAU;EAC9B;EAEA,GAAG,aAAa,UAAU;GACxB,IAAI;IACF,MAAM,OAAO,KAAK,MAAM,MAAM,IAAI;IAClC,MAAM,SAAS,eACX,aAAa,IAAI,IACjB,oBAAoB,IAAI;IAE5B,IAAI,QACF,OAAO,KAAK,OAAO,aAAa,OAAO,KAAK;GAEhD,SAAS,GAAG,CAIZ;EACF;EAEA,GAAG,gBAAgB;GACjB,mBAAmB,OAAO;EAC5B;EAEA,GAAG,gBAAgB;GACjB,mBAAmB,cAAc;GAEjC,IAAI,aAAa,kBAAkB,UAAU,eAAe;IAC1D,kBAAkB;IAClB,kBAAkB,UAAU,WAC1B,SACA,iBAAiB,kBAAkB,OACrC;GACF;EACF;EAEA,MAAM,UAAU;CAClB,GAAG;EAAC;EAAK;EAAW;EAAgB;EAAe;EAAc;CAAM,CAAC;CAExE,MAAM,aAAa,kBAAkB;EACnC,aAAa,kBAAkB,OAAO;EACtC,MAAM,SAAS,MAAM;EACrB,MAAM,UAAU;CAClB,GAAG,CAAC,CAAC;CAGL,gBAAgB;EACd,QAAQ;EACR,OAAO;CACT,GAAG,CAAC,SAAS,UAAU,CAAC;CAExB,OAAO;EACL,GAAG;EACH;EACA;EACA;CACF;AACF;AAEA,SAAS,oBACP,MACwD;CACxD,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM,OAAO;CAEtD,MAAM,MAAM;CAEZ,IAAI,UAAU,OAAO,WAAW,KAC9B,OAAO;EACL,aAAa,OAAO,IAAI,gBAAgB,WAAW,IAAI,cAAc;EACrE,OAAO;GACL,MAAM,IAAI;GACV,OAAO,IAAI;EACb;CACF;CAGF,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"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: {\n fileId: string;\n }) => Promise<{ downloadUrl: string }>;\n requestDisplayMode?: (options: {\n mode: \"inline\" | \"pip\" | \"fullscreen\";\n }) => 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>(\n key: K,\n): OpenAiGlobals[K] {\n const getSnapshot = useCallback(() => {\n return window.openai?.[key];\n }, [key]);\n\n const subscribe = useCallback(\n (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, {\n passive: true,\n });\n return () =>\n window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);\n },\n [key],\n );\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\"\n ? (defaultState as () => T)()\n : 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 =\n 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 ? {\n background: \"#09090b\",\n surface: \"#18181b\",\n surfaceHover: \"#27272a\",\n border: \"#3f3f46\",\n text: \"#fafafa\",\n textMuted: \"#a1a1aa\",\n accent: \"#22d3ee\",\n }\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(\n async (newMode: \"inline\" | \"pip\" | \"fullscreen\") => {\n await window.openai?.requestDisplayMode?.({ mode: newMode });\n },\n [],\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>(\n name: string,\n args: Record<string, unknown>,\n ) => 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(\n 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 );\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 () =>\n window.removeEventListener(\n \"openai:set_globals\",\n handler as EventListener,\n );\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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/react/chatgpt/index.ts"],"sourcesContent":["/**\r\n * ChatGPT Apps SDK Integration\r\n *\r\n * Hooks and utilities for integrating Zendir UI components with ChatGPT's\r\n * window.openai API. Based on: https://developers.openai.com/apps-sdk/build/chatgpt-ui/\r\n *\r\n * Key capabilities:\r\n * - window.openai.toolInput / toolOutput - Tool data from MCP server\r\n * - window.openai.widgetState / setWidgetState - Persistent UI state\r\n * - window.openai.callTool - Invoke other MCP tools from widget\r\n * - window.openai.sendFollowUpMessage - Insert user messages\r\n * - window.openai.theme - Light/dark theme detection\r\n * - window.openai.locale - Localization\r\n * - window.openai.requestDisplayMode - Fullscreen/PiP modes\r\n * - window.openai.notifyIntrinsicHeight - Dynamic height reporting\r\n */\r\n\r\nimport { useState, useEffect, useCallback, useSyncExternalStore } from \"react\";\r\n\r\n// ============================================================================\r\n// Type Definitions\r\n// ============================================================================\r\n\r\nexport interface OpenAiGlobals {\r\n /** Arguments supplied when the tool was invoked */\r\n toolInput?: Record<string, unknown>;\r\n /** Your structuredContent - the model reads this */\r\n toolOutput?: Record<string, unknown>;\r\n /** Metadata payload - only widget sees it */\r\n toolResponseMetadata?: Record<string, unknown>;\r\n /** Snapshot of UI state persisted between renders */\r\n widgetState?: Record<string, unknown>;\r\n /** Current theme */\r\n theme?: \"light\" | \"dark\";\r\n /** Current locale (e.g., 'en-US') */\r\n locale?: string;\r\n /** Display mode */\r\n displayMode?: \"inline\" | \"pip\" | \"fullscreen\";\r\n /** Maximum allowed height */\r\n maxHeight?: number;\r\n /** View type */\r\n view?: \"conversation\" | \"modal\";\r\n /** User agent info */\r\n userAgent?: string;\r\n /** Safe area insets */\r\n safeArea?: { top: number; bottom: number; left: number; right: number };\r\n\r\n // API Methods\r\n setWidgetState?: (state: Record<string, unknown>) => void;\r\n callTool?: (name: string, args: Record<string, unknown>) => Promise<unknown>;\r\n sendFollowUpMessage?: (options: { prompt: string }) => Promise<void>;\r\n uploadFile?: (file: File) => Promise<{ fileId: string }>;\r\n getFileDownloadUrl?: (options: {\r\n fileId: string;\r\n }) => Promise<{ downloadUrl: string }>;\r\n requestDisplayMode?: (options: {\r\n mode: \"inline\" | \"pip\" | \"fullscreen\";\r\n }) => Promise<void>;\r\n requestModal?: (options: unknown) => Promise<void>;\r\n notifyIntrinsicHeight?: (height: number) => void;\r\n openExternal?: (options: { href: string }) => void;\r\n requestClose?: () => void;\r\n}\r\n\r\ndeclare global {\r\n interface Window {\r\n openai?: OpenAiGlobals;\r\n }\r\n}\r\n\r\n// ============================================================================\r\n// Core Hook: useOpenAiGlobal\r\n// ============================================================================\r\n\r\nconst SET_GLOBALS_EVENT_TYPE = \"openai:set_globals\";\r\n\r\ninterface SetGlobalsEvent extends CustomEvent {\r\n detail: { globals: Partial<OpenAiGlobals> };\r\n}\r\n\r\n/**\r\n * Subscribe to a specific window.openai global value\r\n */\r\nexport function useOpenAiGlobal<K extends keyof OpenAiGlobals>(\r\n key: K,\r\n): OpenAiGlobals[K] {\r\n const getSnapshot = useCallback(() => {\r\n return window.openai?.[key];\r\n }, [key]);\r\n\r\n const subscribe = useCallback(\r\n (onChange: () => void) => {\r\n const handleSetGlobal = (event: Event) => {\r\n const customEvent = event as SetGlobalsEvent;\r\n if (customEvent.detail?.globals?.[key] !== undefined) {\r\n onChange();\r\n }\r\n };\r\n\r\n window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {\r\n passive: true,\r\n });\r\n return () =>\r\n window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);\r\n },\r\n [key],\r\n );\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\r\n}\r\n\r\n// ============================================================================\r\n// Widget State Hook\r\n// ============================================================================\r\n\r\ntype SetStateAction<T> = T | ((prevState: T) => T);\r\n\r\n/**\r\n * Manage widget state that persists across renders via window.openai.setWidgetState\r\n */\r\nexport function useWidgetState<T extends Record<string, unknown>>(\r\n defaultState: T | (() => T),\r\n): readonly [T, (state: SetStateAction<T>) => void] {\r\n const widgetStateFromWindow = useOpenAiGlobal(\"widgetState\") as T | undefined;\r\n\r\n const [widgetState, _setWidgetState] = useState<T>(() => {\r\n if (widgetStateFromWindow != null) {\r\n return widgetStateFromWindow;\r\n }\r\n return typeof defaultState === \"function\"\r\n ? (defaultState as () => T)()\r\n : defaultState;\r\n });\r\n\r\n useEffect(() => {\r\n if (widgetStateFromWindow != null) {\r\n _setWidgetState(widgetStateFromWindow);\r\n }\r\n }, [widgetStateFromWindow]);\r\n\r\n const setWidgetState = useCallback((state: SetStateAction<T>) => {\r\n _setWidgetState((prevState: T) => {\r\n const newState =\r\n typeof state === \"function\" ? (state as (p: T) => T)(prevState) : state;\r\n window.openai?.setWidgetState?.(newState);\r\n return newState;\r\n });\r\n }, []);\r\n\r\n return [widgetState, setWidgetState] as const;\r\n}\r\n\r\n// ============================================================================\r\n// Tool Output Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Read the tool output (structuredContent) from the MCP server response\r\n */\r\nexport function useToolOutput<T = Record<string, unknown>>(): T | undefined {\r\n return useOpenAiGlobal(\"toolOutput\") as T | undefined;\r\n}\r\n\r\n/**\r\n * Read the tool input (arguments) from the tool invocation\r\n */\r\nexport function useToolInput<T = Record<string, unknown>>(): T | undefined {\r\n return useOpenAiGlobal(\"toolInput\") as T | undefined;\r\n}\r\n\r\n// ============================================================================\r\n// Theme Hook\r\n// ============================================================================\r\n\r\nexport type ThemeMode = \"light\" | \"dark\";\r\n\r\n/**\r\n * Get current ChatGPT theme and provide CSS-friendly values\r\n * Named useChatGPTTheme to avoid conflict with the core useTheme from ThemeProvider\r\n */\r\nexport function useChatGPTTheme(): {\r\n theme: ThemeMode;\r\n isDark: boolean;\r\n colors: {\r\n background: string;\r\n surface: string;\r\n surfaceHover: string;\r\n border: string;\r\n text: string;\r\n textMuted: string;\r\n accent: string;\r\n };\r\n} {\r\n const theme = useOpenAiGlobal(\"theme\") ?? \"dark\";\r\n const isDark = theme === \"dark\";\r\n\r\n return {\r\n theme,\r\n isDark,\r\n colors: isDark\r\n ? {\r\n background: \"#09090b\",\r\n surface: \"#18181b\",\r\n surfaceHover: \"#27272a\",\r\n border: \"#3f3f46\",\r\n text: \"#fafafa\",\r\n textMuted: \"#a1a1aa\",\r\n accent: \"#22d3ee\",\r\n }\r\n : {\r\n background: \"#ffffff\",\r\n surface: \"#f4f4f5\",\r\n surfaceHover: \"#e4e4e7\",\r\n border: \"#d4d4d8\",\r\n text: \"#18181b\",\r\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)\r\n accent: \"#0ea5e9\",\r\n },\r\n };\r\n}\r\n\r\n// ============================================================================\r\n// Locale Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Get current locale for internationalization\r\n */\r\nexport function useLocale(): string {\r\n return useOpenAiGlobal(\"locale\") ?? \"en-US\";\r\n}\r\n\r\n// ============================================================================\r\n// Display Mode Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Manage widget display mode (inline, PiP, fullscreen)\r\n */\r\nexport function useDisplayMode(): {\r\n mode: \"inline\" | \"pip\" | \"fullscreen\";\r\n maxHeight?: number;\r\n requestMode: (mode: \"inline\" | \"pip\" | \"fullscreen\") => Promise<void>;\r\n close: () => void;\r\n} {\r\n const mode = useOpenAiGlobal(\"displayMode\") ?? \"inline\";\r\n const maxHeight = useOpenAiGlobal(\"maxHeight\");\r\n\r\n const requestMode = useCallback(\r\n async (newMode: \"inline\" | \"pip\" | \"fullscreen\") => {\r\n await window.openai?.requestDisplayMode?.({ mode: newMode });\r\n },\r\n [],\r\n );\r\n\r\n const close = useCallback(() => {\r\n window.openai?.requestClose?.();\r\n }, []);\r\n\r\n return { mode, maxHeight, requestMode, close };\r\n}\r\n\r\n// ============================================================================\r\n// Tool Calling Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Call another MCP tool from within a widget\r\n */\r\nexport function useCallTool(): {\r\n callTool: <T = unknown>(\r\n name: string,\r\n args: Record<string, unknown>,\r\n ) => Promise<T>;\r\n isLoading: boolean;\r\n error: Error | null;\r\n} {\r\n const [isLoading, setIsLoading] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n const callTool = useCallback(\r\n async <T = unknown>(\r\n name: string,\r\n args: Record<string, unknown>,\r\n ): Promise<T> => {\r\n if (!window.openai?.callTool) {\r\n throw new Error(\"window.openai.callTool not available\");\r\n }\r\n\r\n setIsLoading(true);\r\n setError(null);\r\n\r\n try {\r\n const result = await window.openai.callTool(name, args);\r\n return result as T;\r\n } catch (err) {\r\n const error = err instanceof Error ? err : new Error(String(err));\r\n setError(error);\r\n throw error;\r\n } finally {\r\n setIsLoading(false);\r\n }\r\n },\r\n [],\r\n );\r\n\r\n return { callTool, isLoading, error };\r\n}\r\n\r\n// ============================================================================\r\n// Follow-up Message Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Send a follow-up message as if the user asked it\r\n */\r\nexport function useSendMessage(): (prompt: string) => Promise<void> {\r\n return useCallback(async (prompt: string) => {\r\n await window.openai?.sendFollowUpMessage?.({ prompt });\r\n }, []);\r\n}\r\n\r\n// ============================================================================\r\n// Dynamic Height Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Get the maximum height available for the widget from the host environment.\r\n * This is the CORRECT pattern per OpenAI Apps SDK examples.\r\n *\r\n * The host (ChatGPT/MCPJam) provides maxHeight, and widgets should constrain\r\n * themselves to this height. Widgets should NOT report their height back.\r\n *\r\n * @returns The maximum height in pixels, or undefined if not available\r\n */\r\nexport function useMaxHeight(): number | undefined {\r\n const [maxHeight, setMaxHeight] = useState<number | undefined>(\r\n () => window.openai?.maxHeight,\r\n );\r\n\r\n useEffect(() => {\r\n const handler = (event: CustomEvent) => {\r\n if (event.detail?.globals && \"maxHeight\" in event.detail.globals) {\r\n setMaxHeight(event.detail.globals.maxHeight);\r\n }\r\n };\r\n\r\n window.addEventListener(\"openai:set_globals\", handler as EventListener);\r\n return () =>\r\n window.removeEventListener(\r\n \"openai:set_globals\",\r\n handler as EventListener,\r\n );\r\n }, []);\r\n\r\n return maxHeight;\r\n}\r\n\r\n// ============================================================================\r\n// External Link Hook\r\n// ============================================================================\r\n\r\n/**\r\n * Open vetted external links in the user's browser\r\n */\r\nexport function useOpenExternal(): (href: string) => void {\r\n return useCallback((href: string) => {\r\n window.openai?.openExternal?.({ href });\r\n }, []);\r\n}\r\n\r\n// ============================================================================\r\n// Utility: Check if running in ChatGPT\r\n// ============================================================================\r\n\r\nexport function isInChatGPT(): boolean {\r\n return typeof window !== \"undefined\" && window.openai !== undefined;\r\n}\r\n\r\n// ============================================================================\r\n// Re-exports (types are already exported above)\r\n// ============================================================================\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CategoryContext.js","names":[],"sources":["../../../src/react/context/CategoryContext.tsx"],"sourcesContent":["/**\n * @zendir/ui — CategoryProvider\n *\n * Optional React context for sharing a `CategoryPalette` and display label\n * across sibling components in a dashboard layout. Components that are\n * \"category-aware\" (Timeline, GroundTrackMap, AstroChart, DataTable) can\n * read from this context to resolve colors/labels without each needing\n * explicit palette props.\n *\n * **This context is fully opt-in.** Components continue to work standalone\n * — explicit `color` / `team` props always take precedence over context.\n *\n * @example\n * ```tsx\n * import { CategoryProvider, useCategoryPalette } from '@zendir/ui';\n *\n * const categories = [\n * { id: 'alpha', label: 'Team Alpha', color: '#2dccff' },\n * { id: 'bravo', label: 'Team Bravo', color: '#fce83a' },\n * ];\n *\n * // Wrap a section of your dashboard\n * <CategoryProvider categories={categories} categoryLabel=\"Squads\">\n * <Timeline events={events} />\n * <GroundTrackMap pins={pins} />\n * </CategoryProvider>\n *\n * // Inside any component\n * function MyWidget() {\n * const ctx = useCategoryPalette();\n * if (!ctx) return null; // graceful when no provider\n * const color = ctx.palette.getColor('alpha');\n * const label = ctx.categoryLabel; // \"Squads\"\n * }\n * ```\n */\n\nimport React, {\n createContext,\n useContext,\n useMemo,\n type ReactNode,\n} from \"react\";\nimport { CategoryPalette } from \"../utils/categoryPalette\";\nimport type { CategoryDef } from \"../types\";\n\n// =============================================================================\n// Context value\n// =============================================================================\n\nexport interface CategoryContextValue {\n /** Palette instance built from the provided categories. */\n palette: CategoryPalette;\n /** All registered category definitions. */\n categories: CategoryDef[];\n /**\n * Human-readable label for the grouping dimension.\n * Defaults to `\"Categories\"`. Components that support a label override\n * (e.g. Timeline's `teamLabel`) should fall back to this when no\n * explicit prop is provided.\n */\n categoryLabel: string;\n}\n\nconst CategoryContext = createContext<CategoryContextValue | null>(null);\nCategoryContext.displayName = \"CategoryContext\";\n\n// =============================================================================\n// Provider\n// =============================================================================\n\nexport interface CategoryProviderProps {\n /** Category definitions — each must have `id`, `label`, and `color`. */\n categories: CategoryDef[];\n /**\n * Display label for the grouping dimension, e.g. `\"Teams\"`, `\"Squads\"`,\n * `\"Subsystems\"`. Defaults to `\"Categories\"`.\n */\n categoryLabel?: string;\n /**\n * Optional custom fallback palette for auto-assigned unknown ids.\n * Defaults to the Astro data-viz palette built into `CategoryPalette`.\n */\n fallbackPalette?: string[];\n children: ReactNode;\n}\n\n/**\n * Provides a shared `CategoryPalette` to descendant components.\n *\n * Wrap a dashboard section (not the entire app) so sibling components\n * like Timeline + Map + Charts resolve the same colors for the same ids.\n */\nexport function CategoryProvider({\n categories,\n categoryLabel = \"Categories\",\n fallbackPalette,\n children,\n}: CategoryProviderProps): React.ReactElement {\n const value = useMemo<CategoryContextValue>(\n () => ({\n palette: new CategoryPalette(categories, fallbackPalette),\n categories,\n categoryLabel,\n }),\n [categories, categoryLabel, fallbackPalette],\n );\n\n return (\n <CategoryContext.Provider value={value}>\n {children}\n </CategoryContext.Provider>\n );\n}\n\n// =============================================================================\n// Hooks\n// =============================================================================\n\n/**\n * Access the nearest `CategoryProvider`'s palette and label.\n *\n * Returns `null` when no provider is present — components should treat\n * this as \"standalone mode\" and fall back to explicit props / defaults.\n */\nexport function useCategoryPalette(): CategoryContextValue | null {\n return useContext(CategoryContext);\n}\n\n/**\n * Like `useCategoryPalette()` but throws if no provider is found.\n * Use only when you know a provider is always present.\n */\nexport function useCategoryPaletteRequired(): CategoryContextValue {\n const ctx = useContext(CategoryContext);\n if (!ctx) {\n throw new Error(\n \"useCategoryPaletteRequired() was called outside of a <CategoryProvider>. \" +\n \"Wrap a parent component with <CategoryProvider categories={[...]}> or \" +\n \"use useCategoryPalette() for graceful null handling.\",\n );\n }\n return ctx;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,IAAM,kBAAkB,cAA2C,IAAI;AACvE,gBAAgB,cAAc;;;;;;;AA4B9B,SAAgB,iBAAiB,EAC/B,YACA,gBAAgB,cAChB,iBACA,YAC4C;CAC5C,MAAM,QAAQ,eACL;EACL,SAAS,IAAI,gBAAgB,YAAY,eAAe;EACxD;EACA;CACF,IACA;EAAC;EAAY;EAAe;CAAe,CAC7C;CAEA,OACE,oBAAC,gBAAgB,UAAjB;EAAiC;EAC9B;CACuB,CAAA;AAE9B;;;;;;;AAYA,SAAgB,qBAAkD;CAChE,OAAO,WAAW,eAAe;AACnC;;;;;AAMA,SAAgB,6BAAmD;CACjE,MAAM,MAAM,WAAW,eAAe;CACtC,IAAI,CAAC,KACH,MAAM,IAAI,MACR,qMAGF;CAEF,OAAO;AACT"}
1
+ {"version":3,"file":"CategoryContext.js","names":[],"sources":["../../../src/react/context/CategoryContext.tsx"],"sourcesContent":["/**\r\n * @zendir/ui — CategoryProvider\r\n *\r\n * Optional React context for sharing a `CategoryPalette` and display label\r\n * across sibling components in a dashboard layout. Components that are\r\n * \"category-aware\" (Timeline, GroundTrackMap, AstroChart, DataTable) can\r\n * read from this context to resolve colors/labels without each needing\r\n * explicit palette props.\r\n *\r\n * **This context is fully opt-in.** Components continue to work standalone\r\n * — explicit `color` / `team` props always take precedence over context.\r\n *\r\n * @example\r\n * ```tsx\r\n * import { CategoryProvider, useCategoryPalette } from '@zendir/ui';\r\n *\r\n * const categories = [\r\n * { id: 'alpha', label: 'Team Alpha', color: '#2dccff' },\r\n * { id: 'bravo', label: 'Team Bravo', color: '#fce83a' },\r\n * ];\r\n *\r\n * // Wrap a section of your dashboard\r\n * <CategoryProvider categories={categories} categoryLabel=\"Squads\">\r\n * <Timeline events={events} />\r\n * <GroundTrackMap pins={pins} />\r\n * </CategoryProvider>\r\n *\r\n * // Inside any component\r\n * function MyWidget() {\r\n * const ctx = useCategoryPalette();\r\n * if (!ctx) return null; // graceful when no provider\r\n * const color = ctx.palette.getColor('alpha');\r\n * const label = ctx.categoryLabel; // \"Squads\"\r\n * }\r\n * ```\r\n */\r\n\r\nimport React, {\r\n createContext,\r\n useContext,\r\n useMemo,\r\n type ReactNode,\r\n} from \"react\";\r\nimport { CategoryPalette } from \"../utils/categoryPalette\";\r\nimport type { CategoryDef } from \"../types\";\r\n\r\n// =============================================================================\r\n// Context value\r\n// =============================================================================\r\n\r\nexport interface CategoryContextValue {\r\n /** Palette instance built from the provided categories. */\r\n palette: CategoryPalette;\r\n /** All registered category definitions. */\r\n categories: CategoryDef[];\r\n /**\r\n * Human-readable label for the grouping dimension.\r\n * Defaults to `\"Categories\"`. Components that support a label override\r\n * (e.g. Timeline's `teamLabel`) should fall back to this when no\r\n * explicit prop is provided.\r\n */\r\n categoryLabel: string;\r\n}\r\n\r\nconst CategoryContext = createContext<CategoryContextValue | null>(null);\r\nCategoryContext.displayName = \"CategoryContext\";\r\n\r\n// =============================================================================\r\n// Provider\r\n// =============================================================================\r\n\r\nexport interface CategoryProviderProps {\r\n /** Category definitions — each must have `id`, `label`, and `color`. */\r\n categories: CategoryDef[];\r\n /**\r\n * Display label for the grouping dimension, e.g. `\"Teams\"`, `\"Squads\"`,\r\n * `\"Subsystems\"`. Defaults to `\"Categories\"`.\r\n */\r\n categoryLabel?: string;\r\n /**\r\n * Optional custom fallback palette for auto-assigned unknown ids.\r\n * Defaults to the Astro data-viz palette built into `CategoryPalette`.\r\n */\r\n fallbackPalette?: string[];\r\n children: ReactNode;\r\n}\r\n\r\n/**\r\n * Provides a shared `CategoryPalette` to descendant components.\r\n *\r\n * Wrap a dashboard section (not the entire app) so sibling components\r\n * like Timeline + Map + Charts resolve the same colors for the same ids.\r\n */\r\nexport function CategoryProvider({\r\n categories,\r\n categoryLabel = \"Categories\",\r\n fallbackPalette,\r\n children,\r\n}: CategoryProviderProps): React.ReactElement {\r\n const value = useMemo<CategoryContextValue>(\r\n () => ({\r\n palette: new CategoryPalette(categories, fallbackPalette),\r\n categories,\r\n categoryLabel,\r\n }),\r\n [categories, categoryLabel, fallbackPalette],\r\n );\r\n\r\n return (\r\n <CategoryContext.Provider value={value}>\r\n {children}\r\n </CategoryContext.Provider>\r\n );\r\n}\r\n\r\n// =============================================================================\r\n// Hooks\r\n// =============================================================================\r\n\r\n/**\r\n * Access the nearest `CategoryProvider`'s palette and label.\r\n *\r\n * Returns `null` when no provider is present — components should treat\r\n * this as \"standalone mode\" and fall back to explicit props / defaults.\r\n */\r\nexport function useCategoryPalette(): CategoryContextValue | null {\r\n return useContext(CategoryContext);\r\n}\r\n\r\n/**\r\n * Like `useCategoryPalette()` but throws if no provider is found.\r\n * Use only when you know a provider is always present.\r\n */\r\nexport function useCategoryPaletteRequired(): CategoryContextValue {\r\n const ctx = useContext(CategoryContext);\r\n if (!ctx) {\r\n throw new Error(\r\n \"useCategoryPaletteRequired() was called outside of a <CategoryProvider>. \" +\r\n \"Wrap a parent component with <CategoryProvider categories={[...]}> or \" +\r\n \"use useCategoryPalette() for graceful null handling.\",\r\n );\r\n }\r\n return ctx;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,IAAM,kBAAkB,cAA2C,IAAI;AACvE,gBAAgB,cAAc;;;;;;;AA4B9B,SAAgB,iBAAiB,EAC/B,YACA,gBAAgB,cAChB,iBACA,YAC4C;CAC5C,MAAM,QAAQ,eACL;EACL,SAAS,IAAI,gBAAgB,YAAY,eAAe;EACxD;EACA;CACF,IACA;EAAC;EAAY;EAAe;CAAe,CAC7C;CAEA,OACE,oBAAC,gBAAgB,UAAjB;EAAiC;EAC9B;CACuB,CAAA;AAE9B;;;;;;;AAYA,SAAgB,qBAAkD;CAChE,OAAO,WAAW,eAAe;AACnC;;;;;AAMA,SAAgB,6BAAmD;CACjE,MAAM,MAAM,WAAW,eAAe;CACtC,IAAI,CAAC,KACH,MAAM,IAAI,MACR,qMAGF;CAEF,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"DisplaySettingsContext.js","names":[],"sources":["../../../src/react/context/DisplaySettingsContext.tsx"],"sourcesContent":["/**\n * @zendir/ui - DisplaySettingsContext\n *\n * Centralized context for app-wide display preferences.\n * Controls compact mode, accent colors, glass tint, and other display settings.\n *\n * Features:\n * - Compact mode toggle (affects all cards globally)\n * - Accent color selection (for card accents)\n * - Glass tint selection (for transparent themes)\n * - Color target control (both, accent-only, glass-only)\n * - LocalStorage persistence\n *\n * @example\n * ```tsx\n * import { DisplaySettingsProvider, useDisplaySettings } from '@zendir/ui';\n *\n * // Wrap your app\n * <DisplaySettingsProvider>\n * <App />\n * </DisplaySettingsProvider>\n *\n * // Use in components\n * function MyCard() {\n * const { compactMode, accentColor } = useDisplaySettings();\n * return <Card compact={compactMode} />;\n * }\n * ```\n */\n\nimport React, {\n createContext,\n useContext,\n useState,\n useCallback,\n useMemo,\n useEffect,\n type ReactNode,\n} from \"react\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type ColorTarget = \"both\" | \"accent\" | \"glass\";\n\nexport interface DisplaySettings {\n /** Global compact mode - affects all cards */\n compactMode: boolean;\n\n /** Accent color for cards and UI elements */\n accentColor: string;\n\n /** Glass tint color for transparent themes */\n glassTint: string;\n\n /** Which color setting to apply: both, accent-only, or glass-only */\n colorTarget: ColorTarget;\n\n /** Enable accent-colored borders on cards (for transparent themes) */\n accentBorders: boolean;\n}\n\nexport interface DisplaySettingsContextValue extends DisplaySettings {\n /** Set compact mode on/off */\n setCompactMode: (value: boolean) => void;\n\n /** Toggle compact mode */\n toggleCompactMode: () => void;\n\n /** Set accent color */\n setAccentColor: (color: string) => void;\n\n /** Set glass tint color */\n setGlassTint: (color: string) => void;\n\n /** Set which color setting to apply */\n setColorTarget: (target: ColorTarget) => void;\n\n /** Set a color that applies based on current target */\n setColor: (color: string) => void;\n\n /** Set accent borders on/off */\n setAccentBorders: (value: boolean) => void;\n\n /** Toggle accent borders */\n toggleAccentBorders: () => void;\n\n /** Reset all settings to defaults */\n resetSettings: () => void;\n}\n\n// =============================================================================\n// Preset Colors\n// =============================================================================\n\nexport const PRESET_COLORS = {\n purple: \"#8b5cf6\",\n blue: \"#3b82f6\",\n cyan: \"#06b6d4\",\n teal: \"#14b8a6\",\n green: \"#10b981\",\n lime: \"#84cc16\",\n amber: \"#f59e0b\",\n orange: \"#f97316\",\n pink: \"#ec4899\",\n red: \"#ef4444\",\n slate: \"#64748b\",\n} as const;\n\nexport const GLASS_TINTS = {\n ...PRESET_COLORS,\n clear: \"rgba(255,255,255,0.05)\",\n dark: \"rgba(0,0,0,0.3)\",\n} as const;\n\nexport type PresetColorKey = keyof typeof PRESET_COLORS;\nexport type GlassTintKey = keyof typeof GLASS_TINTS;\n\n// =============================================================================\n// Default Values\n// =============================================================================\n\nconst DEFAULT_SETTINGS: DisplaySettings = {\n compactMode: false,\n accentColor: PRESET_COLORS.purple,\n glassTint: PRESET_COLORS.purple,\n colorTarget: \"both\",\n accentBorders: true,\n};\n\nconst STORAGE_KEY = \"zendir-display-settings\";\n\n// =============================================================================\n// Context\n// =============================================================================\n\nconst DisplaySettingsContext =\n createContext<DisplaySettingsContextValue | null>(null);\n\n// =============================================================================\n// Provider Props\n// =============================================================================\n\nexport interface DisplaySettingsProviderProps {\n children: ReactNode;\n\n /** Initial compact mode state */\n defaultCompactMode?: boolean;\n\n /** Initial accent color */\n defaultAccentColor?: string;\n\n /** Initial glass tint color */\n defaultGlassTint?: string;\n\n /** Initial color target */\n defaultColorTarget?: ColorTarget;\n\n /** Initial accent borders state */\n defaultAccentBorders?: boolean;\n\n /** Enable localStorage persistence (default: true) */\n persist?: boolean;\n}\n\n// =============================================================================\n// Provider Component\n// =============================================================================\n\nexport function DisplaySettingsProvider({\n children,\n defaultCompactMode,\n defaultAccentColor,\n defaultGlassTint,\n defaultColorTarget,\n defaultAccentBorders,\n persist = true,\n}: DisplaySettingsProviderProps): React.ReactElement {\n // Initialize state from localStorage or defaults\n const [settings, setSettings] = useState<DisplaySettings>(() => {\n if (persist && typeof window !== \"undefined\") {\n try {\n const stored = localStorage.getItem(STORAGE_KEY);\n if (stored) {\n const parsed = JSON.parse(stored);\n return {\n compactMode:\n parsed.compactMode ??\n defaultCompactMode ??\n DEFAULT_SETTINGS.compactMode,\n accentColor:\n parsed.accentColor ??\n defaultAccentColor ??\n DEFAULT_SETTINGS.accentColor,\n glassTint:\n parsed.glassTint ??\n defaultGlassTint ??\n DEFAULT_SETTINGS.glassTint,\n colorTarget:\n parsed.colorTarget ??\n defaultColorTarget ??\n DEFAULT_SETTINGS.colorTarget,\n accentBorders:\n parsed.accentBorders ??\n defaultAccentBorders ??\n DEFAULT_SETTINGS.accentBorders,\n };\n }\n } catch {\n // Ignore parse errors\n }\n }\n\n return {\n compactMode: defaultCompactMode ?? DEFAULT_SETTINGS.compactMode,\n accentColor: defaultAccentColor ?? DEFAULT_SETTINGS.accentColor,\n glassTint: defaultGlassTint ?? DEFAULT_SETTINGS.glassTint,\n colorTarget: defaultColorTarget ?? DEFAULT_SETTINGS.colorTarget,\n accentBorders: defaultAccentBorders ?? DEFAULT_SETTINGS.accentBorders,\n };\n });\n\n // Persist to localStorage when settings change\n useEffect(() => {\n if (persist && typeof window !== \"undefined\") {\n try {\n localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));\n } catch {\n // Ignore storage errors\n }\n }\n }, [settings, persist]);\n\n // Setters\n const setCompactMode = useCallback((value: boolean) => {\n setSettings((prev) => ({ ...prev, compactMode: value }));\n }, []);\n\n const toggleCompactMode = useCallback(() => {\n setSettings((prev) => ({ ...prev, compactMode: !prev.compactMode }));\n }, []);\n\n const setAccentColor = useCallback((color: string) => {\n setSettings((prev) => ({ ...prev, accentColor: color }));\n }, []);\n\n const setGlassTint = useCallback((color: string) => {\n setSettings((prev) => ({ ...prev, glassTint: color }));\n }, []);\n\n const setColorTarget = useCallback((target: ColorTarget) => {\n setSettings((prev) => ({ ...prev, colorTarget: target }));\n }, []);\n\n // Set color based on current target\n const setColor = useCallback((color: string) => {\n setSettings((prev) => {\n switch (prev.colorTarget) {\n case \"accent\":\n return { ...prev, accentColor: color };\n case \"glass\":\n return { ...prev, glassTint: color };\n case \"both\":\n default:\n return { ...prev, accentColor: color, glassTint: color };\n }\n });\n }, []);\n\n const setAccentBorders = useCallback((value: boolean) => {\n setSettings((prev) => ({ ...prev, accentBorders: value }));\n }, []);\n\n const toggleAccentBorders = useCallback(() => {\n setSettings((prev) => ({ ...prev, accentBorders: !prev.accentBorders }));\n }, []);\n\n const resetSettings = useCallback(() => {\n setSettings(DEFAULT_SETTINGS);\n }, []);\n\n // Memoized context value\n const value = useMemo<DisplaySettingsContextValue>(\n () => ({\n ...settings,\n setCompactMode,\n toggleCompactMode,\n setAccentColor,\n setGlassTint,\n setColorTarget,\n setColor,\n setAccentBorders,\n toggleAccentBorders,\n resetSettings,\n }),\n [\n settings,\n setCompactMode,\n toggleCompactMode,\n setAccentColor,\n setGlassTint,\n setColorTarget,\n setColor,\n setAccentBorders,\n toggleAccentBorders,\n resetSettings,\n ],\n );\n\n return (\n <DisplaySettingsContext.Provider value={value}>\n {children}\n </DisplaySettingsContext.Provider>\n );\n}\n\n// =============================================================================\n// Hook\n// =============================================================================\n\n/**\n * Access display settings context\n * Must be used within a DisplaySettingsProvider\n */\nexport function useDisplaySettings(): DisplaySettingsContextValue {\n const context = useContext(DisplaySettingsContext);\n\n if (!context) {\n throw new Error(\n \"useDisplaySettings must be used within a DisplaySettingsProvider\",\n );\n }\n\n return context;\n}\n\n/**\n * Optional hook that returns null if not within provider\n * Useful for components that should work with or without the provider\n */\nexport function useDisplaySettingsOptional(): DisplaySettingsContextValue | null {\n return useContext(DisplaySettingsContext);\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Get effective compact mode value\n * Priority: explicit prop > context value > default (false)\n */\nexport function getEffectiveCompactMode(\n propValue: boolean | undefined,\n contextValue: boolean | undefined,\n): boolean {\n if (propValue !== undefined) return propValue;\n if (contextValue !== undefined) return contextValue;\n return false;\n}\n\n/**\n * Get effective accent borders value\n * Priority: explicit prop > context value > default (true)\n */\nexport function getEffectiveAccentBorders(\n propValue: boolean | undefined,\n contextValue: boolean | undefined,\n): boolean {\n if (propValue !== undefined) return propValue;\n if (contextValue !== undefined) return contextValue;\n return true;\n}\n\n/**\n * Hook to get card border style based on accentBorders setting\n * Returns the appropriate border style for transparent themes\n */\nexport function useCardBorderStyle(\n tokens: any,\n isTransparentTheme: boolean,\n): React.CSSProperties {\n const displaySettings = useDisplaySettingsOptional();\n const accentBorders = displaySettings?.accentBorders ?? true;\n\n if (!isTransparentTheme) {\n return (\n tokens.colors.border.cardStyle ?? {\n border: `1px solid ${tokens.colors.border.muted}`,\n }\n );\n }\n\n if (accentBorders) {\n return tokens.colors.border.cardStyle ?? {};\n }\n\n // Fallback to subtle neutral border when accent borders disabled\n return { border: \"1px solid rgba(255, 255, 255, 0.08)\" };\n}\n\nexport default DisplaySettingsProvider;\n"],"mappings":"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"}
1
+ {"version":3,"file":"DisplaySettingsContext.js","names":[],"sources":["../../../src/react/context/DisplaySettingsContext.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - DisplaySettingsContext\r\n *\r\n * Centralized context for app-wide display preferences.\r\n * Controls compact mode, accent colors, glass tint, and other display settings.\r\n *\r\n * Features:\r\n * - Compact mode toggle (affects all cards globally)\r\n * - Accent color selection (for card accents)\r\n * - Glass tint selection (for transparent themes)\r\n * - Color target control (both, accent-only, glass-only)\r\n * - LocalStorage persistence\r\n *\r\n * @example\r\n * ```tsx\r\n * import { DisplaySettingsProvider, useDisplaySettings } from '@zendir/ui';\r\n *\r\n * // Wrap your app\r\n * <DisplaySettingsProvider>\r\n * <App />\r\n * </DisplaySettingsProvider>\r\n *\r\n * // Use in components\r\n * function MyCard() {\r\n * const { compactMode, accentColor } = useDisplaySettings();\r\n * return <Card compact={compactMode} />;\r\n * }\r\n * ```\r\n */\r\n\r\nimport React, {\r\n createContext,\r\n useContext,\r\n useState,\r\n useCallback,\r\n useMemo,\r\n useEffect,\r\n type ReactNode,\r\n} from \"react\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport type ColorTarget = \"both\" | \"accent\" | \"glass\";\r\n\r\nexport interface DisplaySettings {\r\n /** Global compact mode - affects all cards */\r\n compactMode: boolean;\r\n\r\n /** Accent color for cards and UI elements */\r\n accentColor: string;\r\n\r\n /** Glass tint color for transparent themes */\r\n glassTint: string;\r\n\r\n /** Which color setting to apply: both, accent-only, or glass-only */\r\n colorTarget: ColorTarget;\r\n\r\n /** Enable accent-colored borders on cards (for transparent themes) */\r\n accentBorders: boolean;\r\n}\r\n\r\nexport interface DisplaySettingsContextValue extends DisplaySettings {\r\n /** Set compact mode on/off */\r\n setCompactMode: (value: boolean) => void;\r\n\r\n /** Toggle compact mode */\r\n toggleCompactMode: () => void;\r\n\r\n /** Set accent color */\r\n setAccentColor: (color: string) => void;\r\n\r\n /** Set glass tint color */\r\n setGlassTint: (color: string) => void;\r\n\r\n /** Set which color setting to apply */\r\n setColorTarget: (target: ColorTarget) => void;\r\n\r\n /** Set a color that applies based on current target */\r\n setColor: (color: string) => void;\r\n\r\n /** Set accent borders on/off */\r\n setAccentBorders: (value: boolean) => void;\r\n\r\n /** Toggle accent borders */\r\n toggleAccentBorders: () => void;\r\n\r\n /** Reset all settings to defaults */\r\n resetSettings: () => void;\r\n}\r\n\r\n// =============================================================================\r\n// Preset Colors\r\n// =============================================================================\r\n\r\nexport const PRESET_COLORS = {\r\n purple: \"#8b5cf6\",\r\n blue: \"#3b82f6\",\r\n cyan: \"#06b6d4\",\r\n teal: \"#14b8a6\",\r\n green: \"#10b981\",\r\n lime: \"#84cc16\",\r\n amber: \"#f59e0b\",\r\n orange: \"#f97316\",\r\n pink: \"#ec4899\",\r\n red: \"#ef4444\",\r\n slate: \"#64748b\",\r\n} as const;\r\n\r\nexport const GLASS_TINTS = {\r\n ...PRESET_COLORS,\r\n clear: \"rgba(255,255,255,0.05)\",\r\n dark: \"rgba(0,0,0,0.3)\",\r\n} as const;\r\n\r\nexport type PresetColorKey = keyof typeof PRESET_COLORS;\r\nexport type GlassTintKey = keyof typeof GLASS_TINTS;\r\n\r\n// =============================================================================\r\n// Default Values\r\n// =============================================================================\r\n\r\nconst DEFAULT_SETTINGS: DisplaySettings = {\r\n compactMode: false,\r\n accentColor: PRESET_COLORS.purple,\r\n glassTint: PRESET_COLORS.purple,\r\n colorTarget: \"both\",\r\n accentBorders: true,\r\n};\r\n\r\nconst STORAGE_KEY = \"zendir-display-settings\";\r\n\r\n// =============================================================================\r\n// Context\r\n// =============================================================================\r\n\r\nconst DisplaySettingsContext =\r\n createContext<DisplaySettingsContextValue | null>(null);\r\n\r\n// =============================================================================\r\n// Provider Props\r\n// =============================================================================\r\n\r\nexport interface DisplaySettingsProviderProps {\r\n children: ReactNode;\r\n\r\n /** Initial compact mode state */\r\n defaultCompactMode?: boolean;\r\n\r\n /** Initial accent color */\r\n defaultAccentColor?: string;\r\n\r\n /** Initial glass tint color */\r\n defaultGlassTint?: string;\r\n\r\n /** Initial color target */\r\n defaultColorTarget?: ColorTarget;\r\n\r\n /** Initial accent borders state */\r\n defaultAccentBorders?: boolean;\r\n\r\n /** Enable localStorage persistence (default: true) */\r\n persist?: boolean;\r\n}\r\n\r\n// =============================================================================\r\n// Provider Component\r\n// =============================================================================\r\n\r\nexport function DisplaySettingsProvider({\r\n children,\r\n defaultCompactMode,\r\n defaultAccentColor,\r\n defaultGlassTint,\r\n defaultColorTarget,\r\n defaultAccentBorders,\r\n persist = true,\r\n}: DisplaySettingsProviderProps): React.ReactElement {\r\n // Initialize state from localStorage or defaults\r\n const [settings, setSettings] = useState<DisplaySettings>(() => {\r\n if (persist && typeof window !== \"undefined\") {\r\n try {\r\n const stored = localStorage.getItem(STORAGE_KEY);\r\n if (stored) {\r\n const parsed = JSON.parse(stored);\r\n return {\r\n compactMode:\r\n parsed.compactMode ??\r\n defaultCompactMode ??\r\n DEFAULT_SETTINGS.compactMode,\r\n accentColor:\r\n parsed.accentColor ??\r\n defaultAccentColor ??\r\n DEFAULT_SETTINGS.accentColor,\r\n glassTint:\r\n parsed.glassTint ??\r\n defaultGlassTint ??\r\n DEFAULT_SETTINGS.glassTint,\r\n colorTarget:\r\n parsed.colorTarget ??\r\n defaultColorTarget ??\r\n DEFAULT_SETTINGS.colorTarget,\r\n accentBorders:\r\n parsed.accentBorders ??\r\n defaultAccentBorders ??\r\n DEFAULT_SETTINGS.accentBorders,\r\n };\r\n }\r\n } catch {\r\n // Ignore parse errors\r\n }\r\n }\r\n\r\n return {\r\n compactMode: defaultCompactMode ?? DEFAULT_SETTINGS.compactMode,\r\n accentColor: defaultAccentColor ?? DEFAULT_SETTINGS.accentColor,\r\n glassTint: defaultGlassTint ?? DEFAULT_SETTINGS.glassTint,\r\n colorTarget: defaultColorTarget ?? DEFAULT_SETTINGS.colorTarget,\r\n accentBorders: defaultAccentBorders ?? DEFAULT_SETTINGS.accentBorders,\r\n };\r\n });\r\n\r\n // Persist to localStorage when settings change\r\n useEffect(() => {\r\n if (persist && typeof window !== \"undefined\") {\r\n try {\r\n localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));\r\n } catch {\r\n // Ignore storage errors\r\n }\r\n }\r\n }, [settings, persist]);\r\n\r\n // Setters\r\n const setCompactMode = useCallback((value: boolean) => {\r\n setSettings((prev) => ({ ...prev, compactMode: value }));\r\n }, []);\r\n\r\n const toggleCompactMode = useCallback(() => {\r\n setSettings((prev) => ({ ...prev, compactMode: !prev.compactMode }));\r\n }, []);\r\n\r\n const setAccentColor = useCallback((color: string) => {\r\n setSettings((prev) => ({ ...prev, accentColor: color }));\r\n }, []);\r\n\r\n const setGlassTint = useCallback((color: string) => {\r\n setSettings((prev) => ({ ...prev, glassTint: color }));\r\n }, []);\r\n\r\n const setColorTarget = useCallback((target: ColorTarget) => {\r\n setSettings((prev) => ({ ...prev, colorTarget: target }));\r\n }, []);\r\n\r\n // Set color based on current target\r\n const setColor = useCallback((color: string) => {\r\n setSettings((prev) => {\r\n switch (prev.colorTarget) {\r\n case \"accent\":\r\n return { ...prev, accentColor: color };\r\n case \"glass\":\r\n return { ...prev, glassTint: color };\r\n case \"both\":\r\n default:\r\n return { ...prev, accentColor: color, glassTint: color };\r\n }\r\n });\r\n }, []);\r\n\r\n const setAccentBorders = useCallback((value: boolean) => {\r\n setSettings((prev) => ({ ...prev, accentBorders: value }));\r\n }, []);\r\n\r\n const toggleAccentBorders = useCallback(() => {\r\n setSettings((prev) => ({ ...prev, accentBorders: !prev.accentBorders }));\r\n }, []);\r\n\r\n const resetSettings = useCallback(() => {\r\n setSettings(DEFAULT_SETTINGS);\r\n }, []);\r\n\r\n // Memoized context value\r\n const value = useMemo<DisplaySettingsContextValue>(\r\n () => ({\r\n ...settings,\r\n setCompactMode,\r\n toggleCompactMode,\r\n setAccentColor,\r\n setGlassTint,\r\n setColorTarget,\r\n setColor,\r\n setAccentBorders,\r\n toggleAccentBorders,\r\n resetSettings,\r\n }),\r\n [\r\n settings,\r\n setCompactMode,\r\n toggleCompactMode,\r\n setAccentColor,\r\n setGlassTint,\r\n setColorTarget,\r\n setColor,\r\n setAccentBorders,\r\n toggleAccentBorders,\r\n resetSettings,\r\n ],\r\n );\r\n\r\n return (\r\n <DisplaySettingsContext.Provider value={value}>\r\n {children}\r\n </DisplaySettingsContext.Provider>\r\n );\r\n}\r\n\r\n// =============================================================================\r\n// Hook\r\n// =============================================================================\r\n\r\n/**\r\n * Access display settings context\r\n * Must be used within a DisplaySettingsProvider\r\n */\r\nexport function useDisplaySettings(): DisplaySettingsContextValue {\r\n const context = useContext(DisplaySettingsContext);\r\n\r\n if (!context) {\r\n throw new Error(\r\n \"useDisplaySettings must be used within a DisplaySettingsProvider\",\r\n );\r\n }\r\n\r\n return context;\r\n}\r\n\r\n/**\r\n * Optional hook that returns null if not within provider\r\n * Useful for components that should work with or without the provider\r\n */\r\nexport function useDisplaySettingsOptional(): DisplaySettingsContextValue | null {\r\n return useContext(DisplaySettingsContext);\r\n}\r\n\r\n// =============================================================================\r\n// Utility Functions\r\n// =============================================================================\r\n\r\n/**\r\n * Get effective compact mode value\r\n * Priority: explicit prop > context value > default (false)\r\n */\r\nexport function getEffectiveCompactMode(\r\n propValue: boolean | undefined,\r\n contextValue: boolean | undefined,\r\n): boolean {\r\n if (propValue !== undefined) return propValue;\r\n if (contextValue !== undefined) return contextValue;\r\n return false;\r\n}\r\n\r\n/**\r\n * Get effective accent borders value\r\n * Priority: explicit prop > context value > default (true)\r\n */\r\nexport function getEffectiveAccentBorders(\r\n propValue: boolean | undefined,\r\n contextValue: boolean | undefined,\r\n): boolean {\r\n if (propValue !== undefined) return propValue;\r\n if (contextValue !== undefined) return contextValue;\r\n return true;\r\n}\r\n\r\n/**\r\n * Hook to get card border style based on accentBorders setting\r\n * Returns the appropriate border style for transparent themes\r\n */\r\nexport function useCardBorderStyle(\r\n tokens: any,\r\n isTransparentTheme: boolean,\r\n): React.CSSProperties {\r\n const displaySettings = useDisplaySettingsOptional();\r\n const accentBorders = displaySettings?.accentBorders ?? true;\r\n\r\n if (!isTransparentTheme) {\r\n return (\r\n tokens.colors.border.cardStyle ?? {\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n }\r\n );\r\n }\r\n\r\n if (accentBorders) {\r\n return tokens.colors.border.cardStyle ?? {};\r\n }\r\n\r\n // Fallback to subtle neutral border when accent borders disabled\r\n return { border: \"1px solid rgba(255, 255, 255, 0.08)\" };\r\n}\r\n\r\nexport default DisplaySettingsProvider;\r\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"SpatialSelectionContext.js","names":[],"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 {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useState,\n} 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 =\n 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>(\n defaultSelectedId,\n );\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,kCACJ,cAAmD,IAAI;;;;;;;;;;;AAyEzD,SAAgB,sBAA2D;CACzE,OAAO,WAAW,+BAA+B;AACnD"}
1
+ {"version":3,"file":"SpatialSelectionContext.js","names":[],"sources":["../../../src/react/context/SpatialSelectionContext.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - SpatialSelectionContext\r\n *\r\n * Optional shared selection state for spatial dashboards. Wrap a tree\r\n * containing `<ZenSpace3D />`, `<GroundTrackMap />`,\r\n * `<ObjectInventoryPanel />`, etc. — they cross-talk via the context\r\n * and stay synchronised without prop-drilling:\r\n *\r\n * <SpatialSelectionProvider>\r\n * <ObjectInventoryPanel objects={scene.objects} /> // writes selection\r\n * <ZenSpace3D satellites={...} /> // reads selection\r\n * <GroundTrackMap satellites={tracks} /> // reads selection\r\n * </SpatialSelectionProvider>\r\n *\r\n * The provider is **opt-in**. Components that consume the context use\r\n * `useSpatialSelection()` which returns `null` outside a provider —\r\n * that's how every component stays standalone-usable. No coupling.\r\n */\r\n\r\nimport {\r\n createContext,\r\n useCallback,\r\n useContext,\r\n useMemo,\r\n useState,\r\n} from \"react\";\r\nimport type { ReactNode } from \"react\";\r\n\r\n// ─── Types ────────────────────────────────────────────────────────────────────\r\n\r\nexport interface SpatialSelectionContextValue {\r\n /** ID of the currently selected object, or null if nothing is selected. */\r\n selectedId: string | null;\r\n /** Set the selection (or clear with `null`). */\r\n setSelectedId: (id: string | null) => void;\r\n /** Toggle selection: select the id if it isn't selected, clear otherwise. */\r\n toggleSelection: (id: string) => void;\r\n /** Convenience: clear any current selection. */\r\n clearSelection: () => void;\r\n}\r\n\r\nconst SpatialSelectionContextInternal =\r\n createContext<SpatialSelectionContextValue | null>(null);\r\n\r\n// ─── Provider ─────────────────────────────────────────────────────────────────\r\n\r\nexport interface SpatialSelectionProviderProps {\r\n children: ReactNode;\r\n /** Initial selected id. Default null (nothing selected). */\r\n defaultSelectedId?: string | null;\r\n /** Controlled mode: when set, the provider mirrors this value instead\r\n * of owning state. Pair with `onSelectionChange`. */\r\n selectedId?: string | null;\r\n /** Called whenever the selection changes (uncontrolled or controlled). */\r\n onSelectionChange?: (id: string | null) => void;\r\n}\r\n\r\nexport function SpatialSelectionProvider({\r\n children,\r\n defaultSelectedId = null,\r\n selectedId: controlledId,\r\n onSelectionChange,\r\n}: SpatialSelectionProviderProps) {\r\n const [internalId, setInternalId] = useState<string | null>(\r\n defaultSelectedId,\r\n );\r\n const isControlled = controlledId !== undefined;\r\n const selectedId = isControlled ? controlledId : internalId;\r\n\r\n const setSelectedId = useCallback(\r\n (id: string | null) => {\r\n if (!isControlled) setInternalId(id);\r\n onSelectionChange?.(id);\r\n },\r\n [isControlled, onSelectionChange],\r\n );\r\n\r\n const toggleSelection = useCallback(\r\n (id: string) => {\r\n const next = selectedId === id ? null : id;\r\n if (!isControlled) setInternalId(next);\r\n onSelectionChange?.(next);\r\n },\r\n [selectedId, isControlled, onSelectionChange],\r\n );\r\n\r\n const clearSelection = useCallback(() => {\r\n if (!isControlled) setInternalId(null);\r\n onSelectionChange?.(null);\r\n }, [isControlled, onSelectionChange]);\r\n\r\n const value = useMemo<SpatialSelectionContextValue>(\r\n () => ({ selectedId, setSelectedId, toggleSelection, clearSelection }),\r\n [selectedId, setSelectedId, toggleSelection, clearSelection],\r\n );\r\n\r\n return (\r\n <SpatialSelectionContextInternal.Provider value={value}>\r\n {children}\r\n </SpatialSelectionContextInternal.Provider>\r\n );\r\n}\r\n\r\n// ─── Hooks ────────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Read the spatial-selection context. Returns `null` when used outside\r\n * a `<SpatialSelectionProvider>` — components MUST handle that case so\r\n * they stay standalone-usable. Treat the provider as a progressive\r\n * enhancement, never a hard dependency.\r\n *\r\n * const ctx = useSpatialSelection();\r\n * const selectedId = ctx?.selectedId ?? null; // safe outside provider\r\n * const onSelect = ctx?.setSelectedId ?? noop; // safe outside provider\r\n */\r\nexport function useSpatialSelection(): SpatialSelectionContextValue | null {\r\n return useContext(SpatialSelectionContextInternal);\r\n}\r\n\r\n/**\r\n * Strict variant — throws if used outside a provider. Useful in the\r\n * provider's own children when the dependency is intentional.\r\n */\r\nexport function useSpatialSelectionStrict(): SpatialSelectionContextValue {\r\n const ctx = useContext(SpatialSelectionContextInternal);\r\n if (!ctx) {\r\n throw new Error(\r\n \"useSpatialSelectionStrict() called outside <SpatialSelectionProvider>. \" +\r\n \"Wrap the tree, or use useSpatialSelection() (returns null) for opt-in usage.\",\r\n );\r\n }\r\n return ctx;\r\n}\r\n\r\nexport default SpatialSelectionProvider;\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,IAAM,kCACJ,cAAmD,IAAI;;;;;;;;;;;AAyEzD,SAAgB,sBAA2D;CACzE,OAAO,WAAW,+BAA+B;AACnD"}