From 447567a41195b171fcda7644d89441810c8f4718 Mon Sep 17 00:00:00 2001 From: R3D347HR4Y Date: Thu, 21 May 2026 09:55:10 +0200 Subject: [PATCH] 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. --- app/globals.css | 190 +++++++++++++++++++++++++++++ app/layout.tsx | 3 +- components/first-launch-splash.tsx | 74 +++++++++++ components/theme-init-script.tsx | 3 + 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 components/first-launch-splash.tsx diff --git a/app/globals.css b/app/globals.css index 54c3d90..621266b 100644 --- a/app/globals.css +++ b/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)); diff --git a/app/layout.tsx b/app/layout.tsx index d7b36f2..c571a62 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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({ - {children} + {children} {process.env.NODE_ENV === 'production' && } diff --git a/components/first-launch-splash.tsx b/components/first-launch-splash.tsx new file mode 100644 index 0000000..8c4817c --- /dev/null +++ b/components/first-launch-splash.tsx @@ -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 ? ( +
+
+
+
+
ULTIMAIL
+ +

+ Synchronisation de votre boite de reception... +

+
+ +
+
+
+ ) : null} + + ) +} diff --git a/components/theme-init-script.tsx b/components/theme-init-script.tsx index 7603c3e..19d58f8 100644 --- a/components/theme-init-script.tsx +++ b/components/theme-init-script.tsx @@ -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);