@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
package/src/components/Input.tsx
CHANGED
|
@@ -9,7 +9,7 @@ export type ValidationState = 'error' | 'success' | 'warning' | null;
|
|
|
9
9
|
/**
|
|
10
10
|
* Input component props
|
|
11
11
|
*/
|
|
12
|
-
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
12
|
+
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
13
13
|
/** Input label text */
|
|
14
14
|
label?: string;
|
|
15
15
|
/** Helper text displayed below input */
|
|
@@ -40,6 +40,33 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|
|
40
40
|
onClear?: () => void;
|
|
41
41
|
/** Show loading spinner in input */
|
|
42
42
|
loading?: boolean;
|
|
43
|
+
|
|
44
|
+
// Mobile optimization props
|
|
45
|
+
/**
|
|
46
|
+
* Input mode hint for mobile keyboards.
|
|
47
|
+
* 'none' - No virtual keyboard
|
|
48
|
+
* 'text' - Standard text keyboard (default)
|
|
49
|
+
* 'decimal' - Decimal number keyboard
|
|
50
|
+
* 'numeric' - Numeric keyboard
|
|
51
|
+
* 'tel' - Telephone keypad
|
|
52
|
+
* 'search' - Search optimized keyboard
|
|
53
|
+
* 'email' - Email optimized keyboard
|
|
54
|
+
* 'url' - URL optimized keyboard
|
|
55
|
+
*/
|
|
56
|
+
inputMode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
57
|
+
/**
|
|
58
|
+
* Enter key hint for mobile keyboards.
|
|
59
|
+
* 'enter' - Standard enter key
|
|
60
|
+
* 'done' - Done action
|
|
61
|
+
* 'go' - Go/navigate action
|
|
62
|
+
* 'next' - Move to next field
|
|
63
|
+
* 'previous' - Move to previous field
|
|
64
|
+
* 'search' - Search action
|
|
65
|
+
* 'send' - Send action
|
|
66
|
+
*/
|
|
67
|
+
enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
|
|
68
|
+
/** Size variant - 'md' is default, 'lg' provides larger touch target (44px min) */
|
|
69
|
+
size?: 'sm' | 'md' | 'lg';
|
|
43
70
|
}
|
|
44
71
|
|
|
45
72
|
/**
|
|
@@ -47,6 +74,11 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|
|
47
74
|
*
|
|
48
75
|
* A feature-rich text input with support for validation states, character counting,
|
|
49
76
|
* password visibility toggle, prefix/suffix text and icons, and clearable functionality.
|
|
77
|
+
*
|
|
78
|
+
* Mobile optimizations:
|
|
79
|
+
* - inputMode prop for appropriate mobile keyboard
|
|
80
|
+
* - enterKeyHint prop for mobile keyboard action button
|
|
81
|
+
* - Size variants with touch-friendly targets (44px for 'lg')
|
|
50
82
|
*
|
|
51
83
|
* @example Basic input with label
|
|
52
84
|
* ```tsx
|
|
@@ -54,6 +86,8 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|
|
54
86
|
* label="Email"
|
|
55
87
|
* type="email"
|
|
56
88
|
* placeholder="Enter your email"
|
|
89
|
+
* inputMode="email"
|
|
90
|
+
* enterKeyHint="next"
|
|
57
91
|
* />
|
|
58
92
|
* ```
|
|
59
93
|
*
|
|
@@ -79,11 +113,23 @@ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|
|
79
113
|
* />
|
|
80
114
|
* ```
|
|
81
115
|
*
|
|
116
|
+
* @example Mobile-optimized phone input
|
|
117
|
+
* ```tsx
|
|
118
|
+
* <Input
|
|
119
|
+
* label="Phone Number"
|
|
120
|
+
* type="tel"
|
|
121
|
+
* inputMode="tel"
|
|
122
|
+
* enterKeyHint="done"
|
|
123
|
+
* size="lg"
|
|
124
|
+
* />
|
|
125
|
+
* ```
|
|
126
|
+
*
|
|
82
127
|
* @example With prefix/suffix
|
|
83
128
|
* ```tsx
|
|
84
129
|
* <Input
|
|
85
130
|
* label="Amount"
|
|
86
131
|
* type="number"
|
|
132
|
+
* inputMode="decimal"
|
|
87
133
|
* prefixIcon={<DollarSign />}
|
|
88
134
|
* suffix="USD"
|
|
89
135
|
* clearable
|
|
@@ -113,6 +159,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
113
159
|
type = 'text',
|
|
114
160
|
value,
|
|
115
161
|
maxLength,
|
|
162
|
+
inputMode,
|
|
163
|
+
enterKeyHint,
|
|
164
|
+
size = 'md',
|
|
116
165
|
...props
|
|
117
166
|
},
|
|
118
167
|
ref
|
|
@@ -144,6 +193,31 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
144
193
|
const currentLength = value ? String(value).length : 0;
|
|
145
194
|
const showCounter = showCount && maxLength;
|
|
146
195
|
|
|
196
|
+
// Auto-detect inputMode based on type if not specified
|
|
197
|
+
const effectiveInputMode = inputMode || (() => {
|
|
198
|
+
switch (type) {
|
|
199
|
+
case 'email': return 'email';
|
|
200
|
+
case 'tel': return 'tel';
|
|
201
|
+
case 'url': return 'url';
|
|
202
|
+
case 'number': return 'decimal';
|
|
203
|
+
case 'search': return 'search';
|
|
204
|
+
default: return undefined;
|
|
205
|
+
}
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
// Size classes
|
|
209
|
+
const sizeClasses = {
|
|
210
|
+
sm: 'h-8 text-sm',
|
|
211
|
+
md: 'h-10 text-base',
|
|
212
|
+
lg: 'h-12 text-base min-h-touch', // 44px touch target
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const buttonSizeClasses = {
|
|
216
|
+
sm: 'p-1',
|
|
217
|
+
md: 'p-1.5',
|
|
218
|
+
lg: 'p-2 min-w-touch-sm min-h-touch-sm', // 36px touch target for buttons
|
|
219
|
+
};
|
|
220
|
+
|
|
147
221
|
const getValidationIcon = () => {
|
|
148
222
|
switch (validationState) {
|
|
149
223
|
case 'error':
|
|
@@ -223,8 +297,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
223
297
|
type={actualType}
|
|
224
298
|
value={value}
|
|
225
299
|
maxLength={maxLength}
|
|
300
|
+
inputMode={effectiveInputMode}
|
|
301
|
+
enterKeyHint={enterKeyHint}
|
|
226
302
|
className={`
|
|
227
303
|
input
|
|
304
|
+
${sizeClasses[size]}
|
|
228
305
|
${getValidationClasses()}
|
|
229
306
|
${prefix ? 'pl-' + (prefix.length * 8 + 12) : ''}
|
|
230
307
|
${prefixIcon && !prefix ? 'pl-10' : ''}
|
|
@@ -247,7 +324,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
247
324
|
)}
|
|
248
325
|
|
|
249
326
|
{/* Right Icon, Validation Icon, Clear Button, or Password Toggle */}
|
|
250
|
-
<div className="absolute inset-y-0 right-0 pr-3 flex items-center gap-
|
|
327
|
+
<div className="absolute inset-y-0 right-0 pr-3 flex items-center gap-1">
|
|
251
328
|
{/* Loading Spinner */}
|
|
252
329
|
{loading && (
|
|
253
330
|
<div className="pointer-events-none text-ink-400">
|
|
@@ -267,7 +344,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
267
344
|
<button
|
|
268
345
|
type="button"
|
|
269
346
|
onClick={handleClear}
|
|
270
|
-
className=
|
|
347
|
+
className={`text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`}
|
|
271
348
|
aria-label="Clear input"
|
|
272
349
|
>
|
|
273
350
|
<X className="h-4 w-4" />
|
|
@@ -279,7 +356,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
|
279
356
|
<button
|
|
280
357
|
type="button"
|
|
281
358
|
onClick={() => setShowPassword(!showPassword)}
|
|
282
|
-
className=
|
|
359
|
+
className={`text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`}
|
|
283
360
|
aria-label={showPassword ? 'Hide password' : 'Show password'}
|
|
284
361
|
>
|
|
285
362
|
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Menu, Search, MoreVertical, Share, Heart, Check, Bell } from 'lucide-react';
|
|
3
|
+
import MobileHeader, { MobileHeaderSpacer } from './MobileHeader';
|
|
4
|
+
import Button from './Button';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof MobileHeader> = {
|
|
7
|
+
title: 'Mobile/MobileHeader',
|
|
8
|
+
component: MobileHeader,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'fullscreen',
|
|
11
|
+
viewport: {
|
|
12
|
+
defaultViewport: 'mobile1',
|
|
13
|
+
},
|
|
14
|
+
docs: {
|
|
15
|
+
description: {
|
|
16
|
+
component: 'Mobile app header with navigation controls (menu, back, close) and optional right actions.',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
decorators: [
|
|
21
|
+
(Story) => (
|
|
22
|
+
<div style={{ minHeight: '100vh', background: '#f5f5f4' }}>
|
|
23
|
+
<Story />
|
|
24
|
+
<div style={{ padding: '16px' }}>
|
|
25
|
+
{Array.from({ length: 15 }).map((_, i) => (
|
|
26
|
+
<div key={i} style={{ padding: '16px', margin: '8px 0', background: 'white', borderRadius: '8px' }}>
|
|
27
|
+
Content Item {i + 1}
|
|
28
|
+
</div>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof MobileHeader>;
|
|
38
|
+
|
|
39
|
+
export const WithMenuButton: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
title: 'Dashboard',
|
|
42
|
+
onMenuClick: () => console.log('Menu clicked'),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const WithSubtitle: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
title: 'User Details',
|
|
49
|
+
subtitle: 'Profile',
|
|
50
|
+
onMenuClick: () => console.log('Menu clicked'),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const WithBackButton: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
title: 'Settings',
|
|
57
|
+
subtitle: 'Account',
|
|
58
|
+
onBackClick: () => console.log('Back clicked'),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const WithCloseButton: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
title: 'New Message',
|
|
65
|
+
onCloseClick: () => console.log('Close clicked'),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const WithRightAction: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
title: 'Search',
|
|
72
|
+
onMenuClick: () => console.log('Menu clicked'),
|
|
73
|
+
rightAction: (
|
|
74
|
+
<Button variant="ghost" iconOnly onClick={() => console.log('Search')}>
|
|
75
|
+
<Search className="w-5 h-5" />
|
|
76
|
+
</Button>
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const WithMultipleActions: Story = {
|
|
82
|
+
args: {
|
|
83
|
+
title: 'Photo',
|
|
84
|
+
onBackClick: () => console.log('Back'),
|
|
85
|
+
rightAction: (
|
|
86
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
87
|
+
<Button variant="ghost" iconOnly onClick={() => console.log('Like')}>
|
|
88
|
+
<Heart className="w-5 h-5" />
|
|
89
|
+
</Button>
|
|
90
|
+
<Button variant="ghost" iconOnly onClick={() => console.log('Share')}>
|
|
91
|
+
<Share className="w-5 h-5" />
|
|
92
|
+
</Button>
|
|
93
|
+
<Button variant="ghost" iconOnly onClick={() => console.log('More')}>
|
|
94
|
+
<MoreVertical className="w-5 h-5" />
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const WithPrimaryAction: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
title: 'Edit Profile',
|
|
104
|
+
onCloseClick: () => console.log('Cancel'),
|
|
105
|
+
rightAction: (
|
|
106
|
+
<Button variant="ghost" iconOnly onClick={() => console.log('Save')}>
|
|
107
|
+
<Check className="w-5 h-5 text-accent-600" />
|
|
108
|
+
</Button>
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const TransparentVariant: Story = {
|
|
114
|
+
args: {
|
|
115
|
+
title: 'Photo Gallery',
|
|
116
|
+
onBackClick: () => console.log('Back'),
|
|
117
|
+
variant: 'transparent',
|
|
118
|
+
bordered: false,
|
|
119
|
+
},
|
|
120
|
+
decorators: [
|
|
121
|
+
(Story) => (
|
|
122
|
+
<div style={{ minHeight: '100vh', background: 'linear-gradient(180deg, #667eea 0%, #764ba2 100%)' }}>
|
|
123
|
+
<Story />
|
|
124
|
+
<div style={{ padding: '16px', color: 'white' }}>
|
|
125
|
+
<p>Content with transparent header</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
),
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const BlurVariant: Story = {
|
|
133
|
+
args: {
|
|
134
|
+
title: 'Messages',
|
|
135
|
+
onBackClick: () => console.log('Back'),
|
|
136
|
+
variant: 'blur',
|
|
137
|
+
rightAction: (
|
|
138
|
+
<Button variant="ghost" iconOnly>
|
|
139
|
+
<Bell className="w-5 h-5" />
|
|
140
|
+
</Button>
|
|
141
|
+
),
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const NotSticky: Story = {
|
|
146
|
+
args: {
|
|
147
|
+
title: 'Non-Sticky Header',
|
|
148
|
+
subtitle: 'Scrolls with content',
|
|
149
|
+
onMenuClick: () => console.log('Menu'),
|
|
150
|
+
sticky: false,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export const NoBorder: Story = {
|
|
155
|
+
args: {
|
|
156
|
+
title: 'No Border',
|
|
157
|
+
onMenuClick: () => console.log('Menu'),
|
|
158
|
+
bordered: false,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const CustomLeftAction: Story = {
|
|
163
|
+
args: {
|
|
164
|
+
title: 'Custom Actions',
|
|
165
|
+
leftAction: (
|
|
166
|
+
<Button variant="ghost" size="sm" onClick={() => console.log('Cancel')}>
|
|
167
|
+
Cancel
|
|
168
|
+
</Button>
|
|
169
|
+
),
|
|
170
|
+
rightAction: (
|
|
171
|
+
<Button variant="primary" size="sm" onClick={() => console.log('Save')}>
|
|
172
|
+
Save
|
|
173
|
+
</Button>
|
|
174
|
+
),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const LongTitle: Story = {
|
|
179
|
+
args: {
|
|
180
|
+
title: 'This is a very long title that should be truncated',
|
|
181
|
+
subtitle: 'And this is also a long subtitle text',
|
|
182
|
+
onBackClick: () => console.log('Back'),
|
|
183
|
+
rightAction: (
|
|
184
|
+
<Button variant="ghost" iconOnly>
|
|
185
|
+
<MoreVertical className="w-5 h-5" />
|
|
186
|
+
</Button>
|
|
187
|
+
),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const SpacerExample: Story = {
|
|
192
|
+
render: () => (
|
|
193
|
+
<div style={{ minHeight: '100vh', background: '#f5f5f4' }}>
|
|
194
|
+
<MobileHeader
|
|
195
|
+
title="With Spacer"
|
|
196
|
+
onMenuClick={() => console.log('Menu')}
|
|
197
|
+
sticky={false}
|
|
198
|
+
/>
|
|
199
|
+
<MobileHeaderSpacer />
|
|
200
|
+
<div style={{ padding: '16px' }}>
|
|
201
|
+
<p>The spacer maintains consistent spacing when header is not sticky.</p>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
),
|
|
205
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Menu, ChevronLeft, X } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* MobileHeader component props
|
|
6
|
+
*/
|
|
7
|
+
export interface MobileHeaderProps {
|
|
8
|
+
/** Page/section title */
|
|
9
|
+
title: string;
|
|
10
|
+
/** Subtitle or breadcrumb text */
|
|
11
|
+
subtitle?: string;
|
|
12
|
+
/** Handler for menu button click (hamburger) */
|
|
13
|
+
onMenuClick?: () => void;
|
|
14
|
+
/** Handler for back button click - shows back arrow instead of menu */
|
|
15
|
+
onBackClick?: () => void;
|
|
16
|
+
/** Handler for close button click - shows X instead of menu */
|
|
17
|
+
onCloseClick?: () => void;
|
|
18
|
+
/** Right side action element (button, icon, etc.) */
|
|
19
|
+
rightAction?: React.ReactNode;
|
|
20
|
+
/** Left side action element (overrides menu/back/close buttons) */
|
|
21
|
+
leftAction?: React.ReactNode;
|
|
22
|
+
/** Make header sticky at top */
|
|
23
|
+
sticky?: boolean;
|
|
24
|
+
/** Show border at bottom */
|
|
25
|
+
bordered?: boolean;
|
|
26
|
+
/** Background style */
|
|
27
|
+
variant?: 'solid' | 'transparent' | 'blur';
|
|
28
|
+
/** Additional CSS classes */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Safe area handling for notched devices */
|
|
31
|
+
safeArea?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* MobileHeader - Mobile app header with navigation controls
|
|
36
|
+
*
|
|
37
|
+
* A flexible mobile header component with support for:
|
|
38
|
+
* - Hamburger menu button (default)
|
|
39
|
+
* - Back navigation arrow
|
|
40
|
+
* - Close button (X)
|
|
41
|
+
* - Custom left/right actions
|
|
42
|
+
* - Sticky positioning
|
|
43
|
+
* - Blur/transparent variants
|
|
44
|
+
*
|
|
45
|
+
* @example Basic with menu button
|
|
46
|
+
* ```tsx
|
|
47
|
+
* <MobileHeader
|
|
48
|
+
* title="Dashboard"
|
|
49
|
+
* onMenuClick={() => setDrawerOpen(true)}
|
|
50
|
+
* />
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example With back button
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <MobileHeader
|
|
56
|
+
* title="User Details"
|
|
57
|
+
* subtitle="Profile"
|
|
58
|
+
* onBackClick={() => navigate(-1)}
|
|
59
|
+
* />
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @example With right action
|
|
63
|
+
* ```tsx
|
|
64
|
+
* <MobileHeader
|
|
65
|
+
* title="Settings"
|
|
66
|
+
* onMenuClick={openMenu}
|
|
67
|
+
* rightAction={
|
|
68
|
+
* <Button variant="ghost" iconOnly onClick={save}>
|
|
69
|
+
* <Check className="w-5 h-5" />
|
|
70
|
+
* </Button>
|
|
71
|
+
* }
|
|
72
|
+
* />
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Transparent with blur
|
|
76
|
+
* ```tsx
|
|
77
|
+
* <MobileHeader
|
|
78
|
+
* title="Photo Gallery"
|
|
79
|
+
* variant="blur"
|
|
80
|
+
* onBackClick={goBack}
|
|
81
|
+
* />
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export default function MobileHeader({
|
|
85
|
+
title,
|
|
86
|
+
subtitle,
|
|
87
|
+
onMenuClick,
|
|
88
|
+
onBackClick,
|
|
89
|
+
onCloseClick,
|
|
90
|
+
rightAction,
|
|
91
|
+
leftAction,
|
|
92
|
+
sticky = true,
|
|
93
|
+
bordered = true,
|
|
94
|
+
variant = 'solid',
|
|
95
|
+
className = '',
|
|
96
|
+
safeArea = true,
|
|
97
|
+
}: MobileHeaderProps) {
|
|
98
|
+
// Determine which left button to show
|
|
99
|
+
const renderLeftButton = () => {
|
|
100
|
+
// Custom left action takes priority
|
|
101
|
+
if (leftAction) {
|
|
102
|
+
return leftAction;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Close button
|
|
106
|
+
if (onCloseClick) {
|
|
107
|
+
return (
|
|
108
|
+
<button
|
|
109
|
+
onClick={onCloseClick}
|
|
110
|
+
className="
|
|
111
|
+
flex items-center justify-center
|
|
112
|
+
w-10 h-10 -ml-2
|
|
113
|
+
text-ink-600 hover:text-ink-900
|
|
114
|
+
hover:bg-paper-100 rounded-full
|
|
115
|
+
transition-colors duration-200
|
|
116
|
+
active:bg-paper-200
|
|
117
|
+
"
|
|
118
|
+
aria-label="Close"
|
|
119
|
+
>
|
|
120
|
+
<X className="w-6 h-6" />
|
|
121
|
+
</button>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Back button
|
|
126
|
+
if (onBackClick) {
|
|
127
|
+
return (
|
|
128
|
+
<button
|
|
129
|
+
onClick={onBackClick}
|
|
130
|
+
className="
|
|
131
|
+
flex items-center justify-center
|
|
132
|
+
w-10 h-10 -ml-2
|
|
133
|
+
text-ink-600 hover:text-ink-900
|
|
134
|
+
hover:bg-paper-100 rounded-full
|
|
135
|
+
transition-colors duration-200
|
|
136
|
+
active:bg-paper-200
|
|
137
|
+
"
|
|
138
|
+
aria-label="Go back"
|
|
139
|
+
>
|
|
140
|
+
<ChevronLeft className="w-6 h-6" />
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Menu button (default)
|
|
146
|
+
if (onMenuClick) {
|
|
147
|
+
return (
|
|
148
|
+
<button
|
|
149
|
+
onClick={onMenuClick}
|
|
150
|
+
className="
|
|
151
|
+
flex items-center justify-center
|
|
152
|
+
w-10 h-10 -ml-2
|
|
153
|
+
text-ink-600 hover:text-ink-900
|
|
154
|
+
hover:bg-paper-100 rounded-full
|
|
155
|
+
transition-colors duration-200
|
|
156
|
+
active:bg-paper-200
|
|
157
|
+
"
|
|
158
|
+
aria-label="Open menu"
|
|
159
|
+
>
|
|
160
|
+
<Menu className="w-6 h-6" />
|
|
161
|
+
</button>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// No left button
|
|
166
|
+
return <div className="w-10 h-10" />;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Background variant styles
|
|
170
|
+
const variantStyles = {
|
|
171
|
+
solid: 'bg-white',
|
|
172
|
+
transparent: 'bg-transparent',
|
|
173
|
+
blur: 'bg-white/80 backdrop-blur-md',
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<header
|
|
178
|
+
className={`
|
|
179
|
+
${sticky ? 'sticky top-0 z-30' : ''}
|
|
180
|
+
${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}
|
|
181
|
+
${variantStyles[variant]}
|
|
182
|
+
${bordered ? 'border-b border-paper-200' : ''}
|
|
183
|
+
${className}
|
|
184
|
+
`}
|
|
185
|
+
>
|
|
186
|
+
<div className="flex items-center justify-between h-14 px-4">
|
|
187
|
+
{/* Left section */}
|
|
188
|
+
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
189
|
+
{renderLeftButton()}
|
|
190
|
+
|
|
191
|
+
{/* Title area */}
|
|
192
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
193
|
+
{subtitle && (
|
|
194
|
+
<span className="text-xs text-ink-500 truncate">
|
|
195
|
+
{subtitle}
|
|
196
|
+
</span>
|
|
197
|
+
)}
|
|
198
|
+
<h1 className="text-lg font-semibold text-ink-900 truncate leading-tight">
|
|
199
|
+
{title}
|
|
200
|
+
</h1>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{/* Right section */}
|
|
205
|
+
<div className="flex items-center gap-1 ml-2 flex-shrink-0">
|
|
206
|
+
{rightAction}
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</header>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* MobileHeaderSpacer - Spacer to prevent content from being hidden behind sticky MobileHeader
|
|
215
|
+
*
|
|
216
|
+
* Place this at the top of your content when NOT using sticky header
|
|
217
|
+
* to maintain consistent spacing.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```tsx
|
|
221
|
+
* <MobileHeader title="Page" sticky={false} />
|
|
222
|
+
* <MobileHeaderSpacer />
|
|
223
|
+
* <main>Content here</main>
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
export function MobileHeaderSpacer({ safeArea = true }: { safeArea?: boolean }) {
|
|
227
|
+
return (
|
|
228
|
+
<div
|
|
229
|
+
className={`h-14 ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}`}
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
}
|