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;
|
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) ── */
|
/* ── Mail : fond décoratif plein écran (derrière toute l’UI) ── */
|
||||||
html {
|
html {
|
||||||
background-color: var(--mail-bg-fallback, var(--app-canvas));
|
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 { 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>
|
||||||
|
|||||||
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 = `
|
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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user