ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ghost Shade 设计系统 inputSurface Recipe 详解:表单控件视觉规则的单点定义与三种应用模式

Ghost Shade 设计系统 inputSurface Recipe 详解:表单控件视觉规则的单点定义与三种应用模式 Ghost Shade 设计系统 inputSurface Recipe 详解表单控件视觉规则的单点定义与三种应用模式【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/GhostGhost 的 Shade 设计系统apps/shade中所有表单类控件共享同一套视觉规则边框、背景、圆角、过渡、聚焦光环与无效状态。这套规则被收敛到一个名为inputSurface的样式配方recipe中并通过一份 Agent Skill 文档规范其使用方式。读完本文你将掌握inputSurface(self)与inputSurface(within)两种模式的适用场景、手动组合原子atoms的边界情况写法、Tailwind JIT 对类名字面量的约束以及配方与消费组件之间的职责边界从而在 Shade 中正确地复用而非重写表单控件外观。背景配方化Recipe与 Agent Skill 机制在 Shade 的设计系统文档中inputSurface被定义为规范示例canonical example的配方Input、Textarea、InputGroup和Select触发器共用的边框、背景、圆角与聚焦光环见 recipes-guide.mdx。本文主体对应的 Skill 文档是 SKILL.md。它不仅面向人类开发者还是一份面向 AI 编码助手的技能定义其 frontmatter 中声明了自动触发条件name: Shade inputSurface recipe description: Use the inputSurface() recipe for form-control chrome (border, background, radius, focus ring, invalid state) — dont roll your own. autoTrigger: - fileEdit: apps/shade/src/components/ui/{input,textarea,input-group,select,combobox,multi-select-combobox,dropzone,calendar,command}.tsx也就是说当编辑apps/shade/src/components/ui/下任何表单控件形态的文件input、textarea、select、combobox、dropzone 等时该技能会被自动加载提醒开发者不要自己拼写聚焦光环与无效状态样式组合 recipe。这是 Ghost 仓库将设计规范工程化到开发工具链中的典型做法。inputSurface 源码七个原子与两种模式组合配方的唯一事实来源source of truth是 input-surface.ts。文件顶部 JSDoc 即规范说明核心结构如下export const inputSurfaceClasses { base: rounded-md border border-control-border bg-control-surface transition-colors, focusSelf: focus-visible:outline-hidden focus-visible:border-focus-ring focus-visible:ring-2 focus-visible:ring-focus-ring/25, focusWithin: has-[:focus-visible]:outline-hidden has-[:focus-visible]:border-focus-ring has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-focus-ring/25, invalidSelf: aria-[invalidtrue]:border-destructive aria-[invalidtrue]:ring-destructive/20 dark:aria-[invalidtrue]:ring-destructive/40, invalidWithin: has-[[aria-invalidtrue]]:border-destructive has-[[aria-invalidtrue]]:ring-destructive/20 dark:has-[[aria-invalidtrue]]:ring-destructive/40, disabledSelf: disabled:cursor-not-allowed disabled:opacity-50, disabledFieldSelf: disabled:bg-control-disabled-surface disabled:text-muted-foreground disabled:opacity-100 disabled:hover:bg-control-disabled-surface, } as const; export function inputSurface(mode: self | within self) { if (mode self) { return cn( inputSurfaceClasses.base, inputSurfaceClasses.focusSelf, inputSurfaceClasses.invalidSelf, inputSurfaceClasses.disabledSelf, ); } return cn( inputSurfaceClasses.base, inputSurfaceClasses.focusWithin, inputSurfaceClasses.invalidWithin, ); }从源码可以确认几个关键事实原子共 7 个。Skill 文档列出 6 个常用原子源码中还有第 7 个disabledFieldSelf——它不改变透明度而是把禁用控件换成disabled背景并保留文本可读性专门给文本输入类控件叠加使用后文 Input 组件即如此。模式即原子的组合。inputSurface(self)组合了base focusSelf invalidSelf disabledSelfwithin组合了base focusWithin invalidWithin刻意不带 disabled 原子——因为包裹层wrapper本身没有 HTMLdisabled属性禁用态由消费者自行处理。所有颜色都走语义化 tokenborder-control-border、bg-control-surface、focus-ring、destructive等而非具体色值无效态还针对深色模式单独调整了 ring 透明度dark:...ring-destructive/40。两种标准模式inputSurface(self)直接作用于可聚焦元素适用于input、textarea或任何自身接收焦点的元素。配方覆盖基础外观chromefocus-visible:光环 aria-[invalidtrue]无效样式 disabled:透明度。实际消费方 Input 组件 的写法const Input React.forwardRefHTMLInputElement, React.ComponentPropsinput( ({ className, type, ...props }, ref) { return ( input ref{ref} className{cn( inputSurface(self), inputSurfaceClasses.disabledFieldSelf, flex h-(--control-height) w-full px-3 py-1 text-control file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground, className, )} type{type} {...props} / ); }, );注意两个细节高度使用 CSS 变量h-(--control-height)以便统一调整控件高度在inputSurface(self)之外再叠加disabledFieldSelf使禁用的输入框背景变化但文字依然可读opacity-100覆盖了disabledSelf的opacity-50。Textarea 组件 采用完全相同的组合模式只是自身布局类不同min-h-[80px] px-3 py-2 text-base。inputSurface(within)作用于包含可聚焦子元素的包裹层当被着色的元素本身不可聚焦、真正接收焦点的是其子元素时例如InputGroup使用within模式。此时聚焦与无效样式通过:has()选择器从任意可聚焦后代派生div className{cn( inputSurface(within), flex h-9 items-center gap-2 px-3, className, )} Icon / input classNamebg-transparent outline-hidden focus:outline-hidden / /div对应源码中的选择器差异维度self原子within原子聚焦触发focus-visible:has-[:focus-visible]:任意可聚焦后代无效状态aria-[invalidtrue]:自身has-[[aria-invalidtrue]]:任意后代禁用样式disabledSelf生效无包裹层无 disabled 属性包裹层内部的真实输入元素应保持透明化bg-transparent outline-hidden让外层统一呈现边框与光环。边界情况手动组合原子 字面量类名当within的任意可聚焦后代过于宽泛时——例如包裹层内有多个可聚焦元素但只有某一个应该驱动表面光环——应放弃inputSurface()函数改为手动组合原子import {inputSurfaceClasses} from /components/ui/input-surface; div className{cn( inputSurfaceClasses.base, inputSurfaceClasses.invalidWithin, // 字面量类名字符串 —— Tailwind 需要在构建期看到它 has-[[data-slotcontrol]:focus-visible]:border-focus-ring, has-[[data-slotcontrol]:focus-visible]:ring-2, has-[[data-slotcontrol]:focus-visible]:ring-focus-ring/25 )} /Skill 文档强调了一个容易被忽略的工程约束聚焦选择器必须写成字面量类名字符串因为 Tailwind 的 JIT 编译器通过静态扫描源码提取类名模板字符串拼接出来的动态类名如has-...:${ringColor}无法被检测会静默丢失样式。仓库中 InputGroup 组件 正是这个模式的生产级实例。它的源码注释明确解释了为什么不用inputSurface(within)className{cn( inputSurfaceClasses.base, inputSurfaceClasses.invalidWithin, group/input-group relative flex w-full items-center outline-hidden ..., // 聚焦状态 —— 精确限定到 input-group 的 control // 这样点击组内的 InputGroupButton 不会触发表面聚焦光环。 // 这就是这里不使用 inputSurface(within) 的原因。 has-[[data-slotinput-group-control]:focus-visible]:border-focus-ring has-[[data-slotinput-group-control]:focus-visible]:ring-2 has-[[data-slotinput-group-control]:focus-visible]:ring-focus-ring/25 has-[[data-slotinput-group-control]:focus-visible]:outline-hidden, className, )}这里通过data-slotinput-group-control数据属性把聚焦光环的范围锁定到组内真正输入的那个input而组内的按钮、附加件聚焦时不产生表面光环——这是通用:has()方案无法精确表达的语义。职责边界配方管什么组件管什么Skill 文档给出的配方拥有 / 消费者补充对照表与源码逐条吻合配方拥有recipe owns消费者补充you add边框border-control-border高度、内边距背景bg-control-surface字体排印text-control、text-sm圆角rounded-md布局flex、items-center过渡transition-colors占位符样式聚焦光环focus-visible:ring-focus-ring/25图标 / 插槽定位无效状态aria-[invalidtrue]:border-destructive组件特有微调禁用态disabled:opacity-50——仅 self 模式—Storybook 中的配方文档路径为 Storybook → Recipes / Input Surface把同样的边界以表格形式可视化并提供了 Self、Within、States、WithinStates、CustomFocusScope 等可交互故事用于验证默认、无效、禁用各状态下两种模式的实际表现。由于配方本身没有组件实体它在 Storybook 中归在 Recipes 分组而非 Components 分组。三类典型反模式Skill 文档明确列出了三种应避免的写法// 反模式 1 —— 在表单控件上自己拼一套聚焦样式 input className rounded-md border border-input bg-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-[invalidtrue]:border-red-500 disabled:opacity-50 / // 反模式 2 —— 对 self 聚焦的元素误用 inputSurface(within)范围过宽 input className{cn(inputSurface(within), h-9)} / // 反模式 3 —— Tailwind JIT 无法识别的非字面量类名拼接 const dyn has-[[data-slotcontrol]:focus-visible]:${ringColor};反模式 1 的问题在于使用了通用 tokenring、red-500、border-input而非控件专用 tokenfocus-ring、destructive、control-border会导致控件外观偏离设计系统并产生重复样式来源反模式 2 会让输入框上的任何后代聚焦都误触包裹层光环反模式 3 如前所述会导致样式在构建期被丢弃。小结与延伸阅读inputSurface用不到 60 行 TypeScript 把 Ghost Shade 中所有表单控件的外观一致性收敛到单点新增控件时只需在 input-surface.ts 修改一处即可全局生效常规场景调用inputSurface(self | within)聚焦范围需要精确控制时用inputSurfaceClasses原子加字面量has-[[data-slot...]:focus-visible]:选择器手工组合。相关入口配方实现与规范 JSDocinput-surface.ts消费方实例input.tsx、textarea.tsx、select.tsx、input-group.tsxStorybook 文档故事input-surface.stories.tsx设计系统配方指南recipes-guide.mdx本文对应 Skill 文档SKILL.md【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进