UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

122 lines 4.54 kB
import { compileRoute, createComponent, LocationService, NestedRouteLink, Shade } from '@furystack/shades'; import { cssVariableTheme } from '../services/css-variable-theme.js'; /** * A breadcrumb navigation component that works with NestedRouter to provide * navigation through route hierarchies. * * Supports: * - Dynamic route parameters (e.g. `/users/:id`) * - Custom labels and rendering * - Configurable separators * - Active item detection * - Optional home/root link * * Route parameters are automatically inferred from the path pattern: * - `path="/buttons"` — `params` is optional * - `path="/users/:id"` — `params: { id: string }` is required * * For additional URL validation against a route tree, use {@link createBreadcrumb}. * * @example * ```typescript * <Breadcrumb * homeItem={{ path: '/', label: 'Home' }} * items={[ * { path: '/users', label: 'Users' }, * { path: '/users/:id', label: 'User Details', params: { id: '123' } }, * ]} * separator=" > " * /> * ``` */ export const Breadcrumb = Shade({ customElementName: 'shade-breadcrumb', elementBase: HTMLElement, elementBaseName: 'nav', css: { display: 'flex', alignItems: 'center', gap: cssVariableTheme.spacing.sm, padding: `${cssVariableTheme.spacing.sm} 0`, fontFamily: cssVariableTheme.typography.fontFamily, fontSize: '0.9em', color: cssVariableTheme.text.secondary, '& a': { color: 'inherit', textDecoration: 'none', transition: `opacity ${cssVariableTheme.transitions.duration.normal} ${cssVariableTheme.transitions.easing.easeInOut}`, opacity: '0.8', }, '& a:hover': { opacity: '1', }, '& [data-active="true"]': { opacity: '0.6', cursor: 'default', }, '& [data-separator="true"]': { opacity: '0.5', }, '& [data-non-clickable="true"]': { cursor: 'default', }, }, render: ({ props, injector, useObservable }) => { const { items, separator = '/', homeItem, lastItemClickable = false } = props; const locationService = injector.get(LocationService); const [currentPath] = useObservable('currentPath', locationService.onLocationPathChanged); const allItems = homeItem ? [homeItem, ...items] : items; const renderItem = (item, _index, isLast) => { const compiledPath = item.params ? compileRoute(item.path, item.params) : item.path; const isActive = currentPath === compiledPath; if (item.render) { return item.render(item, isActive); } if (isLast && !lastItemClickable) { return (createComponent("span", { "data-active": isActive, "data-non-clickable": "true" }, item.label)); } return (createComponent(NestedRouteLink, { path: compiledPath, "data-active": isActive }, item.label)); }; const renderSeparator = () => { if (typeof separator === 'string') { return createComponent("span", { "data-separator": "true" }, separator); } return separator; }; return (createComponent(createComponent, null, allItems.map((item, index) => (createComponent(createComponent, null, renderItem(item, index, index === allItems.length - 1), index < allItems.length - 1 && renderSeparator()))))); }, }); /** * Creates a type-safe wrapper around Breadcrumb constrained to a specific route tree. * The returned component has the same runtime behavior but narrows paths to only accept * valid route paths, and requires `params` when the route has parameters. * * @typeParam TRoutes - The route tree type (use `typeof yourRoutes`) * @returns A narrowed Breadcrumb component * * @example * ```typescript * const AppBreadcrumb = createBreadcrumb<typeof appRoutes>() * * // Type-safe: only valid paths accepted * <AppBreadcrumb * items={[{ path: '/buttons', label: 'Buttons' }]} * /> * * // TypeScript error: invalid path * <AppBreadcrumb items={[{ path: '/nonexistent', label: 'Error' }]} /> * * // Params required for parameterized routes * <AppBreadcrumb * items={[{ path: '/users/:id', label: 'User', params: { id: '123' } }]} * /> * ``` */ export const createBreadcrumb = () => { return Breadcrumb; }; //# sourceMappingURL=breadcrumb.js.map