# SSR 边界策略

OAS-UI 是 Web Components 组件库，组件在浏览器运行时自定义元素、Shadow DOM 与事件。在 SSR / 静态生成环境下需要遵循以下边界。

## 核心规则

::: danger 不要在服务端渲染期间执行组件库的副作用导入
组件库入口 `@oas-ui/ui` 会调用 `customElements.define` 与 DOM API，在 Node（无 DOM）环境下会抛错（如 `HTMLElement is not defined`）。
:::

## DSD 路线（渐进增强）

组件库的 SSR 长期策略走 Declarative Shadow DOM（DSD）路线：服务端输出 `<template shadowrootmode="open">` 的静态结构 + 样式，浏览器 upgrade 自定义元素后由组件接管交互。

当前进展：

- 基类 `OASElement` 已支持复用 declarative shadow root：元素在 upgrade 前已由 `<template shadowrootmode="open">` 挂好 shadow root 时，构造器复用已有 root，不再调用 `attachShadow`（否则会抛 `NotSupportedError` 导致组件报废）。
- `@oas-ui/ssr` 渲染器已落地：Node 环境用 happy-dom 起最小 DOM shim，注册组件类后按入参渲染并把 shadow 快照序列化为 DSD，输出完整宿主 HTML 字符串（见「服务端渲染」）。
- 真水合：upgrade 检测到 DSD 快照指纹时跳过 shadow 重建，只缓存节点并绑定事件 + 增量 update；快照结构不符时回退全量重渲染（正确性优先）。
- 数据组件声明式数据通道：table / tree / select / transfer / toggle-group 的 `columns` / `data` / `options` / `items` 走 JSON attribute 声明式通道（property 赋值单向反射 attribute，非法 JSON 回退空态），SSR 快照可序列化表头与数据行 / 树节点行 / 下拉选项 / 穿梭面板数据 / 按钮组。
- 表单组件（DSD 白名单化）：input / textarea / checkbox / radio / switch / slider / input-number / rate / auto-complete / combobox / cascader / tree-select / mentions / date-picker / time-picker / calendar / upload / color-picker / toggle-button / toggle-group / pin-input / dynamic-input / dynamic-tags / editable / form / form-item / form-list 均拆出 `template()`（纯函数）/ `bind()`（缓存节点 + 绑事件）/ `hydrate()`（校验快照结构 + 接管）三段式，SSR 快照含骨架与已选值；下拉面板默认关闭态、上传列表为空态、textarea autosize 高度为未校正态（水合首帧后 rAF 校正，与 affix 同策略）属预期。
- 反馈组件（DSD 白名单化）：alert / progress / spin / skeleton / result / backdrop / modal / drawer / popconfirm 均按同一三段式拆分——可见态组件（alert/progress/spin/skeleton/result）快照直出完整视觉；backdrop 以 `open` 直出可见遮罩（默认关闭态会在 update 时自卸载，不纳入快照场景）；modal/drawer 默认关闭态快照为宿主骨架（`display:none`，服务端直出 `visible` 时快照含完整弹层）；popconfirm 快照含触发 slot 与关闭态气泡。命令式组件（message / notification / toast / snackbar / loading-bar / confirm）由命令式 API 动态创建、不在初始 DOM，SSR 无意义，不纳入白名单。
- 数据展示组件（DSD 白名单化）：card / avatar / avatar-group / image / qrcode / watermark / collapse（+collapse-item）/ descriptions（+descriptions-item）/ timeline（+timeline-item）/ list（+list-item）/ carousel / statistic / countdown / chart / code / equation / log / masonry / comment / marquee / number-animation / gradient-text / aspect-ratio / virtual-list 均按同一三段式拆分——纯展示组件（card/avatar/qrcode/watermark/descriptions/statistic/masonry/comment/gradient-text/aspect-ratio 等）快照直出完整视觉；chart/code/equation 为同步确定性渲染（SVG path / 正则高亮 / LaTeX 子集全部纯计算，非 canvas、无异步），快照含完整图形与高亮；动态组件（carousel/countdown/number-animation/marquee）快照为初始帧/初始值（carousel 按 index 同步 transform 与指示器、countdown 直出完整初始值、number-animation 以 duration=0 直出目标值或默认时长下直出初始值），动画与计时由浏览器 upgrade 后接管；virtual-list 快照为 scrollTop=0 的首屏窗口行 + 上下 padding 占位（窗口由 height/item-height/buffer 纯属性计算，非滚动测量），升级后按同属性重算窗口一致；log 增量同步采纳快照已有行、marquee 克隆组幂等重同步，均不重复追加。
- 导航布局组件（DSD 白名单化）：tabs（+tab-panel）/ bottom-navigation / pagination / steps / segmented / breadcrumb / anchor / back-top / menu / dropdown / context-menu / menubar / navigation-menu / toolbar / command / tour / hover-card / splitter / flex / page-header / float-button / speed-dial / layout（+header/sider/content/footer 多 tag）/ sidebar / container / grid（+grid-item）均按同一三段式拆分——静态结构组件（tabs/steps/pagination/breadcrumb/segmented/flex/page-header/container/grid/splitter 等）快照直出完整结构；浮层触发类（dropdown/context-menu/hover-card/command/tour/speed-dial）面板默认关闭、快照为触发器骨架（服务端直出 open 时含完整浮层），浏览器 upgrade 后交互展开；可见菜单类（menu/menubar/navigation-menu/toolbar）快照直出菜单结构（子菜单默认收起）；layout 多 tag 组件依赖嵌套递归序列化（见下）。
- 纯展示与框架容器组件（DSD 白名单化）：badge / button-group / icon / kbd / label / link / space / visually-hidden 纯展示组件快照直出完整视觉（badge 徽标数字、kbd 键帽拆分、space 宿主内联布局样式均确定性写入）；tooltip / popover 浮层触发类默认关闭、快照为触发器 slot 原样 + 关闭态气泡骨架（content/title 文本同步，定位在触发时计算）；config-provider / app 框架级容器无自身视觉、快照为子树原样 + 容器属性就位（data-theme / size 等），嵌套子组件由 injectNestedDSD 覆盖。theme-editor 为开发工具组件（开发期主题编辑面板），SSR 意义低，评估后排除白名单，保持客户端专属。
- 补位批次（DSD 白名单化）：highlight 文本命中高亮（同步确定性渲染，纯展示直出）；scheduler / gantt / barcode 数据组件（数据走 JSON attribute 声明式通道，同 calendar / kanban / table / qrcode 类，同步确定性渲染、无测量依赖）；form 收尾 knob / questionnaire；conversation 五件 bubble / attachment（+attachment-group）/ message-scroller / marker / message-row；workbench 构件族 titlebar / statusbar / inspector / action-bar 及各自子件——纯展示/骨架直出快照（浮层与输入交互默认关闭/空态），全部组件有 `hydrate()` 真水合覆写。导航追加 app-bar（应用栏，可见结构直出）与 stepper（步骤面板，+stepper-panel）。
- 嵌套递归序列化：light DOM 里已 upgrade 的白名单子组件（如 form>form-item>oas-input、tabs>tab-panel、descriptions>descriptions-item、layout>sider/header、grid>grid-item、timeline>timeline-item）会被递归包成嵌套 `<template shadowrootmode="open">`（含子组件指纹）插到该子元素内容最前，禁 JS 时子组件 shadow 内容（label 文本等）同样可见；浏览器 upgrade 后父子均按指纹走真水合。子组件 tag 在渲染前按需装载（非白名单子组件不装载、按原始标记输出）；light DOM 输出用处理后的 `el.innerHTML`（保留组件对 light DOM 的同步，如 tabs 非激活面板 hidden、collapse open 态）。
- 测量组件首帧闪动治理：affix / ellipsis / scroll-area 检测到 DSD 快照时把布局写入延迟到首帧后（rAF）——快照 = 未校正态，upgrade 首帧与快照一致无跳动，rAF 后按真实布局校正。
- 框架集成插件：`@oas-ui/nuxt`（Nuxt 3 module）与 `@oas-ui/next`（Next.js App Router 集成）已落地——nuxt module 自动配置 Vue `compilerOptions.isCustomElement`（识别 `oas-*`）、注入 `@oas-ui/theme` 全局 CSS、注册 `renderOasToString` SSR helper 自动导入；next 提供 RSC `<OasComponent>` 服务端产 DSD 快照 + `<OasRegistry>` 客户端注册引导（见下「Nuxt（推荐）」/「Next.js（推荐）」）。

