From 0ed3e07e44a7b59e20fbf0c597225b4440aa0e08 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Thu, 3 Sep 2026 14:35:09 +0800 Subject: [PATCH 1/3] refactor(thread-chat): introduce semantic style tokens Co-authored-by: CommandCodeBot --- CLAUDE.md | 2 +- .../branching/assistant/anchored-markdown.tsx | 9 +- .../branching/selection/bubble-shape.tsx | 4 +- .../selection/selection-draft-guard.css | 48 +-- .../branching/selection/text-anchor.ts | 14 +- .../chat/message/markdown-body.tsx | 7 +- .../artifacts/store-bound-project-panel.tsx | 16 +- .../orchestration/canvas/canvas-node.tsx | 2 +- .../orchestration/canvas/thread-canvas.tsx | 2 +- .../orchestration/canvas/use-canvas-layout.ts | 4 +- .../orchestration/columns/thread-columns.tsx | 4 +- .../navigation/thread-switcher-panel.tsx | 8 +- .../orchestration/overlays/help-panel.tsx | 2 +- app/thread-chat/styles/artifact-card.css | 40 +-- app/thread-chat/styles/boot-loading.css | 4 +- app/thread-chat/styles/branch-source.css | 4 +- app/thread-chat/styles/canvas.css | 143 ++++----- app/thread-chat/styles/columns-collapse.css | 22 +- app/thread-chat/styles/columns.css | 106 +++---- app/thread-chat/styles/composer.css | 32 +- app/thread-chat/styles/drawer.css | 206 ++++++------- app/thread-chat/styles/help-panel.css | 12 +- app/thread-chat/styles/keyframes.css | 2 +- app/thread-chat/styles/markdown.css | 275 +++++++++--------- app/thread-chat/styles/message-actions.css | 34 +-- app/thread-chat/styles/messages-stream.css | 52 ++-- app/thread-chat/styles/messages.css | 63 ++-- app/thread-chat/styles/scrollbar.css | 8 +- app/thread-chat/styles/selection.css | 94 +++--- app/thread-chat/styles/switcher-subtree.css | 10 +- app/thread-chat/styles/switcher.css | 75 ++--- app/thread-chat/styles/toast.css | 18 +- app/thread-chat/styles/tokens.css | 71 +---- app/thread-chat/styles/tokens/base.css | 33 +++ app/thread-chat/styles/tokens/border.css | 16 + .../styles/tokens/color-derived.css | 131 +++++++++ app/thread-chat/styles/tokens/content.css | 36 +++ app/thread-chat/styles/tokens/depth.css | 12 + app/thread-chat/styles/tokens/palette.css | 87 ++++++ app/thread-chat/styles/tokens/prose.css | 81 ++++++ app/thread-chat/styles/tokens/surface.css | 35 +++ app/thread-chat/styles/tokens/typography.css | 8 + app/thread-chat/styles/tokens/z-index.css | 24 ++ app/thread-chat/styles/topbar.css | 47 +-- app/thread-chat/styles/tree-list.css | 56 ++-- app/thread-chat/theme.ts | 13 +- .../.openspec.yaml | 2 + .../design.md | 116 ++++++++ .../implementation-record.md | 93 ++++++ .../proposal.md | 50 ++++ .../specs/thread-chat-styling/spec.md | 104 +++++++ .../tasks.md | 37 +++ 52 files changed, 1614 insertions(+), 760 deletions(-) create mode 100644 app/thread-chat/styles/tokens/base.css create mode 100644 app/thread-chat/styles/tokens/border.css create mode 100644 app/thread-chat/styles/tokens/color-derived.css create mode 100644 app/thread-chat/styles/tokens/content.css create mode 100644 app/thread-chat/styles/tokens/depth.css create mode 100644 app/thread-chat/styles/tokens/palette.css create mode 100644 app/thread-chat/styles/tokens/prose.css create mode 100644 app/thread-chat/styles/tokens/surface.css create mode 100644 app/thread-chat/styles/tokens/typography.css create mode 100644 app/thread-chat/styles/tokens/z-index.css create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/.openspec.yaml create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/design.md create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/implementation-record.md create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/proposal.md create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/specs/thread-chat-styling/spec.md create mode 100644 openspec/changes/refactor-thread-chat-style-tokens/tasks.md diff --git a/CLAUDE.md b/CLAUDE.md index 23f16edb..802e7080 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ Next.js **16** App Router project (React 19, TypeScript, Tailwind CSS **v4**), s - **shadcn/ui on Base UI, not Radix.** `components.json` uses the `base-rhea` style; primitives in `components/ui/` import from `@base-ui/react` (e.g. `@base-ui/react/button`). Don't reach for `@radix-ui/*` when editing or adding components. - **The full component kit is already vendored** in `components/ui/` (~60 components), including chat-oriented primitives: `message.tsx`, `message-scroller.tsx`, `bubble.tsx`, `attachment.tsx`, `marker.tsx`. Check for an existing component before adding or writing a new one. - **Tailwind v4, CSS-first config.** There is no tailwind.config file; theme tokens live as CSS variables in `app/globals.css`. Class merging goes through `cn()` in `lib/utils.ts`. -- **thread-chat 的手写样式** 是独立于 Tailwind 的一层:全部收敛在 `.tc` 作用域(手工命名空间的手稿风设计系统,语义类名、非原子类)。实体规则按功能区块拆在 `app/thread-chat/styles/*.css`,`app/thread-chat/thread-chat.css` 只是按**源码顺序** `@import` 它们的桶文件——改这里务必保持 `@import` 顺序(级联依赖它),且非相邻功能刻意拆成 `*-collapse/-stream/-extras` 等后缀文件以保序(如流式的 `.send.stop` 覆盖必须在 `composer.css` 之后)。设计 token(`--paper/--ink/--d1..d5/字体/尺寸`)的**单一来源**是 `styles/tokens.css`,`theme.ts` 的深度→变量映射依赖此处变量名。 +- **thread-chat 的手写样式** 是独立于 Tailwind 的一层:全部收敛在 `.tc` 作用域(手工命名空间的手稿风设计系统,语义类名、非原子类)。实体规则按功能区块拆在 `app/thread-chat/styles/*.css`,`app/thread-chat/thread-chat.css` 只是按**源码顺序** `@import` 它们的桶文件——改这里务必保持 `@import` 顺序(级联依赖它),且非相邻功能刻意拆成 `*-collapse/-stream/-extras` 等后缀文件以保序(如流式的 `.send.stop` 覆盖必须在 `composer.css` 之后)。设计 token 的单一入口是 `styles/tokens.css`,内部按序导入 `styles/tokens/`:primitive(`palette.css`)→ semantic(surface/content/border/depth/typography/prose/z-index)→ 派生色(`color-derived.css`);区块文件只能引用 semantic/contextual token,不能直引 primitive、写裸颜色或 `color-mix()`。`theme.ts` 的深度映射指向 `--tc-depth-*`。新增或迁移的 px 值必须为整数且尽量为偶数;确需保留亚像素时必须就近添加 `/* tc-review: 非偶数值,保留原因 */`。`.md-body` 只保留文本锚点 DOM 契约,正文排版由 `.tc-prose` 负责。 - Path aliases: `@/components`, `@/components/ui`, `@/lib`, `@/hooks` (see `components.json` and tsconfig.json). - Theming via `next-themes` through `components/theme-provider.tsx`, wired up in `app/layout.tsx` (dark mode toggles with the `d` key on the starter page). diff --git a/app/thread-chat/branching/assistant/anchored-markdown.tsx b/app/thread-chat/branching/assistant/anchored-markdown.tsx index 25242416..bbc8a196 100644 --- a/app/thread-chat/branching/assistant/anchored-markdown.tsx +++ b/app/thread-chat/branching/assistant/anchored-markdown.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useReducer, useRef } from "react" import type { Message, ThreadTreeState } from "../../core/types" import { threadTitle } from "../../core/selectors" -import { dc } from "../../theme" +import { dc, dvar } from "../../theme" import { MarkdownBody } from "../../chat/message/markdown-body" import { useSmoothText } from "../../chat/message/smooth-text" import { @@ -89,8 +89,11 @@ export function AnchoredMarkdown({ } } if (!markdownBody || !located) continue - const color = `color-mix(in srgb, var(--d${dc(fork.depth)}) 20%, transparent)` - paintRange(located.range, fork.threadId, color) + // 高亮底色统一走 CSS 派生 token(--tc-fc-mark 内做 20% 混色), + // 这里只注入 fork 深度的 contextual 变量(与 marks 的 fc-N 类同源) + paintRange(located.range, fork.threadId, "var(--tc-fc-mark)", { + "--fc": dvar(fork.depth), + }) const marks = markdownBody.querySelectorAll( `[data-text-anchor-mark="${cssEscape(fork.threadId)}"]` diff --git a/app/thread-chat/branching/selection/bubble-shape.tsx b/app/thread-chat/branching/selection/bubble-shape.tsx index 61391f66..e026bc18 100644 --- a/app/thread-chat/branching/selection/bubble-shape.tsx +++ b/app/thread-chat/branching/selection/bubble-shape.tsx @@ -101,7 +101,7 @@ export function buildBubblePath({ * 纯形状(svg 背景层)。面板宽 W、高 H,尾巴另占 ah 高。 * dir="down":面板在上、尾巴朝下(顶点 y=H+ah); * dir="up" :整条 path 竖直翻转,面板落到下半、尾巴翻到上半(顶点 y=0)。 - * fill 默认 var(--ink),与气泡深底一致;shadow 用 feDropShadow 复刻原气泡阴影。 + * fill 默认 var(--tc-surface-ink),与气泡深底一致;shadow 用 feDropShadow 复刻原气泡阴影。 */ export function BubbleShape({ W, @@ -109,7 +109,7 @@ export function BubbleShape({ cx, geo, dir, - fill = "var(--ink)", + fill = "var(--tc-surface-ink)", shadow = true, }: { W: number diff --git a/app/thread-chat/branching/selection/selection-draft-guard.css b/app/thread-chat/branching/selection/selection-draft-guard.css index ceb6a340..f8aae4fa 100644 --- a/app/thread-chat/branching/selection/selection-draft-guard.css +++ b/app/thread-chat/branching/selection/selection-draft-guard.css @@ -23,13 +23,13 @@ position: absolute; top: calc(100% + 7px); left: 0; - font-family: var(--font-mono); + font-family: var(--tc-typography-family-code); font-size: 10px; line-height: 1; - color: #e8e2d4; + color: var(--tc-onink-secondary); text-shadow: - 0 1px 2px rgba(30, 26, 20, 0.7), - 0 0 6px rgba(30, 26, 20, 0.5); + 0 1px 2px var(--tc-shadow-hint-strong), + 0 0 6px var(--tc-shadow-hint-soft); white-space: nowrap; pointer-events: none; animation: tc-fade-in 0.12s ease; @@ -43,8 +43,8 @@ .tc .sb-confirm-mask { position: fixed; inset: 0; - z-index: 70; - background: rgba(30, 26, 20, 0.32); + z-index: var(--tc-z-confirm); + background: var(--tc-shadow-confirm-mask); display: flex; align-items: center; justify-content: center; @@ -52,25 +52,25 @@ } .tc .sb-confirm { width: min(340px, calc(100vw - 48px)); - background: var(--paper); - color: var(--ink); - border: 1px solid color-mix(in srgb, var(--ink) 18%, transparent); + background: var(--tc-surface-base); + color: var(--tc-content-primary); + border: 1px solid var(--tc-ink-confirm-border); border-radius: 12px; padding: 18px 18px 16px; - box-shadow: 0 12px 32px rgba(30, 26, 20, 0.28); + box-shadow: 0 12px 32px var(--tc-shadow-confirm); animation: tc-pop 0.14s ease; } .tc .sb-confirm-title { - font-family: var(--font-ui); - font-size: 15px; + font-family: var(--tc-typography-family-ui); + font-size: 16px; font-weight: 700; margin-bottom: 8px; } .tc .sb-confirm-body { - font-family: var(--font-read); - font-size: 13px; + font-family: var(--tc-typography-family-read); + font-size: 14px; line-height: 1.55; - color: var(--ink-soft); + color: var(--tc-content-secondary); margin-bottom: 16px; } .tc .sb-confirm-actions { @@ -83,26 +83,26 @@ cursor: pointer; border-radius: 8px; padding: 7px 14px; - font-size: 13px; + font-size: 14px; font-weight: 600; - font-family: var(--font-ui); + font-family: var(--tc-typography-family-ui); } .tc .sb-confirm-actions button:focus-visible { - outline: 2px solid var(--ink); + outline: 2px solid var(--tc-content-primary); outline-offset: 1px; } .tc .sb-confirm-actions button.ghost { background: transparent; - color: var(--ink-soft); - border: 1px solid color-mix(in srgb, var(--ink) 25%, transparent); + color: var(--tc-content-secondary); + border: 1px solid var(--tc-ink-confirm-action-border); } .tc .sb-confirm-actions button.ghost:hover { - background: var(--paper-2); - color: var(--ink); + background: var(--tc-surface-raised); + color: var(--tc-content-primary); } .tc .sb-confirm-actions button.danger { - background: var(--d3); - color: #fff; + background: var(--tc-depth-3); + color: var(--tc-content-on-accent); } .tc .sb-confirm-actions button.danger:hover { filter: brightness(1.08); diff --git a/app/thread-chat/branching/selection/text-anchor.ts b/app/thread-chat/branching/selection/text-anchor.ts index 53e668cd..541c1b37 100644 --- a/app/thread-chat/branching/selection/text-anchor.ts +++ b/app/thread-chat/branching/selection/text-anchor.ts @@ -441,7 +441,10 @@ function isHighlightableTextNode(node: Text): boolean { export function paintRange( range: Range, id: string, - color = "rgba(255, 214, 0, 0.4)" + /** 高亮底色:CSS 值或变量引用(如 "var(--tc-fc-mark)"),不内置颜色字面量 */ + color: string, + /** 需要一并写到高亮 span 上的 CSS 变量(contextual 注入,如 { "--fc": dvar(depth) }) */ + vars?: Record ): boolean { if (range.collapsed) return false const nodes = collectTextNodes(range) @@ -467,6 +470,11 @@ export function paintRange( const span = target.ownerDocument.createElement("span") span.setAttribute(MARK_ATTR, id) + if (vars) { + for (const [name, value] of Object.entries(vars)) { + span.style.setProperty(name, value) + } + } span.style.background = color span.style.borderRadius = "2px" span.style.color = "inherit" @@ -513,11 +521,11 @@ function cssEscape(value: string): string { * const anchor = describeRange(root, range) // { quote, position } * if (anchor) save(id, anchor) // JSON.stringify 存起来 * - * // 2) 恢复高亮:页面(可能已漂移)加载后,把锚点找回来重新绘制 + * 2) 恢复高亮:页面(可能已漂移)加载后,把锚点找回来重新绘制 * for (const { id, anchor } of load()) { * const hit = locateAnchor(root, anchor) // 三层降级 position/exact/fuzzy * if (hit) { - * paintRange(hit.range, id) + * paintRange(hit.range, id, "var(--tc-fc-mark)", { "--fc": dvar(depth) }) * console.log(id, hit.strategy, hit.score) // 'fuzzy' 时 score < 1 * } else { * console.warn(id, '内容改动过大,判定丢失') diff --git a/app/thread-chat/chat/message/markdown-body.tsx b/app/thread-chat/chat/message/markdown-body.tsx index 7a623714..a62c3d60 100644 --- a/app/thread-chat/chat/message/markdown-body.tsx +++ b/app/thread-chat/chat/message/markdown-body.tsx @@ -6,7 +6,8 @@ * 关键约束:整个组件按 source 用 memo 记忆——source 不变时绝不重渲染。 * 这样 anchored-markdown 在渲染后「手绘」到 .md-body 里的锚点高亮 / 脚注上标, * 不会被 React 的 reconcile 抹掉(详见 anchored-markdown 的绘制 effect 注释)。 - * 渲染进一个稳定容器 `
`,即锚点定位的坐标系容器。 + * 渲染进稳定容器 `
`:md-body 是锚点坐标契约, + * tc-prose 承担正文排版。 */ import React, { @@ -92,7 +93,7 @@ function CodeBlock({ ) return ( -
+
{lang ? (
{lang} @@ -189,7 +190,7 @@ export const MarkdownBody = memo(function MarkdownBody({ return (
diff --git a/app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx b/app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx index bcd3d291..c3b88b2a 100644 --- a/app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx +++ b/app/thread-chat/orchestration/artifacts/store-bound-project-panel.tsx @@ -8,22 +8,28 @@ import type { ConversationCommands } from "../../net/commands/conversation-comma import { ProjectPanel } from "./project-panel" function findMessageElement(messageId: string): HTMLElement | null { - return [...document.querySelectorAll("[data-thread-chat-message-id]")].find( - (element) => element.dataset.threadChatMessageId === messageId - ) ?? null + return ( + [ + ...document.querySelectorAll( + "[data-thread-chat-message-id]" + ), + ].find((element) => element.dataset.threadChatMessageId === messageId) ?? + null + ) } function revealMessage(messageId: string, attempt = 0) { const element = findMessageElement(messageId) if (!element) { - if (attempt < 8) window.setTimeout(() => revealMessage(messageId, attempt + 1), 60) + if (attempt < 8) + window.setTimeout(() => revealMessage(messageId, attempt + 1), 60) return } element.scrollIntoView({ behavior: "smooth", block: "center" }) element.animate( [ { backgroundColor: "transparent" }, - { backgroundColor: "color-mix(in srgb, currentColor 8%, transparent)" }, + { backgroundColor: "var(--tc-ink-hover)" }, { backgroundColor: "transparent" }, ], { duration: 1600, easing: "ease-out" } diff --git a/app/thread-chat/orchestration/canvas/canvas-node.tsx b/app/thread-chat/orchestration/canvas/canvas-node.tsx index 4a8d406a..aa837bae 100644 --- a/app/thread-chat/orchestration/canvas/canvas-node.tsx +++ b/app/thread-chat/orchestration/canvas/canvas-node.tsx @@ -60,7 +60,7 @@ export const CanvasCard = memo(function CanvasCard({ className="canvas-card" /* 选中态样式由 .react-flow__node.selected 提供;此前的条件类拼接丢空格产出 canvas-cardexpanded 单 token,选中即丢全部卡片样式(codex review P1) */ style={ { - "--accent": data.accent, + "--tc-accent": data.accent, "--canvas-card-width": `${CANVAS_CARD_DIMENSIONS.width}px`, "--canvas-card-padding-block": `${CANVAS_CARD_DIMENSIONS.paddingBlock}px`, "--canvas-card-padding-inline": `${CANVAS_CARD_DIMENSIONS.paddingInline}px`, diff --git a/app/thread-chat/orchestration/canvas/thread-canvas.tsx b/app/thread-chat/orchestration/canvas/thread-canvas.tsx index 5a4c77ac..7acec120 100644 --- a/app/thread-chat/orchestration/canvas/thread-canvas.tsx +++ b/app/thread-chat/orchestration/canvas/thread-canvas.tsx @@ -193,7 +193,7 @@ function CanvasFlow({ pannable zoomable nodeClassName={minimapNodeClass} - maskColor="rgba(245, 242, 234, 0.75)" + maskColor="var(--tc-canvas-minimap-mask)" />
-
+
{selectedArtifact.kind === "markdown" && ( - + )} {selectedArtifact.kind === "code" && (
{selectedArtifact.content}
@@ -552,7 +576,8 @@ export function ProjectPanel({
PROJECT ARTIFACTS

整个 Project 的持久化成果

- 包含根 Thread 和所有 Fork 产生的 Artifact;仅发现与查看,不会自动注入无关 Thread。 + 包含根 Thread 和所有 Fork 产生的 + Artifact;仅发现与查看,不会自动注入无关 Thread。

@@ -569,7 +594,8 @@ export function ProjectPanel({ 还没有 Artifact - 在任意 Thread 中生成 Markdown、Code 或 Note 后会出现在这里。 + 在任意 Thread 中生成 Markdown、Code 或 Note + 后会出现在这里。
) : ( diff --git a/app/thread-chat/orchestration/canvas/canvas-expand.tsx b/app/thread-chat/orchestration/canvas/canvas-expand.tsx index 97a97a9d..70dd97fd 100644 --- a/app/thread-chat/orchestration/canvas/canvas-expand.tsx +++ b/app/thread-chat/orchestration/canvas/canvas-expand.tsx @@ -71,6 +71,7 @@ export function CanvasExpand({ state={state} message={assistantMessage} onOpenThread={(id) => actions.focusThread(id)} + density="compact" /> ) : null } diff --git a/app/thread-chat/orchestration/canvas/canvas-node.tsx b/app/thread-chat/orchestration/canvas/canvas-node.tsx index aa837bae..5b20ba4c 100644 --- a/app/thread-chat/orchestration/canvas/canvas-node.tsx +++ b/app/thread-chat/orchestration/canvas/canvas-node.tsx @@ -57,7 +57,7 @@ export const CanvasCard = memo(function CanvasCard({ }: NodeProps) { return (
- n.data.depth > 0 ? `fc-${dc(n.data.depth)}` : "" + n.data.depth > 0 ? `tc-fork-context fc-${dc(n.data.depth)}` : "" export interface ThreadCanvasProps { store: ThreadTreeReadableStore diff --git a/app/thread-chat/orchestration/columns/thread-columns.tsx b/app/thread-chat/orchestration/columns/thread-columns.tsx index c2356a04..e774832b 100644 --- a/app/thread-chat/orchestration/columns/thread-columns.tsx +++ b/app/thread-chat/orchestration/columns/thread-columns.tsx @@ -36,7 +36,7 @@ function ColumnShell({ const isMain = thread.id === "main" return (
720px`,与 `--lane-max: 760px` 联动校准)。compact 等修饰符为窄/标准/宽松档分别提供离散偶数字号与间距 token,由同一容器查询切换,SHALL NOT 使用 `0.9` 乘法产生 `14.4px/12.6px` 等亚像素计算值。实施 `container-type: inline-size` 时须同步审计 containment 副作用:布局包含使该元素成为后代 `absolute/fixed` 定位的包含块,switcher 弹层(z 72/74)、help-panel 等依赖更外层定位祖先的元素需逐一核对登记。 +`MarkdownBody` 通过 typed `density="default" | "compact"` 作为唯一变体入口;Canvas 和 Artifact 只选择 density,不在各自区块 CSS 中重写 `.tc-prose` 或标题字号。容器宽度负责档位,density 负责同档位内的密度,两者正交组合。 + ### D10:双轨验收 - **轨 A(颜色、z-index、引用面清理)**:值不变、只改名换引用——以「构建产物 CSS 除变量名外无规则级差异 + grep 清单零命中 + 计算样式抽查等价」验收。 @@ -102,7 +106,7 @@ primitive 初值映射:`#fdfbf8→--tc-palette-paper-100`、`#f4f0e9→paper-2 2. **z-index 轮(轨 A)**:两族语义 token + 10 处收口 + stacking 审计。 3. **prose 结构轮(轨 B)**:`.tc-prose` 迁移(markdown + drawer + canvas)→ 数值取整(D7 表)→ 轨 B 截图基线。 4. **档位轮(轨 B)**:容器查询 + compact 缩放系数 → 五场景截图验收。 -5. 每轮独立 commit,revert 即回滚;颜色轮与后续轮之间无交叉依赖。 +5. 每轮必须形成可独立审查的验收检查点;最终提交若因交付需要压缩,implementation record 必须保留各检查点的验证证据与行为边界,避免把颜色等价迁移与排版视觉变化描述成同一类改动。 > **执行顺序修订(用户指令 2026-09-02)**:第 1、2 步为第一阶段(本轮 apply,零差异); > 第 3、4 步整体推迟到改造验收通过后单独执行,且第二阶段先做「值原样保留」的结构迁移 diff --git a/openspec/changes/refactor-thread-chat-style-tokens/implementation-record.md b/openspec/changes/refactor-thread-chat-style-tokens/implementation-record.md index 752f27fb..bb9d6524 100644 --- a/openspec/changes/refactor-thread-chat-style-tokens/implementation-record.md +++ b/openspec/changes/refactor-thread-chat-style-tokens/implementation-record.md @@ -20,7 +20,11 @@ primitive 仅定义在 `tokens/palette.css`;区块文件只消费 semantic/con ## 派生色用途聚类 -`tokens/color-derived.css` 是 `color-mix()` 与 alpha 色的唯一生产代码归属文件: +`tokens/color-derived.css` 是 `color-mix()` 与 alpha 色的唯一生产代码归属文件。派生公式仅消费 semantic 值:白色透明层从 `--tc-surface-plain` 取色,阴影从 `--tc-surface-shadow-source` / `--tc-surface-shadow-deep-source` 取色;不再重复写 `#fff` 或阴影 RGB 源值。 + +contextual 派生只在显式 `.tc-accent-context` / `.tc-fork-context` 边界计算。注入 `--tc-accent` / `--fc` 的元素同时挂对应 class;token 层不再维护业务组件白名单,accent/fork 公式各保留一份。 + +用途聚类如下: - contextual accent:浅底、选中底、焦点环、横幅底/边框、闪烁色、Artifact tab 底。 - fork color:锚点下划线/高亮、Artifact 图标/进度底、进度轨道。 @@ -56,8 +60,10 @@ prose token 的标题间距、列表项间距、引用缩进与代码 padding - 10 处裸 z-index 已全部替换为 `--tc-z-*`。 - 列内 token 只在 `.column` 或 selection 内容内部的局部层叠上下文生效;文档弹层均为 `position: fixed`,未发现带 transform/filter/opacity 的共同祖先隔离其层级。 -## Container containment 审计 +## Prose 变体与 Container containment 审计 +- `MarkdownBody` 以 typed `density="default" | "compact"` 输出 `.tc-prose` / `.tc-prose-compact`;Canvas 与 Artifact 通过组件参数选择 compact。 +- 删除 drawer 对正文 `font-size` 的高优先级覆盖,以及 Canvas 对 h1~h4 的专用字号覆盖;正文和标题都由 density token + 容器档位统一决定。 - `.lane`:只包列内阅读内容;switcher/help-panel 不在其后代,不改变弹层定位祖先。 - `.art-body`:只包抽屉正文,不包含抽屉自身或全局弹层。 - `.canvas-expand`:容器声明在外挂面板自身;面板仍由 `.canvas-card { position: relative }` 定位,containing block 不变。 @@ -79,6 +85,10 @@ prose token 的标题间距、列表项间距、引用缩进与代码 padding 最终验证:`pnpm typecheck`、`pnpm build`、`pnpm openspec:validate` 均通过;OpenSpec 27/27 项合法。 +## 交付与验收边界 + +颜色/z-index 等价迁移、`.tc-prose` 结构迁移、数值取整、容器档位曾分别完成验收检查点;当前 PR 的最终提交包含这些已批准阶段。review 修复后不再声称整个提交是单一“零差异迁移”,而是按上述检查点分别解释和验证,避免颜色等价结论掩盖排版视觉变化。 + ## 扫描豁免 `app/thread-chat/gate-3-harness/normalized-harness.tsx` 的控制面板是测试 harness 自身,不属于 `.tc` 产品样式体系;其中 3 处内联测试页颜色列为 grep 白名单。Shiki 运行时主题色注入同样豁免。 diff --git a/openspec/changes/refactor-thread-chat-style-tokens/specs/thread-chat-styling/spec.md b/openspec/changes/refactor-thread-chat-style-tokens/specs/thread-chat-styling/spec.md index b28485bb..0a28f99e 100644 --- a/openspec/changes/refactor-thread-chat-style-tokens/specs/thread-chat-styling/spec.md +++ b/openspec/changes/refactor-thread-chat-style-tokens/specs/thread-chat-styling/spec.md @@ -51,7 +51,7 @@ thread-chat 的设计 token SHALL 采用分层模型定义在 `app/thread-chat/s - **primitive 层**(纯值:`--tc-palette-*` 色板、`--tc-font-stack-*` 字体栈等,不代表用途)SHALL 仅被 semantic 层引用; - **semantic 层**(语义角色,统一命名 `--tc-{类目}-{语义角色}-{状态?}`,如 `--tc-surface-base`、`--tc-typography-family-ui`)是区块样式与组件**唯一允许引用**的层; - **component 层**(`--tc-composer-*`)仅当单个组件需要独立覆写语义值时才允许创建; -- **contextual 层**(如 `--fc/--dc/--accent` 的收口形态)由 TS 按实例注入,fallback SHALL 引用 semantic 层。 +- **contextual 层**(如 `--fc/--dc/--accent` 的收口形态)由 TS 按实例注入,fallback SHALL 引用 semantic 层;派生公式 SHALL 位于显式上下文边界类中,token 层 SHALL NOT 枚举业务组件选择器。 已纳入**当前迁移范围**类目的值 SHALL 经 semantic token 引用,SHALL NOT 重新声明 token、直接引用 primitive 或写裸值;**未纳入当前迁移范围**的存量裸值允许保留原状,但不得新增。`app/thread-chat/styles/tokens.css` SHALL 保留为单一入口文件(内部 `@import` 分层文件);`theme.ts` 的「深度 → CSS 变量名」映射 SHALL 指向语义 token 名,重命名 SHALL 同步更新该映射及 `anchored-markdown.tsx` 等动态拼接处。 @@ -70,13 +70,18 @@ thread-chat 的设计 token SHALL 采用分层模型定义在 `app/thread-chat/s - **WHEN** 检查 `--fc/--dc/--accent` 的 fallback 声明 - **THEN** fallback 均为 semantic token 引用(如 `var(--tc-depth-1)`),不存在硬编码色值 +#### Scenario: contextual 边界显式声明 + +- **WHEN** 组件注入 `--tc-accent` 或 `--fc` +- **THEN** 同一元素挂载对应的 `.tc-accent-context` 或 `.tc-fork-context`,派生 token 文件无需知道该组件的业务 class + ### Requirement: `.tc-prose` 插件式正文排版 markdown 正文排版 SHALL 收敛为 `.tc-prose` 排版类,排版值全部由 prose 语义 token 驱动: - 迁移范围 SHALL 包含 `markdown.css` 的全部排版规则**以及** `drawer.css`(`.tc .art-body .md-body`)与 `canvas.css`(`.tc .canvas-expand .md-body h1~h4` 等)中的排版选择器,统一迁至 `.tc-prose`;`.md-body` SHALL 仅保留「锚点定位坐标系」DOM 契约(TS 字符串选择器 `closest(".md-body")` 等不变),SHALL NOT 再承担排版职责; - 阅读档位(紧凑 / 标准 / 宽松)SHALL 由**正文容器宽度**经容器查询(`@container`)驱动,SHALL NOT 使用视口媒体查询决定正文排版(多列布局下视口宽度不代表列宽);档位 token 挂 `.tc` 根,档位切换 SHALL 允许视觉变化并以截图基线验收; -- 尺寸修饰符(至少 `.tc-prose-compact`)SHALL 以各容器档位对应的离散偶数 token 与档位正交组合,SHALL NOT 通过产生亚像素计算值的比例缩放实现; +- 尺寸修饰符(至少 `.tc-prose-compact`)SHALL 以各容器档位对应的离散偶数 token 与档位正交组合,SHALL NOT 通过产生亚像素计算值的比例缩放实现;`MarkdownBody` SHALL 通过显式 typed density 选择修饰符,场景区块 SHALL NOT 直接覆写 `.tc-prose` 或标题字号; - 迁移时排版值 SHALL 遵守数值规范(整数偶数 + 待查验标注)。 #### Scenario: md-body 仅保留 DOM 契约 diff --git a/openspec/changes/refactor-thread-chat-style-tokens/tasks.md b/openspec/changes/refactor-thread-chat-style-tokens/tasks.md index 7dbd5296..6a3406cd 100644 --- a/openspec/changes/refactor-thread-chat-style-tokens/tasks.md +++ b/openspec/changes/refactor-thread-chat-style-tokens/tasks.md @@ -35,3 +35,10 @@ - [x] 5.1 `pnpm openspec:validate` 通过;变更登记表(颜色映射、color-mix 用途聚类、非偶数取整、stacking 审计)归档至 change 目录 - [x] 5.2 更新 `CLAUDE.md` 中 thread-chat 手写样式段落:token 单一来源描述改为分层模型 + `tokens/` 结构 + 数值规范(整数偶数、`tc-review` 标注) - [x] 5.3 在本 change 附 Future Work 清单(space/radius/elevation/state、sizing/motion/focus/scrim、UI 排版 role、主题注入机制与深浅色),作为后续 Change 的输入 + +## 6. Review 修复 + +- [x] 6.1 `MarkdownBody` 增加显式 `default | compact` density;Canvas 与 Artifact 通过组件契约选择 compact,删除 drawer/canvas 对 `.tc-prose` 字号和标题的专用覆盖,确保容器档位与 density 正交组合 +- [x] 6.2 contextual 派生改为显式 `.tc-accent-context` / `.tc-fork-context` 边界,删除 token 层对业务组件选择器的白名单与根节点重复公式 +- [x] 6.3 派生色中的白色、阴影色全部从 semantic/primitive 单一来源取值,清除未消费的 palette 基色与 `#fff`/重复 RGB 源值 +- [x] 6.4 校正实施文档:区分已完成的独立验收检查点与最终提交组织,记录 review 后的 canonical 变体/上下文边界 From 31dc0bade0c5b02f9948001ea3d79e4d0c166099 Mon Sep 17 00:00:00 2001 From: zilin <276161014@qq.com> Date: Thu, 3 Sep 2026 19:48:22 +0800 Subject: [PATCH 3/3] chore(commandcode): add project taste preferences --- .commandcode/taste/frontend/taste.md | 13 +++++++++++++ .commandcode/taste/taste.md | 6 ++++++ .commandcode/taste/workflow/taste.md | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 .commandcode/taste/frontend/taste.md create mode 100644 .commandcode/taste/taste.md create mode 100644 .commandcode/taste/workflow/taste.md diff --git a/.commandcode/taste/frontend/taste.md b/.commandcode/taste/frontend/taste.md new file mode 100644 index 00000000..b414b0ab --- /dev/null +++ b/.commandcode/taste/frontend/taste.md @@ -0,0 +1,13 @@ +# Frontend / Styling + +- Wants CSS refactored away from raw files joined by `@import` cascade order into a managed, tooling-based architecture (Sass/Less, or a Tailwind-plugin-style abstraction like `@tailwindcss/typography`). Confidence: 0.85 +- Strongly prefers semantic design tokens: every fontsize, padding, margin, and background should carry meaning (e.g. `--surface-raised`, `--text-body`) rather than hardcoded pixel values. Confidence: 0.9 +- Wants responsive typography/readability controlled via CSS variables + classNames across breakpoints (mobile / pc / lg screens), instead of scattered per-file media queries. Confidence: 0.85 +- Wants tokens and colors managed through a config layer that supports override and extension (theme config / Tailwind theme tokens). Confidence: 0.85 +- When defining conventions (token taxonomies, naming grammars, patterns), wants them grounded in and traceable to established industry design systems (e.g. Material 3, IBM Carbon, GitHub Primer, Atlassian, Apple HIG, Polaris, Ant Design) rather than invented ad hoc — asked to extend the semantic categories "from big-company styleguides you know". Confidence: 0.8 +- Inviolable rule (user-declared "不可打破的原则"): px values must be integers, preferably even — fractional px like `font-size: 12.5px` are rejected and rounded to nearest even (12.5→12, 13.5→14); values that genuinely can't be even (sub-pixel `letter-spacing: 0.5px`, `1.5px` borders, negative hot-zone margins) stay but must carry an inline flag for later re-inspection (e.g. `/* tc-review: 非偶数值,保留原因 */`). Confidence: 0.95 +- In styling refactors, tokenize ALL colors first as the initial round — including collapsing every `color-mix()` into a derived variable/token — before migrating other value categories (spacing, sizing, etc.). Confidence: 0.9 +- Color organization must be deliberately re-planned by proximity: similar colors grouped adjacent (e.g. paper family → ink family → depth family → functional colors → whites/shadows) so palette files read as clusters. Confidence: 0.9 +- z-index must always be controlled through semantic variables (named layer roles, e.g. column-local layers vs overlay layers with stacking-context audit notes), never bare numeric literals. Confidence: 0.95 +- Cares that finished styling modules (e.g. the `.tc-prose` typography module) are reusable in other Tailwind projects — asked about cross-project portability before confirming: values self-contained, zero-runtime, purely CSS-variable-driven forms (vendor copy → npm package → Tailwind `@plugin`) over build-tool-coupled ones (Sass mixins would become dead code once copied out). Confidence: 0.65 +- Styling-refactor acceptance runs on two tracks that must never be mixed: value-preserving steps (token renames, same-value re-pointing like `#b07d2e` → `--tc-depth-2`, deleting dead fallbacks) must prove computed-style equivalence (grep zero-hit lists + build-output comparison), while any value-altering fix (e.g. re-targeting the unsaved-state gold to depth-1) is logged as a registered visual change and moved to the screenshot-baseline track — ambiguous historical intent gets logged as a decision, never silently fixed. Confidence: 0.7 diff --git a/.commandcode/taste/taste.md b/.commandcode/taste/taste.md new file mode 100644 index 00000000..57d8fa07 --- /dev/null +++ b/.commandcode/taste/taste.md @@ -0,0 +1,6 @@ +# Taste + +## Communication +- Communicates in Simplified Chinese; respond in Chinese. Confidence: 0.7 +- When asked to react to external reviews (e.g. a GPT critique of its own spec artifacts), first fact-checks the review's claims against the codebase, then delivers a structured verdict — verified-valid points, points held with reservations, plus findings the review missed — and proposes concrete revisions instead of blindly accepting or dismissing. Confidence: 0.8 +- Also commissions adversarial re-audits of the assistant's own recent replies, via a second model ("@codex 你来阅读下这最近2条回复看下是否有不对的地方") — so every factual/numeric claim written into replies or artifacts may be re-checked later; on such an audit pass, re-verify each claim against the code (grep counts, file:line evidence, exact numbers not approximations), report findings classified by severity (substantive error vs inconsistency/omission), own mistakes plainly, propose concrete fixes to the artifacts, and apply corrections only after confirmation. Confidence: 0.75 diff --git a/.commandcode/taste/workflow/taste.md b/.commandcode/taste/workflow/taste.md new file mode 100644 index 00000000..6b79e112 --- /dev/null +++ b/.commandcode/taste/workflow/taste.md @@ -0,0 +1,7 @@ +# Workflow / Process + +- Uses OpenSpec for change management: formalize a design as a change (`pnpm exec openspec new change ""`), write artifacts in the tool's prescribed build order (proposal → specs delta → design → tasks), fetching per-artifact instructions via `openspec instructions --change ...`, and finish with `openspec validate --all --strict`. Confidence: 0.85 +- Wants change artifacts (design/tasks) grounded in verified code reality before being written — e.g. confirmed `theme.ts` dynamically builds `var(--d${N})`, enumerated every `.md-body` selector dependency in TS/CSS — rather than resting on discussion-level assumptions. Confidence: 0.75 +- Operates as a principles-then-review loop: states a few inviolable principles up front, delegates the remaining design judgment ("其他的你再看看"), and then wants the written/revised artifacts summarized and presented for his explicit confirmation before implementation/apply begins ("给我再看一眼变更,我来确认") — end the authoring phase by waiting, don't proceed into apply unbidden. Confidence: 0.85 +- When confirming a design before apply, wants concrete end-state previews — the finished module's form factor, the planned directory/file structure (e.g. the `tokens/` tree), and sample code — not prose summaries alone ("可以先把 tokens 的结构给我看吗"). Confidence: 0.7 +- When a batch of edits lands on the same artifact in parallel, don't trust the per-edit result snapshots (they can show stale/inconsistent state) — re-verify the file's final on-disk state (grep for each inserted marker/phrase, spot-read the long lines) before running validation or reporting completion; caught this twice (design.md, tasks.md) and corrected course. Confidence: 0.65