@papernote/ui 1.7.1 → 1.7.3

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/styles.css CHANGED
@@ -1575,10 +1575,6 @@ input:checked + .slider:before{
1575
1575
  margin-left: -0.5rem;
1576
1576
  }
1577
1577
 
1578
- .-ml-3{
1579
- margin-left: -0.75rem;
1580
- }
1581
-
1582
1578
  .-ml-px{
1583
1579
  margin-left: -1px;
1584
1580
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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,31 +1,192 @@
1
- import React from 'react';
2
- import { Link } from 'react-router-dom';
1
+ import React, { useEffect, useRef } from 'react';
2
+ import { Link, useNavigate, useLocation, useInRouterContext } 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
+ * Note: This hook requires React Router context. If used outside a Router,
18
+ * it will be a no-op (the callback will never be called).
19
+ *
20
+ * @param onReset - Callback fired when breadcrumb navigation is detected
21
+ *
22
+ * @example
23
+ * function ProductsPage() {
24
+ * const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
25
+ *
26
+ * // Automatically reset to list view when breadcrumb is clicked
27
+ * useBreadcrumbReset(() => setViewMode('list'));
28
+ *
29
+ * // ... rest of component
30
+ * }
31
+ */
32
+ export function useBreadcrumbReset(onReset: () => void): void {
33
+ const inRouter = useInRouterContext();
34
+ const lastResetRef = useRef<number | null>(null);
35
+
36
+ // Only use useLocation when inside Router context
37
+ const location = inRouter ? useLocation() : null;
38
+
39
+ useEffect(() => {
40
+ if (!location) return;
41
+
42
+ const state = location.state as BreadcrumbNavigationState | null;
43
+
44
+ if (state?.breadcrumbReset && state.breadcrumbReset !== lastResetRef.current) {
45
+ lastResetRef.current = state.breadcrumbReset;
46
+ onReset();
47
+ }
48
+ }, [location?.state, onReset, location]);
49
+ }
50
+
5
51
  export interface BreadcrumbItem {
52
+ /** Display text for the breadcrumb */
6
53
  label: string;
54
+ /** URL to navigate to. When provided, renders as a Link */
7
55
  href?: string;
56
+ /**
57
+ * Optional callback fired when breadcrumb is clicked.
58
+ * Called in addition to navigation when href is provided.
59
+ * Use for custom actions like analytics, state resets, etc.
60
+ */
8
61
  onClick?: () => void;
62
+ /** Optional icon to display before the label */
9
63
  icon?: React.ReactNode;
10
64
  }
11
65
 
12
66
  export interface BreadcrumbsProps {
13
67
  items: BreadcrumbItem[];
68
+ /** Whether to show the home icon link at the start. Default: true */
14
69
  showHome?: boolean;
15
70
  }
16
71
 
72
+ /**
73
+ * Breadcrumbs navigation component.
74
+ *
75
+ * When a breadcrumb with href is clicked:
76
+ * - If navigating to a different route: standard navigation occurs
77
+ * - If navigating to the same route: navigation state is updated with a unique key,
78
+ * which can be used by host apps to detect "reset" navigation via useLocation().state
79
+ *
80
+ * @example
81
+ * // Basic usage
82
+ * <Breadcrumbs items={[
83
+ * { label: 'Home', href: '/' },
84
+ * { label: 'Products', href: '/products' },
85
+ * { label: 'Widget' } // Current page (no href)
86
+ * ]} />
87
+ *
88
+ * @example
89
+ * // Host app detecting breadcrumb navigation for state reset
90
+ * function ProductsPage() {
91
+ * const location = useLocation();
92
+ * const [viewMode, setViewMode] = useState<'list' | 'detail'>('list');
93
+ *
94
+ * // Reset to list view when breadcrumb navigation occurs
95
+ * useEffect(() => {
96
+ * if (location.state?.breadcrumbReset) {
97
+ * setViewMode('list');
98
+ * }
99
+ * }, [location.state?.breadcrumbReset]);
100
+ *
101
+ * // ... rest of component
102
+ * }
103
+ */
17
104
  export default function Breadcrumbs({ items, showHome = true }: BreadcrumbsProps) {
105
+ const inRouter = useInRouterContext();
106
+
107
+ // Only use router hooks when inside Router context
108
+ const navigate = inRouter ? useNavigate() : null;
109
+ const location = inRouter ? useLocation() : null;
110
+
111
+ /**
112
+ * Handle breadcrumb click with same-route detection.
113
+ * When clicking a breadcrumb that points to the current route,
114
+ * we navigate with state to trigger a reset in the host component.
115
+ */
116
+ const handleBreadcrumbClick = (
117
+ e: React.MouseEvent,
118
+ href: string,
119
+ onClick?: () => void
120
+ ) => {
121
+ // Always call onClick if provided (for custom actions)
122
+ onClick?.();
123
+
124
+ // If not in router context, let the browser handle navigation naturally
125
+ if (!navigate || !location) return;
126
+
127
+ // Check if we're navigating to the same base path
128
+ const targetPath = href.split('?')[0].split('#')[0];
129
+ const currentPath = location.pathname;
130
+
131
+ if (targetPath === currentPath) {
132
+ // Same route - prevent default Link behavior and use navigate with state
133
+ e.preventDefault();
134
+ navigate(href, {
135
+ state: {
136
+ breadcrumbReset: Date.now(),
137
+ from: 'breadcrumb'
138
+ },
139
+ replace: true
140
+ });
141
+ }
142
+ // Different route - let the Link handle it normally
143
+ };
144
+
145
+ // Helper to render a link - uses Link when in router, <a> when not
146
+ const renderLink = (
147
+ href: string,
148
+ children: React.ReactNode,
149
+ className: string,
150
+ onClick?: (e: React.MouseEvent) => void,
151
+ ariaLabel?: string
152
+ ) => {
153
+ if (inRouter) {
154
+ return (
155
+ <Link
156
+ to={href}
157
+ className={className}
158
+ onClick={onClick}
159
+ aria-label={ariaLabel}
160
+ >
161
+ {children}
162
+ </Link>
163
+ );
164
+ }
165
+ return (
166
+ <a
167
+ href={href}
168
+ className={className}
169
+ onClick={(e) => {
170
+ onClick?.(e);
171
+ }}
172
+ aria-label={ariaLabel}
173
+ >
174
+ {children}
175
+ </a>
176
+ );
177
+ };
178
+
18
179
  return (
19
180
  <nav aria-label="Breadcrumb" className="flex items-center space-x-2 text-sm">
20
181
  {showHome && (
21
182
  <>
22
- <Link
23
- to="/"
24
- className="text-ink-500 hover:text-ink-900 transition-colors"
25
- aria-label="Home"
26
- >
27
- <Home className="h-4 w-4" />
28
- </Link>
183
+ {renderLink(
184
+ '/',
185
+ <Home className="h-4 w-4" />,
186
+ 'text-ink-500 hover:text-ink-900 transition-colors',
187
+ (e) => handleBreadcrumbClick(e, '/'),
188
+ 'Home'
189
+ )}
29
190
  {items.length > 0 && <ChevronRight className="h-4 w-4 text-ink-400" />}
30
191
  </>
31
192
  )}
@@ -53,16 +214,13 @@ export default function Breadcrumbs({ items, showHome = true }: BreadcrumbsProps
53
214
  );
54
215
  }
55
216
 
56
- // Has href - render as Link, also call onClick if provided
217
+ // Has href - render as Link (or <a> if no router) with same-route detection
57
218
  if (item.href) {
58
- return (
59
- <Link
60
- to={item.href}
61
- onClick={item.onClick}
62
- className="flex items-center gap-2 text-ink-500 hover:text-ink-900 hover:underline transition-colors"
63
- >
64
- {content}
65
- </Link>
219
+ return renderLink(
220
+ item.href,
221
+ content,
222
+ 'flex items-center gap-2 text-ink-500 hover:text-ink-900 hover:underline transition-colors',
223
+ (e) => handleBreadcrumbClick(e, item.href!, item.onClick)
66
224
  );
67
225
  }
68
226
 
@@ -1,6 +1,6 @@
1
1
 
2
2
  import React, { useState, useMemo } from 'react';
3
- import { ChevronLeft, ChevronRight } from 'lucide-react';
3
+ import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
4
4
 
5
5
  export interface CalendarEvent {
6
6
  date: Date;
@@ -14,6 +14,8 @@ export interface CalendarProps {
14
14
  value?: Date;
15
15
  /** Callback when date is selected */
16
16
  onChange?: (date: Date) => void;
17
+ /** Callback when displayed month changes (via navigation buttons or goToToday) */
18
+ onMonthChange?: (date: Date) => void;
17
19
  /** Events to display on calendar */
18
20
  events?: CalendarEvent[];
19
21
  /** Callback when event marker is clicked */
@@ -41,6 +43,7 @@ export interface CalendarProps {
41
43
  export default function Calendar({
42
44
  value,
43
45
  onChange,
46
+ onMonthChange,
44
47
  events = [],
45
48
  onEventClick,
46
49
  rangeMode = false,
@@ -200,25 +203,35 @@ export default function Calendar({
200
203
 
201
204
  // Navigate months
202
205
  const previousMonth = () => {
203
- setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1));
206
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
207
+ setCurrentMonth(newMonth);
208
+ onMonthChange?.(newMonth);
204
209
  };
205
210
 
206
211
  const nextMonth = () => {
207
- setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1));
212
+ const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
213
+ setCurrentMonth(newMonth);
214
+ onMonthChange?.(newMonth);
208
215
  };
209
216
 
210
217
  // Navigate years
211
218
  const previousYear = () => {
212
- setCurrentMonth(new Date(currentMonth.getFullYear() - 1, currentMonth.getMonth(), 1));
219
+ const newMonth = new Date(currentMonth.getFullYear() - 1, currentMonth.getMonth(), 1);
220
+ setCurrentMonth(newMonth);
221
+ onMonthChange?.(newMonth);
213
222
  };
214
223
 
215
224
  const nextYear = () => {
216
- setCurrentMonth(new Date(currentMonth.getFullYear() + 1, currentMonth.getMonth(), 1));
225
+ const newMonth = new Date(currentMonth.getFullYear() + 1, currentMonth.getMonth(), 1);
226
+ setCurrentMonth(newMonth);
227
+ onMonthChange?.(newMonth);
217
228
  };
218
229
 
219
230
  // Go to today
220
231
  const goToToday = () => {
221
- setCurrentMonth(new Date());
232
+ const today = new Date();
233
+ setCurrentMonth(today);
234
+ onMonthChange?.(today);
222
235
  };
223
236
 
224
237
  // Day names
@@ -244,8 +257,7 @@ export default function Calendar({
244
257
  className="p-1.5 hover:bg-paper-100 rounded transition-colors"
245
258
  aria-label="Previous year"
246
259
  >
247
- <ChevronLeft className="h-4 w-4 text-ink-600" />
248
- <ChevronLeft className="h-4 w-4 text-ink-600 -ml-3" />
260
+ <ChevronsLeft className="h-4 w-4 text-ink-600" />
249
261
  </button>
250
262
  <button
251
263
  onClick={previousMonth}
@@ -281,8 +293,7 @@ export default function Calendar({
281
293
  className="p-1.5 hover:bg-paper-100 rounded transition-colors"
282
294
  aria-label="Next year"
283
295
  >
284
- <ChevronRight className="h-4 w-4 text-ink-600" />
285
- <ChevronRight className="h-4 w-4 text-ink-600 -ml-3" />
296
+ <ChevronsRight className="h-4 w-4 text-ink-600" />
286
297
  </button>
287
298
  </div>
288
299
  </div>
@@ -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';