(false); // State to track mount node readiness\n\n\n\n // Ref to store the initial size at the start of dragging\n const initialSizeRef = useRef<{ width: number }>(initialSize);\n\n const updateIframeHeight = () => {\n if (!iframeRef.current || !mountNodeRef.current) return;\n const newHeight = mountNodeRef.current.scrollHeight;\n iframeRef.current.style.height = `${newHeight}px`;\n };\n\n const dragConfig = useMemo(() => {\n return {\n axis: \"x\",\n from: () => [0, 0],\n filterTaps: true,\n } as DragConfig\n }, []);\n\n // Handle resizing using useDrag\n const bindResizer = useDrag(\n ({ down, movement: [mx], first }) => {\n if (first) {\n // Capture the initial size when drag starts\n initialSizeRef.current = {\n width: iframeRef.current?.offsetWidth || 0,\n };\n }\n\n if (down) {\n // Calculate new size based on initial size and movement\n setIframeSize({\n width: initialSizeRef.current.width + mx,\n });\n }\n },\n dragConfig as any\n );\n\n useLayoutEffect(() => {\n if (resizable && iframeRef.current) {\n setIframeSize({\n width: iframeRef.current.parentElement?.offsetWidth || 600, // Fallback to 600px or any default width\n });\n }\n }, [resizable]);\n\n useLayoutEffect(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n const handleLoad = () => {\n const iframeDoc =\n iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n\n if (!mountNodeRef.current) {\n // Create a div inside the iframe for mounting React components only once\n const mountNode = iframeDoc.createElement(\"div\");\n mountNode.id = \"iframe-mount-node\";\n iframeDoc.body.appendChild(mountNode);\n mountNodeRef.current = mountNode;\n\n iframeDoc.body.style.backgroundColor = \"transparent\";\n\n // Function to inject stylesheets into the iframe\n const injectStylesheets = () => {\n // Select all linked stylesheets in the parent document\n const links = Array.from(\n document.querySelectorAll('link[rel=\"stylesheet\"]')\n );\n // Select all style tags in the parent document\n const styles = Array.from(document.querySelectorAll(\"style\"));\n\n // Inject linked stylesheets\n links.forEach((link) => {\n const clonedLink = iframeDoc.createElement(\"link\");\n clonedLink.rel = \"stylesheet\";\n clonedLink.href = (link as HTMLLinkElement).href;\n iframeDoc.head.appendChild(clonedLink);\n });\n\n // Inject inline styles\n styles.forEach((style) => {\n const clonedStyle = iframeDoc.createElement(\"style\");\n clonedStyle.textContent = style.textContent || \"\";\n iframeDoc.head.appendChild(clonedStyle);\n });\n };\n\n // Inject styles on initial load\n injectStylesheets();\n\n const resizeObserver = new ResizeObserver(() => {\n updateIframeHeight();\n });\n\n // Observe the mount node for size changes\n resizeObserver.observe(mountNodeRef.current);\n\n const mutationObserver = new MutationObserver(() => {\n updateIframeHeight();\n });\n\n // Observe the mount node for changes\n mutationObserver.observe(mountNodeRef.current, {\n childList: true,\n subtree: true,\n });\n\n // Initial height update\n setTimeout(updateIframeHeight, 0);\n\n // Set the mount node as ready\n setIsMounted(true);\n\n // Cleanup observer on unmount\n return () => {\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }\n };\n\n // Attach the load event listener\n iframe.addEventListener(\"load\", handleLoad);\n\n // If the iframe is already loaded, trigger the handler\n if (iframe.contentDocument?.readyState === \"complete\") {\n handleLoad();\n }\n\n // Cleanup event listener on unmount\n return () => {\n iframe.removeEventListener(\"load\", handleLoad);\n };\n }, [children]);\n\n\n const resizerStyle = useMemo(() => {\n return {\n left: iframeSize?.width ? `${iframeSize.width}px` : undefined\n }\n }, [iframeSize]);\n\n const iframeStyle = useMemo(() => {\n return {\n width: resizable\n ? iframeSize\n ? `${iframeSize.width}px`\n : \"100%\"\n : undefined,\n opacity: isMounted ? 1 : 0,\n transition: \"opacity 0.5s ease-in\",\n ...style,\n }\n }, [iframeSize, resizable, isMounted, style]);\n\n const bindResizerValues = useMemo(() => {\n return typeof bindResizer === 'function' ? bindResizer() : {};\n }, [bindResizer]);\n\n return (\n \n {resizable && (\n \n \n \n )}\n \n {isMounted &&\n mountNodeRef.current &&\n ReactDOM.createPortal(children, mountNodeRef.current)}\n {resizable && (\n \n \n \n )}\n
\n );\n }\n);\n\nIframeWrapper.displayName = \"IframeWrapper\";\n\nconst Resizer = ({\n className,\n children,\n ...props\n}: React.HTMLAttributes) => {\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n e.stopPropagation();\n e.preventDefault();\n }, []);\n\n const handleTouchStart = useCallback((e: React.TouchEvent) => {\n e.stopPropagation();\n }, []);\n\n return (\n \n {children}\n
\n );\n};\n",
+ "content": "import React, {\n useCallback,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport ReactDOM from \"react-dom\";\nimport { GripVertical } from \"lucide-react\";\nimport { DragConfig, useDrag } from \"@use-gesture/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst initialSize = { width: 0 };\n\ninterface IframeWrapperProps extends React.HTMLAttributes {\n children: React.ReactNode;\n frameId?: string;\n resizable: boolean;\n}\n\nexport const IframeWrapper: React.FC = React.memo(\n ({ children, frameId, resizable, style, ...props }) => {\n const iframeRef = useRef(null);\n const mountNodeRef = useRef(null); // Ref to store the mount node\n const [iframeSize, setIframeSize] = useState<{ width: number } | null>(\n null\n );\n const [isMounted, setIsMounted] = useState(false); // State to track mount node readiness\n\n\n\n // Ref to store the initial size at the start of dragging\n const initialSizeRef = useRef<{ width: number }>(initialSize);\n\n const updateIframeHeight = () => {\n if (!iframeRef.current || !mountNodeRef.current) return;\n const newHeight = mountNodeRef.current.scrollHeight;\n iframeRef.current.style.height = `${newHeight}px`;\n };\n\n const dragConfig = useMemo(() => {\n return {\n axis: \"x\",\n from: () => [0, 0],\n filterTaps: true,\n } as DragConfig\n }, []);\n\n // Handle resizing using useDrag\n const bindResizer = useDrag(\n ({ down, movement: [mx], first }) => {\n if (first) {\n // Capture the initial size when drag starts\n initialSizeRef.current = {\n width: iframeRef.current?.offsetWidth || 0,\n };\n }\n\n if (down) {\n // Calculate new size based on initial size and movement\n setIframeSize({\n width: initialSizeRef.current.width + mx,\n });\n }\n },\n dragConfig as any\n );\n\n useLayoutEffect(() => {\n if (resizable && iframeRef.current) {\n setIframeSize({\n width: iframeRef.current.parentElement?.offsetWidth || 600, // Fallback to 600px or any default width\n });\n }\n }, [resizable]);\n\n useLayoutEffect(() => {\n const iframe = iframeRef.current;\n if (!iframe) return;\n\n const handleLoad = () => {\n const iframeDoc =\n iframe.contentDocument || iframe.contentWindow?.document;\n if (!iframeDoc) return;\n\n if (!mountNodeRef.current) {\n // Create a div inside the iframe for mounting React components only once\n const mountNode = iframeDoc.createElement(\"div\");\n mountNode.id = \"iframe-mount-node\";\n iframeDoc.body.appendChild(mountNode);\n mountNodeRef.current = mountNode;\n\n iframeDoc.body.style.backgroundColor = \"transparent\";\n\n // Copy font variables and classes from parent to iframe\n const parentBody = document.body;\n const parentHtml = document.documentElement;\n const parentComputedStyle = window.getComputedStyle(parentHtml);\n\n // Copy all CSS custom properties (variables) from parent html element\n for (let i = 0; i < parentComputedStyle.length; i++) {\n const property = parentComputedStyle[i];\n if (property.startsWith(\"--\")) {\n const value = parentComputedStyle.getPropertyValue(property);\n iframeDoc.documentElement.style.setProperty(property, value);\n iframeDoc.body.style.setProperty(property, value);\n }\n }\n\n // Copy font-related classes from parent body\n parentBody.classList.forEach((className) => {\n if (\n className.includes(\"font-\") ||\n className === \"antialiased\" ||\n className.includes(\"__variable\")\n ) {\n iframeDoc.body.classList.add(className);\n }\n });\n\n // Function to inject stylesheets into the iframe\n const injectStylesheets = () => {\n // Select all linked stylesheets in the parent document\n const links = Array.from(\n document.querySelectorAll('link[rel=\"stylesheet\"]')\n );\n // Select all style tags in the parent document\n const styles = Array.from(document.querySelectorAll(\"style\"));\n\n // Inject linked stylesheets\n links.forEach((link) => {\n const clonedLink = iframeDoc.createElement(\"link\");\n clonedLink.rel = \"stylesheet\";\n clonedLink.href = (link as HTMLLinkElement).href;\n iframeDoc.head.appendChild(clonedLink);\n });\n\n // Inject inline styles\n styles.forEach((style) => {\n const clonedStyle = iframeDoc.createElement(\"style\");\n clonedStyle.textContent = style.textContent || \"\";\n iframeDoc.head.appendChild(clonedStyle);\n });\n };\n\n // Inject styles on initial load\n injectStylesheets();\n\n const resizeObserver = new ResizeObserver(() => {\n updateIframeHeight();\n });\n\n // Observe the mount node for size changes\n resizeObserver.observe(mountNodeRef.current);\n\n const mutationObserver = new MutationObserver(() => {\n updateIframeHeight();\n });\n\n // Observe the mount node for changes\n mutationObserver.observe(mountNodeRef.current, {\n childList: true,\n subtree: true,\n });\n\n // Initial height update\n setTimeout(updateIframeHeight, 0);\n\n // Set the mount node as ready\n setIsMounted(true);\n\n // Cleanup observer on unmount\n return () => {\n resizeObserver.disconnect();\n mutationObserver.disconnect();\n };\n }\n };\n\n // Attach the load event listener\n iframe.addEventListener(\"load\", handleLoad);\n\n // If the iframe is already loaded, trigger the handler\n if (iframe.contentDocument?.readyState === \"complete\") {\n handleLoad();\n }\n\n // Cleanup event listener on unmount\n return () => {\n iframe.removeEventListener(\"load\", handleLoad);\n };\n }, [children]);\n\n\n const resizerStyle = useMemo(() => {\n return {\n left: iframeSize?.width ? `${iframeSize.width}px` : undefined\n }\n }, [iframeSize]);\n\n const iframeStyle = useMemo(() => {\n return {\n width: resizable\n ? iframeSize\n ? `${iframeSize.width}px`\n : \"100%\"\n : undefined,\n opacity: isMounted ? 1 : 0,\n transition: \"opacity 0.5s ease-in\",\n ...style,\n }\n }, [iframeSize, resizable, isMounted, style]);\n\n const bindResizerValues = useMemo(() => {\n return typeof bindResizer === 'function' ? bindResizer() : {};\n }, [bindResizer]);\n\n return (\n \n {resizable && (\n \n \n \n )}\n \n {isMounted &&\n mountNodeRef.current &&\n ReactDOM.createPortal(children, mountNodeRef.current)}\n {resizable && (\n \n \n \n )}\n
\n );\n }\n);\n\nIframeWrapper.displayName = \"IframeWrapper\";\n\nconst Resizer = ({\n className,\n children,\n ...props\n}: React.HTMLAttributes) => {\n const handleMouseDown = useCallback((e: React.MouseEvent) => {\n e.stopPropagation();\n e.preventDefault();\n }, []);\n\n const handleTouchStart = useCallback((e: React.TouchEvent) => {\n e.stopPropagation();\n }, []);\n\n return (\n \n {children}\n
\n );\n};\n",
"type": "registry:ui",
"target": "components/ui/ui-builder/internal/iframe-wrapper.tsx"
},
@@ -334,7 +334,7 @@
},
{
"path": "components/ui/ui-builder/internal/classname-control/breakpoint-classname-control.tsx",
- "content": "import { useState, useEffect, useCallback } from \"react\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport ClassNameMultiselect from \"@/components/ui/ui-builder/internal/classname-multiselect\";\nimport { ClassNameItemControl } from \"@/components/ui/ui-builder/internal/classname-control/classname-item-control\";\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from \"@/components/ui/accordion\";\nimport { Badge } from \"@/components/ui/badge\";\n\ninterface BreakpointClassNameControlProps {\n onChange?: (classes: string) => void;\n value?: string;\n}\nexport const BreakpointClassNameControl = ({\n onChange,\n value,\n}: BreakpointClassNameControlProps) => {\n // Helper to parse classString into base, md, rest\n const parseClassString = (str: string) => {\n const tokens = str.trim().split(/\\s+/);\n const base: string[] = [];\n const md: string[] = [];\n const rest: string[] = [];\n for (const token of tokens) {\n if (token.startsWith(\"md:\")) {\n md.push(token.slice(3));\n } else if (token.includes(\":\")) {\n rest.push(token);\n } else if (token) {\n base.push(token);\n }\n }\n return {\n base: base.join(\" \"),\n md: md.join(\" \"),\n rest: rest.join(\" \"),\n };\n };\n\n // State for the full class string\n const [classString, setClassString] = useState(value || \"\");\n // State for the tab\n const [tab, setTab] = useState<\"base\" | \"md\">(\"base\");\n\n // Sync classString with value prop (uncontrolled to controlled fix)\n useEffect(() => {\n if (typeof value === \"string\" && value !== classString) {\n setClassString(value);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n // Parse the class string for the tabs\n const { base, md, rest } = parseClassString(classString);\n\n // Handlers for each tab\n const handleBaseChange = useCallback(\n (newBase: string) => {\n const newClassString = [\n newBase,\n md &&\n md\n .split(\" \")\n .map((cls) => `md:${cls}`)\n .join(\" \"),\n rest,\n ]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .trim();\n setClassString(newClassString);\n },\n [md, rest]\n );\n\n const handleMdChange = useCallback(\n (newMd: string) => {\n const mdClasses = newMd\n .split(\" \")\n .filter(Boolean)\n .map((cls) => `md:${cls}`)\n .join(\" \");\n const newClassString = [base, mdClasses, rest]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .trim();\n setClassString(newClassString);\n },\n [base, rest]\n );\n\n // When classString changes, call parent onChange\n useEffect(() => {\n if (onChange) onChange(classString);\n }, [classString, onChange]);\n\n // When multiselect changes, update classString (and tabs will re-parse)\n const handleMultiselectChange = useCallback((newClassString: string) => {\n setClassString(newClassString);\n }, []);\n\n const handleTabChange = useCallback(\n (val: string) => setTab(val as \"base\" | \"md\"),\n []\n );\n\n return (\n \n
\n \n \n \n \n \n Base\n {base && (\n \n {base.split(\" \").filter(Boolean).length}\n \n )}\n
\n \n Base styles for all screen sizes\n \n \n \n \n \n \n \n Tablet & Desktop\n \n {md && (\n \n {md.split(\" \").filter(Boolean).length}\n \n )}\n
\n \n \n Overrides for screens larger than 768px (md:*)\n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n Edit Classes\n \n \n \n \n \n \n
\n );\n};\n",
+ "content": "import { useState, useEffect, useCallback } from \"react\";\nimport {\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport ClassNameMultiselect from \"@/components/ui/ui-builder/internal/classname-multiselect\";\nimport { ClassNameItemControl } from \"@/components/ui/ui-builder/internal/classname-control/classname-item-control\";\nimport {\n Accordion,\n AccordionItem,\n AccordionTrigger,\n AccordionContent,\n} from \"@/components/ui/accordion\";\nimport { Badge } from \"@/components/ui/badge\";\n\ninterface BreakpointClassNameControlProps {\n onChange?: (classes: string) => void;\n value?: string;\n}\nexport const BreakpointClassNameControl = ({\n onChange,\n value,\n}: BreakpointClassNameControlProps) => {\n // Helper to parse classString into base, md, rest\n const parseClassString = (str: string) => {\n const tokens = str.trim().split(/\\s+/);\n const base: string[] = [];\n const md: string[] = [];\n const rest: string[] = [];\n for (const token of tokens) {\n if (token.startsWith(\"md:\")) {\n md.push(token.slice(3));\n } else if (token.includes(\":\")) {\n rest.push(token);\n } else if (token) {\n base.push(token);\n }\n }\n return {\n base: base.join(\" \"),\n md: md.join(\" \"),\n rest: rest.join(\" \"),\n };\n };\n\n // State for the full class string\n const [classString, setClassString] = useState(value || \"\");\n // State for the tab\n const [tab, setTab] = useState<\"base\" | \"md\">(\"base\");\n\n // Sync classString with value prop (uncontrolled to controlled fix)\n useEffect(() => {\n if (typeof value === \"string\" && value !== classString) {\n setClassString(value);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [value]);\n\n // Parse the class string for the tabs\n const { base, md, rest } = parseClassString(classString);\n\n // Handlers for each tab\n const handleBaseChange = useCallback(\n (newBase: string) => {\n const newClassString = [\n newBase,\n md &&\n md\n .split(\" \")\n .map((cls) => `md:${cls}`)\n .join(\" \"),\n rest,\n ]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .trim();\n setClassString(newClassString);\n },\n [md, rest]\n );\n\n const handleMdChange = useCallback(\n (newMd: string) => {\n const mdClasses = newMd\n .split(\" \")\n .filter(Boolean)\n .map((cls) => `md:${cls}`)\n .join(\" \");\n const newClassString = [base, mdClasses, rest]\n .filter(Boolean)\n .join(\" \")\n .replace(/\\s+/g, \" \")\n .trim();\n setClassString(newClassString);\n },\n [base, rest]\n );\n\n // When classString changes, call parent onChange\n useEffect(() => {\n if (onChange) onChange(classString);\n }, [classString, onChange]);\n\n // When multiselect changes, update classString (and tabs will re-parse)\n const handleMultiselectChange = useCallback((newClassString: string) => {\n setClassString(newClassString);\n }, []);\n\n const handleTabChange = useCallback(\n (val: string) => setTab(val as \"base\" | \"md\"),\n []\n );\n\n return (\n \n
\n \n \n \n \n \n Base\n {base && (\n \n {base.split(\" \").filter(Boolean).length}\n \n )}\n
\n \n Base styles for all screen sizes\n \n \n \n \n \n \n \n Tablet & Desktop\n \n {md && (\n \n {md.split(\" \").filter(Boolean).length}\n \n )}\n
\n \n \n Overrides for screens larger than 768px (md:*)\n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n Edit Classes\n \n \n \n \n \n \n
\n );\n};\n",
"type": "registry:ui",
"target": "components/ui/ui-builder/internal/classname-control/breakpoint-classname-control.tsx"
},