@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,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
- <Card.Header>
255
- <Card.Title>Card 1</Card.Title>
256
- </Card.Header>
257
- <Card.Content>
254
+ <CardHeader>
255
+ <CardTitle>Card 1</CardTitle>
256
+ </CardHeader>
257
+ <CardContent>
258
258
  <Text color="secondary">Content for the first card.</Text>
259
- </Card.Content>
259
+ </CardContent>
260
260
  </Card>
261
261
  <Card>
262
- <Card.Header>
263
- <Card.Title>Card 2</Card.Title>
264
- </Card.Header>
265
- <Card.Content>
262
+ <CardHeader>
263
+ <CardTitle>Card 2</CardTitle>
264
+ </CardHeader>
265
+ <CardContent>
266
266
  <Text color="secondary">Content for the second card.</Text>
267
- </Card.Content>
267
+ </CardContent>
268
268
  </Card>
269
269
  <Card>
270
- <Card.Header>
271
- <Card.Title>Card 3</Card.Title>
272
- </Card.Header>
273
- <Card.Content>
270
+ <CardHeader>
271
+ <CardTitle>Card 3</CardTitle>
272
+ </CardHeader>
273
+ <CardContent>
274
274
  <Text color="secondary">Content for the third card.</Text>
275
- </Card.Content>
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
+ };