首页
/ Coolify 仓库 shadcn 组件组合规范(composition.md)全解:Group 嵌套、覆盖层选择与"组件替代自定义标记"实践

Coolify 仓库 shadcn 组件组合规范(composition.md)全解:Group 嵌套、覆盖层选择与"组件替代自定义标记"实践

2026-09-04 20:17:46作者:傅爽业Veleda

本文以 Coolify 仓库中 .agents/skills/shadcn/rules/composition.md 为核心,完整拆解其中 14 条组件组合(Composition)规则:Item 必须嵌套在 Group 内、覆盖层组件选型、Dialog/Sheet/Drawer 的 Title 强制要求、Card 完整结构、Button 加载态的正确写法,以及 Separator/Skeleton/Badge 替代自定义标记的模式。读完你可以直接把这些规则作为 shadcn/ui 项目(或 AI Agent 辅助编码场景)的组件组合检查清单,并对照 SKILL.md 中的 Critical Rules 理解每条规则的来龙去脉。

这份文档在 Coolify 仓库中的定位

composition.md 位于 .agents/skills/shadcn/rules/ 目录,是 Coolify 仓库为 AI Agent 内置的一套 shadcn/ui 技能(skill)中的"组件结构"规则文件。从 SKILL.md 的 Critical Rules 部分可以看到,它被明确归类为两条规则的详细出处:

  • Component Structure → composition.md:覆盖 Group 嵌套、asChild/render 自定义触发器、Title 强制要求、Card 完整组合、Button 无 isPending、TabsTrigger 位置、AvatarFallback 等;
  • Use Components, Not Custom Markup → composition.md:覆盖 Alert、Empty、sonner Toast、Separator、Skeleton、Badge 等"用现成组件而非手写标记"的规则。

该 skill 的适用前提是"任何带 components.json 的 shadcn 项目"(SKILL.md 描述中的触发条件),配套文件还有 forms.md(表单布局)、styling.md(样式与 Tailwind)、icons.md(图标规范)和 base-vs-radix.md(base 与 radix 两套底层原语的 API 差异)。composition.md 与它们的关系是:它管"组件之间怎么嵌套",而不是"样式怎么写"或"表单怎么布局"。需要说明的是,Coolify 主站 UI 本身是 Laravel + Blade + Alpine.js 技术栈(见 package.jsonTECH_STACK.md),本规则文件属于面向 Agent 的通用 shadcn 技能包内容,因此下文的代码示例均为 shadcn/ui 生态下的 React/TSX 用法。

总览:14 条组合规则清单

文档开头给出的 Contents 完整列出了全部规则,可视为检查清单:

  1. Items always inside their Group component(Item 必须位于 Group 内)
  2. Callouts use Alert(提示框用 Alert)
  3. Empty states use Empty component(空状态用 Empty)
  4. Toast notifications use sonner(Toast 用 sonner)
  5. Choosing between overlay components(覆盖层组件选型)
  6. Dialog, Sheet, and Drawer always need a Title(覆盖层必须有 Title)
  7. Card structure(Card 完整结构)
  8. Button has no isPending or isLoading prop(Button 加载态的正确写法)
  9. TabsTrigger must be inside TabsList(TabsTrigger 必须在 TabsList 内)
  10. Avatar always needs AvatarFallback(Avatar 必须带 Fallback)
  11. Use Separator instead of raw hr or border divs(分隔线用 Separator)
  12. Use Skeleton for loading placeholders(加载占位用 Skeleton)
  13. Use Badge instead of custom styled spans(徽章用 Badge)
  14. Use existing components instead of custom markup(总原则:优先用组件而非自定义标记)

Item 必须嵌套在 Group 组件内

这是文档的第一条规则,核心要求是:永远不要把 Item 直接渲染在内容容器里。以 Select 为例,文档给出 Incorrect/Correct 对照:

// 错误:SelectItem 直接放在 SelectContent 下
<SelectContent>
  <SelectItem value="apple">Apple</SelectItem>
  <SelectItem value="banana">Banana</SelectItem>
</SelectContent>

