fix(08): 用主题色替换留存的 white/black 工具颜色,

This commit is contained in:
肖应宇 2026-04-23 10:12:54 +08:00
parent 161e5fad3c
commit 54ef439226
17 changed files with 83 additions and 44 deletions

View File

@ -37,6 +37,13 @@
- [ ] **ATREF-03**: 引用文件复用 `additional_kwargs.files` 提交,含来源元信息;失效引用软剔除并不阻断消息发送
- [ ] **ATREF-04**: 引用能力具备自动化回归验证(单测 + E2E及按 style/logic/tests/docs 的提交分组计划
### Theme Tokenization and Color Guard (Phase 8)
- [ ] **P8-01**: Workspace 核心页面与组件thread page、input box、artifact detail/list、workspace layout/header中的 `bg-[#...]`/`text-[#...]`/`stroke="#..."` 等硬编码颜色迁移为 light/dark 主题 token
- [ ] **P8-02**: 建立颜色 token 注册表并满足“每个 distinct 颜色值对应一个 distinct token 名称”的唯一性约束(禁止多个不同颜色值映射到同名 token
- [ ] **P8-03**: 增加自动化扫描守卫,阻止新增 `#hex``bg-[#...]`/`text-[#...]`(含同类 arbitrary color回归
- [ ] **P8-04**: 覆盖 workspace 关键页面与组件的 light/dark 回归验证(静态扫描 + 自动化用例 + 可复现命令)
## v2 Requirements
### Tooling Improvements
@ -73,10 +80,14 @@
| ATREF-02 | Phase 6 | Pending |
| ATREF-03 | Phase 6 | Pending |
| ATREF-04 | Phase 6 | Pending |
| P8-01 | Phase 8 | Pending |
| P8-02 | Phase 8 | Pending |
| P8-03 | Phase 8 | Pending |
| P8-04 | Phase 8 | Pending |
**Coverage:**
- v1 requirements: 17 total
- Mapped to phases: 17
- v1 requirements: 21 total
- Mapped to phases: 21
- Unmapped: 0
---

View File

@ -78,6 +78,19 @@ Plans:
- [x] 07-01-PLAN.md — 提交态增强文本组装 + 三入口统一透传 + 显示态/提交态分离回归
- [x] 07-02-PLAN.md — gap closure修复 ContextMenu 自动引用、提示前缀唯一化、Skill 使用 id 拼接
### Phase 8: 现在系统中有非常多写死的颜色值比如bg-[#00000],text-[#000000],我想把这些颜色值都提升到浅色模式和深色模式里面
**Goal:** 将 workspace 核心页面/组件中的硬编码颜色迁移为 light/dark 主题 token并建立防回归扫描守卫。
**Requirements**: P8-01, P8-02, P8-03, P8-04
**Depends on:** Phase 7
**Plans:** 4 plans
Plans:
- [ ] 08-01-PLAN.md — 建立颜色 token 注册表与扫描守卫基础能力
- [ ] 08-02-PLAN.md — 迁移 chat/input/workspace 关键页面组件的硬编码颜色
- [ ] 08-03-PLAN.md — 迁移 artifact 关键组件的硬编码颜色与局部样式变量
- [ ] 08-04-PLAN.md — 建立回归验证闭环并固化防回归检查
---
*Milestone status:* `complete`
*Next command:* `/gsd-new-milestone`

View File

@ -2,15 +2,15 @@
gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: v1.0 milestone complete
last_updated: "2026-04-22T02:08:30.000Z"
last_activity: 2026-04-22
status: Executing Phase 8
last_updated: "2026-04-23T01:22:12.681Z"
last_activity: 2026-04-23
progress:
total_phases: 8
completed_phases: 7
total_plans: 13
total_plans: 17
completed_plans: 16
percent: 100
percent: 94
---
# STATE.md
@ -20,7 +20,7 @@ progress:
See: .planning/PROJECT.md (updated 2026-04-07)
**Core value:** Keep the frontend visually familiar while preserving and hardening new-system behavior end to end.
**Current focus:** Milestone v1.0 completed
**Current focus:** Phase 8 — 现在系统中有非常多写死的颜色值比如bg-[#00000],text-[#000000],我想把这些颜色值都提升到浅色模式和深色模式里面
## Workflow State
@ -46,6 +46,7 @@ See: .planning/PROJECT.md (updated 2026-04-07)
- Phase 6 added: 在输入框输入@时,可引用已生成文件和已上传附件
- Phase 7 added: 发送时拼接附件与Skill优先提示词并在消息区过滤
- Phase 8 added: 现在系统中有非常多写死的颜色值比如bg-[#00000],text-[#000000],我想把这些颜色值都提升到浅色模式和深色模式里面
### Quick Tasks Completed
@ -55,4 +56,4 @@ See: .planning/PROJECT.md (updated 2026-04-07)
| 260416-koe | 归档 Phase 06 明确指代(“这张图”)语义修复到 GSD 流程(已验收,通过人工确认,免验证) | 2026-04-16 | pending | [260416-koe-phase-06](./quick/260416-koe-phase-06/) |
| 260422-e2i | 后端为会话历史消息增加时间戳字段(前端不显示) | 2026-04-22 | pending | [260422-e2i-message-timestamp](./quick/260422-e2i-message-timestamp/) |
Last activity: 2026-04-22
Last activity: 2026-04-23

View File

@ -13,6 +13,8 @@ const TOKENS_PATH = path.join(SRC_ROOT, "styles", "workspace-color-tokens.ts");
const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g;
const ARBITRARY_COLOR_RE =
/\b(?:bg|text|border|ring|from|to|via|fill|stroke)-\[[^\]]+\]/g;
const NAMED_COLOR_RE =
/\b(?:bg|text|border|ring|from|to|via|fill|stroke)-(?:white|black)(?:\/\d+)?\b/g;
const EXCLUDED_HEX_FILES = new Set([GLOBALS_PATH, TOKENS_PATH]);
const MODE = process.argv.includes("--mode=guard") ? "guard" : "audit";
@ -53,6 +55,7 @@ function scanFullSource() {
const report = {
hex: [],
arbitrary: [],
named: [],
};
for (const file of files) {
@ -74,6 +77,10 @@ function scanFullSource() {
for (const finding of arbitraryFindings) {
report.arbitrary.push({ file, ...finding });
}
const namedFindings = collectMatchesInContent(content, NAMED_COLOR_RE, () => true);
for (const finding of namedFindings) {
report.named.push({ file, ...finding });
}
}
return report;
@ -151,6 +158,7 @@ function scanAddedViolations() {
const report = {
hex: [],
arbitrary: [],
named: [],
};
for (const [file, lines] of addedLines.entries()) {
@ -165,6 +173,10 @@ function scanAddedViolations() {
for (const match of content.matchAll(ARBITRARY_COLOR_RE)) {
report.arbitrary.push({ file, line, match: match[0] });
}
NAMED_COLOR_RE.lastIndex = 0;
for (const match of content.matchAll(NAMED_COLOR_RE)) {
report.named.push({ file, line, match: match[0] });
}
}
}
@ -276,10 +288,10 @@ async function main() {
console.log(`[color-guard] mode=${MODE}`);
console.log(
`[summary] full-scan hex=${fullScan.hex.length} arbitrary=${fullScan.arbitrary.length}`,
`[summary] full-scan hex=${fullScan.hex.length} arbitrary=${fullScan.arbitrary.length} named=${fullScan.named.length}`,
);
console.log(
`[summary] added-violations hex=${addedViolations.hex.length} arbitrary=${addedViolations.arbitrary.length}`,
`[summary] added-violations hex=${addedViolations.hex.length} arbitrary=${addedViolations.arbitrary.length} named=${addedViolations.named.length}`,
);
console.log(
`[summary] ws-vars root=${globalsValidation.rootCount} dark=${globalsValidation.darkCount} inline=${globalsValidation.inlineCount}`,
@ -287,6 +299,7 @@ async function main() {
printFindings("[added] hex violations", addedViolations.hex);
printFindings("[added] arbitrary color violations", addedViolations.arbitrary);
printFindings("[added] named color violations", addedViolations.named);
const semanticErrors = [...tokenValidation.errors, ...globalsValidation.errors];
if (semanticErrors.length > 0) {
@ -299,6 +312,7 @@ async function main() {
const hasViolations =
addedViolations.hex.length > 0 ||
addedViolations.arbitrary.length > 0 ||
addedViolations.named.length > 0 ||
semanticErrors.length > 0;
if (MODE === "guard" && hasViolations) {

View File

@ -400,7 +400,7 @@ export default function ChatPage() {
<div className="flex items-center justify-end gap-2 overflow-hidden">
{/* 取消TodoList */}
{/* <DevTodoList
className="bg-white"
className="bg-ws-ffffff"
todos={thread.values.todos ?? []}
hidden={
!thread.values.todos || thread.values.todos.length === 0
@ -438,7 +438,7 @@ export default function ChatPage() {
className={cn(
"flex min-h-0 max-w-full grow flex-col",
showWelcomeStyle && !hasSubmitted
? "bg-white"
? "bg-ws-ffffff"
: "bg-background",
)}
>
@ -609,14 +609,14 @@ export default function ChatPage() {
</p>
<DevDialogFooter>
<Button
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-white"
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-primary-foreground"
variant="ghost"
onClick={() => setShowExitDialog(false)}
>
{t.common.cancel}
</Button>
<Button
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-white"
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-primary-foreground"
variant="ghost"
onClick={async () => {
// 如果正在生成,先终止再退出
@ -665,7 +665,7 @@ export default function ChatPage() {
</p>
<DevDialogFooter singleColumn>
<Button
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-white"
className="w-full bg-ws-f9f8fa hover:bg-ws-8e47f0 hover:text-primary-foreground"
variant="ghost"
onClick={clearSelectedSkillError}
>

View File

@ -138,12 +138,12 @@ export default function WorkspaceLayout({
/* 单行布局,内容水平居中 */
"flex items-center justify-center gap-0",
/* 整体文字样式 */
"text-white text-sm font-normal font-sans",
"text-primary-foreground text-sm font-normal font-sans",
/* 去掉 icon 区域间距 */
"[&>[data-icon]]:hidden",
].join(" "),
title:
"text-white! text-sm font-normal text-center w-full leading-snug",
"text-primary-foreground! text-sm font-normal text-center w-full leading-snug",
description: "hidden",
icon: "hidden",
},

View File

@ -36,7 +36,7 @@ export const Message = ({
"group flex w-full flex-col gap-2",
from === "user"
? cn("is-user ml-auto justify-end", !isFirstInSession && "mt-6")
: "is-assistant rounded-[10px] bg-white p-4",
: "is-assistant rounded-[10px] bg-ws-ffffff p-4",
className,
)}
{...props}

View File

@ -352,7 +352,7 @@ export function PromptInputAttachment({
{/* 删除按钮 - 右上角 */}
<button
aria-label={t.common.removeAttachment}
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-white/20"
className="absolute top-1.5 right-1.5 z-10 flex size-4 cursor-pointer items-center justify-center rounded-sm transition-colors hover:bg-ws-ffffff/20"
onClick={(e) => {
e.stopPropagation();
if (onRemove) {
@ -397,7 +397,7 @@ export function PromptInputAttachment({
{/* 关闭按钮 - 右上角 */}
<button
aria-label={t.common.removeAttachment}
className="absolute top-1 right-1 z-10 flex size-5 cursor-pointer items-center justify-center rounded bg-white/90 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-white dark:bg-gray-800/90 dark:hover:bg-gray-800"
className="absolute top-1 right-1 z-10 flex size-5 cursor-pointer items-center justify-center rounded bg-ws-ffffff/90 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-ws-ffffff dark:bg-gray-800/90 dark:hover:bg-gray-800"
onClick={(e) => {
e.stopPropagation();
if (onRemove) {

View File

@ -430,7 +430,7 @@ export function ArtifactFileDetail({
type="single"
variant={null}
size="default"
className="h-[28px] bg-white"
className="h-[28px] bg-ws-ffffff"
value={viewMode}
onValueChange={(value) => {
if (value) {
@ -721,7 +721,7 @@ export function ArtifactFileDetail({
</ArtifactHeader>
<ArtifactContent>
{/* 遮挡多余的滚动顶部 */}
{/* <div className="absolute w-[calc(100%-40px)] bg-white z-20 h-5 rounded-t-[10px] top-[57px]"></div> */}
{/* <div className="absolute w-[calc(100%-40px)] bg-ws-ffffff z-20 h-5 rounded-t-[10px] top-[57px]"></div> */}
{previewable &&
viewMode === "preview" &&
(language === "markdown" || language === "html") && (
@ -734,7 +734,7 @@ export function ArtifactFileDetail({
/>
)}
{isCodeFile && viewMode === "code" && (
<div className="mb-0 mb-[207px] min-h-full rounded-b-[10px] bg-white p-0">
<div className="mb-0 mb-[207px] min-h-full rounded-b-[10px] bg-ws-ffffff p-0">
<CodeEditor
className="size-full resize-none rounded-none border-none py-[20px]"
value={displayContent ?? ""}
@ -917,7 +917,7 @@ export function ArtifactFilePreview({
if (language === "markdown") {
return (
<div
className={cn("mb-[207px] w-full bg-white p-[20px]")}
className={cn("mb-[207px] w-full bg-ws-ffffff p-[20px]")}
style={{ "--zoom-scale": zoomScale } as CSSProperties}
>
<Streamdown
@ -974,7 +974,7 @@ function PreviewIframe({
{...props}
/>
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/85">
<div className="absolute inset-0 z-10 flex items-center justify-center bg-ws-ffffff/85">
<LoaderIcon className="text-muted-foreground size-5 animate-spin" />
</div>
)}
@ -1046,7 +1046,7 @@ function ArtifactPdfPreview({
const pageWrapper = document.createElement("div");
pageWrapper.className =
"mx-auto mb-4 w-fit rounded-md border border-ws-e4e7ec bg-white p-2 shadow-sm";
"mx-auto mb-4 w-fit rounded-md border border-ws-e4e7ec bg-ws-ffffff p-2 shadow-sm";
const canvas = document.createElement("canvas");
canvas.style.width = `${viewport.width}px`;
@ -1090,7 +1090,7 @@ function ArtifactPdfPreview({
if (error) {
return (
<div className={cn("relative overflow-auto bg-ws-f9f8fa p-4", className)}>
<div className="mx-auto grid max-w-xl gap-3 rounded-md border border-ws-e4e7ec bg-white p-5 text-center">
<div className="mx-auto grid max-w-xl gap-3 rounded-md border border-ws-e4e7ec bg-ws-ffffff p-5 text-center">
<p className="text-sm font-medium break-all">{fileName}</p>
<p className="text-muted-foreground text-sm">{error}</p>
<a
@ -1115,7 +1115,7 @@ function ArtifactPdfPreview({
</div>
<div ref={containerRef} />
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/70">
<div className="absolute inset-0 z-10 flex items-center justify-center bg-ws-ffffff/70">
<LoaderIcon className="text-muted-foreground size-5 animate-spin" />
</div>
)}
@ -1313,7 +1313,7 @@ function ArtifactOfficePreview({
}, [canRenderPptx, t.artifactPreview.pptxDownloadHint]);
return (
<div className={cn("relative h-full overflow-hidden bg-white", className)}>
<div className={cn("relative h-full overflow-hidden bg-ws-ffffff", className)}>
{canRenderXlsx && sheetNames.length > 0 && (
<div className="border-border flex items-center gap-1 overflow-x-auto border-b p-2">
{sheetNames.map((sheetName) => (
@ -1357,7 +1357,7 @@ function ArtifactOfficePreview({
/>
)}
{isLoading && (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/85">
<div className="absolute inset-0 z-10 flex items-center justify-center bg-ws-ffffff/85">
<LoaderIcon className="text-muted-foreground size-5 animate-spin" />
</div>
)}
@ -1376,7 +1376,7 @@ function ArtifactPreviewFallback({
}) {
const { t } = useI18n();
return (
<div className="absolute inset-0 z-20 grid place-content-center bg-white p-6 text-center">
<div className="absolute inset-0 z-20 grid place-content-center bg-ws-ffffff p-6 text-center">
<p className="text-foreground mb-2 text-sm font-medium">{fileName}</p>
<p className="text-muted-foreground mb-3 text-xs">{message}</p>
<a

View File

@ -34,7 +34,7 @@ export function DevTodoList({
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent
className={cn(
"z-[100] rounded-[20px] bg-white p-5 shadow-[0_0_20px_0_rgba(0,0,0,0.20)]",
"z-[100] rounded-[20px] bg-ws-ffffff p-5 shadow-[0_0_20px_0_rgba(0,0,0,0.20)]",
className,
)}
align="start"

View File

@ -143,7 +143,7 @@ export function IframeTestPanel() {
return (
<button
className={cn(
"fixed z-[9999] rounded-full bg-violet-500 px-3 py-1 text-xs font-bold text-white shadow-lg hover:bg-violet-600",
"fixed z-[9999] rounded-full bg-violet-500 px-3 py-1 text-xs font-bold text-primary-foreground shadow-lg hover:bg-violet-600",
position ? "top-0 left-0" : "bottom-24 left-3",
)}
style={position ? { left: position.x, top: position.y } : undefined}
@ -157,7 +157,7 @@ export function IframeTestPanel() {
<div
ref={panelRef}
className={cn(
"fixed z-[9999] w-72 rounded-xl border border-violet-200 bg-white/95 shadow-2xl backdrop-blur-sm",
"fixed z-[9999] w-72 rounded-xl border border-violet-200 bg-ws-ffffff/95 shadow-2xl backdrop-blur-sm",
position ? "top-0 left-0" : "bottom-24 left-3",
)}
style={position ? { left: position.x, top: position.y } : undefined}
@ -170,17 +170,17 @@ export function IframeTestPanel() {
)}
onPointerDown={handlePointerDown}
>
<span className="text-xs font-bold text-white">🧪 iframe </span>
<span className="text-xs font-bold text-primary-foreground">🧪 iframe </span>
<div className="flex items-center gap-2">
<button
className="text-white/70 hover:text-white"
className="text-primary-foreground/70 hover:text-primary-foreground"
onPointerDown={(event) => event.stopPropagation()}
onClick={() => setCollapsed((prev) => !prev)}
>
{collapsed ? "▢" : "—"}
</button>
<button
className="text-white/70 hover:text-white"
className="text-primary-foreground/70 hover:text-primary-foreground"
onPointerDown={(event) => event.stopPropagation()}
onClick={() => setOpen(false)}
>

View File

@ -114,7 +114,7 @@ export function MessageGroup({
);
return (
<ChainOfThought
className={cn("w-full gap-2 rounded-lg bg-white", className)}
className={cn("w-full gap-2 rounded-lg bg-ws-ffffff", className)}
open={true}
>
{aboveLastToolCallSteps.length > 0 && (

View File

@ -225,7 +225,7 @@ export function MessageList({
{showScrollToBottomButton && (
<ConversationScrollButton
className={cn(
"z-20 rounded-full border bg-white/90 shadow-sm backdrop-blur-sm",
"z-20 rounded-full border bg-ws-ffffff/90 shadow-sm backdrop-blur-sm",
scrollButtonClassName,
)}
title={t.chats.scrollToBottom}

View File

@ -157,7 +157,7 @@ function ThemePreviewCard({
"relative overflow-hidden rounded-md border text-xs transition-colors",
previewMode === "dark"
? "border-neutral-800 bg-neutral-900 text-neutral-200"
: "border-slate-200 bg-white text-slate-900",
: "border-slate-200 bg-ws-ffffff text-slate-900",
)}
>
<div className="border-border/50 flex items-center gap-2 border-b px-3 py-2">

View File

@ -39,7 +39,7 @@ export function TodoList({
return (
<div
className={cn(
"flex h-fit w-full origin-bottom translate-y-4 flex-col overflow-hidden rounded-t-xl border border-b-0 bg-white backdrop-blur-sm transition-all duration-200 ease-out",
"flex h-fit w-full origin-bottom translate-y-4 flex-col overflow-hidden rounded-t-xl border border-b-0 bg-ws-ffffff backdrop-blur-sm transition-all duration-200 ease-out",
hidden ? "pointer-events-none translate-y-8 opacity-0" : "",
className,
)}

View File

@ -28,7 +28,7 @@ export function WorkspaceSidebar({
<WorkspaceNavChatList />
{isSidebarOpen && <RecentChatList />}
</SidebarContent>
<SidebarFooter>{/* <WorkspaceNavMenu /> */}</SidebarFooter>
<SidebarFooter><WorkspaceNavMenu /></SidebarFooter>
<SidebarRail />
</Sidebar>
</>

View File

@ -226,7 +226,7 @@ test.describe("安全 / 思考块与敏感信息泄露", () => {
// 不限制在单条 assistant 消息内:以 Chain-of-thought 容器出现 “steps” 作为信号。
const stepsSignal = page
.locator(".not-prose.w-full.gap-2.rounded-lg.bg-white")
.locator(".not-prose.w-full.gap-2.rounded-lg.bg-background")
.locator("text=/steps/i");
const hasStepsSignal = await waitForConditionWithLeakCheck({