用 coss Empty 原语构建 Kaneo 的空状态与恢复式 UI 实战指南
【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app
空状态(Empty State)是列表类界面中最容易被忽略、却又直接影响用户留存体验的环节:当项目列表、标签列表或搜索结果为空时,用户需要的不是一行干巴巴的"暂无数据",而是明确的方向指引和可执行的动作。本文基于 Kaneo(app116)开源项目仓库中skills/coss/references/primitives/empty.md的 coss Empty 原语规范,结合apps/web/src/components/ui/empty.tsx的真实实现与页面级使用案例,讲解如何在 React + Tailwind CSS v4 项目中搭建语义完整、带恢复引导的空状态组件。读完本文,你将掌握 coss Empty 的完整 API 结构、组合模式、真实项目落地写法,以及容易踩中的三类陷阱。
Empty 原语是什么:何时使用、何时不用
coss 是一套基于 Base UI、提供 shadcn 式开发体验的组件库,仓库中的 coss skill(skills/coss/SKILL.md)为其 53 个原语各维护了一份参考指南,empty.md是其中之一。它专门回答一个核心问题:列表没有数据时,界面该怎么呈现。
按empty.md的"When to use"定义,Empty 原语适用于两类场景:
- 无数据 / 无结果状态(No-data/no-results):列表、搜索结果、过滤结果为空的页面,需要给出引导性说明;
- 面向行动恢复的 UI(Action-oriented recovery):空内容列表需要引导用户立刻执行某个恢复动作,例如"创建第一个项目"。
反过来说,Empty 原语不是万能的容器——加载中和报错状态应使用专门的加载/错误原语(如 skeleton、alert),而不是复用空状态组件。这一点在"Common pitfalls"一节中被明确列为反面典型,下文会展开。
安装:CLI 一键添加与手动依赖
empty.md给出了两种安装方式。
方式一:shadcn CLI 安装(推荐)
npx shadcn@latest add @coss/empty这是 coss 组件注册表的标准安装入口(与skills/coss/references/cli.md中的整体安装流程一致),CLI 会把组件文件写入项目的components/ui目录。
方式二:手动安装
# No extra runtime dependency required for this primitive.原语文档明确说明:Empty 不需要任何额外的运行时依赖。这与许多需要 Radix 或 Base UI 包支撑的原语不同——Empty 本质上是纯展示型结构组件,只依赖 React 与 Tailwind。手动安装时只需把组件文件复制到本地、并将导入路径改为当前应用的别名配置即可,这也是skills/coss/SKILL.md中"Quick manual pattern"的通用流程。
在 Kaneo 仓库中,Empty 的落点正是apps/web/src/components/ui/empty.tsx,导入别名是@/components/ui/empty。
组件 API 结构:六个子组件与源码级剖析
coss Empty 采用"容器 + 语义子组件"的组合式 API,empty.md给出的规范导入如下:
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from "@/components/ui/empty"对照apps/web/src/components/ui/empty.tsx的真实实现,这六个导出都有明确的职责划分:
| 子组件 | DOM 元素 | 职责 | 关键样式要点(源码确认) |
|---|---|---|---|
Empty | div | 整体容器 | flex flex-col items-center justify-center gap-6 text-center p-6 md:p-12,带data-slot="empty" |
EmptyHeader | div | 头部信息区 | max-w-sm限制宽度,垂直居中 |
EmptyMedia | div | 视觉媒体区(图标) | 基于cva的variant变体:default(透明)与icon(带边框小方块) |
EmptyTitle | div | 标题 | font-heading font-semibold text-xl |
EmptyDescription | p | 描述文字 | text-muted-foreground text-sm,内嵌链接自动下划线 |
EmptyContent | div | 动作区 | max-w-sm,子元素垂直排列gap-4 |
值得注意的实现细节:
1.data-slot语义选择器。每个子组件都携带data-slot属性(empty、empty-header、empty-media、empty-title、empty-description、empty-content),这与 coss skill 的skills/coss/references/rules/styling.md中强调的><Empty> <EmptyHeader> <EmptyMedia variant="icon"> <Icon /> </EmptyMedia> <EmptyTitle>No data</EmptyTitle> <EmptyDescription>No data found</EmptyDescription> </EmptyHeader> <EmptyContent> <Button>Add data</Button> </EmptyContent> </Empty>
注意结构层次:EmptyMedia / EmptyTitle / EmptyDescription必须统一放在EmptyHeader内,动作按钮放在EmptyContent内,这是组合式 API 的约定俗成,破坏这个层次会导致布局与语义错乱。
实战模式:Kaneo 中的真实空状态用例
empty.md提供了带图标与动作按钮的推荐模式,而 Kaneo 仓库中正好有三个页面级实现可供对照,其中两个值得逐行解读。
用例一:工作区无项目(完整恢复式空状态)
apps/web/src/routes/_layout/_authenticated/dashboard/workspace/$workspaceId/index.tsx是empty.md中"Always include an actionable next step"原则的教科书级落地:
<Empty className="min-h-[60vh]"> <EmptyHeader> <EmptyMedia variant="icon"> <LayoutGrid /> </EmptyMedia> <EmptyTitle>{t("workspace:projects.emptyTitle")}</EmptyTitle> <EmptyDescription> {canCreate ? t("workspace:projects.emptyDescription") : t("workspace:projects.emptyDescriptionReadOnly")} </EmptyDescription> </EmptyHeader> <EmptyContent> {canCreate && ( <Button onClick={handleCreateProject}> <Plus /> {t("workspace:projects.createProject")} </Button> )} </EmptyContent> </Empty>几个可以照抄到任何项目的细节:
className="min-h-[60vh]":通过 className 透传撑高整个空状态区,避免空页面显得头重脚轻,这是Empty容器接受React.ComponentProps<"div">的灵活之处;- 权限感知的文案与动作:
canCreate为 true 时显示"创建项目"按钮与创建引导文案;无权限时只显示只读文案并隐藏按钮——空状态的"下一步"必须与用户实际权限匹配,否则是无效引导; - 图标语义:
LayoutGrid暗示"项目"的业务语义,Plus表示"新增",均带aria-hidden由描述文字承担可访问性信息; - 动作即跳转:
handleCreateProject会打开CreateProjectModal(紧随其后的CreateProjectModal),完成"空状态 → 引导 → 创建 → 列表刷新"的完整闭环。
用例二:无自定义角色(引导性空状态)
apps/web/src/routes/_layout/_authenticated/dashboard/settings/workspace/roles.tsx展示了无动作按钮的变体——当"创建"动作以其他形式存在(页面头部有新建入口)时,空状态只需提供图标 + 标题 + 描述:
<Empty> <EmptyHeader> <EmptyMedia variant="icon"> <Shield /> </EmptyMedia> <EmptyTitle> {t("settings:workspaceRoles.emptyTitle")} </EmptyTitle> <EmptyDescription> {t("settings:workspaceRoles.emptyDescription")} </EmptyDescription> </EmptyHeader> </Empty>该文件还展示了正确的条件渲染顺序(roles.tsx):isLoading → 加载提示 → error → 错误提示 → 空列表 → Empty 组件 → 否则渲染真实列表,即 Empty 只接管"数据已加载且确认为空"的分支,绝不与加载态、错误态混用——这正是empty.md"Common pitfalls" 第 2 条的直接印证。同样的模式还出现在labels.tsx(工作区标签为空时)。
常见陷阱:三类高频失误与规避方案
empty.md的"Common pitfalls"一节列出了三条高频错误,结合 Kaneo 源码可以给出更具体的规避手段:
陷阱 1:空状态没有可执行的下一步。只展示"暂无数据"四个字而不给按钮/链接,用户会陷入死胡同。规避:只要业务上允许,就在EmptyContent中放入明确的恢复动作(按钮、链接、快捷键提示);即便动作放在页面其他位置,也应如 roles.tsx 那样在描述中说明"如何开始"。
陷阱 2:用空状态组件冒充加载态 / 错误态。加载中应使用骨架屏(skeleton),错误应使用 alert/destructive 提示,两者与"空"是语义不同的状态。规避:如 roles.tsx 所示,把isLoading、error、empty三个分支拆开渲染,Empty 只负责data.length === 0的场景。
陷阱 3:纯文案空状态,缺少上下文相关的恢复指引。描述文字要"因场景而异":工作区空项目与搜索无结果、筛选无匹配的引导文案与动作完全不同。规避:描述文案走 i18n(Kaneo 中统一使用t("workspace:projects.emptyDescription")这类翻译键),且按canCreate等上下文动态切换,确保每条空状态都回答"为什么空 + 下一步干什么"。
延伸阅读
- 原语规范:
skills/coss/references/primitives/empty.md(本文的规范来源,含p-empty-1核心粒子模式索引) - 组件实现:
apps/web/src/components/ui/empty.tsx - 页面级用例:工作区无项目、无自定义角色、无标签
- coss 技能总览与组件注册表:
skills/coss/SKILL.md、skills/coss/references/component-registry.md - 样式与组合规则:
skills/coss/references/rules/styling.md、skills/coss/references/rules/composition.md
【免费下载链接】app🎯 All you need. Nothing you don't. Open source project management that works for you, not against you.项目地址: https://gitcode.com/GitHub_Trending/app116/app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考