跳转到内容

安装与接入 ​

从 npm 安装 ​

XiHan.UI 的 18 个公开包都已发布到 npm,同属一个版本组、始终同号。当前版本号以 npm 为准:npm view @xihan-ui/vue version。

安装的包取决于使用的适配器:

bash
# 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 安装,还有两条本地路径:

  1. 克隆仓库直接开发:构建库包后运行文档站,136 个组件的示例都是真实组件;
  2. 本地构建后链接进项目:适合跟随仓库最新改动。

WARNING

@xihan-ui/icons 只收录自研的一等图标集,当前 184 枚,覆盖组件与常见界面所需,不追求完整。 需要整套图标时请自行准备,或用 XhIcon 接入任意图标源:它接受 IconRecord 纯数据,任何来源都可以转换。

环境要求 ​

项要求
Node(安装使用)≥ 18,与包的 engines 声明一致
Node / pnpm(参与本仓库开发)≥ 24.0.0 / ≥ 11.0.0
模块格式ESM only,不提供 CJS
浏览器支持 oklch()、@layer、:where() 的现代浏览器

路径一:克隆仓库开发 ​

bash
git clone https://github.com/XiHanFun/XiHan.UI.git
cd XiHan.UI/ui
pnpm install --frozen-lockfile
pnpm build

在浏览器中查看组件请运行文档站:它通过 link: 指向本仓库的库包,示例渲染的是真实组件,Vue 与自定义元素两套写法并排,因此需要先 pnpm build。

bash
cd ../docs
pnpm install
pnpm dev

回到 ui/ 的常用命令:

bash
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 前需要安装浏览器:

bash
pnpm exec playwright install chromium

路径二:链接进现有项目 ​

先在 XiHan.UI 仓库中构建:

bash
cd XiHan.UI/ui && pnpm build

再在项目中用 link: 协议指向它(pnpm 写法,路径按实际填写):

json
{
  "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 项目 ​

ts
// 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");

组件按需从主入口取,不需要注册插件:

vue
<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 项目 ​

ts
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 标出角色节点:

html
<xh-button variant="solid">
  <button data-xh-part="root">提交</button>
</xh-button>

详见 Web Components 适配器。

样式的三种接法 ​

@xihan-ui/styles 是纯 CSS 包,与 JS 层无关,三种粒度任选:

全量:令牌 + 层序 + reset + 全部组件皮肤。主入口 index.css 是一份生成的扁平文件:Action Control / Field Chrome / Collection Item / Swatch 四份家族配方与共享关键帧在 focus / label / description / pointer 四份公共层之后只内联一次、排在一切组件皮肤之前,各皮肤随后按源序内联、去掉自带的家族 @import。除令牌那一条外没有 @import,任何打包器都不会把家族复制几十份。

ts
import "@xihan-ui/styles";

按组件引入。layers.css 与 tokens.css 各自带完整层序声明,先引入任一即可;组件皮肤不能排在它们之前。每份单皮肤文件头自带它所属家族的 @import '../family/*.css',单独引入时家族在场。

ts
import "@xihan-ui/styles/layers.css";
import "@xihan-ui/tokens/tokens.css";
import "@xihan-ui/styles/button.css";
import "@xihan-ui/styles/dialog.css";

只引入令牌,自行编写皮肤。

ts
import "@xihan-ui/tokens/tokens.css";

第三种同样保持层序:tokens.css 自带完整的层序声明,自行编写的皮肤直接写进 @layer xihan.overrides 即可。组件不依赖默认皮肤,只向 DOM 写入 data-scope / data-part / data-state 等属性,样式完全由使用者决定。参见皮肤与样式分层。

第二种需要注意两点

  1. 漏引默认是静默的。少引一份皮肤时,该组件的 data-scope / data-part 照常存在、其他皮肤也已加载,只有它渲染为没有内边距、没有底色的裸元素。开发模式下开启下文的探测器可以发现。
  2. 顺序按 index.css 里各皮肤段的相对顺序。同一个 @layer xihan.components 内,等特异性的规则由源序决定。自行排序(按字母、按目录读取序)当前可能看不出差别,将来增加跨组件规则后会与全量引入的渲染不同。需要按需引入时,按 index.css 里 /* styles/xxx.css */ 段标记的顺序过滤,不自行排序。
  3. 混用多份单皮肤时,每份都各带一份家族 @import。打包器对同一 URL 的 @import 去重(Vite 默认的 postcss 路径会去重)时没有代价;不去重的内联器(例如 @tailwindcss/vite 自带的那条)会把家族复制多份,产物随之翻倍,而且后出现的副本会排在前面的皮肤之后。家族的关键覆盖点已按特指度而非源序设计,但多份副本仍是纯浪费——引入的皮肤超过几份时改用主入口。

全量是 148 份皮肤加令牌,压缩后约 130 kB gzip。没有明确的体积压力时使用第一种。

开发模式下查漏引 ​

每份组件皮肤在自己的 [data-scope='X'] 上写入一个 --xh-X-skin 标记。startSkinCheck() 扫描页面上出现过的每个 scope,取不到标记即说明该 CSS 未加载:

ts
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 主题或其他产物:

ts
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,例如

css
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 完全一致(家族同样只内联一次、排在皮肤之前),由构建脚本从同一份源序生成:

ts
// 宿主带无层 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 / normalizeindex.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 时,覆盖位置是现成的:

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 收起,避免内容以裸文本出现在页面流中被读屏和搜索引擎视为正文。

下一步 ​

Released under The MIT License