← back to Open Seo
src/client/features/ai-mcp/SetupControls.tsx
145 lines
import { Check, ChevronDown, Copy } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
export function Collapsible({
id,
title,
subtitle,
icon,
children,
}: {
id: string;
title: string;
subtitle?: string;
icon?: React.ReactNode;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
const contentId = `collapsible-${id}`;
return (
<div>
<button
type="button"
onClick={() => setOpen((value) => !value)}
aria-expanded={open}
aria-controls={contentId}
className="flex w-full items-center justify-between gap-3 px-4 py-3.5 text-left transition-colors hover:bg-base-300/50"
>
<div className="flex min-w-0 items-center gap-3">
{icon ? (
<span className="flex size-5 shrink-0 items-center justify-center text-base-content">
{icon}
</span>
) : null}
<div className="flex min-w-0 flex-col gap-0.5">
<span className="text-sm font-medium text-base-content">
{title}
</span>
{subtitle ? (
<span className="text-xs text-base-content/55">{subtitle}</span>
) : null}
</div>
</div>
<ChevronDown
className={`size-4 shrink-0 text-base-content/50 transition-transform ${
open ? "rotate-180" : ""
}`}
/>
</button>
{open ? (
<div id={contentId} className="space-y-4 px-4 pb-6 pt-3">
{children}
</div>
) : null}
</div>
);
}
export function CodeBlock({
code,
onCopy,
}: {
code: string;
onCopy?: () => void;
}) {
return (
<div className="flex items-stretch overflow-hidden rounded-md border border-base-300 bg-base-100">
<pre className="min-w-0 flex-1 overflow-x-auto p-3 text-xs leading-relaxed text-base-content">
<code className="font-mono">{code}</code>
</pre>
<div className="flex shrink-0 items-start border-l border-base-300 p-1.5">
<CopyButton
value={code}
successMessage="Copied to clipboard"
iconOnly
onCopy={onCopy}
/>
</div>
</div>
);
}
export function CopyButton({
value,
successMessage,
iconOnly = false,
onCopy,
}: {
value: string;
successMessage: string;
iconOnly?: boolean;
onCopy?: () => void;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
toast.error("Clipboard not available");
return;
}
try {
await navigator.clipboard.writeText(value);
toast.success(successMessage);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
onCopy?.();
} catch {
toast.error("Could not copy to clipboard");
}
};
if (iconOnly) {
return (
<button
type="button"
onClick={handleCopy}
aria-label="Copy"
className="flex size-7 items-center justify-center rounded-md text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
>
{copied ? (
<Check className="size-3.5 text-success" />
) : (
<Copy className="size-3.5" />
)}
</button>
);
}
return (
<button
type="button"
onClick={handleCopy}
className="inline-flex items-center gap-1.5 rounded-md border border-base-300 bg-base-100 px-2 py-1 text-xs font-medium text-base-content/70 transition-colors hover:bg-base-300/50 hover:text-base-content"
>
{copied ? (
<Check className="size-3 text-success" />
) : (
<Copy className="size-3" />
)}
Copy
</button>
);
}