@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,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;