From 3e34dbde206a364bb5a4071c80ed6d429f085295 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 20 Dec 2025 03:23:36 -0500 Subject: [PATCH] nleh --- src/auth/AuthContext.jsx | 4 +- src/auth/AuthContext.jsx.bak.1766217948 | 425 ++++++++++++++++++ src/components/Layout/TopBar.jsx | 275 ++++++++---- .../Layout/TopBar.jsx.bak.1766217948 | 392 ++++++++++++++++ src/styles/auth-modal.css | 77 ++++ src/styles/auth-modal.css.bak.1766217948 | 139 ++++++ src/styles/auth-toast.css | 11 + src/styles/topbar.css | 148 ++++++ src/styles/topbar.css.bak.1766217948 | 425 ++++++++++++++++++ 9 files changed, 1801 insertions(+), 95 deletions(-) create mode 100644 src/auth/AuthContext.jsx.bak.1766217948 create mode 100644 src/components/Layout/TopBar.jsx.bak.1766217948 create mode 100644 src/styles/auth-modal.css.bak.1766217948 create mode 100644 src/styles/topbar.css.bak.1766217948 diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx index 38e3ace..ebccb1f 100644 --- a/src/auth/AuthContext.jsx +++ b/src/auth/AuthContext.jsx @@ -181,7 +181,7 @@ export function AuthProvider({ children }) { async function refreshProfile(accessTokenOverride) { const at = accessTokenOverride || tokens?.access_token; - if (!at) return { ok: False }; + if (!at) return { ok: false }; // /api/me is the truth; token claim is fallback only const mr = await me(at); @@ -414,6 +414,8 @@ export function AuthProvider({ children }) { setError, setLastInfo, setNeedsEmailVerification, + clearError: () => setError(""), + clearInfo: () => setLastInfo(""), }; }, [status, user, tokens, error, lastInfo, needsEmailVerification]); diff --git a/src/auth/AuthContext.jsx.bak.1766217948 b/src/auth/AuthContext.jsx.bak.1766217948 new file mode 100644 index 0000000..38e3ace --- /dev/null +++ b/src/auth/AuthContext.jsx.bak.1766217948 @@ -0,0 +1,425 @@ +import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { + clearAuth, + guessUsernameFromToken, + jwtExpSeconds, + loadAuth, + saveAuth, + decodeJwtPayload, +} from "./authStorage"; +import { parseAuthError } from "./authErrors"; + +const AuthCtx = createContext(null); + +function apiBase() { + return (import.meta?.env?.VITE_API_BASE_URL || "").replace(/\/+$/, ""); +} + +async function apiFetch(path, opts = {}) { + const url = apiBase() + path; + const res = await fetch(url, { + credentials: "include", + ...opts, + headers: { + ...(opts.headers || {}), + }, + }); + return res; +} + +function tokenEmailVerified(accessToken) { + try { + const p = decodeJwtPayload(accessToken); + if (!p) return null; + if (typeof p.email_verified === "boolean") return p.email_verified; + if (typeof p.emailVerified === "boolean") return p.emailVerified; + return null; + } catch { + return null; + } +} + +// tolerant whoami: accepts {authenticated:true} OR {username:""} OR {user:""} OR just ok+token +async function whoami(accessToken) { + const res = await apiFetch("/api/whoami", { + method: "GET", + headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, + }); + + const text = await res.text().catch(() => ""); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + + const uname = + (json && (json.username || json.user || json.preferred_username)) || + guessUsernameFromToken(accessToken) || + ""; + + const authed = + !!res.ok && + (json?.authenticated === true || + json?.ok === true || + !!json?.username || + !!json?.user || + !!uname); + + return { ok: !!res.ok, status: res.status, json, text, authenticated: authed, username: uname }; +} + +// NEW: /api/me is the source of truth for email_verified +async function me(accessToken) { + const res = await apiFetch("/api/me", { + method: "GET", + headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, + }); + + const text = await res.text().catch(() => ""); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + + // Try multiple shapes + const u = (json && (json.user || json.profile || json.me)) || json || null; + + const emailVerified = + u && typeof u.email_verified === "boolean" + ? u.email_verified + : u && typeof u.emailVerified === "boolean" + ? u.emailVerified + : null; + + return { + ok: !!res.ok, + status: res.status, + json, + text, + emailVerified, + }; +} + +async function refreshTokens(refreshToken) { + const res = await apiFetch("/api/refresh", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }), + }); + + const text = await res.text().catch(() => ""); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + + return { ok: res.ok, status: res.status, json, text }; +} + +async function resendVerification(accessToken) { + const res = await apiFetch("/api/resend-verification", { + method: "POST", + headers: { + "Content-Type": "application/json", + ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}), + }, + body: JSON.stringify({}), + }); + + const text = await res.text().catch(() => ""); + let json = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + json = null; + } + + return { ok: res.ok, status: res.status, json, text }; +} + +function nowSec() { + return Math.floor(Date.now() / 1000); +} + +export function AuthProvider({ children }) { + const [status, setStatus] = useState("loading"); // loading | anon | auth + const [user, setUser] = useState(null); // { username } + const [tokens, setTokens] = useState(null); // { access_token, refresh_token, ... } + + const [error, setError] = useState(""); + const [lastInfo, setLastInfo] = useState(""); + const [needsEmailVerification, setNeedsEmailVerification] = useState(false); + + const refreshTimer = useRef(null); + + function setAnon(msg = "") { + setStatus("anon"); + setUser(null); + setTokens(null); + setError(msg || ""); + setLastInfo(""); + setNeedsEmailVerification(false); + clearAuth(); + } + + function setAuthFromTokens(t, usernameGuess = "") { + setTokens(t); + saveAuth({ ...t, obtained_at: Date.now() }); + + const u = usernameGuess || guessUsernameFromToken(t?.access_token) || ""; + setUser(u ? { username: u } : null); + + setStatus("auth"); + setError(""); + } + + async function refreshProfile(accessTokenOverride) { + const at = accessTokenOverride || tokens?.access_token; + if (!at) return { ok: False }; + + // /api/me is the truth; token claim is fallback only + const mr = await me(at); + let verified = mr.emailVerified + + if (typeof verified !== "boolean") { + const tv = tokenEmailVerified(at); + if (typeof tv === "boolean") verified = tv; + } + + if (typeof verified === "boolean") { + setNeedsEmailVerification(!verified); + return { ok: true, verified }; + } + + // If cannot determine, do not block UI aggressively + setNeedsEmailVerification(false); + return { ok: mr.ok, verified: null }; + } + + async function ensureFreshSession(existing) { + if (!existing?.access_token) return { ok: false, reason: "no_access_token" }; + + const exp = jwtExpSeconds(existing.access_token); + const skew = 60; + const isExpiredOrSoon = !exp || exp <= nowSec() + skew; + + if (isExpiredOrSoon) { + if (!existing.refresh_token) return { ok: false, reason: "expired_no_refresh" }; + + const rr = await refreshTokens(existing.refresh_token); + if (!rr.ok || !rr.json?.access_token) { + return { ok: false, reason: `refresh_failed_${rr.status}`, detail: rr.text }; + } + + const merged = { ...existing, ...rr.json }; + if (!merged.refresh_token) merged.refresh_token = existing.refresh_token; + + setAuthFromTokens(merged); + existing = merged; + } else { + setAuthFromTokens(existing); + } + + const w = await whoami(existing.access_token); + if (w.ok && w.authenticated) { + setUser(w.username ? { username: w.username } : null); + setStatus("auth"); + setError(""); + + // ✅ Update email verification state from backend + await refreshProfile(existing.access_token); + + return { ok: true }; + } + + return { ok: false, reason: `whoami_${w.status}`, detail: w.text || "" }; + } + + function scheduleAutoRefresh() { + if (refreshTimer.current) { + clearInterval(refreshTimer.current); + refreshTimer.current = null; + } + + refreshTimer.current = setInterval(async () => { + const cur = loadAuth(); + if (!cur?.access_token) return; + + const exp = jwtExpSeconds(cur.access_token); + if (!exp) return; + + if (exp <= nowSec() + 90) { + if (!cur.refresh_token) { + setAnon("Session expirée (pas de refresh_token)."); + return; + } + + const rr = await refreshTokens(cur.refresh_token); + if (!rr.ok || !rr.json?.access_token) { + setAnon("Session expirée. Please login again."); + return; + } + + const merged = { ...cur, ...rr.json }; + if (!merged.refresh_token) merged.refresh_token = cur.refresh_token; + + setAuthFromTokens(merged); + + // keep verification fresh too + await refreshProfile(merged.access_token); + } else { + // lightweight: refresh verification occasionally without spamming + // (every 30s interval already runs; but this call is only parsing token unless needed) + const tv = tokenEmailVerified(cur.access_token); + if (typeof tv === "boolean") setNeedsEmailVerification(!tv); + } + }, 30000); + } + + async function restore() { + setStatus("loading"); + setError(""); + setLastInfo(""); + const saved = loadAuth(); + if (!saved?.access_token) { + setAnon(""); + return; + } + + const r = await ensureFreshSession(saved); + if (!r.ok) { + setAnon("Login desynced. Please login again."); + return; + } + + scheduleAutoRefresh(); + } + + async function login(username, password) { + setError(""); + setLastInfo(""); + setStatus("loading"); + + const res = await apiFetch("/api/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + const text = await res.text().catch(() => ""); + let j = null; + try { + j = text ? JSON.parse(text) : null; + } catch { + j = null; + } + + if (!res.ok || !j?.access_token) { + const parsed = parseAuthError(text || "", res.status); + setNeedsEmailVerification(parsed.code === "email_verification_required"); + setAnon(parsed.message || j?.error_description || j?.error || text || "Login failed"); + return { ok: false, status: res.status, json: j, text }; + } + + const t = { ...j, obtained_at: Date.now() }; + setAuthFromTokens(t); + scheduleAutoRefresh(); + + const w = await whoami(t.access_token); + if (!w.ok || !w.authenticated) { + setAnon("Login desynced. Please login again."); + return { ok: false, status: w.status, json: w.json, text: w.text }; + } + + setUser(w.username ? { username: w.username } : null); + setStatus("auth"); + + // ✅ update verified state from backend + await refreshProfile(t.access_token); + + return { ok: true }; + } + + function logout() { + if (refreshTimer.current) clearInterval(refreshTimer.current); + refreshTimer.current = null; + setAnon(""); + } + + async function resendVerificationEmail() { + const at = tokens?.access_token || ""; + if (!at) { + setError("Not logged in."); + return { ok: false }; + } + + setError(""); + setLastInfo(""); + + const r = await resendVerification(at); + if (!r.ok) { + setError(r.json?.error || r.json?.message || r.text || `Resend failed (${r.status})`); + return { ok: false, status: r.status }; + } + + setLastInfo("Verification email sent. Check inbox/spam, then refresh session (logout/login)."); + return { ok: true }; + } + + useEffect(() => { + restore(); + const onStorage = (e) => { + if (e.key === "sociowire:auth") restore(); + }; + window.addEventListener("storage", onStorage); + return () => window.removeEventListener("storage", onStorage); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Backward-compatible fields expected by TopBar/MapView + const value = useMemo(() => { + const loading = status === "loading"; + const authenticated = status === "auth"; + const username = user?.username || ""; + const token = tokens?.access_token || ""; + + return { + // internal + status, + user, + tokens, + error, + + // expected by UI + loading, + authenticated, + username, + token, + + lastError: error || "", + lastInfo: lastInfo || "", + needsEmailVerification: !!needsEmailVerification, + resendVerificationEmail, + + login, + logout, + restore, + refreshProfile, + setError, + setLastInfo, + setNeedsEmailVerification, + }; + }, [status, user, tokens, error, lastInfo, needsEmailVerification]); + + return {children}; +} + +export function useAuth() { + return useContext(AuthCtx); +} diff --git a/src/components/Layout/TopBar.jsx b/src/components/Layout/TopBar.jsx index ddc7d39..8635cbc 100644 --- a/src/components/Layout/TopBar.jsx +++ b/src/components/Layout/TopBar.jsx @@ -4,12 +4,66 @@ import { registerUser } from "../../api/client"; import AuthToast from "../Auth/AuthToast"; const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" }; +const LAST_SIGNUP_KEY = "sociowire:lastSignup"; 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() { + // Support both: + // - /?verified=1&email=... + // - /#/something?verified=1&email=... + 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; + }; + + const hasAny = (...keys) => any(keys, qs, hs) + + return { qs, hs, get, hash }; +} + +function any(keys, qs, hs){ + for (const k of keys){ + if (qs.has(k) || hs.has(k)) return true; + } + return false; +} + export default function TopBar({ theme = "dark", onChangeTheme }) { const { loading, @@ -18,9 +72,7 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { login, logout, needsEmailVerification, - openKeycloak, lastError, - lastErrorCode, lastInfo, clearError, clearInfo, @@ -45,16 +97,17 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { setMode(nextMode); setShowAuth(true); setSignupError(""); - setSignupInfo(""); + // keep signupInfo (so verify/signup notice stays visible) }; const closeModal = () => { setShowAuth(false); setLoginPass(""); setSignupError(""); - setSignupInfo(""); + // keep signupInfo }; + // Close auth modal automatically when logged in useEffect(() => { if (authenticated) setShowAuth(false); }, [authenticated]); @@ -62,21 +115,47 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { /* SW_URL_NOTICE:BEGIN */ useEffect(() => { try { - const qs = new URLSearchParams(window.location.search || ""); - const verified = qs.get("verified") || qs.get("email_verified"); - const signup = qs.get("signup"); - const notice = qs.get("notice"); + const { get, qs, hs, hash } = parseMergedParams(); - if (verified === "1" || verified === "true") { - setSignupInfo("✅ Email confirmé. Tu peux te connecter."); + const verified = + get("verified") || + get("email_verified") || + get("emailVerified") || + get("kc_verified"); + + const signup = get("signup"); + const notice = get("notice"); + + const email = + get("email") || + get("user_email") || + get("mail"); + + const uname = + get("username") || + get("user") || + get("preferred_username") || + get("login"); + + const last = readLastSignup(); + + if (truthyParam(verified)) { + const who = (email || uname || last?.email || last?.username || "").trim(); + setSignupInfo(`✅ Email confirmé${who ? ` (${who})` : ""}. Tu peux te connecter.`); setMode("login"); setShowAuth(true); - } else if (signup === "1" || signup === "true") { - setSignupInfo("✅ Compte créé. Vérifie tes emails (et le spam), puis connecte-toi."); + + const prefill = (uname || email || last?.username || last?.email || "").trim(); + if (prefill) setLoginUser(prefill); + } else if (truthyParam(signup)) { + const who = (email || uname || last?.email || last?.username || "").trim(); + setSignupInfo(`✅ Compte créé${who ? ` (${who})` : ""}. Vérifie tes emails (et le spam), puis connecte-toi.`); setMode("login"); setShowAuth(true); + + const prefill = (uname || email || last?.username || last?.email || "").trim(); + if (prefill) setLoginUser(prefill); } else if (notice) { - // allow custom notice (URL-encoded) const msg = decodeURIComponent(notice); if (msg && msg.trim()) { setSignupInfo(msg.trim()); @@ -85,18 +164,22 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { } } - // clean URL (remove params without reload) - if (verified || signup || notice) { - const clean = window.location.pathname + (window.location.hash || ""); + // clean URL (remove params without reload) — both search and hash query + const touched = + qs.has("verified") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") || + hs.has("verified") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice"); + + if (touched) { + const cleanHash = hash.includes("?") ? hash.split("?")[0] : hash; + const clean = window.location.pathname + cleanHash; window.history.replaceState({}, "", clean); } -} catch (e) { + } catch (e) { // ignore } }, []); /* SW_URL_NOTICE:END */ - const handleLoginSubmit = async (e) => { e.preventDefault(); await login(loginUser, loginPass); @@ -115,17 +198,25 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { try { setSignupLoading(true); + const u = regUser.trim(); + const em = regEmail.trim(); + await registerUser({ - username: regUser.trim(), - email: regEmail.trim(), + username: u, + email: em, password: regPass, firstName: firstName.trim(), lastName: lastName.trim(), }); - setSignupInfo("Account created. Check your inbox (spam) to verify your email, then login."); + // Remember for the "verified" return + try { + localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em })); + } catch {} + + setSignupInfo(`✅ Compte créé (${em}). Vérifie tes emails (spam aussi), puis connecte-toi.`); setMode("login"); - setLoginUser(regUser.trim()); + setLoginUser(u || em); setLoginPass(""); } catch (err) { console.error(err); @@ -150,90 +241,85 @@ export default function TopBar({ theme = "dark", onChangeTheme }) { return ( <> {/* Toast area (under topbar) */} - {(toast?.message) && ( + {toast?.message ? (
{ - if (lastError) clearError(); - if (lastInfo) clearInfo(); + if (lastError && typeof clearError === "function") clearError(); + if (lastInfo && typeof clearInfo === "function") clearInfo(); }} /> - {needsEmailVerification && ( -
- -
- )}
- )} + ) : null}
-
-
SW
-
-
SOCIOWIRE.com
-
Wired to life
+
+
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
+
+ + {/* Theme dots UNDER the brand (prevents topbar wrapping / overflow) */} +
+
+ {["dark", "blue", "light"].map((t) => ( + + ))} +
-
- {["dark", "blue", "light"].map((t) => ( - - ))} -
- {authenticated ? ( -
-
-
- {initialLetter(username)} -
-
-
Connecté
-
@{username || "user"}
-
+
+
+ {initialLetter(username)} +
+
+
Online
+
@{username || "user"}
-
) : ( -
-
-
- -
-
-
{loading ? "Loading…" : "Guest"}
-
anon
-
+
+
+ +
+
+
{loading ? "Loading…" : "Guest"}
+
anon
- - -
)} + + {/* Single button: Logon / Logoff (same spot, same style) */} +
- {showAuth && ( + {showAuth ? (
e.stopPropagation()}>
@@ -256,18 +342,18 @@ export default function TopBar({ theme = "dark", onChangeTheme }) {
- {/* SW_MODAL_NOTICE:BEGIN */} - {signupInfo ? ( -
- {signupInfo} + {/* Pretty notices inside modal */} + {signupInfo ?
{signupInfo}
: null} + {mode === "login" && lastError ? ( +
+ {lastError}
) : null} - {/* SW_MODAL_NOTICE:END */} {mode === "login" ? (
- )} + ) : null} ); } diff --git a/src/components/Layout/TopBar.jsx.bak.1766217948 b/src/components/Layout/TopBar.jsx.bak.1766217948 new file mode 100644 index 0000000..4fc365e --- /dev/null +++ b/src/components/Layout/TopBar.jsx.bak.1766217948 @@ -0,0 +1,392 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { useAuth } from "../../auth/AuthContext"; +import { registerUser } from "../../api/client"; +import AuthToast from "../Auth/AuthToast"; + +const THEME_LABELS = { dark: "Dark", blue: "Blue", light: "Light" }; +const LAST_EMAIL_KEY = "sociowire:lastEmail"; + +function initialLetter(name = "") { + const s = (name || "").trim(); + return s ? s[0].toUpperCase() : "G"; +} + +function looksLikeEmail(s = "") { + return typeof s === "string" && /.+@.+\..+/.test(s.trim()); +} + +export default function TopBar({ theme = "dark", onChangeTheme }) { + const { + loading, + authenticated, + username, + login, + logout, + needsEmailVerification, + resendVerificationEmail, + lastError, + lastInfo, + setError, + setLastInfo, + } = useAuth(); + + const clearError = () => setError && setError(""); + const clearInfo = () => setLastInfo && setLastInfo(""); + + const [showAuth, setShowAuth] = useState(false); + const [mode, setMode] = useState("login"); + + 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 openModal = (nextMode) => { + setMode(nextMode); + setShowAuth(true); + setSignupError(""); + setSignupInfo(""); + + // Prefill login with last known email (nice for email-verified return) + if (nextMode === "login") { + try { + const last = localStorage.getItem(LAST_EMAIL_KEY) || ""; + if (!loginUser.trim() && last.trim()) setLoginUser(last.trim()); + } catch {} + } + }; + + const closeModal = () => { + setShowAuth(false); + setLoginPass(""); + setSignupError(""); + setSignupInfo(""); + }; + + useEffect(() => { + if (authenticated) setShowAuth(false); + }, [authenticated]); + + /* SW_URL_NOTICE:BEGIN */ + useEffect(() => { + try { + const qs = new URLSearchParams(window.location.search || ""); + const verified = qs.get("verified") || qs.get("email_verified"); + const signup = qs.get("signup"); + const notice = qs.get("notice"); + const email = (qs.get("email") || qs.get("user") || qs.get("username") || "").trim(); + + if (email) { + try { localStorage.setItem(LAST_EMAIL_KEY, email); } catch {} + // If user uses email to login, prefill it + if (!loginUser.trim()) setLoginUser(email); + if (!regEmail.trim()) setRegEmail(email); + } + + if (verified === "1" || verified === "true") { + setSignupInfo(`✅ Email confirmé${email ? ` (${email})` : ""}. Tu peux te connecter.`); + setMode("login"); + setShowAuth(true); + } else if (signup === "1" || signup === "true") { + setSignupInfo("✅ Compte créé. Vérifie tes emails (et le spam), puis connecte-toi."); + setMode("login"); + setShowAuth(true); + } else if (notice) { + const msg = decodeURIComponent(notice); + if (msg && msg.trim()) { + setSignupInfo(msg.trim()); + setMode("login"); + setShowAuth(true); + } + } + + // clean URL (remove params without reload) + if (verified || signup || notice || email) { + const clean = window.location.pathname + (window.location.hash || ""); + window.history.replaceState({}, "", clean); + } + } catch (e) { + // ignore + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + /* SW_URL_NOTICE:END */ + + const handleLoginSubmit = async (e) => { + e.preventDefault(); + // store email-like login for next time + try { + if (looksLikeEmail(loginUser)) localStorage.setItem(LAST_EMAIL_KEY, loginUser.trim()); + } catch {} + await login(loginUser, loginPass); + }; + + const handleSignupSubmit = async (e) => { + e.preventDefault(); + setSignupError(""); + setSignupInfo(""); + + if (!firstName.trim() || !lastName.trim() || !regUser.trim() || !regEmail.trim() || !regPass) { + setSignupError("First name, last name, username, email and password are required."); + return; + } + + try { + setSignupLoading(true); + + await registerUser({ + username: regUser.trim(), + email: regEmail.trim(), + password: regPass, + firstName: firstName.trim(), + lastName: lastName.trim(), + }); + + try { localStorage.setItem(LAST_EMAIL_KEY, regEmail.trim()); } catch {} + + setSignupInfo("✅ Account created. Check your inbox (spam) to verify your email, then login."); + setMode("login"); + + // Some people login with email, some with username => prefer email if present + setLoginUser((regEmail.trim() || regUser.trim())); + setLoginPass(""); + } catch (err) { + console.error(err); + setSignupError(err.message || "Registration error."); + } finally { + setSignupLoading(false); + } + }; + + const toast = useMemo(() => { + if (lastError) { + const t = needsEmailVerification ? "warn" : "error"; + const title = needsEmailVerification ? "Vérification email requise" : "Connexion impossible"; + return { type: t, title, message: lastError }; + } + if (lastInfo) return { type: "success", title: "", message: lastInfo }; + return null; + }, [lastError, lastInfo, needsEmailVerification]); + + const authComboLabel = authenticated ? "Log off" : (loading ? "..." : "Log on"); + const authLine = authenticated ? "Online" : "Guest"; + const authHandle = authenticated ? `@${username || "user"}` : "anon"; + + return ( + <> + {/* Toast area (under topbar) */} + {(toast?.message) && ( +
+ { + if (lastError) clearError(); + if (lastInfo) clearInfo(); + }} + /> + + {needsEmailVerification && ( +
+ + + +
+ )} +
+ )} + +
+
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
+
+ +
+
+ {["dark", "blue", "light"].map((t) => ( + + ))} +
+ + {/* ONE unified auth button (guest online / log on / log off) */} + + + {!authenticated && ( + + )} +
+
+ + {showAuth && ( +
+
e.stopPropagation()}> +
+ + + +
+ + {/* Nice notices (modal) */} + {signupInfo ?
{signupInfo}
: null} + + {mode === "login" && needsEmailVerification ? ( +
+ ⚠️ Ton compte doit confirmer l’email avant de se connecter. Vérifie ta boîte mail (et le spam), + clique le lien, puis reviens ici. +
+ ) : null} + + {mode === "login" && lastError ? ( +
{lastError}
+ ) : null} + + {mode === "login" && lastInfo ? ( +
{lastInfo}
+ ) : null} + + {mode === "login" ? ( + + + + + + + + ) : ( +
+
+ + +
+ + + + + + + + {signupError ?
{signupError}
: null} + {signupInfo ?
{signupInfo}
: null} + + +
+ )} +
+
+ )} + + ); +} diff --git a/src/styles/auth-modal.css b/src/styles/auth-modal.css index 696056f..27f49ae 100644 --- a/src/styles/auth-modal.css +++ b/src/styles/auth-modal.css @@ -96,3 +96,80 @@ body[data-theme="light"] .auth-form input { .auth-modal { padding:.95rem .9rem 1.1rem; } .auth-row { flex-direction:column; } } + +/* SW_AUTH_NOTICE:BEGIN */ +.auth-notice{ + margin: .2rem 0 .65rem; + padding: .55rem .7rem; + border-radius: 14px; + font-size: .82rem; + font-weight: 800; + line-height: 1.2; + border: 1px solid rgba(148,163,184,.35); + background: rgba(2,6,23,.22); + color: #e5e7eb; +} + +.auth-notice-success{ + background: rgba(34,197,94,0.14); + border-color: rgba(34,197,94,0.55); + color: #d1fae5; +} + +.auth-notice-error{ + background: rgba(248,113,113,0.14); + border-color: rgba(248,113,113,0.55); + color: #ffe4e6; +} + +.auth-notice-warn{ + background: rgba(251,191,36,0.12); + border-color: rgba(251,191,36,0.40); + color: #fffbeb; +} + +body[data-theme="light"] .auth-notice{ + background: rgba(255,255,255,0.92); + border-color: rgba(148,163,184,0.75); + color: #0b1220; +} +body[data-theme="light"] .auth-notice-success{ color:#0b1220; } +body[data-theme="light"] .auth-notice-error{ color:#0b1220; } +body[data-theme="light"] .auth-notice-warn{ color:#0b1220; } +/* SW_AUTH_NOTICE:END */ + + +/* SW_AUTH_MODAL_NOTICE_POLISH:BEGIN */ +.auth-notice{ + margin: 0 0 .65rem 0; + padding: .65rem .75rem; + border-radius: 14px; + font-size: .82rem; + font-weight: 800; + line-height: 1.2; + border: 1px solid rgba(148,163,184,.35); + background: rgba(2,6,23,.22); + color: #e5e7eb; +} + +.auth-notice-success{ + background: rgba(34,197,94,0.14); + border-color: rgba(34,197,94,0.55); + color: #d1fae5; +} +.auth-notice-error{ + background: rgba(248,113,113,0.14); + border-color: rgba(248,113,113,0.55); + color: #ffe4e6; +} +.auth-notice-warn{ + background: rgba(251,191,36,0.12); + border-color: rgba(251,191,36,0.40); + color: #fffbeb; +} +.auth-notice-info{ + background: rgba(56,189,248,0.14); + border-color: rgba(56,189,248,0.55); + color: #e0f2fe; +} +/* SW_AUTH_MODAL_NOTICE_POLISH:END */ diff --git a/src/styles/auth-modal.css.bak.1766217948 b/src/styles/auth-modal.css.bak.1766217948 new file mode 100644 index 0000000..7b18c9a --- /dev/null +++ b/src/styles/auth-modal.css.bak.1766217948 @@ -0,0 +1,139 @@ +.auth-modal-backdrop { + position: fixed; + inset: 0; + z-index: 60; + display: flex; + align-items: center; + justify-content: center; + background: rgba(2, 6, 23, 0.65); + backdrop-filter: blur(8px); +} + +.auth-modal { + width: min(640px, 95vw); + border-radius: 22px; + padding: 1.1rem 1.25rem 1.35rem; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.85); + border: 1px solid rgba(148, 163, 184, 0.7); +} + +body[data-theme="dark"] .auth-modal { + background: radial-gradient(circle at top left, #0f172a, #020617 70%); +} +body[data-theme="blue"] .auth-modal { + background: radial-gradient(circle at top left, #0ea5e9, #020617 65%); + border-color: rgba(96, 165, 250, 0.9); +} +body[data-theme="light"] .auth-modal { + background: radial-gradient(circle at top left, #ffffff, #e5e7eb 70%); + color: #020617; + border-color: rgba(148, 163, 184, 0.7); +} + +.auth-modal-header { display:flex; align-items:center; gap:.5rem; margin-bottom:.9rem; } + +.auth-tab { + padding: .45rem .95rem; + border-radius: 999px; + border: 1px solid transparent; + background: transparent; + font-size: .85rem; + color: #e5e7eb; + cursor: pointer; +} +.auth-tab-active { + background: linear-gradient(135deg, #1d4ed8, #38bdf8); + border-color: rgba(191, 219, 254, 0.9); + color: #f9fafb; +} +body[data-theme="light"] .auth-tab { color:#0f172a; } +body[data-theme="light"] .auth-tab-active { color:#f9fafb; } + +.auth-close { + margin-left:auto; + width:32px; height:32px; + border-radius:999px; + border:1px solid rgba(148, 163, 184, 0.6); + background: rgba(15, 23, 42, 0.85); + color:#e5e7eb; + font-size:.8rem; + cursor:pointer; +} +body[data-theme="light"] .auth-close { background:#e5e7eb; color:#020617; } + +.auth-form { display:flex; flex-direction:column; gap:.7rem; } +.auth-row { display:flex; gap:.6rem; } +.auth-row label { flex:1; } +.auth-form label { display:flex; flex-direction:column; font-size:.8rem; gap:.25rem; } + +.auth-form input { + border-radius: 999px; + border: 1px solid rgba(51, 65, 85, 0.9); + background: rgba(15, 23, 42, 0.96); + color: #e5e7eb; + padding: .55rem .85rem; + font-size: .85rem; +} +body[data-theme="light"] .auth-form input { + background:#f9fafb; + color:#020617; + border-color: rgba(148, 163, 184, 0.9); +} + +.btn-full { width:100%; margin-top:.4rem; } + +.auth-error { + margin-top:.25rem; + font-size:.8rem; + color:#fecaca; + background: rgba(127, 29, 29, 0.55); + border-radius:.5rem; + padding:.4rem .6rem; + border: 1px solid rgba(248, 113, 113, 0.7); +} + +@media (max-width:480px){ + .auth-modal { padding:.95rem .9rem 1.1rem; } + .auth-row { flex-direction:column; } +} + +/* SW_AUTH_NOTICE:BEGIN */ +.auth-notice{ + margin: .2rem 0 .65rem; + padding: .55rem .7rem; + border-radius: 14px; + font-size: .82rem; + font-weight: 800; + line-height: 1.2; + border: 1px solid rgba(148,163,184,.35); + background: rgba(2,6,23,.22); + color: #e5e7eb; +} + +.auth-notice-success{ + background: rgba(34,197,94,0.14); + border-color: rgba(34,197,94,0.55); + color: #d1fae5; +} + +.auth-notice-error{ + background: rgba(248,113,113,0.14); + border-color: rgba(248,113,113,0.55); + color: #ffe4e6; +} + +.auth-notice-warn{ + background: rgba(251,191,36,0.12); + border-color: rgba(251,191,36,0.40); + color: #fffbeb; +} + +body[data-theme="light"] .auth-notice{ + background: rgba(255,255,255,0.92); + border-color: rgba(148,163,184,0.75); + color: #0b1220; +} +body[data-theme="light"] .auth-notice-success{ color:#0b1220; } +body[data-theme="light"] .auth-notice-error{ color:#0b1220; } +body[data-theme="light"] .auth-notice-warn{ color:#0b1220; } +/* SW_AUTH_NOTICE:END */ diff --git a/src/styles/auth-toast.css b/src/styles/auth-toast.css index f0daa28..232aa31 100644 --- a/src/styles/auth-toast.css +++ b/src/styles/auth-toast.css @@ -91,3 +91,14 @@ body[data-theme="light"] .sw-toast{ } body[data-theme="light"] .sw-toast-icon{ background: rgba(255,255,255,.95); } body[data-theme="light"] .sw-toast-x{ background: rgba(255,255,255,.95); } + +/* SW_TOAST_SUBACTIONS:BEGIN */ +.sw-toast-subactions{ + max-width: 980px; + margin: 10px auto 0; + display:flex; + gap:10px; + justify-content:center; + pointer-events: auto; +} +/* SW_TOAST_SUBACTIONS:END */ diff --git a/src/styles/topbar.css b/src/styles/topbar.css index 5926e5b..e661632 100644 --- a/src/styles/topbar.css +++ b/src/styles/topbar.css @@ -340,3 +340,151 @@ body[data-theme="light"] .sw-avatar{ } /* SW_TOPBAR_POLISH:END */ + +/* SW_TOPBAR_OVERFLOW_FIX:BEGIN */ +.topbar{ + flex-wrap: wrap; /* ✅ allow wrap instead of overflowing right */ + row-gap: .45rem; +} + +.topbar-left{ + display:flex; + align-items:center; + gap: .65rem; + min-width: 0; /* ✅ allow shrinking */ +} + +.title-block{ min-width:0; } +.main-title, .sub-title{ + max-width: 58vw; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.topbar-right{ + min-width: 0; + max-width: 100%; + flex: 1 1 auto; + justify-content: flex-end; + flex-wrap: wrap; /* ✅ wrap buttons instead of clipping */ + gap: .45rem; +} + +@media (max-width:640px){ + .main-title, .sub-title{ max-width: 86vw; } + .topbar-right{ + justify-content: flex-start; + width: 100%; + } +} + +/* Unified auth button (guest online / log on / log off) */ +.sw-auth-combo{ + display:flex; + align-items:center; + gap:.5rem; + padding:.25rem .55rem; + border-radius: 999px; + border: 1px solid rgba(148,163,184,.55); + background: rgba(2,6,23,.22); + color: #e5e7eb; + cursor:pointer; + max-width: 100%; + box-shadow: 0 4px 14px rgba(0,0,0,.22); +} + +.sw-auth-combo .sw-usertext{ + min-width: 0; +} + +.sw-auth-combo .sw-userhandle{ + max-width: 120px; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sw-auth-action{ + margin-left: .25rem; + padding: .22rem .55rem; + border-radius: 999px; + font-weight: 950; + font-size: .70rem; + border: 1px solid rgba(56,189,248,.35); + background: rgba(56,189,248,.14); + color: #dbeafe; + white-space: nowrap; +} + +.sw-auth-combo--on .sw-auth-action{ + border-color: rgba(34,197,94,.45); + background: rgba(34,197,94,.16); + color: #d1fae5; +} +/* SW_TOPBAR_OVERFLOW_FIX:END */ + + +/* SW_TOPBAR_ONE_LINE_FIX:BEGIN */ +/* Keep the header on ONE ROW (no wrapping to a second line) */ +.topbar{ + flex-wrap: nowrap; + align-items: center; +} + +/* Left brand stack (logo + title, with theme dots underneath) */ +.brand-stack{ + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; +} + +.brand-toprow{ + display: flex; + align-items: center; + gap: .65rem; + min-width: 0; +} + +.brand-under{ + display: flex; + align-items: center; + gap: .5rem; + padding-left: 46px; /* aligns under title block visually */ +} + +/* Prevent title from pushing the right side off-screen */ +.title-block{ min-width: 0; } +.main-title{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: min(360px, 48vw); +} +.sub-title{ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: min(360px, 48vw); +} + +/* Right side never wraps */ +.topbar-right{ + flex-wrap: nowrap !important; + white-space: nowrap; + min-width: 0; + max-width: 52vw; + overflow: hidden; +} + +/* Make user handle truncate instead of forcing width */ +.sw-userhandle{ max-width: 140px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sw-userchip{ min-width: 0; } + +@media (max-width: 640px){ + .main-title, .sub-title{ max-width: 62vw; } + .topbar-right{ max-width: 45vw; } + .brand-under{ padding-left: 0; } +} +/* SW_TOPBAR_ONE_LINE_FIX:END */ diff --git a/src/styles/topbar.css.bak.1766217948 b/src/styles/topbar.css.bak.1766217948 new file mode 100644 index 0000000..af2ccea --- /dev/null +++ b/src/styles/topbar.css.bak.1766217948 @@ -0,0 +1,425 @@ +.topbar{ + position: sticky; top:0; z-index:100; + width:100%; + padding: .55rem .9rem; + display:flex; align-items:center; justify-content:space-between; gap:.6rem; + background: linear-gradient(135deg,#071225 0%,#0b2b5f 38%,#0e65c0 72%,#27a8ff 100%); + border-bottom-left-radius:18px; border-bottom-right-radius:18px; + box-shadow: 0 10px 24px rgba(0,0,0,.55), 0 0 18px rgba(56,189,248,.45); + overflow:hidden; +} + +.logo-circle{ + width:40px; height:40px; border-radius:50%; flex-shrink:0; + background-image:url("/icons/logo-master.png"); + background-size:cover; background-position:center; background-repeat:no-repeat; + box-shadow: 0 0 10px rgba(37,99,235,.8), 0 0 18px rgba(56,189,248,.6); + color:transparent; +} + +.title-block{ display:flex; flex-direction:column; line-height:1.05; min-width:0; } +.main-title{ font-size:1.02rem; font-weight:800; letter-spacing:.04em; color:#f9fafb; } +.sub-title{ font-size:.74rem; color:#dbeafe; opacity:.95; } + +.topbar-right{ display:flex; align-items:center; gap:.5rem; margin-left:auto; flex-wrap:wrap; } + +.theme-switch{ display:flex; gap:.28rem; } +.theme-dot{ + width:22px; height:22px; border-radius:999px; + border:1px solid rgba(148,163,184,.85); + background:rgba(15,23,42,.9); color:#e5e7eb; font-size:.65rem; + display:flex; align-items:center; justify-content:center; padding:0; cursor:pointer; + box-shadow:0 0 6px rgba(15,23,42,.9); transition:transform 0.36s ease; +} +.theme-dot:hover{ transform:scale(1.06); } +.theme-dot-active{ border-color:#facc15; box-shadow:0 0 10px rgba(250,204,21,.9),0 0 14px rgba(56,189,248,.55); } + +.btn-primary, .btn-secondary{ + padding:.32rem .85rem; font-size:.78rem; border-radius:999px; font-weight:700; cursor:pointer; + white-space:nowrap; line-height:1; +} +.btn-primary{ + border:1px solid #22c55e; + background:radial-gradient(circle at 30% 30%,#22c55e,#16a34a); + color:#f9fafb; + box-shadow:0 4px 10px rgba(22,163,74,.5),0 0 8px rgba(22,163,74,.6); +} +.btn-primary:disabled{ opacity:.6; cursor:default; } +.btn-secondary{ + border:1px solid #bfdbfe; + background:rgba(15,23,42,.25); + color:#e0f2fe; + box-shadow:0 3px 10px rgba(15,23,42,.6); +} +.btn-secondary:hover{ background:rgba(15,23,42,.45); } + +@media (max-width:640px){ + .topbar{ padding:.5rem .7rem; border-bottom-left-radius:16px; border-bottom-right-radius:16px; } + .main-title{ font-size:1rem; } + .sub-title{ font-size:.7rem; } + .topbar-right{ width:100%; justify-content:flex-start; gap:.45rem; } +} + +/* SW_AUTH_UI:BEGIN */ +.auth-strip{ + margin-top: 6px; + display:flex; + align-items:center; + gap: .45rem; + flex-wrap: wrap; +} + +.auth-pill{ + display:inline-flex; + align-items:center; + gap: .35rem; + padding: .18rem .55rem; + border-radius: 999px; + font-size: .70rem; + font-weight: 900; + letter-spacing: .02em; + border: 1px solid rgba(148,163,184,.55); + background: rgba(2,6,23,.25); + color: #e5e7eb; +} +.auth-pill i{ font-size: .85rem; opacity:.95; } + +.auth-pill--ok{ + border-color: rgba(34,197,94,.65); + background: rgba(34,197,94,.16); +} +.auth-pill--loading{ + border-color: rgba(56,189,248,.65); + background: rgba(56,189,248,.14); +} +.auth-pill--anon{ + border-color: rgba(148,163,184,.55); + background: rgba(2,6,23,.20); +} + +.auth-banner{ + margin-top: 6px; + padding: 6px 10px; + border-radius: 14px; + font-size: .72rem; + font-weight: 800; + display:flex; + gap: 8px; + align-items:center; + flex-wrap:wrap; +} +.auth-banner--error{ + background: rgba(248,113,113,0.14); + border: 1px solid rgba(248,113,113,0.55); + color: #ffe4e6; +} +.auth-banner--info{ + background: rgba(56,189,248,0.14); + border: 1px solid rgba(56,189,248,0.55); + color: #e0f2fe; +} +.auth-banner--success{ + background: rgba(34,197,94,0.14); + border: 1px solid rgba(34,197,94,0.55); + color: #d1fae5; +} + +.user-chip{ + display:inline-flex; + align-items:center; + gap: .45rem; + padding: .22rem .55rem; + border-radius: 999px; + background: rgba(2,6,23,.25); + border: 1px solid rgba(148,163,184,.55); +} +.user-avatar{ + width: 26px; + height: 26px; + border-radius: 999px; + display:flex; + align-items:center; + justify-content:center; + background: rgba(56,189,248,0.18); + border: 1px solid rgba(56,189,248,0.35); +} +.user-avatar i{ font-size: 14px; } + +.user-name{ + font-size: .80rem; + font-weight: 950; + color: #f9fafb; + max-width: 220px; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Light theme readability */ +body[data-theme="light"] .auth-pill{ + background: rgba(255,255,255,0.82); + border-color: rgba(148,163,184,0.85); + color: #0b1220; +} +body[data-theme="light"] .user-chip{ + background: rgba(255,255,255,0.82); + border-color: rgba(148,163,184,0.85); +} +body[data-theme="light"] .user-name{ color:#0b1220; } +body[data-theme="light"] .user-avatar{ + background: rgba(30,107,255,0.12); + border-color: rgba(30,107,255,0.35); + color:#0b1220; +} +/* SW_AUTH_UI:END */ + +/* SW_AUTH_UI2:BEGIN */ +.sw-notice{ + margin: 10px 14px 0 14px; + padding: 10px 12px; + border-radius: 16px; + font-size: .78rem; + font-weight: 800; + line-height: 1.2; + border: 1px solid rgba(148,163,184,.35); + background: rgba(2,6,23,.22); + color: #e5e7eb; +} +.sw-notice--error{ + background: rgba(248,113,113,0.14); + border-color: rgba(248,113,113,0.55); + color: #ffe4e6; +} +.sw-notice--info{ + background: rgba(56,189,248,0.14); + border-color: rgba(56,189,248,0.55); + color: #e0f2fe; +} +.sw-notice--success{ + background: rgba(34,197,94,0.14); + border-color: rgba(34,197,94,0.55); + color: #d1fae5; +} +.sw-notice--warn{ + background: rgba(251,191,36,0.12); + border-color: rgba(251,191,36,0.40); + color: #fffbeb; +} + +/* single user chip (no duplicates) */ +.user-chip{ + display:inline-flex; + align-items:center; + gap: .45rem; + padding: .22rem .55rem; + border-radius: 999px; + background: rgba(2,6,23,.25); + border: 1px solid rgba(148,163,184,.55); +} +.user-avatar{ + width: 26px; + height: 26px; + border-radius: 999px; + display:flex; + align-items:center; + justify-content:center; + background: rgba(56,189,248,0.18); + border: 1px solid rgba(56,189,248,0.35); +} +.user-avatar i{ font-size: 14px; } +.user-name{ + font-size: .82rem; + font-weight: 950; + color: #f9fafb; + max-width: 180px; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.user-badge{ + display:inline-flex; + align-items:center; + gap: .35rem; + padding: .10rem .45rem; + border-radius: 999px; + font-size: .70rem; + font-weight: 950; + border: 1px solid rgba(34,197,94,.55); + background: rgba(34,197,94,.14); + color: #d1fae5; +} +/* SW_AUTH_UI2:END */ + + +/* SW_TOPBAR_POLISH:BEGIN */ + +/* Make right side look like a clean “glass dock” */ +.topbar-right{ + padding: .28rem .35rem; + border-radius: 999px; + background: rgba(2,6,23,.18); + border: 1px solid rgba(255,255,255,.14); + backdrop-filter: blur(10px); +} + +/* User chip used by TopBar.jsx (missing styles before) */ +.sw-userchip{ + display:flex; + align-items:center; + gap:.5rem; + padding:.28rem .55rem; + border-radius: 999px; + background: rgba(2,6,23,.22); + border: 1px solid rgba(148,163,184,.45); + box-shadow: 0 4px 14px rgba(0,0,0,.25); +} + +.sw-avatar{ + width: 30px; + height: 30px; + border-radius: 999px; + display:grid; + place-items:center; + background: rgba(56,189,248,0.18); + border: 1px solid rgba(56,189,248,0.35); + color: #e5e7eb; + font-weight: 950; + overflow:hidden; +} +.sw-avatar i{ font-size: 14px; } + +.sw-usertext{ + display:flex; + flex-direction:column; + line-height: 1.05; + min-width: 0; +} +.sw-userline{ + font-size: .72rem; + font-weight: 950; + color: #f9fafb; +} +.sw-userhandle{ + font-size: .68rem; + font-weight: 800; + color: #dbeafe; + opacity: .92; + max-width: 160px; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Mobile: dock becomes full width and nice */ +@media (max-width:640px){ + .topbar-right{ + width:100%; + border-radius: 16px; + justify-content: flex-start; + } +} + +/* Light theme readability */ +body[data-theme="light"] .topbar-right{ + background: rgba(255,255,255,.55); + border-color: rgba(148,163,184,.55); +} +body[data-theme="light"] .sw-userchip{ + background: rgba(255,255,255,.80); + border-color: rgba(148,163,184,.70); + color: #0b1220; +} +body[data-theme="light"] .sw-userline, +body[data-theme="light"] .sw-userhandle{ + color:#0b1220; +} +body[data-theme="light"] .sw-avatar{ + background: rgba(30,107,255,0.10); + border-color: rgba(30,107,255,0.35); + color:#0b1220; +} + +/* SW_TOPBAR_POLISH:END */ + +/* SW_TOPBAR_OVERFLOW_FIX:BEGIN */ +.topbar{ + flex-wrap: wrap; /* ✅ allow wrap instead of overflowing right */ + row-gap: .45rem; +} + +.topbar-left{ + display:flex; + align-items:center; + gap: .65rem; + min-width: 0; /* ✅ allow shrinking */ +} + +.title-block{ min-width:0; } +.main-title, .sub-title{ + max-width: 58vw; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.topbar-right{ + min-width: 0; + max-width: 100%; + flex: 1 1 auto; + justify-content: flex-end; + flex-wrap: wrap; /* ✅ wrap buttons instead of clipping */ + gap: .45rem; +} + +@media (max-width:640px){ + .main-title, .sub-title{ max-width: 86vw; } + .topbar-right{ + justify-content: flex-start; + width: 100%; + } +} + +/* Unified auth button (guest online / log on / log off) */ +.sw-auth-combo{ + display:flex; + align-items:center; + gap:.5rem; + padding:.25rem .55rem; + border-radius: 999px; + border: 1px solid rgba(148,163,184,.55); + background: rgba(2,6,23,.22); + color: #e5e7eb; + cursor:pointer; + max-width: 100%; + box-shadow: 0 4px 14px rgba(0,0,0,.22); +} + +.sw-auth-combo .sw-usertext{ + min-width: 0; +} + +.sw-auth-combo .sw-userhandle{ + max-width: 120px; + overflow:hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sw-auth-action{ + margin-left: .25rem; + padding: .22rem .55rem; + border-radius: 999px; + font-weight: 950; + font-size: .70rem; + border: 1px solid rgba(56,189,248,.35); + background: rgba(56,189,248,.14); + color: #dbeafe; + white-space: nowrap; +} + +.sw-auth-combo--on .sw-auth-action{ + border-color: rgba(34,197,94,.45); + background: rgba(34,197,94,.16); + color: #d1fae5; +} +/* SW_TOPBAR_OVERFLOW_FIX:END */