@papernote/ui 1.3.0 → 1.5.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/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 +50 -1
- package/dist/components/Modal.d.ts.map +1 -1
- 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/Spreadsheet.d.ts +5 -1
- package/dist/components/Spreadsheet.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 +27 -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 +1666 -65
- package/dist/index.esm.js +2875 -8619
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2908 -8617
- package/dist/index.js.map +1 -1
- package/dist/styles.css +404 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -4
- 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 +183 -0
- package/src/components/Modal.tsx +84 -3
- 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 +191 -8
- package/src/components/Spreadsheet.tsx +8 -57
- 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 +63 -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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
2
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { ChevronDown, ChevronRight, X } from 'lucide-react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
4
5
|
|
|
5
6
|
export interface SidebarItem {
|
|
6
7
|
id: string;
|
|
@@ -22,6 +23,14 @@ export interface SidebarProps {
|
|
|
22
23
|
header?: React.ReactNode; // Logo or header content
|
|
23
24
|
footer?: React.ReactNode; // User profile or footer content
|
|
24
25
|
currentPath?: string; // Current route for auto-active detection
|
|
26
|
+
|
|
27
|
+
// Mobile drawer props
|
|
28
|
+
/** Whether sidebar is open on mobile (drawer mode) */
|
|
29
|
+
mobileOpen?: boolean;
|
|
30
|
+
/** Callback when mobile drawer should close */
|
|
31
|
+
onMobileClose?: () => void;
|
|
32
|
+
/** Width of the sidebar (default: 256px / w-64) */
|
|
33
|
+
width?: string;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
export interface SidebarGroupProps {
|
|
@@ -169,15 +178,147 @@ export function SidebarGroup({ title, items, onNavigate, defaultExpanded = true,
|
|
|
169
178
|
);
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Sidebar - Navigation sidebar with mobile drawer support
|
|
183
|
+
*
|
|
184
|
+
* On desktop: Renders as a fixed-width sidebar
|
|
185
|
+
* On mobile: Renders as a drawer overlay when mobileOpen is true
|
|
186
|
+
*
|
|
187
|
+
* @example Desktop usage (no mobile props)
|
|
188
|
+
* ```tsx
|
|
189
|
+
* <Sidebar
|
|
190
|
+
* items={navItems}
|
|
191
|
+
* header={<Logo />}
|
|
192
|
+
* footer={<UserProfile />}
|
|
193
|
+
* currentPath={location.pathname}
|
|
194
|
+
* onNavigate={(href) => navigate(href)}
|
|
195
|
+
* />
|
|
196
|
+
* ```
|
|
197
|
+
*
|
|
198
|
+
* @example With mobile drawer support
|
|
199
|
+
* ```tsx
|
|
200
|
+
* const [mobileOpen, setMobileOpen] = useState(false);
|
|
201
|
+
*
|
|
202
|
+
* <Sidebar
|
|
203
|
+
* items={navItems}
|
|
204
|
+
* header={<Logo />}
|
|
205
|
+
* mobileOpen={mobileOpen}
|
|
206
|
+
* onMobileClose={() => setMobileOpen(false)}
|
|
207
|
+
* onNavigate={(href) => {
|
|
208
|
+
* navigate(href);
|
|
209
|
+
* setMobileOpen(false); // Close drawer on navigation
|
|
210
|
+
* }}
|
|
211
|
+
* />
|
|
212
|
+
* ```
|
|
213
|
+
*/
|
|
214
|
+
export default function Sidebar({
|
|
215
|
+
items,
|
|
216
|
+
onNavigate,
|
|
217
|
+
className = '',
|
|
218
|
+
header,
|
|
219
|
+
footer,
|
|
220
|
+
currentPath,
|
|
221
|
+
mobileOpen,
|
|
222
|
+
onMobileClose,
|
|
223
|
+
width = 'w-64',
|
|
224
|
+
}: SidebarProps) {
|
|
225
|
+
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
226
|
+
const [isAnimating, setIsAnimating] = useState(false);
|
|
227
|
+
const [shouldRender, setShouldRender] = useState(mobileOpen);
|
|
228
|
+
|
|
229
|
+
// Handle animation states for mobile drawer
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (mobileOpen) {
|
|
232
|
+
setShouldRender(true);
|
|
233
|
+
// Small delay to trigger animation
|
|
234
|
+
requestAnimationFrame(() => {
|
|
235
|
+
setIsAnimating(true);
|
|
236
|
+
});
|
|
237
|
+
return; // No cleanup needed when opening
|
|
238
|
+
} else {
|
|
239
|
+
setIsAnimating(false);
|
|
240
|
+
// Wait for animation to complete before unmounting
|
|
241
|
+
const timer = setTimeout(() => {
|
|
242
|
+
setShouldRender(false);
|
|
243
|
+
}, 300);
|
|
244
|
+
return () => clearTimeout(timer);
|
|
245
|
+
}
|
|
246
|
+
}, [mobileOpen]);
|
|
247
|
+
|
|
248
|
+
// Handle escape key for mobile drawer
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!mobileOpen) return;
|
|
251
|
+
|
|
252
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
253
|
+
if (e.key === 'Escape') {
|
|
254
|
+
onMobileClose?.();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
document.addEventListener('keydown', handleEscape);
|
|
259
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
260
|
+
}, [mobileOpen, onMobileClose]);
|
|
261
|
+
|
|
262
|
+
// Lock body scroll when mobile drawer is open
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (mobileOpen) {
|
|
265
|
+
document.body.style.overflow = 'hidden';
|
|
266
|
+
} else {
|
|
267
|
+
document.body.style.overflow = '';
|
|
268
|
+
}
|
|
269
|
+
return () => {
|
|
270
|
+
document.body.style.overflow = '';
|
|
271
|
+
};
|
|
272
|
+
}, [mobileOpen]);
|
|
273
|
+
|
|
274
|
+
// Handle navigation with auto-close on mobile
|
|
275
|
+
const handleNavigate = (href: string, external?: boolean) => {
|
|
276
|
+
onNavigate?.(href, external);
|
|
277
|
+
// Auto-close mobile drawer on navigation
|
|
278
|
+
if (mobileOpen) {
|
|
279
|
+
onMobileClose?.();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
// Sidebar content (shared between desktop and mobile)
|
|
284
|
+
const sidebarContent = (
|
|
285
|
+
<div
|
|
286
|
+
ref={sidebarRef}
|
|
287
|
+
className={`flex flex-col h-full bg-white border-r border-paper-300 notebook-binding ${width} ${className}`}
|
|
288
|
+
>
|
|
289
|
+
{/* Mobile close button */}
|
|
290
|
+
{mobileOpen !== undefined && (
|
|
291
|
+
<div className="flex items-center justify-between px-4 pt-4 md:hidden">
|
|
292
|
+
<div className="flex-1">
|
|
293
|
+
{header}
|
|
294
|
+
</div>
|
|
295
|
+
<button
|
|
296
|
+
onClick={onMobileClose}
|
|
297
|
+
className="
|
|
298
|
+
flex items-center justify-center
|
|
299
|
+
w-10 h-10 -mr-2
|
|
300
|
+
text-ink-500 hover:text-ink-700
|
|
301
|
+
hover:bg-paper-100 rounded-full
|
|
302
|
+
transition-colors
|
|
303
|
+
"
|
|
304
|
+
aria-label="Close sidebar"
|
|
305
|
+
>
|
|
306
|
+
<X className="w-5 h-5" />
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
|
|
311
|
+
{/* Header (Logo) - desktop only when mobile drawer has its own */}
|
|
312
|
+
{header && mobileOpen === undefined && (
|
|
177
313
|
<div className="px-6 pt-6 pb-4">
|
|
178
314
|
{header}
|
|
179
315
|
</div>
|
|
180
316
|
)}
|
|
317
|
+
{header && mobileOpen !== undefined && (
|
|
318
|
+
<div className="px-6 pt-2 pb-4 hidden md:block">
|
|
319
|
+
{header}
|
|
320
|
+
</div>
|
|
321
|
+
)}
|
|
181
322
|
|
|
182
323
|
{/* Navigation */}
|
|
183
324
|
<nav className="flex-1 px-3 py-2 space-y-1 overflow-y-auto">
|
|
@@ -192,7 +333,7 @@ export default function Sidebar({ items, onNavigate, className = '', header, foo
|
|
|
192
333
|
<SidebarNavItem
|
|
193
334
|
key={item.id}
|
|
194
335
|
item={item}
|
|
195
|
-
onNavigate={
|
|
336
|
+
onNavigate={handleNavigate}
|
|
196
337
|
currentPath={currentPath}
|
|
197
338
|
/>
|
|
198
339
|
);
|
|
@@ -207,4 +348,46 @@ export default function Sidebar({ items, onNavigate, className = '', header, foo
|
|
|
207
348
|
)}
|
|
208
349
|
</div>
|
|
209
350
|
);
|
|
351
|
+
|
|
352
|
+
// If mobileOpen is not defined, render as regular sidebar (desktop mode)
|
|
353
|
+
if (mobileOpen === undefined) {
|
|
354
|
+
return sidebarContent;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Mobile drawer mode
|
|
358
|
+
if (!shouldRender) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return createPortal(
|
|
363
|
+
<div
|
|
364
|
+
className="fixed inset-0 z-50 md:hidden"
|
|
365
|
+
role="dialog"
|
|
366
|
+
aria-modal="true"
|
|
367
|
+
aria-label="Navigation menu"
|
|
368
|
+
>
|
|
369
|
+
{/* Backdrop */}
|
|
370
|
+
<div
|
|
371
|
+
className={`
|
|
372
|
+
absolute inset-0 bg-ink-900/50 backdrop-blur-sm
|
|
373
|
+
transition-opacity duration-300
|
|
374
|
+
${isAnimating ? 'opacity-100' : 'opacity-0'}
|
|
375
|
+
`}
|
|
376
|
+
onClick={onMobileClose}
|
|
377
|
+
aria-hidden="true"
|
|
378
|
+
/>
|
|
379
|
+
|
|
380
|
+
{/* Sidebar drawer */}
|
|
381
|
+
<div
|
|
382
|
+
className={`
|
|
383
|
+
absolute inset-y-0 left-0 flex max-w-full
|
|
384
|
+
transition-transform duration-300 ease-out
|
|
385
|
+
${isAnimating ? 'translate-x-0' : '-translate-x-full'}
|
|
386
|
+
`}
|
|
387
|
+
>
|
|
388
|
+
{sidebarContent}
|
|
389
|
+
</div>
|
|
390
|
+
</div>,
|
|
391
|
+
document.body
|
|
392
|
+
);
|
|
210
393
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React, { useState, useCallback } from 'react';
|
|
2
2
|
import BaseSpreadsheet, { CellBase, Matrix } from 'react-spreadsheet';
|
|
3
|
-
import {
|
|
3
|
+
import { utils, writeFile } from 'xlsx';
|
|
4
4
|
import Button from './Button';
|
|
5
5
|
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
6
|
import Stack from './Stack';
|
|
7
|
-
import { Download,
|
|
7
|
+
import { Download, Save } from 'lucide-react';
|
|
8
8
|
import { addSuccessMessage, addErrorMessage } from './StatusBar';
|
|
9
9
|
import './Spreadsheet.css';
|
|
10
10
|
|
|
@@ -39,7 +39,11 @@ export interface SpreadsheetProps {
|
|
|
39
39
|
rowLabels?: string[];
|
|
40
40
|
/** Show toolbar with actions */
|
|
41
41
|
showToolbar?: boolean;
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Enable Excel import
|
|
44
|
+
* @deprecated Excel import has been disabled due to security vulnerabilities in the xlsx library.
|
|
45
|
+
* This prop is kept for API compatibility but has no effect.
|
|
46
|
+
*/
|
|
43
47
|
enableImport?: boolean;
|
|
44
48
|
/** Enable Excel export */
|
|
45
49
|
enableExport?: boolean;
|
|
@@ -123,7 +127,7 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
123
127
|
columnLabels,
|
|
124
128
|
rowLabels,
|
|
125
129
|
showToolbar = false,
|
|
126
|
-
enableImport = false,
|
|
130
|
+
enableImport: _enableImport = false, // Deprecated - kept for API compatibility
|
|
127
131
|
enableExport = false,
|
|
128
132
|
enableSave = false,
|
|
129
133
|
onSave,
|
|
@@ -153,44 +157,6 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
153
157
|
[onChange]
|
|
154
158
|
);
|
|
155
159
|
|
|
156
|
-
// Handle Excel import
|
|
157
|
-
const handleImport = useCallback(
|
|
158
|
-
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
159
|
-
const file = event.target.files?.[0];
|
|
160
|
-
if (!file) return;
|
|
161
|
-
|
|
162
|
-
const reader = new FileReader();
|
|
163
|
-
reader.onload = (e) => {
|
|
164
|
-
try {
|
|
165
|
-
const workbook: WorkBook = read(e.target?.result, { type: 'binary' });
|
|
166
|
-
const sheetName = workbook.SheetNames[0];
|
|
167
|
-
const worksheet = workbook.Sheets[sheetName];
|
|
168
|
-
|
|
169
|
-
// Convert to array of arrays
|
|
170
|
-
const jsonData: any[][] = utils.sheet_to_json(worksheet, { header: 1 });
|
|
171
|
-
|
|
172
|
-
// Convert to spreadsheet format
|
|
173
|
-
const spreadsheetData: Matrix<SpreadsheetCell> = jsonData.map(row =>
|
|
174
|
-
row.map(cell => ({
|
|
175
|
-
value: cell,
|
|
176
|
-
}))
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
handleChange(spreadsheetData);
|
|
180
|
-
addSuccessMessage('Excel file imported successfully');
|
|
181
|
-
} catch (error) {
|
|
182
|
-
console.error('Error importing Excel file:', error);
|
|
183
|
-
addErrorMessage('Failed to import Excel file');
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
reader.readAsBinaryString(file);
|
|
187
|
-
|
|
188
|
-
// Reset input
|
|
189
|
-
event.target.value = '';
|
|
190
|
-
},
|
|
191
|
-
[handleChange]
|
|
192
|
-
);
|
|
193
|
-
|
|
194
160
|
// Handle Excel export
|
|
195
161
|
const handleExport = useCallback(() => {
|
|
196
162
|
try {
|
|
@@ -238,20 +204,6 @@ export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
|
238
204
|
<Stack direction="horizontal" spacing="md" align="center" className="mb-4">
|
|
239
205
|
{title && <div className="text-lg font-medium text-ink-900 flex-1">{title}</div>}
|
|
240
206
|
|
|
241
|
-
{enableImport && (
|
|
242
|
-
<label>
|
|
243
|
-
<input
|
|
244
|
-
type="file"
|
|
245
|
-
accept=".xlsx,.xls,.csv"
|
|
246
|
-
onChange={handleImport}
|
|
247
|
-
className="hidden"
|
|
248
|
-
/>
|
|
249
|
-
<Button variant="ghost" size="sm" icon={<Upload className="h-4 w-4" />}>
|
|
250
|
-
Import
|
|
251
|
-
</Button>
|
|
252
|
-
</label>
|
|
253
|
-
)}
|
|
254
|
-
|
|
255
207
|
{enableExport && (
|
|
256
208
|
<Button
|
|
257
209
|
variant="ghost"
|
|
@@ -342,7 +294,6 @@ export const SpreadsheetReport: React.FC<
|
|
|
342
294
|
<Spreadsheet
|
|
343
295
|
{...props}
|
|
344
296
|
showToolbar
|
|
345
|
-
enableImport
|
|
346
297
|
enableExport
|
|
347
298
|
enableSave
|
|
348
299
|
wrapInCard
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import SwipeActions from './SwipeActions';
|
|
3
|
+
import { Trash, Archive, Edit, Star, Share, Mail, MoreHorizontal } from 'lucide-react';
|
|
4
|
+
import Card, { CardContent } from './Card';
|
|
5
|
+
import Text from './Text';
|
|
6
|
+
import Badge from './Badge';
|
|
7
|
+
import Stack from './Stack';
|
|
8
|
+
|
|
9
|
+
const meta: Meta<typeof SwipeActions> = {
|
|
10
|
+
title: 'Mobile/SwipeActions',
|
|
11
|
+
component: SwipeActions,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'padded',
|
|
14
|
+
viewport: {
|
|
15
|
+
defaultViewport: 'mobileM',
|
|
16
|
+
},
|
|
17
|
+
docs: {
|
|
18
|
+
description: {
|
|
19
|
+
component: `
|
|
20
|
+
Touch-based swipe actions for mobile list items. Reveals action buttons when
|
|
21
|
+
swiping left or right, similar to iOS mail/messages.
|
|
22
|
+
|
|
23
|
+
Features:
|
|
24
|
+
- Left and right swipe directions
|
|
25
|
+
- Multiple actions per side
|
|
26
|
+
- Full swipe to trigger primary action
|
|
27
|
+
- Spring-back animation
|
|
28
|
+
- Touch and mouse support
|
|
29
|
+
- Customizable thresholds
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
decorators: [
|
|
35
|
+
(Story) => (
|
|
36
|
+
<div className="max-w-md mx-auto">
|
|
37
|
+
<Story />
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default meta;
|
|
44
|
+
type Story = StoryObj<typeof SwipeActions>;
|
|
45
|
+
|
|
46
|
+
const SampleListItem = ({ title, subtitle, badge }: { title: string; subtitle: string; badge?: string }) => (
|
|
47
|
+
<div className="p-4 bg-white border-b border-paper-200">
|
|
48
|
+
<div className="flex items-start justify-between">
|
|
49
|
+
<div className="flex-1 min-w-0">
|
|
50
|
+
<Text weight="medium" className="truncate">{title}</Text>
|
|
51
|
+
<Text size="sm" color="muted" className="truncate mt-0.5">{subtitle}</Text>
|
|
52
|
+
</div>
|
|
53
|
+
{badge && (
|
|
54
|
+
<Badge variant="primary" size="sm">{badge}</Badge>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Basic swipe left to reveal delete action
|
|
62
|
+
*/
|
|
63
|
+
export const DeleteAction: Story = {
|
|
64
|
+
args: {
|
|
65
|
+
leftActions: [
|
|
66
|
+
{
|
|
67
|
+
id: 'delete',
|
|
68
|
+
label: 'Delete',
|
|
69
|
+
icon: <Trash className="h-5 w-5" />,
|
|
70
|
+
color: 'error',
|
|
71
|
+
onClick: () => alert('Delete clicked!'),
|
|
72
|
+
primary: true,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
children: <SampleListItem title="Swipe left to delete" subtitle="Try swiping this item to the left" />,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Multiple actions on the left side
|
|
81
|
+
*/
|
|
82
|
+
export const MultipleLeftActions: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
leftActions: [
|
|
85
|
+
{
|
|
86
|
+
id: 'delete',
|
|
87
|
+
label: 'Delete',
|
|
88
|
+
icon: <Trash className="h-5 w-5" />,
|
|
89
|
+
color: 'error',
|
|
90
|
+
onClick: () => alert('Delete clicked!'),
|
|
91
|
+
primary: true,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'archive',
|
|
95
|
+
label: 'Archive',
|
|
96
|
+
icon: <Archive className="h-5 w-5" />,
|
|
97
|
+
color: 'warning',
|
|
98
|
+
onClick: () => alert('Archive clicked!'),
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
children: <SampleListItem title="Multiple delete options" subtitle="Swipe left to see archive and delete" />,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Actions on both sides
|
|
107
|
+
*/
|
|
108
|
+
export const BothSides: Story = {
|
|
109
|
+
args: {
|
|
110
|
+
leftActions: [
|
|
111
|
+
{
|
|
112
|
+
id: 'delete',
|
|
113
|
+
label: 'Delete',
|
|
114
|
+
icon: <Trash className="h-5 w-5" />,
|
|
115
|
+
color: 'error',
|
|
116
|
+
onClick: () => alert('Delete clicked!'),
|
|
117
|
+
primary: true,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
rightActions: [
|
|
121
|
+
{
|
|
122
|
+
id: 'edit',
|
|
123
|
+
label: 'Edit',
|
|
124
|
+
icon: <Edit className="h-5 w-5" />,
|
|
125
|
+
color: 'primary',
|
|
126
|
+
onClick: () => alert('Edit clicked!'),
|
|
127
|
+
primary: true,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'star',
|
|
131
|
+
label: 'Star',
|
|
132
|
+
icon: <Star className="h-5 w-5" />,
|
|
133
|
+
color: 'warning',
|
|
134
|
+
onClick: () => alert('Star clicked!'),
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
children: <SampleListItem title="Swipe both ways" subtitle="Left for delete, right for edit/star" />,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Full swipe to trigger action
|
|
143
|
+
*/
|
|
144
|
+
export const FullSwipe: Story = {
|
|
145
|
+
args: {
|
|
146
|
+
leftActions: [
|
|
147
|
+
{
|
|
148
|
+
id: 'delete',
|
|
149
|
+
label: 'Delete',
|
|
150
|
+
icon: <Trash className="h-5 w-5" />,
|
|
151
|
+
color: 'error',
|
|
152
|
+
onClick: () => alert('Deleted via full swipe!'),
|
|
153
|
+
primary: true,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
fullSwipe: true,
|
|
157
|
+
fullSwipeThreshold: 0.4,
|
|
158
|
+
children: <SampleListItem title="Full swipe to delete" subtitle="Swipe all the way left to delete instantly" badge="Full swipe" />,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Email-style actions (like iOS Mail)
|
|
164
|
+
*/
|
|
165
|
+
export const EmailStyle: Story = {
|
|
166
|
+
render: () => (
|
|
167
|
+
<Stack spacing="none">
|
|
168
|
+
<SwipeActions
|
|
169
|
+
leftActions={[
|
|
170
|
+
{
|
|
171
|
+
id: 'trash',
|
|
172
|
+
label: 'Trash',
|
|
173
|
+
icon: <Trash className="h-5 w-5" />,
|
|
174
|
+
color: 'error',
|
|
175
|
+
onClick: () => alert('Trash'),
|
|
176
|
+
primary: true,
|
|
177
|
+
},
|
|
178
|
+
]}
|
|
179
|
+
rightActions={[
|
|
180
|
+
{
|
|
181
|
+
id: 'more',
|
|
182
|
+
label: 'More',
|
|
183
|
+
icon: <MoreHorizontal className="h-5 w-5" />,
|
|
184
|
+
color: 'default',
|
|
185
|
+
onClick: () => alert('More options'),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'star',
|
|
189
|
+
label: 'Flag',
|
|
190
|
+
icon: <Star className="h-5 w-5" />,
|
|
191
|
+
color: 'warning',
|
|
192
|
+
onClick: () => alert('Flagged'),
|
|
193
|
+
},
|
|
194
|
+
]}
|
|
195
|
+
fullSwipe
|
|
196
|
+
>
|
|
197
|
+
<SampleListItem
|
|
198
|
+
title="Meeting reminder"
|
|
199
|
+
subtitle="Don't forget the team sync at 3pm..."
|
|
200
|
+
badge="New"
|
|
201
|
+
/>
|
|
202
|
+
</SwipeActions>
|
|
203
|
+
|
|
204
|
+
<SwipeActions
|
|
205
|
+
leftActions={[
|
|
206
|
+
{
|
|
207
|
+
id: 'trash',
|
|
208
|
+
label: 'Trash',
|
|
209
|
+
icon: <Trash className="h-5 w-5" />,
|
|
210
|
+
color: 'error',
|
|
211
|
+
onClick: () => alert('Trash'),
|
|
212
|
+
primary: true,
|
|
213
|
+
},
|
|
214
|
+
]}
|
|
215
|
+
rightActions={[
|
|
216
|
+
{
|
|
217
|
+
id: 'more',
|
|
218
|
+
label: 'More',
|
|
219
|
+
icon: <MoreHorizontal className="h-5 w-5" />,
|
|
220
|
+
color: 'default',
|
|
221
|
+
onClick: () => alert('More options'),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'reply',
|
|
225
|
+
label: 'Reply',
|
|
226
|
+
icon: <Mail className="h-5 w-5" />,
|
|
227
|
+
color: 'primary',
|
|
228
|
+
onClick: () => alert('Reply'),
|
|
229
|
+
},
|
|
230
|
+
]}
|
|
231
|
+
fullSwipe
|
|
232
|
+
>
|
|
233
|
+
<SampleListItem
|
|
234
|
+
title="Weekly report"
|
|
235
|
+
subtitle="Here's the summary of this week's progress..."
|
|
236
|
+
/>
|
|
237
|
+
</SwipeActions>
|
|
238
|
+
|
|
239
|
+
<SwipeActions
|
|
240
|
+
leftActions={[
|
|
241
|
+
{
|
|
242
|
+
id: 'archive',
|
|
243
|
+
label: 'Archive',
|
|
244
|
+
icon: <Archive className="h-5 w-5" />,
|
|
245
|
+
color: 'success',
|
|
246
|
+
onClick: () => alert('Archived'),
|
|
247
|
+
primary: true,
|
|
248
|
+
},
|
|
249
|
+
]}
|
|
250
|
+
rightActions={[
|
|
251
|
+
{
|
|
252
|
+
id: 'share',
|
|
253
|
+
label: 'Share',
|
|
254
|
+
icon: <Share className="h-5 w-5" />,
|
|
255
|
+
color: 'primary',
|
|
256
|
+
onClick: () => alert('Share'),
|
|
257
|
+
},
|
|
258
|
+
]}
|
|
259
|
+
fullSwipe
|
|
260
|
+
>
|
|
261
|
+
<SampleListItem
|
|
262
|
+
title="Project update"
|
|
263
|
+
subtitle="The new feature has been deployed to production..."
|
|
264
|
+
/>
|
|
265
|
+
</SwipeActions>
|
|
266
|
+
</Stack>
|
|
267
|
+
),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Disabled state
|
|
272
|
+
*/
|
|
273
|
+
export const Disabled: Story = {
|
|
274
|
+
args: {
|
|
275
|
+
leftActions: [
|
|
276
|
+
{
|
|
277
|
+
id: 'delete',
|
|
278
|
+
label: 'Delete',
|
|
279
|
+
icon: <Trash className="h-5 w-5" />,
|
|
280
|
+
color: 'error',
|
|
281
|
+
onClick: () => alert('Delete clicked!'),
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
disabled: true,
|
|
285
|
+
children: <SampleListItem title="Swipe disabled" subtitle="This item cannot be swiped" />,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* In a card context
|
|
291
|
+
*/
|
|
292
|
+
export const InCard: Story = {
|
|
293
|
+
render: () => (
|
|
294
|
+
<Card>
|
|
295
|
+
<CardContent className="p-0">
|
|
296
|
+
<SwipeActions
|
|
297
|
+
leftActions={[
|
|
298
|
+
{
|
|
299
|
+
id: 'delete',
|
|
300
|
+
label: 'Delete',
|
|
301
|
+
icon: <Trash className="h-5 w-5" />,
|
|
302
|
+
color: 'error',
|
|
303
|
+
onClick: () => alert('Delete'),
|
|
304
|
+
primary: true,
|
|
305
|
+
},
|
|
306
|
+
]}
|
|
307
|
+
rightActions={[
|
|
308
|
+
{
|
|
309
|
+
id: 'edit',
|
|
310
|
+
label: 'Edit',
|
|
311
|
+
icon: <Edit className="h-5 w-5" />,
|
|
312
|
+
color: 'primary',
|
|
313
|
+
onClick: () => alert('Edit'),
|
|
314
|
+
},
|
|
315
|
+
]}
|
|
316
|
+
>
|
|
317
|
+
<div className="p-4">
|
|
318
|
+
<Text weight="semibold">Card with swipe actions</Text>
|
|
319
|
+
<Text size="sm" color="muted" className="mt-1">
|
|
320
|
+
Swipe left to delete, right to edit
|
|
321
|
+
</Text>
|
|
322
|
+
</div>
|
|
323
|
+
</SwipeActions>
|
|
324
|
+
</CardContent>
|
|
325
|
+
</Card>
|
|
326
|
+
),
|
|
327
|
+
};
|