refactor(brand): 品牌颜色系统重构,使用 CSS 自定义属性驱动主题
将品牌主色从硬编码值迁移为 CSS 自定义属性(--brand-color-primary 等),新增 getBrandPrimaryColor / syncBrandClassName 等辅助函数统一管理品牌状态,清理 layout 中未使用的 rootClassName 引用。
This commit is contained in:
parent
3f9fad05f5
commit
6958efb2ad
@ -40,7 +40,6 @@ function WorkspaceBrandShell({
|
||||
const pressedKeysRef = useRef<Set<string>>(new Set());
|
||||
const comboTriggeredRef = useRef(false);
|
||||
const searchParams = useSearchParams();
|
||||
const { rootClassName } = useBrand();
|
||||
|
||||
// iframe 技能模式(mode=skill)时隐藏侧边栏
|
||||
const isSkillMode = searchParams.get("mode") === "skill";
|
||||
@ -126,7 +125,7 @@ function WorkspaceBrandShell({
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrandSessionInitializer />
|
||||
<SidebarProvider
|
||||
className={cn("h-screen", rootClassName)}
|
||||
className="h-screen"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
|
||||
@ -1188,10 +1188,15 @@ export const PromptInputSubmit = ({
|
||||
className={cn(
|
||||
"h-[36px] w-[36px] rounded-[50%] border-0 font-bold transition-all ",
|
||||
isDisabled
|
||||
? "cursor-not-allowed !bg-[#15003399] text-gray-400"
|
||||
: "!bg-[#150033] text-[#8E47F0] hover:text-[#FFFFFF]",
|
||||
? "cursor-not-allowed text-gray-400"
|
||||
: "text-ws-interactive-primary hover:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: isDisabled
|
||||
? "var(--brand-color-primary-60)"
|
||||
: "var(--brand-color-primary)",
|
||||
}}
|
||||
size={size}
|
||||
type="submit"
|
||||
variant={variant}
|
||||
|
||||
@ -69,7 +69,7 @@ function ToggleGroupItem({
|
||||
variant: context.variant || variant,
|
||||
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",
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
export const BRAND_SESSION_STORAGE_KEY = "deerflow.brand-session";
|
||||
export const DEFAULT_BRAND = "default" 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;
|
||||
|
||||
@ -59,10 +63,51 @@ export function resolveBrandSession({
|
||||
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 {
|
||||
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 {
|
||||
const value = storage.getItem(BRAND_SESSION_STORAGE_KEY);
|
||||
return isBrand(value) ? value : null;
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
parseBrandFromSearchParams,
|
||||
readStoredBrand,
|
||||
resolveBrandSession,
|
||||
syncBrandClassName,
|
||||
writeStoredBrand,
|
||||
type Brand,
|
||||
} from "./index";
|
||||
@ -37,5 +38,10 @@ export function BrandSessionInitializer() {
|
||||
}
|
||||
}, [resolvedBrand, setBrand]);
|
||||
|
||||
useEffect(() => {
|
||||
syncBrandClassName(document.documentElement, brand);
|
||||
syncBrandClassName(document.body, brand);
|
||||
}, [brand]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { createContext, useContext, useState, type ReactNode } from "react";
|
||||
import {
|
||||
BRAND_COPY,
|
||||
DEFAULT_BRAND,
|
||||
getInitialBrandFromBrowser,
|
||||
getBrandRootClassName,
|
||||
type Brand,
|
||||
} from "./index";
|
||||
@ -24,7 +25,13 @@ function getInitialBrand(): Brand {
|
||||
}
|
||||
|
||||
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 }) {
|
||||
|
||||
@ -69,9 +69,15 @@
|
||||
@source inline("border-{border,input}");
|
||||
|
||||
.brand-default {
|
||||
--brand-color-primary: #150033;
|
||||
--brand-color-primary-10: #1500331a;
|
||||
--brand-color-primary-60: #15003399;
|
||||
}
|
||||
|
||||
.brand-sxwz {
|
||||
--brand-color-primary: #000f33;
|
||||
--brand-color-primary-10: #000f331a;
|
||||
--brand-color-primary-60: #000f3399;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
@ -286,13 +292,13 @@
|
||||
--primary: oklch(0 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
/* --secondary: oklch(0.9455 0.0098 87.47); */
|
||||
--secondary: #1500331a;
|
||||
--secondary: var(--brand-color-primary-10);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
/* --muted: oklch(0.97 0.0098 87.47); */
|
||||
--muted: #1500331a;
|
||||
--muted: var(--brand-color-primary-10);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
/* --accent: oklch(0.94 0.0098 87.47); */
|
||||
--accent: #1500331a;
|
||||
--accent: var(--brand-color-primary-10);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: #00000015;
|
||||
@ -314,19 +320,19 @@
|
||||
--sidebar-border: oklch(0.922 0.0098 87.47);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--tooltip-background: #00000066;
|
||||
--ws-color-base-1: #150033;
|
||||
--ws-color-base-1: var(--brand-color-primary);
|
||||
--ws-color-fg-primary: #333333;
|
||||
--ws-color-surface-subtle: #f9f8fa;
|
||||
--ws-color-surface-elevated: #fbfafc;
|
||||
--ws-color-interactive-hover: #1500331A;
|
||||
--ws-color-interactive-primary: #150033;
|
||||
--ws-color-interactive-hover: var(--brand-color-primary-10);
|
||||
--ws-color-interactive-primary: var(--brand-color-primary);
|
||||
--ws-color-line-default: #e4e7ec;
|
||||
--ws-color-text-muted: #667085;
|
||||
--ws-color-icon-muted: #a3a1a1;
|
||||
--ws-color-overlay-neutral: #999999;
|
||||
--ws-color-text-subtle-strong: #000000c5;
|
||||
--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-base: #ffffff;
|
||||
--ws-color-text-primary-strong: #0f172a;
|
||||
@ -372,7 +378,7 @@
|
||||
--ws-color-fg-primary: #f5f5f5;
|
||||
--ws-color-surface-subtle: #1f1f1f;
|
||||
--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-text-muted: #98a2b3;
|
||||
--ws-color-icon-muted: #d0d0d0;
|
||||
@ -639,7 +645,14 @@ code {
|
||||
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;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
|
||||
@ -13,9 +13,18 @@ export type WorkspaceColorToken = {
|
||||
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 命名)。
|
||||
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-surface-subtle": { light: "#f9f8fa", dark: "#1f1f1f" },
|
||||
"ws-surface-elevated": { light: "#fbfafc", dark: "#24222a" },
|
||||
@ -26,7 +35,10 @@ export const WORKSPACE_COLOR_TOKENS = {
|
||||
"ws-overlay-neutral": { light: "#999999", dark: "#c2c2c2" },
|
||||
"ws-text-subtle-strong": { light: "#000000c5", dark: "#ffffffcc" },
|
||||
"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-base": { light: "#ffffff", dark: "#2a2731" },
|
||||
"ws-text-primary-strong": { light: "#0f172a", dark: "#e6eaf2" },
|
||||
|
||||
Loading…
Reference in New Issue
Block a user