@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,338 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import MobileLayout from './MobileLayout';
|
|
3
|
+
import { Home, CheckSquare, Settings, Users, Calendar, Bell, Search, Plus, User } from 'lucide-react';
|
|
4
|
+
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
5
|
+
import Text from './Text';
|
|
6
|
+
import Stack from './Stack';
|
|
7
|
+
import Button from './Button';
|
|
8
|
+
import Badge from './Badge';
|
|
9
|
+
|
|
10
|
+
const meta: Meta<typeof MobileLayout> = {
|
|
11
|
+
title: 'Layout/MobileLayout',
|
|
12
|
+
component: MobileLayout,
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'fullscreen',
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component: `
|
|
18
|
+
Auto-responsive layout that switches between desktop and mobile patterns:
|
|
19
|
+
|
|
20
|
+
- **Desktop (≥1024px)**: Standard sidebar layout with gutter and page navigation
|
|
21
|
+
- **Mobile/Tablet (<1024px)**: Mobile header with drawer navigation and bottom tab bar
|
|
22
|
+
|
|
23
|
+
Key features:
|
|
24
|
+
- Automatic viewport detection
|
|
25
|
+
- Drawer navigation on mobile
|
|
26
|
+
- Bottom navigation bar
|
|
27
|
+
- Safe area support for notched devices
|
|
28
|
+
- Force mobile/desktop mode for testing
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
decorators: [
|
|
34
|
+
(Story) => (
|
|
35
|
+
<div style={{ height: '100vh', overflow: 'hidden' }}>
|
|
36
|
+
<Story />
|
|
37
|
+
</div>
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof MobileLayout>;
|
|
44
|
+
|
|
45
|
+
const sampleSidebarItems = [
|
|
46
|
+
{ id: 'home', label: 'Dashboard', icon: <Home className="h-5 w-5" />, href: '/' },
|
|
47
|
+
{ id: 'tasks', label: 'Tasks', icon: <CheckSquare className="h-5 w-5" />, href: '/tasks', badge: 5 },
|
|
48
|
+
{ id: 'calendar', label: 'Calendar', icon: <Calendar className="h-5 w-5" />, href: '/calendar' },
|
|
49
|
+
{ id: 'users', label: 'Users', icon: <Users className="h-5 w-5" />, href: '/users' },
|
|
50
|
+
{ id: 'settings', label: 'Settings', icon: <Settings className="h-5 w-5" />, href: '/settings' },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const DemoContent = () => (
|
|
54
|
+
<Stack spacing="md" className="p-4">
|
|
55
|
+
<Card>
|
|
56
|
+
<CardHeader>
|
|
57
|
+
<CardTitle>Welcome to MobileLayout</CardTitle>
|
|
58
|
+
</CardHeader>
|
|
59
|
+
<CardContent>
|
|
60
|
+
<Text>
|
|
61
|
+
This layout automatically adapts to your screen size. On desktop, you'll see
|
|
62
|
+
the standard sidebar layout. On mobile, you'll see a hamburger menu, drawer
|
|
63
|
+
navigation, and bottom tab bar.
|
|
64
|
+
</Text>
|
|
65
|
+
</CardContent>
|
|
66
|
+
</Card>
|
|
67
|
+
|
|
68
|
+
<Card>
|
|
69
|
+
<CardHeader>
|
|
70
|
+
<CardTitle>Try Resizing</CardTitle>
|
|
71
|
+
</CardHeader>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<Text>
|
|
74
|
+
Resize your browser window or use Storybook's viewport controls to see the
|
|
75
|
+
layout switch between desktop and mobile modes.
|
|
76
|
+
</Text>
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
|
|
80
|
+
<Card>
|
|
81
|
+
<CardHeader>
|
|
82
|
+
<CardTitle>Sample Data Card</CardTitle>
|
|
83
|
+
</CardHeader>
|
|
84
|
+
<CardContent>
|
|
85
|
+
<Stack spacing="sm">
|
|
86
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
87
|
+
<div key={i} className="flex justify-between items-center p-2 bg-paper-50 rounded">
|
|
88
|
+
<Text>Item {i}</Text>
|
|
89
|
+
<Badge variant="primary">Active</Badge>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</Stack>
|
|
93
|
+
</CardContent>
|
|
94
|
+
</Card>
|
|
95
|
+
</Stack>
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const SimpleHeader = () => (
|
|
99
|
+
<div className="flex items-center gap-2 px-4 py-3">
|
|
100
|
+
<div className="w-8 h-8 rounded bg-accent-500 flex items-center justify-center text-white font-bold">
|
|
101
|
+
N
|
|
102
|
+
</div>
|
|
103
|
+
<Text weight="semibold">Notebook</Text>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const SimpleUserProfile = () => (
|
|
108
|
+
<div className="flex items-center gap-2 px-4 py-3 border-t border-paper-200">
|
|
109
|
+
<div className="w-8 h-8 rounded-full bg-paper-300 flex items-center justify-center">
|
|
110
|
+
<User className="h-4 w-4 text-ink-500" />
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<Text size="sm" weight="medium" className="truncate">John Doe</Text>
|
|
114
|
+
<Text size="xs" color="muted" className="truncate">john@example.com</Text>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Default layout - automatically switches between desktop and mobile based on viewport.
|
|
121
|
+
*/
|
|
122
|
+
export const Default: Story = {
|
|
123
|
+
args: {
|
|
124
|
+
sidebarItems: sampleSidebarItems,
|
|
125
|
+
currentPath: '/',
|
|
126
|
+
title: 'Dashboard',
|
|
127
|
+
header: <SimpleHeader />,
|
|
128
|
+
userProfile: <SimpleUserProfile />,
|
|
129
|
+
},
|
|
130
|
+
render: (args) => (
|
|
131
|
+
<MobileLayout {...args}>
|
|
132
|
+
<DemoContent />
|
|
133
|
+
</MobileLayout>
|
|
134
|
+
),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Force mobile layout regardless of viewport size - useful for testing.
|
|
139
|
+
*/
|
|
140
|
+
export const ForceMobile: Story = {
|
|
141
|
+
args: {
|
|
142
|
+
sidebarItems: sampleSidebarItems,
|
|
143
|
+
currentPath: '/',
|
|
144
|
+
title: 'Mobile Preview',
|
|
145
|
+
subtitle: 'Forced mobile layout',
|
|
146
|
+
header: <SimpleHeader />,
|
|
147
|
+
userProfile: <SimpleUserProfile />,
|
|
148
|
+
forceMobile: true,
|
|
149
|
+
headerRightAction: (
|
|
150
|
+
<Button variant="ghost" size="sm" iconOnly>
|
|
151
|
+
<Bell className="h-5 w-5" />
|
|
152
|
+
</Button>
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
render: (args) => (
|
|
156
|
+
<MobileLayout {...args}>
|
|
157
|
+
<DemoContent />
|
|
158
|
+
</MobileLayout>
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Force desktop layout regardless of viewport size - useful for testing.
|
|
164
|
+
*/
|
|
165
|
+
export const ForceDesktop: Story = {
|
|
166
|
+
args: {
|
|
167
|
+
sidebarItems: sampleSidebarItems,
|
|
168
|
+
currentPath: '/',
|
|
169
|
+
title: 'Desktop Preview',
|
|
170
|
+
header: <SimpleHeader />,
|
|
171
|
+
userProfile: <SimpleUserProfile />,
|
|
172
|
+
forceDesktop: true,
|
|
173
|
+
},
|
|
174
|
+
render: (args) => (
|
|
175
|
+
<MobileLayout {...args}>
|
|
176
|
+
<DemoContent />
|
|
177
|
+
</MobileLayout>
|
|
178
|
+
),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Mobile layout with custom bottom navigation items - useful when you want
|
|
183
|
+
* different items in the bottom nav than in the drawer.
|
|
184
|
+
*/
|
|
185
|
+
export const CustomBottomNav: Story = {
|
|
186
|
+
args: {
|
|
187
|
+
sidebarItems: sampleSidebarItems,
|
|
188
|
+
currentPath: '/',
|
|
189
|
+
title: 'My App',
|
|
190
|
+
forceMobile: true,
|
|
191
|
+
bottomNavItems: [
|
|
192
|
+
{ id: 'home', label: 'Home', icon: <Home className="h-5 w-5" />, href: '/' },
|
|
193
|
+
{ id: 'search', label: 'Search', icon: <Search className="h-5 w-5" />, href: '/search' },
|
|
194
|
+
{ id: 'add', label: 'Add', icon: <Plus className="h-5 w-5" />, onClick: () => alert('Add clicked!') },
|
|
195
|
+
{ id: 'notifications', label: 'Alerts', icon: <Bell className="h-5 w-5" />, href: '/notifications', badge: 3 },
|
|
196
|
+
{ id: 'profile', label: 'Profile', icon: <User className="h-5 w-5" />, href: '/profile' },
|
|
197
|
+
],
|
|
198
|
+
activeBottomNavId: 'home',
|
|
199
|
+
},
|
|
200
|
+
render: (args) => (
|
|
201
|
+
<MobileLayout {...args}>
|
|
202
|
+
<DemoContent />
|
|
203
|
+
</MobileLayout>
|
|
204
|
+
),
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Mobile layout without bottom navigation - useful for single-purpose screens.
|
|
209
|
+
*/
|
|
210
|
+
export const NoBottomNav: Story = {
|
|
211
|
+
args: {
|
|
212
|
+
sidebarItems: sampleSidebarItems,
|
|
213
|
+
currentPath: '/',
|
|
214
|
+
title: 'Detail View',
|
|
215
|
+
forceMobile: true,
|
|
216
|
+
hideBottomNav: true,
|
|
217
|
+
headerRightAction: (
|
|
218
|
+
<Button variant="primary" size="sm">
|
|
219
|
+
Save
|
|
220
|
+
</Button>
|
|
221
|
+
),
|
|
222
|
+
},
|
|
223
|
+
render: (args) => (
|
|
224
|
+
<MobileLayout {...args}>
|
|
225
|
+
<DemoContent />
|
|
226
|
+
</MobileLayout>
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Mobile layout with primary variant header - uses accent color background.
|
|
232
|
+
*/
|
|
233
|
+
export const PrimaryHeader: Story = {
|
|
234
|
+
args: {
|
|
235
|
+
sidebarItems: sampleSidebarItems,
|
|
236
|
+
currentPath: '/',
|
|
237
|
+
title: 'Notebook App',
|
|
238
|
+
subtitle: 'Your daily companion',
|
|
239
|
+
forceMobile: true,
|
|
240
|
+
headerVariant: 'primary',
|
|
241
|
+
headerRightAction: (
|
|
242
|
+
<Button variant="ghost" size="sm" iconOnly className="text-white hover:bg-white/10">
|
|
243
|
+
<Bell className="h-5 w-5" />
|
|
244
|
+
</Button>
|
|
245
|
+
),
|
|
246
|
+
},
|
|
247
|
+
render: (args) => (
|
|
248
|
+
<MobileLayout {...args}>
|
|
249
|
+
<DemoContent />
|
|
250
|
+
</MobileLayout>
|
|
251
|
+
),
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Desktop layout with page sections for gutter navigation.
|
|
256
|
+
*/
|
|
257
|
+
export const WithSections: Story = {
|
|
258
|
+
args: {
|
|
259
|
+
sidebarItems: sampleSidebarItems,
|
|
260
|
+
currentPath: '/',
|
|
261
|
+
title: 'With Sections',
|
|
262
|
+
header: <SimpleHeader />,
|
|
263
|
+
userProfile: <SimpleUserProfile />,
|
|
264
|
+
forceDesktop: true,
|
|
265
|
+
sections: [
|
|
266
|
+
{ id: 'overview', label: 'Overview' },
|
|
267
|
+
{ id: 'stats', label: 'Statistics' },
|
|
268
|
+
{ id: 'activity', label: 'Activity' },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
render: (args) => (
|
|
272
|
+
<MobileLayout {...args}>
|
|
273
|
+
<div className="p-4 space-y-8">
|
|
274
|
+
<section id="overview">
|
|
275
|
+
<Card>
|
|
276
|
+
<CardHeader>
|
|
277
|
+
<CardTitle>Overview</CardTitle>
|
|
278
|
+
</CardHeader>
|
|
279
|
+
<CardContent>
|
|
280
|
+
<Text>Overview section content goes here.</Text>
|
|
281
|
+
</CardContent>
|
|
282
|
+
</Card>
|
|
283
|
+
</section>
|
|
284
|
+
|
|
285
|
+
<section id="stats">
|
|
286
|
+
<Card>
|
|
287
|
+
<CardHeader>
|
|
288
|
+
<CardTitle>Statistics</CardTitle>
|
|
289
|
+
</CardHeader>
|
|
290
|
+
<CardContent>
|
|
291
|
+
<Text>Statistics section content goes here.</Text>
|
|
292
|
+
</CardContent>
|
|
293
|
+
</Card>
|
|
294
|
+
</section>
|
|
295
|
+
|
|
296
|
+
<section id="activity">
|
|
297
|
+
<Card>
|
|
298
|
+
<CardHeader>
|
|
299
|
+
<CardTitle>Activity</CardTitle>
|
|
300
|
+
</CardHeader>
|
|
301
|
+
<CardContent>
|
|
302
|
+
<Text>Activity section content goes here.</Text>
|
|
303
|
+
</CardContent>
|
|
304
|
+
</Card>
|
|
305
|
+
</section>
|
|
306
|
+
</div>
|
|
307
|
+
</MobileLayout>
|
|
308
|
+
),
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Mobile layout without header - useful for full-screen content.
|
|
313
|
+
*/
|
|
314
|
+
export const NoHeader: Story = {
|
|
315
|
+
args: {
|
|
316
|
+
sidebarItems: sampleSidebarItems,
|
|
317
|
+
currentPath: '/',
|
|
318
|
+
forceMobile: true,
|
|
319
|
+
hideMobileHeader: true,
|
|
320
|
+
},
|
|
321
|
+
render: (args) => (
|
|
322
|
+
<MobileLayout {...args}>
|
|
323
|
+
<div className="p-4">
|
|
324
|
+
<Card>
|
|
325
|
+
<CardHeader>
|
|
326
|
+
<CardTitle>Full Screen Content</CardTitle>
|
|
327
|
+
</CardHeader>
|
|
328
|
+
<CardContent>
|
|
329
|
+
<Text>
|
|
330
|
+
This layout has no mobile header, giving you full control over the
|
|
331
|
+
screen. The drawer can still be accessed programmatically.
|
|
332
|
+
</Text>
|
|
333
|
+
</CardContent>
|
|
334
|
+
</Card>
|
|
335
|
+
</div>
|
|
336
|
+
</MobileLayout>
|
|
337
|
+
),
|
|
338
|
+
};
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
|
|
2
|
+
// Desktop: Standard sidebar layout
|
|
3
|
+
// Mobile: Drawer navigation + bottom navigation bar + mobile header
|
|
4
|
+
|
|
5
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
6
|
+
import { useIsMobile, useIsTablet } from '../hooks/useResponsive';
|
|
7
|
+
import Sidebar, { SidebarItem } from './Sidebar';
|
|
8
|
+
import MobileHeader, { MobileHeaderProps } from './MobileHeader';
|
|
9
|
+
import BottomNavigation, { BottomNavItem, BottomNavigationSpacer } from './BottomNavigation';
|
|
10
|
+
import { PageNavigation } from './PageNavigation';
|
|
11
|
+
|
|
12
|
+
export interface Section {
|
|
13
|
+
/** Unique identifier for the section */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Display label for the section in navigation */
|
|
16
|
+
label: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MobileLayoutProps {
|
|
20
|
+
/** Main page content */
|
|
21
|
+
children: React.ReactNode;
|
|
22
|
+
|
|
23
|
+
// Desktop sidebar props
|
|
24
|
+
/** Sidebar navigation items (required for both desktop sidebar and mobile drawer) */
|
|
25
|
+
sidebarItems: SidebarItem[];
|
|
26
|
+
/** Current active path for highlighting */
|
|
27
|
+
currentPath?: string;
|
|
28
|
+
/** Handler for navigation clicks */
|
|
29
|
+
onNavigate?: (href: string) => void;
|
|
30
|
+
/** Header component for sidebar (logo, branding, etc.) */
|
|
31
|
+
header?: React.ReactNode;
|
|
32
|
+
/** User profile button for sidebar footer */
|
|
33
|
+
userProfile?: React.ReactNode;
|
|
34
|
+
/** Additional sidebar content */
|
|
35
|
+
sidebarFooter?: React.ReactNode;
|
|
36
|
+
|
|
37
|
+
// Mobile header props
|
|
38
|
+
/** Title displayed in mobile header (required for mobile layout) */
|
|
39
|
+
title: string;
|
|
40
|
+
/** Subtitle displayed in mobile header */
|
|
41
|
+
subtitle?: string;
|
|
42
|
+
/** Right action for mobile header */
|
|
43
|
+
headerRightAction?: React.ReactNode;
|
|
44
|
+
/** Custom left action for mobile header (overrides menu button) */
|
|
45
|
+
headerLeftAction?: React.ReactNode;
|
|
46
|
+
/** Mobile header variant */
|
|
47
|
+
headerVariant?: MobileHeaderProps['variant'];
|
|
48
|
+
|
|
49
|
+
// Bottom navigation props
|
|
50
|
+
/** Bottom navigation items for mobile (if not provided, uses sidebarItems) */
|
|
51
|
+
bottomNavItems?: BottomNavItem[];
|
|
52
|
+
/** Active bottom nav item ID */
|
|
53
|
+
activeBottomNavId?: string;
|
|
54
|
+
/** Show labels on bottom nav */
|
|
55
|
+
showBottomNavLabels?: boolean;
|
|
56
|
+
|
|
57
|
+
// Layout options
|
|
58
|
+
/** Optional status bar component displayed at the bottom (desktop only) */
|
|
59
|
+
statusBar?: React.ReactNode;
|
|
60
|
+
/** Additional CSS classes */
|
|
61
|
+
className?: string;
|
|
62
|
+
/** Page sections for navigation dots in desktop gutter */
|
|
63
|
+
sections?: Section[];
|
|
64
|
+
/** Force mobile layout even on desktop */
|
|
65
|
+
forceMobile?: boolean;
|
|
66
|
+
/** Force desktop layout even on mobile */
|
|
67
|
+
forceDesktop?: boolean;
|
|
68
|
+
/** Hide bottom navigation on mobile */
|
|
69
|
+
hideBottomNav?: boolean;
|
|
70
|
+
/** Hide mobile header */
|
|
71
|
+
hideMobileHeader?: boolean;
|
|
72
|
+
/** Use safe area insets for notched devices */
|
|
73
|
+
safeArea?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
|
|
78
|
+
*
|
|
79
|
+
* This component automatically detects the viewport size and renders the appropriate layout:
|
|
80
|
+
* - **Desktop** (≥1024px): Standard Layout with sidebar, gutter, and scrollable content
|
|
81
|
+
* - **Mobile/Tablet** (<1024px): Mobile header, drawer navigation, bottom tab bar
|
|
82
|
+
*
|
|
83
|
+
* The mobile layout features:
|
|
84
|
+
* - Sticky header with hamburger menu to open drawer
|
|
85
|
+
* - Sidebar rendered as a slide-in drawer
|
|
86
|
+
* - Bottom navigation bar for primary navigation
|
|
87
|
+
* - Safe area support for notched devices
|
|
88
|
+
*
|
|
89
|
+
* @example Basic usage
|
|
90
|
+
* ```tsx
|
|
91
|
+
* <MobileLayout
|
|
92
|
+
* sidebarItems={[
|
|
93
|
+
* { id: 'home', label: 'Home', icon: <Home />, href: '/' },
|
|
94
|
+
* { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks' },
|
|
95
|
+
* { id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' }
|
|
96
|
+
* ]}
|
|
97
|
+
* currentPath={location.pathname}
|
|
98
|
+
* onNavigate={(href) => navigate(href)}
|
|
99
|
+
* title="My App"
|
|
100
|
+
* header={<Logo />}
|
|
101
|
+
* userProfile={<UserProfileButton user={user} />}
|
|
102
|
+
* >
|
|
103
|
+
* <Page>
|
|
104
|
+
* <h1>Dashboard</h1>
|
|
105
|
+
* </Page>
|
|
106
|
+
* </MobileLayout>
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @example With custom bottom nav items
|
|
110
|
+
* ```tsx
|
|
111
|
+
* <MobileLayout
|
|
112
|
+
* sidebarItems={fullNavItems}
|
|
113
|
+
* bottomNavItems={[
|
|
114
|
+
* { id: 'home', label: 'Home', icon: <Home />, href: '/' },
|
|
115
|
+
* { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
|
|
116
|
+
* { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' }
|
|
117
|
+
* ]}
|
|
118
|
+
* currentPath={location.pathname}
|
|
119
|
+
* title="My App"
|
|
120
|
+
* >
|
|
121
|
+
* {children}
|
|
122
|
+
* </MobileLayout>
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example Force mobile layout for testing
|
|
126
|
+
* ```tsx
|
|
127
|
+
* <MobileLayout
|
|
128
|
+
* sidebarItems={items}
|
|
129
|
+
* title="Mobile Preview"
|
|
130
|
+
* forceMobile
|
|
131
|
+
* >
|
|
132
|
+
* {children}
|
|
133
|
+
* </MobileLayout>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export const MobileLayout: React.FC<MobileLayoutProps> = ({
|
|
137
|
+
children,
|
|
138
|
+
sidebarItems,
|
|
139
|
+
currentPath,
|
|
140
|
+
onNavigate,
|
|
141
|
+
header,
|
|
142
|
+
userProfile,
|
|
143
|
+
sidebarFooter,
|
|
144
|
+
title,
|
|
145
|
+
subtitle,
|
|
146
|
+
headerRightAction,
|
|
147
|
+
headerLeftAction,
|
|
148
|
+
headerVariant = 'solid',
|
|
149
|
+
bottomNavItems,
|
|
150
|
+
activeBottomNavId,
|
|
151
|
+
showBottomNavLabels = true,
|
|
152
|
+
statusBar,
|
|
153
|
+
className = '',
|
|
154
|
+
sections,
|
|
155
|
+
forceMobile = false,
|
|
156
|
+
forceDesktop = false,
|
|
157
|
+
hideBottomNav = false,
|
|
158
|
+
hideMobileHeader = false,
|
|
159
|
+
safeArea = true,
|
|
160
|
+
}) => {
|
|
161
|
+
const isMobileViewport = useIsMobile();
|
|
162
|
+
const isTabletViewport = useIsTablet();
|
|
163
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
164
|
+
|
|
165
|
+
// Determine if we should use mobile layout
|
|
166
|
+
const useMobileLayout = forceDesktop
|
|
167
|
+
? false
|
|
168
|
+
: forceMobile || isMobileViewport || isTabletViewport;
|
|
169
|
+
|
|
170
|
+
// Open/close drawer
|
|
171
|
+
const openDrawer = useCallback(() => setDrawerOpen(true), []);
|
|
172
|
+
const closeDrawer = useCallback(() => setDrawerOpen(false), []);
|
|
173
|
+
|
|
174
|
+
// Handle navigation from drawer - close drawer after navigation
|
|
175
|
+
const handleDrawerNavigate = useCallback((href: string) => {
|
|
176
|
+
closeDrawer();
|
|
177
|
+
onNavigate?.(href);
|
|
178
|
+
}, [closeDrawer, onNavigate]);
|
|
179
|
+
|
|
180
|
+
// Handle bottom nav navigation - matches BottomNavigation's onNavigate signature
|
|
181
|
+
const handleBottomNavNavigate = useCallback((id: string, href?: string) => {
|
|
182
|
+
if (href) {
|
|
183
|
+
onNavigate?.(href);
|
|
184
|
+
}
|
|
185
|
+
// Also check if there's a custom onClick in the bottom nav items
|
|
186
|
+
const item = bottomNavItems?.find(i => i.id === id);
|
|
187
|
+
item?.onClick?.();
|
|
188
|
+
}, [onNavigate, bottomNavItems]);
|
|
189
|
+
|
|
190
|
+
// Convert sidebar items to bottom nav items if not provided
|
|
191
|
+
const effectiveBottomNavItems: BottomNavItem[] = bottomNavItems || sidebarItems
|
|
192
|
+
.filter(item => !item.children && item.href) // Only top-level items with href
|
|
193
|
+
.slice(0, 5) // Max 5 items for bottom nav
|
|
194
|
+
.map(item => ({
|
|
195
|
+
id: item.id,
|
|
196
|
+
label: item.label,
|
|
197
|
+
icon: item.icon,
|
|
198
|
+
href: item.href,
|
|
199
|
+
badge: typeof item.badge === 'number' ? item.badge : undefined,
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
// Determine active bottom nav ID
|
|
203
|
+
const effectiveActiveBottomNavId = activeBottomNavId ||
|
|
204
|
+
effectiveBottomNavItems.find(item => item.href === currentPath)?.id;
|
|
205
|
+
|
|
206
|
+
// Close drawer on escape key
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
if (!drawerOpen) return;
|
|
209
|
+
|
|
210
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
211
|
+
if (e.key === 'Escape') {
|
|
212
|
+
closeDrawer();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
window.addEventListener('keydown', handleEscape);
|
|
217
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
218
|
+
}, [drawerOpen, closeDrawer]);
|
|
219
|
+
|
|
220
|
+
// Desktop Layout
|
|
221
|
+
if (!useMobileLayout) {
|
|
222
|
+
return (
|
|
223
|
+
<div className={`h-screen flex flex-col bg-paper-100 ${className}`}>
|
|
224
|
+
{/* Main layout - sidebar, gutter, and content */}
|
|
225
|
+
<div className="flex flex-1 overflow-hidden relative">
|
|
226
|
+
{/* Sidebar */}
|
|
227
|
+
<Sidebar
|
|
228
|
+
items={sidebarItems}
|
|
229
|
+
currentPath={currentPath}
|
|
230
|
+
onNavigate={onNavigate}
|
|
231
|
+
header={header}
|
|
232
|
+
footer={
|
|
233
|
+
<>
|
|
234
|
+
{userProfile}
|
|
235
|
+
{sidebarFooter}
|
|
236
|
+
</>
|
|
237
|
+
}
|
|
238
|
+
/>
|
|
239
|
+
|
|
240
|
+
{/* Gutter area - between sidebar and content with page navigation */}
|
|
241
|
+
<div className="w-8 h-full bg-paper-100 flex-shrink-0 relative flex items-center justify-center">
|
|
242
|
+
<PageNavigation sections={sections} />
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
{/* Main content area - scrollable */}
|
|
246
|
+
<div className="flex-1 overflow-auto">
|
|
247
|
+
{children}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
{/* Status Bar - at bottom (optional) */}
|
|
252
|
+
{statusBar}
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Mobile Layout
|
|
258
|
+
return (
|
|
259
|
+
<div className={`min-h-screen flex flex-col bg-paper-100 ${className}`}>
|
|
260
|
+
{/* Mobile Header */}
|
|
261
|
+
{!hideMobileHeader && (
|
|
262
|
+
<MobileHeader
|
|
263
|
+
title={title}
|
|
264
|
+
subtitle={subtitle}
|
|
265
|
+
onMenuClick={headerLeftAction ? undefined : openDrawer}
|
|
266
|
+
leftAction={headerLeftAction}
|
|
267
|
+
rightAction={headerRightAction}
|
|
268
|
+
variant={headerVariant}
|
|
269
|
+
sticky
|
|
270
|
+
bordered
|
|
271
|
+
safeArea={safeArea}
|
|
272
|
+
/>
|
|
273
|
+
)}
|
|
274
|
+
|
|
275
|
+
{/* Drawer Sidebar */}
|
|
276
|
+
<Sidebar
|
|
277
|
+
items={sidebarItems}
|
|
278
|
+
currentPath={currentPath}
|
|
279
|
+
onNavigate={handleDrawerNavigate}
|
|
280
|
+
header={header}
|
|
281
|
+
footer={
|
|
282
|
+
<>
|
|
283
|
+
{userProfile}
|
|
284
|
+
{sidebarFooter}
|
|
285
|
+
</>
|
|
286
|
+
}
|
|
287
|
+
mobileOpen={drawerOpen}
|
|
288
|
+
onMobileClose={closeDrawer}
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
{/* Main content area */}
|
|
292
|
+
<div className="flex-1 overflow-auto">
|
|
293
|
+
{children}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Bottom Navigation */}
|
|
297
|
+
{!hideBottomNav && effectiveBottomNavItems.length > 0 && (
|
|
298
|
+
<>
|
|
299
|
+
<BottomNavigationSpacer />
|
|
300
|
+
<BottomNavigation
|
|
301
|
+
items={effectiveBottomNavItems}
|
|
302
|
+
activeId={effectiveActiveBottomNavId}
|
|
303
|
+
onNavigate={handleBottomNavNavigate}
|
|
304
|
+
showLabels={showBottomNavLabels}
|
|
305
|
+
safeArea={safeArea}
|
|
306
|
+
/>
|
|
307
|
+
</>
|
|
308
|
+
)}
|
|
309
|
+
</div>
|
|
310
|
+
);
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export default MobileLayout;
|