style: add board light mode

This commit is contained in:
2026-05-18 16:51:34 +08:00
parent cdffc4ba08
commit 78bd294d57
4 changed files with 188 additions and 16 deletions

View File

@@ -540,6 +540,135 @@ nextjs-portal {
color: #fff;
}
.skg-board-theme--light {
color: #22261f;
background:
radial-gradient(circle at 12% 0%, rgba(214, 179, 106, 0.18), transparent 30%),
radial-gradient(circle at 82% 8%, rgba(143, 176, 113, 0.14), transparent 28%),
linear-gradient(126deg, #f7f4ea 0%, #eef1e7 48%, #f9f6ee 100%);
}
.skg-board-theme--light::before {
background:
linear-gradient(90deg, rgba(42, 50, 36, 0.05) 1px, transparent 1px),
linear-gradient(180deg, rgba(42, 50, 36, 0.045) 1px, transparent 1px);
opacity: 0.72;
}
.skg-board-theme--light::after {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.36), transparent 46%, rgba(214, 179, 106, 0.08)),
linear-gradient(90deg, rgba(255, 255, 255, 0.3), transparent 42%, rgba(255, 255, 255, 0.24));
}
.skg-board-theme--light .skg-board-ambient {
background:
radial-gradient(circle at 20% 18%, rgba(214, 179, 106, 0.2), transparent 28%),
radial-gradient(circle at 70% 6%, rgba(143, 176, 113, 0.16), transparent 30%),
radial-gradient(circle at 52% 100%, rgba(214, 179, 106, 0.12), transparent 38%);
}
.skg-board-theme--light .skg-board-topbar,
.skg-board-theme--light .skg-board-panel {
border-color: rgba(82, 93, 62, 0.16) !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.78), rgba(255, 255, 255, 0.48)),
rgba(249, 246, 236, 0.7) !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.78),
0 18px 48px rgba(65, 55, 30, 0.1);
}
.skg-board-theme--light .skg-board-topbar {
background:
linear-gradient(100deg, rgba(214, 179, 106, 0.14), rgba(143, 176, 113, 0.08) 42%, rgba(255, 255, 255, 0.58)),
rgba(252, 249, 241, 0.82) !important;
}
.skg-board-theme--light .skg-board-theme-toggle {
border-color: rgba(82, 93, 62, 0.16) !important;
background: rgba(255, 255, 255, 0.54) !important;
color: rgba(36, 40, 30, 0.72) !important;
}
.skg-board-theme--light .text-white,
.skg-board-theme--light [class*="text-white/"] {
color: rgba(32, 36, 28, 0.78) !important;
}
.skg-board-theme--light [class*="bg-black/"],
.skg-board-theme--light [class*="bg-white/"] {
background-color: rgba(255, 255, 250, 0.52) !important;
}
.skg-board-theme--light [class*="border-white/"] {
border-color: rgba(70, 78, 54, 0.14) !important;
}
.skg-board-theme--light [class*="text-[#d7efbc]"] {
color: #43662d !important;
}
.skg-board-theme--light [class*="text-[#e8c77a]"],
.skg-board-theme--light [class*="text-[#f2d58a]"],
.skg-board-theme--light [class*="text-[#f5d98e]"] {
color: #856015 !important;
}
.skg-board-theme--light [class*="text-emerald-"] {
color: #2f6d3d !important;
}
.skg-board-theme--light [class*="text-cyan-"],
.skg-board-theme--light [class*="text-sky-"],
.skg-board-theme--light [class*="text-teal-"] {
color: #17606f !important;
}
.skg-board-theme--light [class*="text-amber-"],
.skg-board-theme--light [class*="text-yellow-"] {
color: #8a5c00 !important;
}
.skg-board-theme--light [class*="text-rose-"],
.skg-board-theme--light [class*="text-red-"] {
color: #9f1239 !important;
}
.skg-board-theme--light [class*="text-violet-"],
.skg-board-theme--light [class*="text-purple-"] {
color: #62438a !important;
}
.skg-board-theme--light [class*="border-[#8fb071]"] {
border-color: rgba(67, 102, 45, 0.28) !important;
}
.skg-board-theme--light [class*="border-[#d6b36a]"] {
border-color: rgba(133, 96, 21, 0.26) !important;
}
.skg-board-theme--light [class*="bg-[#8fb071]"],
.skg-board-theme--light [class*="bg-[#d6b36a]"] {
background-color: rgba(214, 179, 106, 0.14) !important;
}
.skg-board-theme--light input,
.skg-board-theme--light textarea,
.skg-board-theme--light select {
color: #22261f !important;
}
.skg-board-theme--light input::placeholder,
.skg-board-theme--light textarea::placeholder {
color: rgba(34, 38, 31, 0.36) !important;
}
.skg-board-theme--light ::selection {
background: rgba(214, 179, 106, 0.32);
color: #171a14;
}
.login-hero {
isolation: isolate;
color: #282828;

View File

@@ -1,6 +1,5 @@
"use client"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { useTheme } from "next-themes"
import {
ReactFlow, Background, BackgroundVariant, Controls,
useNodesState, useEdgesState,
@@ -14,7 +13,6 @@ import {
type CanvasPanelDock,
type NodeData,
} from "@/components/nodes"
import { ThemeToggle } from "@/components/theme-toggle"
import { AdRecreationBoard } from "@/components/ad-recreation-board"
import {
addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteJob, deleteFrame, deleteGeneratedImage,
@@ -144,7 +142,6 @@ const EDGES_RAW: Array<[string, string]> = [
]
export default function Home() {
const { resolvedTheme } = useTheme()
const [jobs, setJobs] = useState<Job[]>([])
const [activeJobId, setActiveJobId] = useState<string | null>(null)
const job = useMemo(() => jobs.find((j) => j.id === activeJobId) ?? null, [jobs, activeJobId])
@@ -1199,9 +1196,6 @@ export default function Home() {
<div className="canvas-bg" />
<main className="relative flex h-screen w-screen overflow-hidden">
<AdRecreationBoard data={nodeData} onGenerateVideo={handleQuickGenerateVideo} />
<div className="absolute bottom-4 right-4 z-30 pointer-events-auto">
<ThemeToggle />
</div>
<Toaster theme="system" position="top-center" />
</main>
</>

View File

@@ -4,7 +4,7 @@ import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState }
import { createPortal } from "react-dom"
import {
AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2,
Mic, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Sparkles, Trash2, Upload, Wand2,
Mic, Moon, Package, PanelRight, Play, Plus, RefreshCw, Scissors, Sparkles, Sun, Trash2, Upload, Wand2,
} from "lucide-react"
import { toast } from "sonner"
import {
@@ -78,6 +78,9 @@ const VIDEO_MODELS = [
] as const
type VideoModel = (typeof VIDEO_MODELS)[number]["value"]
type BoardThemeMode = "dark" | "light"
const BOARD_THEME_STORAGE_KEY = "skg-board-theme"
type DraftSegment = {
id: string
@@ -1268,6 +1271,7 @@ export function AdRecreationBoard({
const [sixViewBusyKey, setSixViewBusyKey] = useState<string | null>(null)
const [generatingAll, setGeneratingAll] = useState(false)
const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>()
const [boardTheme, setBoardTheme] = useState<BoardThemeMode>("dark")
const fileRef = useRef<HTMLInputElement | null>(null)
const selectedFrames = job
? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp)
@@ -1307,6 +1311,15 @@ export function AdRecreationBoard({
setSelectedVideoIds(new Set())
}, [activeJobId])
useEffect(() => {
try {
const saved = window.localStorage.getItem(BOARD_THEME_STORAGE_KEY)
if (saved === "light" || saved === "dark") setBoardTheme(saved)
} catch {
// Ignore storage failures; dark mode remains the product default.
}
}, [])
useEffect(() => {
let cancelled = false
getRuntimeHealth()
@@ -1334,6 +1347,18 @@ export function AdRecreationBoard({
if (trimmed) setUrl("")
}
const toggleBoardTheme = () => {
setBoardTheme((current) => {
const next: BoardThemeMode = current === "dark" ? "light" : "dark"
try {
window.localStorage.setItem(BOARD_THEME_STORAGE_KEY, next)
} catch {
// Ignore storage failures; the in-memory theme still switches.
}
return next
})
}
const selectAllFrames = () => {
if (!job) return
for (const frame of job.frames) {
@@ -1477,7 +1502,7 @@ export function AdRecreationBoard({
}
return (
<section className="skg-board-theme relative z-20 h-screen w-screen overflow-hidden bg-black text-white">
<section className={`skg-board-theme ${boardTheme === "light" ? "skg-board-theme--light" : ""} relative z-20 h-screen w-screen overflow-hidden bg-black text-white`}>
<div className="skg-board-ambient pointer-events-none absolute inset-0" />
<div className="relative z-10 flex h-full flex-col px-4 py-4">
<header className="skg-board-topbar mb-3 flex items-center justify-between gap-4 rounded-lg border border-white/10 bg-white/[0.04] px-4 py-3">
@@ -1485,12 +1510,23 @@ export function AdRecreationBoard({
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/40">feed ad recreation worksheet</div>
<h1 className="mt-1 text-[22px] font-semibold leading-tight text-white">广</h1>
</div>
<div className="grid min-w-[520px] grid-cols-5 gap-2 text-[11px] text-white/48">
<Metric label="素材" value={`${jobs.length}`} />
<Metric label="当前" value={shortId(activeJobId)} />
<Metric label="视频" value={job?.video_url ? "ready" : "-"} />
<Metric label="文案段" value={`${transcriptCount}`} />
<Metric label="背景音" value={backgroundReady ? "ready" : "-"} />
<div className="flex shrink-0 items-center gap-2">
<button
type="button"
onClick={toggleBoardTheme}
className="skg-board-theme-toggle inline-flex h-10 items-center gap-1.5 rounded-md border border-white/10 bg-black/24 px-3 text-[11px] font-semibold text-white/62 transition hover:border-[#d6b36a]/45 hover:text-white"
title={boardTheme === "dark" ? "切换到明亮模式" : "切换到暗色模式"}
>
{boardTheme === "dark" ? <Sun className="h-3.5 w-3.5" /> : <Moon className="h-3.5 w-3.5" />}
{boardTheme === "dark" ? "明亮" : "暗色"}
</button>
<div className="grid min-w-[520px] grid-cols-5 gap-2 text-[11px] text-white/48">
<Metric label="素材" value={`${jobs.length}`} />
<Metric label="当前" value={shortId(activeJobId)} />
<Metric label="视频" value={job?.video_url ? "ready" : "-"} />
<Metric label="文案段" value={`${transcriptCount}`} />
<Metric label="背景音" value={backgroundReady ? "ready" : "-"} />
</div>
</div>
</header>