# ranui

基于 `Web Components` 的实验性组件库。组件使用 Shadow DOM 封装、CSS Token 主题体系，并提供 SSR / Declarative Shadow DOM 支持。

---

<a href="https://github.com/chaxus/ran"><img src="https://img.shields.io/github/actions/workflow/status/chaxus/ran/ci.yml" alt="Build Status"></a>
<a href="https://github.com/chaxus/ran"><img src="https://img.shields.io/npm/v/ranui.svg" alt="npm-v"></a>
<a href="https://github.com/chaxus/ran"><img src="https://img.shields.io/npm/dt/ranui.svg" alt="npm-d"></a>
<a href="https://github.com/chaxus/ran"><img src="https://img.badgesize.io/https:/unpkg.com/ranui/dist/umd/shadowless/shadowless.umd.cjs?label=brotli&compression=brotli" alt="brotli"></a>
<a href="https://github.com/chaxus/ran"><img src="https://img.shields.io/badge/module%20formats-umd%2C%20esm-green.svg" alt="module formats: umd, esm"></a>

**中文** | [English](./README.md)

## ⚠️ 重要说明

这是一个**实验性 UI 库**，处于早期开发阶段。虽然功能可用，但主要用于学习和实验。

**关键要点：**

- 🚧 **早期开发**: 功能仍在开发和完善中
- 🧪 **实验性**: API 可能会频繁变化
- 📚 **学习导向**: 主要用于学习 Web Components 和 UI 开发

## 特点

1. **跨框架兼容：** 与 React, Vue, Preact, SolidJS, Svelte 等兼容。可以和遵循 W3C 标准的任何 JavaScript 项目集成。
2. **原生体验：** 易于入门，像使用原生 HTML 标签一样使用 `<r-button>`、`<r-modal>` 等自定义元素。
3. **模块化设计：** 可选导入和全量导入，以增强可维护性和可伸缩性。
4. **Shadow DOM 封装：** 组件内部样式默认隔离，并通过 CSS Token、`::part()` 和 `sheet` 属性开放可控的样式覆盖能力。
5. **支持类型校验：** 基于 TypeScript 构建，具有类型支持，确保代码的健壮性和可维护性。
6. **SSR 友好：** 通过 `defineSSR`、`renderToString` 和 Declarative Shadow DOM 支持服务端渲染场景。
7. **无障碍：** ARIA 角色/状态、完整键盘导航、表单参与控件（`<r-checkbox>`/`<r-input>`/`<r-select>` 可被原生 `FormData` 收集）、live-region 提示，并尊重 `prefers-reduced-motion`。

## 安装

使用 npm:

```console
npm install ranui --save
```

## 文档和示例