// 正确:先包一层 SelectGroup
<SelectContent>
  <SelectGroup>
    <SelectItem value="apple">Apple</SelectItem>
    <SelectItem value="banana">Banana</SelectItem>
  </SelectGroup>
</SelectContent>

文档随后给出了一张完整的 Item → Group 映射表,说明该规则适用于所有"基于 Group 的组件":

Item Group
SelectItemSelectLabel SelectGroup
DropdownMenuItemDropdownMenuLabelDropdownMenuSub DropdownMenuGroup
MenubarItem MenubarGroup
ContextMenuItem ContextMenuGroup
CommandItem CommandGroup

这条规则同样出现在 SKILL.md 的 Critical Rules 摘要中("Items always inside their Group"),并且在 SKILL.md 的 Workflow 第 7 步中还被用作审查从社区 registry 添加组件时的检查项——"Check for missing sub-components (e.g. SelectItem without SelectGroup)"。也就是说,从第三方 registry 拉下来的组件若缺失 Group 包裹,属于需要人工/Agent 修复的组合缺陷,而不是可接受的写法。

补充一层背景:为什么 Group 层如此重要?从 base-vs-radix.md 对 Select 的 API 描述可以看出,base 原语要求把数据通过 items 属性传给根组件、radix 则用内联 JSX,两种底层的 SelectContent 渲染机制都依赖 SelectGroup 来组织列表结构(包括键盘导航分组与视觉间距)。因此"漏掉 Group"不仅是不规范的代码风格问题,还可能导致组件在某个底层原语上渲染或行为异常——这也是该规则被放进"always enforced"(始终强制)类别的原因。

提示框(Callout)使用 Alert

第二条规则约定提示类内容一律用 Alert 组合,文档给出标准结构:

<Alert>
  <AlertTitle>Warning</AlertTitle>
  <AlertDescription>Something needs attention.</AlertDescription>
</Alert>
</parameter>

Alert 根组件 + AlertTitle + AlertDescription 三段式结构。SKILL.md 的对应摘要为"Callouts use Alert. Don't build custom styled divs."——禁止用自定义样式的 div 手搓提示框,因为 Alert 已内置语义化角色、间距与图标位(结合 icons.md 可知 Alert 内的图标由组件 CSS 处理尺寸,不需要 size-4 之类的类名)。

空状态使用 Empty 组件

空状态不手写,而用 Empty 组件族完整组合。文档示例展示了四个子组件的层级:EmptyEmptyHeader →(EmptyMedia + EmptyTitle + EmptyDescription)→ EmptyContent

<Empty>
  <EmptyHeader>
    <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
    <EmptyTitle>No projects yet</EmptyTitle>
    <EmptyDescription>Get started by creating a new project.</EmptyDescription>
  </EmptyHeader>
  <EmptyContent>
    <Button>Create Project</Button>
  </EmptyContent>
</Empty>
</parameter>

要点有两处:EmptyMedia 通过 variant="icon" 声明媒体类型为图标并直接承载图标组件;EmptyContent 是放置主操作(如"Create Project"按钮)的位置。SKILL.md 的组件选择表也明确把"Empty states"映射到 Empty 这一个组件(SKILL.md),空状态没有第二种合规写法。

Toast 通知使用 sonner

文档约定 Toast 一律来自 sonner 库的 toast() 函数,而不是 shadcn 自家的组件:

import { toast } from "sonner"

toast.success("Changes saved.")
toast.error("Something went wrong.")
toast("File deleted.", {
  action: { label: "Undo", onClick: () => undoDelete() },
})

三种用法分别覆盖:成功通知(toast.success)、错误通知(toast.error)、带操作按钮的通知(第二个参数传 action 对象,label + onClick 实现"撤销删除"这类可交互 Toast)。这与 SKILL.md 组件选择表"Feedback → sonner (toast)"(SKILL.md)一致:shadcn 生态中 Toast 的职责由 sonner 承担,Alert/Badge/Progress/Skeleton/Spinner 承担其余反馈形态。

覆盖层组件选型:Dialog、Sheet、Drawer 与 HoverCard

