mirror of
https://github.com/Xevion/xevion.dev.git
synced 2026-01-31 04:26:43 -06:00
feat: add View Transitions API with persistent backgrounds
- Add fade in/out page transitions using View Transitions API - Move background/dots to root layout for persistence across routes - Hide native scrollbar immediately to prevent FOUC - Set body background in theme script to prevent flash - Increase inline style threshold for better initial render
This commit is contained in:
@@ -204,3 +204,30 @@ body {
|
|||||||
.os-scrollbar-handle {
|
.os-scrollbar-handle {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* View Transitions API - page transition animations */
|
||||||
|
@keyframes page-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page-fade-out {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation: page-fade-out 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: page-fade-in 150ms ease-in 50ms;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<style>
|
||||||
|
/* Hide native scrollbar immediately to prevent layout shift */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const stored = localStorage.getItem("theme");
|
const stored = localStorage.getItem("theme");
|
||||||
@@ -13,6 +24,8 @@
|
|||||||
if (isDark) {
|
if (isDark) {
|
||||||
document.documentElement.classList.add("dark");
|
document.documentElement.classList.add("dark");
|
||||||
}
|
}
|
||||||
|
// Set body background immediately to prevent flash
|
||||||
|
document.body.style.backgroundColor = isDark ? "#000000" : "#ffffff";
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
|
|||||||
@@ -1,31 +1,34 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { cn } from "$lib/utils";
|
import { cn } from "$lib/utils";
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from "svelte";
|
||||||
import Dots from "./Dots.svelte";
|
|
||||||
import ThemeToggle from "./ThemeToggle.svelte";
|
import ThemeToggle from "./ThemeToggle.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
class: className = "",
|
class: className = "",
|
||||||
backgroundClass = "",
|
|
||||||
bgColor = "",
|
bgColor = "",
|
||||||
showThemeToggle = true,
|
showThemeToggle = true,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
class?: string;
|
class?: string;
|
||||||
backgroundClass?: string;
|
|
||||||
bgColor?: string;
|
bgColor?: string;
|
||||||
showThemeToggle?: boolean;
|
showThemeToggle?: boolean;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<!--
|
||||||
|
Background: Public pages get their background from root +layout.svelte for persistence.
|
||||||
|
Admin/internal pages can use bgColor prop to set their own background.
|
||||||
|
-->
|
||||||
|
{#if bgColor}
|
||||||
|
<div
|
||||||
class={cn(
|
class={cn(
|
||||||
"pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300",
|
"pointer-events-none fixed inset-0 -z-20 transition-colors duration-300",
|
||||||
bgColor,
|
bgColor,
|
||||||
)}
|
)}
|
||||||
></div>
|
></div>
|
||||||
<Dots class={[backgroundClass]} />
|
{/if}
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class={cn(
|
class={cn(
|
||||||
"relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300",
|
"relative min-h-screen text-zinc-900 dark:text-zinc-50 transition-colors duration-300",
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
import { OverlayScrollbars } from "overlayscrollbars";
|
import { OverlayScrollbars } from "overlayscrollbars";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { themeStore } from "$lib/stores/theme.svelte";
|
import { themeStore } from "$lib/stores/theme.svelte";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
import { onNavigate } from "$app/navigation";
|
||||||
|
import Dots from "$lib/components/Dots.svelte";
|
||||||
|
|
||||||
let { children, data } = $props();
|
let { children, data } = $props();
|
||||||
|
|
||||||
@@ -20,6 +23,31 @@
|
|||||||
|
|
||||||
const metadata = $derived(data?.metadata ?? defaultMetadata);
|
const metadata = $derived(data?.metadata ?? defaultMetadata);
|
||||||
|
|
||||||
|
// Check if current route is admin (admin has its own layout/background)
|
||||||
|
const isAdminRoute = $derived($page.url.pathname.startsWith("/admin"));
|
||||||
|
// Check if current route is internal (OG preview, etc.)
|
||||||
|
const isInternalRoute = $derived($page.url.pathname.startsWith("/internal"));
|
||||||
|
// Show global background for public pages only
|
||||||
|
const showGlobalBackground = $derived(!isAdminRoute && !isInternalRoute);
|
||||||
|
|
||||||
|
// Use View Transitions API for smooth page transitions (Chrome 111+, Safari 18+)
|
||||||
|
onNavigate((navigation) => {
|
||||||
|
// Skip transitions for same-page navigations or if API not supported
|
||||||
|
if (
|
||||||
|
!document.startViewTransition ||
|
||||||
|
navigation.from?.url.pathname === navigation.to?.url.pathname
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
document.startViewTransition(async () => {
|
||||||
|
resolve();
|
||||||
|
await navigation.complete;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize theme store
|
// Initialize theme store
|
||||||
themeStore.init();
|
themeStore.init();
|
||||||
@@ -63,4 +91,13 @@
|
|||||||
<meta name="twitter:image" content={metadata.ogImage} />
|
<meta name="twitter:image" content={metadata.ogImage} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<!-- Persistent background layer - only for public routes -->
|
||||||
|
{#if showGlobalBackground}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none fixed inset-0 -z-20 bg-white dark:bg-black transition-colors duration-300"
|
||||||
|
></div>
|
||||||
|
<Dots />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Page content - transitions handled by View Transitions API -->
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
|||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
inlineStyleThreshold: 1000,
|
inlineStyleThreshold: 2000,
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter({
|
adapter: adapter({
|
||||||
out: "build",
|
out: "build",
|
||||||
|
|||||||
Reference in New Issue
Block a user