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

File diff suppressed because one or more lines are too long

View File

@@ -540,6 +540,135 @@ nextjs-portal {
color: #fff; 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 { .login-hero {
isolation: isolate; isolation: isolate;
color: #282828; color: #282828;

View File

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

View File

@@ -4,7 +4,7 @@ import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState }
import { createPortal } from "react-dom" import { createPortal } from "react-dom"
import { import {
AlertTriangle, Check, ChevronDown, Circle, Film, FileText, Image as ImageIcon, Info, Link2, Loader2, 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" } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { import {
@@ -78,6 +78,9 @@ const VIDEO_MODELS = [
] as const ] as const
type VideoModel = (typeof VIDEO_MODELS)[number]["value"] type VideoModel = (typeof VIDEO_MODELS)[number]["value"]
type BoardThemeMode = "dark" | "light"
const BOARD_THEME_STORAGE_KEY = "skg-board-theme"
type DraftSegment = { type DraftSegment = {
id: string id: string
@@ -1268,6 +1271,7 @@ export function AdRecreationBoard({
const [sixViewBusyKey, setSixViewBusyKey] = useState<string | null>(null) const [sixViewBusyKey, setSixViewBusyKey] = useState<string | null>(null)
const [generatingAll, setGeneratingAll] = useState(false) const [generatingAll, setGeneratingAll] = useState(false)
const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>() const [runtimeModels, setRuntimeModels] = useState<RuntimeModels | undefined>()
const [boardTheme, setBoardTheme] = useState<BoardThemeMode>("dark")
const fileRef = useRef<HTMLInputElement | null>(null) const fileRef = useRef<HTMLInputElement | null>(null)
const selectedFrames = job const selectedFrames = job
? job.frames.filter((frame) => data.selectedFrames.has(frame.index)).sort((a, b) => a.timestamp - b.timestamp) ? 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()) setSelectedVideoIds(new Set())
}, [activeJobId]) }, [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(() => { useEffect(() => {
let cancelled = false let cancelled = false
getRuntimeHealth() getRuntimeHealth()
@@ -1334,6 +1347,18 @@ export function AdRecreationBoard({
if (trimmed) setUrl("") 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 = () => { const selectAllFrames = () => {
if (!job) return if (!job) return
for (const frame of job.frames) { for (const frame of job.frames) {
@@ -1477,7 +1502,7 @@ export function AdRecreationBoard({
} }
return ( 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="skg-board-ambient pointer-events-none absolute inset-0" />
<div className="relative z-10 flex h-full flex-col px-4 py-4"> <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"> <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,6 +1510,16 @@ export function AdRecreationBoard({
<div className="text-[11px] font-medium uppercase tracking-[0.18em] text-white/40">feed ad recreation worksheet</div> <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> <h1 className="mt-1 text-[22px] font-semibold leading-tight text-white">广</h1>
</div> </div>
<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"> <div className="grid min-w-[520px] grid-cols-5 gap-2 text-[11px] text-white/48">
<Metric label="素材" value={`${jobs.length}`} /> <Metric label="素材" value={`${jobs.length}`} />
<Metric label="当前" value={shortId(activeJobId)} /> <Metric label="当前" value={shortId(activeJobId)} />
@@ -1492,6 +1527,7 @@ export function AdRecreationBoard({
<Metric label="文案段" value={`${transcriptCount}`} /> <Metric label="文案段" value={`${transcriptCount}`} />
<Metric label="背景音" value={backgroundReady ? "ready" : "-"} /> <Metric label="背景音" value={backgroundReady ? "ready" : "-"} />
</div> </div>
</div>
</header> </header>
<WorkflowOrderBar steps={workflowSteps} /> <WorkflowOrderBar steps={workflowSteps} />