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.

459 lines (328 loc) 15.5 kB
# ranui An experimental UI component library based on Web Components. Components use Shadow DOM encapsulation, CSS Token theming, and SSR / Declarative Shadow DOM support. --- <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> [中文](./README.zh-CN.md) | **English** ## Important Notice This is an **experimental UI library** in early development. It is usable, but primarily intended for learning and experimentation. Key points: - **Early development**: features are still being developed and refined. - **Experimental**: APIs may change frequently. - **Learning-oriented**: mainly intended for learning Web Components and UI development. ## Features 1. **Cross-framework compatibility:** works with React, Vue, Preact, SolidJS, Svelte, and any JavaScript project that follows W3C standards. 2. **Native experience:** use custom elements such as `<r-button>` and `<r-modal>` like native HTML elements. 3. **Modular design:** supports both full imports and per-component imports for better maintainability and bundle control. 4. **Shadow DOM encapsulation:** component internals are isolated by default, while CSS Tokens, `::part()`, and the `sheet` attribute provide controlled styling hooks. 5. **TypeScript support:** built with TypeScript and type definitions. 6. **SSR friendly:** supports server rendering through `defineSSR`, `renderToString`, and Declarative Shadow DOM. 7. **Accessible:** ARIA roles/states, full keyboard navigation, form-associated inputs (`<r-checkbox>`/`<r-input>`/`<r-select>` participate in native `FormData`), live-region toasts, and `prefers-reduced-motion` support. ## Installation Using npm: ```console npm install ranui --save ``` ## Documentation and Examples [See components and use examples](https://chaxus.github.io/ran/src/ranui/) ### Components & API reference Every element's attributes, properties, **events (with `detail` shapes)**, slots, and `::part()` names are generated from source — no need to grep the exports: - Per-element API: [docs/COMPONENTS.md](./docs/COMPONENTS.md) - Design standard (color/spacing/typography/motion/a11y): [docs/DESIGN.md](./docs/DESIGN.md) Regenerate after changing a component's API with: ```bash pnpm doc:api ``` ### AI / Claude Code skill A ready-to-use skill lets AI assistants (Claude Code) read and use ranui without spelunking the source. It's published from the `ran` plugin marketplace: ```bash /plugin marketplace add chaxus/ran /plugin install ranui@ran ``` Once installed, Claude uses it automatically when you work with ranui (or invoke it explicitly as `/ranui:ranui`). The skill covers the import map, element inventory, the builder/reactivity API, accessibility, and usage examples, and points to the API reference shipped in the package ([docs/COMPONENTS.md](./docs/COMPONENTS.md)). ### Styling Documentation The styling system is unified around CSS Tokens and `::part()`. - Style override guide: [docs/style-override.md](./docs/style-override.md) - Complete Token/Part list, generated automatically: [docs/style-tokens-parts.md](./docs/style-tokens-parts.md) - Public styling API for consumers, generated automatically: [docs/style-tokens-public.md](./docs/style-tokens-public.md) - Public token filter config: [docs/style-token-filter.json](./docs/style-token-filter.json) Refresh the styling docs with: ```bash pnpm doc:style ``` ### Theming ranui ships a single token system based on the [Geist design system](https://vercel.com/geist) — Vercel's open-source design language, where color is a **state ladder** (each scale runs 100→1000, one job per step: background → hover → border → solid fill → text). ranui adopts that ladder plus **Geist Sans / Geist Mono**, so dark mode just redefines the base scale and every semantic token flips automatically. Three modes — `light`, `dark`, `system` — and no theme packs. Switch the mode or override any token at runtime (SSR-safe): ```ts import { initTheme, setTheme, setThemeToken, setThemeTokens } from 'ranui/theme'; import 'ranui/style'; initTheme(); // restore the persisted choice on load setTheme('system'); // 'light' | 'dark' | 'system' setThemeToken('--ran-color-primary', '#6c47ff'); setThemeTokens({ '--ran-radius-md': '10px' }); ``` The `ranui/theme` entry ships only the theming engine — no custom elements are registered, so it stays out of your bundle if you just want tokens/dark mode. The same APIs are also re-exported from the `ranui` barrel. Dark mode redefines only the base color scale; semantic tokens (`--ran-color-*`) reference it and flip automatically. See [docs/THEME_STYLE_SYSTEM_DESIGN.md](./docs/THEME_STYLE_SYSTEM_DESIGN.md) and [docs/DESIGN.md](./docs/DESIGN.md). ### Internationalization A framework-agnostic i18n engine ships as its own `ranui/i18n` entry — like `ranui/theme`, it registers no custom elements: ```ts import { createI18n, useI18n } from 'ranui/i18n'; createI18n({ // each locale is a flat dictionary — keys are used verbatim (no nesting) messages: { en: { 'hero.title': 'Hi {name}' }, zh: { 'hero.title': '你好 {name}' } }, fallbackLocale: 'en', persist: true, // remember the choice in localStorage detectNavigator: true, // pick the initial locale from the browser }); useI18n()!.t('hero.title', { name: 'Ada' }); // → "Hi Ada" useI18n()!.setLocale('zh'); // persists and notifies subscribers ``` `t()` falls back to the fallback locale, then to the key itself; `{param}` placeholders are interpolated. The core is SSR-safe. ## Imports Use per-component imports to reduce bundle size: ```js import 'ranui/button'; ``` Non-component subpaths ship the utilities on their own, so you can pull in just the engine you need without registering every element: ```js import { initTheme } from 'ranui/theme'; // theming only import { createI18n } from 'ranui/i18n'; // i18n only ``` If styles are missing, import the stylesheet manually: ```js import 'ranui/style'; ``` If type resolution fails, import one of the type entry points manually: ```ts import 'ranui/typings'; // or import 'ranui/dist/index.d.ts'; // or import 'ranui/type'; // or import 'ranui/dist/typings'; ``` Only one working type entry is needed. Full import is also supported: ```ts import 'ranui'; ``` ES module: ```js import 'ranui'; ``` or: ```js import 'ranui/button'; ``` UMD, IIFE, CJS: ```html <script src="./ranui/dist/umd/index.umd.cjs"></script> ``` ### Without a bundler (static pages / CDN) Pick the distribution that matches how many components the page uses: | Scenario | Best option | Why | | ------------------------------ | ---------------------------------------------- | -------------------------------------------------------------------- | | 1–2 components, one script tag | Per-component IIFE: `dist/iife/<name>.iife.js` | Self-contained, no module syntax needed | | Several components | Per-component ES modules: `dist/<name>.js` | Shared runtime chunks are deduplicated by the browser's module graph | | Everything | Full bundle: `dist/index.iife.js` | One file, every component registered | | Project with a bundler | npm imports: `import 'ranui/<name>'` | Tree-shaking and a single shared runtime | Per-component IIFE — one tag, no build step: ```html <script src="https://cdn.jsdelivr.net/npm/ranui/dist/iife/select.iife.js" defer></script> ``` Each IIFE inlines its internal dependencies (e.g. `select` includes `icon`); element registration is guarded, so loading several files that share dependencies is safe — but each file carries its own copy of the shared runtime. When a page needs several components, prefer the ES modules instead, which deduplicate it: ```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> ``` ## Usage RanUI components are Web Components, so they can be used without framework-specific wrappers. In most cases, use them like native HTML elements. Examples: - 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 Position and Container `window.message` supports custom top offset, z-index, and mount container: ```ts import 'ranui/message'; const customRoot = document.getElementById('custom-message-root'); window.message?.success({ content: 'Saved', duration: 2000, top: 24, zIndex: 3000, getContainer: () => customRoot, }); ``` `top` supports `number | string`; `24` becomes `24px`, while `'2rem'` keeps its unit. `zIndex` supports `number | string`. `getContainer` must return an `HTMLElement`; when omitted, messages mount to `document.body`. ### Reactive Primitives `signal`, `createEffect`, `computed`, `batch`, `untrack`, and an ownership layer (`createRoot` / `onCleanup` / `getOwner` / `runWithOwner`) ship alongside the DOM builder for building reactive page sections without a framework. The design mirrors SwiftUI's `@Observable` with Solid.js-style guarantees: effects auto-clean stale subscriptions before each re-run; `batch()` coalesces writes into one flush; `computed` is **lazy + value-memoized** (an unread memo never computes, and it wakes dependents only when its value actually changes); and every effect/memo/binding is owned by its scope, so disposing a `createRoot` tears down everything it spawned in one call — the teardown unit for a page/route. `ElementBuilder` chainables (`text`/`attr`/`class`/…) also accept a signal getter for auto-updating bindings. Full guide: [`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('reset') .listen( scope, 'click', () => batch(() => { setCount(0); setStep(1); }), // two writes → one effect flush ), ) .build(); const dispose = createEffect(() => { label.textContent = `${count()} (×2 = ${doubled()})`; }); container.appendChild(view); return () => { dispose(); scope.abort(); }; // teardown } ``` See [Utility Documentation](./utils/README.md) for the full API. ### Routing RanUI includes client-side routing via declarative components and a JavaScript API. **Declarative components:** ```html <r-router> <nav> <r-link href="/">Home</r-link> <r-link href="/about">About</r-link> </nav> <r-route path="/" exact><h2>Home</h2></r-route> <r-route path="/about"><h2>About</h2></r-route> <r-route path="/users/:id"><h2>User detail</h2></r-route> </r-router> ``` **JavaScript API with navigation guard:** ```ts import { createRouter } from 'ranui'; const router = createRouter({ mode: 'history', routes: [ { path: '/', exact: true, meta: { title: 'Home' } }, { 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'); ``` For pure MPA sites (no JS router needed), use `enableMpaViewTransitions()` to inject `@view-transition { navigation: auto }`. Shared-element morph animations are supported via the standard `view-transition-name` CSS property. ```ts import { enableMpaViewTransitions } from 'ranui'; enableMpaViewTransitions(); ``` See the [Router documentation](https://chaxus.github.io/ran/src/ranui/router/) for the full API including guards, `onPageSwap`/`onPageReveal`, and per-element transition names. ### SSR & Builder For SSR or declarative UI construction, RanUI internally uses `builder`, the SSR registry, and Declarative Shadow DOM. Components reuse existing Shadow Roots through `ensureShadowRoot` and keep initialization idempotent through `ensureShadowElement`. Source-level SSR rendering example: ```ts import { Button } from '@/components/button'; import { renderToString } from '@/utils/ssr'; const button = new Button(); button.setAttribute('effect', 'true'); // Outputs an HTML string containing Declarative Shadow DOM. const html = renderToString(button); ``` See [Utility Documentation](./utils/README.md) for details. ## Component Development Conventions When adding or maintaining components, follow the package conventions: - Extend `RanElement`; do not directly extend the browser `HTMLElement`. - Use `ensureShadowRoot` to create or reuse Shadow Roots; do not call `attachShadow` directly. - Use `ensureShadowElement` to build Shadow DOM subtrees idempotently. - Include `sheet` in `observedAttributes` and sync component-level style overrides through `syncSheetAttribute`. - Guard `attributeChangedCallback` with `if (old === next) return;`. - Register components with `defineSSR('r-name', Component)`, not direct `customElements.define`. - Add both type exports and side-effect imports in `index.ts`; also add standalone entries in `vite.config.ts` and `package.json`. - Use `EventManager` from `@/utils/builder` for lifecycle-bound listeners in `connectedCallback`; call `manager.abort()` in `disconnectedCallback` instead of tracking individual `removeEventListener` calls. ## Contributing Contributions from learners and developers are welcome. This is an experimental project, so please expect active iteration. ## Contributors <a href="https://github.com/chaxus/ran/graphs/contributors"> <img src="https://contrib.rocks/image?repo=chaxus/ran" /> </a> ## Meta [LICENSE (MIT)](/LICENSE)