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}`;