Ghost Shade 设计系统:新增组件的验收清单与实现规范(命名、cva 变体、Story 约定与 Token 纪律)

发布时间:2026/9/6 15:57:24
Ghost Shade 设计系统:新增组件的验收清单与实现规范(命名、cva 变体、Story 约定与 Token 纪律) Ghost Shade 设计系统新增组件的验收清单与实现规范命名、cva 变体、Story 约定与 Token 纪律【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/GhostGhost 的 Shade 设计系统apps/shade为仓库中的各个微前端应用apps/admin、apps/activitypub等提供统一的 UI 组件层。仓库内置了一份名为shade-new-component的 Agent 技能文档 SKILL.md它规定了在apps/shade/src/components下新增或编辑组件/模式文件时必须通过的验收清单命名规则、Storybook 标题前缀、基于class-variance-authority的组件标准形态、四种必需交互状态以及语义 Token 纪律。读完本文你将掌握在 Shade 中交付一个合格组件的完整流程并能对照仓库源码理解每条规则背后的实际实现。技能文档的定位与触发条件这份清单文档带有标准的技能 frontmatter明确了它的适用范围与自动触发条件name: Shade new component description: Acceptance checklist for adding or editing a Shade component or pattern file — naming, sibling story, className forwarding, cva variants, required states, recipe usage. autoTrigger: - fileEdit: apps/shade/src/components/**/*.{ts,tsx}即只要发生对apps/shade/src/components下任意.ts/.tsx文件的编辑就该清单自动生效。它的定位是验收而非教学——在把组件标记为完成之前下列各项必须全部通过。Shade 的 Agent 指引文档 AGENTS.md 也要求先使用shade-component-decision技能判断代码应落在哪一层再按shade-new-component走验收清单两者配合使用。文件与命名约定文档规定的命名规则与仓库源码一一对应对象约定仓库实例文件名kebab-casedropdown-menu.tsx与 ShadCN CLI 输出保持一致不要因大小写而改名apps/shade/src/components/ui/dropdown-menu.tsx、button.tsx导出标识符PascalCaseDropdownMenuexport { Button, buttonVariants }button.tsx钩子、函数、变量camelCasecn、debounceds-utils.ts同目录 Story必需的name.stories.tsx兄弟文件button.stories.tsx与button.tsx同目录包内导入统一走/别名/lib/utils、/components/ui/input-surfaceimport { cn } from /lib/utils值得注意的是/lib/utils本身是一个再导出层utils.ts 只有一行export * from ./ds-utils真正的工具函数实现在 ds-utils.ts。Shade 的人类文档 contributing.mdx 中的命名表格与本清单完全一致说明这是同一套约定的两个表达面清单面向机器触发文档面向人类阅读。Storybook 标题前缀与 Story 文件要求Shade 按层组织 Storybook 的顶层目录组件的title必须匹配其所处的层层标题前缀Primitive布局原语Primitives / NameComponent通用控件Components / NameRecipe纯类名配方Recipes / NamePattern产品组合Patterns / NameToken galleryToken 展示Tokens / Topic这五个前缀正好对应apps/shade/src/components/下的目录划分primitives/、ui/、patterns/、page-templates/以及根部的components.ts、primitives.ts、patterns.ts、page-templates.ts各层入口桶文件见 package.json 的exports字段各层均有独立子路径导出。每个 story 文件还必须具备以下要素tags: [autodocs]—— 启用自动文档页parameters.docs.description.component—— 一行的组件级摘要每个 story 的parameters.docs.description.story—— 一句话说明该变体在何时使用每个重要变体/状态各写一个 story宁要多个小而聚焦的 story不要一个长文案的 story。button.stories.tsx 是这些要求的标准范本const meta { title: Components / Button, component: Button, tags: [autodocs], parameters: { docs: { description: { component: Reusable button for interactive actions across the UI. ..., }, }, }, } satisfies Metatypeof Button;其中title: Components / Button使用了 Component 层前缀随后Destructive、Outline、Secondary、Ghost、LinkVariant等每个变体各自一个 story并逐个附带description.story如 destructive 的说明是用于危险或不可逆操作删除、移除、重置。组件标准形态cva 变体 forwardRef className 透传文档给出了组件的标准骨架这是一个可复制即可运行的模板import * as React from react; import {cva, type VariantProps} from class-variance-authority; import {cn} from /lib/utils; const thingVariants cva(base-classes-here, { variants: { variant: {default: ..., destructive: ...}, size: {default: ..., sm: ...} }, defaultVariants: {variant: default, size: default} }); export interface ThingProps extends React.HTMLAttributesHTMLDivElement, VariantPropstypeof thingVariants {} const Thing React.forwardRefHTMLDivElement, ThingProps( ({className, variant, size, ...props}, ref) ( div ref{ref} className{cn(thingVariants({variant, size, className}))} {...props} / ) ); Thing.displayName Thing; export {Thing, thingVariants};对照真实实现 button.tsx 可以看到该骨架的完整落地buttonVariants用cva()声明了variantdefault/destructive/outline/secondary/ghost/link/dropdown与sizedefault/sm/lg/icon两组变体并设置defaultVariantsButton通过React.forwardRef定义L43className经cn(buttonVariants({...}))合并后落到 DOML60末尾Button.displayName Button并导出组件与变体函数两个符号L66-L68。文档在此骨架上提出三条硬性要求className必须透传并用cn()合并——绝不覆盖、绝不丢弃适用于每个渲染 DOM 的组件。cn()的实际实现是twMerge(clsx(inputs))ds-utils.ts即先做布尔/数组类归一化再用 Tailwind 的冲突合并规则解决同类冲突例如消费方传入的h-8能正确覆盖组件内部的h-9这正是透传不覆盖的底层保障。只允许视觉/交互类 propsvariant、size、loading禁止工作流类 props如isMembersPage、layoutMode——一旦出现这类 props说明你需要的是一个 Pattern 包装层而不是通用 Component。多区域组件用复合子组件.Title、.Actions、.Body暴露结构而不是堆一袋 props。forwardRef、cva 与 Recipe 的适用边界文档对三个易误用的机制给出了明确的用 / 不用判据forwardRef组件渲染单个 DOM 元素且消费方可能需要 ref 时使用大多数 UI 控件。以下场景跳过纯 provider/context 包装器、本身不渲染 DOM 的 Radix root 再导出、ref 语义已由子组件处理的组件。cva()组件有变体或状态化类名分支variant、size、tone时使用单一样式且一行cn(...)更清晰的简单组件可不用。Recipename.ts无 JSX不写forwardRef、不写cva()、不引入 React只返回类名字符串。仓库中的 input-surface.ts 是 Recipe 的范例它是纯.ts文件导出inputSurfaceClasses原子类名base、focusSelf、focusWithin、invalidSelf 等和inputSurface(mode)函数注释明确说明它拥有边框、背景、圆角、过渡、焦点环与失效态消费方只负责尺寸、内边距与排版。四种必需状态default / hover / focus-visible / disabled文档要求每个交互组件在以下四种状态下都必须正常工作且每种状态都要在 story 中可见defaulthoverfocus-visible使用focus-visible:前缀绝不用focus:disabledactive、loading、error、empty 属于可选状态仅在适用时提供。button.tsx的基础类串即体现了这条纪律focus-visible:ring-1 focus-visible:ring-focus-ring focus-visible:outline-hidden负责焦点环hover:bg-primary/90负责悬停disabled:pointer-events-none disabled:opacity-50负责禁用态。对表单控件文档要求通过inputSurfacerecipe驱动外观而不是各组件自己拼 border/focus ring。这正是 input-surface.ts 的设计self模式直接作用于可聚焦元素input、textareawithin模式作用于包含可聚焦子元素的包装器通过:has()从任意聚焦后代派生焦点/失效样式确有特殊场景时再手动组合inputSurfaceClasses原子。相关的深度说明在姊妹技能 shade-input-surface-recipe 中。Token 纪律无 hex、无裸灰阶、无dark:颜色变体文档的 Token 章节只有两条规则但都是硬性约束禁止 hex 值禁止bg-gray-200这类裸调色板工具类用于 UI 外观禁止dark:颜色变体——深色模式由语义 Token 统一处理。这与 Shade 的 CSS 架构直接对应根部的 tokens.css 是仅 Token的入口它只做两件事——import ./tailwind.theme.cssTailwind 主题映射与原始 Token和import ./theme-variables.css语义 Token 及其深色模式取值。也就是说深色模式不是靠组件里写dark:bg-xxx实现的而是在theme-variables.css中为同一组语义变量提供深色值。button.tsx中使用的bg-primary、text-primary-foreground、border-control-border、ring-focus-ring全部是这类语义 Token。更详细的规则见 shade-tokens-not-hex 与 shade-no-dark-variants 两份技能文档。完成前验收清单逐项继承原文档以下清单必须逐项通过才允许把组件/模式标记为完成位于正确的层由 shade-component-decision 判定kebab-case 文件名、PascalCase 导出、同目录name.stories.tsxclassName已透传并用cn()合并始终如此若组件渲染的 DOM 可能被消费方持有 ref则使用forwardRef若组件有变体则使用cva()并设置defaultVariants四种必需状态均正常工作且在 story 中可见仅用语义 Token——无 hex、无裸灰色、无dark:颜色变体通用 Component 上不携带产品特异的 propsStory 具备tags: [autodocs]、组件级描述与每个 story 的一行描述pnpm lint、pnpm test与 Storybook 全部干净最后一条对应 package.json 中的真实脚本定义pnpm test 类型检查 带覆盖率的 Vitestpnpm test:types vitest run --coveragepnpm lint ESLint 检查src/与test/pnpm storybook 在 6006 端口启动 Storybook 查看src/docs/下的文档。在独立应用中消费 Shade 时注意Shade 当前是私有包嵌入 Ghost Admin 的应用apps/admin、apps/activitypub不应重复导入tryghost/shade/styles.css或再包一层ShadeApp应从层级子路径导入组件如import {Button} from tryghost/shade/componentsCSS 与应用包装由 Admin 集中提供见 apps/shade/README.md。唯一事实来源Source of Truth清单文档末尾声明规则的权威出处是Storybook 的 Overview / Contributing 页contributing.mdx与各组件自身的 stories。换言之这份 SKILL.md 是机器可触发的验收快照当约定演进时应以 Storybook 人类文档为准同步更新——这也是 AGENTS.md 中当共享约定变化时更新 Storybook 人类文档不要只让本文件成为唯一来源这条工作流要求。配合阅读同组技能文档shade-component-decision、shade-imports、shade-shadcn-install、shade-use-primitives可以覆盖选层 → 安装/引入 → 实现 → 验收的完整链路。【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考