白名单组件可直接服务端渲染；其余组件仍按"客户端专属"方式接入（见下）。

### Vue（Nuxt / Vite SSR）

使用动态导入并在客户端挂载后执行：

```ts
// 只在客户端加载
onMounted(async () => {
  const { OASMessage } = await import('@oas-ui/ui')
  OASMessage?.success?.('已加载')
})
```

```html
<ClientOnly>
  <oas-table :columns="…"></oas-table>
</ClientOnly>
```

### React（Next.js）

只在客户端渲染组件：

```tsx
'use client'
import { useEffect, useState } from 'react'
```

或在 `next/dynamic` 中禁用 SSR：

```tsx
const Table = dynamic(() => import('./TablePage'), { ssr: false })
```

### 原生 / 其他框架

服务端只输出静态占位，脚本资源由浏览器执行注册。

## 服务端渲染

`@oas-ui/ssr` 包提供渲染器 `renderToString(tag, attrs, slotHTML, { locale })`：在 Node 服务端把白名单组件渲染为「宿主标签 + `<template shadowrootmode="open">` 快照」的完整 HTML 字符串。浏览器拿到该字符串后无需 JS 即可呈现结构与样式；随后加载组件库脚本完成 upgrade，由组件接管交互。

> 注意：SSR 快照的 shadow 内联样式引用 `--oas-*` 主题 token（token 定义在 `@oas-ui/theme` 的 `:root`）。页面必须照常引入 `@oas-ui/theme`，否则快照中的组件没有颜色（token 解析失败回落到透明）。

