@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
122 lines • 4.54 kB
JavaScript
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