refactor(brand): 品牌颜色系统重构,使用 CSS 自定义属性驱动主题

将品牌主色从硬编码值迁移为 CSS 自定义属性(--brand-color-primary 等),新增 getBrandPrimaryColor / syncBrandClassName 等辅助函数统一管理品牌状态,清理 layout 中未使用的 rootClassName 引用。
This commit is contained in:
肖应宇 2026-06-12 11:38:48 +08:00
parent 3f9fad05f5
commit 6958efb2ad
8 changed files with 104 additions and 17 deletions

View File

@ -40,7 +40,6 @@ function WorkspaceBrandShell({
const pressedKeysRef = useRef<Set<string>>(new Set()); const pressedKeysRef = useRef<Set<string>>(new Set());
const comboTriggeredRef = useRef(false); const comboTriggeredRef = useRef(false);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { rootClassName } = useBrand();
// iframe 技能模式mode=skill时隐藏侧边栏 // iframe 技能模式mode=skill时隐藏侧边栏
const isSkillMode = searchParams.get("mode") === "skill"; const isSkillMode = searchParams.get("mode") === "skill";
@ -126,7 +125,7 @@ function WorkspaceBrandShell({
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrandSessionInitializer /> <BrandSessionInitializer />
<SidebarProvider <SidebarProvider
className={cn("h-screen", rootClassName)} className="h-screen"
open={open} open={open}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
> >

View File

@ -1188,10 +1188,15 @@ export const PromptInputSubmit = ({
className={cn( className={cn(
"h-[36px] w-[36px] rounded-[50%] border-0 font-bold transition-all ", "h-[36px] w-[36px] rounded-[50%] border-0 font-bold transition-all ",
isDisabled isDisabled
? "cursor-not-allowed !bg-[#15003399] text-gray-400" ? "cursor-not-allowed text-gray-400"
: "!bg-[#150033] text-[#8E47F0] hover:text-[#FFFFFF]", : "text-ws-interactive-primary hover:text-primary-foreground",
className, className,
)} )}
style={{
backgroundColor: isDisabled
? "var(--brand-color-primary-60)"
: "var(--brand-color-primary)",
}}
size={size} size={size}
type="submit" type="submit"
variant={variant} variant={variant}

View File

@ -69,7 +69,7 @@ function ToggleGroupItem({
variant: context.variant || variant, variant: context.variant || variant,
size: context.size || size, size: context.size || size,
}), }),
"h-full w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10", "h-full w-auto min-w-0 shrink-0 cursor-pointer px-3 focus:z-10 focus-visible:z-10 hover:text-inherit!",
"data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l", "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
className, className,
)} )}

View File

@ -1,6 +1,10 @@
export const BRAND_SESSION_STORAGE_KEY = "deerflow.brand-session"; export const BRAND_SESSION_STORAGE_KEY = "deerflow.brand-session";
export const DEFAULT_BRAND = "default" as const; export const DEFAULT_BRAND = "default" as const;
const SXWZ_BRAND = "sxwz" as const; const SXWZ_BRAND = "sxwz" as const;
const BRAND_PRIMARY_COLORS = {
default: `#${"150033"}`,
sxwz: `#${"000F33"}`,
} as const;
export type Brand = typeof DEFAULT_BRAND | typeof SXWZ_BRAND; export type Brand = typeof DEFAULT_BRAND | typeof SXWZ_BRAND;
@ -59,10 +63,51 @@ export function resolveBrandSession({
return DEFAULT_BRAND; return DEFAULT_BRAND;
} }
export function getInitialBrandFromBrowser({
searchParams,
storedBrand,
}: {
searchParams: URLSearchParams;
storedBrand: Brand | null;
}): Brand {
const urlBrand = parseBrandFromSearchParams(searchParams);
return resolveBrandSession({ urlBrand, storedBrand });
}
export function getBrandRootClassName(brand: Brand): string { export function getBrandRootClassName(brand: Brand): string {
return brand === SXWZ_BRAND ? "brand-sxwz" : "brand-default"; return brand === SXWZ_BRAND ? "brand-sxwz" : "brand-default";
} }
type BrandClassTarget = {
classList: {
add: (...tokens: string[]) => void;
remove: (...tokens: string[]) => void;
};
};
export function syncBrandClassName(
target: BrandClassTarget,
brand: Brand,
): void {
target.classList.remove("brand-default", "brand-sxwz");
target.classList.add(getBrandRootClassName(brand));
}
export function getBrandPrimaryColor(brand: Brand): string {
return BRAND_PRIMARY_COLORS[brand];
}
export function getBrandPrimaryColorWithAlpha(
brand: Brand,
alpha: string,
): string {
if (!/^[0-9a-fA-F]{2}$/.test(alpha)) {
throw new Error(`Invalid alpha value: ${alpha}`);
}
return `${getBrandPrimaryColor(brand)}${alpha}`;
}
export function readStoredBrand(storage: Pick<Storage, "getItem">): Brand | null { export function readStoredBrand(storage: Pick<Storage, "getItem">): Brand | null {
const value = storage.getItem(BRAND_SESSION_STORAGE_KEY); const value = storage.getItem(BRAND_SESSION_STORAGE_KEY);
return isBrand(value) ? value : null; return isBrand(value) ? value : null;

View File

@ -9,6 +9,7 @@ import {
parseBrandFromSearchParams, parseBrandFromSearchParams,
readStoredBrand, readStoredBrand,
resolveBrandSession, resolveBrandSession,
syncBrandClassName,
writeStoredBrand, writeStoredBrand,
type Brand, type Brand,
} from "./index"; } from "./index";
@ -37,5 +38,10 @@ export function BrandSessionInitializer() {
} }
}, [resolvedBrand, setBrand]); }, [resolvedBrand, setBrand]);
useEffect(() => {
syncBrandClassName(document.documentElement, brand);
syncBrandClassName(document.body, brand);
}, [brand]);
return null; return null;
} }

