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.

210 lines (209 loc) 10.2 kB
import type { EventManager } from './events'; import { type Getter } from './signal'; export interface Ref<T extends HTMLElement = HTMLElement> { current: T | null; } export declare const createRef: <T extends HTMLElement = HTMLElement>() => Ref<T>; /** A single directly-appendable child: element, text, nested builder, or nothing. * Internal helper — the public, general type is {@link Child}. */ type StaticChild = HTMLElement | string | ElementBuilder<any> | undefined | null; /** * The one type every `children()` / `replaceChildren()` argument accepts — * fully composable (recursive): a node, text, nested builder, `null`/`undefined`, * a (nested) array, a {@link For}/{@link Index} handle for a keyed list, or a * getter `() => Child` marking a reactive region. A getter (or `Show`/`Switch` * branch) may itself return any `Child`, including a `For`/`Index` or another * getter — control-flow nests freely. On SSR getters / `For` / `Index` render once. */ export type Child = StaticChild | ForHandle | IndexHandle | Child[] | (() => Child); declare const FOR_BRAND = "__ranFor"; /** Options for {@link For}. */ export interface ForOptions<T> { /** Reactive source array. Read inside an effect, so the list updates on change. */ each: () => readonly T[] | null | undefined; /** Stable identity per item — **must be unique** within the list. Reused across * updates to match old nodes to new items (that is what preserves DOM state). */ key: (item: T, index: number) => string | number; /** Render one item to a single node. `index` is a **getter** (reactive): it * reflects the item's current position even after the list reorders. Runs once * per item (not on every list change) — drive per-item updates with signals. */ render: (item: T, index: Getter<number>) => StaticChild; } /** Opaque handle returned by {@link For}; pass it straight to `children()`. */ export type ForHandle = { readonly [FOR_BRAND]: true; }; /** * Keyed list for `children()`. Unlike a plain getter child (which rebuilds the * whole region on every change), `For` matches items by `key` and **reuses their * DOM nodes** — only added/removed/moved items touch the DOM, so focus, scroll, * input values and in-flight transitions inside surviving rows are preserved. * * Ul().children( * For({ * each: () => rows(), * key: (r) => r.id, * render: (r, i) => Li().text(() => `${i()}. ${r.title}`), * }), * ); * * On SSR the list is rendered once as a static snapshot. Must be built inside a * `createRoot` so per-item scopes are disposed with the page. */ export declare function For<T>(options: ForOptions<T>): ForHandle; /** Options for {@link Show}. */ export interface ShowOptions<T> { /** Condition source. Truthy → `children`, falsy → `fallback`. */ when: () => T; /** Built when `when` is truthy. May return any {@link Child} — a `For`/`Index` * list, a nested `Show`, etc. Receives an accessor to the narrowed value — * read it inside a binding (`.text(() => v())`) to update without rebuilding. */ children: (value: () => NonNullable<T>) => Child; /** Built when `when` is falsy. Omitted → nothing is rendered. */ fallback?: () => Child; } /** * Fine-grained conditional child. Unlike a raw getter child (which re-runs on * **every** change it reads), `Show` rebuilds the branch **only when the * truthiness of `when` flips** — the condition is memoized. Content inside a * branch updates through its own bindings, not by re-running `Show`. This is the * SwiftUI/Solid model: build once, toggle only when the branch actually changes. * * Show({ * when: () => user(), // reads a signal * children: (u) => Div().text(() => u().name), * fallback: () => Div().text('Signed out'), * }) * * `Show` returns a getter, so it is accepted anywhere `children()` takes a child. * Must be created inside a `createRoot` (it owns a memo + the branch effect). */ export declare function Show<T>(options: ShowOptions<T>): () => Child; /** One branch of a {@link Switch}; build with {@link Match}. */ export interface MatchClause<T> { when: () => T; children: (value: () => NonNullable<T>) => Child; } /** Declare one `Switch` branch (identity helper — gives per-clause type inference). */ export declare function Match<T>(clause: MatchClause<T>): MatchClause<T>; /** Options for {@link Switch}. */ export interface SwitchOptions { /** Branches, tried in order; the first with a truthy `when` renders. Clauses are * heterogeneous (each `Match<T>` carries its own `T`), hence `any` here. */ children: MatchClause<any>[]; /** Rendered when no branch matches. Omitted → nothing. */ fallback?: () => Child; } /** * Fine-grained multi-branch conditional (the n-way `Show`). Renders the first * `Match` whose `when` is truthy, else `fallback`. Only the **index of the * winning branch** is memoized, so it rebuilds only when the active branch * changes — not on every change a `when` reads. Evaluation short-circuits at the * first match (later branches aren't subscribed while an earlier one wins). * * Switch({ * fallback: () => Span().text('idle'), * children: [ * Match({ when: () => status() === 'loading', children: () => Spinner() }), * Match({ when: () => error(), children: (e) => ErrorView(e) }), * ], * }) */ export declare function Switch(options: SwitchOptions): () => Child; declare const INDEX_BRAND = "__ranIndex"; /** Options for {@link Index}. */ export interface IndexOptions<T> { /** Reactive source array. */ each: () => readonly T[] | null | undefined; /** Render the slot at a position. `item` is a **getter** (a signal): when the * value at this index changes, it updates in place — the node is not rebuilt. * `index` is a fixed number (the position never moves). */ render: (item: () => T, index: number) => StaticChild; } /** Opaque handle returned by {@link Index}; pass it straight to `children()`. */ export type IndexHandle = { readonly [INDEX_BRAND]: true; }; /** * Position-keyed list. The node at index `i` is **reused** across updates and its * `item` signal is updated in place — nodes never move. Use it when position is * the identity (primitive arrays, fixed-length rows). Use {@link For} instead * when items have a stable id and can reorder. SSR renders once. * * Ul().children( * Index({ each: () => nums(), render: (n, i) => Li().text(() => `${i}: ${n()}`) }), * ); */ export declare function Index<T>(options: IndexOptions<T>): IndexHandle; export declare class ElementBuilder<T extends HTMLElement = HTMLElement> { private el; constructor(tag: string); id(value: string): this; /** * Apply a value now, or bind it reactively when a getter is passed. * A getter creates an effect owned by the current reactive scope (createRoot), * so the binding is disposed automatically when that scope is torn down. */ private bind; class(name: string | Getter<string>): this; addClass(...names: string[]): this; removeClass(...names: string[]): this; attr(name: string, value: string | Getter<string>): this; attrs(values: Record<string, string | number | boolean | null | undefined>): this; boolAttr(name: string, value: boolean | Getter<boolean>, enabledValue?: string): this; part(value: string | Getter<string>): this; data(key: string, value: string | Getter<string>): this; style(keyOrMap: string | Record<string, string>, value?: string | Getter<string>): this; cssVar(name: string, value: string | Getter<string>): this; aria(key: string, value: string | Getter<string>): this; role(value: string | Getter<string>): this; tabIndex(value: number): this; label(value: string | Getter<string>): this; labelledBy(id: string | Getter<string>): this; describedBy(id: string | Getter<string>): this; ariaHidden(hidden?: boolean): this; /** * Permanent build-time listener — tied to the element's lifetime. * Use for internal shadow DOM elements created in the constructor. */ on<K extends keyof HTMLElementEventMap>(type: K, listener: (this: T, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): this; /** * Lifecycle-managed listener — registered into an EventManager. * Use in connectedCallback when building elements that need cleanup on disconnect. * Call manager.abort() in disconnectedCallback to remove all listeners at once. */ listen<K extends keyof HTMLElementEventMap>(manager: EventManager, type: K, handler: (this: T, ev: HTMLElementEventMap[K]) => any, options?: Omit<AddEventListenerOptions, 'signal'>): this; /** * Delegated listener — the built element acts as the parent container. * Fires handler only when the event originates from a descendant matching selector. * Registered into an EventManager so it is cleaned up with manager.abort(). * * Div().class('list') * .children(...) * .delegate(scope, '.item', 'click', (ev, item) => handleItem(item)) * .build(); */ delegate<K extends keyof HTMLElementEventMap>(manager: EventManager, selector: string, type: K, handler: (ev: HTMLElementEventMap[K], target: Element) => void, options?: Omit<AddEventListenerOptions, 'signal'>): this; children(...items: Child[]): this; replaceChildren(...items: Child[]): this; text(value: string | Getter<string>): this; ref(holder: Ref<T>): this; shadow(options?: ShadowRootInit): ShadowBuilder<T>; build(): T; serialize(): string; } export declare class ShadowBuilder<T extends HTMLElement = HTMLElement> { private root; private hostEl; private options; constructor(host: T, root: ShadowRoot, options: ShadowRootInit); children(...items: Child[]): this; adoptSheet(...sheets: CSSStyleSheet[]): this; css(cssText: string): this; done(): { host: T; shadow: ShadowRoot; }; serialize(): string; } export {};