@papernote/ui 1.7.0 → 1.7.2
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/dist/components/Breadcrumbs.d.ts +66 -0
- package/dist/components/Breadcrumbs.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +68 -2
- package/dist/index.esm.js +106 -9
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +105 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/Breadcrumbs.tsx +166 -21
- package/src/components/index.ts +2 -2
package/package.json
CHANGED
|
@@ -1,19 +1,133 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Link } from 'react-router-dom';
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
3
3
|
import { ChevronRight, Home } from 'lucide-react';
|
|
4
4
|
|
|
5
|
+
/** State passed during breadcrumb navigation */
|
|
6
|
+
export interface BreadcrumbNavigationState {
|
|
7
|
+
/** Unique timestamp to detect navigation, even to the same route */
|
|
8
|
+
breadcrumbReset: number;
|
|
9
|
+
/** Identifies the source of navigation */
|
|
10
|
+
from: 'breadcrumb';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to detect breadcrumb navigation and trigger callbacks.
|
|
15
|
+
* Use this in host components to reset state when a breadcrumb is clicked.
|
|
16
|
+
*
|
|
17
|
+
* @param onReset - Callback fired when breadcrumb navigation is detected
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* function ProductsPage() {
|
|
21
|
+
* const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
|
|
22
|
+
*
|
|
23
|
+
* // Automatically reset to list view when breadcrumb is clicked
|
|
24
|
+
* useBreadcrumbReset(() => setViewMode('list'));
|
|
25
|
+
*
|
|
26
|
+
* // ... rest of component
|
|
27
|
+
* }
|
|
28
|
+
*/
|
|
29
|
+
export function useBreadcrumbReset(onReset: () => void): void {
|
|
30
|
+
const location = useLocation();
|
|
31
|
+
const lastResetRef = useRef<number | null>(null);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const state = location.state as BreadcrumbNavigationState | null;
|
|
35
|
+
|
|
36
|
+
if (state?.breadcrumbReset && state.breadcrumbReset !== lastResetRef.current) {
|
|
37
|
+
lastResetRef.current = state.breadcrumbReset;
|
|
38
|
+
onReset();
|
|
39
|
+
}
|
|
40
|
+
}, [location.state, onReset]);
|
|
41
|
+
}
|
|
42
|
+
|
|
5
43
|
export interface BreadcrumbItem {
|
|
44
|
+
/** Display text for the breadcrumb */
|
|
6
45
|
label: string;
|
|
46
|
+
/** URL to navigate to. When provided, renders as a Link */
|
|
7
47
|
href?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Optional callback fired when breadcrumb is clicked.
|
|
50
|
+
* Called in addition to navigation when href is provided.
|
|
51
|
+
* Use for custom actions like analytics, state resets, etc.
|
|
52
|
+
*/
|
|
53
|
+
onClick?: () => void;
|
|
54
|
+
/** Optional icon to display before the label */
|
|
8
55
|
icon?: React.ReactNode;
|
|
9
56
|
}
|
|
10
57
|
|
|
11
58
|
export interface BreadcrumbsProps {
|
|
12
59
|
items: BreadcrumbItem[];
|
|
60
|
+
/** Whether to show the home icon link at the start. Default: true */
|
|
13
61
|
showHome?: boolean;
|
|
14
62
|
}
|
|
15
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Breadcrumbs navigation component.
|
|
66
|
+
*
|
|
67
|
+
* When a breadcrumb with href is clicked:
|
|
68
|
+
* - If navigating to a different route: standard navigation occurs
|
|
69
|
+
* - If navigating to the same route: navigation state is updated with a unique key,
|
|
70
|
+
* which can be used by host apps to detect "reset" navigation via useLocation().state
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Basic usage
|
|
74
|
+
* <Breadcrumbs items={[
|
|
75
|
+
* { label: 'Home', href: '/' },
|
|
76
|
+
* { label: 'Products', href: '/products' },
|
|
77
|
+
* { label: 'Widget' } // Current page (no href)
|
|
78
|
+
* ]} />
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Host app detecting breadcrumb navigation for state reset
|
|
82
|
+
* function ProductsPage() {
|
|
83
|
+
* const location = useLocation();
|
|
84
|
+
* const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
|
|
85
|
+
*
|
|
86
|
+
* // Reset to list view when breadcrumb navigation occurs
|
|
87
|
+
* useEffect(() => {
|
|
88
|
+
* if (location.state?.breadcrumbReset) {
|
|
89
|
+
* setViewMode('list');
|
|
90
|
+
* }
|
|
91
|
+
* }, [location.state?.breadcrumbReset]);
|
|
92
|
+
*
|
|
93
|
+
* // ... rest of component
|
|
94
|
+
* }
|
|
95
|
+
*/
|
|
16
96
|
export default function Breadcrumbs({ items, showHome = true }: BreadcrumbsProps) {
|
|
97
|
+
const navigate = useNavigate();
|
|
98
|
+
const location = useLocation();
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Handle breadcrumb click with same-route detection.
|
|
102
|
+
* When clicking a breadcrumb that points to the current route,
|
|
103
|
+
* we navigate with state to trigger a reset in the host component.
|
|
104
|
+
*/
|
|
105
|
+
const handleBreadcrumbClick = (
|
|
106
|
+
e: React.MouseEvent,
|
|
107
|
+
href: string,
|
|
108
|
+
onClick?: () => void
|
|
109
|
+
) => {
|
|
110
|
+
// Always call onClick if provided (for custom actions)
|
|
111
|
+
onClick?.();
|
|
112
|
+
|
|
113
|
+
// Check if we're navigating to the same base path
|
|
114
|
+
const targetPath = href.split('?')[0].split('#')[0];
|
|
115
|
+
const currentPath = location.pathname;
|
|
116
|
+
|
|
117
|
+
if (targetPath === currentPath) {
|
|
118
|
+
// Same route - prevent default Link behavior and use navigate with state
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
navigate(href, {
|
|
121
|
+
state: {
|
|
122
|
+
breadcrumbReset: Date.now(),
|
|
123
|
+
from: 'breadcrumb'
|
|
124
|
+
},
|
|
125
|
+
replace: true
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// Different route - let the Link handle it normally
|
|
129
|
+
};
|
|
130
|
+
|
|
17
131
|
return (
|
|
18
132
|
<nav aria-label="Breadcrumb" className="flex items-center space-x-2 text-sm">
|
|
19
133
|
{showHome && (
|
|
@@ -22,6 +136,7 @@ export default function Breadcrumbs({ items, showHome = true }: BreadcrumbsProps
|
|
|
22
136
|
to="/"
|
|
23
137
|
className="text-ink-500 hover:text-ink-900 transition-colors"
|
|
24
138
|
aria-label="Home"
|
|
139
|
+
onClick={(e) => handleBreadcrumbClick(e, '/')}
|
|
25
140
|
>
|
|
26
141
|
<Home className="h-4 w-4" />
|
|
27
142
|
</Link>
|
|
@@ -32,33 +147,63 @@ export default function Breadcrumbs({ items, showHome = true }: BreadcrumbsProps
|
|
|
32
147
|
{items.map((item, index) => {
|
|
33
148
|
const isLast = index === items.length - 1;
|
|
34
149
|
const isActive = isLast;
|
|
150
|
+
const content = (
|
|
151
|
+
<>
|
|
152
|
+
{item.icon && <span className="flex-shrink-0">{item.icon}</span>}
|
|
153
|
+
<span>{item.label}</span>
|
|
154
|
+
</>
|
|
155
|
+
);
|
|
35
156
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
157
|
+
const renderBreadcrumb = () => {
|
|
158
|
+
// Active item (last item) - always render as non-clickable span
|
|
159
|
+
if (isActive) {
|
|
160
|
+
return (
|
|
161
|
+
<span
|
|
162
|
+
className="flex items-center gap-2 px-2 py-1 rounded-md bg-accent-50 text-accent-900 font-semibold transition-colors"
|
|
163
|
+
aria-current="page"
|
|
164
|
+
>
|
|
165
|
+
{content}
|
|
166
|
+
</span>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Has href - render as Link with same-route detection
|
|
171
|
+
if (item.href) {
|
|
172
|
+
return (
|
|
39
173
|
<Link
|
|
40
174
|
to={item.href}
|
|
175
|
+
onClick={(e) => handleBreadcrumbClick(e, item.href!, item.onClick)}
|
|
41
176
|
className="flex items-center gap-2 text-ink-500 hover:text-ink-900 hover:underline transition-colors"
|
|
42
177
|
>
|
|
43
|
-
{
|
|
44
|
-
<span>{item.label}</span>
|
|
178
|
+
{content}
|
|
45
179
|
</Link>
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Only onClick (no href) - render as button
|
|
184
|
+
if (item.onClick) {
|
|
185
|
+
return (
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
onClick={item.onClick}
|
|
189
|
+
className="flex items-center gap-2 text-ink-500 hover:text-ink-900 hover:underline transition-colors bg-transparent border-none cursor-pointer p-0"
|
|
56
190
|
>
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
191
|
+
{content}
|
|
192
|
+
</button>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Neither href nor onClick - render as non-clickable span
|
|
197
|
+
return (
|
|
198
|
+
<span className="flex items-center gap-2 text-ink-700 font-medium">
|
|
199
|
+
{content}
|
|
200
|
+
</span>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
61
203
|
|
|
204
|
+
return (
|
|
205
|
+
<React.Fragment key={index}>
|
|
206
|
+
{renderBreadcrumb()}
|
|
62
207
|
{!isLast && <ChevronRight className="h-4 w-4 text-ink-400" />}
|
|
63
208
|
</React.Fragment>
|
|
64
209
|
);
|
package/src/components/index.ts
CHANGED
|
@@ -201,8 +201,8 @@ export type { ExpandablePanelProps } from './ExpandablePanel';
|
|
|
201
201
|
export { Show, Hide } from './ResponsiveUtilities';
|
|
202
202
|
|
|
203
203
|
// Navigation Components
|
|
204
|
-
export { default as Breadcrumbs } from './Breadcrumbs';
|
|
205
|
-
export type { BreadcrumbsProps, BreadcrumbItem } from './Breadcrumbs';
|
|
204
|
+
export { default as Breadcrumbs, useBreadcrumbReset } from './Breadcrumbs';
|
|
205
|
+
export type { BreadcrumbsProps, BreadcrumbItem, BreadcrumbNavigationState } from './Breadcrumbs';
|
|
206
206
|
|
|
207
207
|
export { default as Tabs } from './Tabs';
|
|
208
208
|
export type { TabsProps, Tab } from './Tabs';
|