[See components and use examples](https://chaxus.github.io/ran/cn/src/ranui/)

### 样式定制文档

当前样式系统已统一为 CSS Token 与 `::part()` 规范。

- 样式覆盖规范：[docs/style-override.md](./docs/style-override.md)
- 完整 Token/Part 清单（自动生成）: [docs/style-tokens-parts.md](./docs/style-tokens-parts.md)
- 面向使用方的公开样式 API（自动生成）: [docs/style-tokens-public.md](./docs/style-tokens-public.md)
- 公开 Token 过滤配置：[docs/style-token-filter.json](./docs/style-token-filter.json)

可通过以下命令刷新样式文档：

```bash
pnpm doc:style
```

### 主题

RanUI 基于 [Geist 设计体系](https://vercel.com/geist) 提供统一的 CSS Token 主题体系——Geist 是 Vercel 的开源设计语言，其核心是把颜色组织成一条**状态阶梯**（每条色阶 100→1000，每档一个固定职责：背景 → 悬停 → 边框 → 实心填充 → 文字）。RanUI 采用这套阶梯并搭配 **Geist Sans / Geist Mono**，因此暗色模式只需重定义基础色阶，所有语义 Token 自动翻转。支持 `light`、`dark`、`system` 三种模式（已不再提供主题包）。可在运行时切换模式或覆盖任意 Token（SSR 安全）：

```ts
import { initTheme, setTheme, setThemeToken, setThemeTokens } from 'ranui/theme';
import 'ranui/style';

initTheme(); // 页面加载时恢复上次的选择
setTheme('system'); // 'light' | 'dark' | 'system'
setThemeToken('--ran-color-primary', '#6c47ff');
setThemeTokens({ '--ran-radius-md': '10px' });
```

`ranui/theme` 入口只包含主题引擎，不会注册任何自定义元素——只需要 Token / 暗色模式时它不会把组件带进你的包体。这些 API 同样从 `ranui` 主入口重新导出。

暗色模式只重定义基础色阶，语义 Token（`--ran-color-*`）引用色阶后自动翻转。详见 [docs/THEME_STYLE_SYSTEM_DESIGN.md](./docs/THEME_STYLE_SYSTEM_DESIGN.md) 与 [docs/DESIGN.md](./docs/DESIGN.md)。

### 国际化

框架无关的 i18n 引擎作为独立的 `ranui/i18n` 入口提供，与 `ranui/theme` 一样不注册任何自定义元素：

```ts
import { createI18n, useI18n } from 'ranui/i18n';

createI18n({
  // 每种语言是扁平字典——key 原样使用（不做嵌套）
  messages: { en: { 'hero.title': 'Hi {name}' }, zh: { 'hero.title': '你好 {name}' } },
  fallbackLocale: 'en',
  persist: true, // 记住选择（localStorage）
  detectNavigator: true, // 按浏览器语言初始化
});

useI18n()!.t('hero.title', { name: 'Ada' }); // → "Hi Ada"
useI18n()!.setLocale('zh'); // 持久化并通知订阅者
```

`t()` 依次回退到 fallback locale、再到 key 本身；`{param}` 占位符会被插值。核心逻辑 SSR 安全。

## 引入方式

支持按需导入，以显著减少包体积大小

```js
import 'ranui/button';
```

非组件入口单独打包对应的工具引擎，可以只引入需要的部分，而不注册全部元素：

```js
import { initTheme } from 'ranui/theme'; // 仅主题
import { createI18n } from 'ranui/i18n'; // 仅 i18n
```

如果遇到样式问题，可以选择手动导入样式文件

```js
import 'ranui/style';
```

如果遇到类型问题，可以选择手动导入类型文件

```ts
import 'ranui/typings';
// 或者
import 'ranui/dist/index.d.ts';
// 或者
import 'ranui/type';
// 或者
import 'ranui/dist/typings';
```

并不是都要，选一个能生效的就行

也支持全量导入

```ts
import 'ranui';
```

- ES module

```js
import 'ranui';
```

或者

```js
import 'ranui/button';
```

- UMD, IIFE, CJS

```html
<script src="./ranui/dist/umd/index.umd.cjs"></script>
```

### 无构建工具场景（静态页 / CDN）

按页面用到的组件数量选择分发方式：

| 场景                         | 推荐方式                                | 原因                                      |
| ---------------------------- | --------------------------------------- | ----------------------------------------- |
| 只用 1–2 个组件，一行 script | 按组件 IIFE：`dist/iife/<name>.iife.js` | 自包含，无需模块语法                      |
| 用多个组件                   | 按组件 ES 模块：`dist/<name>.js`        | 共享 runtime chunk 由浏览器模块图自动去重 |
| 全都要                       | 全量包：`dist/index.iife.js`            | 一个文件注册所有组件                      |
| 项目里有构建工具             | npm 引入：`import 'ranui/<name>'`       | 可摇树，共享一份 runtime                  |

按组件 IIFE——一行引入、零构建：

```html
<script src="https://cdn.jsdelivr.net/npm/ranui/dist/iife/select.iife.js" defer></script>
```

每个 IIFE 内联了自己的内部依赖（如 `select` 内含 `icon`）；元素注册有守卫，多个文件共享依赖时同时加载是安全的——但每个文件都带一份共享 runtime。页面需要多个组件时，建议改用 ES 模块，浏览器会自动去重：

```html
<script type="module">
  import 'https://cdn.jsdelivr.net/npm/ranui/dist/button.js';
  import 'https://cdn.jsdelivr.net/npm/ranui/dist/select.js';
</script>
```

## 使用方式

它是基于`Web Components`的组件，你可以不用关注框架就可以使用它。

在大多数情况下，您可以像使用本地 `div` 标签一样使用它

下面是一些例子：

- html
- js
- jsx
- vue
- tsx

### html

```html
<script src="./ranui/dist/umd/index.umd.cjs"></script>

<body>
  <r-button>Button</r-button>
</body>
```

### js

```js
import 'ranui';

const Button = document.createElement('r-button');
Button.textContent = 'this is button text';
document.body.appendChild(Button);
```

### jsx

```jsx
import 'ranui';

const App = () => {
  return (
    <>
      <r-button>Button</r-button>
    </>
  );
};
```

### vue

```vue
<template>
  <r-button></r-button>
</template>
<script>
import 'ranui';
</script>
```

### tsx

```tsx
import 'ranui/button';

const Button = () => {
  return (
    <div>
      <r-button type="primary">button</r-button>
    </div>
  );
};
```

### Message 位置与容器配置

`window.message` 现已支持自定义顶部偏移、层级和挂载容器：

```ts
import 'ranui/message';

const customRoot = document.getElementById('custom-message-root');

window.message?.success({
  content: '保存成功',
  duration: 2000,
  top: 24,
  zIndex: 3000,
  getContainer: () => customRoot,
});
```

`top` 支持 `number | string`（`24` 会转成 `24px`，`'2rem'` 会保留原单位）。

`zIndex` 支持 `number | string`。

`getContainer` 需要返回 `HTMLElement`；未传时默认挂载到 `document.body`。

### 响应式原语

`signal`、`createEffect`、`computed`、`batch`、`untrack` 以及所有权层（`createRoot` / `onCleanup` / `getOwner` / `runWithOwner`）与 DOM builder 一起提供，用于在无框架依赖的情况下构建响应式页面区块。设计参考 SwiftUI 的 `@Observable`，并采用 Solid.js 风格的保证：effect 重新执行前自动清理过期订阅；`batch()` 将多次写入合并为一次 flush；`computed` 是**惰性 + 按值记忆化**的（未被读取的 memo 从不计算，且仅当值真正改变时才唤醒依赖）；每个 effect/memo/绑定都归其作用域所有，销毁一个 `createRoot` 即可一次性拆除其派生的一切 —— 这是页面/路由的销毁单元。`ElementBuilder` 链式方法（`text`/`attr`/`class`/…）也接受 signal getter 作为自动更新的绑定。完整指南见 [`docs/BUILDER.md`](docs/BUILDER.md)。

```ts
import { signal, createEffect, computed, batch, EventManager, Div, ButtonBuilder } from 'ranui/builder';

function initCounter(container: HTMLElement) {
  const [count, setCount] = signal(0);
  const [step, setStep] = signal(1);
  const doubled = computed(() => count() * 2);
  const scope = new EventManager();

  const label = Div().build();
  const view = Div()
    .children(
      label,
      ButtonBuilder()
        .text('+')
        .listen(scope, 'click', () => setCount((n) => n + step())),
      ButtonBuilder()
        .text('重置')
        .listen(
          scope,
          'click',
          () =>
            batch(() => {
              setCount(0);
              setStep(1);
            }), // 两次写入 → 一次 flush
        ),
    )
    .build();

  const dispose = createEffect(() => {
    label.textContent = `${count()} (×2 = ${doubled()})`;
  });

  container.appendChild(view);
  return () => {
    dispose();
    scope.abort();
  }; // 区块销毁时清理
}
```

详细 API 请参考 [工具文档](./utils/README.zh-CN.md)。

### 路由

RanUI 内置客户端路由，提供声明式组件和 JavaScript API 两种方式。

**声明式组件：**

```html
<r-router>
  <nav>
    <r-link href="/">首页</r-link>
    <r-link href="/about">关于</r-link>
  </nav>

  <r-route path="/" exact><h2>首页</h2></r-route>
  <r-route path="/about"><h2>关于</h2></r-route>
  <r-route path="/users/:id"><h2>用户详情</h2></r-route>
</r-router>
```

**JavaScript API，含导航守卫：**

```ts
import { createRouter } from 'ranui';

const router = createRouter({
  mode: 'history',
  routes: [
    { path: '/', exact: true, meta: { title: '首页' } },
    { path: '/users/:id', meta: { requiresAuth: true } },
  ],
  viewTransition: 'spa', // 'spa' | 'mpa' | 'both'
});

router.beforeEach((to, from, next) => {
  if (to.meta?.requiresAuth && !isLoggedIn()) next('/login');
  else next();
});

router.push('/users/42');
```

纯 MPA 站点（无需 JS router）可使用 `enableMpaViewTransitions()` 注入 `@view-transition { navigation: auto }`。共享元素过渡动画通过标准 `view-transition-name` CSS 属性实现。

```ts
import { enableMpaViewTransitions } from 'ranui';
enableMpaViewTransitions();
```

完整 API（守卫、`onPageSwap`/`onPageReveal`、元素级动画命名）请参考 [路由文档](https://chaxus.github.io/ran/cn/src/ranui/router/)。

### SSR & Builder (推荐)

对于需要服务端渲染 (SSR) 或更声明式构建 UI 的场景，RanUI 内部使用 `builder`、SSR registry 与 Declarative Shadow DOM。组件会通过 `ensureShadowRoot` 复用已有 Shadow Root，并通过 `ensureShadowElement` 保持初始化幂等。

源码内的 SSR 渲染示例：

```ts
import { Button } from '@/components/button';
import { renderToString } from '@/utils/ssr';

const button = new Button();
button.setAttribute('effect', 'true');

// 输出包含 Declarative Shadow DOM 的 HTML 字符串
const html = renderToString(button);
```

更多细节请查看 [Utility Documentation](./utils/README.md)。

## 组件开发约定

新增或维护组件时请遵循当前包内约定：

- 组件继承 `RanElement`，不要直接继承浏览器环境下的 `HTMLElement`。
- 使用 `ensureShadowRoot` 创建或复用 Shadow Root，不要直接调用 `attachShadow`。
- 使用 `ensureShadowElement` 构建 Shadow DOM 子树，保证重复构造时幂等。
- `observedAttributes` 包含 `sheet`，并通过 `syncSheetAttribute` 同步组件级样式覆盖。
- `attributeChangedCallback` 首行使用 `if (old === next) return;` 避免重复同步。
- 使用 `defineSSR('r-name', Component)` 注册组件，而不是直接调用 `customElements.define`。
- 在 `index.ts` 中同时添加类型导出和副作用导入；在 `vite.config.ts`、`package.json` 中补齐独立入口与导出。
- 在 `connectedCallback` 中使用 `@/utils/builder` 导出的 `EventManager` 管理生命周期事件；在 `disconnectedCallback` 中调用 `manager.abort()` 一次清理所有监听器，不要逐个调用 `removeEventListener`。

## 贡献

我们欢迎学习者和开发者的贡献！这是一个实验性项目，请对开发过程保持耐心。

## 贡献者

<a href="https://github.com/chaxus/ran/graphs/contributors">
  <img src="https://contrib.rocks/image?repo=chaxus/ran" />
</a>

## Meta

[LICENSE (MIT)](/LICENSE)