选择哪个覆盖层组件,文档给出一张按"使用场景 → 组件"的对照表:

使用场景 组件
需要输入的聚焦任务 Dialog
破坏性操作确认 AlertDialog
侧边面板(详情或筛选) Sheet
移动端优先的底部面板 Drawer
悬停显示快速信息 HoverCard
点击显示小范围上下文内容 Popover

这张表与 SKILL.md 组件选择表中"Overlays"一行(Dialog (modal)、Sheet (side panel)、Drawer (bottom sheet)、AlertDialog (confirmation))互为印证。选型判断可归纳为三个维度:是否需要用户输入(Dialog)、是否不可逆(AlertDialog)、内容在屏幕上的空间形态(居中 / 侧边 / 底部 / 浮动跟随)。

Dialog、Sheet、Drawer 必须提供 Title

这是文档中明确的可访问性(accessibility)强制项DialogTitleSheetTitleDrawerTitle 均为必需;若视觉上不需要展示标题,用 className="sr-only" 隐藏(屏幕阅读器仍可读取):

<DialogContent>
  <DialogHeader>
    <DialogTitle>Edit Profile</DialogTitle>
    <DialogDescription>Update your profile.</DialogDescription>
  </DialogHeader>
  ...
</DialogContent>

结构上遵循 DialogContentDialogHeader →(DialogTitle + DialogDescription)的层级。SKILL.md 将这条与"Use className=\"sr-only\" if visually hidden"一并列入 Critical Rules(SKILL.md),可见"缺 Title"会被视为违规而非风格问题。

Card 结构:使用完整组合,而非全部塞进 CardContent

文档要求 Card 使用"full composition",并明确反对把所有内容堆进 CardContent

<Card>
  <CardHeader>
    <CardTitle>Team Members</CardTitle>
    <CardDescription>Manage your team.</CardDescription>
  </CardHeader>
  <CardContent>...</CardContent>
  <CardFooter>
    <Button>Invite</Button>
  </CardFooter>
</Card>

CardHeader(承载 CardTitleCardDescription)、CardContent(正文)、CardFooter(放操作按钮等收尾内容)各司其职。这与 SKILL.md 的表述一致:"Use full Card composition. CardHeader/CardTitle/CardDescription/CardContent/CardFooter. Don't dump everything in CardContent."。

Button 没有 isPending / isLoading 属性

shadcn 的 Button 组件不内置加载态属性(没有 isPendingisLoading prop),正确做法是组合 Spinner + data-icon + disabled

<Button disabled>
  <Spinner data-icon="inline-start" />
  Saving...
</Button>

这里同时体现了 icons.md 的规范:data-icon="inline-start" 声明图标在按钮内为前缀位置,且不给图标加尺寸类(组件 CSS 负责图标尺寸)。这套"Spinner 前置 + 文案 + disabled"的组合模式意味着加载态是展示层组合出来的,加载逻辑本身(请求是否结束)仍由业务代码控制。

TabsTrigger 必须在 TabsList 内

TabsTrigger 永远不能直接渲染在 Tabs 下,必须包在 TabsList 中:

<Tabs defaultValue="account">
  <TabsList>
    <TabsTrigger value="account">Account</TabsTrigger>
    <TabsTrigger value="password">Password</TabsTrigger>
  </TabsList>
  <TabsContent value="account">...</TabsContent>
</Tabs>

Tabs 根组件下分两支:TabsList(承载全部 TabsTrigger)与一个或多个 TabsContent(通过 value 与对应的 TabsTrigger 关联)。defaultValue 声明初始选中的 tab。这条规则在 SKILL.md 摘要中被表述为"TabsTrigger must be inside TabsList. Never render triggers directly in Tabs.",其重要性等级与 Group 嵌套规则相同,属于结构层面的硬性约束。

Avatar 必须包含 AvatarFallback

图片头像加载失败时必须有兜底,因此 AvatarFallback 是必备子组件:

<Avatar>
  <AvatarImage src="/avatar.png" alt="User" />
  <AvatarFallback>JD</AvatarFallback>
</Avatar>

