CopilotKit Threads 面板主题化实践:基于 V2 设计系统的定制组件设计与令牌继承
2026/9/10 19:23:36 网站建设 项目流程

CopilotKit Threads 面板主题化实践:基于 V2 设计系统的定制组件设计与令牌继承

【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit

本篇文章以 CopilotKit 仓库中examples/integrations/a2a-a2ui示例的 Threads 面板(Threads Drawer)为例,讲解如何围绕 CopilotKit V2 设计系统(@copilotkit/react-core/v2)为自定义业务组件建立统一的产品视觉语言。你将掌握 V2 设计令牌的继承方法、强制浅色模式的实现原理、与侧边栏聊天组件对齐的排版与圆角规范,以及如何在示例项目中定位这些实现细节。

背景:为什么 Threads 面板需要一份专属的设计说明

examples/integrations/a2a-a2ui这个 A2A(Agent-to-Agent)示例中,页面被组织成一个左右分栏布局:左侧是可切换的 Threads 面板(会话列表抽屉),右侧是通过@copilotkit/react-core/v2提供的CopilotChat聊天组件(见 app/page.tsx)。文档 THEME.md 明确说明:这个 Threads 面板是mastra 专属的定制副本(bespoke copies),它已经不再是共享/令牌化的基础组件——所有表面、边框、圆角与字号体系都直接取自 CopilotKit 的 V2 设计系统,目的只有一个:让左侧面板与右侧的CopilotSidebar聊天读起来像同一个产品。

这份设计说明的价值在于:它回答了一个在真实项目中高频出现的问题——当 CopilotKit 没有提供与你业务形态完全一致的现成组件时,如何让自研 UI 与 CopilotKit 内置聊天界面保持视觉上的"同频"。下面按令牌继承、强制浅色、排版体系、组件级落地四个维度展开。

V2 设计系统:主题令牌的唯一事实来源

THEME.md 指出,所有 surface(表面色)、border(边框)、radius(圆角)和 type ramp(字号梯度)都源自 CopilotKit V2 设计系统,具体文件为 packages/react-core/src/v2/styles/globals.css,以及聊天组件族:CopilotModalHeaderCopilotChatSuggestionPillCopilotChatInputCopilotSidebarView(均位于 packages/react-core/src/v2/components/chat)。

这些令牌被逐字(verbatim)镜像到示例应用的 app/globals.css 中。对照源码可以确认镜像关系:

  • :root--card: oklch(1 0 0)--destructive: oklch(0.577 0.245 27.325)--radius: 0.625rem等定义,与 V2globals.css第 12、24、34 行完全一致;
  • 圆角梯度通过calc()派生:--radius-sm--radius-md--radius-lg--radius-xl分别基于--radius加减 4px/2px,形成一套可组合的圆角体系。

核心令牌速查表(V2 Light 模式)

Token取值(V2 light)在面板中的角色
--card/--backgroundoklch(1 0 0)(白色)面板与卡片表面
--foregroundoklch(0.145 0 0)标题、会话标题、对话框文本
--muted/--secondary/--accentoklch(0.97 0 0)悬浮/激活表面、分段控件轨道、已归档徽章、代码井
--muted-foregroundoklch(0.556 0 0)元信息文本、静止图标、描述文字、占位符
--border/--inputoklch(0.922 0 0)所有发丝级细边框
--primaryoklch(0.205 0 0)(近黑)新建会话药丸、选中强调色、主 CTA
--primary-foregroundoklch(0.985 0 0)主按钮文字
--destructiveoklch(0.577 0.245 27.325)删除悬停态
--ringoklch(0.708 0 0)焦点环(2px box-shadow)
--radius0.625rem(+ sm/md/lg/xl)矩形控件圆角;图标按钮/药丸/分段控件使用999px

