@papernote/ui 1.10.6 → 1.10.7
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/SwipeableListItem.d.ts +51 -32
- package/dist/components/SwipeableListItem.d.ts.map +1 -1
- package/dist/index.d.ts +51 -32
- package/dist/index.esm.js +232 -148
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +232 -148
- package/dist/index.js.map +1 -1
- package/dist/styles.css +135 -0
- package/package.json +1 -1
- package/src/components/SwipeableListItem.stories.tsx +366 -294
- package/src/components/SwipeableListItem.tsx +297 -202
package/dist/index.esm.js
CHANGED
|
@@ -8103,54 +8103,72 @@ function SwipeableCard({ children, onSwipeRight, onSwipeLeft, rightAction = {
|
|
|
8103
8103
|
}
|
|
8104
8104
|
|
|
8105
8105
|
// Color classes for action backgrounds
|
|
8106
|
-
const
|
|
8106
|
+
const getColorClasses = (color) => {
|
|
8107
8107
|
const colorMap = {
|
|
8108
|
-
destructive: 'bg-error-500',
|
|
8109
|
-
warning: 'bg-warning-500',
|
|
8110
|
-
success: 'bg-success-500',
|
|
8111
|
-
primary: 'bg-accent-500',
|
|
8108
|
+
destructive: { bg: 'bg-gradient-to-r from-error-500 to-error-600', hover: 'hover:from-error-600 hover:to-error-700' },
|
|
8109
|
+
warning: { bg: 'bg-gradient-to-r from-warning-500 to-warning-600', hover: 'hover:from-warning-600 hover:to-warning-700' },
|
|
8110
|
+
success: { bg: 'bg-gradient-to-r from-success-500 to-success-600', hover: 'hover:from-success-600 hover:to-success-700' },
|
|
8111
|
+
primary: { bg: 'bg-gradient-to-r from-accent-500 to-accent-600', hover: 'hover:from-accent-600 hover:to-accent-700' },
|
|
8112
|
+
neutral: { bg: 'bg-gradient-to-r from-paper-400 to-paper-500', hover: 'hover:from-paper-500 hover:to-paper-600' },
|
|
8112
8113
|
};
|
|
8113
|
-
return colorMap[color] || color;
|
|
8114
|
+
return colorMap[color] || { bg: color, hover: '' };
|
|
8114
8115
|
};
|
|
8115
8116
|
/**
|
|
8116
|
-
* SwipeableListItem - List item with swipe-to-action
|
|
8117
|
+
* SwipeableListItem - List item with swipe-to-reveal action buttons
|
|
8117
8118
|
*
|
|
8118
|
-
*
|
|
8119
|
-
* -
|
|
8120
|
-
* -
|
|
8121
|
-
* - Arrow keys
|
|
8119
|
+
* Features:
|
|
8120
|
+
* - Multiple actions per side (like email apps)
|
|
8121
|
+
* - Full swipe to trigger primary action
|
|
8122
|
+
* - Keyboard accessibility (Arrow keys + Tab + Enter)
|
|
8122
8123
|
* - Async callback support with loading state
|
|
8124
|
+
* - Haptic feedback on mobile
|
|
8125
|
+
* - Smooth animations and visual polish
|
|
8123
8126
|
*
|
|
8124
|
-
* @example
|
|
8127
|
+
* @example Single action per side
|
|
8125
8128
|
* ```tsx
|
|
8126
8129
|
* <SwipeableListItem
|
|
8127
|
-
*
|
|
8128
|
-
*
|
|
8129
|
-
*
|
|
8130
|
-
*
|
|
8131
|
-
* color: '
|
|
8132
|
-
*
|
|
8133
|
-
* }}
|
|
8134
|
-
* leftAction={{
|
|
8135
|
-
* icon: X,
|
|
8136
|
-
* color: 'destructive',
|
|
8137
|
-
* label: 'Dismiss'
|
|
8138
|
-
* }}
|
|
8130
|
+
* rightActions={[
|
|
8131
|
+
* { id: 'approve', icon: Check, color: 'success', label: 'Approve', onClick: handleApprove }
|
|
8132
|
+
* ]}
|
|
8133
|
+
* leftActions={[
|
|
8134
|
+
* { id: 'delete', icon: Trash, color: 'destructive', label: 'Delete', onClick: handleDelete }
|
|
8135
|
+
* ]}
|
|
8139
8136
|
* >
|
|
8140
8137
|
* <div className="p-4">List item content</div>
|
|
8141
8138
|
* </SwipeableListItem>
|
|
8142
8139
|
* ```
|
|
8140
|
+
*
|
|
8141
|
+
* @example Multiple actions (email-style)
|
|
8142
|
+
* ```tsx
|
|
8143
|
+
* <SwipeableListItem
|
|
8144
|
+
* leftActions={[
|
|
8145
|
+
* { id: 'delete', icon: Trash, color: 'destructive', label: 'Delete', onClick: handleDelete },
|
|
8146
|
+
* { id: 'archive', icon: Archive, color: 'warning', label: 'Archive', onClick: handleArchive },
|
|
8147
|
+
* ]}
|
|
8148
|
+
* rightActions={[
|
|
8149
|
+
* { id: 'read', icon: Mail, color: 'primary', label: 'Read', onClick: handleRead },
|
|
8150
|
+
* { id: 'star', icon: Star, color: 'warning', label: 'Star', onClick: handleStar },
|
|
8151
|
+
* ]}
|
|
8152
|
+
* fullSwipe
|
|
8153
|
+
* >
|
|
8154
|
+
* <EmailListItem />
|
|
8155
|
+
* </SwipeableListItem>
|
|
8156
|
+
* ```
|
|
8143
8157
|
*/
|
|
8144
|
-
function SwipeableListItem({ children,
|
|
8158
|
+
function SwipeableListItem({ children, leftActions = [], rightActions = [], actionWidth = 72, fullSwipe = false, fullSwipeThreshold = 0.5, disabled = false, className = '', onSwipeChange, }) {
|
|
8145
8159
|
const containerRef = useRef(null);
|
|
8146
8160
|
const [isDragging, setIsDragging] = useState(false);
|
|
8147
8161
|
const [offsetX, setOffsetX] = useState(0);
|
|
8148
|
-
const [
|
|
8149
|
-
const [
|
|
8150
|
-
const [
|
|
8162
|
+
const [activeDirection, setActiveDirection] = useState(null);
|
|
8163
|
+
const [loadingActionId, setLoadingActionId] = useState(null);
|
|
8164
|
+
const [focusedActionIndex, setFocusedActionIndex] = useState(-1);
|
|
8151
8165
|
const startX = useRef(0);
|
|
8152
8166
|
const startY = useRef(0);
|
|
8167
|
+
const startTime = useRef(0);
|
|
8153
8168
|
const isHorizontalSwipe = useRef(null);
|
|
8169
|
+
// Calculate total widths
|
|
8170
|
+
const leftActionsWidth = leftActions.length * actionWidth;
|
|
8171
|
+
const rightActionsWidth = rightActions.length * actionWidth;
|
|
8154
8172
|
// Trigger haptic feedback
|
|
8155
8173
|
const triggerHaptic = useCallback((style = 'medium') => {
|
|
8156
8174
|
if ('vibrate' in navigator) {
|
|
@@ -8162,41 +8180,38 @@ function SwipeableListItem({ children, onSwipeRight, onSwipeLeft, rightAction, l
|
|
|
8162
8180
|
navigator.vibrate(patterns[style]);
|
|
8163
8181
|
}
|
|
8164
8182
|
}, []);
|
|
8183
|
+
// Reset position
|
|
8184
|
+
const resetPosition = useCallback(() => {
|
|
8185
|
+
setOffsetX(0);
|
|
8186
|
+
setActiveDirection(null);
|
|
8187
|
+
setFocusedActionIndex(-1);
|
|
8188
|
+
onSwipeChange?.(null);
|
|
8189
|
+
}, [onSwipeChange]);
|
|
8165
8190
|
// Execute action with async support
|
|
8166
|
-
const executeAction = useCallback(async (
|
|
8167
|
-
|
|
8168
|
-
if (!handler)
|
|
8169
|
-
return;
|
|
8170
|
-
setIsLoading(true);
|
|
8191
|
+
const executeAction = useCallback(async (action) => {
|
|
8192
|
+
setLoadingActionId(action.id);
|
|
8171
8193
|
triggerHaptic('heavy');
|
|
8172
|
-
// Animate out
|
|
8173
|
-
const slideDistance = direction === 'right' ? window.innerWidth : -window.innerWidth;
|
|
8174
|
-
setOffsetX(slideDistance);
|
|
8175
8194
|
try {
|
|
8176
|
-
await
|
|
8195
|
+
await action.onClick();
|
|
8177
8196
|
}
|
|
8178
8197
|
finally {
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
setOffsetX(0);
|
|
8182
|
-
setIsTriggered(null);
|
|
8183
|
-
setIsLoading(false);
|
|
8184
|
-
setKeyboardDirection(null);
|
|
8185
|
-
}, 200);
|
|
8198
|
+
setLoadingActionId(null);
|
|
8199
|
+
resetPosition();
|
|
8186
8200
|
}
|
|
8187
|
-
}, [
|
|
8201
|
+
}, [triggerHaptic, resetPosition]);
|
|
8188
8202
|
// Handle drag start
|
|
8189
8203
|
const handleDragStart = useCallback((clientX, clientY) => {
|
|
8190
|
-
if (disabled ||
|
|
8204
|
+
if (disabled || loadingActionId)
|
|
8191
8205
|
return;
|
|
8192
8206
|
setIsDragging(true);
|
|
8193
8207
|
startX.current = clientX;
|
|
8194
8208
|
startY.current = clientY;
|
|
8209
|
+
startTime.current = Date.now();
|
|
8195
8210
|
isHorizontalSwipe.current = null;
|
|
8196
|
-
}, [disabled,
|
|
8211
|
+
}, [disabled, loadingActionId]);
|
|
8197
8212
|
// Handle drag move
|
|
8198
8213
|
const handleDragMove = useCallback((clientX, clientY) => {
|
|
8199
|
-
if (!isDragging || disabled ||
|
|
8214
|
+
if (!isDragging || disabled || loadingActionId)
|
|
8200
8215
|
return;
|
|
8201
8216
|
const deltaX = clientX - startX.current;
|
|
8202
8217
|
const deltaY = clientY - startY.current;
|
|
@@ -8211,64 +8226,100 @@ function SwipeableListItem({ children, onSwipeRight, onSwipeLeft, rightAction, l
|
|
|
8211
8226
|
// Only process horizontal swipes
|
|
8212
8227
|
if (isHorizontalSwipe.current !== true)
|
|
8213
8228
|
return;
|
|
8214
|
-
// Check if we should allow this direction
|
|
8215
|
-
const canSwipeRight = onSwipeRight !== undefined && rightAction !== undefined;
|
|
8216
|
-
const canSwipeLeft = onSwipeLeft !== undefined && leftAction !== undefined;
|
|
8217
8229
|
let newOffset = deltaX;
|
|
8218
|
-
//
|
|
8219
|
-
if (
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8230
|
+
// Swiping left (reveals left actions on right side)
|
|
8231
|
+
if (deltaX < 0) {
|
|
8232
|
+
if (leftActions.length === 0) {
|
|
8233
|
+
newOffset = deltaX * 0.2; // Heavy resistance if no actions
|
|
8234
|
+
}
|
|
8235
|
+
else {
|
|
8236
|
+
const maxSwipe = fullSwipe
|
|
8237
|
+
? -(containerRef.current?.offsetWidth || 300)
|
|
8238
|
+
: -leftActionsWidth;
|
|
8239
|
+
newOffset = Math.max(maxSwipe, deltaX);
|
|
8240
|
+
// Apply resistance past the action buttons
|
|
8241
|
+
if (newOffset < -leftActionsWidth && !fullSwipe) {
|
|
8242
|
+
const overSwipe = newOffset + leftActionsWidth;
|
|
8243
|
+
newOffset = -leftActionsWidth + overSwipe * 0.3;
|
|
8244
|
+
}
|
|
8245
|
+
}
|
|
8246
|
+
if (activeDirection !== 'left') {
|
|
8247
|
+
setActiveDirection('left');
|
|
8248
|
+
onSwipeChange?.('left');
|
|
8249
|
+
}
|
|
8232
8250
|
}
|
|
8233
|
-
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8251
|
+
// Swiping right (reveals right actions on left side)
|
|
8252
|
+
else if (deltaX > 0) {
|
|
8253
|
+
if (rightActions.length === 0) {
|
|
8254
|
+
newOffset = deltaX * 0.2; // Heavy resistance if no actions
|
|
8255
|
+
}
|
|
8256
|
+
else {
|
|
8257
|
+
const maxSwipe = fullSwipe
|
|
8258
|
+
? (containerRef.current?.offsetWidth || 300)
|
|
8259
|
+
: rightActionsWidth;
|
|
8260
|
+
newOffset = Math.min(maxSwipe, deltaX);
|
|
8261
|
+
// Apply resistance past the action buttons
|
|
8262
|
+
if (newOffset > rightActionsWidth && !fullSwipe) {
|
|
8263
|
+
const overSwipe = newOffset - rightActionsWidth;
|
|
8264
|
+
newOffset = rightActionsWidth + overSwipe * 0.3;
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8267
|
+
if (activeDirection !== 'right') {
|
|
8268
|
+
setActiveDirection('right');
|
|
8269
|
+
onSwipeChange?.('right');
|
|
8241
8270
|
}
|
|
8242
|
-
setIsTriggered(newTriggered);
|
|
8243
8271
|
}
|
|
8244
|
-
|
|
8272
|
+
setOffsetX(newOffset);
|
|
8273
|
+
}, [isDragging, disabled, loadingActionId, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, activeDirection, onSwipeChange]);
|
|
8245
8274
|
// Handle drag end
|
|
8246
8275
|
const handleDragEnd = useCallback(() => {
|
|
8247
8276
|
if (!isDragging)
|
|
8248
8277
|
return;
|
|
8249
8278
|
setIsDragging(false);
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
8254
|
-
|
|
8279
|
+
const velocity = Math.abs(offsetX) / (Date.now() - startTime.current);
|
|
8280
|
+
const containerWidth = containerRef.current?.offsetWidth || 300;
|
|
8281
|
+
// Check for full swipe trigger
|
|
8282
|
+
if (fullSwipe) {
|
|
8283
|
+
const swipePercentage = Math.abs(offsetX) / containerWidth;
|
|
8284
|
+
if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
|
|
8285
|
+
if (offsetX < 0 && leftActions.length > 0) {
|
|
8286
|
+
executeAction(leftActions[0]);
|
|
8287
|
+
return;
|
|
8288
|
+
}
|
|
8289
|
+
else if (offsetX > 0 && rightActions.length > 0) {
|
|
8290
|
+
executeAction(rightActions[0]);
|
|
8291
|
+
return;
|
|
8292
|
+
}
|
|
8255
8293
|
}
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8294
|
+
}
|
|
8295
|
+
// Snap to open or closed position
|
|
8296
|
+
const threshold = actionWidth * 0.5;
|
|
8297
|
+
if (Math.abs(offsetX) >= threshold || velocity > 0.3) {
|
|
8298
|
+
// Snap open
|
|
8299
|
+
if (offsetX < 0 && leftActions.length > 0) {
|
|
8300
|
+
setOffsetX(-leftActionsWidth);
|
|
8301
|
+
setActiveDirection('left');
|
|
8302
|
+
onSwipeChange?.('left');
|
|
8303
|
+
}
|
|
8304
|
+
else if (offsetX > 0 && rightActions.length > 0) {
|
|
8305
|
+
setOffsetX(rightActionsWidth);
|
|
8306
|
+
setActiveDirection('right');
|
|
8307
|
+
onSwipeChange?.('right');
|
|
8308
|
+
}
|
|
8309
|
+
else {
|
|
8310
|
+
resetPosition();
|
|
8259
8311
|
}
|
|
8260
8312
|
}
|
|
8261
|
-
|
|
8262
|
-
|
|
8263
|
-
|
|
8264
|
-
}, [isDragging, offsetX,
|
|
8313
|
+
else {
|
|
8314
|
+
resetPosition();
|
|
8315
|
+
}
|
|
8316
|
+
}, [isDragging, offsetX, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, actionWidth, executeAction, resetPosition, onSwipeChange]);
|
|
8265
8317
|
// Touch event handlers
|
|
8266
8318
|
const handleTouchStart = (e) => {
|
|
8267
8319
|
handleDragStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
8268
8320
|
};
|
|
8269
8321
|
const handleTouchMove = (e) => {
|
|
8270
8322
|
handleDragMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
8271
|
-
// Prevent vertical scroll if horizontal swipe
|
|
8272
8323
|
if (isHorizontalSwipe.current === true) {
|
|
8273
8324
|
e.preventDefault();
|
|
8274
8325
|
}
|
|
@@ -8276,7 +8327,7 @@ function SwipeableListItem({ children, onSwipeRight, onSwipeLeft, rightAction, l
|
|
|
8276
8327
|
const handleTouchEnd = () => {
|
|
8277
8328
|
handleDragEnd();
|
|
8278
8329
|
};
|
|
8279
|
-
// Mouse event handlers
|
|
8330
|
+
// Mouse event handlers
|
|
8280
8331
|
const handleMouseDown = (e) => {
|
|
8281
8332
|
handleDragStart(e.clientX, e.clientY);
|
|
8282
8333
|
};
|
|
@@ -8296,93 +8347,126 @@ function SwipeableListItem({ children, onSwipeRight, onSwipeLeft, rightAction, l
|
|
|
8296
8347
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
8297
8348
|
};
|
|
8298
8349
|
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
8299
|
-
//
|
|
8350
|
+
// Close on outside click
|
|
8351
|
+
useEffect(() => {
|
|
8352
|
+
if (activeDirection === null)
|
|
8353
|
+
return;
|
|
8354
|
+
const handleClickOutside = (e) => {
|
|
8355
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
8356
|
+
resetPosition();
|
|
8357
|
+
}
|
|
8358
|
+
};
|
|
8359
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
8360
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
8361
|
+
}, [activeDirection, resetPosition]);
|
|
8362
|
+
// Keyboard navigation
|
|
8300
8363
|
const handleKeyDown = useCallback((e) => {
|
|
8301
|
-
if (disabled ||
|
|
8364
|
+
if (disabled || loadingActionId)
|
|
8302
8365
|
return;
|
|
8303
|
-
const
|
|
8304
|
-
|
|
8366
|
+
const currentActions = activeDirection === 'left' ? leftActions :
|
|
8367
|
+
activeDirection === 'right' ? rightActions : [];
|
|
8305
8368
|
switch (e.key) {
|
|
8306
8369
|
case 'ArrowRight':
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8370
|
+
e.preventDefault();
|
|
8371
|
+
if (activeDirection === null && rightActions.length > 0) {
|
|
8372
|
+
setOffsetX(rightActionsWidth);
|
|
8373
|
+
setActiveDirection('right');
|
|
8374
|
+
setFocusedActionIndex(0);
|
|
8375
|
+
onSwipeChange?.('right');
|
|
8376
|
+
triggerHaptic('light');
|
|
8377
|
+
}
|
|
8378
|
+
else if (activeDirection === 'right' && focusedActionIndex < rightActions.length - 1) {
|
|
8379
|
+
setFocusedActionIndex(prev => prev + 1);
|
|
8380
|
+
}
|
|
8381
|
+
else if (activeDirection === 'left') {
|
|
8382
|
+
resetPosition();
|
|
8313
8383
|
}
|
|
8314
8384
|
break;
|
|
8315
8385
|
case 'ArrowLeft':
|
|
8316
|
-
|
|
8386
|
+
e.preventDefault();
|
|
8387
|
+
if (activeDirection === null && leftActions.length > 0) {
|
|
8388
|
+
setOffsetX(-leftActionsWidth);
|
|
8389
|
+
setActiveDirection('left');
|
|
8390
|
+
setFocusedActionIndex(0);
|
|
8391
|
+
onSwipeChange?.('left');
|
|
8392
|
+
triggerHaptic('light');
|
|
8393
|
+
}
|
|
8394
|
+
else if (activeDirection === 'left' && focusedActionIndex < leftActions.length - 1) {
|
|
8395
|
+
setFocusedActionIndex(prev => prev + 1);
|
|
8396
|
+
}
|
|
8397
|
+
else if (activeDirection === 'right') {
|
|
8398
|
+
resetPosition();
|
|
8399
|
+
}
|
|
8400
|
+
break;
|
|
8401
|
+
case 'Tab':
|
|
8402
|
+
if (activeDirection !== null && currentActions.length > 0) {
|
|
8317
8403
|
e.preventDefault();
|
|
8318
|
-
|
|
8319
|
-
|
|
8320
|
-
|
|
8321
|
-
|
|
8404
|
+
if (e.shiftKey) {
|
|
8405
|
+
setFocusedActionIndex(prev => prev <= 0 ? currentActions.length - 1 : prev - 1);
|
|
8406
|
+
}
|
|
8407
|
+
else {
|
|
8408
|
+
setFocusedActionIndex(prev => prev >= currentActions.length - 1 ? 0 : prev + 1);
|
|
8409
|
+
}
|
|
8322
8410
|
}
|
|
8323
8411
|
break;
|
|
8324
8412
|
case 'Enter':
|
|
8325
|
-
|
|
8413
|
+
case ' ':
|
|
8414
|
+
if (activeDirection !== null && focusedActionIndex >= 0 && focusedActionIndex < currentActions.length) {
|
|
8326
8415
|
e.preventDefault();
|
|
8327
|
-
executeAction(
|
|
8416
|
+
executeAction(currentActions[focusedActionIndex]);
|
|
8328
8417
|
}
|
|
8329
8418
|
break;
|
|
8330
8419
|
case 'Escape':
|
|
8331
|
-
if (
|
|
8420
|
+
if (activeDirection !== null) {
|
|
8332
8421
|
e.preventDefault();
|
|
8333
|
-
|
|
8334
|
-
setOffsetX(0);
|
|
8335
|
-
setIsTriggered(null);
|
|
8422
|
+
resetPosition();
|
|
8336
8423
|
}
|
|
8337
8424
|
break;
|
|
8338
8425
|
}
|
|
8339
|
-
}, [disabled,
|
|
8340
|
-
//
|
|
8341
|
-
const
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8426
|
+
}, [disabled, loadingActionId, activeDirection, leftActions, rightActions, leftActionsWidth, rightActionsWidth, focusedActionIndex, executeAction, resetPosition, onSwipeChange, triggerHaptic]);
|
|
8427
|
+
// Render action button
|
|
8428
|
+
const renderActionButton = (action, index, side) => {
|
|
8429
|
+
const { bg, hover } = getColorClasses(action.color);
|
|
8430
|
+
const isLoading = loadingActionId === action.id;
|
|
8431
|
+
const isFocused = activeDirection === side && focusedActionIndex === index;
|
|
8432
|
+
const IconComponent = action.icon;
|
|
8433
|
+
return (jsxs("button", { onClick: (e) => {
|
|
8434
|
+
e.stopPropagation();
|
|
8435
|
+
executeAction(action);
|
|
8436
|
+
}, disabled: !!loadingActionId, className: `
|
|
8437
|
+
flex flex-col items-center justify-center gap-1
|
|
8438
|
+
h-full text-white
|
|
8439
|
+
${bg} ${hover}
|
|
8440
|
+
transition-all duration-150 ease-out
|
|
8441
|
+
focus:outline-none
|
|
8442
|
+
${isFocused ? 'ring-2 ring-white ring-inset scale-105' : ''}
|
|
8443
|
+
${isLoading ? 'opacity-75' : 'active:scale-95'}
|
|
8444
|
+
disabled:cursor-not-allowed
|
|
8445
|
+
`, style: { width: actionWidth }, "aria-label": action.label, children: [jsx("div", { className: `transition-transform duration-200 ${isFocused ? 'scale-110' : ''}`, children: isLoading ? (jsx(Loader2, { className: "h-5 w-5 animate-spin" })) : (jsx(IconComponent, { className: "h-5 w-5" })) }), jsx("span", { className: "text-[10px] font-medium uppercase tracking-wide opacity-90", children: action.label })] }, action.id));
|
|
8446
|
+
};
|
|
8351
8447
|
// Build aria-label
|
|
8352
8448
|
const ariaLabel = [
|
|
8353
8449
|
'Swipeable list item.',
|
|
8354
|
-
|
|
8355
|
-
|
|
8356
|
-
keyboardDirection ? `Press Enter to confirm or Escape to cancel.` : '',
|
|
8450
|
+
rightActions.length > 0 ? `Swipe right for ${rightActions.map(a => a.label).join(', ')}.` : '',
|
|
8451
|
+
leftActions.length > 0 ? `Swipe left for ${leftActions.map(a => a.label).join(', ')}.` : '',
|
|
8357
8452
|
].filter(Boolean).join(' ');
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
|
|
8365
|
-
|
|
8366
|
-
text-white transform transition-transform duration-200
|
|
8367
|
-
${isTriggered === 'right' ? 'scale-125' : 'scale-100'}
|
|
8368
|
-
`, children: isLoading && isTriggered === 'right' ? (jsx(Loader2, { className: "h-6 w-6 animate-spin" })) : (jsx(rightAction.icon, { className: "h-6 w-6" })) }) })), leftAction && onSwipeLeft && (jsx("div", { className: `
|
|
8369
|
-
absolute inset-y-0 right-0 flex items-center justify-end pr-6
|
|
8370
|
-
${getColorClass(leftAction.color)}
|
|
8371
|
-
transition-opacity duration-100
|
|
8372
|
-
`, style: {
|
|
8373
|
-
opacity: leftActionOpacity,
|
|
8374
|
-
width: Math.abs(offsetX) + 20,
|
|
8375
|
-
}, "aria-hidden": "true", children: jsx("div", { className: `
|
|
8376
|
-
text-white transform transition-transform duration-200
|
|
8377
|
-
${isTriggered === 'left' ? 'scale-125' : 'scale-100'}
|
|
8378
|
-
`, children: isLoading && isTriggered === 'left' ? (jsx(Loader2, { className: "h-6 w-6 animate-spin" })) : (jsx(leftAction.icon, { className: "h-6 w-6" })) }) })), jsx("div", { className: `
|
|
8453
|
+
// Calculate visual progress for full swipe indicator
|
|
8454
|
+
const fullSwipeProgress = fullSwipe
|
|
8455
|
+
? Math.min(1, Math.abs(offsetX) / ((containerRef.current?.offsetWidth || 300) * fullSwipeThreshold))
|
|
8456
|
+
: 0;
|
|
8457
|
+
return (jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, children: [rightActions.length > 0 && (jsx("div", { className: "absolute left-0 top-0 bottom-0 flex shadow-inner", style: { width: rightActionsWidth }, children: rightActions.map((action, index) => renderActionButton(action, index, 'right')) })), leftActions.length > 0 && (jsx("div", { className: "absolute right-0 top-0 bottom-0 flex shadow-inner", style: { width: leftActionsWidth }, children: leftActions.map((action, index) => renderActionButton(action, index, 'left')) })), fullSwipe && fullSwipeProgress > 0.3 && (jsx("div", { className: `
|
|
8458
|
+
absolute inset-0 pointer-events-none
|
|
8459
|
+
${offsetX > 0 ? 'bg-gradient-to-r from-success-500/20 to-transparent' : 'bg-gradient-to-l from-error-500/20 to-transparent'}
|
|
8460
|
+
`, style: { opacity: fullSwipeProgress } })), jsx("div", { className: `
|
|
8379
8461
|
relative bg-white
|
|
8462
|
+
${isDragging ? 'cursor-grabbing' : 'cursor-grab'}
|
|
8380
8463
|
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
8381
8464
|
${disabled ? 'opacity-50 pointer-events-none' : ''}
|
|
8382
|
-
${
|
|
8465
|
+
${isDragging ? 'shadow-lg' : activeDirection ? 'shadow-md' : ''}
|
|
8383
8466
|
`, style: {
|
|
8384
8467
|
transform: `translateX(${offsetX}px)`,
|
|
8385
|
-
|
|
8468
|
+
touchAction: 'pan-y',
|
|
8469
|
+
}, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, onKeyDown: handleKeyDown, role: "button", "aria-label": ariaLabel, tabIndex: disabled ? -1 : 0, children: children })] }));
|
|
8386
8470
|
}
|
|
8387
8471
|
|
|
8388
8472
|
/**
|