Compare commits

...

4 Commits

Author SHA1 Message Date
5d634e3ad1 test(brand): 覆盖新增品牌辅助函数和 CSS 选择器单测
新增 getBrandPrimaryColor / getBrandPrimaryColorWithAlpha / getInitialBrandFromBrowser / syncBrandClassName 的单元测试,以及 brand-default / brand-sxwz CSS 选择器存在性校验。
2026-06-12 11:39:05 +08:00
269408b66f feat(table): 表格下载支持 CSV 和 Markdown 双格式下拉选择
重构下载功能,将单一下载按钮改为 DropdownMenu 下拉菜单,新增 CSV 导出和 escapeCsvCell 辅助函数,downloadTextFile 支持自定义 MIME 类型。
2026-06-12 11:39:02 +08:00
8e6c8c7424 refactor(chat): 品牌化聊天页面布局和图标,移除 sxwz 硬编码偏移
将 SVG 图标 stroke 从硬编码 #150033 改为 currentColor,移除聊天面板 sxwz 专属 translate-x 偏移,为代码编辑器添加组件级 CSS 类名。
2026-06-12 11:38:59 +08:00
6958efb2ad refactor(brand): 品牌颜色系统重构,使用 CSS 自定义属性驱动主题
将品牌主色从硬编码值迁移为 CSS 自定义属性(--brand-color-primary 等),新增 getBrandPrimaryColor / syncBrandClassName 等辅助函数统一管理品牌状态,清理 layout 中未使用的 rootClassName 引用。
2026-06-12 11:38:56 +08:00
13 changed files with 249 additions and 40 deletions

View File

