@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/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 getColorClass = (color) => {
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 functionality
8117
+ * SwipeableListItem - List item with swipe-to-reveal action buttons
8117
8118
  *
8118
- * Designed for mobile workflows with keyboard accessibility:
8119
- * - Swipe right to approve/confirm
8120
- * - Swipe left to dismiss/delete
8121
- * - Arrow keys for keyboard navigation
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
- * onSwipeRight={() => handleApprove()}
8128
- * onSwipeLeft={() => handleDismiss()}
8129
- * rightAction={{
8130
- * icon: Check,
8131
- * color: 'success',
8132
- * label: 'Approve'
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, onSwipeRight, onSwipeLeft, rightAction, leftAction, swipeThreshold = 100, disabled = false, className = '', }) {
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 [isTriggered, setIsTriggered] = useState(null);
8149
- const [isLoading, setIsLoading] = useState(false);
8150
- const [keyboardDirection, setKeyboardDirection] = useState(null);
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 (direction) => {
8167
- const handler = direction === 'right' ? onSwipeRight : onSwipeLeft;
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 handler();
8195
+ await action.onClick();
8177
8196
  }
8178
8197
  finally {
8179
- // Reset state after animation
8180
- setTimeout(() => {
8181
- setOffsetX(0);
8182
- setIsTriggered(null);
8183
- setIsLoading(false);
8184
- setKeyboardDirection(null);
8185
- }, 200);
8198
+ setLoadingActionId(null);
8199
+ resetPosition();
8186
8200
  }
8187
- }, [onSwipeRight, onSwipeLeft, triggerHaptic]);
8201
+ }, [triggerHaptic, resetPosition]);
8188
8202
  // Handle drag start
8189
8203
  const handleDragStart = useCallback((clientX, clientY) => {
8190
- if (disabled || isLoading)
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, isLoading]);
8211
+ }, [disabled, loadingActionId]);
8197
8212
  // Handle drag move
