Files
Sahaj Jain 5fe4958db2 Feature/preview external (#225)
* Enhance Preview Panel and Support External Websites (#184)

* feat: Add Block Viewer for Preview Panel

* feat: support theme injection for external websites

* refactor: Improve Naming and Composition

* Support for Google Fonts in Preview script and Error Feedback (#202)

* feat: Add support for loading Google Fonts in the embed script.

* feat: Add error state and UI feedback

* fix font loading by overriding classes

* add reset button + update some ui

* syntax highlighting plus copy changes

* rename script

* update ui for connection status

* ui

* zustand store for preview state

* use nuqs for active preview tab

* get shadows working

* refactor

* A

* add tracking

---------

Co-authored-by: Luis Llanes <137589205+llanesluis@users.noreply.github.com>
Co-authored-by: Luis Llanes <luisllaboj@gmail.com>
2025-10-12 02:15:57 +05:30

125 lines
3.2 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { CheckIcon, CopyIcon } from "lucide-react";
import type { ComponentProps, HTMLAttributes, ReactNode } from "react";
import { createContext, useContext, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useTheme } from "../theme-provider";
type CodeBlockContextType = {
code: string;
};
const CodeBlockContext = createContext<CodeBlockContextType>({
code: "",
});
export type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {
code: string;
language: string;
showLineNumbers?: boolean;
children?: ReactNode;
};
export const CodeBlock = ({
code,
language,
showLineNumbers = false,
className,
children,
...props
}: CodeBlockProps) => {
const { theme } = useTheme();
return (
<CodeBlockContext.Provider value={{ code }}>
<div
className={cn(
"bg-background text-foreground relative w-full overflow-hidden rounded-md border",
className
)}
{...props}
>
<div className="relative">
<SyntaxHighlighter
className="overflow-hidden"
codeTagProps={{
className: "font-mono text-sm",
}}
customStyle={{
margin: 0,
padding: "1rem",
fontSize: "0.875rem",
background: "hsl(var(--background))",
color: "hsl(var(--foreground))",
}}
language={language}
lineNumberStyle={{
color: "hsl(var(--muted-foreground))",
paddingRight: "1rem",
minWidth: "2.5rem",
}}
showLineNumbers={showLineNumbers}
style={theme === "light" ? oneLight : oneDark}
>
{code}
</SyntaxHighlighter>
{children && (
<div className="absolute top-2 right-2 flex items-center gap-2">{children}</div>
)}
</div>
</div>
</CodeBlockContext.Provider>
);
};
export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {
onCopy?: () => void;
onError?: (error: Error) => void;
timeout?: number;
};
export const CodeBlockCopyButton = ({
onCopy,
onError,
timeout = 2000,
children,
className,
...props
}: CodeBlockCopyButtonProps) => {
const [isCopied, setIsCopied] = useState(false);
const { code } = useContext(CodeBlockContext);
const copyToClipboard = async () => {
if (typeof window === "undefined" || !navigator.clipboard.writeText) {
onError?.(new Error("Clipboard API not available"));
return;
}
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
onCopy?.();
setTimeout(() => setIsCopied(false), timeout);
} catch (error) {
onError?.(error as Error);
}
};
const Icon = isCopied ? CheckIcon : CopyIcon;
return (
<Button
className={cn("shrink-0", className)}
onClick={copyToClipboard}
size="icon"
variant="ghost"
{...props}
>
{children ?? <Icon size={14} />}
</Button>
);
};