@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.
- package/CHANGELOG.md +419 -406
- package/dist/react/3d/CesiumCaptureSource.js.map +1 -1
- package/dist/react/3d/ZenSpace3D.js.map +1 -1
- package/dist/react/3d/ZenSpace3DCesium.js.map +1 -1
- package/dist/react/3d/ZenSpace3DShaders.js.map +1 -1
- package/dist/react/3d/ZenSpace3DUtils.js.map +1 -1
- package/dist/react/3d/capturePngAnalysis.js.map +1 -1
- package/dist/react/3d/threeLoader.js.map +1 -1
- package/dist/react/astro/ClassificationBanner.js.map +1 -1
- package/dist/react/astro/GlobalStatusBar.js.map +1 -1
- package/dist/react/astro/MissionClock.js.map +1 -1
- package/dist/react/astro/MonitoringIcon.js.map +1 -1
- package/dist/react/astro/Notification.js.map +1 -1
- package/dist/react/astro/Progress.js.map +1 -1
- package/dist/react/astro/SimulationControls.js.map +1 -1
- package/dist/react/astro/StatusIndicator.js.map +1 -1
- package/dist/react/astro/UnifiedTimeline.js.map +1 -1
- package/dist/react/cards/AccessCard.js.map +1 -1
- package/dist/react/cards/OrbitCard.js.map +1 -1
- package/dist/react/cards/SpacecraftCard.js.map +1 -1
- package/dist/react/cards/TelemetryCard.js.map +1 -1
- package/dist/react/cards/TelemetryStreamCard.js.map +1 -1
- package/dist/react/charts/GroundTrackMap.js.map +1 -1
- package/dist/react/charts/GroundTrackMapLeaflet.js.map +1 -1
- package/dist/react/charts/groundTrackMapLeafletTiles.js.map +1 -1
- package/dist/react/charts/groundTrackMapLeafletUtils.js.map +1 -1
- package/dist/react/charts/unified/AstroChart.js.map +1 -1
- package/dist/react/charts/unified/PowerOverviewChart.js.map +1 -1
- package/dist/react/charts/unified/domain.js.map +1 -1
- package/dist/react/charts/unified/generators.js.map +1 -1
- package/dist/react/charts/unified/presets.js.map +1 -1
- package/dist/react/charts/unified/sync.js.map +1 -1
- package/dist/react/charts/unified/theme.js.map +1 -1
- package/dist/react/charts/unified/useChartStream.js.map +1 -1
- package/dist/react/chatgpt/index.js.map +1 -1
- package/dist/react/context/CategoryContext.js.map +1 -1
- package/dist/react/context/DisplaySettingsContext.js.map +1 -1
- package/dist/react/context/SpatialSelectionContext.js.map +1 -1
- package/dist/react/core/data/DataTable.js.map +1 -1
- package/dist/react/core/data/DataValue.js.map +1 -1
- package/dist/react/core/data/propertyConfig.js.map +1 -1
- package/dist/react/core/display/AstroIcon.js.map +1 -1
- package/dist/react/core/display/Badge.js.map +1 -1
- package/dist/react/core/display/CardHeader.js.map +1 -1
- package/dist/react/core/display/Container.js.map +1 -1
- package/dist/react/core/display/CopyButton.js.map +1 -1
- package/dist/react/core/display/GlassCard.js.map +1 -1
- package/dist/react/core/display/HeaderIconWithStatus.js.map +1 -1
- package/dist/react/core/display/Icon.js.map +1 -1
- package/dist/react/core/display/Typography.js.map +1 -1
- package/dist/react/core/feedback/ConfirmDialog.js.map +1 -1
- package/dist/react/core/feedback/Dialog.js.map +1 -1
- package/dist/react/core/feedback/Toast.js.map +1 -1
- package/dist/react/core/inputs/Button.js.map +1 -1
- package/dist/react/core/inputs/Checkbox.js.map +1 -1
- package/dist/react/core/inputs/Input.js.map +1 -1
- package/dist/react/core/inputs/LimitsBar.js.map +1 -1
- package/dist/react/core/inputs/NumberInput.js.map +1 -1
- package/dist/react/core/inputs/PinInput.js.map +1 -1
- package/dist/react/core/inputs/Select.js.map +1 -1
- package/dist/react/core/inputs/Toggle.js.map +1 -1
- package/dist/react/core/layout/Box.js.map +1 -1
- package/dist/react/core/layout/Center.js.map +1 -1
- package/dist/react/core/layout/Divider.js.map +1 -1
- package/dist/react/core/layout/Flex.js.map +1 -1
- package/dist/react/core/layout/Grid.js.map +1 -1
- package/dist/react/core/layout/Spacer.js.map +1 -1
- package/dist/react/core/layout/Stack.js.map +1 -1
- package/dist/react/core/layout/responsive.js.map +1 -1
- package/dist/react/core/layout/useBreakpoint.js.map +1 -1
- package/dist/react/core/navigation/AppBar.js.map +1 -1
- package/dist/react/core/navigation/Pagination.js.map +1 -1
- package/dist/react/core/navigation/SideNav.js.map +1 -1
- package/dist/react/core/navigation/Tabs.js.map +1 -1
- package/dist/react/core/overlays/Popover.js.map +1 -1
- package/dist/react/core/overlays/SidePanel.js.map +1 -1
- package/dist/react/core/overlays/Tooltip.js.map +1 -1
- package/dist/react/core/widgets/ActivityPlanner.js.map +1 -1
- package/dist/react/core/widgets/Capture.js.map +1 -1
- package/dist/react/core/widgets/ChatPanel.js.map +1 -1
- package/dist/react/core/widgets/ColorPickerPanel.js.map +1 -1
- package/dist/react/core/widgets/CommandBuilder.js.map +1 -1
- package/dist/react/core/widgets/ConnectionForm.js.map +1 -1
- package/dist/react/core/widgets/FileExplorer.js.map +1 -1
- package/dist/react/core/widgets/HexViewer.js.map +1 -1
- package/dist/react/core/widgets/ImageFrame.js.map +1 -1
- package/dist/react/core/widgets/ImageGallery.js.map +1 -1
- package/dist/react/core/widgets/ImagePanel.js.map +1 -1
- package/dist/react/core/widgets/LogViewer.js.map +1 -1
- package/dist/react/core/widgets/MessageStream.js.map +1 -1
- package/dist/react/core/widgets/MissionCalendar.js.map +1 -1
- package/dist/react/core/widgets/PacketViewer.js.map +1 -1
- package/dist/react/hooks/useCompactMode.js.map +1 -1
- package/dist/react/hooks/useSimulationScene.js.map +1 -1
- package/dist/react/panels/LayerControlPanel.js.map +1 -1
- package/dist/react/panels/ObjectInventoryPanel.js.map +1 -1
- package/dist/react/shared/ErrorBoundary.js.map +1 -1
- package/dist/react/shared/Skeleton.js.map +1 -1
- package/dist/react/theme/ThemeProvider.js +2 -1
- package/dist/react/theme/ThemeProvider.js.map +1 -1
- package/dist/react/theme/cardAccent.js.map +1 -1
- package/dist/react/theme/config.js.map +1 -1
- package/dist/react/theme/types.js.map +1 -1
- package/dist/react/types.js.map +1 -1
- package/dist/react/utils/categoryPalette.js.map +1 -1
- package/dist/react/utils/index.js.map +1 -1
- package/dist/style.css +216 -216
- package/dist/tokens/core.js.map +1 -1
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/palettes/astro.js.map +1 -1
- package/dist/tokens/palettes/index.js.map +1 -1
- package/dist/tokens/palettes/tailwind.js.map +1 -1
- package/dist/tokens/palettes/zendir.js.map +1 -1
- package/dist/tokens/themes.js.map +1 -1
- package/dist/tokens/tokens.css +533 -533
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImageGallery.js","names":[],"sources":["../../../../src/react/core/widgets/ImageGallery.tsx"],"sourcesContent":["/**\n * @zendir/ui - ImageGallery Component\n *\n * Grid of image thumbnails with lightbox, download, delete, and pagination.\n * Designed for spacecraft image capture galleries in operator dashboards.\n *\n * Astro UX Compliance:\n * - Consistent card styling with GlassCard\n * - Status-based border colors\n * - Keyboard navigation (Escape to close lightbox)\n * - Focus management\n *\n * @example\n * ```tsx\n * <ImageGallery\n * images={[\n * { id: '1', name: 'capture_001.png', src: dataUrl, timestamp: Date.now() },\n * { id: '2', name: 'capture_002.png', src: dataUrl, timestamp: Date.now() },\n * ]}\n * columns={[2, 3, 4]}\n * onDownload={(img) => saveAs(img.src, img.name)}\n * onDelete={(img) => removeImage(img.id)}\n * pageSize={12}\n * />\n * ```\n */\n\nimport React, { memo, useState, useCallback, useEffect } from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\nimport { resolveResponsiveValue } from \"../layout/useBreakpoint\";\nimport type { ResponsiveValue } from \"../layout/useBreakpoint\";\nimport { Pagination } from \"../navigation/Pagination\";\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface GalleryImage {\n /** Unique identifier */\n id: string;\n /** Display name */\n name: string;\n /** Image source URL or data URL */\n src: string;\n /** Timestamp (ms since epoch) */\n timestamp?: number;\n /** Optional short label shown as a pill over the thumbnail (e.g. \"NEW\"). */\n badge?: string;\n /** Optional metadata */\n metadata?: Record<string, unknown>;\n}\n\nexport interface ImageGalleryProps {\n /** Array of images */\n images: GalleryImage[];\n /** Number of columns (responsive) */\n columns?: ResponsiveValue<number>;\n /** Images per page (0 = no pagination) */\n pageSize?: number;\n /** Download handler */\n onDownload?: (image: GalleryImage) => void;\n /** Delete handler (shows delete button when provided) */\n onDelete?: (image: GalleryImage) => void;\n /** Click handler */\n onClick?: (image: GalleryImage) => void;\n /** Show lightbox on click (default true) */\n lightbox?: boolean;\n /** Thumbnail height */\n thumbnailHeight?: number;\n /** Empty state message */\n emptyMessage?: string;\n /** New image badge count (shows badge on component) */\n newCount?: number;\n /** Custom style */\n style?: React.CSSProperties;\n}\n\nexport const ImageGallery = memo(function ImageGallery({\n images,\n columns = [2, 3, 4],\n pageSize = 12,\n onDownload,\n onDelete,\n onClick,\n lightbox = true,\n thumbnailHeight = 140,\n emptyMessage = \"No images captured\",\n newCount: _newCount,\n style,\n}: ImageGalleryProps): React.ReactElement {\n const { tokens } = useTheme();\n const { breakpoint } = useBreakpoint();\n const [currentPage, setCurrentPage] = useState(0);\n const [selectedImage, setSelectedImage] = useState<GalleryImage | null>(null);\n const [hoveredId, setHoveredId] = useState<string | null>(null);\n\n const cols = Array.isArray(columns)\n ? resolveResponsiveValue(columns, breakpoint)\n : columns;\n const totalPages = pageSize > 0 ? Math.ceil(images.length / pageSize) : 1;\n const pageImages =\n pageSize > 0\n ? images.slice(currentPage * pageSize, (currentPage + 1) * pageSize)\n : images;\n\n // Reset page if images change\n useEffect(() => {\n if (currentPage >= totalPages && totalPages > 0) {\n setCurrentPage(totalPages - 1);\n }\n }, [images.length, totalPages, currentPage]);\n\n // Lightbox keyboard\n useEffect(() => {\n if (!selectedImage) return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setSelectedImage(null);\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [selectedImage]);\n\n const handleImageClick = useCallback(\n (img: GalleryImage) => {\n onClick?.(img);\n if (lightbox) setSelectedImage(img);\n },\n [onClick, lightbox],\n );\n\n const formatTime = (ts?: number) => {\n if (!ts) return \"\";\n const d = new Date(ts);\n return d.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n };\n\n // Action button helper\n const ActionBtn = ({\n icon,\n label,\n onClick: handler,\n color,\n }: {\n icon: React.ReactNode;\n label: string;\n onClick: () => void;\n color?: string;\n }) => (\n <button\n aria-label={label}\n onClick={(e) => {\n e.stopPropagation();\n handler();\n }}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n width: 28,\n height: 28,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.sm,\n backgroundColor: `${tokens.colors.background.elevated}e0`,\n color: color || tokens.colors.text.secondary,\n cursor: \"pointer\",\n transition: tokens.animation.fast,\n backdropFilter: \"blur(4px)\",\n }}\n >\n {icon}\n </button>\n );\n\n // Download icon\n const downloadIcon = (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n >\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\n <polyline points=\"7 10 12 15 17 10\" />\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\" />\n </svg>\n );\n // Delete icon\n const deleteIcon = (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n >\n <polyline points=\"3 6 5 6 21 6\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n </svg>\n );\n\n return (\n <div style={{ fontFamily: tokens.typography.fontFamily.primary, ...style }}>\n {/* Empty state */}\n {images.length === 0 && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: 200,\n color: tokens.colors.text.tertiary,\n fontSize: tokens.typography.fontSize.sm,\n border: `1px dashed ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n }}\n >\n {emptyMessage}\n </div>\n )}\n\n {/* Grid */}\n {pageImages.length > 0 && (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(${cols}, 1fr)`,\n gap: tokens.spacing.sm,\n }}\n >\n {pageImages.map((img) => {\n const isHovered = hoveredId === img.id;\n return (\n <div\n key={img.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`View image ${img.name || img.id}`}\n onClick={() => handleImageClick(img)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n handleImageClick(img);\n }\n }}\n onMouseEnter={() => setHoveredId(img.id)}\n onMouseLeave={() => setHoveredId(null)}\n style={{\n position: \"relative\",\n borderRadius: tokens.borderRadius.md,\n border: `1px solid ${tokens.colors.border.muted}`,\n overflow: \"hidden\",\n cursor: \"pointer\",\n transition: tokens.animation.fast,\n transform: isHovered ? \"translateY(-2px)\" : \"none\",\n boxShadow: isHovered ? tokens.shadows.md : \"none\",\n backgroundColor: tokens.colors.background.surface,\n }}\n >\n {/* Per-image badge (e.g. newest capture) — overlays the thumbnail */}\n {img.badge ? (\n <span\n style={{\n position: \"absolute\",\n top: tokens.spacing.xs,\n left: tokens.spacing.xs,\n zIndex: 1,\n padding: `2px ${tokens.spacing.xs}`,\n borderRadius: tokens.borderRadius.sm,\n backgroundColor: tokens.colors.accent.primary,\n color: \"#fff\",\n fontSize: tokens.typography.fontSize.micro,\n fontWeight: tokens.typography.fontWeight.bold,\n letterSpacing: \"0.06em\",\n textTransform: \"uppercase\",\n lineHeight: 1.4,\n pointerEvents: \"none\",\n boxShadow: tokens.shadows.sm,\n }}\n >\n {img.badge}\n </span>\n ) : null}\n\n {/* Thumbnail */}\n <div\n style={{\n height: thumbnailHeight,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n overflow: \"hidden\",\n backgroundColor: tokens.colors.background.base,\n }}\n >\n <img\n src={img.src}\n alt={img.name}\n loading=\"lazy\"\n onError={(e) => {\n const target = e.target as HTMLImageElement;\n target.style.display = \"none\";\n target.parentElement!.innerHTML = `<span style=\"color:${tokens.colors.text.tertiary};font-size:${tokens.typography.fontSize.xxs}\">Image unavailable</span>`;\n }}\n style={{\n maxWidth: \"100%\",\n maxHeight: \"100%\",\n objectFit: \"contain\",\n }}\n />\n </div>\n\n {/* Info bar */}\n <div\n style={{\n padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,\n borderTop: `1px solid ${tokens.colors.border.muted}`,\n }}\n >\n <div\n style={{\n fontSize: tokens.typography.fontSize.xxs,\n fontWeight: tokens.typography.fontWeight.medium,\n color: tokens.colors.text.primary,\n whiteSpace: \"nowrap\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {img.name}\n </div>\n {img.timestamp && (\n <div\n style={{\n fontSize: tokens.typography.fontSize.micro,\n color: tokens.colors.text.tertiary,\n fontFamily: tokens.typography.fontFamily.mono,\n }}\n >\n {formatTime(img.timestamp)}\n </div>\n )}\n </div>\n\n {/* Hover actions */}\n {isHovered && (\n <div\n style={{\n position: \"absolute\",\n top: tokens.spacing.xs,\n right: tokens.spacing.xs,\n display: \"flex\",\n gap: \"4px\",\n }}\n >\n {onDownload && (\n <ActionBtn\n icon={downloadIcon}\n label=\"Download\"\n onClick={() => onDownload(img)}\n />\n )}\n {onDelete && (\n <ActionBtn\n icon={deleteIcon}\n label=\"Delete\"\n onClick={() => onDelete(img)}\n color={tokens.colors.status.critical}\n />\n )}\n </div>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <div\n style={{\n display: \"flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n marginTop: tokens.spacing.md,\n }}\n >\n <Pagination\n currentPage={currentPage + 1}\n totalPages={totalPages}\n onChange={(page) => setCurrentPage(Math.max(0, page - 1))}\n showFirstLast={false}\n displayMode=\"text\"\n size=\"small\"\n />\n </div>\n )}\n\n {/* Lightbox */}\n {selectedImage && (\n <>\n <div\n aria-hidden=\"true\"\n onClick={() => setSelectedImage(null)}\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 9998,\n backgroundColor: `${tokens.colors.background.base}e0`,\n backdropFilter: \"blur(8px)\",\n animation: `zendir-lb-fade 200ms ${tokens.animation.easing.default}`,\n }}\n />\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`Image: ${selectedImage.name}`}\n onClick={() => setSelectedImage(null)}\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 9999,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: tokens.spacing.xl,\n }}\n >\n <img\n src={selectedImage.src}\n alt={selectedImage.name}\n onClick={(e) => e.stopPropagation()}\n style={{\n maxWidth: \"90vw\",\n maxHeight: \"80vh\",\n objectFit: \"contain\",\n borderRadius: tokens.borderRadius.md,\n boxShadow: tokens.shadows.xl,\n animation: `zendir-lb-scale 250ms ${tokens.animation.easing.default}`,\n }}\n />\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.md,\n marginTop: tokens.spacing.md,\n }}\n onClick={(e) => e.stopPropagation()}\n >\n <span\n style={{\n color: tokens.colors.text.primary,\n fontSize: tokens.typography.fontSize.sm,\n fontWeight: tokens.typography.fontWeight.medium,\n }}\n >\n {selectedImage.name}\n </span>\n {onDownload && (\n <button\n onClick={() => onDownload(selectedImage)}\n style={{\n padding: \"6px 16px\",\n fontSize: tokens.typography.fontSize.xs,\n color: tokens.colors.text.primary,\n backgroundColor: `${tokens.colors.accent.primary}20`,\n border: `1px solid ${tokens.colors.accent.primary}40`,\n borderRadius: tokens.borderRadius.md,\n cursor: \"pointer\",\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n Download\n </button>\n )}\n {onDelete && (\n <button\n onClick={() => {\n onDelete(selectedImage);\n setSelectedImage(null);\n }}\n style={{\n padding: \"6px 16px\",\n fontSize: tokens.typography.fontSize.xs,\n color: tokens.colors.status.critical,\n backgroundColor: `${tokens.colors.status.critical}15`,\n border: `1px solid ${tokens.colors.status.critical}30`,\n borderRadius: tokens.borderRadius.md,\n cursor: \"pointer\",\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n Delete\n </button>\n )}\n </div>\n </div>\n <style>{`\n @keyframes zendir-lb-fade { from { opacity: 0; } to { opacity: 1; } }\n @keyframes zendir-lb-scale { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }\n `}</style>\n </>\n )}\n </div>\n );\n});\n\nexport default ImageGallery;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ImageGallery.js","names":[],"sources":["../../../../src/react/core/widgets/ImageGallery.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - ImageGallery Component\r\n *\r\n * Grid of image thumbnails with lightbox, download, delete, and pagination.\r\n * Designed for spacecraft image capture galleries in operator dashboards.\r\n *\r\n * Astro UX Compliance:\r\n * - Consistent card styling with GlassCard\r\n * - Status-based border colors\r\n * - Keyboard navigation (Escape to close lightbox)\r\n * - Focus management\r\n *\r\n * @example\r\n * ```tsx\r\n * <ImageGallery\r\n * images={[\r\n * { id: '1', name: 'capture_001.png', src: dataUrl, timestamp: Date.now() },\r\n * { id: '2', name: 'capture_002.png', src: dataUrl, timestamp: Date.now() },\r\n * ]}\r\n * columns={[2, 3, 4]}\r\n * onDownload={(img) => saveAs(img.src, img.name)}\r\n * onDelete={(img) => removeImage(img.id)}\r\n * pageSize={12}\r\n * />\r\n * ```\r\n */\r\n\r\nimport React, { memo, useState, useCallback, useEffect } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { useBreakpoint } from \"../layout/useBreakpoint\";\r\nimport { resolveResponsiveValue } from \"../layout/useBreakpoint\";\r\nimport type { ResponsiveValue } from \"../layout/useBreakpoint\";\r\nimport { Pagination } from \"../navigation/Pagination\";\r\n\r\n// ─── Types ───────────────────────────────────────────────────────────────────\r\n\r\nexport interface GalleryImage {\r\n /** Unique identifier */\r\n id: string;\r\n /** Display name */\r\n name: string;\r\n /** Image source URL or data URL */\r\n src: string;\r\n /** Timestamp (ms since epoch) */\r\n timestamp?: number;\r\n /** Optional short label shown as a pill over the thumbnail (e.g. \"NEW\"). */\r\n badge?: string;\r\n /** Optional metadata */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\nexport interface ImageGalleryProps {\r\n /** Array of images */\r\n images: GalleryImage[];\r\n /** Number of columns (responsive) */\r\n columns?: ResponsiveValue<number>;\r\n /** Images per page (0 = no pagination) */\r\n pageSize?: number;\r\n /** Download handler */\r\n onDownload?: (image: GalleryImage) => void;\r\n /** Delete handler (shows delete button when provided) */\r\n onDelete?: (image: GalleryImage) => void;\r\n /** Click handler */\r\n onClick?: (image: GalleryImage) => void;\r\n /** Show lightbox on click (default true) */\r\n lightbox?: boolean;\r\n /** Thumbnail height */\r\n thumbnailHeight?: number;\r\n /** Empty state message */\r\n emptyMessage?: string;\r\n /** New image badge count (shows badge on component) */\r\n newCount?: number;\r\n /** Custom style */\r\n style?: React.CSSProperties;\r\n}\r\n\r\nexport const ImageGallery = memo(function ImageGallery({\r\n images,\r\n columns = [2, 3, 4],\r\n pageSize = 12,\r\n onDownload,\r\n onDelete,\r\n onClick,\r\n lightbox = true,\r\n thumbnailHeight = 140,\r\n emptyMessage = \"No images captured\",\r\n newCount: _newCount,\r\n style,\r\n}: ImageGalleryProps): React.ReactElement {\r\n const { tokens } = useTheme();\r\n const { breakpoint } = useBreakpoint();\r\n const [currentPage, setCurrentPage] = useState(0);\r\n const [selectedImage, setSelectedImage] = useState<GalleryImage | null>(null);\r\n const [hoveredId, setHoveredId] = useState<string | null>(null);\r\n\r\n const cols = Array.isArray(columns)\r\n ? resolveResponsiveValue(columns, breakpoint)\r\n : columns;\r\n const totalPages = pageSize > 0 ? Math.ceil(images.length / pageSize) : 1;\r\n const pageImages =\r\n pageSize > 0\r\n ? images.slice(currentPage * pageSize, (currentPage + 1) * pageSize)\r\n : images;\r\n\r\n // Reset page if images change\r\n useEffect(() => {\r\n if (currentPage >= totalPages && totalPages > 0) {\r\n setCurrentPage(totalPages - 1);\r\n }\r\n }, [images.length, totalPages, currentPage]);\r\n\r\n // Lightbox keyboard\r\n useEffect(() => {\r\n if (!selectedImage) return;\r\n const handler = (e: KeyboardEvent) => {\r\n if (e.key === \"Escape\") setSelectedImage(null);\r\n };\r\n document.addEventListener(\"keydown\", handler);\r\n return () => document.removeEventListener(\"keydown\", handler);\r\n }, [selectedImage]);\r\n\r\n const handleImageClick = useCallback(\r\n (img: GalleryImage) => {\r\n onClick?.(img);\r\n if (lightbox) setSelectedImage(img);\r\n },\r\n [onClick, lightbox],\r\n );\r\n\r\n const formatTime = (ts?: number) => {\r\n if (!ts) return \"\";\r\n const d = new Date(ts);\r\n return d.toLocaleTimeString([], {\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n });\r\n };\r\n\r\n // Action button helper\r\n const ActionBtn = ({\r\n icon,\r\n label,\r\n onClick: handler,\r\n color,\r\n }: {\r\n icon: React.ReactNode;\r\n label: string;\r\n onClick: () => void;\r\n color?: string;\r\n }) => (\r\n <button\r\n aria-label={label}\r\n onClick={(e) => {\r\n e.stopPropagation();\r\n handler();\r\n }}\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n width: 28,\r\n height: 28,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.sm,\r\n backgroundColor: `${tokens.colors.background.elevated}e0`,\r\n color: color || tokens.colors.text.secondary,\r\n cursor: \"pointer\",\r\n transition: tokens.animation.fast,\r\n backdropFilter: \"blur(4px)\",\r\n }}\r\n >\r\n {icon}\r\n </button>\r\n );\r\n\r\n // Download icon\r\n const downloadIcon = (\r\n <svg\r\n width=\"14\"\r\n height=\"14\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n strokeWidth=\"2\"\r\n strokeLinecap=\"round\"\r\n >\r\n <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\" />\r\n <polyline points=\"7 10 12 15 17 10\" />\r\n <line x1=\"12\" y1=\"15\" x2=\"12\" y2=\"3\" />\r\n </svg>\r\n );\r\n // Delete icon\r\n const deleteIcon = (\r\n <svg\r\n width=\"14\"\r\n height=\"14\"\r\n viewBox=\"0 0 24 24\"\r\n fill=\"none\"\r\n stroke=\"currentColor\"\r\n strokeWidth=\"2\"\r\n strokeLinecap=\"round\"\r\n >\r\n <polyline points=\"3 6 5 6 21 6\" />\r\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\r\n </svg>\r\n );\r\n\r\n return (\r\n <div style={{ fontFamily: tokens.typography.fontFamily.primary, ...style }}>\r\n {/* Empty state */}\r\n {images.length === 0 && (\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n height: 200,\r\n color: tokens.colors.text.tertiary,\r\n fontSize: tokens.typography.fontSize.sm,\r\n border: `1px dashed ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n }}\r\n >\r\n {emptyMessage}\r\n </div>\r\n )}\r\n\r\n {/* Grid */}\r\n {pageImages.length > 0 && (\r\n <div\r\n style={{\r\n display: \"grid\",\r\n gridTemplateColumns: `repeat(${cols}, 1fr)`,\r\n gap: tokens.spacing.sm,\r\n }}\r\n >\r\n {pageImages.map((img) => {\r\n const isHovered = hoveredId === img.id;\r\n return (\r\n <div\r\n key={img.id}\r\n role=\"button\"\r\n tabIndex={0}\r\n aria-label={`View image ${img.name || img.id}`}\r\n onClick={() => handleImageClick(img)}\r\n onKeyDown={(e) => {\r\n if (e.key === \"Enter\" || e.key === \" \") {\r\n e.preventDefault();\r\n handleImageClick(img);\r\n }\r\n }}\r\n onMouseEnter={() => setHoveredId(img.id)}\r\n onMouseLeave={() => setHoveredId(null)}\r\n style={{\r\n position: \"relative\",\r\n borderRadius: tokens.borderRadius.md,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n overflow: \"hidden\",\r\n cursor: \"pointer\",\r\n transition: tokens.animation.fast,\r\n transform: isHovered ? \"translateY(-2px)\" : \"none\",\r\n boxShadow: isHovered ? tokens.shadows.md : \"none\",\r\n backgroundColor: tokens.colors.background.surface,\r\n }}\r\n >\r\n {/* Per-image badge (e.g. newest capture) — overlays the thumbnail */}\r\n {img.badge ? (\r\n <span\r\n style={{\r\n position: \"absolute\",\r\n top: tokens.spacing.xs,\r\n left: tokens.spacing.xs,\r\n zIndex: 1,\r\n padding: `2px ${tokens.spacing.xs}`,\r\n borderRadius: tokens.borderRadius.sm,\r\n backgroundColor: tokens.colors.accent.primary,\r\n color: \"#fff\",\r\n fontSize: tokens.typography.fontSize.micro,\r\n fontWeight: tokens.typography.fontWeight.bold,\r\n letterSpacing: \"0.06em\",\r\n textTransform: \"uppercase\",\r\n lineHeight: 1.4,\r\n pointerEvents: \"none\",\r\n boxShadow: tokens.shadows.sm,\r\n }}\r\n >\r\n {img.badge}\r\n </span>\r\n ) : null}\r\n\r\n {/* Thumbnail */}\r\n <div\r\n style={{\r\n height: thumbnailHeight,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n overflow: \"hidden\",\r\n backgroundColor: tokens.colors.background.base,\r\n }}\r\n >\r\n <img\r\n src={img.src}\r\n alt={img.name}\r\n loading=\"lazy\"\r\n onError={(e) => {\r\n const target = e.target as HTMLImageElement;\r\n target.style.display = \"none\";\r\n target.parentElement!.innerHTML = `<span style=\"color:${tokens.colors.text.tertiary};font-size:${tokens.typography.fontSize.xxs}\">Image unavailable</span>`;\r\n }}\r\n style={{\r\n maxWidth: \"100%\",\r\n maxHeight: \"100%\",\r\n objectFit: \"contain\",\r\n }}\r\n />\r\n </div>\r\n\r\n {/* Info bar */}\r\n <div\r\n style={{\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.sm}`,\r\n borderTop: `1px solid ${tokens.colors.border.muted}`,\r\n }}\r\n >\r\n <div\r\n style={{\r\n fontSize: tokens.typography.fontSize.xxs,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n color: tokens.colors.text.primary,\r\n whiteSpace: \"nowrap\",\r\n overflow: \"hidden\",\r\n textOverflow: \"ellipsis\",\r\n }}\r\n >\r\n {img.name}\r\n </div>\r\n {img.timestamp && (\r\n <div\r\n style={{\r\n fontSize: tokens.typography.fontSize.micro,\r\n color: tokens.colors.text.tertiary,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n }}\r\n >\r\n {formatTime(img.timestamp)}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Hover actions */}\r\n {isHovered && (\r\n <div\r\n style={{\r\n position: \"absolute\",\r\n top: tokens.spacing.xs,\r\n right: tokens.spacing.xs,\r\n display: \"flex\",\r\n gap: \"4px\",\r\n }}\r\n >\r\n {onDownload && (\r\n <ActionBtn\r\n icon={downloadIcon}\r\n label=\"Download\"\r\n onClick={() => onDownload(img)}\r\n />\r\n )}\r\n {onDelete && (\r\n <ActionBtn\r\n icon={deleteIcon}\r\n label=\"Delete\"\r\n onClick={() => onDelete(img)}\r\n color={tokens.colors.status.critical}\r\n />\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n })}\r\n </div>\r\n )}\r\n\r\n {/* Pagination */}\r\n {totalPages > 1 && (\r\n <div\r\n style={{\r\n display: \"flex\",\r\n justifyContent: \"center\",\r\n alignItems: \"center\",\r\n marginTop: tokens.spacing.md,\r\n }}\r\n >\r\n <Pagination\r\n currentPage={currentPage + 1}\r\n totalPages={totalPages}\r\n onChange={(page) => setCurrentPage(Math.max(0, page - 1))}\r\n showFirstLast={false}\r\n displayMode=\"text\"\r\n size=\"small\"\r\n />\r\n </div>\r\n )}\r\n\r\n {/* Lightbox */}\r\n {selectedImage && (\r\n <>\r\n <div\r\n aria-hidden=\"true\"\r\n onClick={() => setSelectedImage(null)}\r\n style={{\r\n position: \"fixed\",\r\n inset: 0,\r\n zIndex: 9998,\r\n backgroundColor: `${tokens.colors.background.base}e0`,\r\n backdropFilter: \"blur(8px)\",\r\n animation: `zendir-lb-fade 200ms ${tokens.animation.easing.default}`,\r\n }}\r\n />\r\n <div\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-label={`Image: ${selectedImage.name}`}\r\n onClick={() => setSelectedImage(null)}\r\n style={{\r\n position: \"fixed\",\r\n inset: 0,\r\n zIndex: 9999,\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n padding: tokens.spacing.xl,\r\n }}\r\n >\r\n <img\r\n src={selectedImage.src}\r\n alt={selectedImage.name}\r\n onClick={(e) => e.stopPropagation()}\r\n style={{\r\n maxWidth: \"90vw\",\r\n maxHeight: \"80vh\",\r\n objectFit: \"contain\",\r\n borderRadius: tokens.borderRadius.md,\r\n boxShadow: tokens.shadows.xl,\r\n animation: `zendir-lb-scale 250ms ${tokens.animation.easing.default}`,\r\n }}\r\n />\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.md,\r\n marginTop: tokens.spacing.md,\r\n }}\r\n onClick={(e) => e.stopPropagation()}\r\n >\r\n <span\r\n style={{\r\n color: tokens.colors.text.primary,\r\n fontSize: tokens.typography.fontSize.sm,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n }}\r\n >\r\n {selectedImage.name}\r\n </span>\r\n {onDownload && (\r\n <button\r\n onClick={() => onDownload(selectedImage)}\r\n style={{\r\n padding: \"6px 16px\",\r\n fontSize: tokens.typography.fontSize.xs,\r\n color: tokens.colors.text.primary,\r\n backgroundColor: `${tokens.colors.accent.primary}20`,\r\n border: `1px solid ${tokens.colors.accent.primary}40`,\r\n borderRadius: tokens.borderRadius.md,\r\n cursor: \"pointer\",\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n Download\r\n </button>\r\n )}\r\n {onDelete && (\r\n <button\r\n onClick={() => {\r\n onDelete(selectedImage);\r\n setSelectedImage(null);\r\n }}\r\n style={{\r\n padding: \"6px 16px\",\r\n fontSize: tokens.typography.fontSize.xs,\r\n color: tokens.colors.status.critical,\r\n backgroundColor: `${tokens.colors.status.critical}15`,\r\n border: `1px solid ${tokens.colors.status.critical}30`,\r\n borderRadius: tokens.borderRadius.md,\r\n cursor: \"pointer\",\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n Delete\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n <style>{`\r\n @keyframes zendir-lb-fade { from { opacity: 0; } to { opacity: 1; } }\r\n @keyframes zendir-lb-scale { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }\r\n `}</style>\r\n </>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nexport default ImageGallery;\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4EA,IAAa,eAAe,KAAK,SAAS,aAAa,EACrD,QACA,UAAU;CAAC;CAAG;CAAG;AAAC,GAClB,WAAW,IACX,YACA,UACA,SACA,WAAW,MACX,kBAAkB,KAClB,eAAe,sBACf,UAAU,WACV,SACwC;CACxC,MAAM,EAAE,WAAW,SAAS;CAC5B,MAAM,EAAE,eAAe,cAAc;CACrC,MAAM,CAAC,aAAa,kBAAkB,SAAS,CAAC;CAChD,MAAM,CAAC,eAAe,oBAAoB,SAA8B,IAAI;CAC5E,MAAM,CAAC,WAAW,gBAAgB,SAAwB,IAAI;CAE9D,MAAM,OAAO,MAAM,QAAQ,OAAO,IAC9B,uBAAuB,SAAS,UAAU,IAC1C;CACJ,MAAM,aAAa,WAAW,IAAI,KAAK,KAAK,OAAO,SAAS,QAAQ,IAAI;CACxE,MAAM,aACJ,WAAW,IACP,OAAO,MAAM,cAAc,WAAW,cAAc,KAAK,QAAQ,IACjE;CAGN,gBAAgB;EACd,IAAI,eAAe,cAAc,aAAa,GAC5C,eAAe,aAAa,CAAC;CAEjC,GAAG;EAAC,OAAO;EAAQ;EAAY;CAAW,CAAC;CAG3C,gBAAgB;EACd,IAAI,CAAC,eAAe;EACpB,MAAM,WAAW,MAAqB;GACpC,IAAI,EAAE,QAAQ,UAAU,iBAAiB,IAAI;EAC/C;EACA,SAAS,iBAAiB,WAAW,OAAO;EAC5C,aAAa,SAAS,oBAAoB,WAAW,OAAO;CAC9D,GAAG,CAAC,aAAa,CAAC;CAElB,MAAM,mBAAmB,aACtB,QAAsB;EACrB,UAAU,GAAG;EACb,IAAI,UAAU,iBAAiB,GAAG;CACpC,GACA,CAAC,SAAS,QAAQ,CACpB;CAEA,MAAM,cAAc,OAAgB;EAClC,IAAI,CAAC,IAAI,OAAO;EAEhB,OAAO,IADO,KAAK,EACZ,CAAA,CAAE,mBAAmB,CAAC,GAAG;GAC9B,MAAM;GACN,QAAQ;GACR,QAAQ;EACV,CAAC;CACH;CAGA,MAAM,aAAa,EACjB,MACA,OACA,SAAS,SACT,YAOA,oBAAC,UAAD;EACE,cAAY;EACZ,UAAU,MAAM;GACd,EAAE,gBAAgB;GAClB,QAAQ;EACV;EACA,OAAO;GACL,SAAS;GACT,YAAY;GACZ,gBAAgB;GAChB,OAAO;GACP,QAAQ;GACR,QAAQ,aAAa,OAAO,OAAO,OAAO;GAC1C,cAAc,OAAO,aAAa;GAClC,iBAAiB,GAAG,OAAO,OAAO,WAAW,SAAS;GACtD,OAAO,SAAS,OAAO,OAAO,KAAK;GACnC,QAAQ;GACR,YAAY,OAAO,UAAU;GAC7B,gBAAgB;EAClB;YAEC;CACK,CAAA;CAIV,MAAM,eACJ,qBAAC,OAAD;EACE,OAAM;EACN,QAAO;EACP,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;YAPhB;GASE,oBAAC,QAAD,EAAM,GAAE,4CAA6C,CAAA;GACrD,oBAAC,YAAD,EAAU,QAAO,mBAAoB,CAAA;GACrC,oBAAC,QAAD;IAAM,IAAG;IAAK,IAAG;IAAK,IAAG;IAAK,IAAG;GAAK,CAAA;EACnC;;CAGP,MAAM,aACJ,qBAAC,OAAD;EACE,OAAM;EACN,QAAO;EACP,SAAQ;EACR,MAAK;EACL,QAAO;EACP,aAAY;EACZ,eAAc;YAPhB,CASE,oBAAC,YAAD,EAAU,QAAO,eAAgB,CAAA,GACjC,oBAAC,QAAD,EAAM,GAAE,iFAAkF,CAAA,CACvF;;CAGP,OACE,qBAAC,OAAD;EAAK,OAAO;GAAE,YAAY,OAAO,WAAW,WAAW;GAAS,GAAG;EAAM;YAAzE;GAEG,OAAO,WAAW,KACjB,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,YAAY;KACZ,gBAAgB;KAChB,QAAQ;KACR,OAAO,OAAO,OAAO,KAAK;KAC1B,UAAU,OAAO,WAAW,SAAS;KACrC,QAAQ,cAAc,OAAO,OAAO,OAAO;KAC3C,cAAc,OAAO,aAAa;IACpC;cAEC;GACE,CAAA;GAIN,WAAW,SAAS,KACnB,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,qBAAqB,UAAU,KAAK;KACpC,KAAK,OAAO,QAAQ;IACtB;cAEC,WAAW,KAAK,QAAQ;KACvB,MAAM,YAAY,cAAc,IAAI;KACpC,OACE,qBAAC,OAAD;MAEE,MAAK;MACL,UAAU;MACV,cAAY,cAAc,IAAI,QAAQ,IAAI;MAC1C,eAAe,iBAAiB,GAAG;MACnC,YAAY,MAAM;OAChB,IAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;QACtC,EAAE,eAAe;QACjB,iBAAiB,GAAG;OACtB;MACF;MACA,oBAAoB,aAAa,IAAI,EAAE;MACvC,oBAAoB,aAAa,IAAI;MACrC,OAAO;OACL,UAAU;OACV,cAAc,OAAO,aAAa;OAClC,QAAQ,aAAa,OAAO,OAAO,OAAO;OAC1C,UAAU;OACV,QAAQ;OACR,YAAY,OAAO,UAAU;OAC7B,WAAW,YAAY,qBAAqB;OAC5C,WAAW,YAAY,OAAO,QAAQ,KAAK;OAC3C,iBAAiB,OAAO,OAAO,WAAW;MAC5C;gBAxBF;OA2BG,IAAI,QACH,oBAAC,QAAD;QACE,OAAO;SACL,UAAU;SACV,KAAK,OAAO,QAAQ;SACpB,MAAM,OAAO,QAAQ;SACrB,QAAQ;SACR,SAAS,OAAO,OAAO,QAAQ;SAC/B,cAAc,OAAO,aAAa;SAClC,iBAAiB,OAAO,OAAO,OAAO;SACtC,OAAO;SACP,UAAU,OAAO,WAAW,SAAS;SACrC,YAAY,OAAO,WAAW,WAAW;SACzC,eAAe;SACf,eAAe;SACf,YAAY;SACZ,eAAe;SACf,WAAW,OAAO,QAAQ;QAC5B;kBAEC,IAAI;OACD,CAAA,IACJ;OAGJ,oBAAC,OAAD;QACE,OAAO;SACL,QAAQ;SACR,SAAS;SACT,YAAY;SACZ,gBAAgB;SAChB,UAAU;SACV,iBAAiB,OAAO,OAAO,WAAW;QAC5C;kBAEA,oBAAC,OAAD;SACE,KAAK,IAAI;SACT,KAAK,IAAI;SACT,SAAQ;SACR,UAAU,MAAM;UACd,MAAM,SAAS,EAAE;UACjB,OAAO,MAAM,UAAU;UACvB,OAAO,cAAe,YAAY,sBAAsB,OAAO,OAAO,KAAK,SAAS,aAAa,OAAO,WAAW,SAAS,IAAI;SAClI;SACA,OAAO;UACL,UAAU;UACV,WAAW;UACX,WAAW;SACb;QACD,CAAA;OACE,CAAA;OAGL,qBAAC,OAAD;QACE,OAAO;SACL,SAAS,GAAG,OAAO,QAAQ,GAAG,GAAG,OAAO,QAAQ;SAChD,WAAW,aAAa,OAAO,OAAO,OAAO;QAC/C;kBAJF,CAME,oBAAC,OAAD;SACE,OAAO;UACL,UAAU,OAAO,WAAW,SAAS;UACrC,YAAY,OAAO,WAAW,WAAW;UACzC,OAAO,OAAO,OAAO,KAAK;UAC1B,YAAY;UACZ,UAAU;UACV,cAAc;SAChB;mBAEC,IAAI;QACF,CAAA,GACJ,IAAI,aACH,oBAAC,OAAD;SACE,OAAO;UACL,UAAU,OAAO,WAAW,SAAS;UACrC,OAAO,OAAO,OAAO,KAAK;UAC1B,YAAY,OAAO,WAAW,WAAW;SAC3C;mBAEC,WAAW,IAAI,SAAS;QACtB,CAAA,CAEJ;;OAGJ,aACC,qBAAC,OAAD;QACE,OAAO;SACL,UAAU;SACV,KAAK,OAAO,QAAQ;SACpB,OAAO,OAAO,QAAQ;SACtB,SAAS;SACT,KAAK;QACP;kBAPF,CASG,cACC,oBAAC,WAAD;SACE,MAAM;SACN,OAAM;SACN,eAAe,WAAW,GAAG;QAC9B,CAAA,GAEF,YACC,oBAAC,WAAD;SACE,MAAM;SACN,OAAM;SACN,eAAe,SAAS,GAAG;SAC3B,OAAO,OAAO,OAAO,OAAO;QAC7B,CAAA,CAEA;;MAEJ;QA1IE,IAAI,EA0IN;IAET,CAAC;GACE,CAAA;GAIN,aAAa,KACZ,oBAAC,OAAD;IACE,OAAO;KACL,SAAS;KACT,gBAAgB;KAChB,YAAY;KACZ,WAAW,OAAO,QAAQ;IAC5B;cAEA,oBAAC,YAAD;KACE,aAAa,cAAc;KACf;KACZ,WAAW,SAAS,eAAe,KAAK,IAAI,GAAG,OAAO,CAAC,CAAC;KACxD,eAAe;KACf,aAAY;KACZ,MAAK;IACN,CAAA;GACE,CAAA;GAIN,iBACC,qBAAA,UAAA,EAAA,UAAA;IACE,oBAAC,OAAD;KACE,eAAY;KACZ,eAAe,iBAAiB,IAAI;KACpC,OAAO;MACL,UAAU;MACV,OAAO;MACP,QAAQ;MACR,iBAAiB,GAAG,OAAO,OAAO,WAAW,KAAK;MAClD,gBAAgB;MAChB,WAAW,wBAAwB,OAAO,UAAU,OAAO;KAC7D;IACD,CAAA;IACD,qBAAC,OAAD;KACE,MAAK;KACL,cAAW;KACX,cAAY,UAAU,cAAc;KACpC,eAAe,iBAAiB,IAAI;KACpC,OAAO;MACL,UAAU;MACV,OAAO;MACP,QAAQ;MACR,SAAS;MACT,eAAe;MACf,YAAY;MACZ,gBAAgB;MAChB,SAAS,OAAO,QAAQ;KAC1B;eAdF,CAgBE,oBAAC,OAAD;MACE,KAAK,cAAc;MACnB,KAAK,cAAc;MACnB,UAAU,MAAM,EAAE,gBAAgB;MAClC,OAAO;OACL,UAAU;OACV,WAAW;OACX,WAAW;OACX,cAAc,OAAO,aAAa;OAClC,WAAW,OAAO,QAAQ;OAC1B,WAAW,yBAAyB,OAAO,UAAU,OAAO;MAC9D;KACD,CAAA,GACD,qBAAC,OAAD;MACE,OAAO;OACL,SAAS;OACT,YAAY;OACZ,KAAK,OAAO,QAAQ;OACpB,WAAW,OAAO,QAAQ;MAC5B;MACA,UAAU,MAAM,EAAE,gBAAgB;gBAPpC;OASE,oBAAC,QAAD;QACE,OAAO;SACL,OAAO,OAAO,OAAO,KAAK;SAC1B,UAAU,OAAO,WAAW,SAAS;SACrC,YAAY,OAAO,WAAW,WAAW;QAC3C;kBAEC,cAAc;OACX,CAAA;OACL,cACC,oBAAC,UAAD;QACE,eAAe,WAAW,aAAa;QACvC,OAAO;SACL,SAAS;SACT,UAAU,OAAO,WAAW,SAAS;SACrC,OAAO,OAAO,OAAO,KAAK;SAC1B,iBAAiB,GAAG,OAAO,OAAO,OAAO,QAAQ;SACjD,QAAQ,aAAa,OAAO,OAAO,OAAO,QAAQ;SAClD,cAAc,OAAO,aAAa;SAClC,QAAQ;SACR,YAAY,OAAO,WAAW,WAAW;QAC3C;kBACD;OAEO,CAAA;OAET,YACC,oBAAC,UAAD;QACE,eAAe;SACb,SAAS,aAAa;SACtB,iBAAiB,IAAI;QACvB;QACA,OAAO;SACL,SAAS;SACT,UAAU,OAAO,WAAW,SAAS;SACrC,OAAO,OAAO,OAAO,OAAO;SAC5B,iBAAiB,GAAG,OAAO,OAAO,OAAO,SAAS;SAClD,QAAQ,aAAa,OAAO,OAAO,OAAO,SAAS;SACnD,cAAc,OAAO,aAAa;SAClC,QAAQ;SACR,YAAY,OAAO,WAAW,WAAW;QAC3C;kBACD;OAEO,CAAA;MAEP;OACF;;IACL,oBAAC,SAAD,EAAA,UAAQ;;;YAGC,CAAA;GACT,EAAA,CAAA;EAED;;AAET,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ImagePanel.js","names":[],"sources":["../../../../src/react/core/widgets/ImagePanel.tsx"],"sourcesContent":["/**\n * @zendir/ui - ImagePanel Component\n *\n * Reusable chrome around `<ImageFrame>` for sensor/CCD-style image\n * surfaces. Adds the bits every imaging panel needs but `<ImageFrame>`\n * intentionally doesn't:\n *\n * - **Refresh button** with disabled / busy states.\n * - **Error banner** when the upstream capture failed.\n * - **Title strip + frame** in a single bordered card.\n *\n * Like `<ImageFrame>`, this component is **purely presentational** —\n * it knows nothing about where the data came from. Callers wire up the\n * I/O (engine, file, websocket) and feed:\n *\n * - `data` / `pixels` / `width` / `maxADU` / `colormap` → forwarded\n * to `<ImageFrame>` unchanged.\n * - `onRefresh` → click handler for the button.\n * - `loading` → toggles the button's \"Capturing…\" label AND the\n * frame's skeleton state when there's no frame yet.\n * - `error` → renders a red strip above the frame.\n *\n * The component handles the common refresh-disabled-while-loading\n * pattern internally so callers don't have to compute it.\n *\n * @example\n * ```tsx\n * <ImagePanel\n * title=\"CCD capture\"\n * data={samples}\n * width={28}\n * maxADU={65535}\n * loading={busy}\n * error={err}\n * onRefresh={capture}\n * refreshDisabled={!ccd}\n * emptyMessage=\"No frame captured yet.\"\n * />\n * ```\n */\n\nimport React, { memo } from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { classNames } from \"../../utils\";\nimport { ImageFrame, type ImageColormap } from \"./ImageFrame\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface ImagePanelProps {\n // -- Frame inputs (forwarded to <ImageFrame>) ----------------------------\n /** Flat, row-major single-channel samples. Length must equal width*height. */\n data?: number[];\n /** Pre-decoded RGBA pixels — escape hatch for callers with their own buffer. */\n pixels?: Uint8ClampedArray;\n /** Frame width in pixels. */\n width: number;\n /** Frame height in pixels. Defaults to `width` (square). */\n height?: number;\n /** Saturation ceiling used to normalise `data` → [0,1]. Default 65535. */\n maxADU?: number;\n /** Colormap applied to normalised intensities. Default 'cividis'. */\n colormap?: ImageColormap;\n /** Show the raw sample values as a grid below the image. Default false. */\n showValues?: boolean;\n /** Message shown when no frame is available. */\n emptyMessage?: string;\n\n // -- Panel chrome --------------------------------------------------------\n /** Title rendered above the frame. */\n title?: string;\n /** True while a capture is in flight. Drives the button label + skeleton. */\n loading?: boolean;\n /** Error from the upstream capture. Rendered as a red banner above the frame. */\n error?: string | null;\n /** Click handler for the refresh button. Omit to hide the button entirely. */\n onRefresh?: () => void;\n /** Label for the idle refresh button. Default 'Refresh'. */\n refreshLabel?: string;\n /** Label while `loading` is true. Default 'Capturing…'. */\n refreshingLabel?: string;\n /**\n * Disable the refresh button even when not loading — e.g. when the\n * caller has no data source bound (no CCD selected). The button is\n * always disabled while `loading` is true regardless of this prop.\n */\n refreshDisabled?: boolean;\n /** Extra class name applied to the outer wrapper. */\n className?: string;\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport const ImagePanel = memo(function ImagePanel({\n // frame\n data,\n pixels,\n width,\n height,\n maxADU,\n colormap,\n showValues,\n emptyMessage,\n // chrome\n title,\n loading = false,\n error = null,\n onRefresh,\n refreshLabel = \"Refresh\",\n refreshingLabel = \"Capturing…\",\n refreshDisabled = false,\n className = \"\",\n}: ImagePanelProps): React.ReactElement {\n const { tokens } = useTheme();\n\n // Button is disabled while loading OR when caller explicitly disables it\n // (e.g. no source bound yet). Coalesced here so callers don't repeat the\n // `loading || refreshDisabled` pattern.\n const buttonDisabled = loading || refreshDisabled;\n\n // Skeleton state inside ImageFrame should fire only while loading AND\n // there's nothing to show yet — once we have data, repeat captures\n // refresh in-place without flashing the skeleton.\n const hasFrame = Array.isArray(data) || pixels instanceof Uint8ClampedArray;\n const frameLoading = loading && !hasFrame;\n\n return (\n <div\n data-testid=\"image-panel\"\n className={classNames(\"zendir-image-panel\", className)}\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: tokens.spacing.sm,\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n {onRefresh && (\n <button\n type=\"button\"\n data-testid=\"image-panel-refresh\"\n onClick={onRefresh}\n disabled={buttonDisabled}\n aria-busy={loading ? \"true\" : \"false\"}\n style={{\n alignSelf: \"flex-start\",\n padding: `${tokens.spacing.xs}px ${tokens.spacing.sm}px`,\n borderRadius: tokens.borderRadius.sm,\n border: `1px solid ${tokens.colors.border.muted}`,\n background: buttonDisabled\n ? \"rgba(255,255,255,0.03)\"\n : \"rgba(255,255,255,0.08)\",\n color: buttonDisabled\n ? tokens.colors.text.tertiary\n : tokens.colors.text.primary,\n cursor: buttonDisabled ? \"default\" : \"pointer\",\n fontSize: tokens.typography.fontSize.xs,\n fontFamily: tokens.typography.fontFamily.primary,\n // Make the disabled state visually consistent across browsers.\n opacity: buttonDisabled ? 0.85 : 1,\n }}\n >\n {loading ? refreshingLabel : refreshLabel}\n </button>\n )}\n\n {error && (\n <div\n data-testid=\"image-panel-error\"\n role=\"alert\"\n style={{\n color: tokens.colors.status.critical,\n fontSize: tokens.typography.fontSize.xs,\n background: `${tokens.colors.status.critical}14`,\n border: `1px solid ${tokens.colors.status.critical}33`,\n borderRadius: tokens.borderRadius.sm,\n padding: `${tokens.spacing.xs}px ${tokens.spacing.sm}px`,\n wordBreak: \"break-word\",\n }}\n >\n {error}\n </div>\n )}\n\n <ImageFrame\n data={data}\n pixels={pixels}\n width={width}\n height={height}\n maxADU={maxADU}\n colormap={colormap}\n showValues={showValues}\n title={title}\n loading={frameLoading}\n emptyMessage={emptyMessage}\n />\n </div>\n );\n});\n\nexport default ImagePanel;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgGA,IAAa,aAAa,KAAK,SAAS,WAAW,EAEjD,MACA,QACA,OACA,QACA,QACA,UACA,YACA,cAEA,OACA,UAAU,OACV,QAAQ,MACR,WACA,eAAe,WACf,kBAAkB,cAClB,kBAAkB,OAClB,YAAY,MAC0B;CACtC,MAAM,EAAE,WAAW,SAAS;CAK5B,MAAM,iBAAiB,WAAW;CAKlC,MAAM,WAAW,MAAM,QAAQ,IAAI,KAAK,kBAAkB;CAC1D,MAAM,eAAe,WAAW,CAAC;CAEjC,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,WAAW,WAAW,sBAAsB,SAAS;EACrD,OAAO;GACL,SAAS;GACT,eAAe;GACf,KAAK,OAAO,QAAQ;GACpB,YAAY,OAAO,WAAW,WAAW;EAC3C;YARF;GAUG,aACC,oBAAC,UAAD;IACE,MAAK;IACL,eAAY;IACZ,SAAS;IACT,UAAU;IACV,aAAW,UAAU,SAAS;IAC9B,OAAO;KACL,WAAW;KACX,SAAS,GAAG,OAAO,QAAQ,GAAG,KAAK,OAAO,QAAQ,GAAG;KACrD,cAAc,OAAO,aAAa;KAClC,QAAQ,aAAa,OAAO,OAAO,OAAO;KAC1C,YAAY,iBACR,2BACA;KACJ,OAAO,iBACH,OAAO,OAAO,KAAK,WACnB,OAAO,OAAO,KAAK;KACvB,QAAQ,iBAAiB,YAAY;KACrC,UAAU,OAAO,WAAW,SAAS;KACrC,YAAY,OAAO,WAAW,WAAW;KAEzC,SAAS,iBAAiB,MAAO;IACnC;cAEC,UAAU,kBAAkB;GACvB,CAAA;GAGT,SACC,oBAAC,OAAD;IACE,eAAY;IACZ,MAAK;IACL,OAAO;KACL,OAAO,OAAO,OAAO,OAAO;KAC5B,UAAU,OAAO,WAAW,SAAS;KACrC,YAAY,GAAG,OAAO,OAAO,OAAO,SAAS;KAC7C,QAAQ,aAAa,OAAO,OAAO,OAAO,SAAS;KACnD,cAAc,OAAO,aAAa;KAClC,SAAS,GAAG,OAAO,QAAQ,GAAG,KAAK,OAAO,QAAQ,GAAG;KACrD,WAAW;IACb;cAEC;GACE,CAAA;GAGP,oBAAC,YAAD;IACQ;IACE;IACD;IACC;IACA;IACE;IACE;IACL;IACP,SAAS;IACK;GACf,CAAA;EACE;;AAET,CAAC"}
|
|
1
|
+
{"version":3,"file":"ImagePanel.js","names":[],"sources":["../../../../src/react/core/widgets/ImagePanel.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - ImagePanel Component\r\n *\r\n * Reusable chrome around `<ImageFrame>` for sensor/CCD-style image\r\n * surfaces. Adds the bits every imaging panel needs but `<ImageFrame>`\r\n * intentionally doesn't:\r\n *\r\n * - **Refresh button** with disabled / busy states.\r\n * - **Error banner** when the upstream capture failed.\r\n * - **Title strip + frame** in a single bordered card.\r\n *\r\n * Like `<ImageFrame>`, this component is **purely presentational** —\r\n * it knows nothing about where the data came from. Callers wire up the\r\n * I/O (engine, file, websocket) and feed:\r\n *\r\n * - `data` / `pixels` / `width` / `maxADU` / `colormap` → forwarded\r\n * to `<ImageFrame>` unchanged.\r\n * - `onRefresh` → click handler for the button.\r\n * - `loading` → toggles the button's \"Capturing…\" label AND the\r\n * frame's skeleton state when there's no frame yet.\r\n * - `error` → renders a red strip above the frame.\r\n *\r\n * The component handles the common refresh-disabled-while-loading\r\n * pattern internally so callers don't have to compute it.\r\n *\r\n * @example\r\n * ```tsx\r\n * <ImagePanel\r\n * title=\"CCD capture\"\r\n * data={samples}\r\n * width={28}\r\n * maxADU={65535}\r\n * loading={busy}\r\n * error={err}\r\n * onRefresh={capture}\r\n * refreshDisabled={!ccd}\r\n * emptyMessage=\"No frame captured yet.\"\r\n * />\r\n * ```\r\n */\r\n\r\nimport React, { memo } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { classNames } from \"../../utils\";\r\nimport { ImageFrame, type ImageColormap } from \"./ImageFrame\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport interface ImagePanelProps {\r\n // -- Frame inputs (forwarded to <ImageFrame>) ----------------------------\r\n /** Flat, row-major single-channel samples. Length must equal width*height. */\r\n data?: number[];\r\n /** Pre-decoded RGBA pixels — escape hatch for callers with their own buffer. */\r\n pixels?: Uint8ClampedArray;\r\n /** Frame width in pixels. */\r\n width: number;\r\n /** Frame height in pixels. Defaults to `width` (square). */\r\n height?: number;\r\n /** Saturation ceiling used to normalise `data` → [0,1]. Default 65535. */\r\n maxADU?: number;\r\n /** Colormap applied to normalised intensities. Default 'cividis'. */\r\n colormap?: ImageColormap;\r\n /** Show the raw sample values as a grid below the image. Default false. */\r\n showValues?: boolean;\r\n /** Message shown when no frame is available. */\r\n emptyMessage?: string;\r\n\r\n // -- Panel chrome --------------------------------------------------------\r\n /** Title rendered above the frame. */\r\n title?: string;\r\n /** True while a capture is in flight. Drives the button label + skeleton. */\r\n loading?: boolean;\r\n /** Error from the upstream capture. Rendered as a red banner above the frame. */\r\n error?: string | null;\r\n /** Click handler for the refresh button. Omit to hide the button entirely. */\r\n onRefresh?: () => void;\r\n /** Label for the idle refresh button. Default 'Refresh'. */\r\n refreshLabel?: string;\r\n /** Label while `loading` is true. Default 'Capturing…'. */\r\n refreshingLabel?: string;\r\n /**\r\n * Disable the refresh button even when not loading — e.g. when the\r\n * caller has no data source bound (no CCD selected). The button is\r\n * always disabled while `loading` is true regardless of this prop.\r\n */\r\n refreshDisabled?: boolean;\r\n /** Extra class name applied to the outer wrapper. */\r\n className?: string;\r\n}\r\n\r\n// =============================================================================\r\n// Component\r\n// =============================================================================\r\n\r\nexport const ImagePanel = memo(function ImagePanel({\r\n // frame\r\n data,\r\n pixels,\r\n width,\r\n height,\r\n maxADU,\r\n colormap,\r\n showValues,\r\n emptyMessage,\r\n // chrome\r\n title,\r\n loading = false,\r\n error = null,\r\n onRefresh,\r\n refreshLabel = \"Refresh\",\r\n refreshingLabel = \"Capturing…\",\r\n refreshDisabled = false,\r\n className = \"\",\r\n}: ImagePanelProps): React.ReactElement {\r\n const { tokens } = useTheme();\r\n\r\n // Button is disabled while loading OR when caller explicitly disables it\r\n // (e.g. no source bound yet). Coalesced here so callers don't repeat the\r\n // `loading || refreshDisabled` pattern.\r\n const buttonDisabled = loading || refreshDisabled;\r\n\r\n // Skeleton state inside ImageFrame should fire only while loading AND\r\n // there's nothing to show yet — once we have data, repeat captures\r\n // refresh in-place without flashing the skeleton.\r\n const hasFrame = Array.isArray(data) || pixels instanceof Uint8ClampedArray;\r\n const frameLoading = loading && !hasFrame;\r\n\r\n return (\r\n <div\r\n data-testid=\"image-panel\"\r\n className={classNames(\"zendir-image-panel\", className)}\r\n style={{\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n gap: tokens.spacing.sm,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n {onRefresh && (\r\n <button\r\n type=\"button\"\r\n data-testid=\"image-panel-refresh\"\r\n onClick={onRefresh}\r\n disabled={buttonDisabled}\r\n aria-busy={loading ? \"true\" : \"false\"}\r\n style={{\r\n alignSelf: \"flex-start\",\r\n padding: `${tokens.spacing.xs}px ${tokens.spacing.sm}px`,\r\n borderRadius: tokens.borderRadius.sm,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n background: buttonDisabled\r\n ? \"rgba(255,255,255,0.03)\"\r\n : \"rgba(255,255,255,0.08)\",\r\n color: buttonDisabled\r\n ? tokens.colors.text.tertiary\r\n : tokens.colors.text.primary,\r\n cursor: buttonDisabled ? \"default\" : \"pointer\",\r\n fontSize: tokens.typography.fontSize.xs,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n // Make the disabled state visually consistent across browsers.\r\n opacity: buttonDisabled ? 0.85 : 1,\r\n }}\r\n >\r\n {loading ? refreshingLabel : refreshLabel}\r\n </button>\r\n )}\r\n\r\n {error && (\r\n <div\r\n data-testid=\"image-panel-error\"\r\n role=\"alert\"\r\n style={{\r\n color: tokens.colors.status.critical,\r\n fontSize: tokens.typography.fontSize.xs,\r\n background: `${tokens.colors.status.critical}14`,\r\n border: `1px solid ${tokens.colors.status.critical}33`,\r\n borderRadius: tokens.borderRadius.sm,\r\n padding: `${tokens.spacing.xs}px ${tokens.spacing.sm}px`,\r\n wordBreak: \"break-word\",\r\n }}\r\n >\r\n {error}\r\n </div>\r\n )}\r\n\r\n <ImageFrame\r\n data={data}\r\n pixels={pixels}\r\n width={width}\r\n height={height}\r\n maxADU={maxADU}\r\n colormap={colormap}\r\n showValues={showValues}\r\n title={title}\r\n loading={frameLoading}\r\n emptyMessage={emptyMessage}\r\n />\r\n </div>\r\n );\r\n});\r\n\r\nexport default ImagePanel;\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgGA,IAAa,aAAa,KAAK,SAAS,WAAW,EAEjD,MACA,QACA,OACA,QACA,QACA,UACA,YACA,cAEA,OACA,UAAU,OACV,QAAQ,MACR,WACA,eAAe,WACf,kBAAkB,cAClB,kBAAkB,OAClB,YAAY,MAC0B;CACtC,MAAM,EAAE,WAAW,SAAS;CAK5B,MAAM,iBAAiB,WAAW;CAKlC,MAAM,WAAW,MAAM,QAAQ,IAAI,KAAK,kBAAkB;CAC1D,MAAM,eAAe,WAAW,CAAC;CAEjC,OACE,qBAAC,OAAD;EACE,eAAY;EACZ,WAAW,WAAW,sBAAsB,SAAS;EACrD,OAAO;GACL,SAAS;GACT,eAAe;GACf,KAAK,OAAO,QAAQ;GACpB,YAAY,OAAO,WAAW,WAAW;EAC3C;YARF;GAUG,aACC,oBAAC,UAAD;IACE,MAAK;IACL,eAAY;IACZ,SAAS;IACT,UAAU;IACV,aAAW,UAAU,SAAS;IAC9B,OAAO;KACL,WAAW;KACX,SAAS,GAAG,OAAO,QAAQ,GAAG,KAAK,OAAO,QAAQ,GAAG;KACrD,cAAc,OAAO,aAAa;KAClC,QAAQ,aAAa,OAAO,OAAO,OAAO;KAC1C,YAAY,iBACR,2BACA;KACJ,OAAO,iBACH,OAAO,OAAO,KAAK,WACnB,OAAO,OAAO,KAAK;KACvB,QAAQ,iBAAiB,YAAY;KACrC,UAAU,OAAO,WAAW,SAAS;KACrC,YAAY,OAAO,WAAW,WAAW;KAEzC,SAAS,iBAAiB,MAAO;IACnC;cAEC,UAAU,kBAAkB;GACvB,CAAA;GAGT,SACC,oBAAC,OAAD;IACE,eAAY;IACZ,MAAK;IACL,OAAO;KACL,OAAO,OAAO,OAAO,OAAO;KAC5B,UAAU,OAAO,WAAW,SAAS;KACrC,YAAY,GAAG,OAAO,OAAO,OAAO,SAAS;KAC7C,QAAQ,aAAa,OAAO,OAAO,OAAO,SAAS;KACnD,cAAc,OAAO,aAAa;KAClC,SAAS,GAAG,OAAO,QAAQ,GAAG,KAAK,OAAO,QAAQ,GAAG;KACrD,WAAW;IACb;cAEC;GACE,CAAA;GAGP,oBAAC,YAAD;IACQ;IACE;IACD;IACC;IACA;IACE;IACE;IACL;IACP,SAAS;IACK;GACf,CAAA;EACE;;AAET,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogViewer.js","names":[],"sources":["../../../../src/react/core/widgets/LogViewer.tsx"],"sourcesContent":["/**\n * @zendir/ui - LogViewer Component\n *\n * Real-time log/event viewer for mission operations and system monitoring.\n * Supports severity filtering, search, auto-scroll, pause/resume,\n * and timestamped entries with color-coded severity levels.\n *\n * Features:\n * - Severity levels: debug, info, warn, error, critical\n * - Color-coded entries with Astro status colors\n * - Real-time auto-scroll with pause/resume\n * - Search/filter by text\n * - Severity filter checkboxes\n * - Timestamp display\n * - Source/component tags\n * - Export log to clipboard or file\n * - Maximum log retention (auto-prune old entries)\n * - Virtualized rendering for large logs\n *\n * @example\n * ```tsx\n * <LogViewer\n * entries={logEntries}\n * maxEntries={5000}\n * showTimestamp\n * showSource\n * />\n * ```\n */\n\nimport React, {\n useState,\n useRef,\n useCallback,\n useEffect,\n useMemo,\n memo,\n} from \"react\";\nimport { useTheme } from \"../../theme\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport type LogSeverity = \"debug\" | \"info\" | \"warn\" | \"error\" | \"critical\";\n\nexport interface LogEntry {\n /** Unique ID */\n id: string | number;\n /** Wall-clock timestamp (ISO string or Date) for ordering/export. */\n timestamp: string | Date;\n /**\n * Simulation-elapsed time in seconds (T+ format) when the event occurred.\n * Optional. When provided AND `timestampFormat=\"sim-elapsed\"`, this is what\n * the timestamp column displays. Falls through to `timestamp` otherwise.\n */\n simTime?: number;\n /** Severity level */\n severity: LogSeverity;\n /** Log message */\n message: string;\n /** Source component/system */\n source?: string;\n /** Additional metadata */\n metadata?: Record<string, unknown>;\n}\n\nconst DEFAULT_ESTIMATED_ROW_HEIGHT = 28;\nconst DEFAULT_OVERSCAN_COUNT = 12;\n\nexport interface LogViewerProps {\n /** Array of log entries */\n entries: LogEntry[];\n /** Maximum entries to keep in memory (default: 5000). Older entries are pruned. */\n maxEntries?: number;\n /** Height of the viewer (default: 400) */\n height?: number | string;\n /** Title */\n title?: string;\n /** Show timestamps (default: true) */\n showTimestamp?: boolean;\n /** Show source column (default: true) */\n showSource?: boolean;\n /** Show severity filter toolbar (default: true) */\n showFilters?: boolean;\n /** Show search bar (default: true) */\n showSearch?: boolean;\n /** Initial severity filters (all enabled by default) */\n defaultSeverities?: LogSeverity[];\n /**\n * Timestamp format. `'sim-elapsed'` displays `entry.simTime` as `T+HHH:MM:SS.s`\n * (falls back to wall-clock formatting if simTime is absent on an entry).\n * Default: 'time'.\n */\n timestampFormat?: \"time\" | \"datetime\" | \"relative\" | \"sim-elapsed\";\n /** Called when an entry is clicked */\n onEntryClick?: (entry: LogEntry) => void;\n /** CSS class */\n className?: string;\n /** Show outer container border (default: true). Set false when embedded inside a Container/card. */\n bordered?: boolean;\n /** Custom style overrides for the outer container */\n style?: React.CSSProperties;\n /** Estimated row height in px for virtualization (default: 28). Enables smooth scroll through large logs. */\n estimatedRowHeight?: number;\n /** Number of rows to render above/below viewport for virtualization (default: 12). */\n overscanCount?: number;\n /**\n * Enable pagination: show entries in pages with Previous/Next.\n * Page 1 = newest entries. Use with large maxEntries so users can navigate to older pages.\n * When set, scrolling is replaced by page navigation (better for very large logs).\n */\n pagination?: boolean | { pageSize?: number };\n}\n\n// =============================================================================\n// Helpers\n// =============================================================================\n\nconst SEVERITY_ORDER: LogSeverity[] = [\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"critical\",\n];\n\nfunction formatSimElapsed(seconds: number): string {\n const sign = seconds < 0 ? \"-\" : \"+\";\n const abs = Math.abs(seconds);\n const h = Math.floor(abs / 3600);\n const m = Math.floor((abs % 3600) / 60);\n const s = abs % 60;\n const whole = Math.floor(s);\n const tenths = Math.floor((s - whole) * 10);\n return `T${sign}${String(h).padStart(2, \"0\")}:${String(m).padStart(2, \"0\")}:${String(whole).padStart(2, \"0\")}.${tenths}`;\n}\n\nfunction formatTimestamp(\n entry: Pick<LogEntry, \"timestamp\" | \"simTime\">,\n format: \"time\" | \"datetime\" | \"relative\" | \"sim-elapsed\",\n): string {\n if (\n format === \"sim-elapsed\" &&\n typeof entry.simTime === \"number\" &&\n Number.isFinite(entry.simTime)\n ) {\n return formatSimElapsed(entry.simTime);\n }\n\n const ts = entry.timestamp;\n const d = typeof ts === \"string\" ? new Date(ts) : ts;\n if (isNaN(d.getTime())) return String(ts);\n\n if (format === \"sim-elapsed\") {\n // Fallback when simTime missing — show wall-clock time so the row isn't blank.\n return d.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n }\n if (format === \"time\") {\n return (\n d.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n }) +\n \".\" +\n d.getMilliseconds().toString().padStart(3, \"0\")\n );\n }\n if (format === \"datetime\") {\n return (\n d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"2-digit\" }) +\n \" \" +\n d.toLocaleTimeString(\"en-US\", {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n })\n );\n }\n // relative\n const diff = Date.now() - d.getTime();\n if (diff < 1000) return \"just now\";\n if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n return `${Math.floor(diff / 3600000)}h ago`;\n}\n\n// =============================================================================\n// Component\n// =============================================================================\n\nexport const LogViewer = memo(function LogViewer({\n entries,\n maxEntries = 5000,\n height = 400,\n title,\n showTimestamp = true,\n showSource = true,\n showFilters = true,\n showSearch = true,\n defaultSeverities,\n timestampFormat = \"time\",\n onEntryClick,\n className = \"\",\n bordered = true,\n style: styleProp,\n estimatedRowHeight = DEFAULT_ESTIMATED_ROW_HEIGHT,\n overscanCount = DEFAULT_OVERSCAN_COUNT,\n pagination,\n}: LogViewerProps): React.ReactElement {\n const { tokens, theme } = useTheme();\n const isTransparentTheme =\n theme === \"transparent\" ||\n theme === \"transparent-bold\" ||\n theme === \"transparent-minimal\";\n const scrollRef = useRef<HTMLDivElement>(null);\n const [paused, setPaused] = useState(false);\n const [searchQuery, setSearchQuery] = useState(\"\");\n const [severityFilter, setSeverityFilter] = useState<Set<LogSeverity>>(\n new Set(defaultSeverities ?? SEVERITY_ORDER),\n );\n const [autoScroll, setAutoScroll] = useState(true);\n const prevLengthRef = useRef(entries.length);\n /** Scroll position and container height for virtualization */\n const [scrollState, setScrollState] = useState({\n scrollTop: 0,\n containerHeight: 300,\n });\n const rafRef = useRef<number | null>(null);\n\n const severityColors: Record<LogSeverity, string> = {\n debug: tokens.colors.text.muted ?? tokens.colors.text.secondary,\n info: tokens.colors.status.standby,\n warn: tokens.colors.status.caution,\n error: tokens.colors.status.serious,\n critical: tokens.colors.status.critical,\n };\n\n /**\n * Astro UX status shapes mapped to log severity levels:\n * debug → off (small dot ·), info → standby (ring ◯),\n * warn → caution (square ■), error → serious (diamond ◆),\n * critical → critical (triangle down ▼)\n */\n const severityShapes: Record<LogSeverity, string> = {\n debug: \"·\",\n info: \"○\",\n warn: \"■\",\n error: \"◆\",\n critical: \"▼\",\n };\n\n const severityLabels: Record<LogSeverity, string> = {\n debug: \"DBG\",\n info: \"INF\",\n warn: \"WRN\",\n error: \"ERR\",\n critical: \"CRT\",\n };\n\n // Filter & limit entries: keep NEWEST maxEntries (entries[0] = newest)\n const filteredEntries = useMemo(() => {\n let filtered = entries.filter((e) => severityFilter.has(e.severity));\n if (searchQuery.trim()) {\n const q = searchQuery.toLowerCase();\n filtered = filtered.filter(\n (e) =>\n e.message.toLowerCase().includes(q) ||\n (e.source && e.source.toLowerCase().includes(q)),\n );\n }\n return filtered.slice(0, maxEntries);\n }, [entries, severityFilter, searchQuery, maxEntries]);\n\n const paginationConfig =\n pagination === true ? { pageSize: 200 } : pagination || undefined;\n const pageSize = paginationConfig?.pageSize ?? 200;\n const totalPages = Math.max(1, Math.ceil(filteredEntries.length / pageSize));\n const [currentPage, setCurrentPage] = useState(1);\n\n // Clamp current page when totalPages shrinks (e.g. after filter change)\n useEffect(() => {\n setCurrentPage((p) => Math.min(p, totalPages));\n }, [totalPages]);\n\n const paginationEntries = useMemo(() => {\n if (!paginationConfig) return null;\n const start = (currentPage - 1) * pageSize;\n return filteredEntries.slice(start, start + pageSize);\n }, [paginationConfig, currentPage, pageSize, filteredEntries]);\n\n const totalHeight = filteredEntries.length * estimatedRowHeight;\n const { scrollTop, containerHeight } = scrollState;\n const visibleRange = useMemo(() => {\n const start = Math.max(\n 0,\n Math.floor(scrollTop / estimatedRowHeight) - overscanCount,\n );\n const end = Math.min(\n filteredEntries.length,\n Math.ceil((scrollTop + containerHeight) / estimatedRowHeight) +\n overscanCount,\n );\n return { start, end };\n }, [\n scrollTop,\n containerHeight,\n estimatedRowHeight,\n overscanCount,\n filteredEntries.length,\n ]);\n const visibleEntries = useMemo(\n () => filteredEntries.slice(visibleRange.start, visibleRange.end),\n [filteredEntries, visibleRange.start, visibleRange.end],\n );\n\n const usePagination = !!paginationConfig;\n\n const renderRow = useCallback(\n (entry: LogEntry) => (\n <div\n key={entry.id}\n role={onEntryClick ? \"button\" : \"listitem\"}\n tabIndex={onEntryClick ? 0 : undefined}\n aria-label={\n onEntryClick\n ? `${severityLabels[entry.severity]}: ${entry.message}`\n : undefined\n }\n onClick={() => onEntryClick?.(entry)}\n onKeyDown={\n onEntryClick\n ? (e) =>\n (e.key === \"Enter\" || e.key === \" \") &&\n (e.preventDefault(), onEntryClick(entry))\n : undefined\n }\n style={{\n display: \"flex\",\n alignItems: \"flex-start\",\n minHeight: estimatedRowHeight,\n boxSizing: \"border-box\",\n padding: \"3px 12px\",\n borderBottom: `1px solid ${tokens.colors.border.muted}11`,\n cursor: onEntryClick ? \"pointer\" : \"default\",\n transition: \"background 150ms ease\",\n }}\n onMouseEnter={(e) => {\n (e.currentTarget as HTMLDivElement).style.background =\n `${tokens.colors.background.surface}`;\n }}\n onMouseLeave={(e) => {\n (e.currentTarget as HTMLDivElement).style.background = \"transparent\";\n }}\n >\n <span\n style={{\n color: severityColors[entry.severity],\n fontWeight: 700,\n fontSize: tokens.typography.fontSize.xxs,\n minWidth: 38,\n textAlign: \"center\",\n background: `${severityColors[entry.severity]}18`,\n borderRadius: tokens.borderRadius.sm,\n padding: \"1px 4px\",\n marginRight: 8,\n marginTop: 1,\n letterSpacing: \"0.3px\",\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: 3,\n }}\n >\n <span style={{ fontSize: \"8px\", lineHeight: 1 }}>\n {severityShapes[entry.severity]}\n </span>\n {severityLabels[entry.severity]}\n </span>\n {showTimestamp && (\n <span\n style={{\n color: tokens.colors.text.muted,\n marginRight: 8,\n // 'sim-elapsed' renders T+HH:MM:SS.s which is wider than the wall-clock formats.\n minWidth: timestampFormat === \"sim-elapsed\" ? 110 : 85,\n fontSize: 11,\n flexShrink: 0,\n fontFamily: tokens.typography.fontFamily.mono,\n }}\n >\n {formatTimestamp(entry, timestampFormat)}\n </span>\n )}\n {showSource && entry.source && (\n <span\n style={{\n color: tokens.colors.interactive.default,\n marginRight: 8,\n fontSize: 11,\n minWidth: 70,\n maxWidth: 100,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n flexShrink: 0,\n }}\n >\n [{entry.source}]\n </span>\n )}\n <span\n style={{\n color:\n entry.severity === \"error\" || entry.severity === \"critical\"\n ? severityColors[entry.severity]\n : tokens.colors.text.primary,\n flex: 1,\n wordBreak: \"break-word\",\n lineHeight: 1.4,\n }}\n >\n {entry.message}\n </span>\n </div>\n ),\n [\n severityColors,\n severityShapes,\n severityLabels,\n estimatedRowHeight,\n tokens,\n showTimestamp,\n showSource,\n timestampFormat,\n onEntryClick,\n ],\n );\n\n // Measure scroll container height\n useEffect(() => {\n const el = scrollRef.current;\n if (!el) return;\n const ro = new ResizeObserver(() => {\n setScrollState((s) => ({ ...s, containerHeight: el.clientHeight }));\n });\n ro.observe(el);\n setScrollState((s) => ({ ...s, containerHeight: el.clientHeight }));\n return () => ro.disconnect();\n }, []);\n\n const handleScroll = useCallback(() => {\n const el = scrollRef.current;\n if (!el) return;\n const { scrollTop: st, scrollHeight, clientHeight } = el;\n const atBottom = scrollHeight - st - clientHeight < 30;\n setAutoScroll(atBottom);\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n rafRef.current = requestAnimationFrame(() => {\n rafRef.current = null;\n setScrollState((prev) => ({ ...prev, scrollTop: el.scrollTop }));\n });\n }, []);\n\n // Auto-scroll to bottom when new entries arrive and user is at bottom (scroll mode).\n // In pagination mode: page 1 always shows newest entries (entries are prepended) so\n // content updates automatically — no forced jump needed. The user decides when to\n // follow new entries by clicking \"First\" (which sets autoScroll=true).\n useEffect(() => {\n if (entries.length <= prevLengthRef.current) {\n prevLengthRef.current = entries.length;\n return;\n }\n if (!usePagination && !paused && autoScroll && scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\n }\n prevLengthRef.current = entries.length;\n }, [entries.length, paused, autoScroll, usePagination]);\n\n useEffect(\n () => () => {\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n },\n [],\n );\n\n const toggleSeverity = useCallback((sev: LogSeverity) => {\n setSeverityFilter((prev) => {\n const next = new Set(prev);\n if (next.has(sev)) next.delete(sev);\n else next.add(sev);\n return next;\n });\n }, []);\n\n const exportLog = useCallback(() => {\n const text = filteredEntries\n .map((e) => {\n const wall = formatTimestamp(e, \"datetime\");\n // When sim-elapsed is in use, include the sim T+ stamp alongside wall time so\n // exports remain useful when correlating with sim playback.\n const sim =\n timestampFormat === \"sim-elapsed\" && typeof e.simTime === \"number\"\n ? ` ${formatSimElapsed(e.simTime)}`\n : \"\";\n const src = e.source ? `[${e.source}]` : \"\";\n return `${wall}${sim} [${e.severity.toUpperCase()}] ${src} ${e.message}`;\n })\n .join(\"\\n\");\n navigator.clipboard?.writeText(text);\n }, [filteredEntries, timestampFormat]);\n\n const clearFilters = useCallback(() => {\n setSeverityFilter(new Set(SEVERITY_ORDER));\n setSearchQuery(\"\");\n }, []);\n\n // Counts by severity\n const counts = useMemo(() => {\n const c: Record<LogSeverity, number> = {\n debug: 0,\n info: 0,\n warn: 0,\n error: 0,\n critical: 0,\n };\n entries.forEach((e) => {\n c[e.severity]++;\n });\n return c;\n }, [entries]);\n\n return (\n <div\n className={`log-viewer ${className}`}\n style={{\n height: typeof height === \"number\" ? `${height}px` : height,\n minHeight: typeof height === \"number\" ? `${height}px` : height,\n background: tokens.colors.background.base,\n ...(bordered\n ? (tokens.colors.border.cardStyle ?? {\n border: `1px solid ${tokens.colors.border.muted}`,\n })\n : { border: \"none\" }),\n borderRadius: tokens.borderRadius.lg,\n overflow: \"hidden\",\n display: \"flex\",\n flexDirection: \"column\",\n fontFamily: tokens.typography.fontFamily.mono,\n ...styleProp,\n fontSize: tokens.typography.fontSize.xs,\n color: tokens.colors.text.primary,\n backdropFilter: isTransparentTheme ? \"blur(10px)\" : undefined,\n WebkitBackdropFilter: isTransparentTheme ? \"blur(10px)\" : undefined,\n }}\n >\n {/* Header */}\n {title && (\n <div\n style={{\n padding: \"8px 12px\",\n borderBottom: `1px solid ${tokens.colors.border.muted}`,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n background: tokens.colors.background.surface,\n fontFamily: tokens.typography.fontFamily.primary,\n backdropFilter: \"blur(8px)\",\n }}\n >\n <span style={{ fontWeight: 600, fontSize: 13 }}>{title}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <span style={{ fontSize: 11, color: tokens.colors.text.muted }}>\n {filteredEntries.length.toLocaleString()} entries\n </span>\n </div>\n </div>\n )}\n\n {/* Toolbar */}\n {(showFilters || showSearch) && (\n <div\n style={{\n padding: \"6px 12px\",\n borderBottom: `1px solid ${tokens.colors.border.muted}`,\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n flexWrap: \"wrap\",\n background: tokens.colors.background.surface,\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n {showFilters &&\n SEVERITY_ORDER.map((sev) => (\n <button\n key={sev}\n onClick={() => toggleSeverity(sev)}\n aria-pressed={severityFilter.has(sev)}\n aria-label={`Filter ${sev} logs (${counts[sev]})`}\n style={{\n background: severityFilter.has(sev)\n ? `${severityColors[sev]}22`\n : \"transparent\",\n color: severityFilter.has(sev)\n ? severityColors[sev]\n : tokens.colors.text.muted,\n border: `1px solid ${severityFilter.has(sev) ? severityColors[sev] + \"66\" : tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.xxs,\n fontWeight: tokens.typography.fontWeight.medium,\n cursor: \"pointer\",\n textTransform: \"uppercase\",\n letterSpacing: \"0.5px\",\n transition: \"all 150ms ease\",\n }}\n >\n <span aria-hidden=\"true\">{severityShapes[sev]}</span> {sev} (\n {counts[sev]})\n </button>\n ))}\n <div style={{ flex: 1 }} />\n {showSearch && (\n <input\n type=\"text\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n placeholder=\"Filter messages...\"\n aria-label=\"Filter log messages\"\n style={{\n background: tokens.colors.background.base,\n color: tokens.colors.text.primary,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n padding: `3px ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.sm,\n outline: \"none\",\n minWidth: 150,\n }}\n />\n )}\n <button\n onClick={() => setPaused((p) => !p)}\n aria-label={paused ? \"Resume log stream\" : \"Pause log stream\"}\n style={{\n background: paused\n ? `${tokens.colors.status.caution}22`\n : tokens.colors.background.base,\n color: paused\n ? tokens.colors.status.caution\n : tokens.colors.text.secondary,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: \"pointer\",\n fontWeight: tokens.typography.fontWeight.medium,\n transition: \"all 150ms ease\",\n }}\n >\n {paused ? \"\\u25B6 Resume\" : \"\\u23F8 Pause\"}\n </button>\n <button\n onClick={exportLog}\n aria-label=\"Copy log to clipboard\"\n style={{\n background: tokens.colors.background.base,\n color: tokens.colors.text.secondary,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: \"pointer\",\n transition: \"all 150ms ease\",\n }}\n >\n Copy\n </button>\n <button\n onClick={clearFilters}\n aria-label=\"Reset all filters\"\n style={{\n background: tokens.colors.background.base,\n color: tokens.colors.text.secondary,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: \"pointer\",\n transition: \"all 150ms ease\",\n }}\n >\n Reset\n </button>\n </div>\n )}\n\n {/* Log entries — minHeight: 0 so flex child can shrink and scroll */}\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n className=\"zendir-log-scroll\"\n style={{\n flex: 1,\n minHeight: 0,\n overflowY: \"auto\",\n overflowX: \"hidden\",\n padding: \"2px 0\",\n overscrollBehavior: \"contain\",\n WebkitOverflowScrolling: \"touch\",\n }}\n >\n {filteredEntries.length === 0 ? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n height: \"100%\",\n color: tokens.colors.text.muted,\n fontSize: 13,\n fontFamily: tokens.typography.fontFamily.primary,\n }}\n >\n {entries.length === 0 ? \"No log entries\" : \"No matching entries\"}\n </div>\n ) : usePagination ? (\n <div\n style={{ padding: \"2px 0\" }}\n role=\"list\"\n aria-label=\"Log entries\"\n >\n {(paginationEntries ?? []).map((entry) => renderRow(entry))}\n </div>\n ) : (\n <div\n style={{\n height: totalHeight,\n position: \"relative\",\n width: \"100%\",\n }}\n role=\"list\"\n aria-label=\"Log entries\"\n >\n {visibleRange.start > 0 && (\n <div\n style={{\n height: visibleRange.start * estimatedRowHeight,\n width: \"100%\",\n }}\n aria-hidden\n />\n )}\n {visibleEntries.map((entry) => renderRow(entry))}\n {visibleRange.end < filteredEntries.length && (\n <div\n style={{\n height:\n (filteredEntries.length - visibleRange.end) *\n estimatedRowHeight,\n width: \"100%\",\n }}\n aria-hidden\n />\n )}\n </div>\n )}\n </div>\n\n {/* Pagination footer */}\n {usePagination && filteredEntries.length > 0 && (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n flexWrap: \"wrap\",\n gap: tokens.spacing.sm,\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\n borderTop: `1px solid ${tokens.colors.border.muted}`,\n background: tokens.colors.background.surface,\n fontFamily: tokens.typography.fontFamily.primary,\n fontSize: tokens.typography.fontSize.xs,\n color: tokens.colors.text.secondary,\n }}\n >\n <span aria-live=\"polite\">\n Page {currentPage} of {totalPages}\n {filteredEntries.length > 0 && (\n <span style={{ marginLeft: tokens.spacing.sm }}>\n ({((currentPage - 1) * pageSize + 1).toLocaleString()}–\n {Math.min(\n currentPage * pageSize,\n filteredEntries.length,\n ).toLocaleString()}{\" \"}\n of {filteredEntries.length.toLocaleString()})\n </span>\n )}\n </span>\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.xs,\n }}\n >\n <button\n type=\"button\"\n onClick={() => {\n setCurrentPage(1);\n setAutoScroll(true);\n }}\n disabled={currentPage <= 1}\n aria-label=\"First page (newest)\"\n style={{\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n background: tokens.colors.background.base,\n color: tokens.colors.text.primary,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: currentPage <= 1 ? \"not-allowed\" : \"pointer\",\n opacity: currentPage <= 1 ? 0.5 : 1,\n }}\n >\n First\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setCurrentPage((p) => Math.max(1, p - 1));\n setAutoScroll(false);\n }}\n disabled={currentPage <= 1}\n aria-label=\"Previous page\"\n style={{\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n background: tokens.colors.background.base,\n color: tokens.colors.text.primary,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: currentPage <= 1 ? \"not-allowed\" : \"pointer\",\n opacity: currentPage <= 1 ? 0.5 : 1,\n }}\n >\n Previous\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setCurrentPage((p) => Math.min(totalPages, p + 1));\n setAutoScroll(false);\n }}\n disabled={currentPage >= totalPages}\n aria-label=\"Next page\"\n style={{\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n background: tokens.colors.background.base,\n color: tokens.colors.text.primary,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: currentPage >= totalPages ? \"not-allowed\" : \"pointer\",\n opacity: currentPage >= totalPages ? 0.5 : 1,\n }}\n >\n Next\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setCurrentPage(totalPages);\n setAutoScroll(false);\n }}\n disabled={currentPage >= totalPages}\n aria-label=\"Last page (oldest)\"\n style={{\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\n border: `1px solid ${tokens.colors.border.muted}`,\n borderRadius: tokens.borderRadius.md,\n background: tokens.colors.background.base,\n color: tokens.colors.text.primary,\n fontSize: tokens.typography.fontSize.xxs,\n cursor: currentPage >= totalPages ? \"not-allowed\" : \"pointer\",\n opacity: currentPage >= totalPages ? 0.5 : 1,\n }}\n >\n Last\n </button>\n </div>\n </div>\n )}\n\n {/* Status bar */}\n <style>{`\n .zendir-log-scroll::-webkit-scrollbar { width: 6px; }\n .zendir-log-scroll::-webkit-scrollbar-track { background: transparent; }\n .zendir-log-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }\n .zendir-log-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }\n .zendir-log-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent; }\n `}</style>\n\n {paused && (\n <div\n role=\"status\"\n style={{\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\n borderTop: `1px solid ${tokens.colors.border.muted}`,\n background: `${tokens.colors.status.caution}15`,\n color: tokens.colors.status.caution,\n fontSize: tokens.typography.fontSize.sm,\n textAlign: \"center\",\n fontFamily: tokens.typography.fontFamily.primary,\n fontWeight: tokens.typography.fontWeight.medium,\n }}\n >\n Log paused — new entries are buffered\n </div>\n )}\n </div>\n );\n});\n\nexport default LogViewer;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LogViewer.js","names":[],"sources":["../../../../src/react/core/widgets/LogViewer.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - LogViewer Component\r\n *\r\n * Real-time log/event viewer for mission operations and system monitoring.\r\n * Supports severity filtering, search, auto-scroll, pause/resume,\r\n * and timestamped entries with color-coded severity levels.\r\n *\r\n * Features:\r\n * - Severity levels: debug, info, warn, error, critical\r\n * - Color-coded entries with Astro status colors\r\n * - Real-time auto-scroll with pause/resume\r\n * - Search/filter by text\r\n * - Severity filter checkboxes\r\n * - Timestamp display\r\n * - Source/component tags\r\n * - Export log to clipboard or file\r\n * - Maximum log retention (auto-prune old entries)\r\n * - Virtualized rendering for large logs\r\n *\r\n * @example\r\n * ```tsx\r\n * <LogViewer\r\n * entries={logEntries}\r\n * maxEntries={5000}\r\n * showTimestamp\r\n * showSource\r\n * />\r\n * ```\r\n */\r\n\r\nimport React, {\r\n useState,\r\n useRef,\r\n useCallback,\r\n useEffect,\r\n useMemo,\r\n memo,\r\n} from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport type LogSeverity = \"debug\" | \"info\" | \"warn\" | \"error\" | \"critical\";\r\n\r\nexport interface LogEntry {\r\n /** Unique ID */\r\n id: string | number;\r\n /** Wall-clock timestamp (ISO string or Date) for ordering/export. */\r\n timestamp: string | Date;\r\n /**\r\n * Simulation-elapsed time in seconds (T+ format) when the event occurred.\r\n * Optional. When provided AND `timestampFormat=\"sim-elapsed\"`, this is what\r\n * the timestamp column displays. Falls through to `timestamp` otherwise.\r\n */\r\n simTime?: number;\r\n /** Severity level */\r\n severity: LogSeverity;\r\n /** Log message */\r\n message: string;\r\n /** Source component/system */\r\n source?: string;\r\n /** Additional metadata */\r\n metadata?: Record<string, unknown>;\r\n}\r\n\r\nconst DEFAULT_ESTIMATED_ROW_HEIGHT = 28;\r\nconst DEFAULT_OVERSCAN_COUNT = 12;\r\n\r\nexport interface LogViewerProps {\r\n /** Array of log entries */\r\n entries: LogEntry[];\r\n /** Maximum entries to keep in memory (default: 5000). Older entries are pruned. */\r\n maxEntries?: number;\r\n /** Height of the viewer (default: 400) */\r\n height?: number | string;\r\n /** Title */\r\n title?: string;\r\n /** Show timestamps (default: true) */\r\n showTimestamp?: boolean;\r\n /** Show source column (default: true) */\r\n showSource?: boolean;\r\n /** Show severity filter toolbar (default: true) */\r\n showFilters?: boolean;\r\n /** Show search bar (default: true) */\r\n showSearch?: boolean;\r\n /** Initial severity filters (all enabled by default) */\r\n defaultSeverities?: LogSeverity[];\r\n /**\r\n * Timestamp format. `'sim-elapsed'` displays `entry.simTime` as `T+HHH:MM:SS.s`\r\n * (falls back to wall-clock formatting if simTime is absent on an entry).\r\n * Default: 'time'.\r\n */\r\n timestampFormat?: \"time\" | \"datetime\" | \"relative\" | \"sim-elapsed\";\r\n /** Called when an entry is clicked */\r\n onEntryClick?: (entry: LogEntry) => void;\r\n /** CSS class */\r\n className?: string;\r\n /** Show outer container border (default: true). Set false when embedded inside a Container/card. */\r\n bordered?: boolean;\r\n /** Custom style overrides for the outer container */\r\n style?: React.CSSProperties;\r\n /** Estimated row height in px for virtualization (default: 28). Enables smooth scroll through large logs. */\r\n estimatedRowHeight?: number;\r\n /** Number of rows to render above/below viewport for virtualization (default: 12). */\r\n overscanCount?: number;\r\n /**\r\n * Enable pagination: show entries in pages with Previous/Next.\r\n * Page 1 = newest entries. Use with large maxEntries so users can navigate to older pages.\r\n * When set, scrolling is replaced by page navigation (better for very large logs).\r\n */\r\n pagination?: boolean | { pageSize?: number };\r\n}\r\n\r\n// =============================================================================\r\n// Helpers\r\n// =============================================================================\r\n\r\nconst SEVERITY_ORDER: LogSeverity[] = [\r\n \"debug\",\r\n \"info\",\r\n \"warn\",\r\n \"error\",\r\n \"critical\",\r\n];\r\n\r\nfunction formatSimElapsed(seconds: number): string {\r\n const sign = seconds < 0 ? \"-\" : \"+\";\r\n const abs = Math.abs(seconds);\r\n const h = Math.floor(abs / 3600);\r\n const m = Math.floor((abs % 3600) / 60);\r\n const s = abs % 60;\r\n const whole = Math.floor(s);\r\n const tenths = Math.floor((s - whole) * 10);\r\n return `T${sign}${String(h).padStart(2, \"0\")}:${String(m).padStart(2, \"0\")}:${String(whole).padStart(2, \"0\")}.${tenths}`;\r\n}\r\n\r\nfunction formatTimestamp(\r\n entry: Pick<LogEntry, \"timestamp\" | \"simTime\">,\r\n format: \"time\" | \"datetime\" | \"relative\" | \"sim-elapsed\",\r\n): string {\r\n if (\r\n format === \"sim-elapsed\" &&\r\n typeof entry.simTime === \"number\" &&\r\n Number.isFinite(entry.simTime)\r\n ) {\r\n return formatSimElapsed(entry.simTime);\r\n }\r\n\r\n const ts = entry.timestamp;\r\n const d = typeof ts === \"string\" ? new Date(ts) : ts;\r\n if (isNaN(d.getTime())) return String(ts);\r\n\r\n if (format === \"sim-elapsed\") {\r\n // Fallback when simTime missing — show wall-clock time so the row isn't blank.\r\n return d.toLocaleTimeString(\"en-US\", {\r\n hour12: false,\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n });\r\n }\r\n if (format === \"time\") {\r\n return (\r\n d.toLocaleTimeString(\"en-US\", {\r\n hour12: false,\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n }) +\r\n \".\" +\r\n d.getMilliseconds().toString().padStart(3, \"0\")\r\n );\r\n }\r\n if (format === \"datetime\") {\r\n return (\r\n d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"2-digit\" }) +\r\n \" \" +\r\n d.toLocaleTimeString(\"en-US\", {\r\n hour12: false,\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n })\r\n );\r\n }\r\n // relative\r\n const diff = Date.now() - d.getTime();\r\n if (diff < 1000) return \"just now\";\r\n if (diff < 60000) return `${Math.floor(diff / 1000)}s ago`;\r\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\r\n return `${Math.floor(diff / 3600000)}h ago`;\r\n}\r\n\r\n// =============================================================================\r\n// Component\r\n// =============================================================================\r\n\r\nexport const LogViewer = memo(function LogViewer({\r\n entries,\r\n maxEntries = 5000,\r\n height = 400,\r\n title,\r\n showTimestamp = true,\r\n showSource = true,\r\n showFilters = true,\r\n showSearch = true,\r\n defaultSeverities,\r\n timestampFormat = \"time\",\r\n onEntryClick,\r\n className = \"\",\r\n bordered = true,\r\n style: styleProp,\r\n estimatedRowHeight = DEFAULT_ESTIMATED_ROW_HEIGHT,\r\n overscanCount = DEFAULT_OVERSCAN_COUNT,\r\n pagination,\r\n}: LogViewerProps): React.ReactElement {\r\n const { tokens, theme } = useTheme();\r\n const isTransparentTheme =\r\n theme === \"transparent\" ||\r\n theme === \"transparent-bold\" ||\r\n theme === \"transparent-minimal\";\r\n const scrollRef = useRef<HTMLDivElement>(null);\r\n const [paused, setPaused] = useState(false);\r\n const [searchQuery, setSearchQuery] = useState(\"\");\r\n const [severityFilter, setSeverityFilter] = useState<Set<LogSeverity>>(\r\n new Set(defaultSeverities ?? SEVERITY_ORDER),\r\n );\r\n const [autoScroll, setAutoScroll] = useState(true);\r\n const prevLengthRef = useRef(entries.length);\r\n /** Scroll position and container height for virtualization */\r\n const [scrollState, setScrollState] = useState({\r\n scrollTop: 0,\r\n containerHeight: 300,\r\n });\r\n const rafRef = useRef<number | null>(null);\r\n\r\n const severityColors: Record<LogSeverity, string> = {\r\n debug: tokens.colors.text.muted ?? tokens.colors.text.secondary,\r\n info: tokens.colors.status.standby,\r\n warn: tokens.colors.status.caution,\r\n error: tokens.colors.status.serious,\r\n critical: tokens.colors.status.critical,\r\n };\r\n\r\n /**\r\n * Astro UX status shapes mapped to log severity levels:\r\n * debug → off (small dot ·), info → standby (ring ◯),\r\n * warn → caution (square ■), error → serious (diamond ◆),\r\n * critical → critical (triangle down ▼)\r\n */\r\n const severityShapes: Record<LogSeverity, string> = {\r\n debug: \"·\",\r\n info: \"○\",\r\n warn: \"■\",\r\n error: \"◆\",\r\n critical: \"▼\",\r\n };\r\n\r\n const severityLabels: Record<LogSeverity, string> = {\r\n debug: \"DBG\",\r\n info: \"INF\",\r\n warn: \"WRN\",\r\n error: \"ERR\",\r\n critical: \"CRT\",\r\n };\r\n\r\n // Filter & limit entries: keep NEWEST maxEntries (entries[0] = newest)\r\n const filteredEntries = useMemo(() => {\r\n let filtered = entries.filter((e) => severityFilter.has(e.severity));\r\n if (searchQuery.trim()) {\r\n const q = searchQuery.toLowerCase();\r\n filtered = filtered.filter(\r\n (e) =>\r\n e.message.toLowerCase().includes(q) ||\r\n (e.source && e.source.toLowerCase().includes(q)),\r\n );\r\n }\r\n return filtered.slice(0, maxEntries);\r\n }, [entries, severityFilter, searchQuery, maxEntries]);\r\n\r\n const paginationConfig =\r\n pagination === true ? { pageSize: 200 } : pagination || undefined;\r\n const pageSize = paginationConfig?.pageSize ?? 200;\r\n const totalPages = Math.max(1, Math.ceil(filteredEntries.length / pageSize));\r\n const [currentPage, setCurrentPage] = useState(1);\r\n\r\n // Clamp current page when totalPages shrinks (e.g. after filter change)\r\n useEffect(() => {\r\n setCurrentPage((p) => Math.min(p, totalPages));\r\n }, [totalPages]);\r\n\r\n const paginationEntries = useMemo(() => {\r\n if (!paginationConfig) return null;\r\n const start = (currentPage - 1) * pageSize;\r\n return filteredEntries.slice(start, start + pageSize);\r\n }, [paginationConfig, currentPage, pageSize, filteredEntries]);\r\n\r\n const totalHeight = filteredEntries.length * estimatedRowHeight;\r\n const { scrollTop, containerHeight } = scrollState;\r\n const visibleRange = useMemo(() => {\r\n const start = Math.max(\r\n 0,\r\n Math.floor(scrollTop / estimatedRowHeight) - overscanCount,\r\n );\r\n const end = Math.min(\r\n filteredEntries.length,\r\n Math.ceil((scrollTop + containerHeight) / estimatedRowHeight) +\r\n overscanCount,\r\n );\r\n return { start, end };\r\n }, [\r\n scrollTop,\r\n containerHeight,\r\n estimatedRowHeight,\r\n overscanCount,\r\n filteredEntries.length,\r\n ]);\r\n const visibleEntries = useMemo(\r\n () => filteredEntries.slice(visibleRange.start, visibleRange.end),\r\n [filteredEntries, visibleRange.start, visibleRange.end],\r\n );\r\n\r\n const usePagination = !!paginationConfig;\r\n\r\n const renderRow = useCallback(\r\n (entry: LogEntry) => (\r\n <div\r\n key={entry.id}\r\n role={onEntryClick ? \"button\" : \"listitem\"}\r\n tabIndex={onEntryClick ? 0 : undefined}\r\n aria-label={\r\n onEntryClick\r\n ? `${severityLabels[entry.severity]}: ${entry.message}`\r\n : undefined\r\n }\r\n onClick={() => onEntryClick?.(entry)}\r\n onKeyDown={\r\n onEntryClick\r\n ? (e) =>\r\n (e.key === \"Enter\" || e.key === \" \") &&\r\n (e.preventDefault(), onEntryClick(entry))\r\n : undefined\r\n }\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"flex-start\",\r\n minHeight: estimatedRowHeight,\r\n boxSizing: \"border-box\",\r\n padding: \"3px 12px\",\r\n borderBottom: `1px solid ${tokens.colors.border.muted}11`,\r\n cursor: onEntryClick ? \"pointer\" : \"default\",\r\n transition: \"background 150ms ease\",\r\n }}\r\n onMouseEnter={(e) => {\r\n (e.currentTarget as HTMLDivElement).style.background =\r\n `${tokens.colors.background.surface}`;\r\n }}\r\n onMouseLeave={(e) => {\r\n (e.currentTarget as HTMLDivElement).style.background = \"transparent\";\r\n }}\r\n >\r\n <span\r\n style={{\r\n color: severityColors[entry.severity],\r\n fontWeight: 700,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n minWidth: 38,\r\n textAlign: \"center\",\r\n background: `${severityColors[entry.severity]}18`,\r\n borderRadius: tokens.borderRadius.sm,\r\n padding: \"1px 4px\",\r\n marginRight: 8,\r\n marginTop: 1,\r\n letterSpacing: \"0.3px\",\r\n display: \"inline-flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n gap: 3,\r\n }}\r\n >\r\n <span style={{ fontSize: \"8px\", lineHeight: 1 }}>\r\n {severityShapes[entry.severity]}\r\n </span>\r\n {severityLabels[entry.severity]}\r\n </span>\r\n {showTimestamp && (\r\n <span\r\n style={{\r\n color: tokens.colors.text.muted,\r\n marginRight: 8,\r\n // 'sim-elapsed' renders T+HH:MM:SS.s which is wider than the wall-clock formats.\r\n minWidth: timestampFormat === \"sim-elapsed\" ? 110 : 85,\r\n fontSize: 11,\r\n flexShrink: 0,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n }}\r\n >\r\n {formatTimestamp(entry, timestampFormat)}\r\n </span>\r\n )}\r\n {showSource && entry.source && (\r\n <span\r\n style={{\r\n color: tokens.colors.interactive.default,\r\n marginRight: 8,\r\n fontSize: 11,\r\n minWidth: 70,\r\n maxWidth: 100,\r\n overflow: \"hidden\",\r\n textOverflow: \"ellipsis\",\r\n whiteSpace: \"nowrap\",\r\n flexShrink: 0,\r\n }}\r\n >\r\n [{entry.source}]\r\n </span>\r\n )}\r\n <span\r\n style={{\r\n color:\r\n entry.severity === \"error\" || entry.severity === \"critical\"\r\n ? severityColors[entry.severity]\r\n : tokens.colors.text.primary,\r\n flex: 1,\r\n wordBreak: \"break-word\",\r\n lineHeight: 1.4,\r\n }}\r\n >\r\n {entry.message}\r\n </span>\r\n </div>\r\n ),\r\n [\r\n severityColors,\r\n severityShapes,\r\n severityLabels,\r\n estimatedRowHeight,\r\n tokens,\r\n showTimestamp,\r\n showSource,\r\n timestampFormat,\r\n onEntryClick,\r\n ],\r\n );\r\n\r\n // Measure scroll container height\r\n useEffect(() => {\r\n const el = scrollRef.current;\r\n if (!el) return;\r\n const ro = new ResizeObserver(() => {\r\n setScrollState((s) => ({ ...s, containerHeight: el.clientHeight }));\r\n });\r\n ro.observe(el);\r\n setScrollState((s) => ({ ...s, containerHeight: el.clientHeight }));\r\n return () => ro.disconnect();\r\n }, []);\r\n\r\n const handleScroll = useCallback(() => {\r\n const el = scrollRef.current;\r\n if (!el) return;\r\n const { scrollTop: st, scrollHeight, clientHeight } = el;\r\n const atBottom = scrollHeight - st - clientHeight < 30;\r\n setAutoScroll(atBottom);\r\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\r\n rafRef.current = requestAnimationFrame(() => {\r\n rafRef.current = null;\r\n setScrollState((prev) => ({ ...prev, scrollTop: el.scrollTop }));\r\n });\r\n }, []);\r\n\r\n // Auto-scroll to bottom when new entries arrive and user is at bottom (scroll mode).\r\n // In pagination mode: page 1 always shows newest entries (entries are prepended) so\r\n // content updates automatically — no forced jump needed. The user decides when to\r\n // follow new entries by clicking \"First\" (which sets autoScroll=true).\r\n useEffect(() => {\r\n if (entries.length <= prevLengthRef.current) {\r\n prevLengthRef.current = entries.length;\r\n return;\r\n }\r\n if (!usePagination && !paused && autoScroll && scrollRef.current) {\r\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight;\r\n }\r\n prevLengthRef.current = entries.length;\r\n }, [entries.length, paused, autoScroll, usePagination]);\r\n\r\n useEffect(\r\n () => () => {\r\n if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\r\n },\r\n [],\r\n );\r\n\r\n const toggleSeverity = useCallback((sev: LogSeverity) => {\r\n setSeverityFilter((prev) => {\r\n const next = new Set(prev);\r\n if (next.has(sev)) next.delete(sev);\r\n else next.add(sev);\r\n return next;\r\n });\r\n }, []);\r\n\r\n const exportLog = useCallback(() => {\r\n const text = filteredEntries\r\n .map((e) => {\r\n const wall = formatTimestamp(e, \"datetime\");\r\n // When sim-elapsed is in use, include the sim T+ stamp alongside wall time so\r\n // exports remain useful when correlating with sim playback.\r\n const sim =\r\n timestampFormat === \"sim-elapsed\" && typeof e.simTime === \"number\"\r\n ? ` ${formatSimElapsed(e.simTime)}`\r\n : \"\";\r\n const src = e.source ? `[${e.source}]` : \"\";\r\n return `${wall}${sim} [${e.severity.toUpperCase()}] ${src} ${e.message}`;\r\n })\r\n .join(\"\\n\");\r\n navigator.clipboard?.writeText(text);\r\n }, [filteredEntries, timestampFormat]);\r\n\r\n const clearFilters = useCallback(() => {\r\n setSeverityFilter(new Set(SEVERITY_ORDER));\r\n setSearchQuery(\"\");\r\n }, []);\r\n\r\n // Counts by severity\r\n const counts = useMemo(() => {\r\n const c: Record<LogSeverity, number> = {\r\n debug: 0,\r\n info: 0,\r\n warn: 0,\r\n error: 0,\r\n critical: 0,\r\n };\r\n entries.forEach((e) => {\r\n c[e.severity]++;\r\n });\r\n return c;\r\n }, [entries]);\r\n\r\n return (\r\n <div\r\n className={`log-viewer ${className}`}\r\n style={{\r\n height: typeof height === \"number\" ? `${height}px` : height,\r\n minHeight: typeof height === \"number\" ? `${height}px` : height,\r\n background: tokens.colors.background.base,\r\n ...(bordered\r\n ? (tokens.colors.border.cardStyle ?? {\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n })\r\n : { border: \"none\" }),\r\n borderRadius: tokens.borderRadius.lg,\r\n overflow: \"hidden\",\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n ...styleProp,\r\n fontSize: tokens.typography.fontSize.xs,\r\n color: tokens.colors.text.primary,\r\n backdropFilter: isTransparentTheme ? \"blur(10px)\" : undefined,\r\n WebkitBackdropFilter: isTransparentTheme ? \"blur(10px)\" : undefined,\r\n }}\r\n >\r\n {/* Header */}\r\n {title && (\r\n <div\r\n style={{\r\n padding: \"8px 12px\",\r\n borderBottom: `1px solid ${tokens.colors.border.muted}`,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"space-between\",\r\n background: tokens.colors.background.surface,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n backdropFilter: \"blur(8px)\",\r\n }}\r\n >\r\n <span style={{ fontWeight: 600, fontSize: 13 }}>{title}</span>\r\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\r\n <span style={{ fontSize: 11, color: tokens.colors.text.muted }}>\r\n {filteredEntries.length.toLocaleString()} entries\r\n </span>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Toolbar */}\r\n {(showFilters || showSearch) && (\r\n <div\r\n style={{\r\n padding: \"6px 12px\",\r\n borderBottom: `1px solid ${tokens.colors.border.muted}`,\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: 8,\r\n flexWrap: \"wrap\",\r\n background: tokens.colors.background.surface,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n {showFilters &&\r\n SEVERITY_ORDER.map((sev) => (\r\n <button\r\n key={sev}\r\n onClick={() => toggleSeverity(sev)}\r\n aria-pressed={severityFilter.has(sev)}\r\n aria-label={`Filter ${sev} logs (${counts[sev]})`}\r\n style={{\r\n background: severityFilter.has(sev)\r\n ? `${severityColors[sev]}22`\r\n : \"transparent\",\r\n color: severityFilter.has(sev)\r\n ? severityColors[sev]\r\n : tokens.colors.text.muted,\r\n border: `1px solid ${severityFilter.has(sev) ? severityColors[sev] + \"66\" : tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n cursor: \"pointer\",\r\n textTransform: \"uppercase\",\r\n letterSpacing: \"0.5px\",\r\n transition: \"all 150ms ease\",\r\n }}\r\n >\r\n <span aria-hidden=\"true\">{severityShapes[sev]}</span> {sev} (\r\n {counts[sev]})\r\n </button>\r\n ))}\r\n <div style={{ flex: 1 }} />\r\n {showSearch && (\r\n <input\r\n type=\"text\"\r\n value={searchQuery}\r\n onChange={(e) => setSearchQuery(e.target.value)}\r\n placeholder=\"Filter messages...\"\r\n aria-label=\"Filter log messages\"\r\n style={{\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.primary,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n padding: `3px ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.sm,\r\n outline: \"none\",\r\n minWidth: 150,\r\n }}\r\n />\r\n )}\r\n <button\r\n onClick={() => setPaused((p) => !p)}\r\n aria-label={paused ? \"Resume log stream\" : \"Pause log stream\"}\r\n style={{\r\n background: paused\r\n ? `${tokens.colors.status.caution}22`\r\n : tokens.colors.background.base,\r\n color: paused\r\n ? tokens.colors.status.caution\r\n : tokens.colors.text.secondary,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: \"pointer\",\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n transition: \"all 150ms ease\",\r\n }}\r\n >\r\n {paused ? \"\\u25B6 Resume\" : \"\\u23F8 Pause\"}\r\n </button>\r\n <button\r\n onClick={exportLog}\r\n aria-label=\"Copy log to clipboard\"\r\n style={{\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.secondary,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: \"pointer\",\r\n transition: \"all 150ms ease\",\r\n }}\r\n >\r\n Copy\r\n </button>\r\n <button\r\n onClick={clearFilters}\r\n aria-label=\"Reset all filters\"\r\n style={{\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.secondary,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: \"pointer\",\r\n transition: \"all 150ms ease\",\r\n }}\r\n >\r\n Reset\r\n </button>\r\n </div>\r\n )}\r\n\r\n {/* Log entries — minHeight: 0 so flex child can shrink and scroll */}\r\n <div\r\n ref={scrollRef}\r\n onScroll={handleScroll}\r\n className=\"zendir-log-scroll\"\r\n style={{\r\n flex: 1,\r\n minHeight: 0,\r\n overflowY: \"auto\",\r\n overflowX: \"hidden\",\r\n padding: \"2px 0\",\r\n overscrollBehavior: \"contain\",\r\n WebkitOverflowScrolling: \"touch\",\r\n }}\r\n >\r\n {filteredEntries.length === 0 ? (\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"center\",\r\n height: \"100%\",\r\n color: tokens.colors.text.muted,\r\n fontSize: 13,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n }}\r\n >\r\n {entries.length === 0 ? \"No log entries\" : \"No matching entries\"}\r\n </div>\r\n ) : usePagination ? (\r\n <div\r\n style={{ padding: \"2px 0\" }}\r\n role=\"list\"\r\n aria-label=\"Log entries\"\r\n >\r\n {(paginationEntries ?? []).map((entry) => renderRow(entry))}\r\n </div>\r\n ) : (\r\n <div\r\n style={{\r\n height: totalHeight,\r\n position: \"relative\",\r\n width: \"100%\",\r\n }}\r\n role=\"list\"\r\n aria-label=\"Log entries\"\r\n >\r\n {visibleRange.start > 0 && (\r\n <div\r\n style={{\r\n height: visibleRange.start * estimatedRowHeight,\r\n width: \"100%\",\r\n }}\r\n aria-hidden\r\n />\r\n )}\r\n {visibleEntries.map((entry) => renderRow(entry))}\r\n {visibleRange.end < filteredEntries.length && (\r\n <div\r\n style={{\r\n height:\r\n (filteredEntries.length - visibleRange.end) *\r\n estimatedRowHeight,\r\n width: \"100%\",\r\n }}\r\n aria-hidden\r\n />\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Pagination footer */}\r\n {usePagination && filteredEntries.length > 0 && (\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"space-between\",\r\n flexWrap: \"wrap\",\r\n gap: tokens.spacing.sm,\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\r\n borderTop: `1px solid ${tokens.colors.border.muted}`,\r\n background: tokens.colors.background.surface,\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n fontSize: tokens.typography.fontSize.xs,\r\n color: tokens.colors.text.secondary,\r\n }}\r\n >\r\n <span aria-live=\"polite\">\r\n Page {currentPage} of {totalPages}\r\n {filteredEntries.length > 0 && (\r\n <span style={{ marginLeft: tokens.spacing.sm }}>\r\n ({((currentPage - 1) * pageSize + 1).toLocaleString()}–\r\n {Math.min(\r\n currentPage * pageSize,\r\n filteredEntries.length,\r\n ).toLocaleString()}{\" \"}\r\n of {filteredEntries.length.toLocaleString()})\r\n </span>\r\n )}\r\n </span>\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.xs,\r\n }}\r\n >\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setCurrentPage(1);\r\n setAutoScroll(true);\r\n }}\r\n disabled={currentPage <= 1}\r\n aria-label=\"First page (newest)\"\r\n style={{\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.primary,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: currentPage <= 1 ? \"not-allowed\" : \"pointer\",\r\n opacity: currentPage <= 1 ? 0.5 : 1,\r\n }}\r\n >\r\n First\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setCurrentPage((p) => Math.max(1, p - 1));\r\n setAutoScroll(false);\r\n }}\r\n disabled={currentPage <= 1}\r\n aria-label=\"Previous page\"\r\n style={{\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.primary,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: currentPage <= 1 ? \"not-allowed\" : \"pointer\",\r\n opacity: currentPage <= 1 ? 0.5 : 1,\r\n }}\r\n >\r\n Previous\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setCurrentPage((p) => Math.min(totalPages, p + 1));\r\n setAutoScroll(false);\r\n }}\r\n disabled={currentPage >= totalPages}\r\n aria-label=\"Next page\"\r\n style={{\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.primary,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: currentPage >= totalPages ? \"not-allowed\" : \"pointer\",\r\n opacity: currentPage >= totalPages ? 0.5 : 1,\r\n }}\r\n >\r\n Next\r\n </button>\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n setCurrentPage(totalPages);\r\n setAutoScroll(false);\r\n }}\r\n disabled={currentPage >= totalPages}\r\n aria-label=\"Last page (oldest)\"\r\n style={{\r\n padding: `${tokens.spacing.xxs} ${tokens.spacing.sm}`,\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n borderRadius: tokens.borderRadius.md,\r\n background: tokens.colors.background.base,\r\n color: tokens.colors.text.primary,\r\n fontSize: tokens.typography.fontSize.xxs,\r\n cursor: currentPage >= totalPages ? \"not-allowed\" : \"pointer\",\r\n opacity: currentPage >= totalPages ? 0.5 : 1,\r\n }}\r\n >\r\n Last\r\n </button>\r\n </div>\r\n </div>\r\n )}\r\n\r\n {/* Status bar */}\r\n <style>{`\r\n .zendir-log-scroll::-webkit-scrollbar { width: 6px; }\r\n .zendir-log-scroll::-webkit-scrollbar-track { background: transparent; }\r\n .zendir-log-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }\r\n .zendir-log-scroll::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }\r\n .zendir-log-scroll { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.15) transparent; }\r\n `}</style>\r\n\r\n {paused && (\r\n <div\r\n role=\"status\"\r\n style={{\r\n padding: `${tokens.spacing.xs} ${tokens.spacing.smd}`,\r\n borderTop: `1px solid ${tokens.colors.border.muted}`,\r\n background: `${tokens.colors.status.caution}15`,\r\n color: tokens.colors.status.caution,\r\n fontSize: tokens.typography.fontSize.sm,\r\n textAlign: \"center\",\r\n fontFamily: tokens.typography.fontFamily.primary,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n }}\r\n >\r\n Log paused — new entries are buffered\r\n </div>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nexport default LogViewer;\r\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageStream.js","names":[],"sources":["../../../../src/react/core/widgets/MessageStream.tsx"],"sourcesContent":["/**\n * @zendir/ui - MessageStream Component\n *\n * Stream/notification window for displaying messages, events, or status updates.\n * Each message card has integrated status system with dual-coding (color + shape).\n *\n * AstroUXDS Compliance:\n * - Status colors per AstroUXDS status system\n * - Dual-coding (color + shape) for accessibility\n * - Clear visual hierarchy\n * - Proper contrast ratios\n *\n * @example\n * ```tsx\n * import { MessageStream } from '@zendir/ui';\n *\n * <MessageStream\n * title=\"Response Status\"\n * messages={[\n * { id: '1', text: 'Message Received', status: 'standby' },\n * { id: '2', text: 'Confirmation Receipt', status: 'normal' },\n * { id: '3', text: 'Failed', status: 'critical' },\n * ]}\n * />\n * ```\n */\n\nimport React, { memo, useMemo } from \"react\";\nimport { useTheme } from \"../../theme\";\nimport { classNames, type StatusLevel } from \"../../utils\";\nimport { type AstroIconName } from \"../display/AstroIcon\";\nimport { Icon } from \"../display/Icon\";\nimport { useCompactMode } from \"../../hooks\";\n\n// =============================================================================\n// Types\n// =============================================================================\n\nexport interface StreamMessage {\n /** Unique identifier */\n id: string;\n /** Message text/label */\n text: string;\n /** Status level for color and shape */\n status?: StatusLevel;\n /** Optional timestamp */\n timestamp?: Date | string;\n /** Optional icon override */\n icon?: AstroIconName;\n /** Optional metadata/details */\n details?: string;\n /** Child messages (for hierarchical display) */\n children?: StreamMessage[];\n /** Whether this message is highlighted/active */\n active?: boolean;\n}\n\nexport interface MessageStreamProps {\n /** Stream title */\n title?: string;\n /** Subtitle or description */\n subtitle?: string;\n /** Array of messages to display */\n messages: StreamMessage[];\n /** Maximum height before scrolling */\n maxHeight?: number | string;\n /** Show timestamps */\n showTimestamps?: boolean;\n /** Time format for timestamps */\n timeFormat?: \"relative\" | \"absolute\" | \"time-only\";\n /** Variant: 'list' for vertical, 'tree' for hierarchical */\n variant?: \"list\" | \"tree\" | \"compact\";\n /** Custom className */\n className?: string;\n /** Card size */\n size?: \"sm\" | \"md\" | \"lg\";\n /** On message click */\n onMessageClick?: (message: StreamMessage) => void;\n /** Loading state */\n loading?: boolean;\n /** Empty state message */\n emptyMessage?: string;\n /** Show connection lines (tree variant) */\n showConnectors?: boolean;\n /** Enable collapsible compact mode - shows condensed view that expands on hover/click */\n collapsible?: boolean;\n /** Start expanded when in collapsible mode (pinned state) */\n defaultExpanded?: boolean;\n /** Callback when pin state changes in collapsible mode */\n onPinChange?: (isPinned: boolean) => void;\n}\n\n// =============================================================================\n// Status Shapes (same as DataValue for consistency)\n// =============================================================================\n\nconst STATUS_SHAPES: Record<\n StatusLevel,\n (size: number, color: string) => React.ReactNode\n> = {\n off: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill={color} />\n </svg>\n ),\n standby: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"none\" stroke={color} strokeWidth=\"2\" />\n </svg>\n ),\n normal: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill={color} />\n </svg>\n ),\n caution: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill={color} />\n </svg>\n ),\n serious: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <polygon points=\"6,1 11,6 6,11 1,6\" fill={color} />\n </svg>\n ),\n critical: (size, color) => (\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\n <polygon points=\"6,11 1,2 11,2\" fill={color} />\n </svg>\n ),\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\nfunction getStatusColor(status: StatusLevel | undefined, tokens: any): string {\n switch (status) {\n case \"normal\":\n return tokens.colors.status.normal;\n case \"standby\":\n return tokens.colors.status.standby;\n case \"caution\":\n return tokens.colors.status.caution;\n case \"serious\":\n return tokens.colors.status.serious;\n case \"critical\":\n return tokens.colors.status.critical;\n case \"off\":\n return tokens.colors.status.off;\n default:\n return tokens.colors.status.standby;\n }\n}\n\nfunction formatTimestamp(\n timestamp: Date | string | undefined,\n format: \"relative\" | \"absolute\" | \"time-only\",\n): string {\n if (!timestamp) return \"\";\n\n const date = typeof timestamp === \"string\" ? new Date(timestamp) : timestamp;\n\n if (format === \"relative\") {\n const now = new Date();\n const diff = now.getTime() - date.getTime();\n const seconds = Math.floor(diff / 1000);\n const minutes = Math.floor(seconds / 60);\n const hours = Math.floor(minutes / 60);\n\n if (seconds < 60) return \"Just now\";\n if (minutes < 60) return `${minutes}m ago`;\n if (hours < 24) return `${hours}h ago`;\n return date.toLocaleDateString();\n }\n\n if (format === \"time-only\") {\n return date.toLocaleTimeString([], {\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n }\n\n return date.toLocaleString();\n}\n\n// =============================================================================\n// Message Card Component\n// =============================================================================\n\ninterface MessageCardProps {\n message: StreamMessage;\n tokens: any;\n size: \"sm\" | \"md\" | \"lg\";\n showTimestamp: boolean;\n timeFormat: \"relative\" | \"absolute\" | \"time-only\";\n onClick?: (message: StreamMessage) => void;\n variant: \"list\" | \"tree\" | \"compact\";\n}\n\nconst MessageCard = memo(function MessageCard({\n message,\n tokens,\n size,\n showTimestamp,\n timeFormat,\n onClick,\n variant,\n}: MessageCardProps) {\n const status = message.status || \"standby\";\n const statusColor = getStatusColor(status, tokens);\n\n const sizeConfig = {\n sm: {\n padding: 8,\n fontSize: \"0.75rem\",\n fontSizeSmall: \"0.625rem\",\n iconSize: 10,\n gap: 6,\n }, // 12px / 10px in rem\n md: {\n padding: 12,\n fontSize: \"0.875rem\",\n fontSizeSmall: \"0.75rem\",\n iconSize: 12,\n gap: 8,\n }, // 14px / 12px in rem\n lg: {\n padding: 16,\n fontSize: \"1rem\",\n fontSizeSmall: \"0.875rem\",\n iconSize: 14,\n gap: 10,\n }, // 16px / 14px in rem\n }[size];\n\n const isCompact = variant === \"compact\";\n\n return (\n <div\n role=\"listitem\"\n aria-label={`${message.text} - ${status}`}\n onClick={() => onClick?.(message)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: sizeConfig.gap,\n padding: isCompact\n ? `${sizeConfig.padding / 2}px ${sizeConfig.padding}px`\n : sizeConfig.padding,\n backgroundColor: tokens.colors.background.elevated,\n border: `1.5px solid ${statusColor}`,\n borderRadius: tokens.borderRadius.md,\n cursor: onClick ? \"pointer\" : \"default\",\n transition: \"all 0.15s ease\",\n ...(message.active && {\n boxShadow: `0 0 0 2px ${statusColor}40`,\n }),\n }}\n onMouseEnter={(e) => {\n if (onClick) {\n e.currentTarget.style.backgroundColor =\n tokens.colors.background.surface;\n e.currentTarget.style.transform = \"translateY(-1px)\";\n }\n }}\n onMouseLeave={(e) => {\n if (onClick) {\n e.currentTarget.style.backgroundColor =\n tokens.colors.background.elevated;\n e.currentTarget.style.transform = \"translateY(0)\";\n }\n }}\n >\n {/* Status Shape */}\n <span style={{ flexShrink: 0, display: \"flex\", alignItems: \"center\" }}>\n {STATUS_SHAPES[status](sizeConfig.iconSize, statusColor)}\n </span>\n\n {/* Message Text */}\n <span\n style={{\n flex: 1,\n fontSize: sizeConfig.fontSize,\n fontWeight: 500,\n color: tokens.colors.text.primary,\n whiteSpace: \"nowrap\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n }}\n >\n {message.text}\n </span>\n\n {/* Details (if any) */}\n {message.details && !isCompact && (\n <span\n style={{\n fontSize: sizeConfig.fontSizeSmall,\n color: tokens.colors.text.tertiary,\n }}\n >\n {message.details}\n </span>\n )}\n\n {/* Timestamp */}\n {showTimestamp && message.timestamp && (\n <span\n style={{\n fontSize: sizeConfig.fontSizeSmall,\n color: tokens.colors.text.tertiary,\n fontFamily: tokens.typography.fontFamily.mono,\n whiteSpace: \"nowrap\",\n }}\n >\n {formatTimestamp(message.timestamp, timeFormat)}\n </span>\n )}\n </div>\n );\n});\n\n// =============================================================================\n// Tree Node Component (for hierarchical display)\n// =============================================================================\n\ninterface TreeNodeProps {\n message: StreamMessage;\n tokens: any;\n size: \"sm\" | \"md\" | \"lg\";\n showTimestamp: boolean;\n timeFormat: \"relative\" | \"absolute\" | \"time-only\";\n onClick?: (message: StreamMessage) => void;\n depth: number;\n showConnectors: boolean;\n isLast: boolean;\n}\n\nconst TreeNode = memo(function TreeNode({\n message,\n tokens,\n size,\n showTimestamp,\n timeFormat,\n onClick,\n depth,\n showConnectors,\n isLast,\n}: TreeNodeProps) {\n const hasChildren = message.children && message.children.length > 0;\n const indentSize = 32;\n\n return (\n <div style={{ position: \"relative\" }}>\n {/* Connector Lines */}\n {showConnectors && depth > 0 && (\n <>\n {/* Vertical line from parent */}\n <div\n style={{\n position: \"absolute\",\n left: (depth - 1) * indentSize + 16,\n top: 0,\n width: 2,\n height: isLast ? \"50%\" : \"100%\",\n backgroundColor: tokens.colors.border.muted,\n }}\n />\n {/* Horizontal line to node */}\n <div\n style={{\n position: \"absolute\",\n left: (depth - 1) * indentSize + 16,\n top: \"50%\",\n width: indentSize - 8,\n height: 2,\n backgroundColor: tokens.colors.border.muted,\n }}\n />\n </>\n )}\n\n {/* Message Card */}\n <div style={{ marginLeft: depth * indentSize }}>\n <MessageCard\n message={message}\n tokens={tokens}\n size={size}\n showTimestamp={showTimestamp}\n timeFormat={timeFormat}\n onClick={onClick}\n variant=\"tree\"\n />\n </div>\n\n {/* Render children */}\n {hasChildren && (\n <div style={{ marginTop: 8 }}>\n {message.children!.map((child, index) => (\n <div key={child.id} style={{ marginTop: index > 0 ? 8 : 0 }}>\n <TreeNode\n message={child}\n tokens={tokens}\n size={size}\n showTimestamp={showTimestamp}\n timeFormat={timeFormat}\n onClick={onClick}\n depth={depth + 1}\n showConnectors={showConnectors}\n isLast={index === message.children!.length - 1}\n />\n </div>\n ))}\n </div>\n )}\n </div>\n );\n});\n\n// =============================================================================\n// MessageStream Component\n// =============================================================================\n\nexport const MessageStream = memo(function MessageStream({\n title,\n subtitle,\n messages,\n maxHeight,\n showTimestamps = false,\n timeFormat = \"relative\",\n variant = \"list\",\n className = \"\",\n size = \"md\",\n onMessageClick,\n loading = false,\n emptyMessage = \"No messages\",\n showConnectors = true,\n collapsible = false,\n defaultExpanded = false,\n onPinChange,\n}: MessageStreamProps): React.ReactElement {\n const { tokens, theme, prefersReducedMotion } = useTheme();\n const isTransparentTheme =\n theme === \"transparent\" ||\n theme === \"transparent-bold\" ||\n theme === \"transparent-minimal\";\n\n // Collapsible compact mode state management\n const { expanded, isPinned, handlers, transitionDuration } = useCompactMode({\n compact: collapsible,\n defaultExpanded,\n onPinChange,\n });\n\n // Card container styles - Use transparent background with blur for transparent themes (matching Timeline)\n const containerStyle = useMemo(\n () => ({\n backgroundColor: isTransparentTheme\n ? \"transparent\"\n : tokens.colors.background.surface,\n ...(isTransparentTheme && {\n backdropFilter: \"blur(12px)\" as const,\n WebkitBackdropFilter: \"blur(12px)\" as const,\n }),\n ...(tokens.colors.border.cardStyle ?? {\n border: `1px solid ${tokens.colors.border.muted}`,\n }),\n borderRadius: tokens.borderRadius.lg,\n padding: tokens.spacing.md,\n display: \"flex\",\n flexDirection: \"column\" as const,\n }),\n [tokens, isTransparentTheme],\n );\n\n // Count messages by status for compact view summary (must be before any conditional return for rules-of-hooks)\n const statusCounts = useMemo(() => {\n const counts = { normal: 0, caution: 0, critical: 0, other: 0 };\n messages.forEach((m) => {\n if (m.status === \"normal\") counts.normal++;\n else if (m.status === \"caution\") counts.caution++;\n else if (m.status === \"critical\" || m.status === \"serious\")\n counts.critical++;\n else counts.other++;\n });\n return counts;\n }, [messages]);\n\n // Loading skeleton\n if (loading) {\n return (\n <article\n className={classNames(\"zendir-message-stream\", className)}\n style={containerStyle}\n aria-busy=\"true\"\n aria-label=\"Loading messages\"\n >\n {title && (\n <div style={{ marginBottom: 16 }}>\n <div\n style={{\n height: 18,\n width: 120,\n backgroundColor: tokens.colors.background.elevated,\n borderRadius: 4,\n marginBottom: 8,\n }}\n />\n {subtitle && (\n <div\n style={{\n height: 14,\n width: 200,\n backgroundColor: tokens.colors.background.elevated,\n borderRadius: 4,\n }}\n />\n )}\n </div>\n )}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n style={{\n height: 44,\n backgroundColor: tokens.colors.background.elevated,\n borderRadius: tokens.borderRadius.md,\n border: `2px solid ${tokens.colors.border.muted}`,\n }}\n />\n ))}\n </div>\n </article>\n );\n }\n\n // Collapsed view - shows summary of messages\n if (collapsible && !expanded) {\n return (\n <article\n className={classNames(\"zendir-message-stream\", \"collapsed\", className)}\n {...handlers}\n style={{\n ...containerStyle,\n padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,\n cursor: \"pointer\",\n transition: `all ${transitionDuration} ease-out`,\n }}\n onMouseEnter={(e) => {\n handlers.onMouseEnter();\n if (!prefersReducedMotion) {\n (e.currentTarget as HTMLElement).style.transform = \"scale(1.01)\";\n (e.currentTarget as HTMLElement).style.boxShadow =\n `0 4px 12px ${tokens.colors.accent.primary}20`;\n }\n }}\n onMouseLeave={(e) => {\n handlers.onMouseLeave();\n (e.currentTarget as HTMLElement).style.transform = \"scale(1)\";\n (e.currentTarget as HTMLElement).style.boxShadow = \"\";\n }}\n aria-label={`${title || \"Messages\"} (collapsed)`}\n >\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"space-between\",\n gap: tokens.spacing.md,\n }}\n >\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.sm,\n }}\n >\n <Icon\n name=\"message\"\n size={14}\n color={tokens.colors.accent.primary}\n />\n <span style={{ fontWeight: tokens.typography.fontWeight.medium }}>\n {title ? title.split(\" \")[0] : \"Messages\"}\n </span>\n <span\n style={{\n color: tokens.colors.text.tertiary,\n fontSize: tokens.typography.fontSize.xs,\n }}\n >\n ({messages.length})\n </span>\n </div>\n\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: tokens.spacing.sm,\n }}\n >\n {statusCounts.critical > 0 && (\n <span\n style={{\n color: tokens.colors.status.critical,\n fontSize: tokens.typography.fontSize.xs,\n fontWeight: tokens.typography.fontWeight.medium,\n }}\n >\n {statusCounts.critical} critical\n </span>\n )}\n {statusCounts.caution > 0 && (\n <span\n style={{\n color: tokens.colors.status.caution,\n fontSize: tokens.typography.fontSize.xs,\n fontWeight: tokens.typography.fontWeight.medium,\n }}\n >\n {statusCounts.caution} warn\n </span>\n )}\n {isPinned && (\n <Icon\n name=\"lock\"\n size={12}\n color={tokens.colors.accent.primary}\n />\n )}\n </div>\n </div>\n </article>\n );\n }\n\n return (\n <div\n className={classNames(\n \"zendir-message-stream\",\n collapsible ? \"expanded\" : \"\",\n className,\n )}\n {...(collapsible ? handlers : {})}\n role={collapsible ? handlers.role : \"log\"}\n tabIndex={collapsible ? handlers.tabIndex : undefined}\n aria-expanded={collapsible ? handlers[\"aria-expanded\"] : undefined}\n style={{\n ...containerStyle,\n cursor: collapsible ? \"pointer\" : undefined,\n transition: collapsible\n ? `all ${transitionDuration} ease-out`\n : undefined,\n }}\n aria-label={title || \"Message stream\"}\n aria-live=\"polite\"\n >\n {/* Header */}\n {(title || subtitle) && (\n <header style={{ marginBottom: 16 }}>\n {title && (\n <h3\n style={{\n fontSize: tokens.typography.fontSize.lg,\n fontWeight: 500, // AstroUXDS medium for headers\n color: tokens.colors.text.primary,\n margin: 0,\n }}\n >\n {title}\n </h3>\n )}\n {subtitle && (\n <p\n style={{\n fontSize: tokens.typography.fontSize.sm,\n color: tokens.colors.text.tertiary,\n margin: \"4px 0 0 0\",\n }}\n >\n {subtitle}\n </p>\n )}\n </header>\n )}\n\n {/* Messages Container — tabIndex for keyboard scroll access (WCAG 2.1.1) */}\n <div\n role=\"list\"\n tabIndex={maxHeight ? 0 : undefined}\n aria-label={\n maxHeight ? `${title || \"Messages\"} scrollable list` : undefined\n }\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n gap: variant === \"compact\" ? 4 : 8,\n ...(maxHeight && {\n maxHeight,\n overflowY: \"auto\",\n paddingRight: 8,\n }),\n }}\n >\n {messages.length === 0 ? (\n <div\n style={{\n textAlign: \"center\",\n padding: 32,\n color: tokens.colors.text.tertiary,\n fontSize: tokens.typography.fontSize.sm,\n }}\n >\n {emptyMessage}\n </div>\n ) : variant === \"tree\" ? (\n messages.map((message, index) => (\n <TreeNode\n key={message.id}\n message={message}\n tokens={tokens}\n size={size}\n showTimestamp={showTimestamps}\n timeFormat={timeFormat}\n onClick={onMessageClick}\n depth={0}\n showConnectors={showConnectors}\n isLast={index === messages.length - 1}\n />\n ))\n ) : (\n messages.map((message) => (\n <MessageCard\n key={message.id}\n message={message}\n tokens={tokens}\n size={size}\n showTimestamp={showTimestamps}\n timeFormat={timeFormat}\n onClick={onMessageClick}\n variant={variant}\n />\n ))\n )}\n </div>\n </div>\n );\n});\n\nexport default MessageStream;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"MessageStream.js","names":[],"sources":["../../../../src/react/core/widgets/MessageStream.tsx"],"sourcesContent":["/**\r\n * @zendir/ui - MessageStream Component\r\n *\r\n * Stream/notification window for displaying messages, events, or status updates.\r\n * Each message card has integrated status system with dual-coding (color + shape).\r\n *\r\n * AstroUXDS Compliance:\r\n * - Status colors per AstroUXDS status system\r\n * - Dual-coding (color + shape) for accessibility\r\n * - Clear visual hierarchy\r\n * - Proper contrast ratios\r\n *\r\n * @example\r\n * ```tsx\r\n * import { MessageStream } from '@zendir/ui';\r\n *\r\n * <MessageStream\r\n * title=\"Response Status\"\r\n * messages={[\r\n * { id: '1', text: 'Message Received', status: 'standby' },\r\n * { id: '2', text: 'Confirmation Receipt', status: 'normal' },\r\n * { id: '3', text: 'Failed', status: 'critical' },\r\n * ]}\r\n * />\r\n * ```\r\n */\r\n\r\nimport React, { memo, useMemo } from \"react\";\r\nimport { useTheme } from \"../../theme\";\r\nimport { classNames, type StatusLevel } from \"../../utils\";\r\nimport { type AstroIconName } from \"../display/AstroIcon\";\r\nimport { Icon } from \"../display/Icon\";\r\nimport { useCompactMode } from \"../../hooks\";\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\nexport interface StreamMessage {\r\n /** Unique identifier */\r\n id: string;\r\n /** Message text/label */\r\n text: string;\r\n /** Status level for color and shape */\r\n status?: StatusLevel;\r\n /** Optional timestamp */\r\n timestamp?: Date | string;\r\n /** Optional icon override */\r\n icon?: AstroIconName;\r\n /** Optional metadata/details */\r\n details?: string;\r\n /** Child messages (for hierarchical display) */\r\n children?: StreamMessage[];\r\n /** Whether this message is highlighted/active */\r\n active?: boolean;\r\n}\r\n\r\nexport interface MessageStreamProps {\r\n /** Stream title */\r\n title?: string;\r\n /** Subtitle or description */\r\n subtitle?: string;\r\n /** Array of messages to display */\r\n messages: StreamMessage[];\r\n /** Maximum height before scrolling */\r\n maxHeight?: number | string;\r\n /** Show timestamps */\r\n showTimestamps?: boolean;\r\n /** Time format for timestamps */\r\n timeFormat?: \"relative\" | \"absolute\" | \"time-only\";\r\n /** Variant: 'list' for vertical, 'tree' for hierarchical */\r\n variant?: \"list\" | \"tree\" | \"compact\";\r\n /** Custom className */\r\n className?: string;\r\n /** Card size */\r\n size?: \"sm\" | \"md\" | \"lg\";\r\n /** On message click */\r\n onMessageClick?: (message: StreamMessage) => void;\r\n /** Loading state */\r\n loading?: boolean;\r\n /** Empty state message */\r\n emptyMessage?: string;\r\n /** Show connection lines (tree variant) */\r\n showConnectors?: boolean;\r\n /** Enable collapsible compact mode - shows condensed view that expands on hover/click */\r\n collapsible?: boolean;\r\n /** Start expanded when in collapsible mode (pinned state) */\r\n defaultExpanded?: boolean;\r\n /** Callback when pin state changes in collapsible mode */\r\n onPinChange?: (isPinned: boolean) => void;\r\n}\r\n\r\n// =============================================================================\r\n// Status Shapes (same as DataValue for consistency)\r\n// =============================================================================\r\n\r\nconst STATUS_SHAPES: Record<\r\n StatusLevel,\r\n (size: number, color: string) => React.ReactNode\r\n> = {\r\n off: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill={color} />\r\n </svg>\r\n ),\r\n standby: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill=\"none\" stroke={color} strokeWidth=\"2\" />\r\n </svg>\r\n ),\r\n normal: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <circle cx=\"6\" cy=\"6\" r=\"5\" fill={color} />\r\n </svg>\r\n ),\r\n caution: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <rect x=\"1\" y=\"1\" width=\"10\" height=\"10\" fill={color} />\r\n </svg>\r\n ),\r\n serious: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <polygon points=\"6,1 11,6 6,11 1,6\" fill={color} />\r\n </svg>\r\n ),\r\n critical: (size, color) => (\r\n <svg viewBox=\"0 0 12 12\" width={size} height={size} aria-hidden=\"true\">\r\n <polygon points=\"6,11 1,2 11,2\" fill={color} />\r\n </svg>\r\n ),\r\n};\r\n\r\n// =============================================================================\r\n// Helper Functions\r\n// =============================================================================\r\n\r\nfunction getStatusColor(status: StatusLevel | undefined, tokens: any): string {\r\n switch (status) {\r\n case \"normal\":\r\n return tokens.colors.status.normal;\r\n case \"standby\":\r\n return tokens.colors.status.standby;\r\n case \"caution\":\r\n return tokens.colors.status.caution;\r\n case \"serious\":\r\n return tokens.colors.status.serious;\r\n case \"critical\":\r\n return tokens.colors.status.critical;\r\n case \"off\":\r\n return tokens.colors.status.off;\r\n default:\r\n return tokens.colors.status.standby;\r\n }\r\n}\r\n\r\nfunction formatTimestamp(\r\n timestamp: Date | string | undefined,\r\n format: \"relative\" | \"absolute\" | \"time-only\",\r\n): string {\r\n if (!timestamp) return \"\";\r\n\r\n const date = typeof timestamp === \"string\" ? new Date(timestamp) : timestamp;\r\n\r\n if (format === \"relative\") {\r\n const now = new Date();\r\n const diff = now.getTime() - date.getTime();\r\n const seconds = Math.floor(diff / 1000);\r\n const minutes = Math.floor(seconds / 60);\r\n const hours = Math.floor(minutes / 60);\r\n\r\n if (seconds < 60) return \"Just now\";\r\n if (minutes < 60) return `${minutes}m ago`;\r\n if (hours < 24) return `${hours}h ago`;\r\n return date.toLocaleDateString();\r\n }\r\n\r\n if (format === \"time-only\") {\r\n return date.toLocaleTimeString([], {\r\n hour: \"2-digit\",\r\n minute: \"2-digit\",\r\n second: \"2-digit\",\r\n });\r\n }\r\n\r\n return date.toLocaleString();\r\n}\r\n\r\n// =============================================================================\r\n// Message Card Component\r\n// =============================================================================\r\n\r\ninterface MessageCardProps {\r\n message: StreamMessage;\r\n tokens: any;\r\n size: \"sm\" | \"md\" | \"lg\";\r\n showTimestamp: boolean;\r\n timeFormat: \"relative\" | \"absolute\" | \"time-only\";\r\n onClick?: (message: StreamMessage) => void;\r\n variant: \"list\" | \"tree\" | \"compact\";\r\n}\r\n\r\nconst MessageCard = memo(function MessageCard({\r\n message,\r\n tokens,\r\n size,\r\n showTimestamp,\r\n timeFormat,\r\n onClick,\r\n variant,\r\n}: MessageCardProps) {\r\n const status = message.status || \"standby\";\r\n const statusColor = getStatusColor(status, tokens);\r\n\r\n const sizeConfig = {\r\n sm: {\r\n padding: 8,\r\n fontSize: \"0.75rem\",\r\n fontSizeSmall: \"0.625rem\",\r\n iconSize: 10,\r\n gap: 6,\r\n }, // 12px / 10px in rem\r\n md: {\r\n padding: 12,\r\n fontSize: \"0.875rem\",\r\n fontSizeSmall: \"0.75rem\",\r\n iconSize: 12,\r\n gap: 8,\r\n }, // 14px / 12px in rem\r\n lg: {\r\n padding: 16,\r\n fontSize: \"1rem\",\r\n fontSizeSmall: \"0.875rem\",\r\n iconSize: 14,\r\n gap: 10,\r\n }, // 16px / 14px in rem\r\n }[size];\r\n\r\n const isCompact = variant === \"compact\";\r\n\r\n return (\r\n <div\r\n role=\"listitem\"\r\n aria-label={`${message.text} - ${status}`}\r\n onClick={() => onClick?.(message)}\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: sizeConfig.gap,\r\n padding: isCompact\r\n ? `${sizeConfig.padding / 2}px ${sizeConfig.padding}px`\r\n : sizeConfig.padding,\r\n backgroundColor: tokens.colors.background.elevated,\r\n border: `1.5px solid ${statusColor}`,\r\n borderRadius: tokens.borderRadius.md,\r\n cursor: onClick ? \"pointer\" : \"default\",\r\n transition: \"all 0.15s ease\",\r\n ...(message.active && {\r\n boxShadow: `0 0 0 2px ${statusColor}40`,\r\n }),\r\n }}\r\n onMouseEnter={(e) => {\r\n if (onClick) {\r\n e.currentTarget.style.backgroundColor =\r\n tokens.colors.background.surface;\r\n e.currentTarget.style.transform = \"translateY(-1px)\";\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n if (onClick) {\r\n e.currentTarget.style.backgroundColor =\r\n tokens.colors.background.elevated;\r\n e.currentTarget.style.transform = \"translateY(0)\";\r\n }\r\n }}\r\n >\r\n {/* Status Shape */}\r\n <span style={{ flexShrink: 0, display: \"flex\", alignItems: \"center\" }}>\r\n {STATUS_SHAPES[status](sizeConfig.iconSize, statusColor)}\r\n </span>\r\n\r\n {/* Message Text */}\r\n <span\r\n style={{\r\n flex: 1,\r\n fontSize: sizeConfig.fontSize,\r\n fontWeight: 500,\r\n color: tokens.colors.text.primary,\r\n whiteSpace: \"nowrap\",\r\n overflow: \"hidden\",\r\n textOverflow: \"ellipsis\",\r\n }}\r\n >\r\n {message.text}\r\n </span>\r\n\r\n {/* Details (if any) */}\r\n {message.details && !isCompact && (\r\n <span\r\n style={{\r\n fontSize: sizeConfig.fontSizeSmall,\r\n color: tokens.colors.text.tertiary,\r\n }}\r\n >\r\n {message.details}\r\n </span>\r\n )}\r\n\r\n {/* Timestamp */}\r\n {showTimestamp && message.timestamp && (\r\n <span\r\n style={{\r\n fontSize: sizeConfig.fontSizeSmall,\r\n color: tokens.colors.text.tertiary,\r\n fontFamily: tokens.typography.fontFamily.mono,\r\n whiteSpace: \"nowrap\",\r\n }}\r\n >\r\n {formatTimestamp(message.timestamp, timeFormat)}\r\n </span>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\n// =============================================================================\r\n// Tree Node Component (for hierarchical display)\r\n// =============================================================================\r\n\r\ninterface TreeNodeProps {\r\n message: StreamMessage;\r\n tokens: any;\r\n size: \"sm\" | \"md\" | \"lg\";\r\n showTimestamp: boolean;\r\n timeFormat: \"relative\" | \"absolute\" | \"time-only\";\r\n onClick?: (message: StreamMessage) => void;\r\n depth: number;\r\n showConnectors: boolean;\r\n isLast: boolean;\r\n}\r\n\r\nconst TreeNode = memo(function TreeNode({\r\n message,\r\n tokens,\r\n size,\r\n showTimestamp,\r\n timeFormat,\r\n onClick,\r\n depth,\r\n showConnectors,\r\n isLast,\r\n}: TreeNodeProps) {\r\n const hasChildren = message.children && message.children.length > 0;\r\n const indentSize = 32;\r\n\r\n return (\r\n <div style={{ position: \"relative\" }}>\r\n {/* Connector Lines */}\r\n {showConnectors && depth > 0 && (\r\n <>\r\n {/* Vertical line from parent */}\r\n <div\r\n style={{\r\n position: \"absolute\",\r\n left: (depth - 1) * indentSize + 16,\r\n top: 0,\r\n width: 2,\r\n height: isLast ? \"50%\" : \"100%\",\r\n backgroundColor: tokens.colors.border.muted,\r\n }}\r\n />\r\n {/* Horizontal line to node */}\r\n <div\r\n style={{\r\n position: \"absolute\",\r\n left: (depth - 1) * indentSize + 16,\r\n top: \"50%\",\r\n width: indentSize - 8,\r\n height: 2,\r\n backgroundColor: tokens.colors.border.muted,\r\n }}\r\n />\r\n </>\r\n )}\r\n\r\n {/* Message Card */}\r\n <div style={{ marginLeft: depth * indentSize }}>\r\n <MessageCard\r\n message={message}\r\n tokens={tokens}\r\n size={size}\r\n showTimestamp={showTimestamp}\r\n timeFormat={timeFormat}\r\n onClick={onClick}\r\n variant=\"tree\"\r\n />\r\n </div>\r\n\r\n {/* Render children */}\r\n {hasChildren && (\r\n <div style={{ marginTop: 8 }}>\r\n {message.children!.map((child, index) => (\r\n <div key={child.id} style={{ marginTop: index > 0 ? 8 : 0 }}>\r\n <TreeNode\r\n message={child}\r\n tokens={tokens}\r\n size={size}\r\n showTimestamp={showTimestamp}\r\n timeFormat={timeFormat}\r\n onClick={onClick}\r\n depth={depth + 1}\r\n showConnectors={showConnectors}\r\n isLast={index === message.children!.length - 1}\r\n />\r\n </div>\r\n ))}\r\n </div>\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\n// =============================================================================\r\n// MessageStream Component\r\n// =============================================================================\r\n\r\nexport const MessageStream = memo(function MessageStream({\r\n title,\r\n subtitle,\r\n messages,\r\n maxHeight,\r\n showTimestamps = false,\r\n timeFormat = \"relative\",\r\n variant = \"list\",\r\n className = \"\",\r\n size = \"md\",\r\n onMessageClick,\r\n loading = false,\r\n emptyMessage = \"No messages\",\r\n showConnectors = true,\r\n collapsible = false,\r\n defaultExpanded = false,\r\n onPinChange,\r\n}: MessageStreamProps): React.ReactElement {\r\n const { tokens, theme, prefersReducedMotion } = useTheme();\r\n const isTransparentTheme =\r\n theme === \"transparent\" ||\r\n theme === \"transparent-bold\" ||\r\n theme === \"transparent-minimal\";\r\n\r\n // Collapsible compact mode state management\r\n const { expanded, isPinned, handlers, transitionDuration } = useCompactMode({\r\n compact: collapsible,\r\n defaultExpanded,\r\n onPinChange,\r\n });\r\n\r\n // Card container styles - Use transparent background with blur for transparent themes (matching Timeline)\r\n const containerStyle = useMemo(\r\n () => ({\r\n backgroundColor: isTransparentTheme\r\n ? \"transparent\"\r\n : tokens.colors.background.surface,\r\n ...(isTransparentTheme && {\r\n backdropFilter: \"blur(12px)\" as const,\r\n WebkitBackdropFilter: \"blur(12px)\" as const,\r\n }),\r\n ...(tokens.colors.border.cardStyle ?? {\r\n border: `1px solid ${tokens.colors.border.muted}`,\r\n }),\r\n borderRadius: tokens.borderRadius.lg,\r\n padding: tokens.spacing.md,\r\n display: \"flex\",\r\n flexDirection: \"column\" as const,\r\n }),\r\n [tokens, isTransparentTheme],\r\n );\r\n\r\n // Count messages by status for compact view summary (must be before any conditional return for rules-of-hooks)\r\n const statusCounts = useMemo(() => {\r\n const counts = { normal: 0, caution: 0, critical: 0, other: 0 };\r\n messages.forEach((m) => {\r\n if (m.status === \"normal\") counts.normal++;\r\n else if (m.status === \"caution\") counts.caution++;\r\n else if (m.status === \"critical\" || m.status === \"serious\")\r\n counts.critical++;\r\n else counts.other++;\r\n });\r\n return counts;\r\n }, [messages]);\r\n\r\n // Loading skeleton\r\n if (loading) {\r\n return (\r\n <article\r\n className={classNames(\"zendir-message-stream\", className)}\r\n style={containerStyle}\r\n aria-busy=\"true\"\r\n aria-label=\"Loading messages\"\r\n >\r\n {title && (\r\n <div style={{ marginBottom: 16 }}>\r\n <div\r\n style={{\r\n height: 18,\r\n width: 120,\r\n backgroundColor: tokens.colors.background.elevated,\r\n borderRadius: 4,\r\n marginBottom: 8,\r\n }}\r\n />\r\n {subtitle && (\r\n <div\r\n style={{\r\n height: 14,\r\n width: 200,\r\n backgroundColor: tokens.colors.background.elevated,\r\n borderRadius: 4,\r\n }}\r\n />\r\n )}\r\n </div>\r\n )}\r\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 8 }}>\r\n {[1, 2, 3].map((i) => (\r\n <div\r\n key={i}\r\n style={{\r\n height: 44,\r\n backgroundColor: tokens.colors.background.elevated,\r\n borderRadius: tokens.borderRadius.md,\r\n border: `2px solid ${tokens.colors.border.muted}`,\r\n }}\r\n />\r\n ))}\r\n </div>\r\n </article>\r\n );\r\n }\r\n\r\n // Collapsed view - shows summary of messages\r\n if (collapsible && !expanded) {\r\n return (\r\n <article\r\n className={classNames(\"zendir-message-stream\", \"collapsed\", className)}\r\n {...handlers}\r\n style={{\r\n ...containerStyle,\r\n padding: `${tokens.spacing.sm} ${tokens.spacing.md}`,\r\n cursor: \"pointer\",\r\n transition: `all ${transitionDuration} ease-out`,\r\n }}\r\n onMouseEnter={(e) => {\r\n handlers.onMouseEnter();\r\n if (!prefersReducedMotion) {\r\n (e.currentTarget as HTMLElement).style.transform = \"scale(1.01)\";\r\n (e.currentTarget as HTMLElement).style.boxShadow =\r\n `0 4px 12px ${tokens.colors.accent.primary}20`;\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n handlers.onMouseLeave();\r\n (e.currentTarget as HTMLElement).style.transform = \"scale(1)\";\r\n (e.currentTarget as HTMLElement).style.boxShadow = \"\";\r\n }}\r\n aria-label={`${title || \"Messages\"} (collapsed)`}\r\n >\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n justifyContent: \"space-between\",\r\n gap: tokens.spacing.md,\r\n }}\r\n >\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.sm,\r\n }}\r\n >\r\n <Icon\r\n name=\"message\"\r\n size={14}\r\n color={tokens.colors.accent.primary}\r\n />\r\n <span style={{ fontWeight: tokens.typography.fontWeight.medium }}>\r\n {title ? title.split(\" \")[0] : \"Messages\"}\r\n </span>\r\n <span\r\n style={{\r\n color: tokens.colors.text.tertiary,\r\n fontSize: tokens.typography.fontSize.xs,\r\n }}\r\n >\r\n ({messages.length})\r\n </span>\r\n </div>\r\n\r\n <div\r\n style={{\r\n display: \"flex\",\r\n alignItems: \"center\",\r\n gap: tokens.spacing.sm,\r\n }}\r\n >\r\n {statusCounts.critical > 0 && (\r\n <span\r\n style={{\r\n color: tokens.colors.status.critical,\r\n fontSize: tokens.typography.fontSize.xs,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n }}\r\n >\r\n {statusCounts.critical} critical\r\n </span>\r\n )}\r\n {statusCounts.caution > 0 && (\r\n <span\r\n style={{\r\n color: tokens.colors.status.caution,\r\n fontSize: tokens.typography.fontSize.xs,\r\n fontWeight: tokens.typography.fontWeight.medium,\r\n }}\r\n >\r\n {statusCounts.caution} warn\r\n </span>\r\n )}\r\n {isPinned && (\r\n <Icon\r\n name=\"lock\"\r\n size={12}\r\n color={tokens.colors.accent.primary}\r\n />\r\n )}\r\n </div>\r\n </div>\r\n </article>\r\n );\r\n }\r\n\r\n return (\r\n <div\r\n className={classNames(\r\n \"zendir-message-stream\",\r\n collapsible ? \"expanded\" : \"\",\r\n className,\r\n )}\r\n {...(collapsible ? handlers : {})}\r\n role={collapsible ? handlers.role : \"log\"}\r\n tabIndex={collapsible ? handlers.tabIndex : undefined}\r\n aria-expanded={collapsible ? handlers[\"aria-expanded\"] : undefined}\r\n style={{\r\n ...containerStyle,\r\n cursor: collapsible ? \"pointer\" : undefined,\r\n transition: collapsible\r\n ? `all ${transitionDuration} ease-out`\r\n : undefined,\r\n }}\r\n aria-label={title || \"Message stream\"}\r\n aria-live=\"polite\"\r\n >\r\n {/* Header */}\r\n {(title || subtitle) && (\r\n <header style={{ marginBottom: 16 }}>\r\n {title && (\r\n <h3\r\n style={{\r\n fontSize: tokens.typography.fontSize.lg,\r\n fontWeight: 500, // AstroUXDS medium for headers\r\n color: tokens.colors.text.primary,\r\n margin: 0,\r\n }}\r\n >\r\n {title}\r\n </h3>\r\n )}\r\n {subtitle && (\r\n <p\r\n style={{\r\n fontSize: tokens.typography.fontSize.sm,\r\n color: tokens.colors.text.tertiary,\r\n margin: \"4px 0 0 0\",\r\n }}\r\n >\r\n {subtitle}\r\n </p>\r\n )}\r\n </header>\r\n )}\r\n\r\n {/* Messages Container — tabIndex for keyboard scroll access (WCAG 2.1.1) */}\r\n <div\r\n role=\"list\"\r\n tabIndex={maxHeight ? 0 : undefined}\r\n aria-label={\r\n maxHeight ? `${title || \"Messages\"} scrollable list` : undefined\r\n }\r\n style={{\r\n display: \"flex\",\r\n flexDirection: \"column\",\r\n gap: variant === \"compact\" ? 4 : 8,\r\n ...(maxHeight && {\r\n maxHeight,\r\n overflowY: \"auto\",\r\n paddingRight: 8,\r\n }),\r\n }}\r\n >\r\n {messages.length === 0 ? (\r\n <div\r\n style={{\r\n textAlign: \"center\",\r\n padding: 32,\r\n color: tokens.colors.text.tertiary,\r\n fontSize: tokens.typography.fontSize.sm,\r\n }}\r\n >\r\n {emptyMessage}\r\n </div>\r\n ) : variant === \"tree\" ? (\r\n messages.map((message, index) => (\r\n <TreeNode\r\n key={message.id}\r\n message={message}\r\n tokens={tokens}\r\n size={size}\r\n showTimestamp={showTimestamps}\r\n timeFormat={timeFormat}\r\n onClick={onMessageClick}\r\n depth={0}\r\n showConnectors={showConnectors}\r\n isLast={index === messages.length - 1}\r\n />\r\n ))\r\n ) : (\r\n messages.map((message) => (\r\n <MessageCard\r\n key={message.id}\r\n message={message}\r\n tokens={tokens}\r\n size={size}\r\n showTimestamp={showTimestamps}\r\n timeFormat={timeFormat}\r\n onClick={onMessageClick}\r\n variant={variant}\r\n />\r\n ))\r\n )}\r\n </div>\r\n </div>\r\n );\r\n});\r\n\r\nexport default MessageStream;\r\n"],"mappings":"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"}
|