@varialkit/breadcrumb 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs.md ADDED
@@ -0,0 +1,78 @@
1
+ # Breadcrumbs
2
+
3
+ The Breadcrumbs component displays the user's location within a hierarchy and provides quick navigation to parent pages.
4
+ It supports custom link rendering for routing frameworks and an optional loading skeleton.
5
+
6
+ ## How to Use
7
+
8
+ ```tsx
9
+ import { Breadcrumbs } from "@solara/breadcrumb";
10
+
11
+ const items = [
12
+ { label: "Home", href: "/" },
13
+ { label: "Projects", href: "/projects" },
14
+ {
15
+ label: "Solara",
16
+ isCurrent: true,
17
+ menuItems: [
18
+ { label: "Copy link", onClick: () => navigator.clipboard.writeText("/projects/solara") },
19
+ { label: "Open settings", onClick: () => console.log("settings clicked") },
20
+ ],
21
+ },
22
+ ];
23
+
24
+ export function Example() {
25
+ return <Breadcrumbs items={items} fontSize="15px" />;
26
+ }
27
+ ```
28
+
29
+ ## Best Practices
30
+
31
+ - **Keep labels short**: Breadcrumbs are scanned quickly; trim long titles.
32
+ - **Use Tooltips for long labels**: If a label is too long, it will be truncated. Use a tooltip to show the full label on hover.
33
+ - **Link parents**: Only the current page should be non-interactive.
34
+ - **Match the hierarchy**: Do not include unrelated pages in the trail.
35
+
36
+ ## Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ | --- | --- | --- | --- |
40
+ | `items` | `BreadcrumbItem[]` | _Required_ | Ordered list of breadcrumb items. See `BreadcrumbItem` below. |
41
+ | `separator` | `ReactNode` | `"/"` | Separator content between items. |
42
+ | `isLoading` | `boolean` | `false` | Shows a loading skeleton. |
43
+ | `renderLink` | `({ item, index, className, children }) => ReactElement` | | Custom link renderer (e.g. Next.js Link). |
44
+ | `onItemClick` | `(item, index, event) => void` | | Callback when a breadcrumb link is clicked. |
45
+ | `className` | `string` | | Custom class name for the root element. |
46
+ | `listClassName` | `string` | | Custom class name for the list. |
47
+ | `itemClassName` | `string` | | Custom class name for each list item. |
48
+ | `linkClassName` | `string` | | Custom class name for breadcrumb links. |
49
+ | `currentClassName` | `string` | | Custom class name for the current breadcrumb. |
50
+ | `limit` | `number` | | Maximum number of breadcrumbs to display. When the number of items exceeds the limit, an ellipsis is shown with a dropdown menu containing the hidden items. |
51
+ | `fontSize` | `string \| number` | | Font size for the breadcrumb items. Accepts any CSS font-size value and applies to the entire trail (including menu triggers). |
52
+ | `style` | `CSSProperties` | | Inline styles for the root element. |
53
+
54
+ ### BreadcrumbItem
55
+
56
+ | Prop | Type | Description |
57
+ | --- | --- | --- |
58
+ | `label` | `ReactNode` | Visible label for the breadcrumb. |
59
+ | `href` | `string` | Optional href for navigable breadcrumbs. |
60
+ | `isCurrent` | `boolean` | Marks the item as the current page. |
61
+ | `disabled` | `boolean` | Disables interaction for this item. |
62
+ | `icon` | `ReactNode` | Optional icon to display with the breadcrumb item. |
63
+ | `iconPosition` | `"left" \| "right"` | Position of the icon relative to the label. |
64
+ | `menuItems` | `BreadcrumbMenuItem[]` | Optional menu actions exposed via a chevron trigger next to the label. |
65
+ | `menuAriaLabel` | `string` | Accessible label for the item's menu trigger. |
66
+
67
+ ### BreadcrumbMenuItem
68
+
69
+ | Prop | Type | Description |
70
+ | --- | --- | --- |
71
+ | `label` | `ReactNode` | Visible label within the item's contextual menu. |
72
+ | `onClick` | `(item, itemIndex, actionIndex, event) => void` | Called when the menu row is clicked. Useful for cell-specific actions like copy or share. |
73
+
74
+ ## Accessibility
75
+
76
+ - Uses `<nav>` and `<ol>` for semantic navigation.
77
+ - Marks the current page with `aria-current="page"`.
78
+ - Links support keyboard focus styling out of the box.
package/examples.tsx ADDED
@@ -0,0 +1,182 @@
1
+ import React from "react";
2
+ import { Breadcrumbs } from "./src/Breadcrumbs";
3
+ import type { BreadcrumbsProps } from "./src/Breadcrumbs.types";
4
+
5
+ const baseItems = [
6
+ { label: "Home", href: "/" },
7
+ { label: "Projects", href: "/projects" },
8
+ { label: "Solara", isCurrent: true },
9
+ ];
10
+
11
+ const longLabelItems = [
12
+ { label: "Home", href: "/" },
13
+ { label: "Design System", href: "/design-system" },
14
+ {
15
+ label: "Breadcrumb migration planning with a very long name",
16
+ isCurrent: true,
17
+ },
18
+ ];
19
+
20
+ const iconItems = [
21
+ { label: "Home", href: "/", icon: "🏠" },
22
+ { label: "Projects", href: "/projects", icon: "📁" },
23
+ { label: "Solara", isCurrent: true, icon: "☀️" },
24
+ ];
25
+
26
+ const menuItems = [
27
+ { label: "Home", href: "/" },
28
+ {
29
+ label: "Projects",
30
+ href: "/projects",
31
+ menuItems: [
32
+ { label: "Copy link", onClick: () => navigator.clipboard.writeText("/projects") },
33
+ { label: "Open in new tab", onClick: () => window.open("/projects", "_blank") },
34
+ ],
35
+ },
36
+ {
37
+ label: "Solara",
38
+ isCurrent: true,
39
+ menuItems: [
40
+ { label: "Rename", onClick: () => console.log("rename clicked") },
41
+ { label: "Duplicate", onClick: () => console.log("duplicate clicked") },
42
+ ],
43
+ },
44
+ ];
45
+
46
+ const longTrailItems = [
47
+ { label: "Home", href: "/" },
48
+ { label: "Products", href: "/products" },
49
+ { label: "Electronics", href: "/products/electronics" },
50
+ { label: "Laptops", href: "/products/electronics/laptops" },
51
+ { label: "Gaming Laptops", isCurrent: true },
52
+ ];
53
+
54
+ export const stories = {
55
+ playground: {
56
+ title: "Playground",
57
+ description: "A basic breadcrumb trail with a current page label.",
58
+ render: (props: BreadcrumbsProps) => (
59
+ <Breadcrumbs {...props} items={baseItems} />
60
+ ),
61
+ controls: [
62
+ { name: "separator", type: "text" },
63
+ { name: "isLoading", type: "boolean" },
64
+ { name: "fontSize", type: "text" },
65
+ ],
66
+ initialProps: {
67
+ separator: "/",
68
+ isLoading: false,
69
+ fontSize: "16px",
70
+ },
71
+ },
72
+ withIcons: {
73
+ title: "With Icons",
74
+ description: "Breadcrumbs with icons next to the labels.",
75
+ showProps: false,
76
+ render: () => <Breadcrumbs items={iconItems} />,
77
+ code: `import { Breadcrumbs } from "@solara/breadcrumb";
78
+
79
+ const iconItems = [
80
+ { label: "Home", href: "/", icon: "🏠" },
81
+ { label: "Projects", href: "/projects", icon: "📁" },
82
+ { label: "Solara", isCurrent: true, icon: "☀️" },
83
+ ];
84
+
85
+ export function Example() {
86
+ return <Breadcrumbs items={iconItems} />;
87
+ }
88
+ `,
89
+ },
90
+ limitedItems: {
91
+ title: "Limited Items",
92
+ description: "Limit the number of breadcrumbs shown, with overflow in a menu.",
93
+ showProps: false,
94
+ render: () => <Breadcrumbs items={longTrailItems} limit={4} />,
95
+ code: `import { Breadcrumbs } from "@solara/breadcrumb";
96
+
97
+ const longTrailItems = [
98
+ { label: "Home", href: "/" },
99
+ { label: "Products", href: "/products" },
100
+ { label: "Electronics", href: "/products/electronics" },
101
+ { label: "Laptops", href: "/products/electronics/laptops" },
102
+ { label: "Gaming Laptops", isCurrent: true },
103
+ ];
104
+
105
+ export function Example() {
106
+ return <Breadcrumbs items={longTrailItems} limit={4} />;
107
+ }
108
+ `,
109
+ },
110
+ longLabels: {
111
+ title: "Long Labels",
112
+ description: "Long labels truncate to keep the row compact. Hover to see the full label.",
113
+ showProps: false,
114
+ render: () => <Breadcrumbs items={longLabelItems} />,
115
+ code: `import { Breadcrumbs } from "@solara/breadcrumb";
116
+
117
+ const longLabelItems = [
118
+ { label: "Home", href: "/" },
119
+ { label: "Design System", href: "/design-system" },
120
+ {
121
+ label: "Breadcrumb migration planning with a very long name",
122
+ isCurrent: true,
123
+ },
124
+ ];
125
+
126
+ export function Example() {
127
+ return <Breadcrumbs items={longLabelItems} />;
128
+ }
129
+ `,
130
+ },
131
+ loading: {
132
+ title: "Loading",
133
+ description: "Use the loading skeleton while the trail is being resolved.",
134
+ showProps: false,
135
+ render: () => <Breadcrumbs items={baseItems} isLoading />,
136
+ code: `import { Breadcrumbs } from "@solara/breadcrumb";
137
+
138
+ const baseItems = [
139
+ { label: "Home", href: "/" },
140
+ { label: "Projects", href: "/projects" },
141
+ { label: "Solara", isCurrent: true },
142
+ ];
143
+
144
+ export function Example() {
145
+ return <Breadcrumbs items={baseItems} isLoading />;
146
+ }
147
+ `,
148
+ },
149
+ withItemMenus: {
150
+ title: "Item Menus",
151
+ description:
152
+ "Expose cell-level actions from a chevron that sits inline with each breadcrumb label. Font size is inherited by the trigger as well.",
153
+ showProps: false,
154
+ render: () => <Breadcrumbs items={menuItems} fontSize="15px" />,
155
+ code: `import { Breadcrumbs } from "@solara/breadcrumb";
156
+
157
+ const items = [
158
+ { label: "Home", href: "/" },
159
+ {
160
+ label: "Projects",
161
+ href: "/projects",
162
+ menuItems: [
163
+ { label: "Copy link", onClick: () => navigator.clipboard.writeText("/projects") },
164
+ { label: "Open in new tab", onClick: () => window.open("/projects", "_blank") },
165
+ ],
166
+ },
167
+ {
168
+ label: "Solara",
169
+ isCurrent: true,
170
+ menuItems: [
171
+ { label: "Rename", onClick: () => console.log("rename clicked") },
172
+ { label: "Duplicate", onClick: () => console.log("duplicate clicked") },
173
+ ],
174
+ },
175
+ ];
176
+
177
+ export function Example() {
178
+ return <Breadcrumbs items={items} fontSize="15px" />;
179
+ }
180
+ `,
181
+ },
182
+ };
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@varialkit/breadcrumb",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./examples": "./examples.tsx"
10
+ },
11
+ "files": [
12
+ "src",
13
+ "docs.md",
14
+ "examples.tsx"
15
+ ],
16
+ "peerDependencies": {
17
+ "react": "^19.0.0",
18
+ "@varialkit/tooltip": "0.1.1",
19
+ "@varialkit/menu": "0.1.1"
20
+ },
21
+ "devDependencies": {
22
+ "@types/react": "19.0.10",
23
+ "react": "19.0.0"
24
+ }
25
+ }
@@ -0,0 +1,174 @@
1
+ .solara-breadcrumbs {
2
+ display: flex;
3
+ align-items: center;
4
+ font-family: var(--font-body);
5
+ font-size: var(--font-size-body-scaled);
6
+ line-height: var(--line-height-body-scaled);
7
+ color: var(--color-on-surface);
8
+ }
9
+
10
+ .solara-breadcrumbs__list {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: calc(var(--space-1) * var(--spacing-multiplier));
14
+ list-style: none;
15
+ margin: 0;
16
+ padding: 0;
17
+ }
18
+
19
+ .solara-breadcrumbs__item {
20
+ display: flex;
21
+ align-items: center;
22
+ min-width: 0;
23
+ }
24
+
25
+ .solara-breadcrumbs__item-content {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: calc(var(--space-1) * var(--spacing-multiplier));
29
+ min-width: 0;
30
+ }
31
+
32
+ .solara-breadcrumbs__separator {
33
+ color: var(--color-text-secondary);
34
+ margin: 0 calc(var(--space-1) * var(--spacing-multiplier));
35
+ user-select: none;
36
+ }
37
+
38
+ .solara-breadcrumbs__link,
39
+ .solara-breadcrumbs__current {
40
+ max-width: 16rem;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ }
45
+
46
+ .solara-breadcrumbs__link {
47
+ color: var(--color-text-secondary);
48
+ text-decoration: none;
49
+ transition: color 0.15s ease, text-decoration-color 0.15s ease;
50
+
51
+ &:hover {
52
+ color: var(--color-on-surface);
53
+ text-decoration: underline;
54
+ }
55
+
56
+ &:focus-visible {
57
+ outline: none;
58
+ box-shadow: 0 0 0 3px var(--color-focus-halo);
59
+ border-radius: var(--radius-1);
60
+ }
61
+ }
62
+
63
+ .solara-breadcrumbs__link--disabled {
64
+ color: var(--color-on-surface-disabled);
65
+ pointer-events: none;
66
+ }
67
+
68
+ .solara-breadcrumbs__current {
69
+ color: var(--color-on-surface);
70
+ font-weight: 600;
71
+ }
72
+
73
+ .solara-breadcrumbs--loading {
74
+ color: var(--color-text-secondary);
75
+ }
76
+
77
+ .solara-breadcrumbs__skeleton {
78
+ display: flex;
79
+ align-items: center;
80
+ }
81
+
82
+ .solara-breadcrumbs__skeleton-block {
83
+ display: inline-block;
84
+ height: 0.9em;
85
+ border-radius: 999px;
86
+ background: var(--color-surface-300);
87
+ animation: solara-breadcrumbs-pulse 1.4s ease-in-out infinite;
88
+ }
89
+
90
+ .solara-breadcrumbs__skeleton-block--short {
91
+ width: 4.5rem;
92
+ }
93
+
94
+ .solara-breadcrumbs__skeleton-block--long {
95
+ width: 7.5rem;
96
+ }
97
+
98
+ @keyframes solara-breadcrumbs-pulse {
99
+ 0% {
100
+ opacity: 0.6;
101
+ }
102
+ 50% {
103
+ opacity: 1;
104
+ }
105
+ 100% {
106
+ opacity: 0.6;
107
+ }
108
+ }
109
+
110
+ .solara-breadcrumbs__icon {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ margin-right: 0.25em;
114
+ }
115
+
116
+ .solara-breadcrumbs__label-wrapper {
117
+ display: inline-block;
118
+ max-width: 100%;
119
+ vertical-align: middle; // Aligns the tooltip wrapper correctly
120
+ }
121
+
122
+ .solara-breadcrumbs__label {
123
+ display: block;
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ white-space: nowrap;
127
+ }
128
+
129
+ .solara-breadcrumbs__label + .solara-breadcrumbs__icon {
130
+ margin-right: 0;
131
+ margin-left: 0.25em;
132
+ }
133
+
134
+ .solara-breadcrumbs__ellipsis {
135
+ background: none;
136
+ border: none;
137
+ color: var(--color-text-secondary);
138
+ cursor: pointer;
139
+ padding: 0;
140
+ font-size: inherit;
141
+
142
+ &:hover {
143
+ color: var(--color-on-surface);
144
+ }
145
+ }
146
+
147
+ .solara-breadcrumbs__menu-trigger {
148
+ display: inline-flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ background: none;
152
+ border: none;
153
+ color: var(--color-text-secondary);
154
+ cursor: pointer;
155
+ padding: 0;
156
+ font-size: inherit;
157
+ line-height: 1;
158
+ margin-left: calc(var(--space-1) * var(--spacing-multiplier));
159
+
160
+ &:hover {
161
+ color: var(--color-on-surface);
162
+ }
163
+
164
+ &:focus-visible {
165
+ outline: none;
166
+ box-shadow: 0 0 0 3px var(--color-focus-halo);
167
+ border-radius: var(--radius-1);
168
+ }
169
+ }
170
+
171
+ .solara-breadcrumbs__menu-icon {
172
+ display: inline-block;
173
+ transform: translateY(1px);
174
+ }
@@ -0,0 +1,254 @@
1
+ import React from "react";
2
+ import { Menu, MenuDropdown, MenuRow } from "@solara/menu";
3
+ import { Tooltip } from "@solara/tooltip";
4
+ import type {
5
+ BreadcrumbItem,
6
+ BreadcrumbMenuItem,
7
+ BreadcrumbsProps,
8
+ } from "./Breadcrumbs.types";
9
+ import "./Breadcrumbs.scss";
10
+
11
+ const DEFAULT_SEPARATOR = "/";
12
+
13
+ function renderLabel(item: BreadcrumbItem) {
14
+ const icon = item.icon ? (
15
+ <span className="solara-breadcrumbs__icon">{item.icon}</span>
16
+ ) : null;
17
+
18
+ const label = (
19
+ <Tooltip
20
+ content={item.label as string}
21
+ containerClassName="solara-breadcrumbs__label-wrapper"
22
+ >
23
+ <span className="solara-breadcrumbs__label">{item.label}</span>
24
+ </Tooltip>
25
+ );
26
+
27
+ if (item.iconPosition === "right") {
28
+ return (
29
+ <>
30
+ {label}
31
+ {icon}
32
+ </>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <>
38
+ {icon}
39
+ {label}
40
+ </>
41
+ );
42
+ }
43
+
44
+ function renderMenu(
45
+ item: BreadcrumbItem,
46
+ itemIndex: number,
47
+ menuItems: BreadcrumbMenuItem[]
48
+ ) {
49
+ if (!menuItems.length) return null;
50
+
51
+ const ariaLabelFallback =
52
+ item.menuAriaLabel ??
53
+ (typeof item.label === "string"
54
+ ? `Open menu for ${item.label}`
55
+ : "Open breadcrumb menu");
56
+
57
+ return (
58
+ <MenuDropdown
59
+ trigger={
60
+ <button
61
+ type="button"
62
+ aria-label={ariaLabelFallback}
63
+ className="solara-breadcrumbs__menu-trigger"
64
+ >
65
+ <span aria-hidden="true" className="solara-breadcrumbs__menu-icon">
66
+
67
+ </span>
68
+ </button>
69
+ }
70
+ >
71
+ <Menu>
72
+ {menuItems.map((menuItem: BreadcrumbMenuItem, actionIndex) => (
73
+ <MenuRow
74
+ key={actionIndex}
75
+ onClick={(event: React.MouseEvent<HTMLButtonElement>) =>
76
+ menuItem.onClick?.(item, itemIndex, actionIndex, event)
77
+ }
78
+ >
79
+ {menuItem.label}
80
+ </MenuRow>
81
+ ))}
82
+ </Menu>
83
+ </MenuDropdown>
84
+ );
85
+ }
86
+
87
+ export function Breadcrumbs({
88
+ items,
89
+ separator = DEFAULT_SEPARATOR,
90
+ isLoading = false,
91
+ renderLink,
92
+ onItemClick,
93
+ className,
94
+ listClassName,
95
+ itemClassName,
96
+ linkClassName,
97
+ currentClassName,
98
+ limit,
99
+ fontSize,
100
+ style,
101
+ ...props
102
+ }: BreadcrumbsProps) {
103
+ if (isLoading) {
104
+ return (
105
+ <nav
106
+ className={["solara-breadcrumbs", "solara-breadcrumbs--loading", className]
107
+ .filter(Boolean)
108
+ .join(" ")}
109
+ aria-label="Breadcrumb"
110
+ aria-busy="true"
111
+ style={{ fontSize, ...style }}
112
+ {...props}
113
+ >
114
+ <div className="solara-breadcrumbs__skeleton" aria-hidden="true">
115
+ <span className="solara-breadcrumbs__skeleton-block solara-breadcrumbs__skeleton-block--short" />
116
+ <span className="solara-breadcrumbs__separator" aria-hidden="true">
117
+ {separator}
118
+ </span>
119
+ <span className="solara-breadcrumbs__skeleton-block solara-breadcrumbs__skeleton-block--long" />
120
+ </div>
121
+ </nav>
122
+ );
123
+ }
124
+
125
+ if (!items || items.length === 0) {
126
+ return null;
127
+ }
128
+
129
+ let displayItems = items;
130
+ let overflowItems: BreadcrumbItem[] = [];
131
+
132
+ if (limit && items.length > limit) {
133
+ const start = items.slice(0, 1);
134
+ const end = items.slice(items.length - (limit - 2));
135
+ overflowItems = items.slice(1, items.length - (limit - 2));
136
+ const ellipsisItem: BreadcrumbItem = {
137
+ label: "...",
138
+ disabled: true,
139
+ };
140
+ displayItems = [...start, ellipsisItem, ...end];
141
+ }
142
+
143
+ const listClasses = ["solara-breadcrumbs__list", listClassName]
144
+ .filter(Boolean)
145
+ .join(" ");
146
+
147
+ return (
148
+ <nav
149
+ className={["solara-breadcrumbs", className].filter(Boolean).join(" ")}
150
+ aria-label="Breadcrumb"
151
+ style={{ fontSize, ...style }}
152
+ {...props}
153
+ >
154
+ <ol className={listClasses}>
155
+ {displayItems.map((item, index) => {
156
+ if (item.label === "...") {
157
+ return (
158
+ <li key="ellipsis" className="solara-breadcrumbs__item">
159
+ <span className="solara-breadcrumbs__separator" aria-hidden="true">
160
+ {separator}
161
+ </span>
162
+ <MenuDropdown
163
+ trigger={<button className="solara-breadcrumbs__ellipsis">...</button>}
164
+ >
165
+ <Menu>
166
+ {overflowItems.map((overflowItem, overflowIndex) => (
167
+ <MenuRow
168
+ key={overflowIndex}
169
+ onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
170
+ onItemClick?.(overflowItem, 1 + overflowIndex, e)
171
+ }
172
+ >
173
+ {overflowItem.label}
174
+ </MenuRow>
175
+ ))}
176
+ </Menu>
177
+ </MenuDropdown>
178
+ </li>
179
+ );
180
+ }
181
+
182
+ const isCurrent = Boolean(item.isCurrent) || index === displayItems.length - 1;
183
+ const isDisabled = Boolean(item.disabled);
184
+ const itemClasses = ["solara-breadcrumbs__item", itemClassName]
185
+ .filter(Boolean)
186
+ .join(" ");
187
+
188
+ const linkClasses = [
189
+ "solara-breadcrumbs__link",
190
+ isDisabled ? "solara-breadcrumbs__link--disabled" : null,
191
+ linkClassName,
192
+ ]
193
+ .filter(Boolean)
194
+ .join(" ");
195
+
196
+ const currentClasses = [
197
+ "solara-breadcrumbs__current",
198
+ currentClassName,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(" ");
202
+
203
+ const label = renderLabel(item);
204
+ const menu = item.menuItems
205
+ ? renderMenu(item, index, item.menuItems)
206
+ : null;
207
+
208
+ const handleClick = (event: React.MouseEvent<HTMLElement>) => {
209
+ if (isDisabled) {
210
+ event.preventDefault();
211
+ return;
212
+ }
213
+ if (onItemClick) onItemClick(item, index, event);
214
+ };
215
+
216
+ const content = isCurrent || !item.href || isDisabled ? (
217
+ <span className={currentClasses} aria-current={isCurrent ? "page" : undefined}>
218
+ {label}
219
+ </span>
220
+ ) : renderLink ? (
221
+ renderLink({
222
+ item,
223
+ index,
224
+ className: linkClasses,
225
+ children: label,
226
+ })
227
+ ) : (
228
+ <a
229
+ href={item.href}
230
+ className={linkClasses}
231
+ onClick={handleClick}
232
+ >
233
+ {label}
234
+ </a>
235
+ );
236
+
237
+ return (
238
+ <li key={`breadcrumb-${index}`} className={itemClasses}>
239
+ {index > 0 ? (
240
+ <span className="solara-breadcrumbs__separator" aria-hidden="true">
241
+ {separator}
242
+ </span>
243
+ ) : null}
244
+ <div className="solara-breadcrumbs__item-content">
245
+ {content}
246
+ {menu}
247
+ </div>
248
+ </li>
249
+ );
250
+ })}
251
+ </ol>
252
+ </nav>
253
+ );
254
+ }
@@ -0,0 +1,70 @@
1
+ import type React from "react";
2
+
3
+ export type BreadcrumbMenuItem = {
4
+ /** Visible label within the item's contextual menu. */
5
+ label: React.ReactNode;
6
+ /** Called when the menu row is clicked. */
7
+ onClick?: (
8
+ item: BreadcrumbItem,
9
+ itemIndex: number,
10
+ actionIndex: number,
11
+ event: React.MouseEvent<HTMLButtonElement>
12
+ ) => void;
13
+ };
14
+
15
+ export type BreadcrumbItem = {
16
+ /** Visible label for the breadcrumb. */
17
+ label: React.ReactNode;
18
+ /** Optional href for navigable breadcrumbs. */
19
+ href?: string;
20
+ /** Marks the item as the current page. */
21
+ isCurrent?: boolean;
22
+ /** Disables interaction for this item. */
23
+ disabled?: boolean;
24
+ /** Optional icon to display with the breadcrumb item. */
25
+ icon?: React.ReactNode;
26
+ /** Position of the icon relative to the label. */
27
+ iconPosition?: "left" | "right";
28
+ /** Optional menu items exposed via a chevron trigger next to the label. */
29
+ menuItems?: BreadcrumbMenuItem[];
30
+ /** Accessible label for the per-item menu trigger. */
31
+ menuAriaLabel?: string;
32
+ };
33
+
34
+ export type BreadcrumbLinkRenderer = (options: {
35
+ item: BreadcrumbItem;
36
+ index: number;
37
+ className: string;
38
+ children: React.ReactNode;
39
+ }) => React.ReactElement;
40
+
41
+ export type BreadcrumbsProps = React.HTMLAttributes<HTMLElement> & {
42
+ /** Ordered list of breadcrumb items. */
43
+ items: BreadcrumbItem[];
44
+ /** Separator content between items. */
45
+ separator?: React.ReactNode;
46
+ /** Displays a loading skeleton instead of items. */
47
+ isLoading?: boolean;
48
+ /** Optional renderer for links (e.g. Next.js Link). */
49
+ renderLink?: BreadcrumbLinkRenderer;
50
+ /** Callback when a breadcrumb link is clicked. */
51
+ onItemClick?: (
52
+ item: BreadcrumbItem,
53
+ index: number,
54
+ event: React.MouseEvent<HTMLElement>
55
+ ) => void;
56
+ /** Custom class name for the list element. */
57
+ listClassName?: string;
58
+ /** Custom class name applied to each list item. */
59
+ itemClassName?: string;
60
+ /** Custom class name for breadcrumb links. */
61
+ linkClassName?: string;
62
+ /** Custom class name for the current breadcrumb label. */
63
+ currentClassName?: string;
64
+ /** Maximum number of breadcrumbs to display. */
65
+ limit?: number;
66
+ /** Font size for the breadcrumb items. Accepts any CSS font-size value. */
67
+ fontSize?: string | number;
68
+ /** Inline styles for the root element. */
69
+ style?: React.CSSProperties;
70
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Breadcrumbs } from "./Breadcrumbs";
2
+ export type { BreadcrumbItem, BreadcrumbLinkRenderer, BreadcrumbsProps } from "./Breadcrumbs.types";