feat: implement account management and subscription payment center components

This commit is contained in:
2569718930@qq.com
2026-05-24 23:07:37 +08:00
parent 79a652e8a9
commit 45e9c28437
12 changed files with 480 additions and 333 deletions
@@ -64,47 +64,47 @@ export function SubscriptionHelpClient() {
}), [isEn]);
return (
<main className="min-h-screen bg-[#070d1d] px-4 py-10 text-slate-100">
<main className="min-h-screen bg-[#f4f7fb] px-4 py-10 text-slate-900">
<div className="mx-auto w-full max-w-4xl">
<Link
href="/account"
className="mb-5 inline-flex items-center gap-2 rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-slate-300 transition hover:bg-white/10"
className="mb-5 inline-flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-sm font-semibold text-slate-600 shadow-sm transition hover:border-slate-300 hover:text-slate-950"
>
<ArrowLeft size={15} />
{copy.back}
</Link>
<section className="rounded-3xl border border-blue-400/20 bg-gradient-to-b from-[#162541] to-[#0e1730] p-6 md:p-8">
<section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
<div className="mb-5 flex items-center gap-3">
<ShieldCheck className="text-cyan-300" size={22} />
<ShieldCheck className="text-blue-700" size={22} />
<h1 className="text-2xl font-bold md:text-3xl">{copy.title}</h1>
</div>
<p className="text-sm text-slate-300 md:text-base">{copy.description}</p>
<p className="text-sm text-slate-600 md:text-base">{copy.description}</p>
<div className="mt-6 grid gap-3 md:grid-cols-3">
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-cyan-300">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center gap-2 text-blue-700">
<CreditCard size={16} />
<span className="text-sm font-semibold">{copy.priceLabel}</span>
</div>
<p className="text-xl font-bold">{copy.priceText}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-emerald-300">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center gap-2 text-emerald-700">
<Coins size={16} />
<span className="text-sm font-semibold">{copy.discountLabel}</span>
</div>
<p className="text-xl font-bold">{copy.discountText}</p>
</div>
<div className="rounded-2xl border border-white/10 bg-white/5 p-4">
<div className="mb-2 flex items-center gap-2 text-violet-300">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center gap-2 text-indigo-700">
<MessageSquare size={16} />
<span className="text-sm font-semibold">{copy.communityLabel}</span>
</div>
<Link
href={TELEGRAM_GROUP_URL}
target="_blank"
className="inline-flex min-h-9 items-center text-sm font-semibold text-blue-300 underline decoration-blue-500/50 underline-offset-4"
className="inline-flex min-h-9 items-center text-sm font-semibold text-blue-700 underline decoration-blue-500/50 underline-offset-4"
>
{copy.communityLink}
</Link>
@@ -112,19 +112,19 @@ export function SubscriptionHelpClient() {
</div>
</section>
<section className="mt-6 rounded-3xl border border-white/10 bg-[#0f162a]/80 p-6 md:p-8">
<section className="mt-6 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm md:p-8">
<h2 className="mb-4 text-lg font-bold">{copy.faqTitle}</h2>
<div className="space-y-4">
{FAQ_ITEMS.map((item) => (
<article
key={item.q_en}
className="rounded-2xl border border-white/10 bg-white/[0.03] p-4"
className="rounded-xl border border-slate-200 bg-slate-50 p-4"
>
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold text-blue-300">
<h3 className="mb-2 flex items-center gap-2 text-sm font-semibold text-blue-700">
<CheckCircle2 size={14} />
{isEn ? item.q_en : item.q_zh}
</h3>
<p className="text-sm leading-6 text-slate-300">
<p className="text-sm leading-6 text-slate-600">
{isEn ? item.a_en : item.a_zh}
</p>
</article>
+102 -104
View File
@@ -2105,27 +2105,25 @@ export function AccountCenter() {
if (loading && !refreshing) {
return (
<div className="flex h-screen w-full items-center justify-center bg-[#0b0f1a]">
<div className="flex h-screen w-full items-center justify-center bg-[#f4f7fb]">
<div className="flex flex-col items-center gap-4">
<Loader2 className="h-12 w-12 animate-spin text-blue-500" />
<p className="text-slate-400 font-medium">{copy.loadingAccount}</p>
<p className="font-medium text-slate-500">{copy.loadingAccount}</p>
</div>
</div>
);
}
return (
<div className="min-h-screen w-full bg-[#0b0f1a] text-slate-200 p-4 md:p-8 font-sans relative overflow-hidden flex flex-col items-center">
{/* Aurora Shadows */}
<div className="absolute top-0 right-0 w-[600px] h-[600px] bg-blue-600/10 rounded-full blur-[140px] pointer-events-none"></div>
<div className="absolute bottom-0 left-0 w-[600px] h-[600px] bg-purple-600/10 rounded-full blur-[140px] pointer-events-none"></div>
<div className="relative flex min-h-screen w-full flex-col items-center overflow-hidden bg-[#f4f7fb] p-4 font-sans text-slate-900 md:p-8">
<div className="absolute inset-x-0 top-0 h-20 border-b border-slate-200 bg-white"></div>
{/* Header */}
<header className="w-full max-w-6xl flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 z-20">
<div className="flex items-center gap-4">
<Link
href="/"
className="p-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-full text-slate-400 hover:text-white transition-all active:scale-90 group"
className="group rounded-lg border border-slate-200 bg-white p-2 text-slate-500 shadow-sm transition-all hover:border-slate-300 hover:text-slate-950 active:scale-95"
title={copy.backHome}
aria-label={copy.backHome}
>
@@ -2135,7 +2133,7 @@ export function AccountCenter() {
/>
</Link>
<div>
<h1 className="text-2xl font-bold text-white flex items-center gap-2">
<h1 className="flex items-center gap-2 text-2xl font-bold text-slate-950">
{copy.accountCenter}
</h1>
</div>
@@ -2144,7 +2142,7 @@ export function AccountCenter() {
{!showOverlay && canOpenCheckoutOverlay && (
<button
onClick={() => setShowOverlay(true)}
className="flex items-center gap-2 px-4 py-2 bg-yellow-500/10 hover:bg-yellow-500/20 border border-yellow-500/30 text-yellow-500 rounded-xl text-sm transition-all animate-pulse"
className="flex items-center gap-2 rounded-lg border border-amber-300 bg-amber-50 px-4 py-2 text-sm font-semibold text-amber-700 transition-all hover:bg-amber-100"
>
<Crown size={16} />{" "}
{showExpiringSoon || showExpiredReminder
@@ -2155,7 +2153,7 @@ export function AccountCenter() {
<button
type="button"
onClick={() => void onRefresh()}
className="flex items-center gap-2 px-4 py-2 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl text-sm transition-all disabled:opacity-50"
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm transition-all hover:border-slate-300 hover:text-slate-950 disabled:opacity-50"
disabled={refreshing}
>
{refreshing ? (
@@ -2168,14 +2166,14 @@ export function AccountCenter() {
{isAuthenticated ? (
<button
onClick={() => void onSignOut()}
className="flex items-center gap-2 px-4 py-2 bg-red-500/10 hover:bg-red-500/20 border border-red-500/20 text-red-400 rounded-xl text-sm transition-all"
className="flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-semibold text-red-700 transition-all hover:bg-red-100"
>
<LogOut size={16} /> {copy.signOut}
</button>
) : (
<Link
href="/auth/login?next=%2Faccount"
className="flex items-center gap-2 px-4 py-2 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/20 text-blue-400 rounded-xl text-sm transition-all"
className="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-sm font-semibold text-blue-700 transition-all hover:bg-blue-100"
>
<LogIn size={16} /> {copy.signIn}
</Link>
@@ -2185,23 +2183,23 @@ export function AccountCenter() {
<main className="w-full max-w-6xl grid grid-cols-1 lg:grid-cols-12 gap-6 z-10 relative">
{(showExpiringSoon || showExpiredReminder) && (
<div className="lg:col-span-12 rounded-[2rem] border border-amber-400/30 bg-amber-500/10 px-6 py-5 shadow-xl">
<div className="lg:col-span-12 rounded-2xl border border-amber-300 bg-amber-50 px-6 py-5 shadow-sm">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div className="flex items-center gap-2 text-sm font-bold text-amber-300">
<div className="flex items-center gap-2 text-sm font-bold text-amber-700">
<Crown size={16} />
<span>{subscriptionStatusTitle}</span>
</div>
<p className="mt-1 text-sm text-amber-50/90">
<p className="mt-1 text-sm text-amber-900">
{subscriptionStatusBody}
</p>
{subscriptionStatusMeta ? (
<p className="mt-1 text-xs text-amber-200/80">
<p className="mt-1 text-xs text-amber-700">
{subscriptionStatusMeta}
</p>
) : null}
{billing.canRedeem ? (
<p className="mt-2 text-xs text-emerald-200/90">
<p className="mt-2 text-xs text-emerald-700">
{billing.pointsUsed} $
{billing.discountAmount.toFixed(2)}
</p>
@@ -2210,7 +2208,7 @@ export function AccountCenter() {
<button
type="button"
onClick={() => setShowOverlay(true)}
className="inline-flex items-center justify-center gap-2 rounded-xl border border-amber-300/35 bg-amber-300/12 px-4 py-2 text-sm font-bold text-amber-100 transition-all hover:bg-amber-300/20"
className="inline-flex items-center justify-center gap-2 rounded-lg border border-amber-300 bg-white px-4 py-2 text-sm font-bold text-amber-800 transition-all hover:bg-amber-100"
>
<Crown size={16} />
{showExpiredReminder ? copy.renewNow : copy.upgradePro}
@@ -2220,39 +2218,39 @@ export function AccountCenter() {
)}
{/* User Card */}
<div className="lg:col-span-8 bg-white/5 backdrop-blur-xl border border-white/10 rounded-[2.5rem] p-8 shadow-2xl flex flex-col md:flex-row items-center gap-8">
<div className="flex flex-col items-center gap-8 rounded-2xl border border-slate-200 bg-white p-8 shadow-sm lg:col-span-8 md:flex-row">
<div className="relative">
<div className="w-24 h-24 rounded-3xl bg-gradient-to-tr from-blue-600 to-indigo-400 flex items-center justify-center text-3xl font-bold text-white shadow-xl shadow-blue-500/30">
<div className="flex h-24 w-24 items-center justify-center rounded-xl border border-blue-200 bg-blue-600 text-3xl font-bold text-white shadow-sm">
{initials}
</div>
<div
className={`absolute -bottom-2 -right-2 p-1.5 rounded-xl border-4 border-[#0b0f1a] ${isSubscribed ? "bg-yellow-500 text-black" : "bg-slate-700 text-slate-400"}`}
className={`absolute -bottom-2 -right-2 rounded-lg border-4 border-white p-1.5 ${isSubscribed ? "bg-amber-400 text-slate-950" : "bg-slate-100 text-slate-400"}`}
>
<Crown size={16} fill="currentColor" />
</div>
</div>
<div className="flex-grow text-center md:text-left">
<div className="flex items-center justify-center md:justify-start gap-3 mb-1">
<h2 className="text-3xl font-bold text-white">{displayName}</h2>
<h2 className="text-3xl font-bold text-slate-950">{displayName}</h2>
<span
className={`px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-tighter border ${isSubscribed ? "bg-blue-500/20 border-blue-500/40 text-blue-400" : "bg-slate-700/50 border-white/10 text-slate-500"}`}
className={`rounded-full border px-2 py-0.5 text-[10px] font-black uppercase ${isSubscribed ? "border-blue-200 bg-blue-50 text-blue-700" : "border-slate-200 bg-slate-50 text-slate-500"}`}
>
{isSubscribed
? copy.proMember
: copy.freeTier}
</span>
</div>
<p className="text-slate-500 font-mono text-sm mb-4">
<p className="mb-4 font-mono text-sm text-slate-500">
{email || copy.guestUser}
</p>
<div className="flex flex-wrap justify-center md:justify-start gap-4">
<div className="flex items-center gap-1.5 text-slate-400 text-xs">
<div className="flex items-center gap-1.5 text-xs text-slate-500">
<Hash size={14} />{" "}
<span className="font-mono">
{userId ? `${userId.substring(0, 12)}...` : "--"}
</span>
</div>
<div className="flex items-center gap-1.5 text-slate-400 text-xs">
<div className="flex items-center gap-1.5 text-xs text-slate-500">
<Clock size={14} />{" "}
<span>
{copy.joinedAt}: {joinedAt}
@@ -2261,29 +2259,29 @@ export function AccountCenter() {
</div>
</div>
<div className="flex flex-col gap-3">
<div className="px-6 py-4 bg-black/40 rounded-2xl border border-white/5 text-center min-w-[140px]">
<p className="text-[10px] text-slate-500 uppercase tracking-widest mb-1">
<div className="min-w-[140px] rounded-xl border border-slate-200 bg-slate-50 px-6 py-4 text-center">
<p className="mb-1 text-[10px] uppercase text-slate-500">
{copy.totalPoints}
</p>
<p className="text-xl font-bold text-white flex items-center justify-center gap-2">
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
<Coins size={16} className="text-yellow-500" />{" "}
{totalPoints.toLocaleString()}
</p>
</div>
<div className="px-6 py-4 bg-emerald-500/10 rounded-2xl border border-emerald-500/20 text-center min-w-[140px]">
<p className="text-[10px] text-emerald-300 uppercase tracking-widest mb-1 font-bold">
<div className="min-w-[140px] rounded-xl border border-emerald-200 bg-emerald-50 px-6 py-4 text-center">
<p className="mb-1 text-[10px] font-bold uppercase text-emerald-700">
{copy.weeklyPoints}
</p>
<p className="text-xl font-bold text-white flex items-center justify-center gap-2">
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
<TrendingUp size={16} className="text-emerald-400" />{" "}
{weeklyPoints.toLocaleString()}
</p>
</div>
<div className="px-6 py-4 bg-blue-500/10 rounded-2xl border border-blue-500/20 text-center min-w-[140px]">
<p className="text-[10px] text-blue-400 uppercase tracking-widest mb-1 font-bold">
<div className="min-w-[140px] rounded-xl border border-blue-200 bg-blue-50 px-6 py-4 text-center">
<p className="mb-1 text-[10px] font-bold uppercase text-blue-700">
{copy.weeklyRank}
</p>
<p className="text-xl font-bold text-white flex items-center justify-center gap-2">
<p className="flex items-center justify-center gap-2 text-xl font-bold text-slate-950">
<Trophy size={16} className="text-amber-400" />{" "}
{weeklyRank === "--" ? weeklyRank : `#${weeklyRank}`}
</p>
@@ -2293,36 +2291,36 @@ export function AccountCenter() {
{/* Weekly Ranking Motivation */}
{showSecondarySections ? (
<div className="lg:col-span-4 bg-gradient-to-br from-indigo-600/20 to-purple-600/20 border border-indigo-500/30 rounded-[2.5rem] p-6 flex flex-col justify-between shadow-xl">
<div className="flex flex-col justify-between rounded-2xl border border-slate-200 bg-white p-6 shadow-sm lg:col-span-4">
<div>
<h3 className="text-lg font-bold flex items-center gap-2 text-white mb-6">
<Sparkles size={20} className="text-yellow-400" />{" "}
<h3 className="mb-6 flex items-center gap-2 text-lg font-bold text-slate-950">
<Sparkles size={20} className="text-amber-500" />{" "}
{copy.weeklyRewards}
</h3>
<div className="space-y-3">
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/5">
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
<span className="text-sm flex items-center gap-2">
<div className="w-5 h-5 bg-yellow-500 rounded text-black font-bold text-[10px] flex items-center justify-center">
1
</div>{" "}
Top 1
</span>
<span className="text-xs font-bold text-yellow-500">
<span className="text-xs font-bold text-amber-600">
+200 & 7Pro
</span>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/5">
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
<span className="text-sm flex items-center gap-2">
<div className="w-5 h-5 bg-slate-300 rounded text-black font-bold text-[10px] flex items-center justify-center">
2
</div>{" "}
Top 2-3
</span>
<span className="text-xs font-bold text-slate-300">
<span className="text-xs font-bold text-slate-600">
+100
</span>
</div>
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/5">
<div className="flex items-center justify-between rounded-xl border border-slate-200 bg-slate-50 p-3">
<span className="text-sm flex items-center gap-2">
<div className="w-5 h-5 bg-orange-800 rounded text-white font-bold text-[10px] flex items-center justify-center">
4
@@ -2335,7 +2333,7 @@ export function AccountCenter() {
</div>
</div>
</div>
<div className="mt-6 flex items-start gap-2 p-3 bg-black/20 rounded-xl">
<div className="mt-6 flex items-start gap-2 rounded-xl border border-slate-200 bg-slate-50 p-3">
<Info size={14} className="text-slate-500 mt-0.5 shrink-0" />
<p className="text-[10px] text-slate-500 leading-normal italic">
+
@@ -2344,12 +2342,12 @@ export function AccountCenter() {
</div>
</div>
) : (
<div className="lg:col-span-4 rounded-[2.5rem] border border-white/10 bg-white/5 p-6">
<div className="h-6 w-40 animate-pulse rounded bg-slate-800/80" />
<div className="rounded-2xl border border-slate-200 bg-white p-6 lg:col-span-4">
<div className="h-6 w-40 animate-pulse rounded bg-slate-200" />
<div className="mt-4 space-y-2">
<div className="h-12 animate-pulse rounded-xl bg-slate-800/60" />
<div className="h-12 animate-pulse rounded-xl bg-slate-800/60" />
<div className="h-12 animate-pulse rounded-xl bg-slate-800/60" />
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
<div className="h-12 animate-pulse rounded-xl bg-slate-100" />
</div>
</div>
)}
@@ -2359,8 +2357,8 @@ export function AccountCenter() {
<div
className={`grid grid-cols-1 md:grid-cols-2 gap-6 transition-all duration-700 ${canOpenCheckoutOverlay && showOverlay ? "blur-md grayscale-[0.3] opacity-30 select-none pointer-events-none" : ""}`}
>
<section className="bg-white/5 border border-white/10 rounded-[2rem] p-6 space-y-3">
<h3 className="text-sm font-bold text-blue-400 uppercase tracking-widest mb-4">
<section className="space-y-3 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<h3 className="mb-4 text-sm font-bold uppercase text-blue-700">
{copy.membershipDetails}
</h3>
<InfoRow
@@ -2392,8 +2390,8 @@ export function AccountCenter() {
isPrimary={isSubscribed}
/>
</section>
<section className="bg-white/5 border border-white/10 rounded-[2rem] p-6 space-y-3">
<h3 className="text-sm font-bold text-indigo-400 uppercase tracking-widest mb-4">
<section className="space-y-3 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<h3 className="mb-4 text-sm font-bold uppercase text-indigo-700">
{copy.identityStatus}
</h3>
<InfoRow
@@ -2418,7 +2416,7 @@ export function AccountCenter() {
value={backend?.authenticated ? copy.passed : copy.restricted}
/>
{queuedExtensionSummary ? (
<p className="rounded-2xl border border-cyan-400/20 bg-cyan-500/10 px-4 py-3 text-xs text-cyan-100">
<p className="rounded-xl border border-cyan-200 bg-cyan-50 px-4 py-3 text-xs text-cyan-800">
{queuedExtensionSummary}
</p>
) : null}
@@ -2464,16 +2462,16 @@ export function AccountCenter() {
{showSecondarySections ? (
<div className="lg:col-span-12 grid grid-cols-1 md:flex gap-6">
{canAccessPaidTelegramGroup && (
<section className="flex-1 bg-white/5 border border-white/10 rounded-[2rem] p-8 relative overflow-hidden group">
<section className="group relative flex-1 overflow-hidden rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
<Bot
size={140}
className="absolute -right-8 -bottom-8 text-white/5 -rotate-12 group-hover:rotate-0 transition-transform duration-1000"
className="absolute -right-8 -bottom-8 -rotate-12 text-slate-100 transition-transform duration-1000 group-hover:rotate-0"
/>
<div className="relative z-10">
<h3 className="text-lg font-bold mb-2 flex items-center gap-2 text-blue-400">
<h3 className="mb-2 flex items-center gap-2 text-lg font-bold text-blue-700">
<Bot size={22} /> {copy.telegramBind}
</h3>
<p className="text-slate-400 text-sm mb-6">
<p className="mb-6 text-sm text-slate-500">
{copy.telegramHint}
</p>
@@ -2485,7 +2483,7 @@ export function AccountCenter() {
href={TELEGRAM_TOPICS_GROUP_URL}
target="_blank"
rel="noreferrer"
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-emerald-400/30 bg-emerald-500/10 px-3 py-2 text-xs font-semibold text-emerald-200 hover:bg-emerald-500/20"
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-xs font-semibold text-emerald-700 hover:bg-emerald-100"
>
{copy.telegramTopicsGroupLink}
<ExternalLink size={12} />
@@ -2496,7 +2494,7 @@ export function AccountCenter() {
href={TELEGRAM_GROUP_URL}
target="_blank"
rel="noreferrer"
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-blue-400/30 bg-blue-500/10 px-3 py-2 text-xs font-semibold text-blue-200 hover:bg-blue-500/20"
className="inline-flex min-h-9 items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-xs font-semibold text-blue-700 hover:bg-blue-100"
>
{copy.telegramGroupLink}
<ExternalLink size={12} />
@@ -2504,13 +2502,13 @@ export function AccountCenter() {
) : null}
</div>
<div className="flex gap-2">
<code className="flex-grow bg-black/40 border border-white/10 p-4 rounded-xl font-mono text-xs text-blue-300 overflow-hidden text-ellipsis whitespace-nowrap">
<code className="flex-grow overflow-hidden text-ellipsis whitespace-nowrap rounded-xl border border-slate-200 bg-slate-50 p-4 font-mono text-xs text-blue-700">
{bindCommand}
</code>
<button
onClick={() => void openTelegramBotBindLink()}
disabled={telegramBindOpening || !isAuthenticated}
className="px-4 py-3 bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed rounded-xl transition-all shadow-lg text-white text-xs font-bold"
className="rounded-xl border border-cyan-700 bg-cyan-600 px-4 py-3 text-xs font-bold text-white shadow-sm transition-all hover:bg-cyan-700 disabled:cursor-not-allowed disabled:opacity-50"
title={copy.telegramBotBindLink}
aria-label={copy.telegramBotBindLink}
>
@@ -2518,7 +2516,7 @@ export function AccountCenter() {
</button>
<button
onClick={() => handleCopy(bindCommand)}
className="p-4 bg-blue-600 hover:bg-blue-500 rounded-xl transition-all shadow-lg text-white"
className="rounded-xl border border-blue-700 bg-blue-600 p-4 text-white shadow-sm transition-all hover:bg-blue-700"
title={copy.copyCommand}
aria-label={copy.copyCommand}
>
@@ -2528,7 +2526,7 @@ export function AccountCenter() {
<p className="mt-2 text-[11px] leading-5 text-slate-400">
{copy.telegramFallbackHint}
</p>
<div className="mt-5 rounded-2xl border border-amber-400/25 bg-amber-500/8 px-4 py-3 text-xs leading-6 text-amber-100/90">
<div className="mt-5 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-xs leading-6 text-amber-800">
{copy.paymentManualSupport}
</div>
</div>
@@ -2536,27 +2534,27 @@ export function AccountCenter() {
)}
{/* Payment Details / Wallet Management */}
<section className={`bg-white/5 border border-white/10 rounded-[2rem] p-8 flex flex-col justify-between ${
<section className={`flex flex-col justify-between rounded-2xl border border-slate-200 bg-white p-8 shadow-sm ${
canAccessPaidTelegramGroup ? "w-full md:w-96" : "w-full"
}`}>
<div>
<h3 className="text-blue-400 text-sm font-bold uppercase tracking-widest mb-6 flex items-center gap-2">
<h3 className="mb-6 flex items-center gap-2 text-sm font-bold uppercase text-blue-700">
<Wallet size={18} /> {copy.paymentMgmt}
</h3>
{paymentError ? (
<div className="mb-4 rounded-xl border border-red-400/40 bg-red-500/10 px-3 py-2 text-[11px] text-red-200">
<div className="mb-4 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-[11px] text-red-700">
{paymentError}
</div>
) : null}
{!paymentError && paymentInfo ? (
<div className="mb-4 rounded-xl border border-cyan-400/35 bg-cyan-500/10 px-3 py-2 text-[11px] text-cyan-200">
<div className="mb-4 rounded-xl border border-cyan-200 bg-cyan-50 px-3 py-2 text-[11px] text-cyan-800">
{paymentInfo}
{telegramBindUrl ? (
<a
href={telegramBindUrl}
target="_blank"
rel="noopener noreferrer"
className="block mt-1 underline text-cyan-400 hover:text-cyan-300 break-all"
className="mt-1 block break-all text-cyan-700 underline hover:text-cyan-900"
>
{telegramBindUrl}
</a>
@@ -2564,7 +2562,7 @@ export function AccountCenter() {
</div>
) : null}
{!paymentHostAllowed ? (
<div className="mb-4 rounded-xl border border-amber-400/40 bg-amber-500/10 px-3 py-2 text-[11px] text-amber-200">
<div className="mb-4 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
{copy.paymentHostBlocked.replace(
"{host}",
allowedPaymentHosts[0] || "polyweather-pro.vercel.app",
@@ -2604,7 +2602,7 @@ export function AccountCenter() {
</div>
{availableTokenList.length > 0 && (
<div className="mb-5">
<p className="text-[11px] uppercase tracking-widest text-slate-500 mb-2">
<p className="mb-2 text-[11px] uppercase text-slate-500">
{copy.paymentToken}
</p>
<div className="grid grid-cols-2 gap-2">
@@ -2622,8 +2620,8 @@ export function AccountCenter() {
disabled={paymentBusy}
className={`rounded-xl border px-3 py-2 text-left transition-all ${
active
? "bg-blue-500/15 border-blue-500/40 text-white"
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10"
? "border-blue-300 bg-blue-50 text-blue-900"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50"
}`}
>
<div className="text-xs font-bold">
@@ -2640,18 +2638,18 @@ export function AccountCenter() {
)}
{/* 支付方式选择 Tabs */}
<div className="mt-6 border-t border-white/10 pt-6">
<p className="text-[10px] uppercase tracking-widest text-slate-500 mb-3 font-semibold">
<div className="mt-6 border-t border-slate-200 pt-6">
<p className="mb-3 text-[10px] font-semibold uppercase text-slate-500">
{copy.paymentMethodLabel}
</p>
<div className="grid grid-cols-2 gap-2 p-1 rounded-xl bg-black/40 border border-white/5 mb-5">
<div className="mb-5 grid grid-cols-2 gap-2 rounded-xl border border-slate-200 bg-slate-100 p-1">
<button
type="button"
onClick={() => setPaymentMethodTab("wallet")}
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
paymentMethodTab === "wallet"
? "bg-blue-600/95 text-white shadow-lg"
: "text-slate-400 hover:text-slate-200 hover:bg-white/5"
? "bg-white text-blue-700 shadow-sm"
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
}`}
>
<Wallet size={12} />
@@ -2662,8 +2660,8 @@ export function AccountCenter() {
onClick={() => setPaymentMethodTab("manual")}
className={`py-2 rounded-lg text-xs font-bold transition-all flex items-center justify-center gap-1.5 ${
paymentMethodTab === "manual"
? "bg-emerald-600/95 text-white shadow-lg"
: "text-slate-400 hover:text-slate-200 hover:bg-white/5"
? "bg-white text-emerald-700 shadow-sm"
: "text-slate-500 hover:bg-white/70 hover:text-slate-900"
}`}
>
<ExternalLink size={12} />
@@ -2676,7 +2674,7 @@ export function AccountCenter() {
<p className="text-[11px] leading-relaxed text-slate-400">
{copy.paymentWalletDesc}
</p>
<div className="rounded-xl border border-amber-400/20 bg-amber-500/10 p-3 text-[11px] leading-relaxed text-amber-100">
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-[11px] leading-relaxed text-amber-800">
{copy.paymentGasWarning}
</div>
@@ -2687,8 +2685,8 @@ export function AccountCenter() {
key={w.address}
className={`p-3 rounded-xl border transition-all ${
selectedWallet === w.address
? "bg-blue-500/10 border-blue-500/30 text-white"
: "bg-white/5 border-white/5 text-slate-400"
? "border-blue-300 bg-blue-50 text-blue-900"
: "border-slate-200 bg-white text-slate-600"
}`}
>
<div className="flex items-center justify-between mb-1">
@@ -2707,7 +2705,7 @@ export function AccountCenter() {
type="button"
onClick={() => void handleUnbindWallet(w.address)}
disabled={paymentBusy}
className="inline-flex items-center gap-1 rounded-md border border-red-500/30 bg-red-500/10 px-2 py-1 text-[10px] font-semibold text-red-300 transition-all hover:bg-red-500/20 disabled:opacity-50"
className="inline-flex items-center gap-1 rounded-md border border-red-200 bg-red-50 px-2 py-1 text-[10px] font-semibold text-red-700 transition-all hover:bg-red-100 disabled:opacity-50"
>
<Minus size={12} />
{copy.unbind}
@@ -2717,7 +2715,7 @@ export function AccountCenter() {
))}
</div>
) : (
<div className="p-4 rounded-xl border border-white/5 bg-white/5 text-center">
<div className="rounded-xl border border-slate-200 bg-slate-50 p-4 text-center">
<p className="text-xs text-slate-400 italic">
{copy.noWallet}
</p>
@@ -2727,7 +2725,7 @@ export function AccountCenter() {
<div className="grid grid-cols-1 gap-2 pt-2">
{injectedProviderOptions.length > 1 && (
<label className="mb-2 block">
<span className="mb-2 block text-[11px] uppercase tracking-widest text-slate-500">
<span className="mb-2 block text-[11px] uppercase text-slate-500">
{copy.walletExtensionDetected}
</span>
<select
@@ -2736,13 +2734,13 @@ export function AccountCenter() {
setSelectedInjectedProviderKey(event.target.value)
}
disabled={paymentBusy}
className="w-full rounded-xl border border-white/10 bg-white/5 px-3 py-3 text-xs text-slate-200 outline-none transition-all hover:bg-white/10 disabled:opacity-60"
className="w-full rounded-xl border border-slate-300 bg-white px-3 py-3 text-xs text-slate-900 outline-none transition-all hover:bg-slate-50 disabled:opacity-60"
>
{injectedProviderOptions.map((option) => (
<option
key={option.key}
value={option.key}
className="bg-slate-900 text-slate-200"
className="bg-white text-slate-900"
>
{option.label}
</option>
@@ -2756,7 +2754,7 @@ export function AccountCenter() {
void connectAndBindWallet("auto");
}}
disabled={paymentBusy || !isAuthenticated}
className="w-full py-3 border border-white/10 bg-white/5 hover:bg-white/10 rounded-xl text-xs font-bold text-slate-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
className="flex w-full items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white py-3 text-xs font-bold text-slate-700 transition-all hover:bg-slate-50 disabled:opacity-60"
>
<PlusIcon className="w-4 h-4" /> {copy.bindExt}
</button>
@@ -2768,7 +2766,7 @@ export function AccountCenter() {
disabled={
paymentBusy || !isAuthenticated || !walletConnectEnabled
}
className="w-full py-3 border border-cyan-400/30 bg-cyan-500/10 hover:bg-cyan-500/20 rounded-xl text-xs font-bold text-cyan-300 transition-all flex items-center justify-center gap-2 disabled:opacity-60"
className="flex w-full items-center justify-center gap-2 rounded-xl border border-cyan-200 bg-cyan-50 py-3 text-xs font-bold text-cyan-700 transition-all hover:bg-cyan-100 disabled:opacity-60"
>
<CreditCard className="w-4 h-4" /> {copy.bindQr}
</button>
@@ -2788,13 +2786,13 @@ export function AccountCenter() {
{copy.paymentManualDesc}
</p>
<div className="rounded-2xl border border-emerald-400/25 bg-emerald-500/8 p-4">
<div className="rounded-2xl border border-emerald-200 bg-emerald-50 p-4">
<div className="flex flex-col gap-3">
<div>
<p className="text-xs font-bold text-emerald-200">
<p className="text-xs font-bold text-emerald-800">
{copy.paymentManualTitle}
</p>
<p className="mt-1 text-[11px] leading-relaxed text-emerald-100/75">
<p className="mt-1 text-[11px] leading-relaxed text-emerald-700">
{copy.paymentManualHint}
</p>
</div>
@@ -2802,18 +2800,18 @@ export function AccountCenter() {
type="button"
onClick={() => void createManualPaymentIntent()}
disabled={paymentBusy || !isAuthenticated}
className="w-full rounded-xl border border-emerald-400/35 bg-emerald-500/15 py-2.5 text-xs font-bold text-emerald-100 transition-all hover:bg-emerald-500/25 disabled:opacity-50"
className="w-full rounded-xl border border-emerald-700 bg-emerald-600 py-2.5 text-xs font-bold text-white transition-all hover:bg-emerald-700 disabled:opacity-50"
>
{copy.paymentManualCreate}
</button>
</div>
{manualPayment ? (
<div className="mt-4 space-y-3 rounded-xl border border-white/10 bg-black/25 p-3">
<div className="mt-4 space-y-3 rounded-xl border border-emerald-200 bg-white p-3">
<div>
<p className="text-[10px] uppercase tracking-widest text-slate-500">
{copy.paymentAmount}
</p>
<p className="font-mono text-sm font-bold text-white">
<p className="font-mono text-sm font-bold text-slate-950">
{manualPayment.amount_usdc}{" "}
{manualPayment.token_symbol || selectedTokenLabel}
</p>
@@ -2823,7 +2821,7 @@ export function AccountCenter() {
{copy.paymentReceiverLabel}
</p>
<div className="mt-1 flex gap-2">
<code className="min-w-0 flex-1 break-all whitespace-normal rounded-lg bg-black/40 px-2 py-2 font-mono text-[11px] text-blue-200">
<code className="min-w-0 flex-1 break-all whitespace-normal rounded-lg border border-blue-200 bg-blue-50 px-2 py-2 font-mono text-[11px] text-blue-800">
{manualPayment.receiver_address}
</code>
<button
@@ -2831,7 +2829,7 @@ export function AccountCenter() {
onClick={() =>
handleCopy(manualPayment.receiver_address)
}
className="rounded-lg bg-blue-600 px-2 text-xs font-bold text-white transition-colors hover:bg-blue-500"
className="rounded-lg border border-blue-700 bg-blue-600 px-2 text-xs font-bold text-white transition-colors hover:bg-blue-700"
>
{copy.paymentCopyAddress}
</button>
@@ -2853,19 +2851,19 @@ export function AccountCenter() {
);
}}
placeholder="0x..."
className="mt-1 w-full rounded-xl border border-white/10 bg-black/30 px-3 py-2 font-mono text-xs text-slate-100 outline-none focus:border-emerald-400/50"
className="mt-1 w-full rounded-xl border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-950 outline-none focus:border-emerald-500 focus:ring-2 focus:ring-emerald-500/20"
/>
{txValidation.loading ? (
<p className="mt-1 text-[10px] text-slate-500">
...
</p>
) : txValidation.checked && txValidation.valid ? (
<p className="mt-1 text-[10px] text-emerald-400">
<p className="mt-1 text-[10px] text-emerald-700">
</p>
) : txValidation.checked &&
txValidation.valid === false ? (
<p className="mt-1 text-[10px] text-red-400">
<p className="mt-1 text-[10px] text-red-700">
{txValidation.reason ===
"tx_not_mined"
? "交易未上链,请等待"
@@ -2890,7 +2888,7 @@ export function AccountCenter() {
paymentBusy ||
(txValidation.checked && !txValidation.valid)
}
className="w-full rounded-xl bg-emerald-600 px-3 py-2 text-xs font-bold text-white transition-all hover:bg-emerald-500 disabled:opacity-50"
className="w-full rounded-xl border border-emerald-700 bg-emerald-600 px-3 py-2 text-xs font-bold text-white transition-all hover:bg-emerald-700 disabled:opacity-50"
>
{copy.paymentManualSubmit}
</button>
@@ -2905,8 +2903,8 @@ export function AccountCenter() {
</div>
) : (
<div className="lg:col-span-12 grid grid-cols-1 gap-6 md:grid-cols-3">
<div className="md:col-span-2 h-48 animate-pulse rounded-[2rem] border border-white/10 bg-white/5" />
<div className="h-48 animate-pulse rounded-[2rem] border border-white/10 bg-white/5" />
<div className="h-48 animate-pulse rounded-2xl border border-slate-200 bg-white md:col-span-2" />
<div className="h-48 animate-pulse rounded-2xl border border-slate-200 bg-white" />
</div>
)}
</main>
+5 -5
View File
@@ -10,14 +10,14 @@ const AccountCenter = dynamic(
{
ssr: false,
loading: () => (
<div className="min-h-screen bg-slate-950 text-slate-300">
<div className="min-h-screen bg-[#f4f7fb] text-slate-700">
<div className="mx-auto w-full max-w-6xl px-6 py-10">
<div className="h-7 w-48 animate-pulse rounded bg-slate-800/80" />
<div className="h-7 w-48 animate-pulse rounded bg-slate-200" />
<div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-3">
<div className="h-32 animate-pulse rounded-3xl bg-slate-800/70 md:col-span-2" />
<div className="h-32 animate-pulse rounded-3xl bg-slate-800/70" />
<div className="h-32 animate-pulse rounded-2xl border border-slate-200 bg-white md:col-span-2" />
<div className="h-32 animate-pulse rounded-2xl border border-slate-200 bg-white" />
</div>
<div className="mt-6 h-72 animate-pulse rounded-3xl bg-slate-800/60" />
<div className="mt-6 h-72 animate-pulse rounded-2xl border border-slate-200 bg-white" />
</div>
</div>
),
@@ -13,17 +13,17 @@ export const InfoRow = ({
value,
isPrimary = false,
}: InfoRowProps) => (
<div className="flex min-w-0 flex-col gap-3 p-4 bg-white/5 rounded-2xl border border-white/5 hover:bg-white/10 transition-all group sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 flex-col gap-3 rounded-xl border border-slate-200 bg-white p-4 transition-all hover:border-slate-300 hover:bg-slate-50 group sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="shrink-0 p-2 bg-slate-800 rounded-lg text-slate-400 group-hover:text-blue-400 transition-colors">
<div className="shrink-0 rounded-lg border border-slate-200 bg-slate-50 p-2 text-slate-500 transition-colors group-hover:text-blue-600">
{Icon && <Icon size={18} />}
</div>
<span className="min-w-0 text-slate-400 text-sm font-medium leading-5">
<span className="min-w-0 text-sm font-medium leading-5 text-slate-500">
{label}
</span>
</div>
<span
className={`min-w-0 break-all text-left text-sm font-semibold font-mono sm:text-right ${isPrimary ? "text-blue-400" : "text-slate-200"}`}
className={`min-w-0 break-all text-left font-mono text-sm font-semibold sm:text-right ${isPrimary ? "text-blue-700" : "text-slate-900"}`}
>
{value}
</span>
+23 -24
View File
@@ -217,53 +217,52 @@ export function LoginClient({ nextPath }: LoginClientProps) {
const isLogin = mode === "login";
return (
<div className="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-[#0f172a] font-sans">
<div className="absolute left-[-10%] top-[-10%] h-[40vw] w-[40vw] animate-pulse rounded-full bg-blue-600/20 blur-[120px]" />
<div className="absolute bottom-[-10%] right-[-10%] h-[30vw] w-[30vw] rounded-full bg-indigo-500/20 blur-[100px]" />
<div className="relative flex min-h-screen w-full flex-col items-center justify-center overflow-hidden bg-[#f4f7fb] px-4 py-8 font-sans text-slate-900">
<div className="absolute inset-x-0 top-0 h-24 border-b border-slate-200 bg-white" />
<div className="relative mx-4 w-full max-w-[420px] rounded-[2rem] border border-white/10 bg-white/5 p-8 shadow-2xl backdrop-blur-xl">
<div className="relative w-full max-w-[420px] rounded-2xl border border-slate-200 bg-white p-8 shadow-[0_24px_70px_rgba(15,23,42,0.12)]">
<Link
href="/"
className="group absolute left-6 top-6 rounded-full border border-white/10 bg-white/5 p-2 text-slate-400 transition-all hover:bg-white/10 hover:text-white active:scale-90"
className="group absolute left-6 top-6 rounded-lg border border-slate-200 bg-slate-50 p-2 text-slate-500 transition-all hover:border-slate-300 hover:bg-white hover:text-slate-900 active:scale-95"
title={copy.backHome}
aria-label={copy.backHome}
>
<ChevronLeft className="h-5 w-5 transition-transform group-hover:-translate-x-0.5" />
</Link>
<div className="mb-8 flex flex-col items-center">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-tr from-blue-500 to-indigo-400 shadow-lg shadow-blue-500/20">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-xl border border-blue-200 bg-blue-600 shadow-sm">
<Cloud className="h-10 w-10 text-white" />
</div>
<h1 className="text-3xl font-bold tracking-tight text-white">PolyWeather</h1>
<p className="mt-2 text-sm text-slate-400">{copy.subtitle}</p>
<h1 className="text-3xl font-bold tracking-tight text-slate-950">PolyWeather</h1>
<p className="mt-2 text-center text-sm text-slate-500">{copy.subtitle}</p>
</div>
<button
type="button"
onClick={() => void onGoogleSignIn()}
disabled={loading}
className="mb-6 flex w-full items-center justify-center rounded-xl bg-white px-4 py-3.5 font-semibold text-slate-900 shadow-lg transition-all duration-200 hover:bg-slate-100 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70"
className="mb-6 flex w-full items-center justify-center rounded-lg border border-slate-300 bg-white px-4 py-3.5 font-semibold text-slate-900 shadow-sm transition-all duration-200 hover:border-slate-400 hover:bg-slate-50 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-70"
>
<Chrome className="mr-3 h-5 w-5" />
{copy.googleOneClick}
</button>
<div className="my-6 flex items-center">
<div className="h-[1px] flex-grow bg-white/10" />
<span className="px-4 text-xs font-medium uppercase tracking-widest text-slate-500">
<div className="h-px flex-grow bg-slate-200" />
<span className="px-4 text-xs font-semibold uppercase text-slate-500">
{copy.orEmail}
</span>
<div className="h-[1px] flex-grow bg-white/10" />
<div className="h-px flex-grow bg-slate-200" />
</div>
<div className="mb-6 flex rounded-xl bg-black/20 p-1">
<div className="mb-6 flex rounded-lg border border-slate-200 bg-slate-100 p-1">
<button
type="button"
onClick={() => setMode("login")}
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all ${
isLogin
? "bg-blue-600 text-white shadow-md"
: "text-slate-400 hover:text-slate-200"
? "bg-white text-slate-950 shadow-sm"
: "text-slate-500 hover:text-slate-900"
}`}
>
{copy.login}
@@ -273,8 +272,8 @@ export function LoginClient({ nextPath }: LoginClientProps) {
onClick={() => setMode("signup")}
className={`flex-1 rounded-lg py-2 text-sm font-medium transition-all ${
!isLogin
? "bg-blue-600 text-white shadow-md"
: "text-slate-400 hover:text-slate-200"
? "bg-white text-slate-950 shadow-sm"
: "text-slate-500 hover:text-slate-900"
}`}
>
{copy.signup}
@@ -290,7 +289,7 @@ export function LoginClient({ nextPath }: LoginClientProps) {
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="you@example.com"
className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
className="w-full rounded-lg border border-slate-300 bg-white py-3.5 pl-12 pr-4 text-slate-950 placeholder:text-slate-400 transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
/>
</div>
<div className="relative">
@@ -306,7 +305,7 @@ export function LoginClient({ nextPath }: LoginClientProps) {
? copy.passwordLoginPlaceholder
: copy.passwordSignupPlaceholder
}
className="w-full rounded-xl border border-white/10 bg-white/5 py-3.5 pl-12 pr-4 text-white placeholder:text-slate-600 transition-all focus:border-blue-500/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50"
className="w-full rounded-lg border border-slate-300 bg-white py-3.5 pl-12 pr-4 text-slate-950 placeholder:text-slate-400 transition-all focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20"
/>
</div>
@@ -316,7 +315,7 @@ export function LoginClient({ nextPath }: LoginClientProps) {
type="button"
onClick={() => void onResetPassword()}
disabled={loading}
className="text-xs text-slate-500 underline-offset-2 transition-all hover:text-blue-400 hover:underline disabled:opacity-50"
className="text-xs text-slate-500 underline-offset-2 transition-all hover:text-blue-600 hover:underline disabled:opacity-50"
>
{copy.reset}
</button>
@@ -326,15 +325,15 @@ export function LoginClient({ nextPath }: LoginClientProps) {
<button
type="submit"
disabled={loading}
className="group mt-8 flex w-full items-center justify-center rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 py-3.5 font-bold text-white shadow-xl shadow-blue-600/20 transition-all hover:from-blue-500 hover:to-indigo-500 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-70"
className="group mt-8 flex w-full items-center justify-center rounded-lg border border-blue-700 bg-blue-600 py-3.5 font-bold text-white shadow-sm transition-all hover:bg-blue-700 active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-70"
>
{isLogin ? copy.loginSubmit : copy.signupSubmit}
<ArrowRight className="ml-2 h-5 w-5 transition-transform group-hover:translate-x-1" />
</button>
</form>
{errorText ? <p className="mt-4 text-sm text-rose-300">{errorText}</p> : null}
{infoText ? <p className="mt-4 text-sm text-emerald-300">{infoText}</p> : null}
{errorText ? <p className="mt-4 rounded-lg border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">{errorText}</p> : null}
{infoText ? <p className="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-700">{infoText}</p> : null}
{errorText && isLogin && errorText.includes("Invalid login") ? (
<p className="mt-1 text-xs text-slate-500">{copy.loginFailedHint}</p>
) : null}
@@ -353,7 +352,7 @@ export function LoginClient({ nextPath }: LoginClientProps) {
) : null}
</div>
<div className="absolute bottom-8 flex items-center gap-4 text-sm text-slate-600">
<div className="relative mt-6 flex items-center gap-4 text-sm font-medium text-slate-500">
<span className="flex items-center">
<Sun className="mr-1 h-4 w-4" /> {copy.realtime}
</span>
@@ -1062,3 +1062,33 @@
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Model Forecast Light Mode Overrides ── */
:global(html.light) .root :global(.model-group) {
background: #f8fafc;
border-left-color: var(--color-border-default);
}
:global(html.light) .root :global(.model-bar-track) {
background: var(--color-bg-input);
}
:global(html.light) .root :global(.model-bar-value) {
color: #ffffff;
text-shadow: 0 1px 2px rgba(15, 23, 42, 0.4);
}
:global(html.light) .root :global(.model-name strong) {
color: var(--color-text-primary);
}
:global(html.light) .root :global(.model-name span) {
color: var(--color-text-muted);
}
:global(html.light) .root :global(.model-stack-summary span) {
background: #ffffff;
border-color: var(--color-border-default);
color: var(--color-text-secondary);
}
@@ -714,6 +714,55 @@
background: var(--bg-card);
}
/* ── Segmented Control: Locale Switcher in Light Mode ── */
.root :global(.scan-terminal.light .scan-locale-switch) {
border-color: #cbd5e1 !important;
background: #e2e8f0 !important;
color: #64748b !important;
}
.root :global(.scan-terminal.light .scan-locale-switch span) {
color: #64748b !important;
}
.root :global(.scan-terminal.light .scan-locale-switch span.active) {
color: #0f172a !important;
background: #ffffff !important;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08) !important;
}
/* ── Status Chips and Manual Refresh button in Light Mode ── */
.root :global(.scan-terminal.light .scan-status-chip) {
border-color: #cbd5e1 !important;
background: #ffffff !important;
color: #475569 !important;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05) !important;
}
.root :global(.scan-terminal.light .scan-status-chip.live) {
border-color: rgba(16, 185, 129, 0.22) !important;
background: rgba(16, 185, 129, 0.12) !important;
color: #065f46 !important;
}
.root :global(.scan-terminal.light .scan-status-chip.stale) {
border-color: rgba(245, 158, 11, 0.24) !important;
background: rgba(245, 158, 11, 0.12) !important;
color: #92400e !important;
}
.root :global(.scan-terminal.light button.scan-status-chip.refresh) {
border-color: #cbd5e1 !important;
background: #ffffff !important;
color: #1e293b !important;
}
.root :global(.scan-terminal.light button.scan-status-chip.refresh:hover) {
background: #f8fafc !important;
border-color: #94a3b8 !important;
}
.root :global(.scan-terminal.light .scan-mode-tab.active) {
color: #1D4ED8;
background: #DBEAFE;
@@ -814,9 +863,7 @@
background: linear-gradient(180deg, rgba(224, 247, 255, 0.82), rgba(241, 248, 255, 0.92));
}
.root :global(.scan-terminal.light .scan-locale-switch),
.root :global(.scan-terminal.light .scan-ghost-button),
.root :global(.scan-terminal.light .scan-theme-button),
.root :global(.scan-terminal.light .scan-sort-pill),
.root :global(.scan-terminal.light .scan-icon-pill),
.root :global(.scan-terminal.light .scan-account-button),
@@ -1221,128 +1268,128 @@
/* ── Leaflet map light overrides (needs !important to beat Leaflet inline styles) ── */
.root :global(.map),
html.light .root :global(.scan-terminal.light .scan-map-shell .map),
html.light .root :global(.scan-terminal.light .scan-map-shell .leaflet-container),
html.light .root :global(.leaflet-container),
html.light .root :global(.leaflet-pane),
html.light .root :global(.leaflet-map-pane),
html.light .root :global(.leaflet-tile-pane) {
:global(html.light) .root :global(.scan-terminal.light .scan-map-shell .map),
:global(html.light) .root :global(.scan-terminal.light .scan-map-shell .leaflet-container),
:global(html.light) .root :global(.leaflet-container),
:global(html.light) .root :global(.leaflet-pane),
:global(html.light) .root :global(.leaflet-map-pane),
:global(html.light) .root :global(.leaflet-tile-pane) {
background: #eef7ff !important;
}
/* ── City detail rail light overrides ── */
html.light .root :global(.scan-city-detail-rail),
html.light .root :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) {
:global(html.light) .root :global(.scan-city-detail-rail),
:global(html.light) .root :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) {
background: linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(255, 255, 255, 0.96));
border-color: rgba(37, 99, 235, 0.16);
color: var(--text-primary);
}
html.light .root :global(.scan-city-detail-rail .panel-header),
html.light .root :global(.scan-city-detail-rail .detail-structured-section),
html.light .root :global(.scan-city-detail-rail .detail-summary-shell),
html.light .root :global(.scan-city-detail-rail .detail-card) {
:global(html.light) .root :global(.scan-city-detail-rail .panel-header),
:global(html.light) .root :global(.scan-city-detail-rail .detail-structured-section),
:global(html.light) .root :global(.scan-city-detail-rail .detail-summary-shell),
:global(html.light) .root :global(.scan-city-detail-rail .detail-card) {
background: rgba(255, 255, 255, 0.74);
border-color: rgba(125, 171, 214, 0.28);
}
html.light .root :global(.scan-city-detail-rail h2),
html.light .root :global(.scan-city-detail-rail h3),
html.light .root :global(.scan-city-detail-rail strong),
html.light .root :global(.scan-city-detail-rail .detail-value),
html.light .root :global(.scan-city-detail-rail .detail-summary-temp),
html.light .root :global(.scan-city-detail-rail .detail-section-head h3),
html.light .root :global(.scan-city-detail-rail .panel-title-area h2) {
:global(html.light) .root :global(.scan-city-detail-rail h2),
:global(html.light) .root :global(.scan-city-detail-rail h3),
:global(html.light) .root :global(.scan-city-detail-rail strong),
:global(html.light) .root :global(.scan-city-detail-rail .detail-value),
:global(html.light) .root :global(.scan-city-detail-rail .detail-summary-temp),
:global(html.light) .root :global(.scan-city-detail-rail .detail-section-head h3),
:global(html.light) .root :global(.scan-city-detail-rail .panel-title-area h2) {
color: var(--text-primary);
}
html.light .root :global(.scan-city-detail-rail p),
html.light .root :global(.scan-city-detail-rail li),
html.light .root :global(.scan-city-detail-rail small),
html.light .root :global(.scan-city-detail-rail .panel-overline),
html.light .root :global(.scan-city-detail-rail .panel-loading-hint),
html.light .root :global(.scan-city-detail-rail .panel-meta-chip),
html.light .root :global(.scan-city-detail-rail .detail-section-kicker),
html.light .root :global(.scan-city-detail-rail .detail-label),
html.light .root :global(.scan-city-detail-rail .detail-value-muted),
html.light .root :global(.scan-city-detail-rail .detail-source-note),
html.light .root :global(.scan-city-detail-rail .detail-source-kind),
html.light .root :global(.scan-city-detail-rail .detail-mini-meta),
html.light .root :global(.scan-city-detail-rail .detail-summary-supporting),
html.light .root :global(.scan-city-detail-rail .scan-detail-city-sub),
html.light .root :global(.scan-city-detail-rail .scan-detail-volume-caption),
html.light .root :global(.scan-city-detail-rail .scan-detail-score-label),
html.light .root :global(.scan-city-detail-rail .scan-detail-empty) {
:global(html.light) .root :global(.scan-city-detail-rail p),
:global(html.light) .root :global(.scan-city-detail-rail li),
:global(html.light) .root :global(.scan-city-detail-rail small),
:global(html.light) .root :global(.scan-city-detail-rail .panel-overline),
:global(html.light) .root :global(.scan-city-detail-rail .panel-loading-hint),
:global(html.light) .root :global(.scan-city-detail-rail .panel-meta-chip),
:global(html.light) .root :global(.scan-city-detail-rail .detail-section-kicker),
:global(html.light) .root :global(.scan-city-detail-rail .detail-label),
:global(html.light) .root :global(.scan-city-detail-rail .detail-value-muted),
:global(html.light) .root :global(.scan-city-detail-rail .detail-source-note),
:global(html.light) .root :global(.scan-city-detail-rail .detail-source-kind),
:global(html.light) .root :global(.scan-city-detail-rail .detail-mini-meta),
:global(html.light) .root :global(.scan-city-detail-rail .detail-summary-supporting),
:global(html.light) .root :global(.scan-city-detail-rail .scan-detail-city-sub),
:global(html.light) .root :global(.scan-city-detail-rail .scan-detail-volume-caption),
:global(html.light) .root :global(.scan-city-detail-rail .scan-detail-score-label),
:global(html.light) .root :global(.scan-city-detail-rail .scan-detail-empty) {
color: var(--text-secondary);
}
html.light .root :global(.scan-city-detail-rail .panel-meta-chip),
html.light .root :global(.scan-city-detail-rail .panel-weather-chip),
html.light .root :global(.scan-city-detail-rail .detail-source-link),
html.light .root :global(.scan-city-detail-rail .panel-action-button-secondary),
html.light .root :global(.scan-city-detail-rail .panel-action-button-ghost) {
:global(html.light) .root :global(.scan-city-detail-rail .panel-meta-chip),
:global(html.light) .root :global(.scan-city-detail-rail .panel-weather-chip),
:global(html.light) .root :global(.scan-city-detail-rail .detail-source-link),
:global(html.light) .root :global(.scan-city-detail-rail .panel-action-button-secondary),
:global(html.light) .root :global(.scan-city-detail-rail .panel-action-button-ghost) {
background: #eef7ff;
border-color: rgba(37, 99, 235, 0.18);
color: #1f3654;
}
/* ── AI city card light overrides ── */
html.light .root :global(.scan-terminal.light .scan-ai-city-card),
html.light .root :global(.scan-terminal.light .scan-ai-city-section),
html.light .root :global(.scan-terminal.light .scan-ai-decision-band),
html.light .root :global(.scan-terminal.light .scan-ai-market-decision),
html.light .root :global(.scan-terminal.light .scan-ai-decision-metrics span),
html.light .root :global(.scan-terminal.light .scan-ai-city-pills span),
html.light .root :global(.scan-terminal.light .scan-ai-city-freshness span),
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics span),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-reason) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-section),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-band),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-decision),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-metrics span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-pills span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-freshness span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics span),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-reason) {
background: #eef7ff;
border-color: rgba(37, 99, 235, 0.16);
}
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
background: #dbeafe;
border-color: rgba(59, 130, 246, 0.34);
}
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics b) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics b) {
color: var(--text-primary);
}
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
color: #1d4ed8;
}
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics small) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-metrics small) {
color: #64748b;
}
html.light .root :global(.scan-terminal.light .scan-ai-city-card p),
html.light .root :global(.scan-terminal.light .scan-ai-city-card li),
html.light .root :global(.scan-terminal.light .scan-ai-city-card small),
html.light .root :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card p),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card small),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) {
color: var(--text-secondary);
}
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) {
color: #047857;
}
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) {
color: #1d4ed8;
}
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) {
color: #b45309;
}
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
color: #b91c1c;
}
/* ── Upgrade announcement light overrides ── */
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement) {
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement) {
background:
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 36%),
var(--bg-card);
@@ -1350,188 +1397,188 @@ html.light .root :global(.scan-terminal.light .scan-upgrade-announcement) {
box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12);
}
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics b),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-reason),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-head h3),
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics b),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-reason),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-head h3),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
color: var(--text-primary);
}
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement li),
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics small),
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
:global(html.light) .root :global(.scan-terminal.light .scan-mobile-decision-metrics small),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
color: var(--text-secondary);
}
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
color: #047857;
}
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement li),
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
html.light .root :global(.scan-terminal.light .scan-ai-decision-why),
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line) {
:global(html.light) .root :global(.scan-terminal.light .scan-upgrade-announcement li),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-why),
:global(html.light) .root :global(.scan-terminal.light .scan-ai-market-mobile-line) {
background: #eef7ff;
border-color: rgba(37, 99, 235, 0.16);
}
html.light .root :global(.scan-terminal.light .scan-ai-decision-why) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-why) {
color: #1d4ed8;
}
/* ── Section titles ── */
html.light .root :global(.scan-terminal.light .scan-ai-city-section-title) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-section-title) {
color: #1d4ed8;
}
html.light .root :global(.scan-terminal.light .scan-ai-decision-band span) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-band span) {
color: #1d4ed8;
}
html.light .root :global(.scan-terminal.light .scan-ai-decision-band p) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-band p) {
color: var(--text-muted);
}
html.light .root :global(.scan-terminal.light .scan-ai-decision-band strong) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-decision-band strong) {
color: var(--text-primary);
}
/* ── Scrollbar light overrides ── */
html.light .root :global(.scan-terminal.light .scan-ai-city-body) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-city-body) {
scrollbar-color: rgba(37, 99, 235, 0.18) transparent;
}
html.light .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
:global(html.light) .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
background: rgba(37, 99, 235, 0.22);
}
/* ── AI prediction dual-card light mode ── */
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card) {
background: var(--bg-card);
border-color: var(--border-glass);
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card.ai) {
background: linear-gradient(180deg, rgba(219, 234, 254, 0.52), var(--bg-card));
border-color: rgba(14, 165, 233, 0.28);
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.deb) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card.deb) {
background: var(--bg-card);
border-color: var(--border-glass);
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card small) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card small) {
color: #64748b;
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card strong) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card strong) {
color: var(--text-primary);
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) {
color: #0369a1;
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card em) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card em) {
color: #64748b;
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending) {
background: rgba(219, 234, 254, 0.28);
border-color: rgba(14, 165, 233, 0.16);
}
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) {
color: #94a3b8;
}
html.light .root :global(.scan-terminal.light .scan-ai-confidence) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-confidence) {
background: rgba(59, 130, 246, 0.1);
color: #2563eb;
}
html.light .root :global(.scan-terminal.light .scan-ai-confidence.high) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-confidence.high) {
background: rgba(16, 185, 129, 0.12);
color: #047857;
}
html.light .root :global(.scan-terminal.light .scan-ai-confidence.medium) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-confidence.medium) {
background: rgba(245, 158, 11, 0.12);
color: #b45309;
}
html.light .root :global(.scan-terminal.light .scan-ai-confidence.low) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-confidence.low) {
background: rgba(148, 163, 184, 0.12);
color: #64748b;
}
html.light .root :global(.scan-terminal.light .scan-ai-confidence.neutral) {
:global(html.light) .root :global(.scan-terminal.light .scan-ai-confidence.neutral) {
background: rgba(148, 163, 184, 0.08);
color: #64748b;
}
/* ── Empty / Error / Retry light overrides ── */
html.light .root :global(.scan-terminal.light .scan-empty-title),
html.light .root :global(.scan-terminal.light .scan-error-title) {
:global(html.light) .root :global(.scan-terminal.light .scan-empty-title),
:global(html.light) .root :global(.scan-terminal.light .scan-error-title) {
color: var(--text-primary);
}
html.light .root :global(.scan-terminal.light .scan-empty-copy),
html.light .root :global(.scan-terminal.light .scan-error-copy) {
:global(html.light) .root :global(.scan-terminal.light .scan-empty-copy),
:global(html.light) .root :global(.scan-terminal.light .scan-error-copy) {
color: var(--text-muted);
}
html.light .root :global(.scan-terminal.light .scan-empty-icon) {
:global(html.light) .root :global(.scan-terminal.light .scan-empty-icon) {
background: rgba(37, 99, 235, 0.08);
border-color: rgba(37, 99, 235, 0.2);
color: #2563EB;
}
html.light .root :global(.scan-terminal.light .scan-error-icon) {
:global(html.light) .root :global(.scan-terminal.light .scan-error-icon) {
background: rgba(239, 68, 68, 0.06);
border-color: rgba(239, 68, 68, 0.18);
color: #DC2626;
}
html.light .root :global(.scan-terminal.light .scan-retry-button) {
:global(html.light) .root :global(.scan-terminal.light .scan-retry-button) {
background: rgba(37, 99, 235, 0.08);
border-color: rgba(37, 99, 235, 0.24);
color: #2563EB;
}
html.light .root :global(.scan-terminal.light .scan-retry-button:hover) {
:global(html.light) .root :global(.scan-terminal.light .scan-retry-button:hover) {
background: rgba(37, 99, 235, 0.14);
}
/* ── AMOS runway panel light overrides ── */
html.light .root :global(.scan-terminal.light .scan-amos-runway-panel) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-runway-panel) {
background: rgba(37, 99, 235, 0.03);
border-color: rgba(37, 99, 235, 0.12);
}
html.light .root :global(.scan-terminal.light .scan-amos-runway-card) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-runway-card) {
background: var(--bg-card);
border-color: rgba(37, 99, 235, 0.08);
}
html.light .root :global(.scan-terminal.light .scan-amos-runway-label) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-runway-label) {
color: #2563EB;
}
html.light .root :global(.scan-terminal.light .scan-amos-runway-temp) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-runway-temp) {
color: var(--text-primary);
}
html.light .root :global(.scan-terminal.light .scan-amos-source-tag) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-source-tag) {
background: rgba(37, 99, 235, 0.08);
color: #2563EB;
}
html.light .root :global(.scan-terminal.light .scan-amos-runway-detail) {
:global(html.light) .root :global(.scan-terminal.light .scan-amos-runway-detail) {
color: #64748B;
}
@@ -1337,3 +1337,60 @@
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── AI prediction dual-card light mode ── */
:global(html.light) .root :global(.scan-ai-prediction-card) {
background: var(--color-bg-raised);
border-color: var(--color-border-default);
}
:global(html.light) .root :global(.scan-ai-prediction-card.ai) {
background: linear-gradient(180deg, rgba(37, 99, 235, 0.08), var(--color-bg-raised));
border-color: rgba(37, 99, 235, 0.22);
}
:global(html.light) .root :global(.scan-ai-prediction-card.deb) {
background: var(--color-bg-raised);
border-color: var(--color-border-default);
}
:global(html.light) .root :global(.scan-ai-prediction-card small) {
color: var(--color-text-muted);
}
:global(html.light) .root :global(.scan-ai-prediction-card strong) {
color: var(--color-text-primary);
}
:global(html.light) .root :global(.scan-ai-prediction-card.ai strong) {
color: var(--color-accent-primary);
}
:global(html.light) .root :global(.scan-ai-prediction-card em) {
color: var(--color-text-muted);
}
:global(html.light) .root :global(.scan-ai-prediction-card.ai.pending) {
background: rgba(37, 99, 235, 0.04);
border-color: rgba(37, 99, 235, 0.12);
}
:global(html.light) .root :global(.scan-ai-prediction-card.ai.pending strong) {
color: var(--color-text-disabled);
}
:global(html.light) .root :global(.scan-ai-confidence) {
background: rgba(37, 99, 235, 0.1);
color: var(--color-accent-primary);
}
:global(html.light) .root :global(.scan-ai-confidence.high) {
background: rgba(16, 185, 129, 0.12);
color: #047857;
}
:global(html.light) .root :global(.scan-ai-confidence.medium) {
background: rgba(245, 158, 11, 0.12);
color: #b45309;
}
@@ -117,6 +117,15 @@ function ScanTerminalScreen() {
}
}, [terminalData?.generated_at]);
useEffect(() => {
if (activeView === "map") {
const timer = setTimeout(() => {
window.dispatchEvent(new Event("resize"));
}, 150);
return () => clearTimeout(timer);
}
}, [activeView]);
const scanTerminalRootClassName = clsx(
styles.root,
scanRootClass,
@@ -520,3 +520,23 @@
@keyframes spin { to { transform: rotate(360deg); } }
/* ── City Jumpbar Light Mode Overrides ── */
:global(html.light) .root :global(.scan-ai-city-jumpbar) {
background: #ffffff;
border-color: #d8e0ec;
box-shadow: var(--shadow-elevation-1);
}
:global(html.light) .root :global(.scan-ai-city-jumpbar button) {
background: var(--color-bg-input);
border-color: var(--color-border-default);
color: var(--color-text-secondary);
}
:global(html.light) .root :global(.scan-ai-city-jumpbar button:hover) {
background: #e2e8f0;
border-color: var(--color-border-hover);
color: var(--color-text-primary);
}
@@ -624,10 +624,6 @@
height: 38px;
}
.root :global(.scan-theme-button) {
width: 38px;
height: 38px;
}
}
/* ── Mobile City Picker ── */
@@ -16,7 +16,7 @@
justify-content: space-between;
gap: 12px;
padding: 6px 0 18px;
border-bottom: 1px solid rgba(118, 146, 188, 0.1);
border-bottom: 1px solid var(--color-border-default);
}
.root :global(.scan-topbar-title) {
@@ -30,12 +30,12 @@
margin: 0;
font-size: 28px;
line-height: 1.08;
letter-spacing: -0.04em;
color: #f3f8ff;
letter-spacing: 0;
color: var(--color-text-primary);
}
.root :global(.scan-topbar-title span) {
color: #8fa4c3;
color: var(--color-text-secondary);
font-size: 14px;
line-height: 1.5;
}
@@ -46,17 +46,17 @@
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
color: #b4c8e2;
color: var(--color-text-secondary);
font-size: 14px;
}
.root :global(.scan-locale-switch) {
height: 36px;
padding: 3px;
border-radius: 12px;
border: 1px solid rgba(99, 132, 180, 0.18);
background: rgba(8, 19, 34, 0.9);
color: #a8bdd8;
border-radius: 10px;
border: 1px solid var(--color-border-default);
background: #e2e8f0;
color: var(--color-text-secondary);
display: inline-flex;
align-items: center;
gap: 2px;
@@ -69,20 +69,20 @@
min-width: 34px;
height: 28px;
padding: 0 8px;
border-radius: 8px;
border-radius: 7px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.root :global(.scan-locale-switch span.active) {
color: #eef7ff;
background: rgba(63, 140, 255, 0.16);
box-shadow: inset 0 0 0 1px rgba(63, 140, 255, 0.22);
color: var(--color-text-primary);
background: #ffffff;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
}
.root :global(.scan-topbar-time) {
color: #b4c8e2;
color: var(--color-text-secondary);
font-variant-numeric: tabular-nums;
}
@@ -92,12 +92,11 @@
gap: 16px;
align-items: center;
margin: -4px 0 6px;
border: 1px solid rgba(77, 163, 255, 0.2);
border-radius: 18px;
border: 1px solid #bfdbfe;
border-radius: 12px;
background:
radial-gradient(circle at top left, rgba(77, 163, 255, 0.18), transparent 36%),
linear-gradient(135deg, rgba(18, 33, 58, 0.96), rgba(15, 23, 42, 0.92));
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.2);
linear-gradient(90deg, rgba(239, 246, 255, 0.98), rgba(255, 255, 255, 0.98));
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.08);
padding: 16px 18px;
}
@@ -106,7 +105,7 @@
}
.root :global(.scan-upgrade-announcement-copy span) {
color: #86efac;
color: #047857;
font-size: 12px;
font-weight: 900;
letter-spacing: 0.04em;
@@ -116,14 +115,14 @@
.root :global(.scan-upgrade-announcement-copy strong) {
display: block;
margin-top: 5px;
color: #f3f8ff;
color: var(--color-text-primary);
font-size: 18px;
line-height: 1.25;
}
.root :global(.scan-upgrade-announcement-copy p) {
margin: 6px 0 0;
color: #b4c8e2;
color: var(--color-text-secondary);
font-size: 13px;
line-height: 1.55;
}
@@ -139,10 +138,10 @@
}
.root :global(.scan-upgrade-announcement li) {
border: 1px solid rgba(77, 163, 255, 0.22);
border: 1px solid #bfdbfe;
border-radius: 999px;
background: rgba(77, 163, 255, 0.1);
color: #d7e5f7;
background: #eff6ff;
color: #1d4ed8;
font-size: 12px;
font-weight: 800;
padding: 7px 10px;
@@ -158,16 +157,16 @@
place-items: center;
border: none;
border-radius: 8px;
background: rgba(148, 163, 184, 0.1);
color: #94a3b8;
background: #f1f5f9;
color: #64748b;
font-size: 14px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.root :global(.scan-announcement-dismiss:hover) {
background: rgba(148, 163, 184, 0.22);
color: #e2e8f0;
background: #e2e8f0;
color: #0f172a;
}
.root :global(.scan-upgrade-announcement) {
@@ -176,14 +175,13 @@
.root :global(.scan-ghost-button),
.root :global(.scan-cta-ghost),
.root :global(.scan-theme-button),
.root :global(.scan-detail-action-button),
.root :global(.scan-detail-icon-button),
.root :global(.scan-view-all-button) {
border: 1px solid rgba(99, 132, 180, 0.18);
background: rgba(8, 19, 34, 0.9);
color: #d7e5f7;
border-radius: 12px;
border: 1px solid var(--color-border-default);
background: var(--color-bg-raised);
color: var(--color-text-primary);
border-radius: 10px;
}
.root :global(.scan-ghost-button),
@@ -202,13 +200,13 @@
min-height: 42px;
padding: 11px 16px;
border: 1px solid rgba(77, 163, 255, 0.42);
border-radius: 12px;
border-radius: 10px;
background: linear-gradient(180deg, var(--color-accent-primary), #3B82F6);
color: #FFFFFF;
font-size: 14px;
font-weight: 900;
text-decoration: none;
box-shadow: 0 10px 24px rgba(59, 130, 246, 0.22);
box-shadow: 0 8px 18px rgba(37, 99, 235, 0.18);
cursor: pointer;
}
@@ -227,18 +225,18 @@
gap: 8px;
min-height: 42px;
padding: 11px 14px;
border: 1px solid rgba(77, 163, 255, 0.3);
border-radius: 12px;
background: linear-gradient(180deg, rgba(77, 163, 255, 0.18), rgba(59, 130, 246, 0.12));
color: #CFE6FF;
border: 1px solid #bfdbfe;
border-radius: 10px;
background: #eff6ff;
color: #1d4ed8;
font-size: 13px;
font-weight: 900;
cursor: pointer;
}
.root :global(.scan-ai-button:hover:not(:disabled)) {
border-color: rgba(111, 183, 255, 0.5);
background: linear-gradient(180deg, rgba(77, 163, 255, 0.26), rgba(59, 130, 246, 0.18));
border-color: #93c5fd;
background: #dbeafe;
}
.root :global(.scan-ai-button:disabled) {
@@ -247,9 +245,9 @@
}
.root :global(.scan-cta-ghost) {
color: var(--color-accent-secondary);
border-color: rgba(23, 217, 139, 0.3);
background: rgba(23, 217, 139, 0.12);
color: #047857;
border-color: #a7f3d0;
background: #ecfdf5;
}
.root :global(.scan-account-button) {
@@ -258,26 +256,19 @@
display: inline-flex;
align-items: center;
justify-content: center;
color: #d7e5f7;
color: var(--color-text-primary);
text-decoration: none;
border: 1px solid rgba(99, 132, 180, 0.18);
background: rgba(8, 19, 34, 0.9);
border-radius: 12px;
border: 1px solid var(--color-border-default);
background: var(--color-bg-raised);
border-radius: 10px;
}
.root :global(.scan-account-button:hover) {
border-color: rgba(63, 140, 255, 0.28);
background: rgba(12, 26, 44, 0.96);
border-color: #94a3b8;
background: #f8fafc;
}
.root :global(.scan-theme-button) {
width: 36px;
height: 36px;
display: inline-grid;
place-items: center;
color: #d7e5f7;
cursor: pointer;
}
.root :global(.scan-ghost-button .spin) {
animation: spin 1s linear infinite;