表格中有一个值得注意的设计决策:V2 侧边栏的主按钮是炭黑色/黑色,而非品牌强调色(the V2 sidebar's primary buttons are charcoal/black,nota brand accent)。这意味着--primary承担的是"选中态强调 + 主操作"双重职责——新建会话药丸、选中会话左侧强调条、确认对话框主按钮全部使用这个近黑色,而不是引入额外的品牌色变量。这在 threads-drawer.module.css 的.newThreadButton.threadItemSelected .threadAccent.dialogButtonPrimary中都有直接体现。

强制浅色:在深色系统下保持面板与侧边栏一致

mastra 的CopilotSidebar无论操作系统配色如何,始终渲染为浅色。为了让左侧面板与其保持一致,示例在 app/globals.css 中对两个作用域重新固定了--foreground--background为 V2 浅色值:

.threadsLayout, body > [role="presentation"] { --foreground: oklch(0.145 0 0); --background: oklch(1 0 0); }

这里的两个选择器各有明确目的:

  • .threadsLayout:页面根布局 wrapper(在 app/page.tsx 中通过<div className={${styles.layout} threadsLayout}>挂载),负责覆盖面板自身;
  • body > [role="presentation"]:确认删除对话框通过createPortal渲染在<body>上,必须单独覆盖其 portal 根。

CSS 中默认的深色模式@media (prefers-color-scheme: dark)块(globals.css 第 41-46 行)只会翻转裸页面上的--background/--foreground;面板的覆盖规则由于作用域限定在布局/portal 根上,优先级更高,因此深色系统下依然保持浅色。这是 CSS 层叠作用域胜过媒体查询的典型应用,也是保证"面板始终浅色"的机制原理。

排版:继承 Geist 与侧边栏的字号/字重梯度

面板使用应用字体 Geist,通过--font-body/--font-code变量(在 globals.css:root中定义为 Arial/Helvetica 与 SFMono/Menlo 的 fallback 链,而示例应用字体文件位于 app/fonts)。THEME.md 明确要求字号与字重严格跟踪侧边栏:

文本层级字号字重其他
头部标题(header title)1rem500tracking-tight
会话标题(thread titles)0.8125rem500
元信息(meta)0.6875rem全部 medium 字重

关键约束:不使用任何重700字重。这在 threads-drawer.module.css 中逐条可见:.drawerTitlefont-size: 1rem; font-weight: 500; letter-spacing: -0.01em.threadTitle0.8125rem+500.threadMeta0.6875rem。空状态标题(.emptyTitle)与锁定状态标题(.lockedTitle)同样保持500字重与tracking-tight,确保整棵组件树内不存在突兀的粗体。

落地实现:面板各区块如何消费这些令牌

以上设计决策并非只停留在文档层面,而是完整落在 threads-drawer.module.css(约 890 行)中。下面按区块拆解它的令牌消费方式,方便你对照源码阅读。

布局与抽屉骨架

.layout使用grid-template-columns: auto minmax(0, 1fr)构建"抽屉 + 主内容"两列;.drawer本身以background: var(--card)为表面、border-right: 1px solid var(--border)为发丝分隔线。展开态宽度18rem,折叠态3.5rem。折叠后呈现一条.collapsedRail垂直工具轨(展开按钮 + 新建会话按钮),两个按钮都是999px圆角的图标按钮——呼应侧边栏的关闭按钮。

头部与图标按钮

.drawerHeader镜像CopilotModalHeaderpadding: 1rem的节奏、底部1px solid var(--border)发丝线、标题不加粗。.iconButton采用2rem × 2remborder-radius: 999pxcolor: var(--muted-foreground),hover/focus 时上浮为background: var(--muted)+color: var(--foreground)。所有可交互元素(图标按钮、会话行、新建按钮、分段选项、对话框按钮)的焦点态统一为box-shadow: 0 0 0 2px var(--ring)——即文档中"focus rings (2px box-shadow)"的实现。

新建会话与分段筛选

.newThreadButton是"药丸"形态:border-radius: 999pxbackground: var(--primary)(近黑)、文字var(--primary-foreground),与侧边栏发送按钮(bg-black、rounded-full、medium 字重)一致。下方.filterBar中的.segmented分段控件是一个var(--muted)底色的999px轨道,激活项.segmentedOptionActivevar(--card)白色"抬起"并叠加0 1px 2px rgb(0 0 0 / 0.08)阴影——这正是文档所说的"白卡浮在 muted 轨道上"的 surface 关系。

会话行、强调条与归档徽章

.threadItem使用border-radius: var(--radius)(矩形圆角)与 hover 态background: var(--accent),行间不画分隔线(聊天列表同样无发丝线)。每行左侧有 3px 宽的.threadAccent强调条,默认透明,选中时填充var(--primary)。归档会话通过.archivedBadgevar(--muted)底、999px圆角、0.625rem字号)与标题降级(var(--muted-foreground)+font-weight: 400)来表达。

