diff --git a/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx b/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx index 12de498..4a8900d 100644 --- a/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx +++ b/src/components/features/ThemeSwitcher/ui/WipeAnimationOverlay.tsx @@ -27,7 +27,7 @@ export function WipeAnimationOverlay({ > {/* Static Background Layer (to prevent target theme flash before wipe starts) */}
(null); const [isCapturing, setIsCapturing] = useState(false); const [animationTargetTheme, setAnimationTargetTheme] = useState(null); const [originalTheme, setOriginalTheme] = useState(null); + const isPuppeteerPage = useMemo(() => { + // We want to use Puppeteer on: + // 1. Homepage: / or /[variant] (e.g. /tim, /tiger) + // 2. Projects: /projects or /[variant]/projects + + const parts = pathname.split('/').filter(Boolean); + + // Homepage: / or /tim or /tiger + if (parts.length === 0 || parts.length === 1) return true; + + // Projects pages: /projects or /tim/projects or /tiger/projects + if (parts.length === 1 && parts[0] === 'projects') return true; + if (parts.length === 2 && parts[1] === 'projects') return true; + + return false; + }, [pathname]); + const setScrollLock = (isLocked: boolean) => { - const hasScrollbar = window.innerWidth > document.documentElement.clientWidth; document.documentElement.style.overflow = isLocked ? 'hidden' : ''; - // Only reserve gutter space if a scrollbar was actually present to prevent layout shift on mobile - document.documentElement.style.scrollbarGutter = (isLocked && hasScrollbar) ? 'stable' : ''; }; const handleAnimationComplete = useCallback(() => { @@ -72,7 +88,7 @@ export function useThemeWipe({ }); const toggleTheme = useCallback(async () => { - const currentTheme = resolvedTheme as Theme; + const currentTheme = (resolvedTheme as Theme) || "light"; const newTheme: Theme = currentTheme === "dark" ? "light" : "dark"; if (snapshots || isCapturing || wipeDirection) { @@ -106,7 +122,7 @@ export function useThemeWipe({ return true; }, style: { - width: `${document.documentElement.clientWidth}px`, + width: `${window.innerWidth}px`, height: `${document.documentElement.scrollHeight}px`, transform: `translateY(-${scrollY}px)`, transformOrigin: 'top left', @@ -115,9 +131,9 @@ export function useThemeWipe({ return await domToPng(document.documentElement, options); }; - const fetchSnapshotsBatch = async (newTheme: Theme) => { + const fetchSnapshotsBatch = async (currentTheme: Theme, newTheme: Theme) => { // 1. Snapshot A (current) - const htmlA = await getFullPageHTML(); + const htmlA = await getFullPageHTML(currentTheme); // 2. Switch theme (to handle layouts that require re-render) setTheme(newTheme); @@ -126,7 +142,7 @@ export function useThemeWipe({ await new Promise(r => setTimeout(r, 250)); // 3. Snapshot B (newly rendered theme) - const htmlB = await getFullPageHTML(); + const htmlB = await getFullPageHTML(newTheme); // 4. Restore original theme state before sending to API // This ensures the live page matches Snapshot A when the wipe animation starts. @@ -209,21 +225,25 @@ export function useThemeWipe({ const mask = await captureMask(); setSnapshots({ a: mask, b: mask, method: "Capturing..." }); - // PHASE 1: Try Puppeteer (20s timeout as per instructions) - console.log("Attempting Puppeteer snapshot..."); - const [snapshotA, snapshotB] = await withTimeout( - fetchSnapshotsBatch(newTheme), - 20000, - "Puppeteer timeout" - ) as [string, string]; - - setSnapshots({ a: snapshotA, b: snapshotB, method: "Puppeteer" }); - await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); - setTheme(newTheme); - setWipeDirection(direction); - - } catch (e: any) { - console.warn("Puppeteer failed or timed out, falling back to modern-screenshot:", e.message); + if (isPuppeteerPage) { + // PHASE 1: Try Puppeteer (20s timeout as per instructions) + console.log("Attempting Puppeteer snapshot..."); + try { + const [snapshotA, snapshotB] = await withTimeout( + fetchSnapshotsBatch(currentTheme, newTheme), + 20000, + "Puppeteer timeout" + ) as [string, string]; + + setSnapshots({ a: snapshotA, b: snapshotB, method: "Puppeteer" }); + await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + setTheme(newTheme); + setWipeDirection(direction); + return; // Exit on success + } catch (e: any) { + console.warn("Puppeteer failed or timed out, falling back to modern-screenshot:", e.message); + } + } try { // PHASE 2: Try modern-screenshot (15s timeout as per instructions) diff --git a/src/utils/dom-serializer.ts b/src/utils/dom-serializer.ts index 5b658a0..873e609 100644 --- a/src/utils/dom-serializer.ts +++ b/src/utils/dom-serializer.ts @@ -181,15 +181,12 @@ export async function getFullPageHTML(themeOverride?: "light" | "dark"): Promise if (themeOverride) { // next-themes typically uses class="dark" or class="light" on html - if (themeOverride === "dark") { - doc.classList.add("dark"); - doc.classList.remove("light"); - } else { - doc.classList.add("light"); - doc.classList.remove("dark"); - } + doc.classList.remove("light", "dark"); + doc.classList.add(themeOverride); // Also handle data-theme if present doc.setAttribute("data-theme", themeOverride); + // Ensure the background color matches the theme + doc.style.colorScheme = themeOverride; } const body = doc.querySelector('body'); @@ -298,9 +295,9 @@ export async function getFullPageHTML(themeOverride?: "light" | "dark"): Promise const scripts = doc.querySelectorAll('script, noscript, template, iframe'); scripts.forEach(s => s.remove()); - // Hide the switcher and overlay + // Hide the switcher and overlay - REMOVE them instead of just display: none const itemsToHide = doc.querySelectorAll('[data-html2canvas-ignore]'); - itemsToHide.forEach(el => (el as HTMLElement).style.display = 'none'); + itemsToHide.forEach(el => el.remove()); const htmlAttrs = Array.from(doc.attributes).map(a => `${a.name}="${a.value}"`).join(' '); return `${doc.innerHTML}`;