/** * @fileoverview Configuration panel modal * * Modal component displaying Writenex Astro configuration settings, * including image settings, editor settings, and discovered collections. * Includes focus trap for accessibility compliance. * * @module @writenex/astro/client/components/ConfigPanel */ import { Check, ChevronDown, Copy, ExternalLink, Folder, Image, Info, Settings, X, } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useSharedApi } from "../../context/ApiContext"; import type { Collection, WritenexClientConfig } from "../../hooks/useApi"; import { useFocusTrap } from "../../hooks/useFocusTrap"; import { type EditorType, getAvailableEditors, getPreferredEditor, openInEditor, setPreferredEditor, } from "../../utils/openInEditor"; import "./ConfigPanel.css"; /** * Props for the ConfigPanel component */ interface ConfigPanelProps { /** Current configuration */ config: WritenexClientConfig | null; /** Discovered collections */ collections: Collection[]; /** Whether the modal is open */ isOpen: boolean; /** Callback to close the modal */ onClose: () => void; } /** * Configuration panel modal component * * @component * @example * ```tsx * setShowConfig(false)} * /> * ``` */ export function ConfigPanel({ config, collections, isOpen, onClose, }: ConfigPanelProps): React.ReactElement | null { const api = useSharedApi(); const triggerRef = useRef(null); const [configPath, setConfigPath] = useState(null); const [hasConfigFile, setHasConfigFile] = useState(false); const [copied, setCopied] = useState(false); const [selectedEditor, setSelectedEditor] = useState( getPreferredEditor() ); const [showEditorDropdown, setShowEditorDropdown] = useState(false); // Fetch config path when modal opens useEffect(() => { if (isOpen) { api .getConfigPath() .then((data) => { setConfigPath(data.configPath); setHasConfigFile(data.hasConfigFile); }) .catch(() => { setConfigPath(null); setHasConfigFile(false); }); } }, [isOpen, api]); // Store the trigger element when modal opens useEffect(() => { if (isOpen) { triggerRef.current = document.activeElement as HTMLElement; } }, [isOpen]); // Focus trap for accessibility const { containerRef } = useFocusTrap({ enabled: isOpen, onEscape: onClose, returnFocusTo: triggerRef.current, }); const handleOpenInEditor = useCallback(() => { if (configPath) { openInEditor(configPath, selectedEditor); setPreferredEditor(selectedEditor); } }, [configPath, selectedEditor]); const handleCopyPath = useCallback(() => { if (configPath) { navigator.clipboard.writeText(configPath).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); } }, [configPath]); const handleEditorSelect = useCallback((editor: EditorType) => { setSelectedEditor(editor); setPreferredEditor(editor); setShowEditorDropdown(false); }, []); if (!isOpen) return null; const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }; return (
{/* Header */}

Configuration

{/* Content */}
{/* Configuration File - Primary action at top */}

Configuration File

{hasConfigFile && configPath ? ( <>

Configuration loaded from{" "} {configPath.split("/").pop()}

{showEditorDropdown && (
{getAvailableEditors().map((editor) => ( ))}
)}
) : (

No configuration file found. Using default settings.

)}
{/* Image Settings */}

Image Settings

{config?.images?.publicPath && ( )} {config?.images?.storagePath && ( )}
{/* Editor Settings */}

Editor Settings

{/* Collections */}

Collections ({collections.length})

{collections.length === 0 ? (

No collections discovered

) : (
{collections.map((col) => ( ))}
)}
); } /** * Get CSS class for strategy value */ function getStrategyClass(strategy?: string): string { switch (strategy) { case "colocated": return "wn-config-item-value--violet"; case "public": return "wn-config-item-value--emerald"; default: return "wn-config-item-value--amber"; } } /** * Get description for strategy value */ function getStrategyDescription(strategy?: string): string { switch (strategy) { case "colocated": return "Images stored alongside content files"; case "public": return "Images stored in public folder"; default: return "Custom storage path"; } } /** * Config item component */ function ConfigItem({ label, value, valueClass = "", description, }: { label: string; value: string; valueClass?: string; description?: string; }): React.ReactElement { return (
{label}
{value} {description && (

{description}

)}
); } /** * Collection card component */ function CollectionCard({ collection, }: { collection: Collection; }): React.ReactElement { return (
{collection.name} {collection.count} items

Path: {collection.path}

Pattern: {collection.filePattern}

{collection.schema && (

Schema: {Object.keys(collection.schema).length} fields detected

)} {collection.previewUrl && (

Preview: {collection.previewUrl}

)}
); }