diff --git a/components/ai-elements/code-block.tsx b/components/ai-elements/code-block.tsx index 4b6f2626..107b459d 100644 --- a/components/ai-elements/code-block.tsx +++ b/components/ai-elements/code-block.tsx @@ -7,6 +7,7 @@ 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; @@ -30,69 +31,49 @@ export const CodeBlock = ({ className, children, ...props -}: CodeBlockProps) => ( - -
-
- - {code} - - - {code} - - {children && ( -
{children}
+}: CodeBlockProps) => { + const { theme } = useTheme(); + return ( + +
+
+ + {code} + + {children && ( +
{children}
+ )} +
-
- -); + + ); +}; export type CodeBlockCopyButtonProps = ComponentProps & { onCopy?: () => void; diff --git a/components/block-viewer.tsx b/components/block-viewer.tsx new file mode 100644 index 00000000..726bf880 --- /dev/null +++ b/components/block-viewer.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { ComponentErrorBoundary } from "@/components/error-boundary"; +import { TooltipWrapper } from "@/components/tooltip-wrapper"; +import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; +import { cn } from "@/lib/utils"; +import { Monitor, Smartphone, Tablet } from "lucide-react"; +import React from "react"; +import { ImperativePanelHandle } from "react-resizable-panels"; + +type BlockViewerContext = { + resizablePanelRef: React.RefObject; + toggleValue: string; + setToggleValue: (value: string) => void; +}; + +const BlockViewerContext = React.createContext(null); + +function useBlockViewer() { + const context = React.useContext(BlockViewerContext); + if (!context) { + throw new Error("useBlockViewer must be used within a BlockViewerProvider."); + } + return context; +} + +export function BlockViewerProvider({ children }: { children: React.ReactNode }) { + const resizablePanelRef = React.useRef(null); + const [toggleValue, setToggleValue] = React.useState("100"); + + return ( + + {children} + + ); +} + +export function BlockViewer({ + className, + name, + children, + ...props +}: React.ComponentPropsWithoutRef<"div"> & { + name: string; +}) { + return ( + +
+ + {children} +
+
+ ); +} + +export function BlockViewerToolbar({ + name, + toolbarControls, +}: { + name: string; + toolbarControls?: React.ReactNode; +}) { + const { resizablePanelRef, toggleValue, setToggleValue } = useBlockViewer(); + + return ( +
+
+
+ {!!toolbarControls ? ( + toolbarControls + ) : ( + {name} + )} +
+ +
+ { + if (value && resizablePanelRef?.current) { + resizablePanelRef.current.resize(parseInt(value)); + setToggleValue(value); + } + }} + > + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} + +export function BlockViewerDisplay({ + name, + className, + children, + ...props +}: React.ComponentPropsWithoutRef<"div"> & { + name: string; +}) { + const { resizablePanelRef, setToggleValue } = useBlockViewer(); + + // Auto-resize to full width when screen goes under lg breakpoint (1024px) + React.useEffect(() => { + const mql = window.matchMedia("(max-width: 1023px)"); + const resizePanel = () => { + if (window.innerWidth < 1024 && resizablePanelRef?.current) { + resizablePanelRef.current.resize(100); + setToggleValue("100"); + } + }; + + resizePanel(); + mql.addEventListener("change", resizePanel); + return () => mql.removeEventListener("change", resizePanel); + }, [resizablePanelRef, setToggleValue]); + + return ( + +
+ +
+ + + {children} + + + + + +
+ + ); +} diff --git a/components/dynamic-website-preview.tsx b/components/dynamic-website-preview.tsx new file mode 100644 index 00000000..f9eafa56 --- /dev/null +++ b/components/dynamic-website-preview.tsx @@ -0,0 +1,611 @@ +"use client"; + +import Logo from "@/assets/logo.svg"; +import { + BlockViewerDisplay, + BlockViewerProvider, + BlockViewerToolbar, +} from "@/components/block-viewer"; +import { LoadingLogo } from "@/components/editor/ai/loading-logo"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { CodeBlock, CodeBlockCopyButton } from "@/components/ai-elements/code-block"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "@/components/ui/hover-card"; +import { Input } from "@/components/ui/input"; +import { useIframeThemeInjector } from "@/hooks/use-iframe-theme-injector"; +import { useWebsitePreview } from "@/hooks/use-website-preview"; +import { cn } from "@/lib/utils"; +import { IframeStatus } from "@/types/live-preview-embed"; +import { usePostHog } from "posthog-js/react"; +import { + AlertCircle, + CheckCircle, + ExternalLink, + Globe, + Info, + Loader, + RefreshCw, + X, + XCircle, +} from "lucide-react"; +import React, { useEffect, useRef } from "react"; + +/** + * Dynamic Website Preview - Load and theme external websites + * + * Usage Examples: + * + * // Same-origin mode (default) - direct DOM theme injection + * + * + * // Cross-origin mode - requires external sites to include embed script + * + * + * The allowCrossOrigin flag must be explicitly set to true to enable + * external website theming via the embed script. + */ + +const SCRIPT_URL = "https://tweakcn.com/live-preview.js"; + +// Code snippets for quick installation across common setups +const HTML_SNIPPET = `\n`; + +const NEXT_APP_SNIPPET = `// app/layout.tsx\nexport default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + +