@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
@@ -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-2">
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="text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto"
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="text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto"
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
+ }