UNPKG

ranui

Version:

A framework-agnostic Web Components UI library built on native custom elements, with TypeScript types, light/dark theming, SSR and PWA support.

426 lines (304 loc) 14.3 kB
# 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)