@papernote/ui 1.3.1 → 1.6.0

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.
Files changed (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. package/tailwind.config.js +56 -1
@@ -0,0 +1,225 @@
1
+ import React from 'react';
2
+
3
+ /**
4
+ * Bottom navigation item configuration
5
+ */
6
+ export interface BottomNavItem {
7
+ /** Unique identifier for the nav item */
8
+ id: string;
9
+ /** Display label */
10
+ label: string;
11
+ /** Icon element (use lucide-react icons) */
12
+ icon: React.ReactNode;
13
+ /** Navigation URL (optional) */
14
+ href?: string;
15
+ /** Badge count for notifications */
16
+ badge?: number;
17
+ /** Click handler (alternative to href) */
18
+ onClick?: () => void;
19
+ /** Disabled state */
20
+ disabled?: boolean;
21
+ }
22
+
23
+ /**
24
+ * BottomNavigation component props
25
+ */
26
+ export interface BottomNavigationProps {
27
+ /** Navigation items (max 5 recommended) */
28
+ items: BottomNavItem[];
29
+ /** Currently active item ID */
30
+ activeId?: string;
31
+ /** Navigation handler - receives item ID and href */
32
+ onNavigate?: (id: string, href?: string) => void;
33
+ /** Show labels below icons */
34
+ showLabels?: boolean;
35
+ /** Additional CSS classes */
36
+ className?: string;
37
+ /** Safe area handling for notched devices */
38
+ safeArea?: boolean;
39
+ }
40
+
41
+ /**
42
+ * BottomNavigation - Mobile-style bottom tab bar
43
+ *
44
+ * iOS/Android-style fixed bottom navigation with icons, labels, and badges.
45
+ * Handles safe area insets for notched devices automatically.
46
+ *
47
+ * Best practices:
48
+ * - Use 3-5 items maximum
49
+ * - Keep labels short (1-2 words)
50
+ * - Use consistent icon style
51
+ *
52
+ * @example Basic usage
53
+ * ```tsx
54
+ * import { BottomNavigation } from 'notebook-ui';
55
+ * import { Home, Search, Bell, User } from 'lucide-react';
56
+ *
57
+ * const navItems = [
58
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
59
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
60
+ * { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3 },
61
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' },
62
+ * ];
63
+ *
64
+ * <BottomNavigation
65
+ * items={navItems}
66
+ * activeId="home"
67
+ * onNavigate={(id, href) => navigate(href)}
68
+ * />
69
+ * ```
70
+ *
71
+ * @example With onClick handlers
72
+ * ```tsx
73
+ * const navItems = [
74
+ * { id: 'home', label: 'Home', icon: <Home />, onClick: () => setTab('home') },
75
+ * { id: 'add', label: 'Add', icon: <Plus />, onClick: openAddModal },
76
+ * ];
77
+ *
78
+ * <BottomNavigation items={navItems} activeId={currentTab} />
79
+ * ```
80
+ */
81
+ export default function BottomNavigation({
82
+ items,
83
+ activeId,
84
+ onNavigate,
85
+ showLabels = true,
86
+ className = '',
87
+ safeArea = true,
88
+ }: BottomNavigationProps) {
89
+ const handleItemClick = (item: BottomNavItem) => {
90
+ if (item.disabled) return;
91
+
92
+ if (item.onClick) {
93
+ item.onClick();
94
+ }
95
+
96
+ if (onNavigate) {
97
+ onNavigate(item.id, item.href);
98
+ }
99
+ };
100
+
101
+ return (
102
+ <nav
103
+ className={`
104
+ fixed bottom-0 left-0 right-0 z-40
105
+ bg-white border-t border-paper-200 shadow-lg
106
+ ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}
107
+ ${className}
108
+ `}
109
+ role="navigation"
110
+ aria-label="Bottom navigation"
111
+ >
112
+ <div className="flex items-center justify-around h-14 max-w-lg mx-auto px-2">
113
+ {items.map((item) => {
114
+ const isActive = item.id === activeId;
115
+
116
+ return (
117
+ <button
118
+ key={item.id}
119
+ onClick={() => handleItemClick(item)}
120
+ disabled={item.disabled}
121
+ className={`
122
+ relative flex flex-col items-center justify-center
123
+ flex-1 h-full min-w-touch-sm
124
+ transition-colors duration-200
125
+ ${item.disabled
126
+ ? 'opacity-40 cursor-not-allowed'
127
+ : 'active:bg-paper-100'
128
+ }
129
+ ${isActive
130
+ ? 'text-accent-600'
131
+ : 'text-ink-500 hover:text-ink-700'
132
+ }
133
+ `}
134
+ aria-current={isActive ? 'page' : undefined}
135
+ aria-label={item.label}
136
+ >
137
+ {/* Icon container */}
138
+ <div className="relative">
139
+ {/* Icon */}
140
+ <div
141
+ className={`
142
+ w-6 h-6 flex items-center justify-center
143
+ transition-transform duration-200
144
+ ${isActive ? 'scale-110' : 'scale-100'}
145
+ `}
146
+ >
147
+ {React.isValidElement(item.icon)
148
+ ? React.cloneElement(item.icon as React.ReactElement<any>, {
149
+ className: 'w-6 h-6',
150
+ })
151
+ : item.icon}
152
+ </div>
153
+
154
+ {/* Badge */}
155
+ {item.badge !== undefined && item.badge > 0 && (
156
+ <span
157
+ className={`
158
+ absolute -top-1 -right-2.5
159
+ min-w-[18px] h-[18px] px-1
160
+ flex items-center justify-center
161
+ text-[10px] font-bold text-white
162
+ bg-error-500 rounded-full
163
+ ${item.badge > 99 ? 'text-[8px]' : ''}
164
+ `}
165
+ >
166
+ {item.badge > 99 ? '99+' : item.badge}
167
+ </span>
168
+ )}
169
+ </div>
170
+
171
+ {/* Label */}
172
+ {showLabels && (
173
+ <span
174
+ className={`
175
+ mt-1 text-[10px] font-medium leading-none
176
+ transition-opacity duration-200
177
+ truncate max-w-full px-1
178
+ ${isActive ? 'opacity-100' : 'opacity-70'}
179
+ `}
180
+ >
181
+ {item.label}
182
+ </span>
183
+ )}
184
+
185
+ {/* Active indicator */}
186
+ {isActive && (
187
+ <div
188
+ className="
189
+ absolute top-0 left-1/2 -translate-x-1/2
190
+ w-8 h-0.5 bg-accent-500 rounded-full
191
+ "
192
+ />
193
+ )}
194
+ </button>
195
+ );
196
+ })}
197
+ </div>
198
+ </nav>
199
+ );
200
+ }
201
+
202
+ /**
203
+ * BottomNavigationSpacer - Spacer to prevent content from being hidden behind BottomNavigation
204
+ *
205
+ * Place this at the bottom of your scrollable content when using BottomNavigation.
206
+ *
207
+ * @example
208
+ * ```tsx
209
+ * <div className="flex flex-col h-screen">
210
+ * <main className="flex-1 overflow-auto">
211
+ * {/* Your content *\/}
212
+ * <BottomNavigationSpacer />
213
+ * </main>
214
+ * <BottomNavigation items={navItems} />
215
+ * </div>
216
+ * ```
217
+ */
218
+ export function BottomNavigationSpacer({ safeArea = true }: { safeArea?: boolean }) {
219
+ return (
220
+ <div
221
+ className={`h-14 ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}`}
222
+ aria-hidden="true"
223
+ />
224
+ );
225
+ }
@@ -312,3 +312,165 @@ export const SelectAll: Story = {
312
312
  );
313
313
  },
