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", () => {
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);
});
void test("resolveBrandSession falls back to default without url or storage", () => {
assert.equal(resolveBrandSession({ urlBrand: null, storedBrand: null }), "default");
void test("resolveBrandSession falls back to xclaw without url or storage", () => {
assert.equal(resolveBrandSession({ urlBrand: null, storedBrand: null }), "xclaw");
});
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", () => {
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", () => {
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", () => {
assert.equal(getBrandRootClassName("default"), "brand-default");
assert.equal(getBrandRootClassName("xclaw"), "brand-xclaw");
assert.equal(getBrandRootClassName("sxwz"), "brand-sxwz");
assert.equal(BRAND_SESSION_STORAGE_KEY, "deerflow.brand-session");
});
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);
});
void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands", () => {
assert.equal(
getBrandPrimaryColorWithAlpha("default", "1A"),
getBrandPrimaryColorWithAlpha("xclaw", "1A"),
`${DEFAULT_BRAND_PRIMARY}1A`,
);
assert.equal(
@ -60,7 +60,7 @@ void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands",
`${SXWZ_BRAND_PRIMARY}1A`,
);
assert.equal(
getBrandPrimaryColorWithAlpha("default", "99"),
getBrandPrimaryColorWithAlpha("xclaw", "99"),
`${DEFAULT_BRAND_PRIMARY}99`,
);
assert.equal(
@ -71,25 +71,29 @@ void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands",
void test("getInitialBrandFromBrowser prioritizes url brand on first render", () => {
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(
getInitialBrandFromBrowser({
searchParams: new URLSearchParams("isSxwz=false"),
storedBrand: "sxwz",
storage,
}),
"default",
"xclaw",
);
assert.equal(
getInitialBrandFromBrowser({
searchParams: new URLSearchParams(""),
storedBrand: "sxwz",
storage,
}),
"sxwz",
"xclaw",
);
});
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 = {
classList: {
add: (value: string) => classSet.add(value),
@ -100,6 +104,6 @@ void test("syncBrandClassName rewrites brand classes on arbitrary targets", () =
syncBrandClassName(target, "sxwz");
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);
});

View File

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

View File

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

View File

@ -8,6 +8,6 @@ const currentDir = path.dirname(url.fileURLToPath(import.meta.url));
const globalsCss = readFileSync(path.join(currentDir, "globals.css"), "utf8");
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*\{/);
});

View File

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

View File

@ -14,17 +14,17 @@ export type WorkspaceColorToken = {
};
export const BRAND_PRIMARY_COLOR_TOKENS = {
default: "#150033",
xclaw: "#150033",
sxwz: "#000F33",
defaultAlpha10: "#1500331A",
xclawAlpha10: "#1500331A",
sxwzAlpha10: "#000F331A",
defaultAlpha60: "#15003399",
xclawAlpha60: "#15003399",
sxwzAlpha60: "#000F3399",
} as const;
// Token 键保持语义化且稳定:`ws-<role>-<level>`(不要再使用原始 hex 命名)。
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-surface-subtle": { light: "#f9f8fa", dark: "#1f1f1f" },
"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-border-hairline": { light: "#00000015", dark: "#ffffff1f" },
"ws-accent-tint-soft": {
light: BRAND_PRIMARY_COLOR_TOKENS.defaultAlpha10,
light: BRAND_PRIMARY_COLOR_TOKENS.xclawAlpha10,
dark: "#f4ebff24",
},
"ws-surface-app": { light: "#f8f9fb", dark: "#20242c" },