"use client" import { useEffect, useMemo, useState, type ReactNode } from "react" import { Alert, AlertDescription, AlertTitle, } from "@/components/reui/alert" import { Badge } from "@/components/reui/badge" import { Frame, FramePanel } from "@/components/reui/frame" import { Button } from "@/components/ui/button" import { ScrollArea } from "@/components/ui/scroll-area" import { Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet" import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" import { CheckIcon, CopyIcon, DownloadIcon, LoaderCircleIcon, RefreshCwIcon, TriangleAlertIcon, } from "lucide-react" import { cn } from "@/lib/utils" export type CodeExportFormat = { id: string label: string filename: string code: string /** Empty / unavailable — show warning instead of code */ emptyMessage?: string /** Content came from live device fetch */ isLive?: boolean } function downloadText(filename: string, content: string) { const blob = new Blob([content], { type: "text/plain;charset=utf-8" }) const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } function highlightLine(line: string): string { const trimmed = line.trimStart() if (line.startsWith("#") || trimmed.startsWith(";")) return "text-muted-foreground" if (trimmed.startsWith("[")) return "text-info" if (trimmed.startsWith("/")) return "text-info" if (/^\s+[a-zA-Z]/.test(line) || /^[A-Za-z][\w-]*=/.test(line)) return "text-foreground" return "text-foreground/90" } function CodeBlock({ code }: { code: string }) { const lines = code.length ? code.split("\n") : [""] return (
      {lines.map((line, i) => (
        
          {line || " "}
        
      ))}
    
) } export interface CodeExportSheetProps { open: boolean onClose: () => void title: string description?: ReactNode formats: CodeExportFormat[] /** Initial format id when sheet opens */ initialFormatId?: string /** Optional live fetch from device */ onLiveFetch?: (formatId: string) => void liveBusy?: boolean /** Short CTA — keep laconic */ liveLabel?: string className?: string /** Extra content under format tabs (e.g. meta strip) */ beforeCode?: ReactNode } /** * Shared code export Sheet — ReUI Frame + sticky footer. * Preview DNA: https://reui.io/preview/base/sheet-8 · Frame https://reui.io/docs/components/base/frame * Alert: https://reui.io/docs/components/base/alert */ function CodeExportSheet({ open, onClose, title, description, formats, initialFormatId, onLiveFetch, liveBusy, liveLabel = "С роутера", className, beforeCode, }: CodeExportSheetProps) { const firstId = formats[0]?.id ?? "default" const [tab, setTab] = useState(initialFormatId ?? firstId) const [copied, setCopied] = useState(false) useEffect(() => { if (!open) return setTab( initialFormatId && formats.some((f) => f.id === initialFormatId) ? initialFormatId : firstId, ) setCopied(false) }, [open, initialFormatId, firstId, formats]) const active = useMemo( () => formats.find((f) => f.id === tab) ?? formats[0], [formats, tab], ) const code = active?.code ?? "" const emptyMessage = active?.emptyMessage const filename = active?.filename ?? "export.txt" const canCopy = Boolean(code) && !emptyMessage const isLive = Boolean(active?.isLive) const showTabs = formats.length > 1 function handleCopy() { if (!canCopy) return void navigator.clipboard.writeText(code).then(() => { setCopied(true) setTimeout(() => setCopied(false), 1800) }) } return ( { if (!v) onClose() }}> {title} {description ? ( {description} ) : null}
{showTabs ? ( setTab(String(v))} className="shrink-0 gap-0" > {formats.map((f) => ( {f.label} ))} ) : null} {onLiveFetch || beforeCode ? (
{onLiveFetch ? ( {isLive ? "С роутера" : "Локально"} ) : null} {beforeCode}
{onLiveFetch ? ( ) : null}
) : null} {emptyMessage ? (
Нет данных {emptyMessage}
) : (
)}
{onLiveFetch && !isLive ? (

Локальный снимок. Подтяните с роутера, если нужен private-key с устройства.

) : null}
} > Закрыть
) } export { CodeExportSheet, downloadText }