314
314
  };
315
+
316
+ // Mobile-optimized stories
317
+ export const MobileLargeTouch: Story = {
318
+ parameters: {
319
+ viewport: { defaultViewport: 'mobile1' },
320
+ docs: {
321
+ description: {
322
+ story: 'Large size (lg) checkbox provides 44px minimum touch target height for mobile devices, meeting Apple HIG guidelines.',
323
+ },
324
+ },
325
+ },
326
+ render: () => {
327
+ const [checked, setChecked] = useState(false);
328
+ return (
329
+ <Checkbox
330
+ checked={checked}
331
+ onChange={setChecked}
332
+ size="lg"
333
+ label="Touch-friendly checkbox with 44px target"
334
+ />
335
+ );
336
+ },
337
+ };
338
+
339
+ export const MobilePreferencesList: Story = {
340
+ parameters: {
341
+ viewport: { defaultViewport: 'mobile1' },
342
+ docs: {
343
+ description: {
344
+ story: 'Mobile settings list with large touch targets and icons for easy interaction.',
345
+ },
346
+ },
347
+ },
348
+ render: () => {
349
+ const [prefs, setPrefs] = useState({
350
+ notifications: true,
351
+ sounds: false,
352
+ location: true,
353
+ analytics: false,
354
+ });
355
+
356
+ return (
357
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', padding: '1rem' }}>
358
+ <h3 style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '0.75rem' }}>Settings</h3>
359
+ <Checkbox
360
+ checked={prefs.notifications}
361
+ onChange={(checked) => setPrefs({ ...prefs, notifications: checked })}
362
+ size="lg"
363
+ label="Push Notifications"
364
+ />
365
+ <Checkbox
366
+ checked={prefs.sounds}
367
+ onChange={(checked) => setPrefs({ ...prefs, sounds: checked })}
368
+ size="lg"
369
+ label="Sound Effects"
370
+ />
371
+ <Checkbox
372
+ checked={prefs.location}
373
+ onChange={(checked) => setPrefs({ ...prefs, location: checked })}
374
+ size="lg"
375
+ label="Location Services"
376
+ />
377
+ <Checkbox
378
+ checked={prefs.analytics}
379
+ onChange={(checked) => setPrefs({ ...prefs, analytics: checked })}
380
+ size="lg"
381
+ label="Analytics Sharing"
382
+ />
383
+ </div>
384
+ );
385
+ },
386
+ };
387
+
388
+ export const MobileFileTypeSelector: Story = {
389
+ parameters: {
390
+ viewport: { defaultViewport: 'mobile1' },
391
+ docs: {
392
+ description: {
393
+ story: 'Mobile file type selector with icons and large touch targets.',
394
+ },
395
+ },
396
+ },
397
+ render: () => {
398
+ const [selected, setSelected] = useState({
399
+ documents: true,
400
+ images: true,
401
+ audio: false,
402
+ video: false,
403
+ });
404
+
405
+ return (
406
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem', padding: '1rem' }}>
407
+ <h3 style={{ fontSize: '1.125rem', fontWeight: 600, marginBottom: '0.75rem' }}>Include File Types</h3>
408
+ <Checkbox
409
+ checked={selected.documents}
410
+ onChange={(checked) => setSelected({ ...selected, documents: checked })}
411
+ size="lg"
412
+ label="Documents"
413
+ icon={<FileText className="h-5 w-5" />}
414
+ />
415
+ <Checkbox
416
+ checked={selected.images}
417
+ onChange={(checked) => setSelected({ ...selected, images: checked })}
418
+ size="lg"
419
+ label="Images"
420
+ icon={<Image className="h-5 w-5" />}
421
+ />
422
+ <Checkbox
423
+ checked={selected.audio}
424
+ onChange={(checked) => setSelected({ ...selected, audio: checked })}
425
+ size="lg"
426
+ label="Audio"
427
+ icon={<Music className="h-5 w-5" />}
428
+ />
429
+ <Checkbox
430
+ checked={selected.video}
431
+ onChange={(checked) => setSelected({ ...selected, video: checked })}
432
+ size="lg"
433
+ label="Video"
434
+ icon={<Video className="h-5 w-5" />}
435
+ />
436
+ </div>
437
+ );
438
+ },
439
+ };
440
+
441
+ export const MobileTermsAgreement: Story = {
442
+ parameters: {
443
+ viewport: { defaultViewport: 'mobile1' },
444
+ docs: {
445
+ description: {
446
+ story: 'Mobile terms agreement with easy-to-tap checkbox for form submissions.',
447
+ },
448
+ },
449
+ },
450
+ render: () => {
451
+ const [agreed, setAgreed] = useState(false);
452
+ const [newsletter, setNewsletter] = useState(false);
453
+
454
+ return (
455
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', padding: '1rem' }}>
456
+ <h3 style={{ fontSize: '1.125rem', fontWeight: 600 }}>Complete Your Signup</h3>
457
+ <Checkbox
458
+ checked={agreed}
459
+ onChange={setAgreed}
460
+ size="lg"
461
+ label="I agree to the Terms of Service and Privacy Policy"
462
+ icon={<FileText className="h-5 w-5" />}
463
+ />
464
+ <Checkbox
465
+ checked={newsletter}
466
+ onChange={setNewsletter}
467
+ size="lg"
468
+ label="Subscribe to our newsletter for updates"
469
+ />
470
+ <p style={{ fontSize: '0.75rem', color: '#666' }}>
471
+ Large touch targets make it easy to check on mobile
472
+ </p>
473
+ </div>
474
+ );
475
+ },
476
+ };
@@ -1,4 +1,5 @@
1
1
  import { forwardRef, useId } from 'react';
