From a1ffc300ff2591a4dcf4558d8d19fd036e96ca2e Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 19 Jan 2026 09:15:35 +0000 Subject: [PATCH] feat: TopBar refonte + map markers animations + theme system - Add TopBarNew with Framer Motion animations and compact design - Enhance map markers with bounce-in animations and hover effects - Improve cluster styles with gradient backgrounds - Add comprehensive theme system (dark/blue/light modes) - Add theme-aware utilities and improved contrasts - Better accessibility with focus rings and motion preferences Co-Authored-By: Claude Opus 4.5 --- src/App.jsx | 2 +- src/components/Layout/TopBarNew.jsx | 865 ++++++++++++++++++++++++++++ src/styles/mapMarkers.css | 301 ++++++++++ src/styles/tailwind.css | 302 ++++++++++ 4 files changed, 1469 insertions(+), 1 deletion(-) create mode 100644 src/components/Layout/TopBarNew.jsx diff --git a/src/App.jsx b/src/App.jsx index ff7a350..f4c3b84 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,7 +1,7 @@ import React, { Suspense, useEffect, useState, useCallback } from "react"; import { useTranslation } from "react-i18next"; import { version as appVersion } from "../package.json"; -import TopBar from "./components/Layout/TopBar"; +import TopBar from "./components/Layout/TopBarNew"; import PostList from "./components/Posts/PostList"; import ContentFilters from "./components/Filters/ContentFilters"; import IslandUniverse from "./components/Island/IslandUniverse"; diff --git a/src/components/Layout/TopBarNew.jsx b/src/components/Layout/TopBarNew.jsx new file mode 100644 index 0000000..383a471 --- /dev/null +++ b/src/components/Layout/TopBarNew.jsx @@ -0,0 +1,865 @@ +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import { useAuth } from "../../auth/AuthContext"; +import { registerUser, updateProfileUsername, fetchFriendRequests, toSmallImageUrl } from "../../api/client"; +import AuthToast from "../Auth/AuthToast"; +import { promptInstall, isRunningAsPWA, canInstall } from '../../pwaInstall'; +import { trackEvent } from "../../utils/analytics"; +import FriendsList from "../Friends/FriendsList"; + +const SUPPORTED_LANGS = ['en', 'fr', 'es']; +const THEMES = ['dark', 'blue', 'light']; +const LAST_SIGNUP_KEY = "sociowire:lastSignup"; + +// ───────────────────────────────────────────────────────────────────────────── +// Utilities +// ───────────────────────────────────────────────────────────────────────────── + +function initialLetter(name = "") { + const s = (name || "").trim(); + return s ? s[0].toUpperCase() : "G"; +} + +function truthyParam(v) { + if (!v) return false; + const x = String(v).toLowerCase().trim(); + return x === "1" || x === "true" || x === "yes" || x === "ok"; +} + +function readLastSignup() { + try { + const raw = localStorage.getItem(LAST_SIGNUP_KEY); + if (!raw) return null; + const j = JSON.parse(raw); + if (!j || typeof j !== "object") return null; + return { username: (j.username || "").toString().trim(), email: (j.email || "").toString().trim() }; + } catch { return null; } +} + +function parseMergedParams() { + const search = window.location.search || ""; + const hash = window.location.hash || ""; + const qs = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search); + let hashQs = ""; + if (hash.includes("?")) hashQs = hash.slice(hash.indexOf("?") + 1); + const hs = new URLSearchParams(hashQs); + const get = (k) => { + const a = qs.get(k); + if (a != null && a !== "") return a; + const b = hs.get(k); + return b != null ? b : null; + }; + return { qs, hs, get, hash }; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Animation Variants +// ───────────────────────────────────────────────────────────────────────────── + +const dropdownVariants = { + hidden: { opacity: 0, y: -8, scale: 0.95 }, + visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.15, ease: "easeOut" } }, + exit: { opacity: 0, y: -8, scale: 0.95, transition: { duration: 0.1 } }, +}; + +const buttonVariants = { + hover: { scale: 1.05, y: -2 }, + tap: { scale: 0.95 }, +}; + +const badgeVariants = { + initial: { scale: 0 }, + animate: { scale: 1, transition: { type: "spring", stiffness: 500, damping: 25 } }, +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Sub-components +// ───────────────────────────────────────────────────────────────────────────── + +function ActionButton({ icon, label, active, color = "default", badge, onClick, title, disabled }) { + const colorClasses = { + default: "from-surface-800/80 to-surface-700/80 border-surface-600/30 text-surface-100 hover:border-accent/50 hover:shadow-glow-sm", + green: "from-emerald-600 to-emerald-700 border-emerald-500/50 text-white hover:from-emerald-500 hover:to-emerald-600", + purple: "from-violet-600 to-purple-700 border-violet-500/50 text-white hover:from-violet-500 hover:to-purple-600", + amber: "from-amber-500 to-orange-600 border-amber-400/50 text-white hover:from-amber-400 hover:to-orange-500", + cyan: "from-cyan-500/20 to-cyan-600/20 border-cyan-400/40 text-cyan-100 hover:border-cyan-300/60", + }; + + return ( + + + {label && {label}} + + {badge > 0 && ( + + {badge} + + )} + + ); +} + +function DropdownMenu({ show, position, children, onClose }) { + const menuRef = useRef(null); + + useEffect(() => { + if (!show) return; + const handleClick = (e) => { + if (menuRef.current && !menuRef.current.contains(e.target)) { + onClose?.(); + } + }; + document.addEventListener("mousedown", handleClick); + return () => document.removeEventListener("mousedown", handleClick); + }, [show, onClose]); + + return ( + + {show && ( + + {children} + + )} + + ); +} + +function DropdownOption({ active, onClick, children }) { + return ( + + {children} + + ); +} + +function UserChip({ authenticated, loading, username, avatarUrl, onClick }) { + const { t } = useTranslation(); + + const displayUsername = useMemo(() => { + const raw = (username || "user").toString(); + if (raw.includes("@")) return "user"; + if (raw.length <= 9) return raw; + return raw.slice(0, 9) + ".."; + }, [username]); + + return ( + + {/* Avatar */} +
+ {avatarUrl ? ( + {username} + ) : ( +
+ {authenticated ? initialLetter(username) : } +
+ )} +
+ + {/* Text - hidden on mobile */} +
+ + {loading ? t('common.loading') : authenticated ? t('common.online') : t('common.guest')} + + + @{authenticated ? displayUsername : t('common.anon')} + +
+
+ ); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Main Component +// ───────────────────────────────────────────────────────────────────────────── + +export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland }) { + const { t, i18n } = useTranslation(); + const { + loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled, + avatarUrl, profileLoaded, needsUsername, needsEmailVerification, lastError, lastInfo, clearError, clearInfo, + } = useAuth(); + + // Dropdown states + const [showLangMenu, setShowLangMenu] = useState(false); + const [langMenuPos, setLangMenuPos] = useState({ top: 0, right: 0 }); + const langBtnRef = useRef(null); + + const [showThemeMenu, setShowThemeMenu] = useState(false); + const [themeMenuPos, setThemeMenuPos] = useState({ top: 0, right: 0 }); + const themeBtnRef = useRef(null); + + const [showAuth, setShowAuth] = useState(false); + const [mode, setMode] = useState("login"); + const [showUsernameModal, setShowUsernameModal] = useState(false); + const [showFriends, setShowFriends] = useState(false); + const [pendingRequestsCount, setPendingRequestsCount] = useState(0); + const [showInstallBtn, setShowInstallBtn] = useState(false); + + // Form states + const [loginUser, setLoginUser] = useState(""); + const [loginPass, setLoginPass] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [regUser, setRegUser] = useState(""); + const [regEmail, setRegEmail] = useState(""); + const [regPass, setRegPass] = useState(""); + const [signupError, setSignupError] = useState(""); + const [signupInfo, setSignupInfo] = useState(""); + const [signupLoading, setSignupLoading] = useState(false); + const [authNotice, setAuthNotice] = useState(""); + const [desiredUsername, setDesiredUsername] = useState(""); + const [usernameError, setUsernameError] = useState(""); + const [usernameInfo, setUsernameInfo] = useState(""); + const [usernameSaving, setUsernameSaving] = useState(false); + + const usernamePromptTimer = useRef(null); + + // Toggle functions + const toggleLangMenu = () => { + if (!showLangMenu && langBtnRef.current) { + const rect = langBtnRef.current.getBoundingClientRect(); + setLangMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right }); + } + setShowLangMenu(!showLangMenu); + setShowThemeMenu(false); + }; + + const toggleThemeMenu = () => { + if (!showThemeMenu && themeBtnRef.current) { + const rect = themeBtnRef.current.getBoundingClientRect(); + setThemeMenuPos({ top: rect.bottom + 8, right: window.innerWidth - rect.right }); + } + setShowThemeMenu(!showThemeMenu); + setShowLangMenu(false); + }; + + const openModal = (nextMode) => { + setMode(nextMode); + setShowAuth(true); + setSignupError(""); + trackEvent("auth_modal_open", { mode: nextMode || "login" }); + }; + + const closeModal = () => { + setShowAuth(false); + setLoginPass(""); + setSignupError(""); + setAuthNotice(""); + }; + + // Reset password URL + const resetPasswordUrl = useMemo(() => { + const base = (import.meta?.env?.VITE_KC_URL || "https://auth.sociowire.com").replace(/\/+$/, ""); + const realm = import.meta?.env?.VITE_KC_REALM || "sociowire"; + const clientId = import.meta?.env?.VITE_KC_CLIENT_ID || "sociowire-frontend"; + const redirectUri = window.location.origin + window.location.pathname; + return `${base}/realms/${realm}/login-actions/reset-credentials?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}`; + }, []); + + // Effects + useEffect(() => { + if (authenticated) setShowAuth(false); + }, [authenticated]); + + useEffect(() => { + if (!authenticated) { setPendingRequestsCount(0); return; } + const loadRequests = async () => { + try { + const res = await fetchFriendRequests(); + const received = (res?.requests || []).filter(r => r?.direction === 'received'); + setPendingRequestsCount(received.length); + } catch { setPendingRequestsCount(0); } + }; + loadRequests(); + const interval = setInterval(loadRequests, 30000); + return () => clearInterval(interval); + }, [authenticated]); + + useEffect(() => { + const checkInstall = () => { + if (isRunningAsPWA()) { setShowInstallBtn(false); return; } + setShowInstallBtn(canInstall()); + }; + checkInstall(); + const handlePrompt = () => setTimeout(checkInstall, 100); + window.addEventListener('beforeinstallprompt', handlePrompt); + window.addEventListener('appinstalled', () => setShowInstallBtn(false)); + return () => { + window.removeEventListener('beforeinstallprompt', handlePrompt); + window.removeEventListener('appinstalled', () => setShowInstallBtn(false)); + }; + }, []); + + useEffect(() => { + const handler = (e) => { + const detail = e?.detail || {}; + const msg = (detail.message || "").toString().trim() || t('auth.pleaseSignIn'); + setAuthNotice(msg); + setMode(detail.mode === "signup" ? "signup" : "login"); + setShowAuth(true); + }; + window.addEventListener("sociowire:auth-required", handler); + return () => window.removeEventListener("sociowire:auth-required", handler); + }, [t]); + + // Username prompt + useEffect(() => { + const requiresUsername = (name) => { + if (!name) return false; + if (name.includes("@")) return true; + return !/^[A-Za-z0-9_.]{3,24}$/.test(name); + }; + if (usernamePromptTimer.current) clearTimeout(usernamePromptTimer.current); + if (loading) return; + if (!authenticated) { setShowUsernameModal(false); return; } + const shouldPrompt = !!needsUsername || requiresUsername(username); + if (shouldPrompt && !showUsernameModal) { + const base = (username || "").split("@")[0] || ""; + const cleaned = base.replace(/[^A-Za-z0-9_.]/g, "").slice(0, 24); + setDesiredUsername(cleaned); + usernamePromptTimer.current = setTimeout(() => setShowUsernameModal(true), 600); + } else if (!shouldPrompt && showUsernameModal) { + setShowUsernameModal(false); + } + }, [authenticated, username, showUsernameModal, loading, needsUsername]); + + // Handlers + const handleLoginSubmit = async (e) => { + e.preventDefault(); + trackEvent("login_submit"); + const res = await login(loginUser, loginPass); + if (res?.ok) trackEvent("login_success"); + }; + + const handleSignupSubmit = async (e) => { + e.preventDefault(); + setSignupError(""); + setSignupInfo(""); + if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) { + setSignupError(t('auth.fieldsRequired')); + return; + } + try { + setSignupLoading(true); + trackEvent("signup_submit"); + const u = regUser.trim(); + const em = regEmail.trim(); + await registerUser({ username: u, email: em, password: regPass, firstName: firstName.trim(), lastName: lastName.trim() }); + try { localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em })); } catch {} + setSignupInfo("✅ " + t('auth.accountCreatedMsg') + " (" + em + ")"); + trackEvent("signup_success"); + setMode("login"); + setLoginUser(u || em); + setLoginPass(""); + } catch (err) { + setSignupError(err.message || t('auth.registrationError')); + } finally { + setSignupLoading(false); + } + }; + + const handleUsernameSave = async (e) => { + e.preventDefault(); + const next = (desiredUsername || "").trim(); + if (!/^[A-Za-z0-9_.]{3,24}$/.test(next) || next.includes("@")) { + setUsernameError(t('auth.usernameHint')); + return; + } + setUsernameSaving(true); + setUsernameError(""); + const res = await updateProfileUsername(next); + setUsernameSaving(false); + if (!res.ok) { + setUsernameError(res.status === 409 ? t('auth.usernameTaken') : (res.text || t('auth.usernameUpdateError'))); + return; + } + setUsernameInfo(t('auth.usernameSaved')); + setShowUsernameModal(false); + await restore(); + }; + + // Toast + const toast = useMemo(() => { + if (lastError) { + const toastType = needsEmailVerification ? "warn" : "error"; + const title = needsEmailVerification ? t('auth.emailVerificationRequired') : t('auth.loginFailed'); + return { type: toastType, title, message: lastError }; + } + if (lastInfo) return { type: "success", title: "", message: lastInfo }; + return null; + }, [lastError, lastInfo, needsEmailVerification, t]); + + // ───────────────────────────────────────────────────────────────────────────── + // Render + // ───────────────────────────────────────────────────────────────────────────── + + return ( + <> + {/* Toast */} + {toast?.message && ( +
+ { + if (lastError) clearError?.(); + if (lastInfo) clearInfo?.(); + }} + /> +
+ )} + + {/* Header */} + + {/* Left: Logo & Brand */} +
+ + SocioWire + + +
+ + SOCIOWIRE + + + {t('topbar.wiredToLife')} + +
+ + {/* Theme dots - desktop only */} +
+ {THEMES.map((themeKey) => ( + onChangeTheme?.(themeKey)} + whileHover={{ scale: 1.15 }} + whileTap={{ scale: 0.9 }} + > + {t(`theme.${themeKey}`)[0]} + + ))} +
+
+ + {/* Right: Actions */} +
+ + {/* User chip */} + authenticated && onUserIsland?.(username)} + /> + + {/* PWA Install */} + {showInstallBtn && ( + promptInstall()} + title={t('topbar.installApp')} + /> + )} + + {/* Theme - mobile */} +
+ +
+ + {/* Language */} +
+ +
+ + {/* Friends */} + {authenticated && ( + setShowFriends(!showFriends)} + title={t('topbar.friends')} + /> + )} + + {/* Auth */} + { + if (authenticated) { trackEvent("logout_click"); logout(); } + else openModal("login"); + }} + disabled={loading} + title={authenticated ? t('topbar.logout') : t('topbar.login')} + /> +
+
+ + {/* Dropdowns */} + setShowLangMenu(false)}> + {SUPPORTED_LANGS.map((lang) => ( + { i18n.changeLanguage(lang); setShowLangMenu(false); }} + > + {t(`language.${lang}`)} + + ))} + + + setShowThemeMenu(false)}> + {THEMES.map((themeKey) => ( + { onChangeTheme?.(themeKey); setShowThemeMenu(false); }} + > + {t(`theme.${themeKey}`)} + + ))} + + + {/* Friends Panel */} + + {showFriends && ( + + { setShowFriends(false); onUserIsland?.(uname); }} + onClose={() => setShowFriends(false)} + /> + + )} + + + {/* Auth Modal */} + + {showAuth && ( + + e.stopPropagation()} + > + {/* Header tabs */} +
+ + + +
+ + {/* Notices */} + {signupInfo &&
{signupInfo}
} + {authNotice &&
{authNotice}
} + {mode === "login" && lastError && ( +
+ {lastError} +
+ )} + + {/* Forms */} +
+ {mode === "login" ? ( +
+ + + + + {loading ? "..." : t('auth.signIn')} + + + {ssoEnabled && ( + <> +
+
+ {t('auth.orContinueWith')} +
+
+
+ loginWithProvider?.("google")} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + + Google + + loginWithProvider?.("facebook")} + whileHover={{ scale: 1.02 }} + whileTap={{ scale: 0.98 }} + > + + Facebook + +
+ + )} + + ) : ( +
+
+ + +
+ + + + {signupError &&
{signupError}
} + + {signupLoading ? "..." : t('auth.createAccount')} + +
+ )} +
+ + + )} + + + {/* Username Modal */} + + {showUsernameModal && ( + + +

{t('auth.chooseUsername')}

+

{t('auth.pickUsername')}

+ {usernameError &&
{usernameError}
} + {usernameInfo &&
{usernameInfo}
} +
+ setDesiredUsername(e.target.value)} + className="w-full px-4 py-3 rounded-xl bg-surface-800/80 border border-surface-600/50 text-surface-100 focus:border-accent/50 focus:outline-none" + placeholder={t('auth.username')} + /> + + {usernameSaving ? t('auth.savingUsername') : t('auth.saveUsername')} + +
+
+
+ )} +
+ + ); +} diff --git a/src/styles/mapMarkers.css b/src/styles/mapMarkers.css index 8eb8496..0e96243 100644 --- a/src/styles/mapMarkers.css +++ b/src/styles/mapMarkers.css @@ -1460,3 +1460,304 @@ body[data-theme="light"] .sw-modal-x{ border-right: 6px solid transparent; border-top: 8px solid #3b82f6; } + +/* ═══════════════════════════════════════════════════════════════════════════ + ENHANCED MARKER ANIMATIONS (v2 - Framer Motion style) + ═══════════════════════════════════════════════════════════════════════════ */ + +/* Smooth entry animations */ +@keyframes markerBounceIn { + 0% { + opacity: 0; + transform: scale(0) translateY(20px); + } + 50% { + transform: scale(1.15) translateY(-5px); + } + 70% { + transform: scale(0.95) translateY(2px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes markerFadeSlide { + 0% { + opacity: 0; + transform: translateY(16px) scale(0.9); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes markerPulseRing { + 0% { + transform: scale(0.8); + opacity: 0.8; + } + 50% { + transform: scale(1.4); + opacity: 0; + } + 100% { + transform: scale(0.8); + opacity: 0; + } +} + +@keyframes glowPulse { + 0%, 100% { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 rgba(96, 165, 250, 0); + } + 50% { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.4); + } +} + +/* Simple marker enhancements */ +.sw-simple-marker { + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease, + border-color 0.2s ease; + will-change: transform; +} + +.post-pin--simple:hover .sw-simple-marker { + transform: scale(1.2) translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 15px rgba(96, 165, 250, 0.35); + border-color: rgba(96, 165, 250, 0.8); +} + +/* Live marker pulsing glow */ +.sw-live-marker { + animation: glowPulse 2s ease-in-out infinite; +} + +.sw-live-marker::before { + content: ""; + position: absolute; + inset: -4px; + border-radius: 50%; + background: rgba(239, 68, 68, 0.3); + animation: markerPulseRing 2s ease-out infinite; + pointer-events: none; +} + +/* Hover card smooth animation */ +.sw-simple-hover-card { + transform: translateX(-50%) translateY(-8px) scale(0.95); + opacity: 0; + pointer-events: none; + transition: opacity 0.25s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.post-pin--simple:hover .sw-simple-hover-card { + opacity: 1; + transform: translateX(-50%) translateY(-12px) scale(1); + pointer-events: auto; +} + +/* Entry animation for markers */ +.sw-appear { + opacity: 0; + animation: markerFadeSlide 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +.sw-appear-in { + opacity: 1; +} + +/* Stagger effect for multiple markers */ +.sw-appear:nth-child(2) { animation-delay: 0.05s; } +.sw-appear:nth-child(3) { animation-delay: 0.1s; } +.sw-appear:nth-child(4) { animation-delay: 0.15s; } +.sw-appear:nth-child(5) { animation-delay: 0.2s; } + +/* Cluster marker styles */ +.sw-cluster-marker { + position: relative; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.sw-cluster-marker:hover { + transform: scale(1.1); +} + +.sw-cluster-bubble { + min-width: 44px; + height: 44px; + padding: 0 12px; + border-radius: 22px; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.95), rgba(139, 92, 246, 0.9)); + border: 3px solid rgba(255, 255, 255, 0.95); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), 0 0 20px rgba(96, 165, 250, 0.25); + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all 0.25s ease; +} + +.sw-cluster-marker:hover .sw-cluster-bubble { + background: linear-gradient(135deg, rgba(96, 165, 250, 0.98), rgba(167, 139, 250, 0.95)); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.4), 0 0 30px rgba(96, 165, 250, 0.4); +} + +.sw-cluster-count { + font-size: 16px; + font-weight: 800; + color: #fff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.sw-cluster-icon { + font-size: 14px; + color: rgba(255, 255, 255, 0.9); +} + +.sw-cluster-pointer { + position: absolute; + bottom: -10px; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 10px solid transparent; + border-right: 10px solid transparent; + border-top: 14px solid rgba(59, 130, 246, 0.95); + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); +} + +/* Category badges on markers */ +.sw-marker-badge { + position: absolute; + top: -6px; + right: -6px; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + background: linear-gradient(135deg, #ef4444, #dc2626); + border: 2px solid #fff; + font-size: 10px; + font-weight: 700; + color: #fff; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); +} + +/* Modal overlay improvements */ +.sw-centered-modal { + animation: modalFadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalFadeIn { + 0% { + opacity: 0; + transform: scale(0.95) translateY(10px); + } + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +/* Modal backdrop blur */ +.sw-modal-backdrop { + position: fixed; + inset: 0; + background: rgba(5, 8, 22, 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + animation: backdropFadeIn 0.3s ease; +} + +@keyframes backdropFadeIn { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +/* Improved hero section with gradient overlay */ +.sw-modal-hero { + position: relative; + overflow: hidden; + border-radius: 12px; +} + +.sw-modal-hero::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + to bottom, + transparent 0%, + transparent 50%, + rgba(15, 23, 42, 0.8) 100% + ); + pointer-events: none; +} + +.sw-modal-hero img { + transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.sw-modal-hero:hover img { + transform: scale(1.05); +} + +/* Stats bar with micro-interactions */ +.sw-stat-pill { + transition: all 0.2s ease; + cursor: pointer; +} + +.sw-stat-pill:hover { + transform: translateY(-2px); + background: rgba(96, 165, 250, 0.25) !important; + border-color: rgba(96, 165, 250, 0.5) !important; +} + +.sw-stat-pill:active { + transform: scale(0.95); +} + +/* Truth score rail animation */ +.sw-truth-rail-marker { + transition: top 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +/* CTA button hover effect */ +.sw-cta-primary { + position: relative; + overflow: hidden; + transition: all 0.25s ease; +} + +.sw-cta-primary::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); + transform: translateX(-100%); + transition: transform 0.5s ease; +} + +.sw-cta-primary:hover::before { + transform: translateX(100%); +} + +.sw-cta-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(96, 165, 250, 0.3); +} diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css index 68c268b..41322ea 100644 --- a/src/styles/tailwind.css +++ b/src/styles/tailwind.css @@ -282,3 +282,305 @@ html { -webkit-box-orient: vertical; overflow: hidden; } + +/* ───────────────────────────────────────────────────────────────────────────── + Enhanced Theme System - Dark/Blue/Light modes + ───────────────────────────────────────────────────────────────────────────── */ + +/* Theme variables - Blue (default) */ +:root, +body[data-theme="blue"] { + --theme-bg-primary: #050816; + --theme-bg-secondary: #0a1628; + --theme-bg-tertiary: #0f1d32; + --theme-bg-elevated: #142238; + --theme-text-primary: #f1f5f9; + --theme-text-secondary: #94a3b8; + --theme-text-muted: #64748b; + --theme-border: rgba(59, 130, 246, 0.2); + --theme-border-hover: rgba(96, 165, 250, 0.4); + --theme-accent: #60a5fa; + --theme-accent-soft: rgba(96, 165, 250, 0.15); + --theme-gradient-start: #0a1628; + --theme-gradient-end: #050816; + --theme-glow: rgba(96, 165, 250, 0.3); +} + +/* Theme variables - Dark */ +body[data-theme="dark"] { + --theme-bg-primary: #09090b; + --theme-bg-secondary: #18181b; + --theme-bg-tertiary: #27272a; + --theme-bg-elevated: #3f3f46; + --theme-text-primary: #fafafa; + --theme-text-secondary: #a1a1aa; + --theme-text-muted: #71717a; + --theme-border: rgba(255, 255, 255, 0.1); + --theme-border-hover: rgba(255, 255, 255, 0.2); + --theme-accent: #a78bfa; + --theme-accent-soft: rgba(167, 139, 250, 0.15); + --theme-gradient-start: #18181b; + --theme-gradient-end: #09090b; + --theme-glow: rgba(167, 139, 250, 0.3); +} + +/* Theme variables - Light */ +body[data-theme="light"] { + --theme-bg-primary: #ffffff; + --theme-bg-secondary: #f8fafc; + --theme-bg-tertiary: #f1f5f9; + --theme-bg-elevated: #ffffff; + --theme-text-primary: #0f172a; + --theme-text-secondary: #475569; + --theme-text-muted: #94a3b8; + --theme-border: rgba(15, 23, 42, 0.1); + --theme-border-hover: rgba(59, 130, 246, 0.3); + --theme-accent: #3b82f6; + --theme-accent-soft: rgba(59, 130, 246, 0.1); + --theme-gradient-start: #ffffff; + --theme-gradient-end: #f1f5f9; + --theme-glow: rgba(59, 130, 246, 0.2); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Theme-aware components + ───────────────────────────────────────────────────────────────────────────── */ + +/* Themed container */ +.themed-container { + background: linear-gradient(180deg, var(--theme-gradient-start), var(--theme-gradient-end)); + color: var(--theme-text-primary); +} + +/* Themed card */ +.themed-card { + background: var(--theme-bg-elevated); + border: 1px solid var(--theme-border); + border-radius: 1rem; + transition: all 0.3s ease; +} + +.themed-card:hover { + border-color: var(--theme-border-hover); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2), 0 0 16px var(--theme-glow); +} + +/* Themed text */ +.text-themed-primary { color: var(--theme-text-primary); } +.text-themed-secondary { color: var(--theme-text-secondary); } +.text-themed-muted { color: var(--theme-text-muted); } +.text-themed-accent { color: var(--theme-accent); } + +/* Themed backgrounds */ +.bg-themed-primary { background: var(--theme-bg-primary); } +.bg-themed-secondary { background: var(--theme-bg-secondary); } +.bg-themed-elevated { background: var(--theme-bg-elevated); } +.bg-themed-accent { background: var(--theme-accent-soft); } + +/* Themed borders */ +.border-themed { border-color: var(--theme-border); } +.border-themed-hover:hover { border-color: var(--theme-border-hover); } + +/* ───────────────────────────────────────────────────────────────────────────── + Improved Contrast Utilities + ───────────────────────────────────────────────────────────────────────────── */ + +/* High contrast text for accessibility */ +.text-high-contrast { + color: #ffffff; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); +} + +body[data-theme="light"] .text-high-contrast { + color: #0f172a; + text-shadow: none; +} + +/* Readable text on images */ +.text-on-image { + color: #ffffff; + text-shadow: + 0 1px 2px rgba(0, 0, 0, 0.8), + 0 2px 8px rgba(0, 0, 0, 0.4); +} + +/* Focus ring for accessibility */ +.focus-ring:focus-visible { + outline: none; + box-shadow: 0 0 0 3px var(--theme-accent-soft), 0 0 0 5px var(--theme-accent); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Interactive Elements + ───────────────────────────────────────────────────────────────────────────── */ + +/* Hover lift effect */ +.hover-lift { + transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.25s ease; +} + +.hover-lift:hover { + transform: translateY(-4px); +} + +/* Press scale effect */ +.press-scale { + transition: transform 0.15s ease; +} + +.press-scale:active { + transform: scale(0.97); +} + +/* Glow on hover */ +.hover-glow { + transition: box-shadow 0.3s ease; +} + +.hover-glow:hover { + box-shadow: 0 0 20px var(--theme-glow); +} + +/* ───────────────────────────────────────────────────────────────────────────── + Status & State Indicators + ───────────────────────────────────────────────────────────────────────────── */ + +/* Online indicator */ +.status-online { + position: relative; +} + +.status-online::after { + content: ""; + position: absolute; + bottom: 0; + right: 0; + width: 12px; + height: 12px; + border-radius: 50%; + background: #22c55e; + border: 2px solid var(--theme-bg-primary); + box-shadow: 0 0 8px rgba(34, 197, 94, 0.5); +} + +/* New content indicator */ +.has-new::before { + content: ""; + position: absolute; + top: -2px; + right: -2px; + width: 8px; + height: 8px; + border-radius: 50%; + background: #ef4444; + animation: livePulse 1.5s ease-in-out infinite; +} + +/* Loading shimmer overlay */ +.shimmer-overlay { + position: relative; + overflow: hidden; +} + +.shimmer-overlay::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.05) 50%, + transparent 100% + ); + background-size: 200% 100%; + animation: shimmer 2s linear infinite; +} + +/* ───────────────────────────────────────────────────────────────────────────── + Spacing & Layout Helpers + ───────────────────────────────────────────────────────────────────────────── */ + +/* Safe area padding for mobile */ +.safe-area-top { padding-top: env(safe-area-inset-top, 0); } +.safe-area-bottom { padding-bottom: env(safe-area-inset-bottom, 0); } +.safe-area-left { padding-left: env(safe-area-inset-left, 0); } +.safe-area-right { padding-right: env(safe-area-inset-right, 0); } + +/* Content container with max-width */ +.content-container { + width: 100%; + max-width: 1400px; + margin-left: auto; + margin-right: auto; + padding-left: 1rem; + padding-right: 1rem; +} + +@media (min-width: 640px) { + .content-container { + padding-left: 1.5rem; + padding-right: 1.5rem; + } +} + +/* ───────────────────────────────────────────────────────────────────────────── + Motion & Transitions + ───────────────────────────────────────────────────────────────────────────── */ + +/* Reduced motion preference */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Smooth entrance animations */ +@keyframes enterFromBottom { + 0% { + opacity: 0; + transform: translateY(16px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes enterFromLeft { + 0% { + opacity: 0; + transform: translateX(-16px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes enterFromRight { + 0% { + opacity: 0; + transform: translateX(16px); + } + 100% { + opacity: 1; + transform: translateX(0); + } +} + +.animate-enter-bottom { + animation: enterFromBottom 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.animate-enter-left { + animation: enterFromLeft 0.4s cubic-bezier(0.16, 1, 0.3, 1); +} + +.animate-enter-right { + animation: enterFromRight 0.4s cubic-bezier(0.16, 1, 0.3, 1); +}