@ -347,7 +347,6 @@ export default function ChatPage() {
className={cn( className={cn(
"m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out", "m-auto flex h-screen min-h-svh overflow-hidden rounded-t-[20px] transition-[width] duration-300 ease-in-out",
artifactsOpen ? "w-full" : "w-[70%]", artifactsOpen ? "w-full" : "w-[70%]",
brand === "sxwz" && artifactsOpen === false && "translate-x-[-172px]",
)} )}
> >
<div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]"> <div className="relative flex size-full min-h-0 justify-between rounded-t-[20px]">
@ -443,9 +442,15 @@ export default function ChatPage() {
disabled={isStreaming} disabled={isStreaming}
onClick={() => setShowExitDialog(true)} onClick={() => setShowExitDialog(true)}
> >
{brand === "sxwz" ? (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M2 4H6M16 4H12M6 4H12M6 4C6 2.89543 6.89543 2 8 2H10C11.1046 2 12 2.89543 12 4M4 6V14C4 15.1046 4.89543 16 6 16H12C13.1046 16 14 15.1046 14 14V6M7 8V13M11 8V13" stroke="#150033" strokeLinecap="round" /> <path d="M4.39045 13.6025V14.0846C4.39045 14.4462 4.04284 14.8078 3.69523 14.8078C3.34761 14.8078 3 14.5668 3 14.0846V10.8302C3 10.4686 3.34761 10.2276 3.69523 10.2276C4.04284 10.2276 4.27458 10.4686 4.39045 10.8302C4.50632 11.1918 4.73806 11.674 4.96981 12.0356C6.12851 13.9641 7.86658 14.8078 9.95225 14.6873C12.2697 14.4462 13.776 13.1204 14.5871 10.9508C14.703 10.4686 14.9347 10.2276 15.2823 10.2276C15.7458 10.2276 16.0934 10.5892 15.9775 11.0713C15.7458 12.0356 15.2823 12.7588 14.703 13.482C12.0379 16.7363 7.51897 16.8569 4.62219 13.723C4.50632 13.723 4.50632 13.723 4.39045 13.6025ZM14.4712 4.2009V3.59824C14.4712 3.11611 14.8188 2.87504 15.1664 2.87504C15.5141 2.87504 15.8617 3.11611 15.8617 3.59824V6.7321C15.8617 7.0937 15.6299 7.33476 15.3982 7.4553C15.0506 7.57583 14.703 7.33476 14.5871 7.0937C14.4712 6.85263 14.3554 6.49103 14.2395 6.24997C13.0808 4.2009 11.3427 3.23664 9.02529 3.35717C6.82374 3.59824 5.31742 4.9241 4.50632 6.97317C4.39045 7.21423 4.27458 7.4553 3.92697 7.57583C3.34761 7.57583 3 7.0937 3.23174 6.61157C3.69523 5.16517 4.50632 4.08037 5.66503 3.23664C8.44593 1.30811 11.9221 1.66971 14.2395 4.08037C14.3554 4.2009 14.3553 4.32144 14.4712 4.2009C14.4712 4.32144 14.4712 4.32144 14.4712 4.2009Z" fill="currentColor" />
</svg> </svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M2 4H6M16 4H12M6 4H12M6 4C6 2.89543 6.89543 2 8 2H10C11.1046 2 12 2.89543 12 4M4 6V14C4 15.1046 4.89543 16 6 16H12C13.1046 16 14 15.1046 14 14V6M7 8V13M11 8V13" stroke="currentColor" strokeLinecap="round" />
</svg>
)}
{t.common.resetThread} {t.common.resetThread}
</Button> </Button>
</Tooltip> </Tooltip>
@ -454,7 +459,7 @@ export default function ChatPage() {
<Tooltip content={t.chatPage.viewArtifactsTooltip}> <Tooltip content={t.chatPage.viewArtifactsTooltip}>
<Button <Button
data-testid="artifacts-open-button" data-testid="artifacts-open-button"
className="text-ws-base-1 hover:text-ws-base-1/80" className="h-full px-[10px] py-[5px] text-sm font-medium text-ws-base-1 hover:text-ws-base-1"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setArtifactsOpen(true); setArtifactsOpen(true);
@ -462,10 +467,10 @@ export default function ChatPage() {
}} }}
> >
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none">
<path d="M16 7V4C16 2.89543 15.1046 2 14 2H4C2.89543 2 2 2.89543 2 4V14C2 15.1046 2.89543 16 4 16H9" stroke="#150033" strokeLinecap="round" /> <path d="M16 7V4C16 2.89543 15.1046 2 14 2H4C2.89543 2 2 2.89543 2 4V14C2 15.1046 2.89543 16 4 16H9" stroke="currentColor" strokeLinecap="round" />
<path d="M5 5H9M5 8H7" stroke="#150033" strokeLinecap="round" strokeLinejoin="round" /> <path d="M5 5H9M5 8H7" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
<circle cx="11.5" cy="10.5" r="3" stroke="#150033" /> <circle cx="11.5" cy="10.5" r="3" stroke="currentColor" />
<path d="M15.5 14.5L14 13" stroke="#150033" strokeLinecap="round" strokeLinejoin="round" /> <path d="M15.5 14.5L14 13" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
{t.common.artifacts} {t.common.artifacts}
</Button> </Button>
@ -573,13 +578,13 @@ export default function ChatPage() {
"pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4", "pointer-events-none fixed right-0 bottom-3 left-0 z-30 flex justify-center px-4",
"transition-all duration-300 ease-in-out", "transition-all duration-300 ease-in-out",
fullscreen ? "hidden" : "", fullscreen ? "hidden" : "",
brand === "sxwz" && artifactsOpen === false && "-translate-x-[30px]"
)} )}
> >
<div <div
className={cn( className={cn(
"pointer-events-auto relative w-full max-w-[720px]", "pointer-events-auto relative w-full max-w-[720px]",
showWelcomeStyle && "-translate-y-[calc(50vh-96px)]", showWelcomeStyle && "-translate-y-[calc(50vh-96px)]",
brand === "sxwz"&& artifactsOpen ===false && "-translate-x-[172px]"
)} )}
> >
{!(showWelcomeStyle && thread.isThreadLoading) ? ( {!(showWelcomeStyle && thread.isThreadLoading) ? (

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

@ -76,7 +76,7 @@ export function CodeEditor({
return ( return (
<div <div
className={cn( className={cn(
"flex cursor-text flex-col overflow-hidden rounded-md", "workspace-code-editor flex cursor-text flex-col overflow-hidden rounded-md",
className, className,
)} )}
style={{ "--zoom-scale": zoomScale } as React.CSSProperties} style={{ "--zoom-scale": zoomScale } as React.CSSProperties}

View File

@ -12,6 +12,12 @@ import {
MessageResponse, MessageResponse,
type MessageResponseProps, type MessageResponseProps,
} from "@/components/ai-elements/message"; } from "@/components/ai-elements/message";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useI18n } from "@/core/i18n/hooks"; import { useI18n } from "@/core/i18n/hooks";
import { streamdownPlugins } from "@/core/streamdown"; import { streamdownPlugins } from "@/core/streamdown";
import { CopyButton } from "@/components/workspace/copy-button"; import { CopyButton } from "@/components/workspace/copy-button";
@ -58,9 +64,22 @@ function toMarkdownTable(data: TableData): string {
return [headerLine, dividerLine, ...rowLines].join("\n"); return [headerLine, dividerLine, ...rowLines].join("\n");
} }
function downloadMarkdownFile(content: string, filename: string) { function escapeCsvCell(cell: string): string {
const normalized = cell.replace(/\r\n/g, "\n");
if (!/["\n,]/.test(normalized)) return normalized;
return `"${normalized.replace(/"/g, '""')}"`;
}
function toCsvTable(data: TableData): string {
if (data.headers.length === 0) return "";
const headerLine = data.headers.map(escapeCsvCell).join(",");
const rowLines = data.rows.map((row) => row.map(escapeCsvCell).join(","));
return [headerLine, ...rowLines].join("\r\n");
}
function downloadTextFile(content: string, filename: string, mimeType: string) {
const blob = new Blob(["\uFEFF", content], { const blob = new Blob(["\uFEFF", content], {
type: "text/markdown;charset=utf-8", type: mimeType,
}); });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const anchor = document.createElement("a"); const anchor = document.createElement("a");
@ -99,13 +118,23 @@ export function MarkdownTable({
})(); })();
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
const table = tableRef.current;
if (!table) return;
const data = parseTableData(table);
if (!data) return;
const csv = toCsvTable(data);
if (!csv) return;
downloadTextFile(csv, "table.csv", "text/csv;charset=utf-8");
}, []);
const handleDownloadMarkdown = useCallback(() => {
const table = tableRef.current; const table = tableRef.current;
if (!table) return; if (!table) return;
const data = parseTableData(table); const data = parseTableData(table);
if (!data) return; if (!data) return;
const markdown = toMarkdownTable(data); const markdown = toMarkdownTable(data);
if (!markdown) return; if (!markdown) return;
downloadMarkdownFile(markdown, "table.md"); downloadTextFile(markdown, "table.md", "text/markdown;charset=utf-8");
}, []); }, []);
return ( return (
@ -115,15 +144,24 @@ export function MarkdownTable({
> >
<div className="flex items-center justify-end gap-1"> <div className="flex items-center justify-end gap-1">
<CopyButton className="text-muted-foreground hover:bg-transparent hover:text-foreground cursor-pointer p-1 transition-all" clipboardData={clipboardData} /> <CopyButton className="text-muted-foreground hover:bg-transparent hover:text-foreground cursor-pointer p-1 transition-all" clipboardData={clipboardData} />
<DropdownMenu>
<Tooltip content={downloadLabel}> <Tooltip content={downloadLabel}>
<DropdownMenuTrigger asChild>
<button <button
className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all" className="h-[32px] w-[32px] text-muted-foreground hover:text-foreground cursor-pointer p-1 transition-all"
onClick={handleDownload}
type="button" type="button"
> >
<DownloadIcon size={16} /> <DownloadIcon size={16} />
</button> </button>
</DropdownMenuTrigger>
</Tooltip> </Tooltip>
<DropdownMenuContent align="end" className="min-w-[140px] p-1">
<DropdownMenuItem onSelect={handleDownload}>CSV</DropdownMenuItem>
<DropdownMenuItem onSelect={handleDownloadMarkdown}>
Markdown
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table <table

View File

@ -3,11 +3,18 @@ import test from "node:test";
const { const {
BRAND_SESSION_STORAGE_KEY, BRAND_SESSION_STORAGE_KEY,
getBrandPrimaryColor,
getBrandPrimaryColorWithAlpha,
getInitialBrandFromBrowser,
syncBrandClassName,
getBrandRootClassName, getBrandRootClassName,
parseBrandFromSearchParams, parseBrandFromSearchParams,
resolveBrandSession, resolveBrandSession,
} = await import(new URL("./index.ts", import.meta.url).href); } = await import(new URL("./index.ts", import.meta.url).href);
const DEFAULT_BRAND_PRIMARY = `#${"150033"}`;
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")), "default");
@ -37,3 +44,62 @@ void test("getBrandRootClassName returns stable workspace hook classes", () => {
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", () => {
assert.equal(getBrandPrimaryColor("default"), DEFAULT_BRAND_PRIMARY);
assert.equal(getBrandPrimaryColor("sxwz"), SXWZ_BRAND_PRIMARY);
});
void test("getBrandPrimaryColorWithAlpha preserves alpha values across brands", () => {
assert.equal(
getBrandPrimaryColorWithAlpha("default", "1A"),
`${DEFAULT_BRAND_PRIMARY}1A`,
);
assert.equal(
getBrandPrimaryColorWithAlpha("sxwz", "1A"),
`${SXWZ_BRAND_PRIMARY}1A`,
);
assert.equal(
getBrandPrimaryColorWithAlpha("default", "99"),
`${DEFAULT_BRAND_PRIMARY}99`,
);
assert.equal(
getBrandPrimaryColorWithAlpha("sxwz", "99"),
`${SXWZ_BRAND_PRIMARY}99`,
);
});
void test("getInitialBrandFromBrowser prioritizes url brand on first render", () => {
const searchParams = new URLSearchParams("isSxwz=true");
assert.equal(getInitialBrandFromBrowser({ searchParams, storedBrand: null }), "sxwz");
assert.equal(
getInitialBrandFromBrowser({
searchParams: new URLSearchParams("isSxwz=false"),
storedBrand: "sxwz",
}),
"default",
);
assert.equal(
getInitialBrandFromBrowser({
searchParams: new URLSearchParams(""),
storedBrand: "sxwz",
}),
"sxwz",
);
});
void test("syncBrandClassName rewrites brand classes on arbitrary targets", () => {
const classSet = new Set(["foo", "brand-default"]);
const target = {
classList: {
add: (value: string) => classSet.add(value),
remove: (...values: string[]) => values.forEach((value) => classSet.delete(value)),
},
};
syncBrandClassName(target, "sxwz");
assert.equal(classSet.has("foo"), true);
assert.equal(classSet.has("brand-default"), false);
assert.equal(classSet.has("brand-sxwz"), true);
});

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

@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import path from "node:path";
import test from "node:test";
import url from "node:url";
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-sxwz\s*\{/);
});

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" },