2
+ import { useIsMobile } from '../hooks/useResponsive';
2
3
  import { Check, Minus } from 'lucide-react';
3
4
 
4
5
  export interface CheckboxProps {
@@ -13,8 +14,17 @@ export interface CheckboxProps {
13
14
  name?: string;
14
15
  /** Optional icon to display next to label */
15
16
  icon?: React.ReactNode;
17
+ /** Size variant - 'lg' provides 44px touch-friendly targets. On mobile, 'md' auto-upgrades to 'lg'. */
18
+ size?: 'sm' | 'md' | 'lg';
16
19
  }
17
20
 
21
+ // Size classes for checkbox box and touch target
22
+ const sizeConfig = {
23
+ sm: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-2' },
24
+ md: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-3' },
25
+ lg: { box: 'w-5 h-5', icon: 'h-4 w-4', text: 'text-base', gap: 'gap-3', touchTarget: 'min-h-touch py-2' }, // 44px touch target
26
+ };
27
+
18
28
  const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
19
29
  checked,
20
30
  onChange,
@@ -26,10 +36,16 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
26
36
  id,
27
37
  name,
28
38
  icon,
39
+ size = 'md',
29
40
  }, ref) => {
30
41
  const generatedId = useId();
31
42
  const checkboxId = id || generatedId;
32
43
  const descId = description ? `${checkboxId}-desc` : undefined;
44
+
45
+ // Auto-size for mobile
46
+ const isMobile = useIsMobile();
47
+ const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
48
+ const config = sizeConfig[effectiveSize];
33
49
 
34
50
  const handleChange = () => {
35
51
  if (!disabled) {
@@ -47,9 +63,9 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
47
63
  return (
48
64
  <label
49
65
  htmlFor={checkboxId}
50
- className={`flex items-start gap-3 ${
66
+ className={`flex items-start ${config.gap} ${
51
67
  disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'
52
- } ${className}`}
68
+ } ${'touchTarget' in config ? config.touchTarget : ''} ${className}`}
53
69
  >
54
70
  {/* Checkbox */}
55
71
  <div className="relative inline-block flex-shrink-0 mt-0.5">
@@ -69,7 +85,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
69
85
  />
70
86
  <div
71
87
  className={`
72
- w-4 h-4 rounded border transition-all duration-200
88
+ ${config.box} rounded border transition-all duration-200
73
89
  flex items-center justify-center
74
90
  ${
75
91
  checked || indeterminate
@@ -80,9 +96,9 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
80
96
  `}
81
97
  >
82
98
  {indeterminate ? (
83
- <Minus className="h-3 w-3 text-white" />
99
+ <Minus className={`${config.icon} text-white`} />
84
100
  ) : checked ? (
85
- <Check className="h-3 w-3 text-white" />
101
+ <Check className={`${config.icon} text-white`} />
86
102
  ) : null}
87
103
  </div>
88
104
  </div>
@@ -93,7 +109,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(({
93
109
  {label && (
94
110
  <div className="flex items-center gap-2">
95
111
  {icon && <span className="text-ink-700">{icon}</span>}
96
- <p className="text-sm font-medium text-ink-900">{label}</p>
112
+ <p className={`${config.text} font-medium text-ink-900`}>{label}</p>
97
113
  </div>
98
114
  )}
99
115
  {description && (