@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "type": "module",
5
5
  "description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
6
6
  "main": "dist/index.js",
@@ -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
- return (
37
- <React.Fragment key={index}>
38
- {item.href && !isActive ? (
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
- {item.icon && <span className="flex-shrink-0">{item.icon}</span>}
44
- <span>{item.label}</span>
178
+ {content}
45
179
  </Link>
46
- ) : (
47
- <span
48
- className={`
49
- flex items-center gap-2 px-2 py-1 rounded-md transition-colors
50
- ${isActive
51
- ? 'bg-accent-50 text-accent-900 font-semibold'
52
- : 'text-ink-700 font-medium'
53
- }
54
- `}
55
- aria-current={isActive ? 'page' : undefined}
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
- {item.icon && <span className="flex-shrink-0">{item.icon}</span>}
58
- <span>{item.label}</span>
59
- </span>
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
  );
@@ -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';