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 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}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 }) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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" },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user