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