@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,383 @@
|
|
|
1
|
+
import { useState, useEffect, ReactNode } from 'react';
|
|
2
|
+
import { ChevronUp, ChevronDown } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
export interface ExpandablePanelProps {
|
|
5
|
+
/** Content shown in the collapsed header bar */
|
|
6
|
+
collapsedContent: ReactNode;
|
|
7
|
+
/** Full content shown when expanded */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** Position of the panel */
|
|
10
|
+
position?: 'bottom' | 'top';
|
|
11
|
+
/**
|
|
12
|
+
* Positioning mode:
|
|
13
|
+
* - 'viewport': Fixed to viewport edges (default, for standalone use)
|
|
14
|
+
* - 'container': Sticky within parent container (for use inside Page/AppLayout)
|
|
15
|
+
*/
|
|
16
|
+
mode?: 'viewport' | 'container';
|
|
17
|
+
/** Whether the panel is expanded (controlled) */
|
|
18
|
+
expanded?: boolean;
|
|
19
|
+
/** Default expanded state (uncontrolled) */
|
|
20
|
+
defaultExpanded?: boolean;
|
|
21
|
+
/** Callback when expanded state changes */
|
|
22
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
23
|
+
/** Height when expanded */
|
|
24
|
+
expandedHeight?: string | number;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum width of the panel (e.g., '1400px', '80%', 1200)
|
|
27
|
+
* When set, the panel will be centered horizontally within its container/viewport
|
|
28
|
+
*/
|
|
29
|
+
maxWidth?: string | number;
|
|
30
|
+
/** Whether to show the expand/collapse toggle button */
|
|
31
|
+
showToggle?: boolean;
|
|
32
|
+
/** Custom toggle button content */
|
|
33
|
+
toggleContent?: ReactNode;
|
|
34
|
+
/** Additional actions to show in the header (right side) */
|
|
35
|
+
headerActions?: ReactNode;
|
|
36
|
+
/** Close on Escape key */
|
|
37
|
+
closeOnEscape?: boolean;
|
|
38
|
+
/** Visual variant */
|
|
39
|
+
variant?: 'default' | 'elevated' | 'bordered';
|
|
40
|
+
/** Size variant affecting header height */
|
|
41
|
+
size?: 'sm' | 'md' | 'lg';
|
|
42
|
+
/** Additional CSS classes for the container */
|
|
43
|
+
className?: string;
|
|
44
|
+
/** Additional CSS classes for the header */
|
|
45
|
+
headerClassName?: string;
|
|
46
|
+
/** Additional CSS classes for the content */
|
|
47
|
+
contentClassName?: string;
|
|
48
|
+
/** Z-index for the panel (only applies in viewport mode) */
|
|
49
|
+
zIndex?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const sizeClasses = {
|
|
53
|
+
sm: {
|
|
54
|
+
header: 'h-10 px-3',
|
|
55
|
+
text: 'text-sm',
|
|
56
|
+
icon: 'h-4 w-4',
|
|
57
|
+
},
|
|
58
|
+
md: {
|
|
59
|
+
header: 'h-12 px-4',
|
|
60
|
+
text: 'text-sm',
|
|
61
|
+
icon: 'h-5 w-5',
|
|
62
|
+
},
|
|
63
|
+
lg: {
|
|
64
|
+
header: 'h-14 px-5',
|
|
65
|
+
text: 'text-base',
|
|
66
|
+
icon: 'h-5 w-5',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const variantClasses = {
|
|
71
|
+
default: {
|
|
72
|
+
container: 'bg-white border-ink-200',
|
|
73
|
+
header: 'bg-paper-50',
|
|
74
|
+
},
|
|
75
|
+
elevated: {
|
|
76
|
+
container: 'bg-white shadow-lg border-ink-200',
|
|
77
|
+
header: 'bg-white',
|
|
78
|
+
},
|
|
79
|
+
bordered: {
|
|
80
|
+
container: 'bg-white border-2 border-ink-300',
|
|
81
|
+
header: 'bg-paper-100',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ExpandablePanel - A panel that sticks to the bottom (or top) and can expand/collapse
|
|
87
|
+
*
|
|
88
|
+
* For bottom position: expands UPWARD (content appears above header)
|
|
89
|
+
* For top position: expands DOWNWARD (content appears below header)
|
|
90
|
+
*
|
|
91
|
+
* Two modes of operation:
|
|
92
|
+
* - `viewport`: Fixed to the viewport (for standalone pages, covers StatusBar)
|
|
93
|
+
* - `container`: Sticky within its parent container (for use inside Page/AppLayout, respects StatusBar)
|
|
94
|
+
*
|
|
95
|
+
* @example Basic usage (viewport mode - full page)
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <ExpandablePanel
|
|
98
|
+
* mode="viewport"
|
|
99
|
+
* collapsedContent={<Text>3 items selected</Text>}
|
|
100
|
+
* expandedHeight="300px"
|
|
101
|
+
* >
|
|
102
|
+
* {content}
|
|
103
|
+
* </ExpandablePanel>
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example Inside Page/AppLayout (container mode - respects StatusBar)
|
|
107
|
+
* ```tsx
|
|
108
|
+
* <Page>
|
|
109
|
+
* <ExpandablePanelContainer>
|
|
110
|
+
* <div className="flex-1 overflow-auto">
|
|
111
|
+
* {pageContent}
|
|
112
|
+
* </div>
|
|
113
|
+
* <ExpandablePanel
|
|
114
|
+
* mode="container"
|
|
115
|
+
* collapsedContent={<Text>3 items selected</Text>}
|
|
116
|
+
* expandedHeight="300px"
|
|
117
|
+
* >
|
|
118
|
+
* {selectedItemsContent}
|
|
119
|
+
* </ExpandablePanel>
|
|
120
|
+
* </ExpandablePanelContainer>
|
|
121
|
+
* </Page>
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @example With maxWidth to match page content
|
|
125
|
+
* ```tsx
|
|
126
|
+
* <ExpandablePanel
|
|
127
|
+
* mode="container"
|
|
128
|
+
* maxWidth="1400px"
|
|
129
|
+
* collapsedContent={<Text>Generated SQL</Text>}
|
|
130
|
+
* expandedHeight="300px"
|
|
131
|
+
* >
|
|
132
|
+
* {sqlContent}
|
|
133
|
+
* </ExpandablePanel>
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export default function ExpandablePanel({
|
|
137
|
+
collapsedContent,
|
|
138
|
+
children,
|
|
139
|
+
position = 'bottom',
|
|
140
|
+
mode = 'viewport',
|
|
141
|
+
expanded: controlledExpanded,
|
|
142
|
+
defaultExpanded = false,
|
|
143
|
+
onExpandedChange,
|
|
144
|
+
expandedHeight = '300px',
|
|
145
|
+
maxWidth,
|
|
146
|
+
showToggle = true,
|
|
147
|
+
toggleContent,
|
|
148
|
+
headerActions,
|
|
149
|
+
closeOnEscape = true,
|
|
150
|
+
variant = 'elevated',
|
|
151
|
+
size = 'md',
|
|
152
|
+
className = '',
|
|
153
|
+
headerClassName = '',
|
|
154
|
+
contentClassName = '',
|
|
155
|
+
zIndex = 40,
|
|
156
|
+
}: ExpandablePanelProps) {
|
|
157
|
+
const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
|
|
158
|
+
|
|
159
|
+
// Determine if controlled or uncontrolled
|
|
160
|
+
const isControlled = controlledExpanded !== undefined;
|
|
161
|
+
const expanded = isControlled ? controlledExpanded : internalExpanded;
|
|
162
|
+
|
|
163
|
+
const setExpanded = (value: boolean) => {
|
|
164
|
+
if (!isControlled) {
|
|
165
|
+
setInternalExpanded(value);
|
|
166
|
+
}
|
|
167
|
+
onExpandedChange?.(value);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const toggleExpanded = () => {
|
|
171
|
+
setExpanded(!expanded);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Close on Escape
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!closeOnEscape || !expanded) return;
|
|
177
|
+
|
|
178
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
179
|
+
if (e.key === 'Escape') {
|
|
180
|
+
setExpanded(false);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
document.addEventListener('keydown', handleEscape);
|
|
185
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
186
|
+
}, [closeOnEscape, expanded]);
|
|
187
|
+
|
|
188
|
+
const sizeStyle = sizeClasses[size];
|
|
189
|
+
const variantStyle = variantClasses[variant];
|
|
190
|
+
|
|
191
|
+
const heightValue = typeof expandedHeight === 'number' ? `${expandedHeight}px` : expandedHeight;
|
|
192
|
+
const maxWidthValue = maxWidth
|
|
193
|
+
? (typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)
|
|
194
|
+
: undefined;
|
|
195
|
+
|
|
196
|
+
// Position classes differ based on mode
|
|
197
|
+
const getPositionClasses = () => {
|
|
198
|
+
if (mode === 'viewport') {
|
|
199
|
+
// Fixed to viewport
|
|
200
|
+
return position === 'bottom'
|
|
201
|
+
? 'fixed bottom-0 left-0 right-0'
|
|
202
|
+
: 'fixed top-0 left-0 right-0';
|
|
203
|
+
} else {
|
|
204
|
+
// Absolute positioning within container - snaps to bottom
|
|
205
|
+
return position === 'bottom'
|
|
206
|
+
? 'absolute bottom-0 left-0 right-0'
|
|
207
|
+
: 'absolute top-0 left-0 right-0';
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// For bottom panel, we want chevron up to expand (reveal content above)
|
|
212
|
+
// For top panel, we want chevron down to expand (reveal content below)
|
|
213
|
+
const ChevronIcon = position === 'bottom'
|
|
214
|
+
? (expanded ? ChevronDown : ChevronUp)
|
|
215
|
+
: (expanded ? ChevronUp : ChevronDown);
|
|
216
|
+
|
|
217
|
+
// Header component
|
|
218
|
+
const header = (
|
|
219
|
+
<div
|
|
220
|
+
className={`
|
|
221
|
+
flex items-center justify-between
|
|
222
|
+
${sizeStyle.header}
|
|
223
|
+
${variantStyle.header}
|
|
224
|
+
border-ink-200
|
|
225
|
+
flex-shrink-0
|
|
226
|
+
${headerClassName}
|
|
227
|
+
`}
|
|
228
|
+
>
|
|
229
|
+
{/* Left side: collapsed content */}
|
|
230
|
+
<div className={`flex-1 flex items-center ${sizeStyle.text}`}>
|
|
231
|
+
{collapsedContent}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
{/* Right side: actions and toggle */}
|
|
235
|
+
<div className="flex items-center gap-2">
|
|
236
|
+
{headerActions}
|
|
237
|
+
|
|
238
|
+
{showToggle && (
|
|
239
|
+
<button
|
|
240
|
+
type="button"
|
|
241
|
+
onClick={toggleExpanded}
|
|
242
|
+
className={`
|
|
243
|
+
flex items-center justify-center
|
|
244
|
+
p-1.5 rounded-md
|
|
245
|
+
text-ink-500 hover:text-ink-700
|
|
246
|
+
hover:bg-ink-100
|
|
247
|
+
transition-colors
|
|
248
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1
|
|
249
|
+
`}
|
|
250
|
+
aria-expanded={expanded}
|
|
251
|
+
aria-label={expanded ? 'Collapse panel' : 'Expand panel'}
|
|
252
|
+
>
|
|
253
|
+
{toggleContent || <ChevronIcon className={sizeStyle.icon} />}
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Content component
|
|
261
|
+
const content = (
|
|
262
|
+
<div
|
|
263
|
+
className={`
|
|
264
|
+
overflow-hidden
|
|
265
|
+
transition-all duration-300 ease-in-out
|
|
266
|
+
`}
|
|
267
|
+
style={{
|
|
268
|
+
maxHeight: expanded ? heightValue : '0px',
|
|
269
|
+
opacity: expanded ? 1 : 0,
|
|
270
|
+
}}
|
|
271
|
+
>
|
|
272
|
+
<div
|
|
273
|
+
className={`
|
|
274
|
+
overflow-y-auto p-4
|
|
275
|
+
${contentClassName}
|
|
276
|
+
`}
|
|
277
|
+
style={{ maxHeight: heightValue }}
|
|
278
|
+
>
|
|
279
|
+
{children}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Build container styles
|
|
285
|
+
const containerStyle: React.CSSProperties = {
|
|
286
|
+
...(mode === 'viewport' ? { zIndex } : {}),
|
|
287
|
+
...(maxWidthValue ? {
|
|
288
|
+
maxWidth: maxWidthValue,
|
|
289
|
+
marginLeft: 'auto',
|
|
290
|
+
marginRight: 'auto'
|
|
291
|
+
} : {}),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
className={`
|
|
297
|
+
${getPositionClasses()}
|
|
298
|
+
${variantStyle.container}
|
|
299
|
+
border-t rounded-t-lg
|
|
300
|
+
transition-all duration-300 ease-in-out
|
|
301
|
+
flex flex-col
|
|
302
|
+
${className}
|
|
303
|
+
`}
|
|
304
|
+
style={containerStyle}
|
|
305
|
+
>
|
|
306
|
+
{/* For bottom position: content ABOVE header (expands up) */}
|
|
307
|
+
{/* For top position: header ABOVE content (expands down) */}
|
|
308
|
+
{position === 'bottom' ? (
|
|
309
|
+
<>
|
|
310
|
+
{content}
|
|
311
|
+
{header}
|
|
312
|
+
</>
|
|
313
|
+
) : (
|
|
314
|
+
<>
|
|
315
|
+
{header}
|
|
316
|
+
{content}
|
|
317
|
+
</>
|
|
318
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* ExpandablePanelSpacer - Adds spacing to prevent content from being hidden behind the panel
|
|
325
|
+
* Only needed in viewport mode. In container mode, the panel is part of the flex layout.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* ```tsx
|
|
329
|
+
* <div>
|
|
330
|
+
* <MainContent />
|
|
331
|
+
* <ExpandablePanelSpacer size="md" />
|
|
332
|
+
* </div>
|
|
333
|
+
* <ExpandablePanel mode="viewport" position="bottom" size="md" {...props} />
|
|
334
|
+
* ```
|
|
335
|
+
*/
|
|
336
|
+
export function ExpandablePanelSpacer({
|
|
337
|
+
size = 'md'
|
|
338
|
+
}: {
|
|
339
|
+
size?: 'sm' | 'md' | 'lg'
|
|
340
|
+
}) {
|
|
341
|
+
const heights = {
|
|
342
|
+
sm: 'h-10',
|
|
343
|
+
md: 'h-12',
|
|
344
|
+
lg: 'h-14',
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
return <div className={heights[size]} />;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* ExpandablePanelContainer - Wrapper that sets up proper layout for container mode
|
|
352
|
+
* Use this to wrap your page content when using ExpandablePanel with mode="container"
|
|
353
|
+
*
|
|
354
|
+
* This creates a relative container with full height so the panel can position absolutely
|
|
355
|
+
* at the bottom while the content scrolls above it.
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* ```tsx
|
|
359
|
+
* <Page>
|
|
360
|
+
* <ExpandablePanelContainer>
|
|
361
|
+
* <div className="flex-1 overflow-auto p-4">
|
|
362
|
+
* {pageContent}
|
|
363
|
+
* </div>
|
|
364
|
+
* <ExpandablePanel mode="container" {...props}>
|
|
365
|
+
* {panelContent}
|
|
366
|
+
* </ExpandablePanel>
|
|
367
|
+
* </ExpandablePanelContainer>
|
|
368
|
+
* </Page>
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
export function ExpandablePanelContainer({
|
|
372
|
+
children,
|
|
373
|
+
className = '',
|
|
374
|
+
}: {
|
|
375
|
+
children: ReactNode;
|
|
376
|
+
className?: string;
|
|
377
|
+
}) {
|
|
378
|
+
return (
|
|
379
|
+
<div className={`relative h-full overflow-hidden ${className}`}>
|
|
380
|
+
{children}
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Plus, Camera, Upload, FileText, Edit, Share, Trash, MessageSquare } from 'lucide-react';
|
|
3
|
+
import FloatingActionButton from './FloatingActionButton';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FloatingActionButton> = {
|
|
6
|
+
title: 'Mobile/FloatingActionButton',
|
|
7
|
+
component: FloatingActionButton,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'fullscreen',
|
|
10
|
+
viewport: {
|
|
11
|
+
defaultViewport: 'mobile1',
|
|
12
|
+
},
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component: 'Material Design style floating action button (FAB) for primary actions on mobile screens.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
decorators: [
|
|
20
|
+
(Story) => (
|
|
21
|
+
<div style={{ minHeight: '100vh', padding: '16px', background: '#f5f5f4' }}>
|
|
22
|
+
<h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>Page Content</h1>
|
|
23
|
+
<p style={{ color: '#666', marginBottom: '16px' }}>The FAB floats above all content.</p>
|
|
24
|
+
{Array.from({ length: 15 }).map((_, i) => (
|
|
25
|
+
<div key={i} style={{ padding: '16px', margin: '8px 0', background: 'white', borderRadius: '8px' }}>
|
|
26
|
+
Item {i + 1}
|
|
27
|
+
</div>
|
|
28
|
+
))}
|
|
29
|
+
<Story />
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof FloatingActionButton>;
|
|
37
|
+
|
|
38
|
+
export const Default: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
onClick: () => console.log('FAB clicked'),
|
|
41
|
+
label: 'Add item',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CustomIcon: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
onClick: () => console.log('Edit clicked'),
|
|
48
|
+
icon: <Edit className="w-6 h-6" />,
|
|
49
|
+
label: 'Edit',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const WithActionMenu: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
actions: [
|
|
56
|
+
{ id: 'camera', icon: <Camera className="w-5 h-5" />, label: 'Take Photo', onClick: () => console.log('Camera') },
|
|
57
|
+
{ id: 'upload', icon: <Upload className="w-5 h-5" />, label: 'Upload File', onClick: () => console.log('Upload') },
|
|
58
|
+
{ id: 'note', icon: <FileText className="w-5 h-5" />, label: 'Create Note', onClick: () => console.log('Note') },
|
|
59
|
+
],
|
|
60
|
+
label: 'Create options',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const ManyActions: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
actions: [
|
|
67
|
+
{ id: 'camera', icon: <Camera className="w-5 h-5" />, label: 'Camera', onClick: () => console.log('Camera') },
|
|
68
|
+
{ id: 'upload', icon: <Upload className="w-5 h-5" />, label: 'Upload', onClick: () => console.log('Upload') },
|
|
69
|
+
{ id: 'note', icon: <FileText className="w-5 h-5" />, label: 'Note', onClick: () => console.log('Note') },
|
|
70
|
+
{ id: 'share', icon: <Share className="w-5 h-5" />, label: 'Share', onClick: () => console.log('Share') },
|
|
71
|
+
{ id: 'message', icon: <MessageSquare className="w-5 h-5" />, label: 'Message', onClick: () => console.log('Message') },
|
|
72
|
+
],
|
|
73
|
+
label: 'Actions',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const WithDisabledAction: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
actions: [
|
|
80
|
+
{ id: 'camera', icon: <Camera className="w-5 h-5" />, label: 'Take Photo', onClick: () => console.log('Camera') },
|
|
81
|
+
{ id: 'upload', icon: <Upload className="w-5 h-5" />, label: 'Upload (disabled)', onClick: () => console.log('Upload'), disabled: true },
|
|
82
|
+
{ id: 'note', icon: <FileText className="w-5 h-5" />, label: 'Create Note', onClick: () => console.log('Note') },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const Extended: Story = {
|
|
88
|
+
args: {
|
|
89
|
+
extended: true,
|
|
90
|
+
extendedLabel: 'New Task',
|
|
91
|
+
onClick: () => console.log('Create task'),
|
|
92
|
+
icon: <Plus className="w-6 h-6" />,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export const ExtendedWithCustomIcon: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
extended: true,
|
|
99
|
+
extendedLabel: 'Compose',
|
|
100
|
+
onClick: () => console.log('Compose'),
|
|
101
|
+
icon: <Edit className="w-5 h-5" />,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const BottomLeft: Story = {
|
|
106
|
+
args: {
|
|
107
|
+
position: 'bottom-left',
|
|
108
|
+
onClick: () => console.log('FAB clicked'),
|
|
109
|
+
label: 'Add',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const BottomCenter: Story = {
|
|
114
|
+
args: {
|
|
115
|
+
position: 'bottom-center',
|
|
116
|
+
onClick: () => console.log('FAB clicked'),
|
|
117
|
+
label: 'Add',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const SecondaryVariant: Story = {
|
|
122
|
+
args: {
|
|
123
|
+
variant: 'secondary',
|
|
124
|
+
onClick: () => console.log('FAB clicked'),
|
|
125
|
+
icon: <Edit className="w-6 h-6" />,
|
|
126
|
+
label: 'Edit',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const LargeSize: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
size: 'lg',
|
|
133
|
+
onClick: () => console.log('FAB clicked'),
|
|
134
|
+
label: 'Add',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const CustomOffset: Story = {
|
|
139
|
+
args: {
|
|
140
|
+
onClick: () => console.log('FAB clicked'),
|
|
141
|
+
offset: { x: 32, y: 100 },
|
|
142
|
+
label: 'Custom position',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const Hidden: Story = {
|
|
147
|
+
args: {
|
|
148
|
+
onClick: () => console.log('FAB clicked'),
|
|
149
|
+
hidden: true,
|
|
150
|
+
label: 'Hidden FAB',
|
|
151
|
+
},
|
|
152
|
+
parameters: {
|
|
153
|
+
docs: {
|
|
154
|
+
description: {
|
|
155
|
+
story: 'FAB can be hidden (e.g., when scrolling down). Use `useFABScroll()` hook for scroll-based visibility.',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export const InteractiveScrollDemo: Story = {
|
|
162
|
+
render: () => {
|
|
163
|
+
// Note: In a real app, you would use useFABScroll() hook
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
<div style={{ padding: '16px', background: '#fef3c7', borderRadius: '8px', marginBottom: '16px' }}>
|
|
167
|
+
<p style={{ fontWeight: 'bold' }}>Scroll Demo</p>
|
|
168
|
+
<p style={{ fontSize: '14px', color: '#666' }}>
|
|
169
|
+
In production, use the <code>useFABScroll()</code> hook to hide FAB when scrolling down.
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
<FloatingActionButton
|
|
173
|
+
actions={[
|
|
174
|
+
{ id: 'camera', icon: <Camera className="w-5 h-5" />, label: 'Photo', onClick: () => {} },
|
|
175
|
+
{ id: 'upload', icon: <Upload className="w-5 h-5" />, label: 'Upload', onClick: () => {} },
|
|
176
|
+
]}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const DeleteAction: Story = {
|
|
184
|
+
args: {
|
|
185
|
+
icon: <Trash className="w-6 h-6" />,
|
|
186
|
+
variant: 'secondary',
|
|
187
|
+
onClick: () => console.log('Delete clicked'),
|
|
188
|
+
label: 'Delete selected',
|
|
189
|
+
},
|
|
190
|
+
parameters: {
|
|
191
|
+
docs: {
|
|
192
|
+
description: {
|
|
193
|
+
story: 'FAB can be used for contextual actions like bulk delete.',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
};
|