多应用,同一组件库
常见的失败模式:为应用 A 构建了一套组件库,应用 B 的按钮高 4 像素、圆角大 2 像素,于是 B “就这一次”复制了按钮。五个应用之后,出现了六个按钮、八个下拉框和三个模态框,做的却是同一件事。
让共享组件变成令牌驱动:它只承载行为、标记和状态,每个主题提供自己的度量值。然后逐个应用进行整合。
令牌驱动组件
将所有应用间有差异的度量值读取自主题令牌:
@theme {
--spacing-btn-sm: 1.75rem; --spacing-btn-px-sm: 0.625rem;
--spacing-btn-md: 2.25rem; --spacing-btn-px-md: 0.875rem;
--spacing-field: 2.125rem; /* 输入框 / 下拉框高度 */
--text-label: 0.8125rem;
}
md: "h-btn-md px-btn-px-md text-sm",
icon: "size-btn-md",
如此:
- 一个套件在其主题中一次性覆盖令牌,其中的每个应用都能获得自己尺寸的控件。
- 圆角、填充和阴影已经是令牌(
rounded-field,bg-accent,--shadow-card)。 - 用颜色加装饰性类来表现填充,例如
bg-accent accent-fill。只定义装饰(如光泽)的主题则永远不会渲染透明按钮。
参见 references/button.tsx。
逐个应用的整合步骤
-
盘点本地副本。 在每个应用中搜索
export function (Btn|Button|IconBtn|Select|Field|Input|Modal|Dialog|Sheet|Badge|Empty|Pager|Toast|Stat|Status|Spinner)。统计调用次数。 -
替换并删除 当共享组件经过主题化后匹配时。显式映射变体名称,并记录映射关系:
本地名称 共享变体 primary,navyaccentsecondaryoutlinequiet,softsubtledanger(实心)danger-solid -
将应用特有的部分保留在应用层:
- 领域组件、图表和应用框架;
- 其形状本身就是品牌的控件(iOS 圆片形、Carbon 扁平控件、48px 药丸形 CTA);
- 拥有共享组件缺乏的行为的控件(三态复选框、可移除标签、“任意”重置项、多选过滤器)。
当只增加一个功能时,使用薄封装层是可以的。通过注释说明原因。
-
弃用原生
<select>和基于菜单的下拉框,统一使用一个 Select 组件。- Radix Select 将
""保留为“无值”:请使用像"all"这样的哨兵值,并映射回来。 - 转发
aria-labelledby和aria-describedby。 - 调用
selectOption()的脚本需要先点击组合框,再点击选项。
- Radix Select 将
-
对照每个应用自己的截图进行验证(参见
visual-regression-pass)。实际差异通常是预期的:尺寸、光泽、按压缩放。
tailwind-merge 陷阱(cn())
tailwind-merge 只识别 Tailwind 内置的度量体系。自定义令牌会在三个地方静默破坏它:
- 未知的间距令牌。
cn("h-btn-md", "h-10")会保留两者,因为它不知道h-btn-md是一个高度属性,样式表顺序决定最终效果。调用方的h-10被令牌覆盖,导致 40px 的按钮渲染成 36px。注册这些令牌:extendTailwindMerge({ extend: { theme: { spacing: ["btn-sm","btn-md","field", …] } } })。 - 未知的文本尺寸。
cn("text-meta", "text-ink")会将text-meta视为颜色并丢弃它。注册尺寸:theme: { text: ["meta","micro","label"] }。 - 自定义阴影和颜色。
text-14和text-t3并存,或者任意shadow-[var(--x)]和命名阴影并存:同样的问题。当代码库到处使用封闭的自定义度量体系时,在这些地方使用普通的clsx。
参见 references/cn.ts。
焦点与验证
aria-invalid:ring-*样式会覆盖focus-visible:ring-*,从而在焦点位于无效字段时隐藏焦点环。请使用aria-invalid:not-focus-visible:ring-…。- 表单内的裸
<button>元素会提交表单。如果共享的 Button 不默认type="button",则每个转换后的非提交调用点都必须显式设置它。