加载、空态与锁定态

初次加载时渲染 4 行骨架屏(.loadingRow),由threadsDrawerPulse关键帧以 1.4s 周期呼吸;空态/错误态使用.emptyCard——var(--radius-lg)圆角 + 发丝边框 + 白卡。无许可证时ThreadsPanelGate(见 locked-state.tsx)渲染.lockedPanel(20rem 宽),内部.lockedCard复用同一套卡面词汇:radius-lg、发丝边框、muted 图标章、以及展示npx copilotkit@latest license的 muted 代码井和近黑主 CTA 药丸。锁定态源码还展示了一个工程细节:该抽屉依赖useThreads这个仅客户端的外部 store,没有服务端快照,因此通过mounted状态推迟到客户端挂载,并在 SSR 阶段渲染一个.drawerPlaceholder占位块以避免首帧内容位移。

悬停操作区、删除确认与动画

会话行 hover 时右侧浮现操作按钮组(归档/恢复、删除),threadActionsopacity: 0 → 1translateY(-50%) scale(0.96 → 1)过渡,并为每个按钮提供纯 CSS::after伪元素 tooltip(白色卡面 + 发丝边框 +var(--radius-md)圆角 +0 8px 24px阴影,符合 V2 下拉/弹层表面,而非反转色芯片)。删除操作弹出ConfirmDialog,通过createPortal渲染到document.body,使用var(--radius-xl)圆角 +0 20px 50px阴影的卡片;删除按钮 hover 时用color-mix(in oklch, var(--destructive) 10%, transparent)生成浅红底色——无需额外定义红色变量即可获得"危险操作"的视觉提示。动画方面,threadItemEnter(420ms,cubic-bezier(0.16, 1, 0.3, 1))负责新会话入场,generatedTitleReveal(360ms)负责 AI 生成标题的模糊显现,二者在 threads-drawer.tsx 中通过enteringThreadIds/revealedTitleIds状态与定时器精确驱动。

响应式:小屏下的浮层化

@media (max-width: 1024px) 下,抽屉从网格占位列变为position: fixed的离屏浮层:折叠态缩成一个左上角悬浮发射器(白卡 + 发丝边框 + 阴影),展开态以width: min(20rem, 92vw)从左侧滑出并盖上聊天区;此时.drawerPlaceholder.lockedPanel均隐藏,避免留下死列。初始是否展开由 threads-drawer.tsx 中的window.innerWidth > 1024决定,且因组件仅客户端挂载,不会造成 hydration 不一致。

源码路径速查

  • 设计说明文档:examples/integrations/a2a-a2ui/app/components/threads-drawer/THEME.md
  • 令牌镜像与强制浅色:examples/integrations/a2a-a2ui/app/globals.css
  • 全部面板样式:examples/integrations/a2a-a2ui/app/components/threads-drawer/threads-drawer.module.css
  • 面板组件与交互逻辑:examples/integrations/a2a-a2ui/app/components/threads-drawer/threads-drawer.tsx
  • 锁定/许可证门控:examples/integrations/a2a-a2ui/app/components/threads-drawer/locked-state.tsx
  • 页面装配(左右分栏):examples/integrations/a2a-a2ui/app/page.tsx
  • A2UI 渲染主题:examples/integrations/a2a-a2ui/app/theme.ts
  • V2 设计系统源头:packages/react-core/src/v2/styles/globals.css 与 packages/react-core/src/v2/components/chat

总结

这份 Threads 面板设计说明为"在 CopilotKit 之上构建与官方聊天组件同源的自定义 UI"提供了一份可复用的方法论:先锁定设计令牌的唯一来源(V2 globals.css),逐字镜像到应用层;再通过作用域覆盖处理深色系统下的强制浅色;最后让每一个子区块(头部、图标按钮、药丸、分段控件、会话行、卡片、对话框、tooltip)都从同一套令牌取色取圆角取字重。这套做法的收益是双重的——既有"面板与聊天读起来像同一个产品"的用户体验一致性,也避免了在业务代码中散落魔法值。如果你的场景需要定制 CopilotKit 的周边 UI,完全可以沿用同样的路径:修改 globals.css 中的令牌值(这些文件是 mastra 专属、不与其它示例共享),即可让整个面板在保持结构不变的前提下完成一次全局换肤。

【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询