From 674ba7bbd41ab1cb95ad322f599bcfea23783b07 Mon Sep 17 00:00:00 2001 From: Nicholai Date: Tue, 20 Jan 2026 09:42:04 -0700 Subject: [PATCH] refactor(dev): redesign fullscreen preview modal - cleaner sidebar layout with proper typography - fix theme detection using data-theme attribute - fix iframe scaling and positioning - improve button visibility and hover effects - add prominent interaction hint pill --- src/components/dev/DevEngageModal.tsx | 642 ++++++++++++-------------- 1 file changed, 287 insertions(+), 355 deletions(-) diff --git a/src/components/dev/DevEngageModal.tsx b/src/components/dev/DevEngageModal.tsx index f2835ab..2842e9a 100644 --- a/src/components/dev/DevEngageModal.tsx +++ b/src/components/dev/DevEngageModal.tsx @@ -19,29 +19,22 @@ interface ViewportPreset { } const VIEWPORT_PRESETS: ViewportPreset[] = [ - { name: 'desktop', width: 1440, height: 900, label: 'DESKTOP' }, - { name: 'tablet', width: 834, height: 1112, label: 'TABLET' }, - { name: 'mobile', width: 390, height: 844, label: 'MOBILE' }, + { name: 'desktop', width: 1440, height: 900, label: 'Desktop' }, + { name: 'tablet', width: 834, height: 1112, label: 'Tablet' }, + { name: 'mobile', width: 390, height: 844, label: 'Mobile' }, ]; -const MIN_BOOT_MS = 850; +const MIN_BOOT_MS = 600; const IFRAME_TIMEOUT_MS = 4500; -const BOOT_LOG_LINES = [ - 'INIT: FRAMEBUFFER', - 'UPLINK: ESTABLISHING', - 'AUTH: INPUT_LOCKED', - 'SIGNAL: STABLE', -]; - const DevEngageModal: React.FC = () => { const [modalState, setModalState] = useState('closed'); const [activeProject, setActiveProject] = useState(null); const [viewport, setViewport] = useState(VIEWPORT_PRESETS[0]); const [scale, setScale] = useState(1); const [bootProgress, setBootProgress] = useState(0); - const [bootLogIndex, setBootLogIndex] = useState(0); const [disarmToast, setDisarmToast] = useState(false); + const [isDarkMode, setIsDarkMode] = useState(true); const iframeRef = useRef(null); const stageRef = useRef(null); @@ -65,22 +58,20 @@ const DevEngageModal: React.FC = () => { const calculateScale = useCallback(() => { if (!stageRef.current) return; const stage = stageRef.current.getBoundingClientRect(); - const stagePadding = 80; - const availableW = stage.width - stagePadding; - const availableH = stage.height - stagePadding; + const padding = 96; + const availableW = stage.width - padding; + const availableH = stage.height - padding; const scaleX = availableW / viewport.width; const scaleY = availableH / viewport.height; - setScale(Math.min(scaleX, scaleY, 1)); + const fitScale = Math.min(scaleX, scaleY); + setScale(Math.max(0.25, Math.min(fitScale, 1))); }, [viewport]); const openModal = useCallback((project: Project, triggerElement?: HTMLElement) => { - if (triggerElement) { - triggerRef.current = triggerElement; - } + if (triggerElement) triggerRef.current = triggerElement; setActiveProject(project); setModalState('booting'); setBootProgress(0); - setBootLogIndex(0); iframeLoadedRef.current = false; bootStartRef.current = performance.now(); lockBodyScroll(); @@ -91,16 +82,11 @@ const DevEngageModal: React.FC = () => { setActiveProject(null); setDisarmToast(false); unlockBodyScroll(); - if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; } - - if (iframeRef.current) { - iframeRef.current.src = 'about:blank'; - } - + if (iframeRef.current) iframeRef.current.src = 'about:blank'; if (triggerRef.current) { triggerRef.current.focus(); triggerRef.current = null; @@ -119,11 +105,8 @@ const DevEngageModal: React.FC = () => { }, [modalState, closeModal]); const toggleArm = useCallback(() => { - if (modalState === 'observe') { - setModalState('armed'); - } else if (modalState === 'armed') { - setModalState('observe'); - } + if (modalState === 'observe') setModalState('armed'); + else if (modalState === 'armed') setModalState('observe'); }, [modalState]); const handleIframeLoad = useCallback(() => { @@ -134,48 +117,26 @@ const DevEngageModal: React.FC = () => { if (!activeProject) return; setModalState('booting'); setBootProgress(0); - setBootLogIndex(0); iframeLoadedRef.current = false; bootStartRef.current = performance.now(); - if (iframeRef.current) { - iframeRef.current.src = activeProject.link; - } - }, [activeProject]); - - const handleCopyLink = useCallback(async () => { - if (!activeProject) return; - try { - await navigator.clipboard.writeText(activeProject.link); - } catch { - const input = document.createElement('input'); - input.value = activeProject.link; - document.body.appendChild(input); - input.select(); - document.execCommand('copy'); - document.body.removeChild(input); - } + if (iframeRef.current) iframeRef.current.src = activeProject.link; }, [activeProject]); useEffect(() => { const handleEngageEvent = (e: CustomEvent<{ project: Project; trigger?: HTMLElement }>) => { openModal(e.detail.project, e.detail.trigger); }; - window.addEventListener('dev:engage' as any, handleEngageEvent); - return () => { - window.removeEventListener('dev:engage' as any, handleEngageEvent); - }; + return () => window.removeEventListener('dev:engage' as any, handleEngageEvent); }, [openModal]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (modalState === 'closed') return; - if (e.key === 'Escape') { e.preventDefault(); closeModal(); } - if (modalState === 'observe' || modalState === 'armed') { if (e.key === '1') setViewport(VIEWPORT_PRESETS[0]); if (e.key === '2') setViewport(VIEWPORT_PRESETS[1]); @@ -183,56 +144,56 @@ const DevEngageModal: React.FC = () => { if (e.key === 'a' || e.key === 'A') toggleArm(); } }; - window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [modalState, closeModal, toggleArm]); useEffect(() => { if (modalState !== 'booting') return; - const progressInterval = setInterval(() => { - setBootProgress(p => Math.min(p + 2, 100)); + setBootProgress(p => Math.min(p + 3, 100)); }, 15); - - const logInterval = setInterval(() => { - setBootLogIndex(i => Math.min(i + 1, BOOT_LOG_LINES.length)); - }, 180); - const checkBootCompletion = () => { const elapsed = performance.now() - bootStartRef.current; const minBootMet = elapsed >= MIN_BOOT_MS; const loaded = iframeLoadedRef.current; - - if (minBootMet && loaded) { - setModalState('observe'); - } else if (minBootMet && elapsed >= IFRAME_TIMEOUT_MS) { - setModalState('blocked'); - } else { - timeoutRef.current = window.setTimeout(checkBootCompletion, 100); - } + if (minBootMet && loaded) setModalState('observe'); + else if (minBootMet && elapsed >= IFRAME_TIMEOUT_MS) setModalState('blocked'); + else timeoutRef.current = window.setTimeout(checkBootCompletion, 100); }; - timeoutRef.current = window.setTimeout(checkBootCompletion, 100); - return () => { clearInterval(progressInterval); - clearInterval(logInterval); - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } + if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, [modalState]); useEffect(() => { if (modalState === 'closed') return; calculateScale(); - + const timer = setTimeout(calculateScale, 50); const handleResize = () => calculateScale(); window.addEventListener('resize', handleResize); - return () => window.removeEventListener('resize', handleResize); + return () => { + clearTimeout(timer); + window.removeEventListener('resize', handleResize); + }; }, [modalState, viewport, calculateScale]); + useEffect(() => { + const checkDarkMode = () => { + const theme = document.documentElement.getAttribute('data-theme'); + setIsDarkMode(theme !== 'light'); + }; + checkDarkMode(); + const observer = new MutationObserver(checkDarkMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'] + }); + return () => observer.disconnect(); + }, []); + if (modalState === 'closed' || !activeProject) return null; const isInteractive = modalState === 'armed'; @@ -242,301 +203,272 @@ const DevEngageModal: React.FC = () => {
-
+ {/* Close button - positioned in preview area, not over sidebar */} + -
+ {/* Main layout */} +
-
+ {/* Preview area - takes most of the space, with left margin for site nav */} +
-
- -
-
-
-
- - SYS.DEV /// LIVE_FEED - -
- - PRJ.0{activeProject.order} / {activeProject.category} - -
+ {/* Iframe container */} +
+