diff --git a/app/components/monaco-editor.tsx b/app/components/monaco-editor.tsx index d5537ce..1c4c02d 100644 --- a/app/components/monaco-editor.tsx +++ b/app/components/monaco-editor.tsx @@ -38,7 +38,7 @@ interface MonacoEditorProps { value: string onChange: (value: string) => void theme?: "light" | "dark" - /** Receives the editor instance on mount, and `null` on unmount. */ + fontSize?: number onEditorReady?: (editor: monaco.editor.IStandaloneCodeEditor | null) => void } @@ -47,6 +47,7 @@ export default function MonacoEditor({ value, onChange, theme, + fontSize = 14, onEditorReady, }: MonacoEditorProps) { const editorRef = useRef(null) @@ -68,6 +69,7 @@ export default function MonacoEditor({ }, }) + monaco.editor.defineTheme("custom-light", { base: "vs", inherit: true, @@ -85,7 +87,7 @@ export default function MonacoEditor({ theme: theme === "dark" ? "custom-dark" : "custom-light", automaticLayout: true, minimap: { enabled: false }, - fontSize: 14, + fontSize: fontSize, lineNumbers: "on", roundedSelection: false, scrollBeyondLastLine: false, @@ -193,6 +195,12 @@ export default function MonacoEditor({ } }, [theme]) + useEffect(() => { + if (monacoRef.current) { + monacoRef.current.updateOptions({ fontSize }) + } + }, [fontSize]) + // Helper: compute and render status text const updateStatusBar = (text: string) => { try { diff --git a/app/page.tsx b/app/page.tsx index d3c5c18..54d21af 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1103,6 +1103,180 @@ export default function CodeEditor() { const [theme, setTheme] = useState<"light" | "dark">("light") const previewRef = useRef(null) + const [splitRatio, setSplitRatio] = useState(50) + const isDragging = useRef(false) + const [isResizing, setIsResizing] = useState(false) + const [isMobile, setIsMobile] = useState(false) + + const [fontSize, setFontSize] = useState(14) + + // use effect for handling full screen mode + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth < 768) + checkMobile() + window.addEventListener('resize', checkMobile) + return () => window.removeEventListener('resize', checkMobile) + }, []) + + useEffect(() => { + const handleFullscreenChange = () => { + setIsFullscreen(!!document.fullscreenElement); + }; + + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => { + document.removeEventListener("fullscreenchange", handleFullscreenChange); + }; + }, []); + + const handleFullscreenToggle = async () => { + try { + if (!document.fullscreenElement) { + await document.documentElement.requestFullscreen(); + } else { + if (document.exitFullscreen) { + await document.exitFullscreen(); + } + } + } catch (err) { + console.error("Error attempting to toggle fullscreen:", err); + setIsFullscreen((prev) => !prev); + } + }; + +const containerRef = useRef(null) +const previewRef = useRef(null) +const handleDragStart = () => { + isDragging.current = true; + setIsResizing(true); + document.body.style.userSelect = "none"; +}; + +const handleDragMove = useCallback((clientX: number, clientY: number) => { + if (!isDragging.current || !containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + const isMobile = window.innerWidth < 768; // Tailwind 'md' breakpoint + + let newRatio; + if (isMobile) { + newRatio = ((clientY - rect.top) / rect.height) * 100; + } else { + newRatio = ((clientX - rect.left) / rect.width) * 100; + } + + const clampedRatio = Math.max(20, Math.min(80, newRatio)); + setSplitRatio(clampedRatio); +}, []); + +const handleMouseMove = useCallback((e: globalThis.MouseEvent) => { + handleDragMove(e.clientX, e.clientY); +}, [handleDragMove]); + +const handleTouchMove = useCallback((e: globalThis.TouchEvent) => { + if (isDragging.current) { + handleDragMove(e.touches[0].clientX, e.touches[0].clientY); + } +}, [handleDragMove]); + +const handleDragEnd = useCallback(() => { + isDragging.current = false; + setIsResizing(false); + document.body.style.userSelect = "auto"; + document.body.style.cursor = "default"; +}, []); + + // Tracks which template is currently active + const [currentTemplateId, setCurrentTemplateId] = useState(null) + + // Per-template memory: stores the user's last-edited code for each template + const [templateSnapshots, setTemplateSnapshots] = useState>(() => { + if (typeof window === 'undefined') return {} + try { + const saved = localStorage.getItem('webify_template_snapshots') + if (saved) return JSON.parse(saved) as Record + } catch { + // corrupted storage — fall through + } + return {} + }) + + + useEffect(() => { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleDragEnd); + window.addEventListener("touchmove", handleTouchMove, { passive: false }); + window.addEventListener("touchend", handleDragEnd); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleDragEnd); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("touchend", handleDragEnd); + }; + }, [handleMouseMove, handleTouchMove, handleDragEnd]); + + const activeEditorRef = useRef<{ + focus: () => void + trigger: (source: string, handlerId: string, payload?: unknown) => void + } | null>(null) + + const codeRef = useRef(code) + const htmlValidation = useMemo(() => validateHtmlSyntax(code.html), [code.html]) + // Keep codeRef in sync so beforeunload always has the latest values + useEffect(() => { + codeRef.current = code + }, [code]) + + + // Auto-save code to localStorage, debounced 500ms + useEffect(() => { + const timer = setTimeout(() => { + try { + localStorage.setItem('webify_code', JSON.stringify(code)) + } catch (err) { + // QuotaExceededError — localStorage full, fail silently + console.warn('Webify: auto-save failed', err) + } + }, 500) + return () => clearTimeout(timer) + }, [code]) + +useEffect(() => { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleDragEnd); + window.addEventListener("touchmove", handleTouchMove, { passive: false }); + window.addEventListener("touchend", handleDragEnd); + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleDragEnd); + window.removeEventListener("touchmove", handleTouchMove); + window.removeEventListener("touchend", handleDragEnd); + }; +}, [handleMouseMove, handleTouchMove, handleDragEnd]); + + // Column resizer: start dragging handler for desktop resizer + const handleMouseDown = () => { + handleDragStart() + document.body.style.cursor = "col-resize" + } + + + // Auto-save per-template snapshots to localStorage, debounced 500ms +useEffect(() => { + const timer = setTimeout(() => { + try { + localStorage.setItem('webify_template_snapshots', JSON.stringify(templateSnapshots)) + } catch (err) { + console.warn('Webify: template snapshot save failed', err) + } + }, 500) + return () => clearTimeout(timer) +}, [templateSnapshots]) + + // empty deps — registers once, codeRef keeps values fresh + // Initialize theme from storage/preferences on mount useEffect(() => { const savedTheme = localStorage.getItem("theme") as "light" | "dark" | null const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches @@ -1187,65 +1361,288 @@ export default function CodeEditor() { localStorage.setItem("theme", next) } + useEffect(() => { + const saved = localStorage.getItem("webify-font-size") + if (saved) setFontSize(Number(saved)) + }, []) + return ( - -
-
- - - Webify - - -
- - - + <> + +
+ {/* Header */} +
+
+
+ + +

Webify

+ + + + + +
+ +
+ {/* Layout Controls */} +
+ + + +
+ + + + {/* Action Buttons */} + + + + + + + + + {/* Font Size Control */} +
+ + {fontSize} + +
+ + +
-
- - setActiveTab(value as keyof CodeContent)} className="flex-1 flex flex-col overflow-hidden"> -
- - HTML - CSS - JS - -
-
- - handleCodeChange("html", v)} theme={theme} /> - - - handleCodeChange("css", v)} theme={theme} /> - - - handleCodeChange("javascript", v)} theme={theme} /> - + + {/* Main Container - Code Editor + Preview */} +
+ {/* CODE EDITOR */} + {(layout === "code" || layout === "split") && ( +
+ setActiveTab(value as keyof CodeContent)} + className="flex-1 flex flex-col" + > + {/* Tabs Header */} +
+ + HTML + CSS + JS + + +
+ + {/* Tabs Content */} +
+ + handleCodeChange("html", value)} + theme={theme} + fontSize={fontSize} + onEditorReady={(ed) => (activeEditorRef.current = ed)} + /> + + + + handleCodeChange("css", value)} + theme={theme} + fontSize={fontSize} + onEditorReady={(ed) => (activeEditorRef.current = ed)} + /> + + + + handleCodeChange("javascript", value)} + theme={theme} + fontSize={fontSize} + onEditorReady={(ed) => (activeEditorRef.current = ed)} + /> + +
+
+
+)} + + {/* RESIZER - DESKTOP ONLY */} + {layout === "split" && !isMobile && ( +
e.preventDefault()} + className="w-1 cursor-col-resize bg-gray-300 dark:bg-gray-600 hover:bg-blue-500 active:bg-blue-600 transition shrink-0 z-10" + /> + )} + + {/* PREVIEW PANEL */} + {(layout === "preview" || layout === "split") && ( +
+
+
+ + Live Preview + + {autoRun ? "Auto-refresh" : "Manual"} + + + {!autoRun && ( + + )} +
- - - - -
-
- - Live Preview + +
+