@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,301 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { Plus, X } from 'lucide-react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Action item for FAB menu
|
|
7
|
+
*/
|
|
8
|
+
export interface FABAction {
|
|
9
|
+
/** Unique identifier */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Icon for the action */
|
|
12
|
+
icon: React.ReactNode;
|
|
13
|
+
/** Label text (shown on hover/long-press) */
|
|
14
|
+
label: string;
|
|
15
|
+
/** Click handler */
|
|
16
|
+
onClick: () => void;
|
|
17
|
+
/** Disabled state */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* FloatingActionButton component props
|
|
23
|
+
*/
|
|
24
|
+
export interface FloatingActionButtonProps {
|
|
25
|
+
/** Primary action when FAB is clicked (without menu) */
|
|
26
|
+
onClick?: () => void;
|
|
27
|
+
/** Icon for the FAB - defaults to Plus */
|
|
28
|
+
icon?: React.ReactNode;
|
|
29
|
+
/** Secondary actions shown in menu */
|
|
30
|
+
actions?: FABAction[];
|
|
31
|
+
/** Position on screen */
|
|
32
|
+
position?: 'bottom-right' | 'bottom-left' | 'bottom-center';
|
|
33
|
+
/** Color variant */
|
|
34
|
+
variant?: 'primary' | 'secondary' | 'accent';
|
|
35
|
+
/** Size */
|
|
36
|
+
size?: 'md' | 'lg';
|
|
37
|
+
/** Accessible label */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Extended FAB with text label */
|
|
40
|
+
extended?: boolean;
|
|
41
|
+
/** Text for extended FAB */
|
|
42
|
+
extendedLabel?: string;
|
|
43
|
+
/** Hide FAB (useful for scroll-based show/hide) */
|
|
44
|
+
hidden?: boolean;
|
|
45
|
+
/** Custom offset from edge (in pixels) */
|
|
46
|
+
offset?: { x?: number; y?: number };
|
|
47
|
+
/** Additional class names */
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const positionClasses = {
|
|
52
|
+
'bottom-right': 'right-4 bottom-4',
|
|
53
|
+
'bottom-left': 'left-4 bottom-4',
|
|
54
|
+
'bottom-center': 'left-1/2 -translate-x-1/2 bottom-4',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const variantClasses = {
|
|
58
|
+
primary: 'bg-accent-600 hover:bg-accent-700 text-white shadow-lg',
|
|
59
|
+
secondary: 'bg-white hover:bg-paper-50 text-ink-700 shadow-lg border border-paper-200',
|
|
60
|
+
accent: 'bg-accent-500 hover:bg-accent-600 text-white shadow-lg',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sizeClasses = {
|
|
64
|
+
md: 'w-14 h-14',
|
|
65
|
+
lg: 'w-16 h-16',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const iconSizeClasses = {
|
|
69
|
+
md: 'h-6 w-6',
|
|
70
|
+
lg: 'h-7 w-7',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* FloatingActionButton - Material Design style FAB for mobile
|
|
75
|
+
*
|
|
76
|
+
* A prominent button for the primary action on a screen.
|
|
77
|
+
* Supports single action or expandable menu with multiple actions.
|
|
78
|
+
*
|
|
79
|
+
* @example Simple FAB
|
|
80
|
+
* ```tsx
|
|
81
|
+
* <FloatingActionButton
|
|
82
|
+
* onClick={() => openCreateModal()}
|
|
83
|
+
* label="Create new item"
|
|
84
|
+
* />
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example FAB with action menu
|
|
88
|
+
* ```tsx
|
|
89
|
+
* <FloatingActionButton
|
|
90
|
+
* actions={[
|
|
91
|
+
* { id: 'photo', icon: <Camera />, label: 'Take Photo', onClick: takePhoto },
|
|
92
|
+
* { id: 'upload', icon: <Upload />, label: 'Upload File', onClick: uploadFile },
|
|
93
|
+
* { id: 'note', icon: <FileText />, label: 'Create Note', onClick: createNote },
|
|
94
|
+
* ]}
|
|
95
|
+
* />
|
|
96
|
+
* ```
|
|
97
|
+
*
|
|
98
|
+
* @example Extended FAB
|
|
99
|
+
* ```tsx
|
|
100
|
+
* <FloatingActionButton
|
|
101
|
+
* extended
|
|
102
|
+
* extendedLabel="New Task"
|
|
103
|
+
* icon={<Plus />}
|
|
104
|
+
* onClick={createTask}
|
|
105
|
+
* />
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export default function FloatingActionButton({
|
|
109
|
+
onClick,
|
|
110
|
+
icon,
|
|
111
|
+
actions,
|
|
112
|
+
position = 'bottom-right',
|
|
113
|
+
variant = 'primary',
|
|
114
|
+
size = 'md',
|
|
115
|
+
label = 'Action button',
|
|
116
|
+
extended = false,
|
|
117
|
+
extendedLabel,
|
|
118
|
+
hidden = false,
|
|
119
|
+
offset,
|
|
120
|
+
className = '',
|
|
121
|
+
}: FloatingActionButtonProps) {
|
|
122
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
123
|
+
const fabRef = useRef<HTMLButtonElement>(null);
|
|
124
|
+
const hasMenu = actions && actions.length > 0;
|
|
125
|
+
|
|
126
|
+
// Close menu on escape
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!isMenuOpen) return;
|
|
129
|
+
|
|
130
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
131
|
+
if (e.key === 'Escape') {
|
|
132
|
+
setIsMenuOpen(false);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
document.addEventListener('keydown', handleEscape);
|
|
137
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
138
|
+
}, [isMenuOpen]);
|
|
139
|
+
|
|
140
|
+
// Close menu on click outside
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!isMenuOpen) return;
|
|
143
|
+
|
|
144
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
145
|
+
if (fabRef.current && !fabRef.current.contains(e.target as Node)) {
|
|
146
|
+
setIsMenuOpen(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
151
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
152
|
+
}, [isMenuOpen]);
|
|
153
|
+
|
|
154
|
+
const handleClick = () => {
|
|
155
|
+
if (hasMenu) {
|
|
156
|
+
setIsMenuOpen(!isMenuOpen);
|
|
157
|
+
} else if (onClick) {
|
|
158
|
+
onClick();
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleActionClick = (action: FABAction) => {
|
|
163
|
+
if (!action.disabled) {
|
|
164
|
+
action.onClick();
|
|
165
|
+
setIsMenuOpen(false);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Custom offset styles
|
|
170
|
+
const offsetStyle = offset ? {
|
|
171
|
+
...(offset.x !== undefined && position.includes('right') ? { right: `${offset.x}px` } : {}),
|
|
172
|
+
...(offset.x !== undefined && position.includes('left') ? { left: `${offset.x}px` } : {}),
|
|
173
|
+
...(offset.y !== undefined ? { bottom: `${offset.y}px` } : {}),
|
|
174
|
+
} : {};
|
|
175
|
+
|
|
176
|
+
const fabContent = (
|
|
177
|
+
<div
|
|
178
|
+
className={`
|
|
179
|
+
fixed z-40 transition-all duration-300
|
|
180
|
+
${positionClasses[position]}
|
|
181
|
+
${hidden ? 'translate-y-20 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
|
|
182
|
+
${className}
|
|
183
|
+
`}
|
|
184
|
+
style={{
|
|
185
|
+
...offsetStyle,
|
|
186
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
187
|
+
}}
|
|
188
|
+
>
|
|
189
|
+
{/* Action Menu */}
|
|
190
|
+
{hasMenu && isMenuOpen && (
|
|
191
|
+
<div className="absolute bottom-full mb-3 flex flex-col-reverse gap-3 items-center">
|
|
192
|
+
{actions.map((action, index) => (
|
|
193
|
+
<div
|
|
194
|
+
key={action.id}
|
|
195
|
+
className="flex items-center gap-3 animate-fade-in"
|
|
196
|
+
style={{ animationDelay: `${index * 50}ms` }}
|
|
197
|
+
>
|
|
198
|
+
{/* Label */}
|
|
199
|
+
<span className="bg-ink-900/80 text-white text-sm px-3 py-1.5 rounded-lg whitespace-nowrap">
|
|
200
|
+
{action.label}
|
|
201
|
+
</span>
|
|
202
|
+
|
|
203
|
+
{/* Mini FAB */}
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => handleActionClick(action)}
|
|
206
|
+
disabled={action.disabled}
|
|
207
|
+
className={`
|
|
208
|
+
w-12 h-12 rounded-full flex items-center justify-center
|
|
209
|
+
transition-all duration-200
|
|
210
|
+
${action.disabled
|
|
211
|
+
? 'bg-paper-200 text-ink-400 cursor-not-allowed'
|
|
212
|
+
: 'bg-white text-ink-700 shadow-lg hover:bg-paper-50 active:scale-95'
|
|
213
|
+
}
|
|
214
|
+
`}
|
|
215
|
+
aria-label={action.label}
|
|
216
|
+
>
|
|
217
|
+
{action.icon}
|
|
218
|
+
</button>
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
223
|
+
|
|
224
|
+
{/* Backdrop for menu */}
|
|
225
|
+
{hasMenu && isMenuOpen && (
|
|
226
|
+
<div
|
|
227
|
+
className="fixed inset-0 bg-black/20 -z-10 animate-fade-in"
|
|
228
|
+
onClick={() => setIsMenuOpen(false)}
|
|
229
|
+
/>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Main FAB */}
|
|
233
|
+
<button
|
|
234
|
+
ref={fabRef}
|
|
235
|
+
onClick={handleClick}
|
|
236
|
+
className={`
|
|
237
|
+
${extended ? 'px-6 rounded-full' : 'rounded-full'}
|
|
238
|
+
${extended ? 'h-14' : sizeClasses[size]}
|
|
239
|
+
${variantClasses[variant]}
|
|
240
|
+
flex items-center justify-center gap-2
|
|
241
|
+
transition-all duration-200
|
|
242
|
+
active:scale-95
|
|
243
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
|
|
244
|
+
`}
|
|
245
|
+
aria-label={label}
|
|
246
|
+
aria-expanded={hasMenu ? isMenuOpen : undefined}
|
|
247
|
+
aria-haspopup={hasMenu ? 'menu' : undefined}
|
|
248
|
+
>
|
|
249
|
+
{hasMenu && isMenuOpen ? (
|
|
250
|
+
<X className={iconSizeClasses[size]} />
|
|
251
|
+
) : (
|
|
252
|
+
icon || <Plus className={iconSizeClasses[size]} />
|
|
253
|
+
)}
|
|
254
|
+
{extended && extendedLabel && (
|
|
255
|
+
<span className="font-medium">{extendedLabel}</span>
|
|
256
|
+
)}
|
|
257
|
+
</button>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Render via portal to ensure proper stacking
|
|
262
|
+
return createPortal(fabContent, document.body);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Hook for scroll-based FAB visibility
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```tsx
|
|
270
|
+
* const { hidden, scrollDirection } = useFABScroll();
|
|
271
|
+
* <FloatingActionButton hidden={hidden} />
|
|
272
|
+
* ```
|
|
273
|
+
*/
|
|
274
|
+
export function useFABScroll(threshold = 10): { hidden: boolean; scrollDirection: 'up' | 'down' | null } {
|
|
275
|
+
const [hidden, setHidden] = useState(false);
|
|
276
|
+
const [scrollDirection, setScrollDirection] = useState<'up' | 'down' | null>(null);
|
|
277
|
+
const lastScrollY = useRef(0);
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
const handleScroll = () => {
|
|
281
|
+
const currentScrollY = window.scrollY;
|
|
282
|
+
const diff = currentScrollY - lastScrollY.current;
|
|
283
|
+
|
|
284
|
+
if (Math.abs(diff) > threshold) {
|
|
285
|
+
if (diff > 0) {
|
|
286
|
+
setHidden(true);
|
|
287
|
+
setScrollDirection('down');
|
|
288
|
+
} else {
|
|
289
|
+
setHidden(false);
|
|
290
|
+
setScrollDirection('up');
|
|
291
|
+
}
|
|
292
|
+
lastScrollY.current = currentScrollY;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
297
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
298
|
+
}, [threshold]);
|
|
299
|
+
|
|
300
|
+
return { hidden, scrollDirection };
|
|
301
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
2
2
|
import { Grid } from './Grid';
|
|
3
3
|
import Box from './Box';
|
|
4
4
|
import Text from './Text';
|
|
5
|
-
import Card from './Card';
|
|
5
|
+
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
6
|
|
|
7
7
|
const meta = {
|
|
8
8
|
title: 'Layout/Grid',
|
|
@@ -251,28 +251,28 @@ export const WithCards: Story = {
|
|
|
251
251
|
render: () => (
|
|
252
252
|
<Grid columns={1} md={2} lg={3} gap="md">
|
|
253
253
|
<Card>
|
|
254
|
-
<
|
|
255
|
-
<
|
|
256
|
-
</
|
|
257
|
-
<
|
|
254
|
+
<CardHeader>
|
|
255
|
+
<CardTitle>Card 1</CardTitle>
|
|
256
|
+
</CardHeader>
|
|
257
|
+
<CardContent>
|
|
258
258
|
<Text color="secondary">Content for the first card.</Text>
|
|
259
|
-
</
|
|
259
|
+
</CardContent>
|
|
260
260
|
</Card>
|
|
261
261
|
<Card>
|
|
262
|
-
<
|
|
263
|
-
<
|
|
264
|
-
</
|
|
265
|
-
<
|
|
262
|
+
<CardHeader>
|
|
263
|
+
<CardTitle>Card 2</CardTitle>
|
|
264
|
+
</CardHeader>
|
|
265
|
+
<CardContent>
|
|
266
266
|
<Text color="secondary">Content for the second card.</Text>
|
|
267
|
-
</
|
|
267
|
+
</CardContent>
|
|
268
268
|
</Card>
|
|
269
269
|
<Card>
|
|
270
|
-
<
|
|
271
|
-
<
|
|
272
|
-
</
|
|
273
|
-
<
|
|
270
|
+
<CardHeader>
|
|
271
|
+
<CardTitle>Card 3</CardTitle>
|
|
272
|
+
</CardHeader>
|
|
273
|
+
<CardContent>
|
|
274
274
|
<Text color="secondary">Content for the third card.</Text>
|
|
275
|
-
</
|
|
275
|
+
</CardContent>
|
|
276
276
|
</Card>
|
|
277
277
|
</Grid>
|
|
278
278
|
),
|
|
@@ -367,3 +367,217 @@ export const LoginForm: Story = {
|
|
|
367
367
|
);
|
|
368
368
|
},
|
|
369
369
|
};
|
|
370
|
+
|
|
371
|
+
// Mobile-optimized stories
|
|
372
|
+
export const MobileLargeTouch: Story = {
|
|
373
|
+
parameters: {
|
|
374
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
375
|
+
docs: {
|
|
376
|
+
description: {
|
|
377
|
+
story: 'Large size (lg) input provides 44px minimum touch target for mobile devices, meeting Apple HIG guidelines.',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
render: () => {
|
|
382
|
+
const [value, setValue] = useState('');
|
|
383
|
+
return (
|
|
384
|
+
<Input
|
|
385
|
+
label="Mobile-Friendly Input"
|
|
386
|
+
size="lg"
|
|
387
|
+
value={value}
|
|
388
|
+
onChange={(e) => setValue(e.target.value)}
|
|
389
|
+
placeholder="44px touch target"
|
|
390
|
+
helperText="Large size for easy touch interaction"
|
|
391
|
+
/>
|
|
392
|
+
);
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export const MobilePhoneInput: Story = {
|
|
397
|
+
parameters: {
|
|
398
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
399
|
+
docs: {
|
|
400
|
+
description: {
|
|
401
|
+
story: 'Phone input with inputMode="tel" shows numeric keyboard on mobile devices.',
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
render: () => {
|
|
406
|
+
const [phone, setPhone] = useState('');
|
|
407
|
+
return (
|
|
408
|
+
<Input
|
|
409
|
+
label="Phone Number"
|
|
410
|
+
type="tel"
|
|
411
|
+
inputMode="tel"
|
|
412
|
+
enterKeyHint="done"
|
|
413
|
+
size="lg"
|
|
414
|
+
value={phone}
|
|
415
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
416
|
+
placeholder="(555) 123-4567"
|
|
417
|
+
helperText="Shows numeric keyboard on mobile"
|
|
418
|
+
/>
|
|
419
|
+
);
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
export const MobileEmailInput: Story = {
|
|
424
|
+
parameters: {
|
|
425
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
426
|
+
docs: {
|
|
427
|
+
description: {
|
|
428
|
+
story: 'Email input with inputMode="email" shows email-optimized keyboard with @ and .com buttons.',
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
},
|
|
432
|
+
render: () => {
|
|
433
|
+
const [email, setEmail] = useState('');
|
|
434
|
+
return (
|
|
435
|
+
<Input
|
|
436
|
+
label="Email Address"
|
|
437
|
+
type="email"
|
|
438
|
+
inputMode="email"
|
|
439
|
+
enterKeyHint="next"
|
|
440
|
+
size="lg"
|
|
441
|
+
value={email}
|
|
442
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
443
|
+
prefixIcon={<Mail className="h-5 w-5" />}
|
|
444
|
+
placeholder="you@example.com"
|
|
445
|
+
helperText="Shows email keyboard with @ button"
|
|
446
|
+
/>
|
|
447
|
+
);
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export const MobileSearchInput: Story = {
|
|
452
|
+
parameters: {
|
|
453
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
454
|
+
docs: {
|
|
455
|
+
description: {
|
|
456
|
+
story: 'Search input with inputMode="search" and enterKeyHint="search" shows search-optimized keyboard.',
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
render: () => {
|
|
461
|
+
const [search, setSearch] = useState('');
|
|
462
|
+
return (
|
|
463
|
+
<Input
|
|
464
|
+
label="Search"
|
|
465
|
+
type="search"
|
|
466
|
+
inputMode="search"
|
|
467
|
+
enterKeyHint="search"
|
|
468
|
+
size="lg"
|
|
469
|
+
value={search}
|
|
470
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
471
|
+
prefixIcon={<Search className="h-5 w-5" />}
|
|
472
|
+
placeholder="Search products..."
|
|
473
|
+
clearable
|
|
474
|
+
onClear={() => setSearch('')}
|
|
475
|
+
helperText="Shows search keyboard with Search button"
|
|
476
|
+
/>
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
export const MobileNumericInput: Story = {
|
|
482
|
+
parameters: {
|
|
483
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
484
|
+
docs: {
|
|
485
|
+
description: {
|
|
486
|
+
story: 'Numeric input with inputMode="decimal" shows decimal number keyboard for currency/amounts.',
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
render: () => {
|
|
491
|
+
const [amount, setAmount] = useState('');
|
|
492
|
+
return (
|
|
493
|
+
<Input
|
|
494
|
+
label="Amount"
|
|
495
|
+
type="text"
|
|
496
|
+
inputMode="decimal"
|
|
497
|
+
enterKeyHint="done"
|
|
498
|
+
size="lg"
|
|
499
|
+
value={amount}
|
|
500
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
501
|
+
prefix="$"
|
|
502
|
+
placeholder="0.00"
|
|
503
|
+
helperText="Shows decimal keyboard on mobile"
|
|
504
|
+
/>
|
|
505
|
+
);
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
export const MobileLoginForm: Story = {
|
|
510
|
+
parameters: {
|
|
511
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
512
|
+
docs: {
|
|
513
|
+
description: {
|
|
514
|
+
story: 'Complete mobile login form with appropriate keyboard types and enter key hints for smooth form flow.',
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
render: () => {
|
|
519
|
+
const [email, setEmail] = useState('');
|
|
520
|
+
const [password, setPassword] = useState('');
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', padding: '1rem' }}>
|
|
524
|
+
<h2 style={{ fontSize: '1.25rem', fontWeight: 600, marginBottom: '0.5rem' }}>Sign In</h2>
|
|
525
|
+
<Input
|
|
526
|
+
label="Email"
|
|
527
|
+
type="email"
|
|
528
|
+
inputMode="email"
|
|
529
|
+
enterKeyHint="next"
|
|
530
|
+
size="lg"
|
|
531
|
+
value={email}
|
|
532
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
533
|
+
prefixIcon={<Mail className="h-5 w-5" />}
|
|
534
|
+
placeholder="you@example.com"
|
|
535
|
+
required
|
|
536
|
+
/>
|
|
537
|
+
<Input
|
|
538
|
+
label="Password"
|
|
539
|
+
type="password"
|
|
540
|
+
enterKeyHint="done"
|
|
541
|
+
size="lg"
|
|
542
|
+
value={password}
|
|
543
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
544
|
+
prefixIcon={<Lock className="h-5 w-5" />}
|
|
545
|
+
placeholder="Enter password"
|
|
546
|
+
showPasswordToggle
|
|
547
|
+
required
|
|
548
|
+
/>
|
|
549
|
+
<p style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.5rem' }}>
|
|
550
|
+
Tap inputs to see mobile keyboard optimizations
|
|
551
|
+
</p>
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
export const MobileURLInput: Story = {
|
|
558
|
+
parameters: {
|
|
559
|
+
viewport: { defaultViewport: 'mobile1' },
|
|
560
|
+
docs: {
|
|
561
|
+
description: {
|
|
562
|
+
story: 'URL input with inputMode="url" shows URL-optimized keyboard with / and .com buttons.',
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
render: () => {
|
|
567
|
+
const [url, setUrl] = useState('');
|
|
568
|
+
return (
|
|
569
|
+
<Input
|
|
570
|
+
label="Website URL"
|
|
571
|
+
type="url"
|
|
572
|
+
inputMode="url"
|
|
573
|
+
enterKeyHint="go"
|
|
574
|
+
size="lg"
|
|
575
|
+
value={url}
|
|
576
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
577
|
+
prefix="https://"
|
|
578
|
+
placeholder="example.com"
|
|
579
|
+
helperText="Shows URL keyboard with / and .com"
|
|
580
|
+
/>
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
};
|