8198
8213
  const handleDragMove = useCallback((clientX, clientY) => {
8199
- if (!isDragging || disabled || isLoading)
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
- // Limit swipe direction based on available actions
8219
- if (!canSwipeRight && deltaX > 0)
8220
- newOffset = 0;
8221
- if (!canSwipeLeft && deltaX < 0)
8222
- newOffset = 0;
8223
- // Add resistance when exceeding threshold
8224
- const maxSwipe = swipeThreshold * 1.5;
8225
- if (Math.abs(newOffset) > swipeThreshold) {
8226
- const overflow = Math.abs(newOffset) - swipeThreshold;
8227
- const resistance = overflow * 0.3;
8228
- newOffset = newOffset > 0
8229
- ? swipeThreshold + resistance
8230
- : -(swipeThreshold + resistance);
8231
- newOffset = Math.max(-maxSwipe, Math.min(maxSwipe, newOffset));
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
- setOffsetX(newOffset);
8234
- // Check for threshold crossing and trigger haptic
8235
- const newTriggered = Math.abs(newOffset) >= swipeThreshold
8236
- ? (newOffset > 0 ? 'right' : 'left')
8237
- : null;
8238
- if (newTriggered !== isTriggered) {
8239
- if (newTriggered) {
8240
- triggerHaptic('medium');
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
- }, [isDragging, disabled, isLoading, onSwipeRight, onSwipeLeft, rightAction, leftAction, swipeThreshold, isTriggered, triggerHaptic]);
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
- // Check if action should be triggered
8251
- if (Math.abs(offsetX) >= swipeThreshold) {
8252
- if (offsetX > 0 && onSwipeRight && rightAction) {
8253
- executeAction('right');
8254
- return;
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
- else if (offsetX < 0 && onSwipeLeft && leftAction) {
8257
- executeAction('left');
8258
- return;
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
- // Snap back
8262
- setOffsetX(0);
8263
- setIsTriggered(null);
8264
- }, [isDragging, offsetX, swipeThreshold, onSwipeRight, onSwipeLeft, rightAction, leftAction, executeAction]);
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 (for desktop testing)
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
- // Keyboard event handlers
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 || isLoading)
8364
+ if (disabled || loadingActionId)
8302
8365
  return;
8303
- const canSwipeRight = onSwipeRight !== undefined && rightAction !== undefined;
8304
- const canSwipeLeft = onSwipeLeft !== undefined && leftAction !== undefined;
8366
+ const currentActions = activeDirection === 'left' ? leftActions :
8367
+ activeDirection === 'right' ? rightActions : [];
8305
8368
  switch (e.key) {
8306
8369
  case 'ArrowRight':
8307
- if (canSwipeRight) {
8308
- e.preventDefault();
8309
- setKeyboardDirection('right');
8310
- setOffsetX(swipeThreshold);
8311
- setIsTriggered('right');
8312
- triggerHaptic('medium');
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
- if (canSwipeLeft) {
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
- setKeyboardDirection('left');
8319
- setOffsetX(-swipeThreshold);
8320
- setIsTriggered('left');
8321
- triggerHaptic('medium');
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
- if (keyboardDirection) {
8413
+ case ' ':
8414
+ if (activeDirection !== null && focusedActionIndex >= 0 && focusedActionIndex < currentActions.length) {
8326
8415
  e.preventDefault();
8327
- executeAction(keyboardDirection);
8416
+ executeAction(currentActions[focusedActionIndex]);
8328
8417
  }
8329
8418
  break;
8330
8419
  case 'Escape':
8331
- if (keyboardDirection) {
8420
+ if (activeDirection !== null) {
8332
8421
  e.preventDefault();
8333
- setKeyboardDirection(null);
8334
- setOffsetX(0);
8335
- setIsTriggered(null);
8422
+ resetPosition();
8336
8423
  }
8337
8424
  break;
8338
8425
  }
8339
- }, [disabled, isLoading, onSwipeRight, onSwipeLeft, rightAction, leftAction, swipeThreshold, keyboardDirection, executeAction, triggerHaptic]);
8340
- // Reset keyboard state on blur
8341
- const handleBlur = useCallback(() => {
8342
- if (keyboardDirection) {
8343
- setKeyboardDirection(null);
8344
- setOffsetX(0);
8345
- setIsTriggered(null);
8346
- }
8347
- }, [keyboardDirection]);
8348
- // Calculate action opacity based on swipe distance
8349
- const rightActionOpacity = offsetX > 0 ? Math.min(1, offsetX / swipeThreshold) : 0;
8350
- const leftActionOpacity = offsetX < 0 ? Math.min(1, Math.abs(offsetX) / swipeThreshold) : 0;
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
- rightAction && onSwipeRight ? `Swipe right or press Arrow Right to ${rightAction.label}.` : '',
8355
- leftAction && onSwipeLeft ? `Swipe left or press Arrow Left to ${leftAction.label}.` : '',
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
- return (jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, children: [rightAction && onSwipeRight && (jsx("div", { className: `
8359
- absolute inset-y-0 left-0 flex items-center justify-start pl-6
8360
- ${getColorClass(rightAction.color)}
8361
- transition-opacity duration-100
8362
- `, style: {
8363
- opacity: rightActionOpacity,
8364
- width: Math.abs(offsetX) + 20,
8365
- }, "aria-hidden": "true", children: jsx("div", { className: `
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
- ${keyboardDirection ? 'ring-2 ring-accent-500 ring-inset' : ''}
8465
+ ${isDragging ? 'shadow-lg' : activeDirection ? 'shadow-md' : ''}
8383
8466
  `, style: {
8384
8467
  transform: `translateX(${offsetX}px)`,
8385
- }, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, onKeyDown: handleKeyDown, onBlur: handleBlur, role: "button", "aria-label": ariaLabel, tabIndex: disabled ? -1 : 0, children: children })] }));
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
  /**