news 2026/9/7 2:06:21

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ghost Shade 设计系统 inputSurface Recipe 详解:表单控件视觉规则的单点定义与三种应用模式

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

【免费下载链接】GhostIndependent technology for modern publishing, memberships, subscriptions and newsletters.项目地址: https://gitcode.com/GitHub_Trending/gh/Ghost

Ghost 的 Shade 设计系统(apps/shade)中,所有表单类控件共享同一套视觉规则:边框、背景、圆角、过渡、聚焦光环与无效状态。这套规则被收敛到一个名为inputSurface的样式配方(recipe)中,并通过一份 Agent Skill 文档规范其使用方式。读完本文,你将掌握inputSurface('self')inputSurface('within')两种模式的适用场景、手动组合原子(atoms)的边界情况写法、Tailwind JIT 对类名字面量的约束,以及配方与消费组件之间的职责边界,从而在 Shade 中正确地"复用而非重写"表单控件外观。

背景:配方化(Recipe)与 Agent Skill 机制

在 Shade 的设计系统文档中,inputSurface被定义为"规范示例"(canonical example)的配方:InputTextareaInputGroupSelect触发器共用的边框、背景、圆角与聚焦光环,见 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) — don't 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-[invalid=true]:border-destructive aria-[invalid=true]:ring-destructive/20 dark:aria-[invalid=true]:ring-destructive/40', invalidWithin: 'has-[[aria-invalid=true]]:border-destructive has-[[aria-invalid=true]]:ring-destructive/20 dark:has-[[aria-invalid=true]]: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, ); }

从源码可以确认几个关键事实:

  1. 原子共 7 个。Skill 文档列出 6 个常用原子,源码中还有第 7 个disabledFieldSelf——它不改变透明度,而是把禁用控件换成disabled背景并保留文本可读性,专门给文本输入类控件叠加使用(后文 Input 组件即如此)。
  2. 模式即原子的组合inputSurface('self')组合了base + focusSelf + invalidSelf + disabledSelf'within'组合了base + focusWithin + invalidWithin,刻意不带 disabled 原子——因为包裹层(wrapper)本身没有 HTMLdisabled属性,禁用态由消费者自行处理。
  3. 所有颜色都走语义化 tokenborder-control-borderbg-control-surfacefocus-ringdestructive等,而非具体色值;无效态还针对深色模式单独调整了 ring 透明度(dark:...ring-destructive/40)。

两种标准模式

inputSurface('self'):直接作用于可聚焦元素

适用于<input><textarea>或任何自身接收焦点的元素。配方覆盖:基础外观(chrome)+focus-visible:光环 +aria-[invalid=true]无效样式 +disabled:透明度。

实际消费方 Input 组件 的写法:

const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( ({ 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覆盖了disabledSelfopacity-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 className="bg-transparent outline-hidden focus:outline-hidden" /> </div>

对应源码中的选择器差异:

维度'self'原子'within'原子
聚焦触发focus-visible:has-[:focus-visible]:(任意可聚焦后代)
无效状态aria-[invalid=true]:(自身)has-[[aria-invalid=true]]:(任意后代)
禁用样式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-slot=control]:focus-visible]:border-focus-ring', 'has-[[data-slot=control]:focus-visible]:ring-2', 'has-[[data-slot=control]: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-slot=input-group-control]:focus-visible]:border-focus-ring has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring/25 has-[[data-slot=input-group-control]:focus-visible]:outline-hidden', className, )}

这里通过data-slot="input-group-control"数据属性把聚焦光环的范围锁定到组内真正输入的那个<input>,而组内的按钮、附加件聚焦时不产生表面光环——这是通用:has()方案无法精确表达的语义。

职责边界:配方管什么,组件管什么

Skill 文档给出的"配方拥有 / 消费者补充"对照表,与源码逐条吻合:

配方拥有(recipe owns)消费者补充(you add)
边框(border-control-border高度、内边距
背景(bg-control-surface字体排印(text-controltext-sm
圆角(rounded-md布局(flexitems-center
过渡(transition-colors占位符样式
聚焦光环(focus-visible:ring-focus-ring/25图标 / 插槽定位
无效状态(aria-[invalid=true]: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-[invalid=true]:border-red-500 disabled:opacity-50 " /> // 反模式 2 —— 对 self 聚焦的元素误用 inputSurface('within')(范围过宽) <input className={cn(inputSurface('within'), 'h-9')} /> // 反模式 3 —— Tailwind JIT 无法识别的非字面量类名拼接 const dyn = `has-[[data-slot=control]:focus-visible]:${ringColor}`;

反模式 1 的问题在于使用了通用 token(ringred-500border-input)而非控件专用 token(focus-ringdestructivecontrol-border),会导致控件外观偏离设计系统并产生重复样式来源;反模式 2 会让输入框上的任何后代聚焦都误触包裹层光环;反模式 3 如前所述会导致样式在构建期被丢弃。

小结与延伸阅读

inputSurface用不到 60 行 TypeScript 把 Ghost Shade 中所有表单控件的外观一致性收敛到单点:新增控件时只需在 input-surface.ts 修改一处即可全局生效;常规场景调用inputSurface('self' | 'within'),聚焦范围需要精确控制时用inputSurfaceClasses原子加字面量has-[[data-slot=...]:focus-visible]:选择器手工组合。相关入口:

  • 配方实现与规范 JSDoc:input-surface.ts
  • 消费方实例:input.tsx、textarea.tsx、select.tsx、input-group.tsx
  • Storybook 文档故事: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 2:06:10

Qt 5.9.9静态编译与xcb插件部署实战解析

简介&#xff1a;这是一份在 Linux 下完成静态编译的 Qt 5.9.9 开发库&#xff0c;编译环境为 CentOS 7.6 x64、GCC 4.8.5、libc 2.17&#xff0c;并启用了 -qt-xcb 图形平台插件。它主要面向需要把 Qt 图形界面程序部署到不带 Qt 运行库的 Linux 目标机、希望以单个可执行文件…

作者头像 李华
网站建设 2026/9/7 2:01:14

数据结构课程设计:基于图论与最短路径算法的景点导游咨询系统

简介&#xff1a;这是一份数据结构课程设计报告&#xff0c;报告题目为“全国著名景点导游咨询”&#xff0c;主要面向高校计算机相关专业、正在完成数据结构课程设计的学生。资源用图结构对全国著名景点及其路径进行建模&#xff0c;实现了查询景点信息、查找任意两个景点之间…

作者头像 李华
网站建设 2026/9/7 1:54:40

Ubuntu下WPS字体缺失乱码?这套安装映射方案一次搞定

简介&#xff1a;这是一套专为Ubuntu系统下WPS办公软件准备的字体补齐方案&#xff0c;目标用户是经常处理含特殊符号文档的Linux使用者&#xff0c;用于解决打开文档时提示缺少Symbol、Wingdings、Wingdings 2、Wingdings 3等字体&#xff0c;导致符号以问号或空白显示的问题。…

作者头像 李华
网站建设 2026/9/7 1:53:19

ComfyUI漫剧工作流实战:从节点机制到批量分镜生成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华