@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.
- package/dist/components/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +78 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PageHeader.d.ts +86 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +31 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1871 -51
- package/dist/index.esm.js +3025 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3063 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +434 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +388 -0
- package/src/components/Modal.tsx +122 -4
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +193 -10
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +69 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- 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
|
|
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
|
-
|
|
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=
|
|
99
|
+
<Minus className={`${config.icon} text-white`} />
|
|
84
100
|
) : checked ? (
|
|
85
|
-
<Check className=
|
|
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=
|
|
112
|
+
<p className={`${config.text} font-medium text-ink-900`}>{label}</p>
|
|
97
113
|
</div>
|
|
98
114
|
)}
|
|
99
115
|
{description && (
|