"use client" import { Button } from "@/components/ui/button" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" import { cn } from "@/lib/utils" import { languageIcons } from "@/settings/LanguageIcon" import { Check, Clipboard, FileCode, RotateCcw, Search, X } from "lucide-react" import Prism from "prismjs" import type React from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react" interface PreProps { children?: React.ReactNode raw?: string className?: string highlightLines?: number[] folderPath?: string showLineNumbers?: boolean contentKey?: string | number title?: string description?: string maxHeight?: number showHeader?: boolean enableSearch?: boolean wordWrap?: boolean showBorder?: boolean showShadow?: boolean autoFormat?: boolean onContentChange?: (content: string) => void customActions?: React.ReactNode } interface CopyButtonProps { content: string className?: string } const DEFAULT_LANGUAGE = "tsx" const COPY_FEEDBACK_DURATION = 2000 const DEFAULT_MAX_HEIGHT = 650 const extractLanguageFromClassName = (className?: string): string => { if (!className?.includes("language-")) return DEFAULT_LANGUAGE return className.split("language-")[1]?.split(" ")[0] || DEFAULT_LANGUAGE } const processContent = (children?: React.ReactNode, raw?: string): string => { if (raw) return raw.trim() if (typeof children === "string") return children.trim() if (children) return children.toString().trim() return "" } const CopyButton: React.FC = ({ content, className }) => { const [state, setState] = useState<"idle" | "copied" | "error">("idle") const [hasCopied, setHasCopied] = useState(false) const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000, onCopy: () => setState("copied"), }) useEffect(() => { if (hasCopied) { const timer = setTimeout(() => setHasCopied(false), 2000) return () => clearTimeout(timer) } }, [hasCopied]) useEffect(() => { if (isCopied) { setState("copied") } else { setState("idle") } }, [isCopied]) const handleCopy = () => { try { copyToClipboard(content) } catch { setState("error") } } const buttonConfig = { idle: { icon: ( ), title: "Copy to clipboard", }, copied: { icon: , title: "Copied!", }, error: { icon: , title: "Failed to copy", }, } const currentConfig = buttonConfig[state] return ( {hasCopied ? "Copied" : "Copy command"} ) } const SearchInput: React.FC<{ searchQuery: string onSearchChange: (query: string) => void onClose: () => void }> = ({ searchQuery, onSearchChange, onClose }) => { const inputRef = useRef(null) useEffect(() => { inputRef.current?.focus() }, []) useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") onClose() } document.addEventListener("keydown", handleEscape) return () => document.removeEventListener("keydown", handleEscape) }, [onClose]) return (
onSearchChange(e.target.value)} placeholder="Search in code..." className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground text-foreground" />
) } const CodeHeader: React.FC<{ title?: string description?: string folderPath?: string language: string enableSearch: boolean isSearchVisible: boolean onToggleSearch: () => void content: string customActions?: React.ReactNode }> = ({ title, description, folderPath, language, enableSearch, isSearchVisible, onToggleSearch, content, customActions, }) => (
{title ? (
{title} {description &&

{description}

}
) : folderPath ? ( {folderPath} ) : null}
{enableSearch && ( )}
{languageIcons[language] || }
{customActions}
) const Pre: React.FC = ({ children, raw, className = "", highlightLines = [], folderPath, showLineNumbers = true, contentKey, title, description, maxHeight = DEFAULT_MAX_HEIGHT, showHeader = true, enableSearch = true, wordWrap = false, autoFormat = true, showBorder = true, onContentChange, showShadow = true, customActions, }) => { const [isClient, setIsClient] = useState(false) const [searchQuery, setSearchQuery] = useState("") const [isSearchVisible, setIsSearchVisible] = useState(false) const preRef = useRef(null) const codeRef = useRef(null) // Memoized values const language = useMemo(() => extractLanguageFromClassName(className), [className]) const content = useMemo(() => { const processed = processContent(children, raw) onContentChange?.(processed) return processed }, [children, raw, contentKey, onContentChange]) const filteredContent = useMemo(() => { if (!searchQuery.trim()) return content const searchRegex = new RegExp(searchQuery.trim(), "gi") return content .split("\n") .filter((line) => searchRegex.test(line)) .join("\n") }, [content, searchQuery]) const displayContent = searchQuery.trim() ? filteredContent : content // Effects useEffect(() => { setIsClient(true) }, []) useEffect(() => { if (!isClient || !codeRef.current) return codeRef.current.textContent = displayContent Prism.highlightElement(codeRef.current) if (showLineNumbers && preRef.current) { preRef.current.classList.add("line-numbers") } }, [displayContent, isClient, language, showLineNumbers]) const handleToggleSearch = useCallback(() => { setIsSearchVisible((prev) => !prev) if (isSearchVisible) { setSearchQuery("") } }, [isSearchVisible]) const handleSearchChange = useCallback((query: string) => { setSearchQuery(query) }, []) const handleCloseSearch = useCallback(() => { setIsSearchVisible(false) setSearchQuery("") }, []) const lineNumbersClass = showLineNumbers ? "line-numbers" : "" if (!isClient) { return (
          {content}
        
) } return (
{showHeader && ( )} {isSearchVisible && ( )}
 0 ? highlightLines.join(",") : undefined}
      >
        
          {displayContent}
        
        {/* {
          showShadow && (
            
) } */}
) } export default Pre