UNPKG

@furystack/shades

Version:

A lightweight UI framework for FuryStack with JSX support

226 lines 10.4 kB
import type { Injector } from '@furystack/inject'; import type { MatchOptions, MatchResult } from 'path-to-regexp'; import type { RenderOptions } from '../models/render-options.js'; import type { ViewTransitionConfig } from '../view-transition.js'; import type { HashLiterals, QueryValidator } from './nested-route-types.js'; /** * Options passed to a dynamic title resolver function. * @typeParam TMatchResult - The type of matched URL parameters */ export type TitleResolverOptions<TMatchResult = unknown> = { match: MatchResult<TMatchResult extends object ? TMatchResult : object>; injector: Injector; }; /** * Metadata associated with a route entry. * Used by consumers (breadcrumbs, document title, navigation trees) to * derive display information from the route hierarchy. * * This is an `interface` so that applications can augment it with custom fields * via declaration merging: * * @example * ```typescript * declare module '@furystack/shades' { * interface NestedRouteMeta { * icon?: IconDefinition * hidden?: boolean * } * } * ``` * * @typeParam TMatchResult - The type of matched URL parameters */ export interface NestedRouteMeta<TMatchResult = unknown> { title?: string | ((options: TitleResolverOptions<TMatchResult>) => string | Promise<string>); } /** * A single route entry in a NestedRouter configuration. * Unlike flat `Route`, the URL is the Record key (not a field), and the * `component` receives an `outlet` for rendering matched child content. * * Routes may additionally declare: * - `query`: a validator that parses the deserialized query string into a * typed shape; `component` receives the parsed value (or `null` when * validation fails). The route still matches on path alone — an invalid * query never prevents navigation. * - `hash`: a readonly tuple of allowed URL hash literals; `component` * receives the current hash when it matches one of the listed literals, * or `undefined` otherwise. * * @typeParam TMatchResult - The type of matched URL parameters * @typeParam TQuery - The typed query shape parsed from the URL search string (defaults to `never`) * @typeParam THash - The readonly tuple of allowed hash literals (defaults to `never`) */ export type NestedRoute<TMatchResult = unknown, TQuery = any, THash extends HashLiterals = readonly any[]> = { meta?: NestedRouteMeta<TMatchResult>; component: (options: { currentUrl: string; match: MatchResult<TMatchResult extends object ? TMatchResult : object>; query: TQuery | null; hash: THash[number] | undefined; outlet?: JSX.Element; }) => JSX.Element; routingOptions?: MatchOptions; /** * Called after the route's DOM has been mounted. When view transitions are enabled, * this runs after the transition's update callback has completed and the new DOM is in place. * Use for imperative side effects like data fetching or focus management — not for visual * animations, which are handled by the View Transition API when `viewTransition` is enabled. */ onVisit?: (options: RenderOptions<unknown> & { element: JSX.Element; }) => Promise<void>; /** * Called before the route's DOM is removed (and before the view transition starts, if enabled). * Use for cleanup or teardown logic — not for exit animations, which are handled by the * View Transition API when `viewTransition` is enabled. */ onLeave?: (options: RenderOptions<unknown> & { element: JSX.Element; }) => Promise<void>; children?: Record<string, NestedRoute<any, any, any>>; viewTransition?: boolean | ViewTransitionConfig; /** * Optional validator that narrows the deserialized URL query string into a * typed shape. Return `null` when the URL's query does not satisfy the route's * contract — the route still matches on path, but `component` receives `null`. */ query?: QueryValidator<TQuery>; /** * Optional readonly tuple of URL hash literals the route understands. Declare * with `as const` to preserve literal types, e.g. `hash: ['tab1', 'tab2'] as const`. * The router forwards the current hash to `component` only when it matches one * of the listed literals; otherwise `component.hash` is `undefined`. */ hash?: THash; }; /** * Props for the NestedRouter component. * Routes are defined as a Record where keys are URL patterns. */ export type NestedRouterProps = { routes: Record<string, NestedRoute<any, any, any>>; notFound?: JSX.Element; viewTransition?: boolean | ViewTransitionConfig; }; /** * A single entry in a match chain, pairing a matched route with its match * result and the typed `query` / `hash` values derived from the URL for that * route's declared schema. */ export type MatchChainEntry = { route: NestedRoute<unknown, any, any>; match: MatchResult<object>; query: unknown; hash: string | undefined; }; /** * Internal state for the NestedRouter component. * `matchChain` is `null` when a notFound fallback has been rendered, * distinguishing it from the initial empty array (not yet processed). */ export type NestedRouterState = { matchChain: MatchChainEntry[] | null; jsx: JSX.Element; chainElements: JSX.Element[]; }; /** * Recursively builds a match chain from outermost to innermost matched route. * * For routes with children, a prefix match (`end: false`) is attempted first. * If a child matches the remaining URL, the parent and child chain are combined. * If no child matches, an exact match on the parent alone is attempted. * * For leaf routes (no children), only exact matching is used. * * The returned entries contain placeholder `query: null` / `hash: undefined` * values; callers are expected to populate them via {@link enrichMatchChain}. * * @param routes - The route definitions to match against * @param currentUrl - The URL path to match * @returns An array of matched chain entries from outermost to innermost, or null if no match */ export declare const buildMatchChain: (routes: Record<string, NestedRoute<any, any, any>>, currentUrl: string) => MatchChainEntry[] | null; /** * Populates each chain entry's `query` and `hash` fields by running the route's * declared validator against the URL's deserialized query string, and matching * the current URL hash against the route's declared literal tuple. * * Entries whose route declares neither `query` nor `hash` are returned with * `query: null` / `hash: undefined`. * * When no entry in the chain declares either `query` or `hash`, the input * array is returned unchanged to avoid a per-navigation allocation on the * common path-only case. * * @param chain - The chain produced by {@link buildMatchChain} * @param deserializedSearch - The deserialized URL query string * @param currentHash - The current URL hash (without the leading `#`) */ export declare const enrichMatchChain: (chain: MatchChainEntry[], deserializedSearch: Record<string, unknown>, currentHash: string) => MatchChainEntry[]; /** * Finds the first index where two match chains diverge, considering route * identity and matched path parameters only. Used to scope lifecycle hooks * (`onLeave` / `onVisit`) so that a query string or hash change does not * fire spurious mount / unmount callbacks. * * Returns the length of the shorter chain if one is a prefix of the other. */ export declare const findDivergenceIndex: (oldChain: MatchChainEntry[], newChain: MatchChainEntry[]) => number; /** * Returns true when any chain entry differs in its `query` value or `hash` * segment, ignoring path-level fields (route identity and params). Used to * force a re-render when the URL's query string or hash changes without the * matched route chain itself changing. * * Query values are compared with a key-order-independent shallow equality — * sufficient for the typed shapes a route's `query` validator surfaces. */ export declare const hasQueryOrHashChanged: (oldChain: MatchChainEntry[], newChain: MatchChainEntry[]) => boolean; /** * The result of rendering a match chain, containing both the fully composed * JSX tree and per-entry elements for scoped lifecycle animations. */ export type RenderMatchChainResult = { jsx: JSX.Element; chainElements: JSX.Element[]; }; /** * Renders a match chain inside-out: starts with the innermost (leaf) route * rendered with `outlet: undefined`, then passes its JSX as `outlet` to * each successive parent up the chain. * * Returns per-entry elements so that lifecycle hooks (`onLeave`/`onVisit`) * receive only the element for their own route level, not the full tree. * * @param chain - The match chain from outermost to innermost * @param currentUrl - The current URL path * @returns The fully composed JSX element and per-entry rendered elements */ export declare const renderMatchChain: (chain: MatchChainEntry[], currentUrl: string) => RenderMatchChainResult; /** * Resolves the effective view transition config for a navigation by merging * the router-level default with the innermost (leaf) route's override. * A per-route `false` disables transitions even when the router default is on. */ export declare const resolveViewTransition: (routerConfig: boolean | ViewTransitionConfig | undefined, newChain: MatchChainEntry[]) => ViewTransitionConfig | false; /** * A nested router component that supports hierarchical route definitions * with parent/child relationships. Parent routes receive an `outlet` prop * containing the rendered child route, enabling layout composition. * * Routes are defined as a Record where keys are URL patterns (following the * RestApi pattern). The matching algorithm builds a chain from outermost to * innermost route, then renders inside-out so each parent wraps its child. * * The router subscribes to path, query string and hash changes; path-level * changes drive `onLeave` / `onVisit` lifecycle hooks while query / hash * changes re-render the chain without firing lifecycle callbacks. */ export declare const NestedRouter: (props: NestedRouterProps & Omit<Partial<HTMLElement>, "style"> & { style?: Partial<CSSStyleDeclaration>; } & { ref?: import("../models/render-options.js").RefObject<Element>; }, children?: import("../index.js").ChildrenList) => JSX.Element; //# sourceMappingURL=nested-router.d.ts.map