Plate UI 的 Shadcn Proofing:让 registry 组件保持可识别的开源代码形态
Plate UI 的 Shadcn Proofing让 registry 组件保持可识别的开源代码形态【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/platePlate 是一个基于 Slate 的富文本编辑器其官网apps/www通过 shadcn 风格的组件注册表对外发布编辑器的节点渲染器与工具栏 UI。shadcn-proofing.md是 Plate 团队为自己定下的组件质检规则当你在 Plate 中编写或重构一个从 shadcn 衍生出来的组件时如何确保它仍然是用户一眼可认、可拷贝、可 diff、可二次拥有的开源代码而不是被层层抽象包裹、无法与上游对照的框架胶水。读完本文你将掌握 Plate 组件体系中的三条 proofing 规则、判断代码该留在组件文件还是抽到 package 的完整标准以及这些规则在 registry 真实代码footnote kit、media/TOC/equation 节点中的落地方式。一、Shadcn Proofing 是什么文档定位与仓库上下文shadcn-proofing.md位于 Plate 的.agents/skills/plate-ui/rules/目录下是plate-ui技能见 SKILL.md中的Shadcn Proofing规则条目。该技能自述为仓库专属的 shadcn 配套技能通用的 shadcn CLI、上游文档和通用规则走shadcn技能而 Plate 特有的组件编写规范——开源代码保持open-code preservation、包抽取边界package extraction boundaries、base/live kit 拆分、跨平台分层与 registry 接线——则由plate-ui技能管辖。原文档只有简短的三节但每一节都是一条硬约束全文结构如下Preserve recognizable open code保持可识别的开源代码Prefer readable files over abstraction churn可读的文件优先于抽象搅动Review like an upstream diff像审查上游 diff 一样审查代码这三条规则共同服务于plate-ui技能声明的第一原则Preserve open code.A shadcn-derived component should still look like source code a user can own, read, diff, and tweak.保留开源代码shadcn 衍生的组件看起来仍应该是用户可以拥有、阅读、diff 和微调的源码。理解这条规则的前提是理解 Plate 的四个代码面Repo Surfaces原文档虽未逐一展开但plate-ui技能给出了完整清单代码面路径角色组件与节点渲染器apps/www/src/registry/ui活的live组件源码即本文讨论的主体base/live kit 接线apps/www/src/registry/components/editor/plugins插件与组件的绑定装配registry 元数据与依赖apps/www/src/registry/registry-*.ts注册表条目、registryDependencies持久化包packages/*transforms、queries、controllers 与公开 hooksPlate 的上游基线同样在仓库内可见registry-shadcn.json 是上游 shadcn/uinew-york-v4 风格的注册表定义列出了 accordion、button、popover、sidebar 等组件的依赖与文件路径components.json 则是应用侧的接入配置。所谓像审查上游 diff 一样审查正是以这些上游文件为参照系。二、规则一Preserve recognizable open code保持可识别的开源代码原文档给出了一条总纲加四个检查点Keep the component source close to normal shadcn expectations让组件源码贴近常规 shadcn 预期clear local composition清晰的局部组合obviousasChild/data-slot/data-state显眼的 Radix 组合与状态约定variants and classes near the JSX that uses them变体与类名放在使用它们的 JSX 附近no abstraction maze for simple UI简单 UI 不做抽象迷宫逐条展开1. clear local composition清晰的局部组合。shadcn 组件的典型形态是一个文件内导出XxxRoot与若干XxxItem子组件子组件通过data-slot属性与父组件协作文件之间几乎无隐式依赖。Plate 要求编辑器侧的组件如 media-image-node.tsx、equation-node.tsx也保持这种形态——组合逻辑写在文件内部读者不需要跳转三层 helper 才能理解这个 popover 什么时候打开。2. obviousasChild/data-slot/data-state。这三个是 Radix/shadcn 体系的方言标记asChild让组件把行为委托给子元素而不额外产生 DOM 节点data-slot用于在组合内部识别部件data-state把状态暴露到 DOM 以便 CSS 选择器生效。shadcn-proofing.md把它们并列为必须显眼存在的要素因为它们是上游代码可识别性的指纹——一旦这些约定被包 hooks 吞掉用户就无法把 Plate 组件与上游 shadcn 源码对上号。3. variants and classes near the JSX that uses them变体与类名就近。使用class-variance-authoritycva定义 variants 时variant函数应当与消费它的 JSX 在同一文件、同一视线范围内。把variant拆到独立工具文件、再让 JSX 通过多层转发引用会让哪个类名决定这个按钮的样式变得不可追溯——这正是抽象迷宫。4. no abstraction maze for simple UI简单 UI 不做抽象迷宫。这条是对前三条的反向兜底如果一个工具栏按钮只需要读一个状态、发一个命令就不值得为它发明一套子组件体系。从仓库结构看这条规则的落点非常具体apps/www/src/registry/ui下的节点渲染器文件本身就是被分发的开源代码用户通过 registry 安装后会直接持有这些文件的所有权。因此它们的可读性不是风格偏好而是产品契约。三、规则二Prefer readable files over abstraction churn可读的文件优先于抽象搅动原文档的完整表述是A component file is allowed to be a little long if the alternative is hiding everything behind package hooks and helper wrappers.如果替代方案是把一切都藏到 package hooks 和 helper 包装器后面那么组件文件被允许稍微长一点。Long but readable open code beats clean indirection that nobody can diff against upstream.长但可读的开源代码胜过没人能拿去和上游 diff 的干净间接层。这条规则直接回答了一个常见的工程纠结文件太长了抽个 hook 吧。Plate 的答案是长度本身不是罪把语义藏进不可 diff 的间接层才是罪。plate-ui体系为这个判断提供了量化工具。与 proofing 规则同目录的 ownership.md 给出了包抽取气味测试smell test当 hook 的返回值大部分是以下类型时不要抽取——labels文案只被一个组件使用的 booleansclass decisions类名决策一个组件的 menu items一个组件的 event handlers如果 hook 的名字实际上等价于这个渲染器的私有状态就把它留在组件文件里。ownership.md同时明确列举了两个坏的抽取理由the file feels long文件感觉太长了和the types are annoying类型写起来烦人。这两条理由恰好是抽象搅动abstraction churn最典型的动机来源——它们优化的是维护者的舒适区而不是用户的可 diff 性。SKILL.md 则把它升级为一组可勾选的抽取测试Extraction Test——满足以下任一条才抽到 package代码拥有文档语义、序列化、transforms 或导航契约多个 UI 面或多个平台需要同一个行为契约代码是稳定的 controller/hook其输出不绑定某一个 shadcn 组件的标记结构否则同一逻辑会在多个包或适配器间重复未来的 native 消费者有可能复用同一份概念契约。而只要命中以下任一条就留在本地代码只服务于一个组件返回形状主要是 labels、JSX 接线、类名决策或 popover/menu 状态抽取的主要动机是文件感觉太长或类型写起来烦抽取会向用户隐藏开源代码结构抽象只在 React/web 下有意义没有合理的 native 对应物。注意第 4 条——它与 proofing 文档完全同源即使通过了语义测试只要抽取损害了可识别性也要撤回。可 diff 性在 Plate 的决策顺序里优先级高于模块化洁癖。四、规则三Review like an upstream diff像审查上游 diff 一样审查原文档的最后一节给出三个自问和一个裁决在抽取之前问自己would a user still recognize this as open source component code?用户还会把它认作开源组件代码吗can they copy, tweak, and own it easily?他们能轻松地拷贝、微调并拥有它吗did we move semantics, or just move clutter?我们移动的是语义还是仅仅挪动了杂物If the answer is we mostly moved clutter, put it back.如果答案是我们主要是在挪杂物就把它放回去。这三个问题构成一个完整的验收流程第一个问题检验可识别性规则一的落点第二个问题检验可拥有性代码是否仍能以文件为单位被用户带走第三个问题做归因——把抽出去的东西按语义semantics与杂物clutter分类。只有当抽出去的是语义时重构才创造了价值当抽出去的主要是杂物时重构实际上是把开源代码的表拆散了裁决是明确且不容商量的put it back放回去。结合 ownership.md 中的正反例语义 vs 杂物的边界可以具体化// 错误package hook 只被一个组件使用且返回值大部分是 UI 胶水 const state useSingleComponentOnlyState(); // 正确package 拥有稳定语义app 拥有局部组合 const { activeContentId, headingList } useTocElementState(); return headingList.map((item) ( Button key{item.id}{item.title}/Button ));plate-ui技能中的 Key Patterns 给出了同一思想的四个好/坏对照其中两个坏例值得特别记诵// Bad: package hook 只为了喂一个 shadcn 组件的局部 UI const state useSingleComponentOnlyState(); return Popover open{state.open}.../Popover; // Bad: 返回渲染器胶水的 React-only package hook const { dialogTitle, menuItems, onOpenChange, popoverOpen, } useToolbarMenuState();后一个坏例揭示了一个更微妙的陷阱dialogTitle、menuItems、onOpenChange、popoverOpen这样的返回形状乍看像状态管理实质是一个组件的 UI 状态集合——它既不可 diff 回上游 shadcn也无法被 native 层复用没有合理的 native 对应物因此同时命中抽取测试的本地化条款 2、4、5。五、规则在 Plate registry 真实代码中的落地证明一套 proofing 规则是否可信最好的方式是看它管辖的真实产物。以下两个案例均来自 component-audit.md 列出的本仓库最强模式。5.1 base/live kit 拆分footnote 组件plate-ui技能要求干净地拆分 static/base 与 live kits。footnote 的两个 kit 文件是这一要求的标准示范。base kitfootnote-base-kit.tsx绑定的是静态渲染器import { BaseFootnoteDefinitionPlugin, BaseFootnoteReferencePlugin, } from platejs/footnote; import { FootnoteDefinitionElementStatic, FootnoteReferenceElementStatic, } from /registry/ui/footnote-node-static; export const BaseFootnoteKit [ BaseFootnoteReferencePlugin.withComponent(FootnoteReferenceElementStatic), BaseFootnoteDefinitionPlugin.withComponent(FootnoteDefinitionElementStatic), ];live kitfootnote-kit.tsx绑定的是可交互渲染器use client; import { FootnoteDefinitionPlugin, FootnoteInputPlugin, FootnoteReferencePlugin, } from platejs/footnote/react; import { FootnoteDefinitionElement, FootnoteInputElement, FootnoteReferenceElement, } from /registry/ui/footnote-node; export const FootnoteKit [ FootnoteInputPlugin.withComponent(FootnoteInputElement), FootnoteReferencePlugin.withComponent(FootnoteReferenceElement), FootnoteDefinitionPlugin.withComponent(FootnoteDefinitionElement), ];用 proofing 规则检验两个 kit 文件短、平、直没有任何 helper 包装——Plugin.withComponent(Element)的配对关系一目了然可识别性 ✓拆分本身只是把插件 ↔ 组件的映射从 registry 装配文件中显式列出没有移动任何语义✓ move semantics, not clutter用户拷贝这两个文件后可以独立理解并替换任意一侧✓ copy, tweak, own。component-audit.md还列出了同模式的 math kitmath-base-kit.tsx、math-kit.tsx说明这是被刻意复制而非偶发形成的惯例。5.2 语义下沉 package、组合留在 appmedia / TOC / equationcomponent-audit.md给出的三组好抽取案例恰好是 proofing 规则抽语义不抽杂物的实证面app 组件保持 shadcn 形态package hook拥有语义为什么成立Mediamedia-image-node.tsxuseMediaState.tspackage hook 拥有真实的 media/editor 状态app 仍负责组合工具栏、caption、resize handles 与 shadcn 风格 UITOCtoc-node.tsxuseTocElement.tspackage hook 拥有稳定的导航契约app 仍渲染行与本地按钮样式Equationequation-node.tsxuseEquationElement.tspackage hook 只做一件持久的事KaTeX 渲染 effectapp 拥有 popover 组合与本地 UI三者共同的形状是const { ...state } useXxxState()之后JSX 里继续出现标准的 shadcn 组件组合。这与SKILL.md中Good: package owns stable semantics, UI composes locally的范式一致// Good: package 拥有稳定语义UI 在本地组合 const { align, focused, readOnly, selected } useMediaState(); return ( MediaToolbar plugin{ImagePlugin} PlateElement {...props}.../PlateElement /MediaToolbar );component-audit.md末尾还给出了一条清醒的提醒仓库中确实存在可能把过多东西抽进了 package hook的组件这些应被当作警告而非先例Treat that as a warning, not a precedent。SKILL.md更进一步为未来大版本重设计立下Major-Release Law以返回渲染器专用 UI props/状态为主的 package React hooks 应被弃用并移回 app 本地package 层只保留跨平台语义/view-model 契约——已存在的违反此法的 hook不应因为它已经在那了而被新代码模仿。六、配套规则registry 接线与样式依赖开源代码保持还延伸到 registry 元数据层一个组件如果装出来的依赖不完整用户拷贝走的开源代码就是跑不起来的残片。registry.md 为此定下三条Kits 与 UI 条目保持对齐新增组件时在正确的 registry 文件中加 UI 条目、按需加 base/live kit 条目并确保 kit 的registryDependencies指向真实的 node/ui 条目——Do not leave the registry half-wired.不要把 registry 接一半。元数据落在 registry-kits.ts、registry-examples.ts 等文件中三者registry-kits.ts、registry-ui.ts、registry-examples.ts需要一起更新。示例需要显式依赖example 应依赖它使用的 kit、它直接导入的额外组件、以及它需要的样式 registry 条目。样式依赖是真实依赖如果组件使用了共享 CSS 变量或纯样式 registry 条目必须显式声明。registry.md给出的错误/正确对照是// 错误example 实际还依赖共享样式 token registryDependencies: [editor-kit] // 正确 registryDependencies: [editor-kit, highlight-style]component-audit.md的收尾提醒与之一致别忘了当组件或 example 使用共享 highlight token 时加上highlight-style这类样式依赖。 这条规则看似琐碎但它保障的正是用户copy, tweak, and own体验的最后一公里——装出来的代码可以独立运行。七、实操清单编写或重构 Plate 组件时如何执行 proofing把三条规则与配套标准合并可以得到一份可执行的自检清单建议在提交组件改动前逐项过一遍可识别性检查对应规则一文件内组合是否清晰读者无需跳转 helper 即可理解 UI 行为asChild/data-slot/data-state是否按 shadcn 惯例显眼存在cvavariants 与类名是否就近于消费它们的 JSX长度 vs 间接层检查对应规则二如果为了缩短文件而抽取抽取动机是文件感觉太长还是类型烦人是则停止。过一遍抽取测试的五条必须与五条保留特别核对返回形状是否主要是 labels / 类名决策 / popover 状态。上游 diff 检查对应规则三把改动当作与上游 shadcn 的 diff 来读用户能认出这是开源组件代码吗能拷贝微调吗抽出去的是语义还是杂物主要是杂物则放回去。接线与 changelog 检查配套规则registry-kits.ts/registry-ui.ts/registry-examples.ts是否同步更新共享样式 token如highlight-style是否已声明为registryDependencies用户可见的 registry 改动是否有 registry changelog 条目或明确的N/A: reasonpackage 导出变化时按仓库流程刷新导出SKILL.md给出的步骤是运行pnpm brl。最小诚实验证来自SKILL.mdWorkflow纯 UI 改动组件 spec 即可动了 package 代码跑 package 构建/类型检查交互面浏览器实机验证。八、小结shadcn-proofing.md的核心命题只有一句话Plate 对外分发的 shadcn 风格组件其可读性与可 diff 性是产品功能的一部分而非风格偏好。规则一锁定形态asChild、data-slot、variants 就近规则二锁定代价权衡长文件优于不可 diff 的间接层规则三提供裁决程序移动语义留下移动杂物撤回。这些规则由仓库内的真实产物支撑footnote base/live kit 的显式装配、useMediaState/useTocElement/useEquationElement的语义下沉以及 registry 元数据层的显式依赖声明共同构成一套可复制、可审计的组件编写标准。对于维护 Plate 或参考其 registry 模式的项目而言这套 proofing 清单是组件级代码评审中最具操作性的一份检查单。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考