mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-03 08:19:04 +02:00
feat: make page transitions smoother and faster
This commit is contained in:
@@ -195,6 +195,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
view-transition-name: settings-content;
|
||||
}
|
||||
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-group(settings-content) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
::view-transition-old(settings-content) {
|
||||
animation: 120ms ease-out both vt-fade-out;
|
||||
}
|
||||
|
||||
::view-transition-new(settings-content) {
|
||||
animation: 180ms ease-out both vt-fade-in;
|
||||
}
|
||||
|
||||
@keyframes vt-fade-out {
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes vt-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bg-zoom {
|
||||
0% {
|
||||
transform: scale(1.3);
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
delay = 50,
|
||||
stagger = 150,
|
||||
children
|
||||
}: {
|
||||
delay?: number;
|
||||
stagger?: number;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let containerNode: HTMLElement | null = $state(null);
|
||||
|
||||
$effect(() => {
|
||||
void page.route;
|
||||
applyAnimationDelays();
|
||||
});
|
||||
|
||||
function applyAnimationDelays() {
|
||||
if (containerNode) {
|
||||
const childNodes = Array.from(containerNode.children);
|
||||
childNodes.forEach((child, index) => {
|
||||
// Skip comment nodes and text nodes
|
||||
if (child.nodeType === 1) {
|
||||
const itemDelay = delay + index * stagger;
|
||||
(child as HTMLElement).style.setProperty('animation-delay', `${itemDelay}ms`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<style>
|
||||
/* Base styles */
|
||||
.fade-wrapper {
|
||||
display: contents;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Apply these styles to all children */
|
||||
.fade-wrapper > *:not(.no-fade) {
|
||||
animation-fill-mode: both;
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
animation-delay: calc(var(--animation-delay, 0ms) + 0.1s);
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
</style>
|
||||
</svelte:head>
|
||||
|
||||
{#if $appConfigStore.disableAnimations}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<div class="fade-wrapper" bind:this={containerNode}>
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { navigating } from '$app/state';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
// Skip the fade when the bar never became visible, otherwise it would start growing during the fade and flash on fast navigations
|
||||
function fadeIfVisible(node: HTMLElement) {
|
||||
const visible = new DOMMatrixReadOnly(getComputedStyle(node).transform).a > 0;
|
||||
return fade(node, { duration: visible ? 200 : 0 });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if navigating.to}
|
||||
<div class="nav-progress" aria-hidden="true" out:fadeIfVisible></div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.nav-progress {
|
||||
position: fixed;
|
||||
inset: 0 0 auto 0;
|
||||
z-index: 100;
|
||||
height: 2px;
|
||||
background: var(--primary);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
pointer-events: none;
|
||||
animation: progress 3s cubic-bezier(0.1, 0.6, 0.3, 1) 100ms forwards;
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
10% {
|
||||
transform: scaleX(0.25);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: scaleX(0.85);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@
|
||||
import ConfirmDialog from '$lib/components/confirm-dialog/confirm-dialog.svelte';
|
||||
import Error from '$lib/components/error.svelte';
|
||||
import Header from '$lib/components/header/header.svelte';
|
||||
import NavigationProgress from '$lib/components/navigation-progress.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
import UnsavedChangesBar from '$lib/components/unsaved-changes-bar.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
@@ -44,6 +45,7 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<NavigationProgress />
|
||||
<ConfirmDialog />
|
||||
<UnsavedChangesBar />
|
||||
<ModeWatcher disableTransitions={false} />
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import EmailVerificationStateBox from '$lib/components/email-verification-state-box.svelte';
|
||||
import FadeWrapper from '$lib/components/fade-wrapper.svelte';
|
||||
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||
import Sidebar from '$lib/components/sidebar.svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { LucideTriangleAlert } from '@lucide/svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -48,6 +49,26 @@
|
||||
if (user?.isAdmin || $userStore?.isAdmin) {
|
||||
items.push({ label: m.administration(), children: adminChildren });
|
||||
}
|
||||
|
||||
onNavigate((navigation) => {
|
||||
if (
|
||||
!document.startViewTransition ||
|
||||
$appConfigStore.disableAnimations ||
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches ||
|
||||
!navigation.to?.url.pathname.startsWith('/settings/') ||
|
||||
navigation.from?.url.pathname === navigation.to.url.pathname
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// onNavigate runs after the load functions resolved, so the transition never delays the new page
|
||||
return new Promise((resolve) => {
|
||||
document.startViewTransition(async () => {
|
||||
resolve();
|
||||
await navigation.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<section>
|
||||
@@ -69,19 +90,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-4 overflow-hidden pb-2 px-2">
|
||||
<FadeWrapper>
|
||||
{#if sqliteStorageWarning && ($userStore?.isAdmin || user?.isAdmin)}
|
||||
<Alert.Root variant="destructive">
|
||||
<LucideTriangleAlert />
|
||||
<Alert.Description>
|
||||
<FormattedMessage message={m.sqlite_storage_warning} />
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<EmailVerificationStateBox />
|
||||
{@render children()}
|
||||
</FadeWrapper>
|
||||
<div class="settings-content flex w-full flex-col gap-4 overflow-hidden pb-2 px-2">
|
||||
{#if sqliteStorageWarning && ($userStore?.isAdmin || user?.isAdmin)}
|
||||
<Alert.Root variant="destructive">
|
||||
<LucideTriangleAlert />
|
||||
<Alert.Description>
|
||||
<FormattedMessage message={m.sqlite_storage_warning} />
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<EmailVerificationStateBox />
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
<div class="animate-fade-in flex flex-col items-center" style="animation-delay: 400ms;">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
let { currentPage }: { currentPage: 'personal' | 'global' } = $props();
|
||||
</script>
|
||||
|
||||
<div class="no-fade flex justify-end">
|
||||
<div class="flex justify-end">
|
||||
<Tabs.Root value={currentPage}>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger onclick={() => goto('/settings/audit-log')} value="personal"
|
||||
|
||||
Reference in New Issue
Block a user