Add first launch splash screen with animations and styles

- Introduced CSS animations for splash screen elements including aurora drift, logo float, loader progress, and card breathing effects.
- Implemented a new FirstLaunchSplash component in layout to display the splash screen on the initial app launch.
- Updated theme initialization script to manage splash screen visibility based on local storage state.
This commit is contained in:
R3D347HR4Y 2026-05-21 09:55:10 +02:00
parent 5bf388e062
commit 447567a411
4 changed files with 269 additions and 1 deletions

View File

@ -374,6 +374,196 @@ body {
transform-origin: center; transform-origin: center;
} }
/* First app open splashscreen */
@keyframes splash-aurora-drift {
0% {
transform: translate3d(-8%, -6%, 0) scale(1);
}
100% {
transform: translate3d(8%, 6%, 0) scale(1.12);
}
}
@keyframes splash-logo-float {
0%,
100% {
transform: translateY(0px);
}
50% {
transform: translateY(-5px);
}
}
@keyframes splash-loader-progress {
0% {
transform: translateX(-104%);
}
100% {
transform: translateX(104%);
}
}
@keyframes splash-grain-pan {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-12%, -10%, 0);
}
}
@keyframes splash-card-breathe {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.008);
}
}
.app-first-launch-splash {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
padding: clamp(1rem, 3vw, 2rem);
background:
radial-gradient(circle at 18% 20%, color-mix(in srgb, #1a73e8 32%, transparent) 0%, transparent 46%),
radial-gradient(circle at 80% 15%, color-mix(in srgb, #34a853 26%, transparent) 0%, transparent 40%),
radial-gradient(circle at 50% 100%, color-mix(in srgb, #ea4335 18%, transparent) 0%, transparent 55%),
var(--app-canvas);
transition:
opacity 0.45s ease,
visibility 0.45s ease;
}
html[data-splash-seen='1'] .app-first-launch-splash {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.app-first-launch-splash--hide {
opacity: 0;
visibility: hidden;
}
.app-first-launch-splash__aurora {
position: absolute;
inset: -30%;
pointer-events: none;
background: conic-gradient(from 145deg, #1a73e8, #34a853, #fbbc04, #ea4335, #1a73e8);
opacity: 0.12;
filter: blur(clamp(56px, 8vw, 120px));
animation: splash-aurora-drift 7.5s ease-in-out infinite alternate;
}
.app-first-launch-splash__grain {
position: absolute;
inset: -20%;
pointer-events: none;
opacity: 0.075;
background-image: radial-gradient(rgb(255 255 255 / 55%) 0.75px, transparent 0.75px);
background-size: 3px 3px;
animation: splash-grain-pan 8s linear infinite alternate;
}
.app-first-launch-splash__content {
position: relative;
width: min(86vw, 420px);
display: grid;
gap: clamp(0.75rem, 1.7vw, 1.1rem);
place-items: center;
border: 1px solid color-mix(in srgb, var(--mail-border) 55%, transparent);
border-radius: clamp(1rem, 2.8vw, 1.5rem);
background: color-mix(in srgb, var(--mail-surface) 87%, transparent);
box-shadow:
0 20px 45px rgb(0 0 0 / 14%),
inset 0 1px 0 rgb(255 255 255 / 45%);
padding: clamp(1.25rem, 4.6vw, 2.2rem);
backdrop-filter: blur(12px) saturate(135%);
animation: splash-card-breathe 2.6s ease-in-out infinite;
}
.app-first-launch-splash__content::before {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: linear-gradient(
125deg,
color-mix(in srgb, #1a73e8 58%, transparent),
color-mix(in srgb, #34a853 45%, transparent),
color-mix(in srgb, #ea4335 42%, transparent)
);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
opacity: 0.75;
}
.app-first-launch-splash__pill {
font-size: 0.66rem;
letter-spacing: 0.16em;
font-weight: 700;
line-height: 1;
padding: 0.4rem 0.62rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--mail-border) 74%, transparent);
background: color-mix(in srgb, var(--mail-surface) 84%, transparent);
color: color-mix(in srgb, var(--mail-text) 92%, white 8%);
}
.app-first-launch-splash__subtitle {
margin: -0.15rem 0 0;
text-align: center;
font-size: clamp(0.76rem, 1.6vw, 0.86rem);
line-height: 1.3;
color: color-mix(in srgb, var(--mail-text-muted) 92%, transparent);
}
.app-first-launch-splash__logo {
animation: splash-logo-float 2s ease-in-out infinite;
}
.app-first-launch-splash__loader {
width: min(58vw, 230px);
height: 4px;
border-radius: 999px;
background: color-mix(in srgb, var(--mail-border) 68%, transparent);
overflow: hidden;
}
.app-first-launch-splash__loader > span {
display: block;
width: 58%;
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #1a73e8, #34a853, #1a73e8);
animation: splash-loader-progress 1.05s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.app-first-launch-splash__aurora,
.app-first-launch-splash__grain,
.app-first-launch-splash__content,
.app-first-launch-splash__logo,
.app-first-launch-splash__loader > span {
animation: none !important;
}
}
/* ── Mail : fond décoratif plein écran (derrière toute lUI) ── */ /* ── Mail : fond décoratif plein écran (derrière toute lUI) ── */
html { html {
background-color: var(--mail-bg-fallback, var(--app-canvas)); background-color: var(--mail-bg-fallback, var(--app-canvas));

View File

@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from 'next/font/google'
import { Analytics } from '@vercel/analytics/next' import { Analytics } from '@vercel/analytics/next'
import './globals.css' import './globals.css'
import { ThemeInitScript } from '@/components/theme-init-script' import { ThemeInitScript } from '@/components/theme-init-script'
import { FirstLaunchSplash } from '@/components/first-launch-splash'
const _geist = Geist({ subsets: ["latin"] }); const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] }); const _geistMono = Geist_Mono({ subsets: ["latin"] });
@ -31,7 +32,7 @@ export default function RootLayout({
<html lang="fr" suppressHydrationWarning className="h-dvh max-h-dvh overflow-hidden"> <html lang="fr" suppressHydrationWarning className="h-dvh max-h-dvh overflow-hidden">
<body className="h-dvh max-h-dvh overflow-hidden bg-background font-sans antialiased touch-manipulation"> <body className="h-dvh max-h-dvh overflow-hidden bg-background font-sans antialiased touch-manipulation">
<ThemeInitScript /> <ThemeInitScript />
{children} <FirstLaunchSplash>{children}</FirstLaunchSplash>
{process.env.NODE_ENV === 'production' && <Analytics />} {process.env.NODE_ENV === 'production' && <Analytics />}
</body> </body>
</html> </html>

View File

@ -0,0 +1,74 @@
"use client"
import { useEffect, useState } from "react"
import { UltiMailLogo } from "@/components/ultimail-logo"
import { cn } from "@/lib/utils"
const SPLASH_SEEN_KEY = "ultimail-splash-seen-v1"
const SPLASH_VISIBLE_MS = 1450
const SPLASH_EXIT_MS = 500
export function FirstLaunchSplash({
children,
}: {
children: React.ReactNode
}) {
const [isHiding, setIsHiding] = useState(false)
const [isComplete, setIsComplete] = useState(false)
useEffect(() => {
const root = document.documentElement
const alreadySeen = root.dataset.splashSeen === "1"
if (alreadySeen) {
setIsComplete(true)
return
}
const hideTimer = window.setTimeout(() => {
setIsHiding(true)
}, SPLASH_VISIBLE_MS)
const completeTimer = window.setTimeout(() => {
try {
localStorage.setItem(SPLASH_SEEN_KEY, "1")
} catch {
// Ignore storage failures (private mode / disabled storage).
}
root.dataset.splashSeen = "1"
setIsComplete(true)
}, SPLASH_VISIBLE_MS + SPLASH_EXIT_MS)
return () => {
window.clearTimeout(hideTimer)
window.clearTimeout(completeTimer)
}
}, [])
return (
<>
{children}
{!isComplete ? (
<div
className={cn("app-first-launch-splash", isHiding && "app-first-launch-splash--hide")}
role="status"
aria-live="polite"
aria-label="Chargement d'Ultimail"
>
<div className="app-first-launch-splash__aurora" aria-hidden />
<div className="app-first-launch-splash__grain" aria-hidden />
<div className="app-first-launch-splash__content">
<div className="app-first-launch-splash__pill">ULTIMAIL</div>
<UltiMailLogo href={null} className="app-first-launch-splash__logo" />
<p className="app-first-launch-splash__subtitle">
Synchronisation de votre boite de reception...
</p>
<div className="app-first-launch-splash__loader" aria-hidden>
<span />
</div>
</div>
</div>
) : null}
</>
)
}

View File

@ -4,6 +4,9 @@ import Script from 'next/script'
export const THEME_INIT_SCRIPT = ` export const THEME_INIT_SCRIPT = `
(function () { (function () {
try { try {
var splashSeen = localStorage.getItem("ultimail-splash-seen-v1") === "1";
document.documentElement.dataset.splashSeen = splashSeen ? "1" : "0";
var raw = localStorage.getItem("ultimail-mail-settings"); var raw = localStorage.getItem("ultimail-mail-settings");
if (!raw) return; if (!raw) return;
var parsed = JSON.parse(raw); var parsed = JSON.parse(raw);