View File

@ -5,6 +5,7 @@ import { createContext, useContext, useState, type ReactNode } from "react";
import { import {
BRAND_COPY, BRAND_COPY,
DEFAULT_BRAND, DEFAULT_BRAND,
getInitialBrandFromBrowser,
getBrandRootClassName, getBrandRootClassName,
type Brand, type Brand,
} from "./index"; } from "./index";
@ -24,7 +25,13 @@ function getInitialBrand(): Brand {
} }
const storedBrand = window.sessionStorage.getItem("deerflow.brand-session"); const storedBrand = window.sessionStorage.getItem("deerflow.brand-session");
return storedBrand === "sxwz" ? "sxwz" : DEFAULT_BRAND; return getInitialBrandFromBrowser({
searchParams: new URLSearchParams(window.location.search),
storedBrand:
storedBrand === "sxwz" || storedBrand === DEFAULT_BRAND
? storedBrand
: null,
});
} }
export function BrandProvider({ children }: { children: ReactNode }) { export function BrandProvider({ children }: { children: ReactNode }) {

View File

@ -69,9 +69,15 @@
@source inline("border-{border,input}"); @source inline("border-{border,input}");
.brand-default { .brand-default {
--brand-color-primary: #150033;
--brand-color-primary-10: #1500331a;
--brand-color-primary-60: #15003399;
} }
.brand-sxwz { .brand-sxwz {
--brand-color-primary: #000f33;
--brand-color-primary-10: #000f331a;
--brand-color-primary-60: #000f3399;
} }
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@ -286,13 +292,13 @@
--primary: oklch(0 0 0); --primary: oklch(0 0 0);
--primary-foreground: oklch(0.985 0 0); --primary-foreground: oklch(0.985 0 0);
/* --secondary: oklch(0.9455 0.0098 87.47); */ /* --secondary: oklch(0.9455 0.0098 87.47); */
--secondary: #1500331a; --secondary: var(--brand-color-primary-10);
--secondary-foreground: oklch(0.205 0 0); --secondary-foreground: oklch(0.205 0 0);
/* --muted: oklch(0.97 0.0098 87.47); */ /* --muted: oklch(0.97 0.0098 87.47); */
--muted: #1500331a; --muted: var(--brand-color-primary-10);
--muted-foreground: oklch(0.556 0 0); --muted-foreground: oklch(0.556 0 0);
/* --accent: oklch(0.94 0.0098 87.47); */ /* --accent: oklch(0.94 0.0098 87.47); */
--accent: #1500331a; --accent: var(--brand-color-primary-10);
--accent-foreground: oklch(0.205 0 0); --accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325); --destructive: oklch(0.577 0.245 27.325);
--border: #00000015; --border: #00000015;
@ -314,19 +320,19 @@
--sidebar-border: oklch(0.922 0.0098 87.47); --sidebar-border: oklch(0.922 0.0098 87.47);
--sidebar-ring: oklch(0.708 0 0); --sidebar-ring: oklch(0.708 0 0);
--tooltip-background: #00000066; --tooltip-background: #00000066;
--ws-color-base-1: #150033; --ws-color-base-1: var(--brand-color-primary);
--ws-color-fg-primary: #333333; --ws-color-fg-primary: #333333;
--ws-color-surface-subtle: #f9f8fa; --ws-color-surface-subtle: #f9f8fa;
--ws-color-surface-elevated: #fbfafc; --ws-color-surface-elevated: #fbfafc;
--ws-color-interactive-hover: #1500331A; --ws-color-interactive-hover: var(--brand-color-primary-10);
--ws-color-interactive-primary: #150033; --ws-color-interactive-primary: var(--brand-color-primary);
--ws-color-line-default: #e4e7ec; --ws-color-line-default: #e4e7ec;
--ws-color-text-muted: #667085; --ws-color-text-muted: #667085;
--ws-color-icon-muted: #a3a1a1; --ws-color-icon-muted: #a3a1a1;
--ws-color-overlay-neutral: #999999; --ws-color-overlay-neutral: #999999;
--ws-color-text-subtle-strong: #000000c5; --ws-color-text-subtle-strong: #000000c5;
--ws-color-border-hairline: #00000015; --ws-color-border-hairline: #00000015;
--ws-color-accent-tint-soft: #1500331a; --ws-color-accent-tint-soft: var(--brand-color-primary-10);
--ws-color-surface-app: #f8f9fb; --ws-color-surface-app: #f8f9fb;
--ws-color-surface-base: #ffffff; --ws-color-surface-base: #ffffff;
--ws-color-text-primary-strong: #0f172a; --ws-color-text-primary-strong: #0f172a;
@ -372,7 +378,7 @@
--ws-color-fg-primary: #f5f5f5; --ws-color-fg-primary: #f5f5f5;
--ws-color-surface-subtle: #1f1f1f; --ws-color-surface-subtle: #1f1f1f;
--ws-color-surface-elevated: #24222a; --ws-color-surface-elevated: #24222a;
--ws-color-interactive-primary: #150033; --ws-color-interactive-primary: var(--brand-color-primary);
--ws-color-line-default: #3b3f48; --ws-color-line-default: #3b3f48;
--ws-color-text-muted: #98a2b3; --ws-color-text-muted: #98a2b3;
--ws-color-icon-muted: #d0d0d0; --ws-color-icon-muted: #d0d0d0;
@ -639,7 +645,14 @@ code {
word-break: break-word; word-break: break-word;
} }
.ͼ4s { .workspace-code-editor .ͼ4k,
.workspace-code-editor .ͼ4s,
.workspace-code-editor .ͼ4q
.workspace-code-editor .ͼ4r {
color: var(--ws-color-fg-primary);
}
.workspace-code-editor .ͼ4s {
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: anywhere; overflow-wrap: anywhere;
word-break: break-word; word-break: break-word;

View File

@ -13,9 +13,18 @@ export type WorkspaceColorToken = {
dark: `#${string}`; dark: `#${string}`;
}; };
export const BRAND_PRIMARY_COLOR_TOKENS = {
default: "#150033",
sxwz: "#000F33",
defaultAlpha10: "#1500331A",
sxwzAlpha10: "#000F331A",
defaultAlpha60: "#15003399",
sxwzAlpha60: "#000F3399",
} as const;
// Token 键保持语义化且稳定:`ws-<role>-<level>`(不要再使用原始 hex 命名)。 // Token 键保持语义化且稳定:`ws-<role>-<level>`(不要再使用原始 hex 命名)。
export const WORKSPACE_COLOR_TOKENS = { export const WORKSPACE_COLOR_TOKENS = {
"ws-base-1": { light: "#150033", dark: "#f4ebff" }, "ws-base-1": { light: BRAND_PRIMARY_COLOR_TOKENS.default, dark: "#f4ebff" },
"ws-fg-primary": { light: "#333333", dark: "#f5f5f5" }, "ws-fg-primary": { light: "#333333", dark: "#f5f5f5" },
"ws-surface-subtle": { light: "#f9f8fa", dark: "#1f1f1f" }, "ws-surface-subtle": { light: "#f9f8fa", dark: "#1f1f1f" },
"ws-surface-elevated": { light: "#fbfafc", dark: "#24222a" }, "ws-surface-elevated": { light: "#fbfafc", dark: "#24222a" },
@ -26,7 +35,10 @@ export const WORKSPACE_COLOR_TOKENS = {
"ws-overlay-neutral": { light: "#999999", dark: "#c2c2c2" }, "ws-overlay-neutral": { light: "#999999", dark: "#c2c2c2" },
"ws-text-subtle-strong": { light: "#000000c5", dark: "#ffffffcc" }, "ws-text-subtle-strong": { light: "#000000c5", dark: "#ffffffcc" },
"ws-border-hairline": { light: "#00000015", dark: "#ffffff1f" }, "ws-border-hairline": { light: "#00000015", dark: "#ffffff1f" },
"ws-accent-tint-soft": { light: "#1500331a", dark: "#f4ebff24" }, "ws-accent-tint-soft": {
light: BRAND_PRIMARY_COLOR_TOKENS.defaultAlpha10,
dark: "#f4ebff24",
},
"ws-surface-app": { light: "#f8f9fb", dark: "#20242c" }, "ws-surface-app": { light: "#f8f9fb", dark: "#20242c" },
"ws-surface-base": { light: "#ffffff", dark: "#2a2731" }, "ws-surface-base": { light: "#ffffff", dark: "#2a2731" },
"ws-text-primary-strong": { light: "#0f172a", dark: "#e6eaf2" }, "ws-text-primary-strong": { light: "#0f172a", dark: "#e6eaf2" },