"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 (
Локальный снимок. Подтяните с роутера, если нужен private-key с устройства.
) : null}