refactor(brand): 重构品牌系统,移除 SESSION_BRAND 并完善初始化逻辑

- 将 DEFAULT_BRAND 重命名为 XCLAW_BRAND,统一品牌命名规范
- 删除未使用的 SESSION_BRAND 类型和相关配置
- 优化 getInitialBrandFromBrowser 逻辑:根据 isSxwz 参数直接设置并返回对应品牌
- 更新品牌颜色 tokens 和 CSS 选择器测试
- 同步更新 provider.tsx 和相关测试用例
This commit is contained in:
肖应宇 2026-06-13 11:29:39 +08:00
parent 02692444a5
commit d0afbf1897
6 changed files with 63 additions and 46 deletions

View File

@ -17,12 +17,12 @@ const SXWZ_BRAND_PRIMARY = `#${"000F33"}`;
void test("parseBrandFromSearchParams returns correct brand per param value", () => { void test("parseBrandFromSearchParams returns correct brand per param value", () => {
assert.equal(parseBrandFromSearchParams(new URLSearchParams("isSxwz=true")), "sxwz"); assert.equal(parseBrandFromSearchParams(new URLSearchParams("isSxwz=true")), "sxwz");
assert.equal(parseBrandFromSearchParams(new URLSearchParams("isSxwz=false")), "default"); assert.equal(parseBrandFromSearchParams(new URLSearchParams("isSxwz=false")), "xclaw");
assert.equal(parseBrandFromSearchParams(new URLSearchParams("")), null); assert.equal(parseBrandFromSearchParams(new URLSearchParams("")), null);
}); });
void test("resolveBrandSession falls back to default without url or storage", () => { void test("resolveBrandSession falls back to xclaw without url or storage", () => {
assert.equal(resolveBrandSession({ urlBrand: null, storedBrand: null }), "default"); assert.equal(resolveBrandSession({ urlBrand: null, storedBrand: null }), "xclaw");
}); });
void test("resolveBrandSession keeps stored sxwz when later url omits the flag", () => { void test("resolveBrandSession keeps stored sxwz when later url omits the flag", () => {
@ -31,28 +31,28 @@ void test("resolveBrandSession keeps stored sxwz when later url omits the flag",
void test("resolveBrandSession downgrades stored sxwz when url explicitly sets isSxwz=false", () => { void test("resolveBrandSession downgrades stored sxwz when url explicitly sets isSxwz=false", () => {
const urlBrand = parseBrandFromSearchParams(new URLSearchParams("isSxwz=false")); const urlBrand = parseBrandFromSearchParams(new URLSearchParams("isSxwz=false"));
assert.equal(resolveBrandSession({ urlBrand, storedBrand: "sxwz" }), "default"); assert.equal(resolveBrandSession({ urlBrand, storedBrand: "sxwz" }), "xclaw");
}); });
void test("resolveBrandSession upgrades to sxwz when url flag is true", () => { void test("resolveBrandSession upgrades to sxwz when url flag is true", () => {
const urlBrand = parseBrandFromSearchParams(new URLSearchParams("isSxwz=true")); const urlBrand = parseBrandFromSearchParams(new URLSearchParams("isSxwz=true"));
assert.equal(resolveBrandSession({ urlBrand, storedBrand: "default" }), "sxwz"); assert.equal(resolveBrandSession({ urlBrand, storedBrand: "xclaw" }), "sxwz");
}); });
void test("getBrandRootClassName returns stable workspace hook classes", () => { void test("getBrandRootClassName returns stable workspace hook classes", () => {
assert.equal(getBrandRootClassName("default"), "brand-default"); assert.equal(getBrandRootClassName("xclaw"), "brand-xclaw");
assert.equal(getBrandRootClassName("sxwz"), "brand-sxwz"); assert.equal(getBrandRootClassName("sxwz"), "brand-sxwz");
assert.equal(BRAND_SESSION_STORAGE_KEY, "deerflow.brand-session"); assert.equal(BRAND_SESSION_STORAGE_KEY, "deerflow.brand-session");
}); });
void test("getBrandPrimaryColor returns the right global brand primary", () => { void test("getBrandPrimaryColor returns the right global brand primary", () => {
assert.equal(getBrandPrimaryColor("default"), DEFAULT_BRAND_PRIMARY); assert.equal(getBrandPrimaryColor("xclaw"), DEFAULT_BRAND_PRIMARY);
assert.equal(getBrandPrimaryColor("sxwz"), SXWZ_BRAND_PRIMARY); assert.equal(getBrandPrimaryColor("sxwz"), SXWZ_BRAND_PRIMARY);
}); });
void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands", () => { void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands", () => {
assert.equal( assert.equal(
getBrandPrimaryColorWithAlpha("default", "1A"), getBrandPrimaryColorWithAlpha("xclaw", "1A"),
`${DEFAULT_BRAND_PRIMARY}1A`, `${DEFAULT_BRAND_PRIMARY}1A`,
); );
assert.equal( assert.equal(
@ -60,7 +60,7 @@ void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands",
`${SXWZ_BRAND_PRIMARY}1A`, `${SXWZ_BRAND_PRIMARY}1A`,
); );
assert.equal( assert.equal(
getBrandPrimaryColorWithAlpha("default", "99"), getBrandPrimaryColorWithAlpha("xclaw", "99"),
`${DEFAULT_BRAND_PRIMARY}99`, `${DEFAULT_BRAND_PRIMARY}99`,
); );
assert.equal( assert.equal(
@ -71,25 +71,29 @@ void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands",
void test("getInitialBrandFromBrowser prioritizes url brand on first render", () => { void test("getInitialBrandFromBrowser prioritizes url brand on first render", () => {
const searchParams = new URLSearchParams("isSxwz=true"); const searchParams = new URLSearchParams("isSxwz=true");
assert.equal(getInitialBrandFromBrowser({ searchParams, storedBrand: null }), "sxwz"); const storage = {
getItem: () => null,
setItem: () => {},
};
assert.equal(getInitialBrandFromBrowser({ searchParams, storage }), "sxwz");
assert.equal( assert.equal(
getInitialBrandFromBrowser({ getInitialBrandFromBrowser({
searchParams: new URLSearchParams("isSxwz=false"), searchParams: new URLSearchParams("isSxwz=false"),
storedBrand: "sxwz", storage,
}), }),
"default", "xclaw",
); );
assert.equal( assert.equal(
getInitialBrandFromBrowser({ getInitialBrandFromBrowser({
searchParams: new URLSearchParams(""), searchParams: new URLSearchParams(""),
storedBrand: "sxwz", storage,
}), }),
"sxwz", "xclaw",
); );
}); });
void test("syncBrandClassName rewrites brand classes on arbitrary targets", () => { void test("syncBrandClassName rewrites brand classes on arbitrary targets", () => {
const classSet = new Set(["foo", "brand-default"]); const classSet = new Set(["foo", "brand-xclaw"]);
const target = { const target = {
classList: { classList: {
add: (value: string) => classSet.add(value), add: (value: string) => classSet.add(value),
@ -100,6 +104,6 @@ void test("syncBrandClassName rewrites brand classes on arbitrary targets", () =
syncBrandClassName(target, "sxwz"); syncBrandClassName(target, "sxwz");
assert.equal(classSet.has("foo"), true); assert.equal(classSet.has("foo"), true);
assert.equal(classSet.has("brand-default"), false); assert.equal(classSet.has("brand-xclaw"), false);
assert.equal(classSet.has("brand-sxwz"), true); assert.equal(classSet.has("brand-sxwz"), true);
}); });

View File

@ -1,12 +1,12 @@
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 XCLAW_BRAND = "xclaw" as const;
const SXWZ_BRAND = "sxwz" as const; export const SXWZ_BRAND = "sxwz" as const;
const BRAND_PRIMARY_COLORS = { const BRAND_PRIMARY_COLORS = {
default: `#${"150033"}`, xclaw: `#${"150033"}`,
sxwz: `#${"000F33"}`, sxwz: `#${"000F33"}`,
} as const; } as const;
export type Brand = typeof DEFAULT_BRAND | typeof SXWZ_BRAND; export type Brand = typeof XCLAW_BRAND | typeof SXWZ_BRAND;
export type BrandCopy = { export type BrandCopy = {
productLabel: string; productLabel: string;
@ -16,7 +16,7 @@ export type BrandCopy = {
}; };
export const BRAND_COPY: Record<Brand, BrandCopy> = { export const BRAND_COPY: Record<Brand, BrandCopy> = {
default: { xclaw: {
productLabel: "轻办公", productLabel: "轻办公",
appName: "coxworker", appName: "coxworker",
appLogoSrc: "/coxwork.png", appLogoSrc: "/coxwork.png",
@ -29,7 +29,7 @@ export const BRAND_COPY: Record<Brand, BrandCopy> = {
}; };
export function isBrand(value: string | null): value is Brand { export function isBrand(value: string | null): value is Brand {
return value === DEFAULT_BRAND || value === SXWZ_BRAND; return value === XCLAW_BRAND || value === SXWZ_BRAND;
} }
export function parseBrandFromSearchParams( export function parseBrandFromSearchParams(
@ -37,7 +37,7 @@ export function parseBrandFromSearchParams(
): Brand | null { ): Brand | null {
const value = searchParams.get("isSxwz"); const value = searchParams.get("isSxwz");
if (value === "true") return SXWZ_BRAND; if (value === "true") return SXWZ_BRAND;
if (value === "false") return DEFAULT_BRAND; if (value === "false") return XCLAW_BRAND;
return null; return null;
} }
@ -52,30 +52,47 @@ export function resolveBrandSession({
return SXWZ_BRAND; return SXWZ_BRAND;
} }
if (urlBrand === DEFAULT_BRAND) { if (urlBrand === XCLAW_BRAND) {
return DEFAULT_BRAND; return XCLAW_BRAND;
} }
if (storedBrand === SXWZ_BRAND) { if (storedBrand === SXWZ_BRAND) {
return SXWZ_BRAND; return SXWZ_BRAND;
} }
return DEFAULT_BRAND; return XCLAW_BRAND;
} }
export function getInitialBrandFromBrowser({ export function getInitialBrandFromBrowser({
searchParams, searchParams,
storedBrand, storage,
}: { }: {
searchParams: URLSearchParams; searchParams: URLSearchParams;
storedBrand: Brand | null; storage: Pick<Storage, "getItem" | "setItem">;
}): Brand { }): Brand {
const urlBrand = parseBrandFromSearchParams(searchParams); const isSxwz = searchParams.get("isSxwz");
return resolveBrandSession({ urlBrand, storedBrand });
if (isSxwz === "true") {
storage.setItem(BRAND_SESSION_STORAGE_KEY, SXWZ_BRAND);
return SXWZ_BRAND;
}
if (isSxwz === "false") {
storage.setItem(BRAND_SESSION_STORAGE_KEY, XCLAW_BRAND);
return XCLAW_BRAND;
}
const storedBrand = storage.getItem(BRAND_SESSION_STORAGE_KEY);
if (storedBrand === SXWZ_BRAND || storedBrand === XCLAW_BRAND) {
return storedBrand;
}
return XCLAW_BRAND;
} }
export function getBrandRootClassName(brand: Brand): string { export function getBrandRootClassName(brand: Brand): string {
return brand === SXWZ_BRAND ? "brand-sxwz" : "brand-default"; if (brand === SXWZ_BRAND) return "brand-sxwz";
return "brand-xclaw";
} }
type BrandClassTarget = { type BrandClassTarget = {
@ -89,7 +106,7 @@ export function syncBrandClassName(
target: BrandClassTarget, target: BrandClassTarget,
brand: Brand, brand: Brand,
): void { ): void {
target.classList.remove("brand-default", "brand-sxwz"); target.classList.remove("brand-xclaw", "brand-sxwz");
target.classList.add(getBrandRootClassName(brand)); target.classList.add(getBrandRootClassName(brand));
} }

View File

@ -4,7 +4,7 @@ import { createContext, useContext, useState, type ReactNode } from "react";
import { import {
BRAND_COPY, BRAND_COPY,
DEFAULT_BRAND, XCLAW_BRAND,
getInitialBrandFromBrowser, getInitialBrandFromBrowser,
getBrandRootClassName, getBrandRootClassName,
type Brand, type Brand,
@ -21,16 +21,12 @@ const BrandContext = createContext<BrandContextValue | null>(null);
function getInitialBrand(): Brand { function getInitialBrand(): Brand {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return DEFAULT_BRAND; return XCLAW_BRAND;
} }
const storedBrand = window.sessionStorage.getItem("deerflow.brand-session");
return getInitialBrandFromBrowser({ return getInitialBrandFromBrowser({
searchParams: new URLSearchParams(window.location.search), searchParams: new URLSearchParams(window.location.search),
storedBrand: storage: window.sessionStorage,
storedBrand === "sxwz" || storedBrand === DEFAULT_BRAND
? storedBrand
: null,
}); });
} }

View File

@ -8,6 +8,6 @@ const currentDir = path.dirname(url.fileURLToPath(import.meta.url));
const globalsCss = readFileSync(path.join(currentDir, "globals.css"), "utf8"); const globalsCss = readFileSync(path.join(currentDir, "globals.css"), "utf8");
void test("brand selectors target :root to outrank default root variables", () => { void test("brand selectors target :root to outrank default root variables", () => {
assert.match(globalsCss, /:root\.brand-default\s*\{/); assert.match(globalsCss, /:root\.brand-xclaw\s*\{/);
assert.match(globalsCss, /:root\.brand-sxwz\s*\{/); assert.match(globalsCss, /:root\.brand-sxwz\s*\{/);
}); });

View File

@ -68,7 +68,7 @@
@source inline("bg-{background,muted,primary,secondary,accent}"); @source inline("bg-{background,muted,primary,secondary,accent}");
@source inline("border-{border,input}"); @source inline("border-{border,input}");
.brand-default { .brand-xclaw {
--brand-color-primary: #150033; --brand-color-primary: #150033;
--brand-color-primary-10: #1500331a; --brand-color-primary-10: #1500331a;
--brand-color-primary-60: #15003399; --brand-color-primary-60: #15003399;

View File

@ -14,17 +14,17 @@ export type WorkspaceColorToken = {
}; };
export const BRAND_PRIMARY_COLOR_TOKENS = { export const BRAND_PRIMARY_COLOR_TOKENS = {
default: "#150033", xclaw: "#150033",
sxwz: "#000F33", sxwz: "#000F33",
defaultAlpha10: "#1500331A", xclawAlpha10: "#1500331A",
sxwzAlpha10: "#000F331A", sxwzAlpha10: "#000F331A",
defaultAlpha60: "#15003399", xclawAlpha60: "#15003399",
sxwzAlpha60: "#000F3399", sxwzAlpha60: "#000F3399",
} as const; } 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: BRAND_PRIMARY_COLOR_TOKENS.default, dark: "#f4ebff" }, "ws-base-1": { light: BRAND_PRIMARY_COLOR_TOKENS.xclaw, 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" },
@ -36,7 +36,7 @@ export const WORKSPACE_COLOR_TOKENS = {
"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": { "ws-accent-tint-soft": {
light: BRAND_PRIMARY_COLOR_TOKENS.defaultAlpha10, light: BRAND_PRIMARY_COLOR_TOKENS.xclawAlpha10,
dark: "#f4ebff24", dark: "#f4ebff24",
}, },
"ws-surface-app": { light: "#f8f9fb", dark: "#20242c" }, "ws-surface-app": { light: "#f8f9fb", dark: "#20242c" },