```ts
import { renderToString } from '@oas-ui/ssr'

const html = await renderToString('oas-button', { type: 'primary', size: 'large' }, '提交', {
  locale: 'zh-CN',
})
// '<oas-button type="primary" size="large"><template shadowrootmode="open">…</template>提交</oas-button>'
```

**为什么 async**：组件类求值依赖全局 DOM shim（`class extends HTMLElement` 需要 `HTMLElement` / `customElements` 已就位），静态 import 会提前求值、无法保证「先装 shim 再装载组件」的顺序，故首次调用时动态装载组件模块（shim 先就位）；模块加载后缓存，后续调用开销仅为渲染本身。

> 注意：DSD 模板由浏览器 **HTML 解析器**附加 shadow root，`innerHTML` 等运行时字符串注入不会触发附加。产出字符串应经由 SSR 输出流（服务端渲染的 HTML 响应）送达浏览器。
>
> 进程级副作用声明：渲染器首次调用会把 happy-dom 的 `document` / `customElements` / `HTMLElement` 等全局安装到 `globalThis`（组件类求值与注册的必需环境）。若你的 Node 进程另有全局 DOM 方案（其他 SSR 库、测试框架环境），请先评估共存——无法覆盖全局时渲染器会抛明确错误。另：`locale` 选项经全局 i18n registry 切换，渲染段为同步执行，单线程下请求间无交错窗口。

### Nuxt（推荐：`@oas-ui/nuxt` 插件）

安装并注册 module 后即开箱即用：

```bash
pnpm add @oas-ui/nuxt @oas-ui/ssr @oas-ui/theme
```

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@oas-ui/nuxt'],
})
```

插件自动完成三件事：

- **Vue isCustomElement**：`vite:extendConfig` 钩子把 `oas-*` 前缀注册进 `compilerOptions.isCustomElement`（与既有配置合并，支持函数 / RegExp / 布尔），Vue 不再把 `oas-*` 当组件解析、不再告警
- **theme CSS 注入**：`@oas-ui/theme` 自动追加到 `nuxt.options.css`（DSD 快照引用的 `--oas-*` token 全局可用）；可用 `oasUi: { theme: false }` 关闭或传自定义 CSS 入口替换
- **SSR helper**：`renderOasToString` / `useOasRender` 自动导入（来自 `@oas-ui/nuxt/ssr`），server/api 与 server components 免 import 直接调用；也可显式 `import { renderOasToString } from '@oas-ui/nuxt/ssr'`

```ts
// server/api/ssr-demo.ts（renderOasToString 已自动导入，无需 import）
export default defineEventHandler(async () => {
  const button = await renderOasToString('oas-button', { type: 'primary' }, '提交')
  const empty = await renderOasToString('oas-empty', { description: '暂无数据' }, '', {
    locale: 'zh-CN',
  })
  return `<div class="ssr-demo">${button}${empty}</div>`
})
```

页面侧把该 HTML 合入服务端渲染输出流，浏览器解析即呈现结构；客户端 upgrade 按上文「客户端专属」方式接入（页面 `onMounted` 后动态 import 组件库入口）。

### Nuxt（手写接入，底层说明）

```ts
// server/api/ssr-demo.ts
import { renderToString } from '@oas-ui/ssr'

