feat: implement HeaderBar and ScanTerminal UI layout components for dashboard navigation and decision workspace

This commit is contained in:
2569718930@qq.com
2026-05-24 22:38:43 +08:00
parent 4832678d72
commit 32a3ba0ea4
5 changed files with 67 additions and 179 deletions
+61 -110
View File
@@ -9,28 +9,28 @@
@layer base { @layer base {
:root { :root {
/* ── Background Scale ── */ /* ── Background Scale ── */
--color-bg-base: #0B1220; --color-bg-base: #f4f7fb;
--color-bg-raised: #111A2E; --color-bg-raised: #ffffff;
--color-bg-overlay: #16213A; --color-bg-overlay: #ffffff;
--color-bg-card: rgba(17, 26, 46, 0.88); --color-bg-card: rgba(255, 255, 255, 0.95);
--color-bg-input: rgba(22, 33, 58, 0.72); --color-bg-input: rgba(241, 245, 249, 0.88);
/* ── Text Scale ── */ /* ── Text Scale ── */
--color-text-primary: #E6EDF3; --color-text-primary: #0F172A;
--color-text-secondary: #9FB2C7; --color-text-secondary: #334155;
--color-text-muted: #7D8FA3; --color-text-muted: #475569;
--color-text-disabled: #7D8FA3; --color-text-disabled: #94A3B8;
/* ── Accent Colors ── */ /* ── Accent Colors ── */
--color-accent-primary: #4DA3FF; --color-accent-primary: #2563EB;
--color-accent-secondary: #6FB7FF; --color-accent-secondary: #3B82F6;
--color-accent-tertiary: #93C5FD; --color-accent-tertiary: #60A5FA;
/* ── Signal / Semantic Colors ── */ /* ── Signal / Semantic Colors ── */
--color-signal-success: #22C55E; --color-signal-success: #00897b;
--color-signal-warning: #F59E0B; --color-signal-warning: #d97706;
--color-signal-danger: #EF4444; --color-signal-danger: #dc2626;
--color-signal-info: #4DA3FF; --color-signal-info: #2563eb;
/* ── Risk Colors (aliased from signal) ── */ /* ── Risk Colors (aliased from signal) ── */
--color-risk-high: var(--color-signal-danger); --color-risk-high: var(--color-signal-danger);
@@ -38,16 +38,16 @@
--color-risk-low: var(--color-signal-success); --color-risk-low: var(--color-signal-success);
/* ── Border ── */ /* ── Border ── */
--color-border-default: rgba(159, 178, 199, 0.16); --color-border-default: #d8e0ec;
--color-border-hover: rgba(77, 163, 255, 0.38); --color-border-hover: #b8c4d6;
--color-border-subtle: rgba(159, 178, 199, 0.08); --color-border-subtle: #e8edf5;
/* ── Shadow / Elevation ── */ /* ── Shadow / Elevation ── */
--shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.3); --shadow-elevation-1: 0 1px 3px rgba(15, 23, 42, 0.05);
--shadow-elevation-2: 0 8px 24px rgba(0, 0, 0, 0.45); --shadow-elevation-2: 0 8px 24px rgba(15, 23, 42, 0.06);
--shadow-elevation-3: 0 20px 60px rgba(0, 0, 0, 0.6); --shadow-elevation-3: 0 20px 60px rgba(15, 23, 42, 0.08);
--shadow-glow-accent: 0 0 20px rgba(77, 163, 255, 0.24); --shadow-glow-accent: 0 0 20px rgba(37, 99, 235, 0.08);
--shadow-glow-secondary: 0 0 20px rgba(111, 183, 255, 0.22); --shadow-glow-secondary: 0 0 20px rgba(96, 165, 250, 0.08);
/* ── Typography ── */ /* ── Typography ── */
--font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
@@ -66,19 +66,19 @@
--space-12: 48px; --space-12: 48px;
/* ── Border Radius ── */ /* ── Border Radius ── */
--radius-sm: 6px; --radius-sm: 4px;
--radius-md: 10px; --radius-md: 6px;
--radius-lg: 14px; --radius-lg: 10px;
--radius-xl: 20px; --radius-xl: 14px;
--radius-full: 9999px; --radius-full: 9999px;
/* ── Glass / Blur ── */ /* ── Glass / Blur ── */
--glass-blur-1: blur(10px); --glass-blur-1: blur(10px);
--glass-blur-2: blur(16px); --glass-blur-2: blur(16px);
--glass-blur-3: blur(24px); --glass-blur-3: blur(24px);
--glass-opacity-1: 0.72; --glass-opacity-1: 0.86;
--glass-opacity-2: 0.85; --glass-opacity-2: 0.92;
--glass-opacity-3: 0.92; --glass-opacity-3: 0.96;
/* ── Layout ── */ /* ── Layout ── */
--header-height: 52px; --header-height: 52px;
@@ -91,8 +91,7 @@
--transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1); --transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1);
--transition: var(--transition-base); --transition: var(--transition-base);
/* ── Legacy Variable Aliases (bridge for older CSS modules) ── */ /* ── Legacy Variable Aliases ── */
/* These map old shorthand names → current design token names. */
--accent-cyan: var(--color-accent-primary); --accent-cyan: var(--color-accent-primary);
--accent-blue: var(--color-accent-secondary); --accent-blue: var(--color-accent-secondary);
--accent-green: var(--color-signal-success); --accent-green: var(--color-signal-success);
@@ -111,68 +110,36 @@
--shadow-lg: var(--shadow-elevation-2); --shadow-lg: var(--shadow-elevation-2);
--glass-blur: 10px; --glass-blur: 10px;
/* ── shadcn/ui Tokens (used by Tailwind @apply border-border) ── */ /* ── shadcn/ui Tokens ── */
--background: 223 53% 4%; --background: 210 40% 98%;
--foreground: 210 40% 98%; --foreground: 222 47% 12%;
--card: 223 46% 8%; --card: 0 0% 100%;
--card-foreground: 210 40% 98%; --card-foreground: 222 47% 12%;
--primary: 159 100% 44%; --primary: 221 83% 53%;
--primary-foreground: 222 47% 8%; --primary-foreground: 210 40% 98%;
--secondary: 224 30% 14%; --secondary: 210 40% 96%;
--secondary-foreground: 210 40% 98%; --secondary-foreground: 222 47% 12%;
--accent: 217 30% 18%; --accent: 210 40% 96%;
--accent-foreground: 210 40% 98%; --accent-foreground: 222 47% 12%;
--border: 221 38% 22%; --border: 214 32% 91%;
} }
/* ── Light Theme Token Overrides ── */ /* ── Monospaced numbers & data globally for professional feel ── */
html.light, .font-mono,
html[data-theme="light"] { .nearby-temp,
--color-bg-base: #F7F9FC; .nearby-wind,
--color-bg-raised: #EEF2F7; .nearby-time,
--color-bg-overlay: #FFFFFF; .nearby-marker,
--color-bg-card: rgba(255, 255, 255, 0.92); .marker-bubble,
--color-bg-input: rgba(238, 242, 247, 0.88); .map-pill,
[class*="temp"],
--color-text-primary: #0F172A; [class*="value"],
--color-text-secondary: #334155; [class*="price"],
--color-text-muted: #475569; [class*="number"],
--color-text-disabled: #94A3B8; [class*="stat"],
[class*="score"],
--color-accent-primary: #2563EB; [class*="time-"] {
--color-accent-secondary: #3B82F6; font-family: var(--font-mono) !important;
--color-accent-tertiary: #60A5FA;
--color-border-default: rgba(148, 163, 184, 0.24);
--color-border-hover: rgba(37, 99, 235, 0.38);
--color-border-subtle: rgba(148, 163, 184, 0.12);
--shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.1);
--shadow-elevation-2: 0 8px 24px rgba(40, 70, 110, 0.12);
--shadow-elevation-3: 0 20px 60px rgba(40, 70, 110, 0.15);
--shadow-glow-accent: 0 0 20px rgba(37, 99, 235, 0.14);
--shadow-glow-secondary: 0 0 20px rgba(96, 165, 250, 0.12);
--glass-blur-1: blur(10px);
--glass-blur-2: blur(16px);
--glass-blur-3: blur(24px);
--glass-opacity-1: 0.86;
--glass-opacity-2: 0.92;
--glass-opacity-3: 0.96;
/* ── Legacy alias overrides for light theme ── */
--accent-cyan: var(--color-accent-primary);
--accent-blue: var(--color-accent-secondary);
--bg-primary: var(--color-bg-base);
--bg-secondary: var(--color-bg-raised);
--bg-card: var(--color-bg-card);
--bg-glass: var(--color-bg-card);
--border-glass: var(--color-border-default);
--border-subtle: var(--color-border-subtle);
--text-primary: var(--color-text-primary);
--text-secondary: var(--color-text-secondary);
--text-muted: var(--color-text-muted);
--shadow-lg: var(--shadow-elevation-2);
} }
* { * {
@@ -207,23 +174,7 @@
body { body {
font-family: var(--font-data); font-family: var(--font-data);
background: background: var(--color-bg-base);
radial-gradient(
circle at 10% -10%,
rgba(0, 224, 164, 0.1),
transparent 40%
),
radial-gradient(
circle at 90% 0%,
rgba(123, 97, 255, 0.08),
transparent 36%
),
radial-gradient(
circle at 80% 100%,
rgba(0, 224, 164, 0.06),
transparent 48%
),
var(--color-bg-base);
color: var(--color-text-primary); color: var(--color-text-primary);
} }
} }
+1 -1
View File
@@ -41,7 +41,7 @@ export default function RootLayout({
return ( return (
<html <html
lang="zh-CN" lang="zh-CN"
className={`${inter.variable} ${jetbrainsMono.variable} dark`} className={`${inter.variable} ${jetbrainsMono.variable} light`}
> >
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -10,8 +10,6 @@ import {
RotateCw, RotateCw,
BookOpen, BookOpen,
MoreHorizontal, MoreHorizontal,
Moon,
Sun,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDashboardStore, useProAccess } from "@/hooks/useDashboardStore"; import { useDashboardStore, useProAccess } from "@/hooks/useDashboardStore";
@@ -42,7 +40,6 @@ export function HeaderBar({
const { proAccess } = useProAccess(); const { proAccess } = useProAccess();
const { locale, t, toggleLocale } = useI18n(); const { locale, t, toggleLocale } = useI18n();
const pathname = usePathname(); const pathname = usePathname();
const [theme, setTheme] = useState<"dark" | "light">("dark");
const isAuthenticated = proAccess.authenticated; const isAuthenticated = proAccess.authenticated;
const docsHref = "/docs/intro"; const docsHref = "/docs/intro";
const docsActive = pathname?.startsWith("/docs"); const docsActive = pathname?.startsWith("/docs");
@@ -94,24 +91,6 @@ export function HeaderBar({
? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left` ? `Pro ${Math.max(expiryInfo?.daysLeft || 0, 0)}d left`
: `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)}`; : `Pro 还剩 ${Math.max(expiryInfo?.daysLeft || 0, 0)}`;
useEffect(() => {
const savedTheme =
typeof window !== "undefined"
? window.localStorage.getItem("polyweather_theme")
: null;
const nextTheme = savedTheme === "light" ? "light" : "dark";
setTheme(nextTheme);
document.documentElement.classList.toggle("dark", nextTheme === "dark");
document.documentElement.classList.toggle("light", nextTheme === "light");
}, []);
const toggleTheme = () => {
const nextTheme = theme === "dark" ? "light" : "dark";
setTheme(nextTheme);
document.documentElement.classList.toggle("dark", nextTheme === "dark");
document.documentElement.classList.toggle("light", nextTheme === "light");
window.localStorage.setItem("polyweather_theme", nextTheme);
};
return ( return (
<header className="header"> <header className="header">
@@ -165,19 +144,6 @@ export function HeaderBar({
<RotateCw size={16} strokeWidth={2} /> <RotateCw size={16} strokeWidth={2} />
</button> </button>
<button
type="button"
className="header-utility-btn"
aria-label={theme === "dark" ? "切换到明亮模式" : "切换到暗黑模式"}
title={theme === "dark" ? "明亮模式" : "暗黑模式"}
onClick={toggleTheme}
>
{theme === "dark" ? (
<Sun size={15} strokeWidth={2} />
) : (
<Moon size={15} strokeWidth={2} />
)}
</button>
<Link <Link
href={docsHref} href={docsHref}
@@ -122,19 +122,7 @@ export function ScanTerminalTopBar({
{isEn ? "Sign in" : "登录"} {isEn ? "Sign in" : "登录"}
</Link> </Link>
)} )}
<button
type="button"
className="scan-theme-button"
aria-label={
themeMode === "light" ? "切换到暗色模式" : "切换到明亮模式"
}
title={themeMode === "light" ? "切换到暗色模式" : "切换到明亮模式"}
onClick={() =>
setThemeMode((current) => (current === "light" ? "dark" : "light"))
}
>
{themeMode === "light" ? <Moon size={15} /> : <Sun size={15} />}
</button>
{isAuthenticated ? ( {isAuthenticated ? (
<Link <Link
href={accountHref} href={accountHref}
@@ -20,30 +20,13 @@ export function useUserLocalClock() {
} }
export function useScanTerminalTheme() { export function useScanTerminalTheme() {
const [themeMode, setThemeMode] = useState<ThemeMode>("dark"); const [themeMode] = useState<ThemeMode>("light");
useEffect(() => { useEffect(() => {
const root = document.documentElement; const root = document.documentElement;
const hadLight = root.classList.contains("light"); root.classList.add("light");
const hadDark = root.classList.contains("dark"); root.classList.remove("dark");
root.classList.toggle("light", themeMode === "light");
root.classList.toggle("dark", themeMode === "dark");
return () => {
root.classList.toggle("light", hadLight);
root.classList.toggle("dark", hadDark);
};
}, [themeMode]);
useEffect(() => {
const stored = window.localStorage.getItem("polyweather_scan_theme");
if (stored === "light") {
setThemeMode("light");
}
}, []); }, []);
useEffect(() => { return { setThemeMode: () => {}, themeMode };
window.localStorage.setItem("polyweather_scan_theme", themeMode);
}, [themeMode]);
return { setThemeMode, themeMode };
} }