安装与接入
从 npm 安装
XiHan.UI 的 18 个公开包都已发布到 npm,同属一个版本组、始终同号。当前版本号以 npm 为准:npm view @xihan-ui/vue version。
安装的包取决于使用的适配器:
# Vue 3 项目:适配器 + 默认皮肤
pnpm add @xihan-ui/vue @xihan-ui/styles
# 原生 / 非 Vue 项目:自定义元素 + 默认皮肤
pnpm add @xihan-ui/web-components @xihan-ui/styles
# 只需要设计令牌,自行编写皮肤
pnpm add @xihan-ui/tokens
# 背景层与代码着色是可选 peer,按需安装
pnpm add @xihan-ui/backgrounds
pnpm add @xihan-ui/code-highlight@xihan-ui/styles 不是必需的:组件不依赖默认皮肤,只使用令牌自行编写样式是可行的路径,见下文“样式的三种接法”。
适配器的引擎侧依赖(core / motion / pointer / viz / headless / position)写在 dependencies 中,安装适配器时一并安装,不需要单独列出。@xihan-ui/vue 的 peer 依赖是 vue@^3.5.0,由项目提供。
backgrounds / sound / code-highlight 是可选 peer,按需安装。未安装 code-highlight 时,代码视图渲染纯文本,不报错。
除了从 npm 安装,还有两条本地路径:
- 克隆仓库直接开发:构建库包后运行文档站,136 个组件的示例都是真实组件;
- 本地构建后链接进项目:适合跟随仓库最新改动。
WARNING
@xihan-ui/icons 只收录自研的一等图标集,当前 184 枚,覆盖组件与常见界面所需,不追求完整。 需要整套图标时请自行准备,或用 XhIcon 接入任意图标源:它接受 IconRecord 纯数据,任何来源都可以转换。
环境要求
| 项 | 要求 |
|---|---|
| Node(安装使用) | ≥ 18,与包的 engines 声明一致 |
| Node / pnpm(参与本仓库开发) | ≥ 24.0.0 / ≥ 11.0.0 |
| 模块格式 | ESM only,不提供 CJS |
| 浏览器 | 支持 oklch()、@layer、:where() 的现代浏览器 |
路径一:克隆仓库开发
git clone https://github.com/XiHanFun/XiHan.UI.git
cd XiHan.UI/ui
pnpm install --frozen-lockfile
pnpm build在浏览器中查看组件请运行文档站:它通过 link: 指向本仓库的库包,示例渲染的是真实组件,Vue 与自定义元素两套写法并排,因此需要先 pnpm build。
cd ../docs
pnpm install
pnpm dev回到 ui/ 的常用命令:
pnpm build # 全部库包出 dist
pnpm typecheck # 类型检查
pnpm lint # oxlint + eslint + stylelint
pnpm test # 单元测试与跨适配器一致性测试(jsdom)
pnpm test:browser # 真实 Chromium 里的无障碍扫描与浮层定位契约
pnpm boundaries # 分层依赖门禁
pnpm gate # 128 项结构门禁
pnpm size # 产物体积棘轮首次运行 pnpm test:browser 前需要安装浏览器:
pnpm exec playwright install chromium路径二:链接进现有项目
先在 XiHan.UI 仓库中构建:
cd XiHan.UI/ui && pnpm build再在项目中用 link: 协议指向它(pnpm 写法,路径按实际填写):
{
"dependencies": {
"@xihan-ui/vue": "link:../XiHan.UI/ui/packages/adapters/vue",
"@xihan-ui/styles": "link:../XiHan.UI/ui/packages/design/styles",
"@xihan-ui/tokens": "link:../XiHan.UI/ui/packages/design/tokens"
}
}@xihan-ui/vue 会通过 dependencies 一并引入 core / motion / pointer / viz / headless / position,这些包不需要单独链接。vue 本身是它的 peer 依赖,由项目提供。
接入 Vue 项目
// main.ts
import { createVisualEnvironmentController } from "@xihan-ui/tokens/runtime";
import { createApp } from "vue";
import App from "./App.vue";
// 皮肤入口自带层序声明与令牌,只引这一行;单独引 tokens.css 是只要令牌不要皮肤的路径
import "@xihan-ui/styles";
// 把八轴视觉环境写到 <html> 上,并显式处理持久化失败
createVisualEnvironmentController({
root: document.documentElement,
storageKey: "app-visual-environment",
onStorageError: detail => console.error("视觉偏好持久化失败", detail),
initial: { mode: "system", motion: "system", transparency: "system" },
});
createApp(App).mount("#app");组件按需从主入口取,不需要注册插件:
<script setup lang="ts">
import { XhDialogContent, XhDialogRoot, XhDialogTitle, XhDialogTrigger } from "@xihan-ui/vue";
</script>包声明了 sideEffects: false,打包器会移除未使用的组件。库包不提供每个组件独立的子路径导出,按需引入依靠 tree-shaking。
中文项目的配置
组件的内建文案(关闭按钮的读屏名称、分页的翻页说明等)默认为英文;日期时间类组件另接受 locale,未提供时跟随浏览器语言,读取失败时使用 en-US。 要将两者固定为中文,在根组件中 provideXhConfig({ locale: 'zh-CN', translations: { … } }) 全局配置一次即可,不需要逐实例传 :translations。文档站的示例是孤立片段,因此逐个传入;真实应用不应如此。见国际化。
接入原生 / 非 Vue 项目
import { createVisualEnvironmentController } from "@xihan-ui/tokens/runtime";
import { defineXhElements } from "@xihan-ui/web-components/define";
import "@xihan-ui/styles";
// 注册全部 xh-* 元素。主入口 import 本身不注册,必须显式调用这一行
defineXhElements();
createVisualEnvironmentController({
root: document.documentElement,
storageKey: "app-visual-environment",
onStorageError: detail => console.error("视觉偏好持久化失败", detail),
initial: { mode: "system", motion: "system", transparency: "system" },
});之后在 HTML 中直接写标签,结构由作者编写,用 data-xh-part 标出角色节点:
<xh-button variant="solid">
<button data-xh-part="root">提交</button>
</xh-button>样式的三种接法
@xihan-ui/styles 是纯 CSS 包,与 JS 层无关,三种粒度任选:
全量:令牌 + 层序 + reset + 全部组件皮肤。主入口 index.css 是一份生成的扁平文件:Action Control / Field Chrome / Collection Item / Swatch 四份家族配方与共享关键帧在 focus / label / description / pointer 四份公共层之后只内联一次、排在一切组件皮肤之前,各皮肤随后按源序内联、去掉自带的家族 @import。除令牌那一条外没有 @import,任何打包器都不会把家族复制几十份。
import "@xihan-ui/styles";按组件引入。layers.css 与 tokens.css 各自带完整层序声明,先引入任一即可;组件皮肤不能排在它们之前。每份单皮肤文件头自带它所属家族的 @import '../family/*.css',单独引入时家族在场。
import "@xihan-ui/styles/layers.css";
import "@xihan-ui/tokens/tokens.css";
import "@xihan-ui/styles/button.css";
import "@xihan-ui/styles/dialog.css";只引入令牌,自行编写皮肤。
import "@xihan-ui/tokens/tokens.css";第三种同样保持层序:tokens.css 自带完整的层序声明,自行编写的皮肤直接写进 @layer xihan.overrides 即可。组件不依赖默认皮肤,只向 DOM 写入 data-scope / data-part / data-state 等属性,样式完全由使用者决定。参见皮肤与样式分层。
第二种需要注意两点
- 漏引默认是静默的。少引一份皮肤时,该组件的
data-scope/data-part照常存在、其他皮肤也已加载,只有它渲染为没有内边距、没有底色的裸元素。开发模式下开启下文的探测器可以发现。 - 顺序按
index.css里各皮肤段的相对顺序。同一个@layer xihan.components内,等特异性的规则由源序决定。自行排序(按字母、按目录读取序)当前可能看不出差别,将来增加跨组件规则后会与全量引入的渲染不同。需要按需引入时,按index.css里/* styles/xxx.css */段标记的顺序过滤,不自行排序。 - 混用多份单皮肤时,每份都各带一份家族
@import。打包器对同一 URL 的@import去重(Vite 默认的 postcss 路径会去重)时没有代价;不去重的内联器(例如@tailwindcss/vite自带的那条)会把家族复制多份,产物随之翻倍,而且后出现的副本会排在前面的皮肤之后。家族的关键覆盖点已按特指度而非源序设计,但多份副本仍是纯浪费——引入的皮肤超过几份时改用主入口。
全量是 148 份皮肤加令牌,压缩后约 130 kB gzip。没有明确的体积压力时使用第一种。
开发模式下查漏引
每份组件皮肤在自己的 [data-scope='X'] 上写入一个 --xh-X-skin 标记。startSkinCheck() 扫描页面上出现过的每个 scope,取不到标记即说明该 CSS 未加载:
if (import.meta.env.DEV) {
const { startSkinCheck } = await import("@xihan-ui/core/skin-check");
startSkinCheck();
}报告到诊断通道,代码为 styles.missing-skin:
[xh][button] [styles] button 的皮肤没引:import '@xihan-ui/styles/button.css',或改引全量的 '@xihan-ui/styles'每个 scope 只探测一次(探测需要读取计算样式,逐实例探测会触发强制样式重算),用 MutationObserver 接收后续进入的节点,返回值是停止函数。全量引入时开启它没有额外输出。
传入 root 时,元素品牌、计算样式和 MutationObserver 全部取自该 root 所属的 Window,因此 iframe 中的按需皮肤可以独立检查;即使顶层没有 DOM globals,有效的显式 root 仍可工作。显式 root 没有活动 Window,或所属 Window 不提供 MutationObserver 时会直接抛错,不会只扫一次后静默停止持续检查。SSR 中不传 root 仍返回空停止函数。
令牌的机读形式也可直接使用,用于生成 Figma 变量、Tailwind 主题或其他产物:
import tokens from "@xihan-ui/tokens/tokens.json" with { type: "json" };
// { "--xh-color-brand-500": "oklch(0.623 0.214 258)", ... }宿主有无层 reset 时改用无层版
默认的 index.css 把全部皮肤包在 @layer xihan.* 内。CSS 级联有一条容易忽略的规则:无层声明胜过任何有层声明,与特异性无关。因此宿主应用只要带一条无层的 reset 或 normalize,例如
button { padding: 0; background-color: transparent; }它就会覆盖皮肤中所有 [data-scope='button'][data-part='root'] 的对应声明,即便后者特异性更高。表现是组件渲染为没有内边距、没有底色的裸元素。Tailwind v3 的 preflight、normalize.css,以及多数文档站 / 脚手架自带的重置都是无层的,都会触发这一问题。
判断方法:组件的 data-scope / data-part 属性都存在、皮肤 CSS 已加载,但盒模型相关的属性全部未生效。
包内为此额外提供一份移除层壳的 index.unlayered.css,内容与 index.css 完全一致(家族同样只内联一次、排在皮肤之前),由构建脚本从同一份源序生成:
// 宿主带无层 reset 时用这份,规则改按特异性竞争
// 皮肤选择器至少是 [data-scope][data-part](0,2,0),稳压 button(0,0,1)
import "@xihan-ui/styles/index.unlayered.css";两份的选择:
| 情况 | 使用 | 覆盖皮肤的方式 |
|---|---|---|
自己的样式也都在 @layer 内(如 Tailwind v4) | index.css | 写进 @layer xihan.overrides,或任何排在 xihan 之后的层 |
| 宿主带无层 reset / normalize | index.unlayered.css | 使用不低于 [data-scope][data-part] 的特异性 |
无层版本里库自己的 reset 层(box-sizing、表单控件的 font: inherit、浮层落位前的 visibility: hidden 等)特指度是 (0,0,0),宿主的元素选择器(button { font-family: … } 这类 (0,0,1))会压过它。这只影响 reset 本身:Action Control / Field Chrome 等家族配方与组件皮肤自己声明的 font-size、line-height 不受影响,宿主 reset 需要与库一致时把同样的声明写成 inherit 即可。取舍见皮肤与样式分层。
使用 index.css 时,覆盖位置是现成的:
@layer xihan.overrides {
[data-scope='button'][data-part='root'] { border-radius: 0; }
}转换是单向的:@import url('...') layer(x) 可以给无层样式套一层,但无法给已层化的样式脱层,因此无层产物由库提供。
本文档站使用的就是无层版:VitePress 自带无层的 button 重置,使用 index.css 时所有示例都会渲染为纯文本。
因此本站的示例全部运行在无层版本上,层序在这里不成立。上表、@layer xihan.overrides 的覆盖写法,以及任何与层序有关的表现,本站示例都无法演示与验证;请在自己的项目中引入 index.css 后验证。
服务端渲染
- 视觉环境运行时在
document/window缺席时自动进入 SSR 分支:不读媒体查询、不写 DOM,使用八轴基线。要避免首屏闪烁,在服务端把data-theme/data-brand/data-density/data-contrast/data-motion/data-transparency/dir七个属性直接渲染到<html>上。 - 自定义元素在 JS 到达之前不会升级。
@xihan-ui/styles中的undefined.css处理这段窗口:用:not(:defined)选中作者编写的data-xh-part,先把浮层族的content/positioner/backdrop/viewport收起,避免内容以裸文本出现在页面流中被读屏和搜索引擎视为正文。