export default defineEventHandler(async () => {
  const button = await renderToString('oas-button', { type: 'primary' }, '提交')
  const empty = await renderToString('oas-empty', { description: '暂无数据' })
  return `<div class="ssr-demo">${button}${empty}</div>`
})
```

页面侧把该 HTML 合入服务端渲染输出流，浏览器解析即呈现结构，随后加载组件库脚本接管交互。

列表场景循环调用即可（模块缓存使后续调用开销仅为渲染本身）：

```ts
const items = await Promise.all(
  rows.map((row) =>
    renderToString('oas-tag', { type: row.status }, row.label, { locale: 'zh-CN' }),
  ),
)
return `<div class="tags">${items.join('')}</div>`
```

### Next.js（推荐：`@oas-ui/next` 插件）

```bash
pnpm add @oas-ui/next @oas-ui/ssr @oas-ui/ui
```

RSC 服务端组件 `<OasComponent>`（`@oas-ui/next/server`）在服务端调 `renderToString` 产 DSD 快照，经 `dangerouslySetInnerHTML` 进 SSR 输出流：

```tsx
// app/ssr-demo/page.tsx（Server Component）
import { OasComponent } from '@oas-ui/next/server'

export default function SsrDemoPage() {
  return (
    <section>
      <OasComponent tag="oas-button" attrs={{ type: 'primary' }}>
        提交
      </OasComponent>
      <OasComponent tag="oas-divider" attrs={{ 'content-position': 'left' }}>
        分割线
      </OasComponent>
    </section>
  )
}
```

客户端注册引导：layout 挂 `<OasRegistry>`（"use client"，副作用 `import '@oas-ui/ui'` 完成全局注册）——整个应用的 `oas-*` 元素 upgrade 后由组件接管交互：

```tsx
// app/layout.tsx
import { OasRegistry } from '@oas-ui/next'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <OasRegistry>
      <html lang="zh-CN">
        <body>{children}</body>
      </html>
    </OasRegistry>
  )
}
```

低层能力：`renderOas(tag, attrs, slotHTML, opts)`（`@oas-ui/next`）为 `renderToString` 的纯逻辑包装（attrs 值支持 `string | number | boolean` 自动序列化）；`OasComponent` 的 `slotHTML` prop 直接传原始 HTML（children 为字符串或 ReactNode 时自动序列化）。

> 注意：`dangerouslySetInnerHTML` 的 DSD 模板只在浏览器 **HTML 解析器**处理时附加 shadow root——初始 SSR 由服务端输出流解析生效；客户端软导航（RSC flight 重建）走 `innerHTML` 注入不会附加 DSD，此时组件已在客户端注册（OasRegistry），由自定义元素自渲染接管，属渐进增强模型。

### Next.js（手写接入，底层说明）

```tsx
// app/ssr-demo/page.tsx
import { renderToString } from '@oas-ui/ssr'

