feat: implement HeaderBar and ScanTerminal UI layout components for dashboard navigation and decision workspace
This commit is contained in:
+61
-110
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user