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:
parent
5bf388e062
commit
447567a411
190
app/globals.css
190
app/globals.css
@ -374,6 +374,196 @@ body {
|
||||
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 l’UI) ── */
|
||||
html {
|
||||
background-color: var(--mail-bg-fallback, var(--app-canvas));
|
||||
|
||||
@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from 'next/font/google'
|
||||
import { Analytics } from '@vercel/analytics/next'
|
||||
import './globals.css'
|
||||
import { ThemeInitScript } from '@/components/theme-init-script'
|
||||
import { FirstLaunchSplash } from '@/components/first-launch-splash'
|
||||
|
||||
const _geist = Geist({ 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">
|
||||
<body className="h-dvh max-h-dvh overflow-hidden bg-background font-sans antialiased touch-manipulation">
|
||||
<ThemeInitScript />
|
||||
{children}
|
||||
<FirstLaunchSplash>{children}</FirstLaunchSplash>
|
||||
{process.env.NODE_ENV === 'production' && <Analytics />}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
74
components/first-launch-splash.tsx
Normal file
74
components/first-launch-splash.tsx
Normal 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}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -4,6 +4,9 @@ import Script from 'next/script'
|
||||
export const THEME_INIT_SCRIPT = `
|
||||
(function () {
|
||||
try {
|
||||
var splashSeen = localStorage.getItem("ultimail-splash-seen-v1") === "1";
|
||||
document.documentElement.dataset.splashSeen = splashSeen ? "1" : "0";
|
||||
|
||||
var raw = localStorage.getItem("ultimail-mail-settings");
|
||||
if (!raw) return;
|
||||
var parsed = JSON.parse(raw);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user