Cloudflare Agents 前端视觉体系实战:基于 Kumo 设计系统构建 Agent Playground 界面
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
本文以仓库中的 design/visuals.md 为核心,系统讲解 Cloudflare Agents 项目(GitHub_Trending/agents1/agents)如何基于 Cloudflare 内部设计系统Kumo(@cloudflare/kumo)统一构建所有示例与 Playground 的前端界面。你将掌握 Kumo 与 Tailwind v4 的集成方式、基于data-mode的自动明暗模式切换、LinkProvider与 React Router 的类型适配方案,以及设计系统能力不足时如何用自定义组件补齐缺口的工程决策。
一、设计系统选型:为什么使用 Kumo
Playground(以及后续所有示例)统一采用 Cloudflare 内部设计系统 Kumo(@cloudflare/kumo),而不是自建组件原语。它带来了三层核心收益:
- 语义化颜色令牌(semantic color tokens):如
bg-kumo-base、text-kumo-default、border-kumo-line,颜色含义由语义驱动,而非固定色值; - 无障碍组件:按钮、输入框、表单控件等均内置 ARIA 支持;
- 自动明暗模式:无需在每个组件上手动维护主题逻辑。
从仓库看,这一决策已贯穿整个示例体系:examples/下几乎所有示例的src/client.tsx与src/styles.css都导入了 Kumo,而 Playground 是落地最完整的参考实现(入口见 examples/playground/src/client.tsx)。
依赖清单
在 examples/playground/package.json 中可以确认以下关键依赖:
| 依赖 | 版本 | 作用 |
|---|---|---|
@cloudflare/kumo | ^2.6.0 | 设计系统组件库(monorepo 根目录安装为 devDependency) |
@phosphor-icons/react | ^2.1.10 | Kumo 的配套图标库(peer 依赖) |
@tailwindcss/vite | ^4 | Tailwind v4 的 Vite 插件 |
图标使用规范:永远使用*Icon后缀导出
@phosphor-icons/reactv2 中,带Icon后缀的导出(如TrashIcon、ShieldIcon)是推荐用法,裸名(Trash、Shield)已废弃。仓库中的真实调用印证了这一点:
- examples/playground/src/components/LogPanel.tsx 中使用了
TrashIcon; - examples/playground/src/layout/Sidebar.tsx 中大量使用
CaretDownIcon、CaretRightIcon、CubeIcon、ChatDotsIcon、MoonIcon、SunIcon等。
二、环境搭建:Tailwind v4 与 Kumo 的集成
Kumo 自带 Tailwind 插件,需要在 Vite 构建链路中挂载@tailwindcss/vite,并在 CSS 入口中显式导入。
Vite 配置
examples/playground/vite.config.ts 中的插件顺序为:
import { cloudflare } from "@cloudflare/vite-plugin"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import agents from "agents/vite"; import { defineConfig } from "vite"; export default defineConfig({ plugins: [agents(), react(), tailwindcss(), cloudflare()], define: { __filename: "'index.ts'" } });tailwindcss()插件与@vitejs/plugin-react、@cloudflare/vite-plugin并列注册。注意这里的agents()是 agents SDK 自己的 Vite 插件,负责在本地开发时注入 Worker 运行时。
styles.css 的三行关键配置
examples/playground/src/styles.css 顶部:
@import "tailwindcss"; @import "@cloudflare/kumo/styles/tailwind"; /* Tailwind ignores node_modules by default, so we source Kumo for class extraction. */ @source "../node_modules/@cloudflare/kumo/dist/**/*.{js,jsx,ts,tsx}";这里有一个重要的工程细节:Tailwind 默认忽略node_modules,因此必须用@source指令将 Kumo 的产物目录纳入类名提取范围,否则 Kumo 组件内部使用的语义类(bg-kumo-base等)不会被生成。
关于@source路径,文档特别强调了一个易踩的坑:@source路径是相对于src/styles.css的(即../node_modules),它指向示例自己的node_modules。这个写法在 pnpm workspace 中(每个包都有自己的带符号链接的node_modules)与示例被单独复制出去独立安装两种场景下都能正确解析,因此不要写成../../../node_modules这类 monorepo 根目录相对路径。
其他示例的对比
以 examples/channels/src/styles.css 等为代表的示例同样遵循这一模式,验证了这是整个示例体系的统一约定,而非 Playground 独有。
三、明暗模式:data-mode属性驱动的主题系统
与常见的 Tailwinddark:类前缀方案不同,Kumo 使用<html>元素上的data-mode属性控制主题。所有 Kumo 语义令牌(bg-kumo-base、text-kumo-default、border-kumo-line等)会自动响应这个属性,整个代码库中不需要任何dark:前缀。
这套机制由两个部分配合完成:
1. index.html 中的首屏防闪烁脚本
examples/playground/index.html 的<head>内联脚本在页面加载前同步执行:
<script> (() => { const mode = localStorage.getItem("theme") || "light"; document.documentElement.setAttribute("data-mode", mode); document.documentElement.style.colorScheme = mode; })(); </script>它在 React 应用挂载之前就把data-mode写入<html>,避免了明暗主题切换时的白屏/闪烁(FOUC)。同时设置了style.colorScheme,让浏览器原生控件(滚动条、表单控件)也跟随主题。
2. 内联 ModeToggle 组件
每个示例内置一份ModeToggle组件(不共享自公共包),负责运行时切换。examples/playground/src/layout/Sidebar.tsx 中的实现如下:
function ModeToggle() { const [mode, setMode] = useState( () => localStorage.getItem("theme") || "light" ); useEffect(() => { document.documentElement.setAttribute("data-mode", mode); document.documentElement.style.colorScheme = mode; localStorage.setItem("theme", mode); }, [mode]); return ( <Button variant="ghost" shape="square" aria-label="Toggle theme" onClick={() => setMode((m) => (m === "light" ? "dark" : "light"))} icon={mode === "light" ? <MoonIcon size={16} /> : <SunIcon size={16} />} /> ); }工作流程是:useState读取localStorage中的theme作为初始值 →useEffect将模式写入data-mode与colorScheme并持久化 → 点击按钮在light/dark间切换,图标也随之从MoonIcon变为SunIcon。
色彩主题(Color Themes)
所有示例目前均使用 Kumo 的默认主题,没有任何自定义主题覆盖。Kumo 支持通过父元素上的data-theme属性进行主题定制,但现有示例全部省略该属性。这意味着如果你的 Agent 应用需要品牌色主题,可以在未来通过data-theme扩展,而无需改动组件代码。
四、标准 UI 模式清单
每个示例都包含以下 UI 元素,且是内联在每个示例中(而非从公共包导出):
| 模式 | 来源 | 用途 |
|---|---|---|
PoweredByCloudflare | @cloudflare/kumo | "Powered by Cloudflare" 页脚徽标——每个示例都应包含 |
CloudflareLogo | @cloudflare/kumo | Cloudflare Logo 组件,含 glyph / full 两种变体 |
ModeToggle | 每个示例内联 | 基于localStorage+data-mode属性的明暗切换 |
ConnectionIndicator | 每个示例内联 | 彩色圆点 + 标签,展示 WebSocket 状态(connecting/connected/disconnected) |
ConnectionIndicator 的实际实现
examples/playground/src/components/ConnectionStatus.tsx 给出了完整的连接状态指示器实现。它用一个statusConfig配置表把三种状态映射到语义类与颜色:
const statusConfig = { connected: { label: "Connected", dot: "bg-green-500", text: "text-kumo-success", bg: "bg-green-500/10" }, connecting: { label: "Connecting…", dot: "bg-kumo-warning animate-pulse", text: "text-kumo-warning", bg: "bg-kumo-warning-tint" }, disconnected: { label: "Disconnected", dot: "bg-kumo-danger", text: "text-kumo-danger", bg: "bg-kumo-danger-tint" } } as const;组件渲染为一个带彩色圆点的圆角徽章,连接成功后还会追加显示agentName/instanceName,让用户直观看到自己连接到了哪个 Agent 实例。
五、路由集成:KumoLinkProvider与 React Router 的类型适配
Kumo 的<LinkProvider>允许注入自定义链接组件,让<Link>通过你的路由库渲染。但在接入 React Router 时存在一个类型不匹配问题:
- Kumo 的
LinkComponentProps定义to?: string(可选); - React Router 的
Link要求to: To(必选,且To = string | Partial<Path>)。
这两个类型在任一方向上都不互相可赋值,直接把RouterLink传给LinkProvider会报类型错误。
解决方案:AppLink 适配器
仓库在 examples/playground/src/client.tsx 中用一个极薄的适配组件桥接两者:
import { LinkProvider, type LinkComponentProps } from "@cloudflare/kumo"; const AppLink = forwardRef<HTMLAnchorElement, LinkComponentProps>( ({ to, ...props }, ref) => { if (to) { return <RouterLink ref={ref} to={to} {...props} />; } // oxlint-disable-next-line jsx-a11y/anchor-has-content -- content comes from spread props return <a ref={ref} {...props} />; } ); function App() { return ( <LinkProvider component={AppLink}> <BrowserRouter> <Routes>...</Routes> </BrowserRouter> </LinkProvider> ); }这个适配器做了两件事:
- 兜底可选性缺口:
to存在时渲染为RouterLink,to缺失时退化为普通<a>,与 Kumo 实际只会传入字符串to的行为对齐; - 收窄类型:将
To收窄为string,这正是 Kumo 在实际调用中始终传入的类型。
此外,仓库还保留了oxlint-disable注释来说明裸<a>无内容时的无障碍检查豁免——内容通过展开的 props 提供。
上游修复建议
文档指出这个问题值得向 Kumo 团队提出,因为每个使用 React Router 的开发者都会遇到。可能的修复方向有三个:
- Kumo 将
LinkComponentProps的to改为必填(组件实际被调用时总是提供该值); - Kumo 直接接受 React Router 的
To类型; - React Router 放宽
Link,接受to?: string。
六、Kumo 组件选用清单
以下是 Playground 中实际使用的 Kumo 组件与其替换的自研组件:
| Kumo 组件 | 替换对象 / 用途 |
|---|---|
Button | 所有按钮(primary / secondary / destructive / ghost 操作) |
Input | 文本输入框;用内置labelprop 作为 Field 包装 |
InputArea | 多行文本框 |
Surface | 卡片 / 面板容器 |
Text | 标题与正文(注意:不接受className,需要 margin/spacing 时外层包<div>) |
Badge | 状态指示与标签 |
Banner | 告警 / 警告横幅 |
CodeBlock | 静态代码示例与动态 JSON 展示 |
Tabs | 标签页切换(如 inbox/outbox) |
Switch | 布尔开关(内置 label) |
Checkbox | 多选复选框 |
Table | 数据表格 |
Empty | 空状态占位 |
Loader | 加载 spinner |
LinkProvider/Link | 路由感知链接 |
以 examples/playground/src/demos/core/RoutingDemo.tsx 为例,可以同时看到多个组件的组合用法:Surface承载控制面板,Text variant="heading3"渲染小节标题(外层用<div className="mb-4">控制间距,正是文档提到的Text不接受className的应对方式),Input label="User ID ..."提供带标签输入框,Radio.Group/Radio.Item实现策略选择。
七、自定义实现及其原因(Kumo 能力边界)
Kumo 并非覆盖一切场景,Playground 在以下几处保留了自研实现,每个决策都有明确理由:
1. 侧边栏分类折叠(Sidebar category toggle)
examples/playground/src/layout/Sidebar.tsx 中的CategorySection使用原生<button>实现分类的展开/收起。原因是 Kumo 的Collapsible只接受label: string,而导航分类需要"图标 + 文本"的组合内容。该按钮还通过aria-expanded、aria-controls维护无障碍语义,配合CaretDownIcon/CaretRightIcon指示折叠状态。
2. 路由策略选择器(RoutingDemo)
RoutingDemo需要一个"每项既有标题又有描述"的类单选 UI,而 Kumo 的Radio.Item只支持label: string,无法容纳 per-option 的描述。从 examples/playground/src/demos/core/RoutingDemo.tsx 可以看到它的折中方案:在Radio.Item的 label 中用—拼接标题与描述(如"Per-User — Each user ID gets their own agent instance"),并在侧栏用"标题 + 副文案"结构展示四种策略的完整说明。
3. 交互式列表项
房间列表(ChatRoomsDemo)、表格列表(SqlDemo)、邮件列表(ReceiveDemo、SecureDemo)以及审批预设按钮(ApprovalDemo)都使用被样式化为列表行的原生<button>。这些是带复杂 active/hover 状态的"选择驱动型列表行",不属于标准按钮模式,而 Kumo 没有可复用的可选列表组件。
4. 范围滑块(BasicDemo)
Workflow 步骤数滑块使用原生<input type="range">,因为 Kumo 未提供 range/slider 组件。
5. 日志面板(LogPanel)
事件日志使用styles.css中定义的少量自定义 CSS 工具类(.log-entry、.log-entry-in、.log-entry-out、.log-entry-error),它们是整个代码库中仅有的自定义 CSS 类。原因在于日志条目是高密度、领域特定的模式,Kumo 没有对应组件。
examples/playground/src/styles.css 中的实际定义展示了如何用@layer components与@apply组合语义令牌:
@layer components { .log-entry { @apply px-3 py-1.5 text-xs font-mono text-kumo-default border-b border-kumo-fill last:border-0; } .log-entry-in { @apply bg-green-500/10; } .log-entry-out { @apply bg-kumo-info-tint; } .log-entry-error { @apply bg-kumo-danger-tint text-kumo-danger; } .log-entry-info { @apply bg-kumo-base; } }对应消费方 examples/playground/src/components/LogPanel.tsx 根据日志方向(in/out/error/info)选择不同的类组合,并以←、→、✕、•分别标记方向,同时自动滚动到底部。
6. 语义色令牌缺口(当前已知限制)
有几个期望存在的 Kumo 语义令牌目前尚不存在,仓库中的回退方案是:
bg-kumo-success-tint/bg-kumo-success→ 回退为bg-green-500/10/bg-green-500border-l-kumo-success→ 回退为border-l-green-500
文档明确警示:这些原生 Tailwind 绿色不会随data-theme变化(它们绕过了令牌系统),应在 Kumo 上游补齐对应令牌后替换。这个"回退但不完美"的处理思路也值得借鉴:先用可用的类保证功能与视觉,同时在代码中留下待替换标记。
八、整体架构与布局实践
从 examples/playground/src/layout/Layout.tsx 可以看到完整的页面骨架:
- 移动端顶栏(
md:hidden)包含打开侧栏的 ghost 按钮、PoweredByCloudflare徽标; - 桌面端为静态侧栏,移动端为覆盖式抽屉(带遮罩层);
- 主内容区
flex-1 overflow-y-auto bg-kumo-base承载路由出口Outlet。
侧栏导航数据定义在 examples/playground/src/layout/Sidebar.tsx 顶部的navigation数组,按 Core / AI / Durable Execution / MCP / Workflows / Multi-Agent / Voice / Email / Product Integrations 分类组织,每个分类带图标;条目用 React Router 的NavLink渲染,根据isActive切换高亮样式。移动端侧栏在路由变化时自动关闭(useEffect监听location.pathname)。
九、实践要点总结
- 统一设计系统:Kumo 提供语义令牌、无障碍组件与自动明暗模式,
examples/全部示例统一使用,可参考 examples/playground/src/styles.css 作为基线; - 工具链组合:
@tailwindcss/vite+@cloudflare/kumo/styles/tailwind+@source提取声明,三者缺一不可; - 主题不写
dark::通过data-mode+localStorage+ 首屏内联脚本实现无闪烁的主题切换; - 路由类型桥接:
AppLink适配器是 Kumo × React Router 组合的标准解法,完整代码在 examples/playground/src/client.tsx; - 承认边界:当设计系统缺少所需组件(滑块、可折叠带图标、可选列表等)时,保留原生元素 + 语义类的自研方案,并注意避免非语义色值破坏主题一致性。
【免费下载链接】agentsBuild and deploy AI Agents on Cloudflare项目地址: https://gitcode.com/GitHub_Trending/agents1/agents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考