Compare commits

...

1 Commits
v0.4.4 ... main

Author SHA1 Message Date
Maksym Sadovnychyy
cd575eb926 (refactor): Breadcrumb uses react-router Link; drop linkComponent injection
Some checks failed
Storybook tests / storybook-tests (push) Has been cancelled
2026-08-12 17:01:44 +02:00
6 changed files with 35 additions and 69 deletions

View File

@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.5] - 2026-08-12
### Changed
- **Breaking:** `Breadcrumb` now renders links with react-router `Link` directly. Dropped injectable `linkComponent`, `href`, and `linkProps` — use `to` on trail items only. Default link/current/separator colors use slate tones.
### Removed
- `BreadcrumbLinkComponent` export (no longer needed).
## [0.4.4] - 2026-08-12 ## [0.4.4] - 2026-08-12
### Fixed ### Fixed

View File

@ -1,30 +1,12 @@
import { import { type FC, type ReactNode } from 'react'
type ComponentType, import { Link } from 'react-router-dom'
type FC,
type ReactNode,
} from 'react'
export interface BreadcrumbItem { export interface BreadcrumbItem {
label: ReactNode label: ReactNode
/** Used by default `<a>` and as fallback for router links. */ /** Omit on the current page (last item). */
href?: string
/** Preferred when injecting react-router `Link`. */
to?: string to?: string
/** Extra props passed to the injected link component. */
linkProps?: Record<string, unknown>
} }
/**
* Injectable link surface for SPA routers.
* `to` is always provided when Breadcrumb renders a link, so react-router `Link` is assignable.
*/
export type BreadcrumbLinkComponent = ComponentType<{
to: string
href?: string
className?: string
children?: ReactNode
}>
export interface BreadcrumbProps { export interface BreadcrumbProps {
items: BreadcrumbItem[] items: BreadcrumbItem[]
/** Defaults to `"/"`. */ /** Defaults to `"/"`. */
@ -33,35 +15,21 @@ export interface BreadcrumbProps {
linkClassName?: string linkClassName?: string
currentClassName?: string currentClassName?: string
separatorClassName?: string separatorClassName?: string
/** Host injects `Link` from react-router (or any anchor-like component). Defaults to `<a>`. */
linkComponent?: BreadcrumbLinkComponent
/** Accessible name for the nav landmark. Defaults to `"Breadcrumb"`. */ /** Accessible name for the nav landmark. Defaults to `"Breadcrumb"`. */
label?: string label?: string
} }
const DefaultLink: BreadcrumbLinkComponent = ({
href,
to,
children,
...rest
}) => (
<a href={href ?? to} {...rest}>
{children}
</a>
)
/** /**
* Presentational page trail. Use links/`span` only never headings. * Page trail (`nav` + `ol`). Links use react-router `Link`.
* Keep a single page `h1` on `FormHeader` or the page title. * Never uses headings keep a single page `h1` on `FormHeader` or the page title.
*/ */
const Breadcrumb: FC<BreadcrumbProps> = ({ const Breadcrumb: FC<BreadcrumbProps> = ({
items, items,
separator = '/', separator = '/',
className = '', className = '',
linkClassName = 'text-sky-700 hover:text-sky-900 hover:underline', linkClassName = 'text-slate-500 hover:text-slate-800 hover:underline',
currentClassName = 'text-gray-700', currentClassName = 'text-slate-700',
separatorClassName = 'text-gray-400', separatorClassName = 'text-slate-400',
linkComponent: LinkComponent = DefaultLink,
label = 'Breadcrumb', label = 'Breadcrumb',
}) => { }) => {
if (items.length === 0) if (items.length === 0)
@ -75,8 +43,7 @@ const Breadcrumb: FC<BreadcrumbProps> = ({
<ol className={'flex flex-wrap items-center gap-x-2 gap-y-1'}> <ol className={'flex flex-wrap items-center gap-x-2 gap-y-1'}>
{items.map((item, index) => { {items.map((item, index) => {
const isLast = index === items.length - 1 const isLast = index === items.length - 1
const target = item.to ?? item.href const isLink = !isLast && Boolean(item.to)
const isLink = !isLast && Boolean(target)
return ( return (
<li <li
@ -92,15 +59,13 @@ const Breadcrumb: FC<BreadcrumbProps> = ({
</span> </span>
) : null} ) : null}
{isLink && target ? ( {isLink && item.to ? (
<LinkComponent <Link
href={item.href ?? (typeof item.to === 'string' ? item.to : undefined)} to={item.to}
to={target}
className={linkClassName} className={linkClassName}
{...(item.linkProps ?? {})}
> >
{item.label} {item.label}
</LinkComponent> </Link>
) : ( ) : (
<span <span
className={currentClassName} className={currentClassName}

View File

@ -1,6 +1,2 @@
export { Breadcrumb } from './Breadcrumb' export { Breadcrumb } from './Breadcrumb'
export type { export type { BreadcrumbProps, BreadcrumbItem } from './Breadcrumb'
BreadcrumbProps,
BreadcrumbItem,
BreadcrumbLinkComponent,
} from './Breadcrumb'

View File

@ -4,11 +4,7 @@ export { SecretComponent } from './components/editors/SecretComponent'
export type { SecretDataSource, SecretComponentProps } from './components/editors/SecretComponent' export type { SecretDataSource, SecretComponentProps } from './components/editors/SecretComponent'
export { FormContainer, FormContent, FormFooter, FormHeader } from './components/FormLayout' export { FormContainer, FormContent, FormFooter, FormHeader } from './components/FormLayout'
export { Breadcrumb } from './components/Breadcrumb' export { Breadcrumb } from './components/Breadcrumb'
export type { export type { BreadcrumbProps, BreadcrumbItem } from './components/Breadcrumb'
BreadcrumbProps,
BreadcrumbItem,
BreadcrumbLinkComponent,
} from './components/Breadcrumb'
export { Offcanvas } from './components/Offcanvas' export { Offcanvas } from './components/Offcanvas'
export { Modal, ConfirmDialog } from './components/Modal' export { Modal, ConfirmDialog } from './components/Modal'
export type { ModalProps, ModalSize, ConfirmDialogProps } from './components/Modal' export type { ModalProps, ModalSize, ConfirmDialogProps } from './components/Modal'

View File

@ -1,6 +1,6 @@
{ {
"name": "@maks-it.com/webui", "name": "@maks-it.com/webui",
"version": "0.4.4", "version": "0.4.5",
"description": "Shared contracts, utilities, and React components for MaksIT WebUI apps", "description": "Shared contracts, utilities, and React components for MaksIT WebUI apps",
"type": "module", "type": "module",
"main": "./dist/index.cjs", "main": "./dist/index.cjs",

View File

@ -11,7 +11,7 @@ const meta = {
docs: { docs: {
description: { description: {
component: component:
'Presentational page trail (`nav` + `ol`). Never uses headings — keep a single page `h1` on `FormHeader` or the page title. Inject `linkComponent` (e.g. react-router `Link`) for SPA navigation.', 'Page trail (`nav` + `ol`) using react-router `Link`. Never uses headings — keep a single page `h1` on `FormHeader` or the page title.',
}, },
}, },
}, },
@ -47,15 +47,14 @@ export const SingleItem: Story = {
export const WithFormHeader: Story = { export const WithFormHeader: Story = {
render: () => ( render: () => (
<div className="space-y-0 border border-gray-200 bg-white"> <div className="space-y-0 border border-gray-200 bg-white">
<div className="bg-gray-50 px-4 py-2"> <Breadcrumb
<Breadcrumb className="bg-gray-50 px-4 py-2"
items={[ items={[
{ label: 'Admin', to: '/admin' }, { label: 'Admin', to: '/admin' },
{ label: 'Shop', to: '/admin/shop' }, { label: 'Shop', to: '/admin/shop' },
{ label: 'Edit item' }, { label: 'Edit item' },
]} ]}
/> />
</div>
<FormHeader>Edit shop item</FormHeader> <FormHeader>Edit shop item</FormHeader>
<div className="p-4 text-sm text-gray-600">Form content</div> <div className="p-4 text-sm text-gray-600">Form content</div>
</div> </div>