AvatarFallback 通常展示用户姓氏首字母(如示例中的 "JD")。SKILL.md 对应规则为"Avatar always needs AvatarFallback. For when the image fails to load."——即使你"确定图片不会失败",该规则也不允许省略,这是以确定性兜底换取列表页、侧边栏等大量头像场景的健壮性。

用组件替代自定义标记:Separator、Skeleton、Badge

文档最后(也是第一条规则"总则")的替换对照表给出了三组高频场景的"不要 → 要"映射:

Instead of(不要) Use(要用)
<hr><div className="border-t"> <Separator />
<div className="animate-pulse"> 加样式 div 拼的骨架 <Skeleton className="h-4 w-3/4" />
<span className="rounded-full bg-green-100 ..."> <Badge variant="secondary">

三条规则的共同逻辑是:shadcn 组件已经封装了语义、深色模式适配与交互行为,手写标记既丢失语义又会在主题切换时失配。其中 Skeleton 通过 className 控制尺寸形状(h-4 w-3/4 表示一行文本的占位),这符合 SKILL.md 中"className for layout, not styling"的原则——className 只用于布局维度(宽高、比例),不承担换色换字的样式职责;Badge 则通过 variant="secondary" 这类内置变体表达状态色,而不是写死 bg-green-100 之类的原始色值。SKILL.md 的关键模式示例也给出了一组对照(SKILL.md):<Badge variant="secondary">+20.1%</Badge> 正确,<span className="text-emerald-600">+20.1%</span> 错误。

与 base / radix 双底层的衔接

读完 composition.md 后还有一条必要的延伸:上述组件的嵌套结构与底层原语无关,但属性 API 会因 components.jsonbase 字段(radixbase)而不同。这一点由姊妹文件 base-vs-radix.md 专门覆盖,与 composition 规则直接交叉的几处包括:

  • 触发器自定义元素:radix 用 asChild(如 <DialogTrigger asChild>),base 用 render(如 <DialogTrigger render={<Button />}>),且 base 将 render 目标改为非 button 元素(<a><span>)时需追加 nativeButton={false}
  • Select:base 要求根组件传 items 属性、用 { value: null } 项表达占位符,而 radix 直接用 <SelectValue placeholder="...">——但无论哪种底层,SelectContent 内的 SelectGroup 包裹结构都保持不变;
  • ToggleGroup / Accordion:base 用 multiple 布尔 + 数组型 defaultValue,radix 用 type="single" | "multiple" + 字符串 defaultValue
  • Slider:base 单滑块接受标量 defaultValue={50},radix 永远是数组 defaultValue={[50]}

因此实际工作流是:先用 composition.md 确定骨架(谁包谁、缺什么子组件),再查 npx shadcn@latest info 输出的 base 字段决定属性写法。SKILL.md 的 Workflow(SKILL.md)把这一流程固化为:获取项目上下文 → 检查已安装组件 → search 找组件 → docs <component> 拉文档 → add 安装 → 审阅新增文件是否违反 Critical Rules(其中就包括本文件的 Group 嵌套检查)。

小结:把 composition.md 当作组件组合检查清单

回到文档本身,它的价值在于把 shadcn/ui 中"组件之间如何嵌套"这一最容易出错的层面,压缩成 14 条可逐条勾选的硬规则:

  • 结构完整性:Item 在 Group 内、TabsTrigger 在 TabsList 内、覆盖层带 Title、Avatar 带 Fallback、Card 用完整五件套;
  • 职责单一化:Callout 归 Alert、空状态归 Empty、Toast 归 sonner、加载占位归 Skeleton、分隔线归 Separator、状态标签归 Badge;
  • 组合优先于 API:Button 加载态不是找 isPending 属性,而是 Spinner + data-icon + disabled 的组合。

配合 forms.md(表单用 FieldGroup/Field/InputGroup)、styling.md(语义色、gap-* 间距、cn() 条件类)、icons.mddata-icon 与图标对象传递)和 base-vs-radix.md(双层 API 差异),这套 rules 目录构成了完整的 shadcn 代码审查标准,也是 SKILL.md 中"always enforced"规则的落地细则。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384