export default async function SsrDemoPage() {
  const button = await renderToString('oas-button', { type: 'primary' }, '提交')
  const divider = await renderToString('oas-divider', { 'content-position': 'left' }, '分割线')
  return (
    <section
      dangerouslySetInnerHTML={{ __html: `<div class="ssr-demo">${button}${divider}</div>` }}
    />
  )
}
```

RSC 在服务端完成渲染，`dangerouslySetInnerHTML` 的字面量进入 SSR 输出流，由浏览器解析器附加 DSD。

### 白名单与边界

- 白名单（纯展示组件 + 声明式数据组件 + 测量组件闪动治理试点 + 表单组件 + 反馈组件 + 数据展示组件 + 导航布局组件 + 纯展示与框架容器组件 + 导航组件追加（app-bar 应用栏 + stepper 步骤面板）+ highlight 文本命中高亮 + 补位批次（scheduler / gantt / barcode 数据组件）+ 补位批次 2（form 收尾 knob / questionnaire、conversation 五件、workbench 构件族）+ oas-kanban（data 族看板），共 160 tag）：`oas-button`、`oas-tag`、`oas-tag-group`、`oas-compact`、`oas-empty`、`oas-divider`、`oas-text`、`oas-title`、`oas-paragraph`、`oas-table`、`oas-kanban`、`oas-affix`、`oas-ellipsis`、`oas-scroll-area`、`oas-tree`、`oas-select`、`oas-input`、`oas-textarea`、`oas-checkbox`、`oas-checkbox-group`、`oas-radio`、`oas-radio-group`、`oas-switch`、`oas-slider`、`oas-input-number`、`oas-rate`、`oas-auto-complete`、`oas-combobox`、`oas-cascader`、`oas-tree-select`、`oas-mentions`、`oas-date-picker`、`oas-time-picker`、`oas-calendar`、`oas-upload`、`oas-transfer`、`oas-color-picker`、`oas-toggle-button`、`oas-toggle-group`、`oas-pin-input`、`oas-dynamic-input`、`oas-dynamic-tags`、`oas-editable`、`oas-form`、`oas-form-item`、`oas-form-list`、`oas-alert`、`oas-progress`、`oas-spin`、`oas-skeleton`、`oas-skeleton-item`、`oas-result`、`oas-backdrop`、`oas-modal`、`oas-drawer`、`oas-popconfirm`、`oas-card`、`oas-avatar`、`oas-avatar-group`、`oas-image`、`oas-qrcode`、`oas-watermark`、`oas-collapse`、`oas-collapse-item`、`oas-collapsible`、`oas-descriptions`、`oas-descriptions-item`、`oas-timeline`、`oas-timeline-item`、`oas-list`、`oas-list-item`、`oas-carousel`、`oas-statistic`、`oas-countdown`、`oas-chart`、`oas-code`、`oas-equation`、`oas-log`、`oas-masonry`、`oas-comment`、`oas-marquee`、`oas-number-animation`、`oas-gradient-text`、`oas-aspect-ratio`、`oas-virtual-list`、`oas-tabs`、`oas-tab-panel`、`oas-bottom-navigation`、`oas-pagination`、`oas-steps`、`oas-segmented`、`oas-breadcrumb`、`oas-anchor`、`oas-back-top`、`oas-menu`、`oas-dropdown`、`oas-context-menu`、`oas-menubar`、`oas-navigation-menu`、`oas-toolbar`、`oas-scope-bar`、`oas-command`、`oas-tour`、`oas-hover-card`、`oas-splitter`、`oas-flex`、`oas-page-header`、`oas-float-button`、`oas-speed-dial`、`oas-layout`、`oas-header`、`oas-sider`、`oas-content`、`oas-footer`、`oas-sidebar`、`oas-container`、`oas-grid`、`oas-grid-item`、`oas-badge`、`oas-button-group`、`oas-icon`、`oas-kbd`、`oas-label`、`oas-link`、`oas-space`、`oas-visually-hidden`、`oas-tooltip`、`oas-popover`、`oas-config-provider`、`oas-app`、`oas-app-bar`、`oas-stepper`、`oas-stepper-panel`、`oas-highlight`、`oas-scheduler`、`oas-gantt`、`oas-barcode`、`oas-knob`、`oas-questionnaire`、`oas-bubble`、`oas-attachment`、`oas-attachment-group`、`oas-message-scroller`、`oas-marker`、`oas-message-row`、`oas-titlebar`、`oas-traffic-lights`、`oas-statusbar`、`oas-statusbar-item`、`oas-inspector`、`oas-inspector-tabs`、`oas-inspector-section`、`oas-inspector-row`、`oas-action-bar`、`oas-action-bar-button`、`oas-action-bar-well`、`oas-statistic-well`、`oas-task-progress-well`、`oas-transport-well`、`oas-music-well`。
- 非白名单调用直接抛错，不会静默降级。
- 命令式组件（message / notification / toast / snackbar / loading-bar / confirm）由命令式 API 动态创建（`document.createElement` + 挂到浮动层），初始 DOM 中不存在实例，SSR 无意义——不纳入白名单，保持客户端专属；confirm 复用 `oas-modal` tag（已白名单），但 `confirm()` 调用本身仍是客户端行为。
- `oas-bottom-sheet`（移动端底部抽屉承载件）不在白名单：它是宿主受控的浮层容器（`open` 属性驱动显隐），SSR 场景由宿主模板内嵌初始结构（关闭态即空面板），组件以 `passive` 被动透传模式保证 SSR/客户端结构一致；服务端直出展开态没有意义，保持客户端专属。
- `oas-theme-editor`（开发工具组件，开发期主题编辑面板）SSR 意义低，不纳入白名单，保持客户端专属。
- **未纳入白名单的其它组件**：① 子件类（如 `oas-menu-item` / `oas-dropdown-group` / `oas-toolbar-toggle` 等）由父组件承载渲染，`renderToString` 忽略嵌套的非白名单 tag，无需单独登记；② 浮层/移动端批次的 standalone 组件（`oas-index-bar` / `oas-picker` / `oas-pull-refresh` / `oas-notice-bar` / `oas-swipe-cell` / `oas-swatch` / `oas-image-group`，以及上方 `oas-bottom-sheet`）SSR 尚未立项，暂不纳入。覆盖对账由 `packages/ssr/src/whitelist-coverage.test.ts` 守卫：新增组件若既不在白名单、又未登记到「有意不 SSR」清单即红灯。
- `oas-table` / `oas-tree` / `oas-select` / `oas-transfer` / `oas-toggle-group` 的 `columns` / `data` / `options` / `items` 走 JSON attribute 声明式通道（property 赋值单向反射，非法 JSON 回退空态），SSR 快照含表头与数据行 / 树节点行 / 下拉选项 / 穿梭面板数据 / 按钮组。
- 表单组件：下拉面板类组件（auto-complete / combobox / cascader / tree-select / mentions / date-picker / time-picker / color-picker）快照为关闭态（面板骨架不含弹出内容，浏览器 upgrade 后交互展开）；upload 快照为空态列表；textarea autosize 高度为未校正态（水合首帧后 rAF 校正）。
- 导航布局组件：浮层触发类（dropdown / context-menu / hover-card / command / tour / speed-dial）面板默认关闭、快照为触发器骨架（服务端直出 open 时含完整浮层）；menu / menubar / navigation-menu / toolbar 为可见菜单结构（子菜单默认收起）；layout 多 tag 组件（layout + header/sider/content/footer）走嵌套递归序列化，子组件 shadow 内容禁 JS 可见。
- 纯展示与框架容器组件：badge / button-group / icon / kbd / label / link / space / visually-hidden 纯展示组件快照直出完整视觉（badge 徽标数字、kbd 键帽拆分、space 宿主内联布局样式确定性写入）；tooltip / popover 浮层触发类默认关闭、快照为触发器 slot 原样 + 关闭态气泡骨架（content/title 文本同步，定位在触发时计算）；config-provider / app 框架级容器快照为子树原样 + 容器属性就位，嵌套子组件由 injectNestedDSD 覆盖。
- 测量组件（affix / ellipsis / scroll-area）快照为未校正态（happy-dom 布局测量全 0），upgrade 首帧与快照一致，rAF 后按真实布局校正——这是闪动治理的设计语义。
- 真水合：upgrade 时跳过 shadow 重建（DOM 引用保持），只缓存节点并绑定事件 + 增量 update；快照结构不符时回退全量重渲染。

## 为什么

1. `customElements.define` 需要真实 DOM。
2. Shadow DOM 样式与布局依赖浏览器渲染。
3. 事件派发（`oas-change` 等 CustomEvent）只在浏览器有意义。

DSD 静态快照解决的是"无 JS 时能看到结构"的渐进增强问题；交互仍由浏览器运行时水合完成，`customElements.define` 与事件派发的边界不变。

## 三行引入（客户端）

```html
<link rel="stylesheet" href="https://unpkg.com/@oas-ui/theme@2/index.css" />
<script src="https://unpkg.com/@oas-ui/ui@2/dist/cdn.js"></script>
```

## 测试验证

- 单测在 happy-dom（模拟 DOM）环境执行；基类对已有 declarative shadow root 的复用行为有专门回归用例。
- e2e 在真实 Chromium 中执行，同时跑 axe 无障碍审计。
- DSD 静态页 e2e：构建期用 `renderToString` 产 DSD 静态页，验证禁 JS 时结构样式完整可见、upgrade 无 `NotSupportedError` 且 console 零报错、前后截图无闪动、事件可触发。
- 文档站（Vitepress）为 SSR 站点，demo 页一律在 `onMounted` 后动态 import 组件，作为 SSR 边界回归用例。
