Compare commits

..

2 Commits
v0.4.3 ... 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
Maksym Sadovnychyy
55611f8c3b (bugfix): fix Breadcrumb/CookieConsent linkComponent typing for react-router
Some checks are pending
Storybook tests / storybook-tests (push) Waiting to run
2026-08-12 16:42:28 +02:00
7 changed files with 75 additions and 75 deletions

View File

@ -4,6 +4,22 @@ 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
### Fixed
- `Breadcrumb` / `CookieConsent` `linkComponent` typing: `to` is required on the injected link props (and always passed when rendering links), so react-router `Link` is assignable without a host adapter. Exports: `BreadcrumbLinkComponent`, `CookieConsentLinkComponent`.
## [0.4.3] - 2026-08-06 ## [0.4.3] - 2026-08-06
### Added ### Added

View File

@ -1,27 +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>
} }
type BreadcrumbLinkComponent = ComponentType<{
href?: string
to?: string
className?: string
children?: ReactNode
[key: string]: unknown
}>
export interface BreadcrumbProps { export interface BreadcrumbProps {
items: BreadcrumbItem[] items: BreadcrumbItem[]
/** Defaults to `"/"`. */ /** Defaults to `"/"`. */
@ -30,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)
@ -72,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
@ -89,15 +59,13 @@ const Breadcrumb: FC<BreadcrumbProps> = ({
</span> </span>
) : null} ) : null}
{isLink ? ( {isLink && item.to ? (
<LinkComponent <Link
href={item.href ?? (typeof item.to === 'string' ? item.to : undefined)} to={item.to}
to={item.to ?? item.href}
className={linkClassName} className={linkClassName}
{...(item.linkProps ?? {})}
> >
{item.label} {item.label}
</LinkComponent> </Link>
) : ( ) : (
<span <span
className={currentClassName} className={currentClassName}

View File

@ -17,12 +17,15 @@ export interface CookieConsentLink {
linkProps?: Record<string, unknown> linkProps?: Record<string, unknown>
} }
type ConsentLinkComponent = ComponentType<{ /**
* Injectable link surface for SPA routers.
* `to` is always provided when CookieConsent renders a link, so react-router `Link` is assignable.
*/
export type CookieConsentLinkComponent = ComponentType<{
to: string
href?: string href?: string
to?: string
className?: string className?: string
children?: ReactNode children?: ReactNode
[key: string]: unknown
}> }>
export interface CookieConsentProps { export interface CookieConsentProps {
@ -33,13 +36,13 @@ export interface CookieConsentProps {
cookieName?: string cookieName?: string
cookieDays?: number cookieDays?: number
/** Host injects `Link` from react-router (or any anchor-like component). Defaults to `<a>`. */ /** Host injects `Link` from react-router (or any anchor-like component). Defaults to `<a>`. */
linkComponent?: ConsentLinkComponent linkComponent?: CookieConsentLinkComponent
onAccept?: () => void onAccept?: () => void
onDismiss?: () => void onDismiss?: () => void
className?: string className?: string
} }
const DefaultLink: ConsentLinkComponent = ({ const DefaultLink: CookieConsentLinkComponent = ({
href, href,
to, to,
children, children,
@ -108,18 +111,24 @@ const CookieConsent: FC<CookieConsentProps> = ({
{message} {message}
{links.length > 0 ? ( {links.length > 0 ? (
<ul className={'mt-2 flex flex-wrap gap-x-3 gap-y-1'}> <ul className={'mt-2 flex flex-wrap gap-x-3 gap-y-1'}>
{links.map((link, index) => ( {links.map((link, index) => {
const target = link.to ?? link.href
if (!target)
return null
return (
<li key={index}> <li key={index}>
<LinkComponent <LinkComponent
href={link.href ?? (typeof link.to === 'string' ? link.to : undefined)} href={link.href ?? (typeof link.to === 'string' ? link.to : undefined)}
to={link.to ?? link.href} to={target}
className={'text-sky-700 underline hover:text-sky-900'} className={'text-sky-700 underline hover:text-sky-900'}
{...(link.linkProps ?? {})} {...(link.linkProps ?? {})}
> >
{link.label} {link.label}
</LinkComponent> </LinkComponent>
</li> </li>
))} )
})}
</ul> </ul>
) : null} ) : null}
</div> </div>

View File

@ -1,3 +1,7 @@
export { CookieConsent } from './CookieConsent' export { CookieConsent } from './CookieConsent'
export type { CookieConsentProps, CookieConsentLink } from './CookieConsent' export type {
CookieConsentProps,
CookieConsentLink,
CookieConsentLinkComponent,
} from './CookieConsent'
export { getCookie, setCookie } from './cookies' export { getCookie, setCookie } from './cookies'

View File

@ -22,7 +22,11 @@ export type { MasonryProps } from './components/Masonry'
export { LightBox } from './components/LightBox' export { LightBox } from './components/LightBox'
export type { LightBoxProps, LightBoxSlide } from './components/LightBox' export type { LightBoxProps, LightBoxSlide } from './components/LightBox'
export { CookieConsent, getCookie, setCookie } from './components/CookieConsent' export { CookieConsent, getCookie, setCookie } from './components/CookieConsent'
export type { CookieConsentProps, CookieConsentLink } from './components/CookieConsent' export type {
CookieConsentProps,
CookieConsentLink,
CookieConsentLinkComponent,
} from './components/CookieConsent'
export { export {
WhatsAppButton, WhatsAppButton,
buildWhatsAppHref, buildWhatsAppHref,

View File

@ -1,6 +1,6 @@
{ {
"name": "@maks-it.com/webui", "name": "@maks-it.com/webui",
"version": "0.4.3", "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>