@papernote/ui 1.7.7 → 1.8.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/Badge.d.ts +3 -1
- package/dist/components/Badge.d.ts.map +1 -1
- package/dist/components/BottomSheet.d.ts +72 -8
- package/dist/components/BottomSheet.d.ts.map +1 -1
- package/dist/components/CompactStat.d.ts +52 -0
- package/dist/components/CompactStat.d.ts.map +1 -0
- package/dist/components/HorizontalScroll.d.ts +43 -0
- package/dist/components/HorizontalScroll.d.ts.map +1 -0
- package/dist/components/NotificationBanner.d.ts +53 -0
- package/dist/components/NotificationBanner.d.ts.map +1 -0
- package/dist/components/Progress.d.ts +2 -2
- package/dist/components/Progress.d.ts.map +1 -1
- package/dist/components/PullToRefresh.d.ts +23 -71
- package/dist/components/PullToRefresh.d.ts.map +1 -1
- package/dist/components/Stack.d.ts +2 -1
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/SwipeableCard.d.ts +65 -0
- package/dist/components/SwipeableCard.d.ts.map +1 -0
- package/dist/components/Text.d.ts +9 -2
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/index.d.ts +11 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +317 -86
- package/dist/index.esm.js +932 -253
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +937 -252
- package/dist/index.js.map +1 -1
- package/dist/styles.css +178 -8
- package/package.json +1 -1
- package/src/components/Badge.tsx +13 -2
- package/src/components/BottomSheet.tsx +227 -98
- package/src/components/Card.tsx +1 -1
- package/src/components/CompactStat.tsx +150 -0
- package/src/components/HorizontalScroll.tsx +275 -0
- package/src/components/NotificationBanner.tsx +238 -0
- package/src/components/Progress.tsx +6 -3
- package/src/components/PullToRefresh.tsx +158 -196
- package/src/components/Stack.tsx +4 -1
- package/src/components/SwipeableCard.tsx +347 -0
- package/src/components/Text.tsx +45 -3
- package/src/components/index.ts +16 -3
- package/src/styles/index.css +32 -0
package/dist/index.js
CHANGED
|
@@ -2527,7 +2527,7 @@ const Card = React.forwardRef(({ children, variant = 'default', width = 'auto',
|
|
|
2527
2527
|
const baseStyles = 'bg-white bg-subtle-grain border-2 border-paper-300 transition-shadow duration-200';
|
|
2528
2528
|
const variantStyles = {
|
|
2529
2529
|
default: 'rounded-xl shadow-lg p-8',
|
|
2530
|
-
compact: 'rounded-lg shadow-md p-
|
|
2530
|
+
compact: 'rounded-lg shadow-md p-3', // 12px padding for mobile-density layouts
|
|
2531
2531
|
flat: 'rounded-lg p-5',
|
|
2532
2532
|
};
|
|
2533
2533
|
const widthStyles = {
|
|
@@ -2643,6 +2643,7 @@ function Separator({ orientation = 'horizontal', className = '', spacing = 'md',
|
|
|
2643
2643
|
*
|
|
2644
2644
|
* Spacing scale (use either `spacing` or `gap` prop - they're aliases):
|
|
2645
2645
|
* - none: 0
|
|
2646
|
+
* - tight: 0.25rem (1) - for mobile-density layouts
|
|
2646
2647
|
* - xs: 0.5rem (2)
|
|
2647
2648
|
* - sm: 0.75rem (3)
|
|
2648
2649
|
* - md: 1.5rem (6)
|
|
@@ -2670,6 +2671,7 @@ const Stack = React.forwardRef(({ children, direction = 'vertical', spacing, gap
|
|
|
2670
2671
|
const spacingClasses = {
|
|
2671
2672
|
vertical: {
|
|
2672
2673
|
none: '',
|
|
2674
|
+
tight: 'space-y-1', // 4px - for mobile-density layouts
|
|
2673
2675
|
xs: 'space-y-2',
|
|
2674
2676
|
sm: 'space-y-3',
|
|
2675
2677
|
md: 'space-y-6',
|
|
@@ -2678,6 +2680,7 @@ const Stack = React.forwardRef(({ children, direction = 'vertical', spacing, gap
|
|
|
2678
2680
|
},
|
|
2679
2681
|
horizontal: {
|
|
2680
2682
|
none: '',
|
|
2683
|
+
tight: 'space-x-1', // 4px - for mobile-density layouts
|
|
2681
2684
|
xs: 'space-x-2',
|
|
2682
2685
|
sm: 'space-x-3',
|
|
2683
2686
|
md: 'space-x-6',
|
|
@@ -2975,7 +2978,7 @@ const GridItem = ({ colSpan, rowSpan, children, className = '', ...boxProps }) =
|
|
|
2975
2978
|
* <Text ref={textRef}>Measurable text</Text>
|
|
2976
2979
|
* ```
|
|
2977
2980
|
*/
|
|
2978
|
-
const Text = React.forwardRef(({ children, as: Component = 'p', size = 'base', weight = 'normal', color = 'primary', align = 'left', truncate = false, lineClamp, transform, className = '', ...htmlProps }, ref) => {
|
|
2981
|
+
const Text = React.forwardRef(({ children, as: Component = 'p', size = 'base', smSize, mdSize, lgSize, weight = 'normal', color = 'primary', align = 'left', truncate = false, lineClamp, transform, className = '', ...htmlProps }, ref) => {
|
|
2979
2982
|
const sizeClasses = {
|
|
2980
2983
|
xs: 'text-xs',
|
|
2981
2984
|
sm: 'text-sm',
|
|
@@ -2984,6 +2987,31 @@ const Text = React.forwardRef(({ children, as: Component = 'p', size = 'base', w
|
|
|
2984
2987
|
xl: 'text-xl',
|
|
2985
2988
|
'2xl': 'text-2xl',
|
|
2986
2989
|
};
|
|
2990
|
+
// Responsive size classes
|
|
2991
|
+
const smSizeClasses = {
|
|
2992
|
+
xs: 'sm:text-xs',
|
|
2993
|
+
sm: 'sm:text-sm',
|
|
2994
|
+
base: 'sm:text-base',
|
|
2995
|
+
lg: 'sm:text-lg',
|
|
2996
|
+
xl: 'sm:text-xl',
|
|
2997
|
+
'2xl': 'sm:text-2xl',
|
|
2998
|
+
};
|
|
2999
|
+
const mdSizeClasses = {
|
|
3000
|
+
xs: 'md:text-xs',
|
|
3001
|
+
sm: 'md:text-sm',
|
|
3002
|
+
base: 'md:text-base',
|
|
3003
|
+
lg: 'md:text-lg',
|
|
3004
|
+
xl: 'md:text-xl',
|
|
3005
|
+
'2xl': 'md:text-2xl',
|
|
3006
|
+
};
|
|
3007
|
+
const lgSizeClasses = {
|
|
3008
|
+
xs: 'lg:text-xs',
|
|
3009
|
+
sm: 'lg:text-sm',
|
|
3010
|
+
base: 'lg:text-base',
|
|
3011
|
+
lg: 'lg:text-lg',
|
|
3012
|
+
xl: 'lg:text-xl',
|
|
3013
|
+
'2xl': 'lg:text-2xl',
|
|
3014
|
+
};
|
|
2987
3015
|
const weightClasses = {
|
|
2988
3016
|
normal: 'font-normal',
|
|
2989
3017
|
medium: 'font-medium',
|
|
@@ -3021,6 +3049,9 @@ const Text = React.forwardRef(({ children, as: Component = 'p', size = 'base', w
|
|
|
3021
3049
|
// Build class list
|
|
3022
3050
|
const classes = [
|
|
3023
3051
|
sizeClasses[size],
|
|
3052
|
+
smSize ? smSizeClasses[smSize] : '',
|
|
3053
|
+
mdSize ? mdSizeClasses[mdSize] : '',
|
|
3054
|
+
lgSize ? lgSizeClasses[lgSize] : '',
|
|
3024
3055
|
weightClasses[weight],
|
|
3025
3056
|
colorClasses[color],
|
|
3026
3057
|
alignClasses[align],
|
|
@@ -3127,24 +3158,48 @@ function Alert({ variant = 'info', title, children, onClose, className = '', act
|
|
|
3127
3158
|
return (jsxRuntime.jsx("div", { className: `rounded-lg border p-4 ${styles.container} ${className}`, role: "alert", children: jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [jsxRuntime.jsx("div", { className: "flex-shrink-0 mt-0.5", children: styles.icon }), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [title && jsxRuntime.jsx("h4", { className: "text-sm font-medium mb-1", children: title }), jsxRuntime.jsx("div", { className: "text-sm", children: children }), actions.length > 0 && (jsxRuntime.jsx("div", { className: "flex gap-2 mt-3", children: actions.map((action, index) => (jsxRuntime.jsx("button", { onClick: action.onClick, className: getButtonStyles(action.variant), children: action.label }, index))) }))] }), onClose && (jsxRuntime.jsx("button", { onClick: onClose, className: "flex-shrink-0 text-current opacity-70 hover:opacity-100 transition-opacity", "aria-label": "Close alert", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }) }));
|
|
3128
3159
|
}
|
|
3129
3160
|
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3161
|
+
/**
|
|
3162
|
+
* BottomSheet - Mobile-friendly modal that slides up from the bottom
|
|
3163
|
+
*
|
|
3164
|
+
* Designed for mobile contexts with touch-friendly interactions:
|
|
3165
|
+
* - Drag handle for swipe-to-dismiss
|
|
3166
|
+
* - Snap points for partial expansion
|
|
3167
|
+
* - Sticky action area at thumb zone
|
|
3168
|
+
*
|
|
3169
|
+
* @example
|
|
3170
|
+
* ```tsx
|
|
3171
|
+
* <BottomSheet open={isOpen} onClose={() => setIsOpen(false)}>
|
|
3172
|
+
* <BottomSheetHeader>
|
|
3173
|
+
* <Text weight="bold">Transaction Details</Text>
|
|
3174
|
+
* </BottomSheetHeader>
|
|
3175
|
+
* <BottomSheetContent>
|
|
3176
|
+
* {content}
|
|
3177
|
+
* </BottomSheetContent>
|
|
3178
|
+
* <BottomSheetActions>
|
|
3179
|
+
* <Button fullWidth>Approve</Button>
|
|
3180
|
+
* </BottomSheetActions>
|
|
3181
|
+
* </BottomSheet>
|
|
3182
|
+
* ```
|
|
3183
|
+
*/
|
|
3184
|
+
function BottomSheet({ open, isOpen, onClose, children, title, height = 'auto', maxHeight = '90vh', snapPoints, closeOnOverlayClick = true, closeOnEscape = true, showHandle = true, showCloseButton = true, preventScroll = true, className = '', }) {
|
|
3185
|
+
// Support both 'open' and 'isOpen' props for flexibility
|
|
3186
|
+
const isSheetOpen = open ?? isOpen ?? false;
|
|
3187
|
+
// Height presets for convenience
|
|
3188
|
+
const heightPresets = {
|
|
3189
|
+
sm: '40vh',
|
|
3190
|
+
md: '60vh',
|
|
3191
|
+
lg: '80vh',
|
|
3192
|
+
full: '100vh',
|
|
3193
|
+
};
|
|
3194
|
+
const sheetRef = React.useRef(null);
|
|
3138
3195
|
const [isDragging, setIsDragging] = React.useState(false);
|
|
3139
3196
|
const [dragOffset, setDragOffset] = React.useState(0);
|
|
3140
|
-
const [
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
const startYRef = React.useRef(0);
|
|
3145
|
-
// Close on Escape
|
|
3197
|
+
const [currentSnapIndex, setCurrentSnapIndex] = React.useState(snapPoints?.length ? snapPoints.length - 1 : 0);
|
|
3198
|
+
const startY = React.useRef(0);
|
|
3199
|
+
const startOffset = React.useRef(0);
|
|
3200
|
+
// Handle escape key
|
|
3146
3201
|
React.useEffect(() => {
|
|
3147
|
-
if (!
|
|
3202
|
+
if (!isSheetOpen || !closeOnEscape)
|
|
3148
3203
|
return;
|
|
3149
3204
|
const handleEscape = (e) => {
|
|
3150
3205
|
if (e.key === 'Escape') {
|
|
@@ -3153,77 +3208,132 @@ function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHand
|
|
|
3153
3208
|
};
|
|
3154
3209
|
document.addEventListener('keydown', handleEscape);
|
|
3155
3210
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
3156
|
-
}, [
|
|
3157
|
-
// Prevent body scroll
|
|
3211
|
+
}, [open, closeOnEscape, onClose]);
|
|
3212
|
+
// Prevent body scroll
|
|
3158
3213
|
React.useEffect(() => {
|
|
3159
|
-
if (
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
document.body.style.overflow = '';
|
|
3164
|
-
}
|
|
3214
|
+
if (!isSheetOpen || !preventScroll)
|
|
3215
|
+
return;
|
|
3216
|
+
const originalOverflow = document.body.style.overflow;
|
|
3217
|
+
document.body.style.overflow = 'hidden';
|
|
3165
3218
|
return () => {
|
|
3166
|
-
document.body.style.overflow =
|
|
3219
|
+
document.body.style.overflow = originalOverflow;
|
|
3167
3220
|
};
|
|
3168
|
-
}, [
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
onClose();
|
|
3172
|
-
}
|
|
3173
|
-
};
|
|
3174
|
-
const handleDragStart = (e) => {
|
|
3221
|
+
}, [open, preventScroll]);
|
|
3222
|
+
// Handle drag start
|
|
3223
|
+
const handleDragStart = React.useCallback((clientY) => {
|
|
3175
3224
|
setIsDragging(true);
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
};
|
|
3179
|
-
|
|
3225
|
+
startY.current = clientY;
|
|
3226
|
+
startOffset.current = dragOffset;
|
|
3227
|
+
}, [dragOffset]);
|
|
3228
|
+
// Handle drag move
|
|
3229
|
+
const handleDragMove = React.useCallback((clientY) => {
|
|
3230
|
+
if (!isDragging)
|
|
3231
|
+
return;
|
|
3232
|
+
const delta = clientY - startY.current;
|
|
3233
|
+
const newOffset = Math.max(0, startOffset.current + delta);
|
|
3234
|
+
setDragOffset(newOffset);
|
|
3235
|
+
}, [isDragging]);
|
|
3236
|
+
// Handle drag end
|
|
3237
|
+
const handleDragEnd = React.useCallback(() => {
|
|
3180
3238
|
if (!isDragging)
|
|
3181
3239
|
return;
|
|
3182
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
3183
|
-
const offset = clientY - startYRef.current;
|
|
3184
|
-
// Only allow dragging down
|
|
3185
|
-
if (offset > 0) {
|
|
3186
|
-
setDragOffset(offset);
|
|
3187
|
-
}
|
|
3188
|
-
};
|
|
3189
|
-
const handleDragEnd = () => {
|
|
3190
3240
|
setIsDragging(false);
|
|
3191
|
-
|
|
3192
|
-
if (dragOffset >
|
|
3193
|
-
|
|
3241
|
+
const threshold = 100; // pixels to trigger close
|
|
3242
|
+
if (dragOffset > threshold) {
|
|
3243
|
+
// If we have snap points, snap to next lower point or close
|
|
3244
|
+
if (snapPoints && currentSnapIndex > 0) {
|
|
3245
|
+
setCurrentSnapIndex(currentSnapIndex - 1);
|
|
3246
|
+
setDragOffset(0);
|
|
3247
|
+
}
|
|
3248
|
+
else {
|
|
3249
|
+
onClose();
|
|
3250
|
+
setDragOffset(0);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
else {
|
|
3254
|
+
// Snap back
|
|
3255
|
+
setDragOffset(0);
|
|
3194
3256
|
}
|
|
3195
|
-
|
|
3257
|
+
}, [isDragging, dragOffset, snapPoints, currentSnapIndex, onClose]);
|
|
3258
|
+
// Touch event handlers
|
|
3259
|
+
const handleTouchStart = (e) => {
|
|
3260
|
+
handleDragStart(e.touches[0].clientY);
|
|
3261
|
+
};
|
|
3262
|
+
const handleTouchMove = (e) => {
|
|
3263
|
+
handleDragMove(e.touches[0].clientY);
|
|
3264
|
+
};
|
|
3265
|
+
const handleTouchEnd = () => {
|
|
3266
|
+
handleDragEnd();
|
|
3267
|
+
};
|
|
3268
|
+
// Mouse event handlers (for desktop testing)
|
|
3269
|
+
const handleMouseDown = (e) => {
|
|
3270
|
+
handleDragStart(e.clientY);
|
|
3196
3271
|
};
|
|
3197
3272
|
React.useEffect(() => {
|
|
3198
3273
|
if (!isDragging)
|
|
3199
3274
|
return;
|
|
3200
|
-
const
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3275
|
+
const handleMouseMove = (e) => {
|
|
3276
|
+
handleDragMove(e.clientY);
|
|
3277
|
+
};
|
|
3278
|
+
const handleMouseUp = () => {
|
|
3279
|
+
handleDragEnd();
|
|
3280
|
+
};
|
|
3281
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
3282
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
3206
3283
|
return () => {
|
|
3207
|
-
document.removeEventListener('
|
|
3208
|
-
document.removeEventListener('
|
|
3209
|
-
document.removeEventListener('touchend', handleEnd);
|
|
3210
|
-
document.removeEventListener('mouseup', handleEnd);
|
|
3284
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
3285
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
3211
3286
|
};
|
|
3212
|
-
}, [isDragging,
|
|
3213
|
-
|
|
3287
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
3288
|
+
// Calculate height based on snap points or presets
|
|
3289
|
+
const getSheetHeight = () => {
|
|
3290
|
+
if (snapPoints && snapPoints[currentSnapIndex]) {
|
|
3291
|
+
return snapPoints[currentSnapIndex];
|
|
3292
|
+
}
|
|
3293
|
+
if (typeof height === 'number') {
|
|
3294
|
+
return `${height}px`;
|
|
3295
|
+
}
|
|
3296
|
+
// Check for preset heights
|
|
3297
|
+
if (typeof height === 'string' && heightPresets[height]) {
|
|
3298
|
+
return heightPresets[height];
|
|
3299
|
+
}
|
|
3300
|
+
return height;
|
|
3301
|
+
};
|
|
3302
|
+
if (!isSheetOpen)
|
|
3214
3303
|
return null;
|
|
3215
|
-
|
|
3304
|
+
const sheetContent = (jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50", children: [jsxRuntime.jsx("div", { className: `
|
|
3216
3305
|
absolute inset-0 bg-black/50 transition-opacity duration-300
|
|
3217
|
-
${
|
|
3218
|
-
|
|
3219
|
-
|
|
3306
|
+
${isSheetOpen ? 'opacity-100' : 'opacity-0'}
|
|
3307
|
+
`, onClick: closeOnOverlayClick ? onClose : undefined, "aria-hidden": "true" }), jsxRuntime.jsxs("div", { ref: sheetRef, className: `
|
|
3308
|
+
absolute bottom-0 left-0 right-0
|
|
3309
|
+
bg-white rounded-t-2xl shadow-2xl
|
|
3220
3310
|
transition-transform duration-300 ease-out
|
|
3221
|
-
${
|
|
3311
|
+
${isDragging ? 'transition-none' : ''}
|
|
3222
3312
|
${className}
|
|
3223
3313
|
`, style: {
|
|
3224
|
-
height:
|
|
3314
|
+
height: getSheetHeight(),
|
|
3315
|
+
maxHeight,
|
|
3225
3316
|
transform: `translateY(${dragOffset}px)`,
|
|
3226
|
-
}, role: "dialog", "aria-modal": "true",
|
|
3317
|
+
}, role: "dialog", "aria-modal": "true", children: [showHandle && (jsxRuntime.jsx("div", { className: "flex justify-center pt-3 pb-2 cursor-grab active:cursor-grabbing touch-none", onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, children: jsxRuntime.jsx("div", { className: "w-10 h-1 bg-paper-300 rounded-full" }) })), title && (jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-paper-200", children: [jsxRuntime.jsx("h2", { className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "p-1 rounded-full text-ink-500 hover:text-ink-700 hover:bg-paper-100 transition-colors", "aria-label": "Close", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] })), jsxRuntime.jsx("div", { className: "flex flex-col h-full overflow-hidden", children: children })] })] }));
|
|
3318
|
+
return reactDom.createPortal(sheetContent, document.body);
|
|
3319
|
+
}
|
|
3320
|
+
/**
|
|
3321
|
+
* BottomSheetHeader - Header section with title and optional close button
|
|
3322
|
+
*/
|
|
3323
|
+
function BottomSheetHeader({ children, className = '' }) {
|
|
3324
|
+
return (jsxRuntime.jsx("div", { className: `flex items-center justify-between px-4 py-3 border-b border-paper-200 ${className}`, children: children }));
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* BottomSheetContent - Scrollable content area
|
|
3328
|
+
*/
|
|
3329
|
+
function BottomSheetContent({ children, className = '' }) {
|
|
3330
|
+
return (jsxRuntime.jsx("div", { className: `flex-1 overflow-y-auto px-4 py-4 ${className}`, children: children }));
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* BottomSheetActions - Sticky footer for action buttons (thumb zone)
|
|
3334
|
+
*/
|
|
3335
|
+
function BottomSheetActions({ children, className = '' }) {
|
|
3336
|
+
return (jsxRuntime.jsx("div", { className: `flex flex-col gap-2 px-4 py-4 border-t border-paper-200 bg-white ${className}`, children: children }));
|
|
3227
3337
|
}
|
|
3228
3338
|
|
|
3229
3339
|
const sizeClasses$8 = {
|
|
@@ -7358,6 +7468,598 @@ function Hide({ children, above, below, only, className = '' }) {
|
|
|
7358
7468
|
return (jsxRuntime.jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
|
|
7359
7469
|
}
|
|
7360
7470
|
|
|
7471
|
+
/**
|
|
7472
|
+
* HorizontalScroll - Horizontally scrollable container with peek indicators
|
|
7473
|
+
*
|
|
7474
|
+
* Designed for mobile carousels of cards with:
|
|
7475
|
+
* - Touch-friendly momentum scrolling
|
|
7476
|
+
* - Peek hint showing more items exist
|
|
7477
|
+
* - Optional snap scrolling
|
|
7478
|
+
* - Navigation arrows for desktop
|
|
7479
|
+
*
|
|
7480
|
+
* @example
|
|
7481
|
+
* ```tsx
|
|
7482
|
+
* <HorizontalScroll gap="md" peekAmount={24} showIndicators>
|
|
7483
|
+
* <Card>Bill 1</Card>
|
|
7484
|
+
* <Card>Bill 2</Card>
|
|
7485
|
+
* <Card>Bill 3</Card>
|
|
7486
|
+
* </HorizontalScroll>
|
|
7487
|
+
* ```
|
|
7488
|
+
*/
|
|
7489
|
+
function HorizontalScroll({ children, gap = 'md', peekAmount = 24, showIndicators = false, snapToItem = true, showArrows = 'hover', scrollBehavior = 'smooth', className = '', scrollClassName = '', }) {
|
|
7490
|
+
const scrollRef = React.useRef(null);
|
|
7491
|
+
const [canScrollLeft, setCanScrollLeft] = React.useState(false);
|
|
7492
|
+
const [canScrollRight, setCanScrollRight] = React.useState(false);
|
|
7493
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
7494
|
+
const [itemCount, setItemCount] = React.useState(0);
|
|
7495
|
+
const [isHovered, setIsHovered] = React.useState(false);
|
|
7496
|
+
// Gap classes
|
|
7497
|
+
const gapClasses = {
|
|
7498
|
+
none: 'gap-0',
|
|
7499
|
+
sm: 'gap-2',
|
|
7500
|
+
md: 'gap-4',
|
|
7501
|
+
lg: 'gap-6',
|
|
7502
|
+
};
|
|
7503
|
+
const gapStyle = typeof gap === 'number' ? { gap: `${gap}px` } : {};
|
|
7504
|
+
const gapClass = typeof gap === 'string' ? gapClasses[gap] : '';
|
|
7505
|
+
// Check scroll position and update state
|
|
7506
|
+
const checkScrollPosition = React.useCallback(() => {
|
|
7507
|
+
const container = scrollRef.current;
|
|
7508
|
+
if (!container)
|
|
7509
|
+
return;
|
|
7510
|
+
const { scrollLeft, scrollWidth, clientWidth } = container;
|
|
7511
|
+
setCanScrollLeft(scrollLeft > 0);
|
|
7512
|
+
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1);
|
|
7513
|
+
// Calculate active index based on scroll position
|
|
7514
|
+
if (showIndicators && container.children.length > 0) {
|
|
7515
|
+
const children = Array.from(container.children);
|
|
7516
|
+
const containerRect = container.getBoundingClientRect();
|
|
7517
|
+
const containerCenter = containerRect.left + containerRect.width / 2;
|
|
7518
|
+
let closestIndex = 0;
|
|
7519
|
+
let closestDistance = Infinity;
|
|
7520
|
+
children.forEach((child, index) => {
|
|
7521
|
+
const childRect = child.getBoundingClientRect();
|
|
7522
|
+
const childCenter = childRect.left + childRect.width / 2;
|
|
7523
|
+
const distance = Math.abs(childCenter - containerCenter);
|
|
7524
|
+
if (distance < closestDistance) {
|
|
7525
|
+
closestDistance = distance;
|
|
7526
|
+
closestIndex = index;
|
|
7527
|
+
}
|
|
7528
|
+
});
|
|
7529
|
+
setActiveIndex(closestIndex);
|
|
7530
|
+
}
|
|
7531
|
+
}, [showIndicators]);
|
|
7532
|
+
// Initialize and handle resize
|
|
7533
|
+
React.useEffect(() => {
|
|
7534
|
+
const container = scrollRef.current;
|
|
7535
|
+
if (!container)
|
|
7536
|
+
return;
|
|
7537
|
+
setItemCount(React.Children.count(children));
|
|
7538
|
+
checkScrollPosition();
|
|
7539
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
7540
|
+
checkScrollPosition();
|
|
7541
|
+
});
|
|
7542
|
+
resizeObserver.observe(container);
|
|
7543
|
+
return () => {
|
|
7544
|
+
resizeObserver.disconnect();
|
|
7545
|
+
};
|
|
7546
|
+
}, [children, checkScrollPosition]);
|
|
7547
|
+
// Handle scroll event
|
|
7548
|
+
React.useEffect(() => {
|
|
7549
|
+
const container = scrollRef.current;
|
|
7550
|
+
if (!container)
|
|
7551
|
+
return;
|
|
7552
|
+
const handleScroll = () => {
|
|
7553
|
+
checkScrollPosition();
|
|
7554
|
+
};
|
|
7555
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
7556
|
+
return () => {
|
|
7557
|
+
container.removeEventListener('scroll', handleScroll);
|
|
7558
|
+
};
|
|
7559
|
+
}, [checkScrollPosition]);
|
|
7560
|
+
// Scroll by one item
|
|
7561
|
+
const scrollByItem = (direction) => {
|
|
7562
|
+
const container = scrollRef.current;
|
|
7563
|
+
if (!container)
|
|
7564
|
+
return;
|
|
7565
|
+
const children = Array.from(container.children);
|
|
7566
|
+
if (children.length === 0)
|
|
7567
|
+
return;
|
|
7568
|
+
const firstChild = children[0];
|
|
7569
|
+
const itemWidth = firstChild.offsetWidth;
|
|
7570
|
+
const gapValue = typeof gap === 'number' ? gap :
|
|
7571
|
+
gap === 'sm' ? 8 : gap === 'md' ? 16 : gap === 'lg' ? 24 : 0;
|
|
7572
|
+
const scrollAmount = itemWidth + gapValue;
|
|
7573
|
+
container.scrollBy({
|
|
7574
|
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
|
7575
|
+
behavior: scrollBehavior,
|
|
7576
|
+
});
|
|
7577
|
+
};
|
|
7578
|
+
// Scroll to specific index
|
|
7579
|
+
const scrollToIndex = (index) => {
|
|
7580
|
+
const container = scrollRef.current;
|
|
7581
|
+
if (!container)
|
|
7582
|
+
return;
|
|
7583
|
+
const children = Array.from(container.children);
|
|
7584
|
+
if (index < 0 || index >= children.length)
|
|
7585
|
+
return;
|
|
7586
|
+
const child = children[index];
|
|
7587
|
+
child.scrollIntoView({
|
|
7588
|
+
behavior: scrollBehavior,
|
|
7589
|
+
block: 'nearest',
|
|
7590
|
+
inline: 'center',
|
|
7591
|
+
});
|
|
7592
|
+
};
|
|
7593
|
+
const showLeftArrow = showArrows === 'always' || (showArrows === 'hover' && isHovered);
|
|
7594
|
+
const showRightArrow = showArrows === 'always' || (showArrows === 'hover' && isHovered);
|
|
7595
|
+
return (jsxRuntime.jsxs("div", { className: `relative ${className}`, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [showLeftArrow && canScrollLeft && (jsxRuntime.jsx("button", { onClick: () => scrollByItem('left'), className: "\n absolute left-0 top-1/2 -translate-y-1/2 z-10\n w-10 h-10 flex items-center justify-center\n bg-white/90 backdrop-blur-sm rounded-full shadow-lg\n text-ink-600 hover:text-ink-900 hover:bg-white\n transition-all duration-200\n -ml-2\n ", "aria-label": "Scroll left", children: jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "h-5 w-5" }) })), jsxRuntime.jsx("div", { ref: scrollRef, className: `
|
|
7596
|
+
flex overflow-x-auto scrollbar-hide
|
|
7597
|
+
${gapClass}
|
|
7598
|
+
${snapToItem ? 'snap-x snap-mandatory' : ''}
|
|
7599
|
+
${scrollClassName}
|
|
7600
|
+
`, style: {
|
|
7601
|
+
...gapStyle,
|
|
7602
|
+
paddingRight: peekAmount > 0 ? `${peekAmount}px` : undefined,
|
|
7603
|
+
scrollPaddingLeft: '0px',
|
|
7604
|
+
scrollPaddingRight: `${peekAmount}px`,
|
|
7605
|
+
}, children: React.Children.map(children, (child, index) => (jsxRuntime.jsx("div", { className: `flex-shrink-0 ${snapToItem ? 'snap-start' : ''}`, children: child }, index))) }), showRightArrow && canScrollRight && (jsxRuntime.jsx("button", { onClick: () => scrollByItem('right'), className: "\n absolute right-0 top-1/2 -translate-y-1/2 z-10\n w-10 h-10 flex items-center justify-center\n bg-white/90 backdrop-blur-sm rounded-full shadow-lg\n text-ink-600 hover:text-ink-900 hover:bg-white\n transition-all duration-200\n -mr-2\n ", "aria-label": "Scroll right", children: jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-5 w-5" }) })), showIndicators && itemCount > 1 && (jsxRuntime.jsx("div", { className: "flex justify-center gap-1.5 mt-3", children: Array.from({ length: itemCount }).map((_, index) => (jsxRuntime.jsx("button", { onClick: () => scrollToIndex(index), className: `
|
|
7606
|
+
w-2 h-2 rounded-full transition-all duration-200
|
|
7607
|
+
${index === activeIndex
|
|
7608
|
+
? 'bg-accent-500 w-4'
|
|
7609
|
+
: 'bg-paper-300 hover:bg-paper-400'}
|
|
7610
|
+
`, "aria-label": `Go to item ${index + 1}`, "aria-current": index === activeIndex ? 'true' : 'false' }, index))) }))] }));
|
|
7611
|
+
}
|
|
7612
|
+
|
|
7613
|
+
/**
|
|
7614
|
+
* SwipeableCard - Card component with swipe-to-action functionality
|
|
7615
|
+
*
|
|
7616
|
+
* Designed for mobile approval workflows:
|
|
7617
|
+
* - Swipe right to approve/confirm
|
|
7618
|
+
* - Swipe left to see options/alternatives
|
|
7619
|
+
* - Visual feedback showing action being revealed
|
|
7620
|
+
* - Haptic feedback on mobile devices
|
|
7621
|
+
*
|
|
7622
|
+
* @example
|
|
7623
|
+
* ```tsx
|
|
7624
|
+
* <SwipeableCard
|
|
7625
|
+
* onSwipeRight={() => handleApprove()}
|
|
7626
|
+
* onSwipeLeft={() => handleShowOptions()}
|
|
7627
|
+
* rightAction={{
|
|
7628
|
+
* icon: <Check />,
|
|
7629
|
+
* color: 'success',
|
|
7630
|
+
* label: 'Approve'
|
|
7631
|
+
* }}
|
|
7632
|
+
* leftAction={{
|
|
7633
|
+
* icon: <MoreHorizontal />,
|
|
7634
|
+
* color: 'neutral',
|
|
7635
|
+
* label: 'Options'
|
|
7636
|
+
* }}
|
|
7637
|
+
* >
|
|
7638
|
+
* <TransactionContent />
|
|
7639
|
+
* </SwipeableCard>
|
|
7640
|
+
* ```
|
|
7641
|
+
*/
|
|
7642
|
+
function SwipeableCard({ children, onSwipeRight, onSwipeLeft, rightAction = {
|
|
7643
|
+
icon: jsxRuntime.jsx(lucideReact.Check, { className: "h-6 w-6" }),
|
|
7644
|
+
color: 'success',
|
|
7645
|
+
label: 'Approve',
|
|
7646
|
+
}, leftAction = {
|
|
7647
|
+
icon: jsxRuntime.jsx(lucideReact.MoreHorizontal, { className: "h-6 w-6" }),
|
|
7648
|
+
color: 'neutral',
|
|
7649
|
+
label: 'Options',
|
|
7650
|
+
}, swipeThreshold = 100, hapticFeedback = true, disabled = false, onSwipeStart, onSwipeEnd, className = '', }) {
|
|
7651
|
+
const cardRef = React.useRef(null);
|
|
7652
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
7653
|
+
const [offsetX, setOffsetX] = React.useState(0);
|
|
7654
|
+
const [isTriggered, setIsTriggered] = React.useState(null);
|
|
7655
|
+
const startX = React.useRef(0);
|
|
7656
|
+
const startY = React.useRef(0);
|
|
7657
|
+
const isHorizontalSwipe = React.useRef(null);
|
|
7658
|
+
// Color classes for action backgrounds
|
|
7659
|
+
const colorClasses = {
|
|
7660
|
+
success: 'bg-success-500',
|
|
7661
|
+
error: 'bg-error-500',
|
|
7662
|
+
warning: 'bg-warning-500',
|
|
7663
|
+
neutral: 'bg-paper-400',
|
|
7664
|
+
primary: 'bg-accent-500',
|
|
7665
|
+
};
|
|
7666
|
+
// Trigger haptic feedback
|
|
7667
|
+
const triggerHaptic = React.useCallback((style = 'medium') => {
|
|
7668
|
+
if (!hapticFeedback)
|
|
7669
|
+
return;
|
|
7670
|
+
// Use Vibration API if available
|
|
7671
|
+
if ('vibrate' in navigator) {
|
|
7672
|
+
const patterns = {
|
|
7673
|
+
light: 10,
|
|
7674
|
+
medium: 25,
|
|
7675
|
+
heavy: [50, 30, 50],
|
|
7676
|
+
};
|
|
7677
|
+
navigator.vibrate(patterns[style]);
|
|
7678
|
+
}
|
|
7679
|
+
}, [hapticFeedback]);
|
|
7680
|
+
// Handle drag start
|
|
7681
|
+
const handleDragStart = React.useCallback((clientX, clientY) => {
|
|
7682
|
+
if (disabled)
|
|
7683
|
+
return;
|
|
7684
|
+
setIsDragging(true);
|
|
7685
|
+
startX.current = clientX;
|
|
7686
|
+
startY.current = clientY;
|
|
7687
|
+
isHorizontalSwipe.current = null;
|
|
7688
|
+
onSwipeStart?.();
|
|
7689
|
+
}, [disabled, onSwipeStart]);
|
|
7690
|
+
// Handle drag move
|
|
7691
|
+
const handleDragMove = React.useCallback((clientX, clientY) => {
|
|
7692
|
+
if (!isDragging || disabled)
|
|
7693
|
+
return;
|
|
7694
|
+
const deltaX = clientX - startX.current;
|
|
7695
|
+
const deltaY = clientY - startY.current;
|
|
7696
|
+
// Determine if this is a horizontal swipe on first significant movement
|
|
7697
|
+
if (isHorizontalSwipe.current === null) {
|
|
7698
|
+
const absDeltaX = Math.abs(deltaX);
|
|
7699
|
+
const absDeltaY = Math.abs(deltaY);
|
|
7700
|
+
if (absDeltaX > 10 || absDeltaY > 10) {
|
|
7701
|
+
isHorizontalSwipe.current = absDeltaX > absDeltaY;
|
|
7702
|
+
}
|
|
7703
|
+
}
|
|
7704
|
+
// Only process horizontal swipes
|
|
7705
|
+
if (isHorizontalSwipe.current !== true)
|
|
7706
|
+
return;
|
|
7707
|
+
// Check if we should allow this direction
|
|
7708
|
+
const canSwipeRight = onSwipeRight !== undefined;
|
|
7709
|
+
const canSwipeLeft = onSwipeLeft !== undefined;
|
|
7710
|
+
let newOffset = deltaX;
|
|
7711
|
+
// Limit swipe direction based on available actions
|
|
7712
|
+
if (!canSwipeRight && deltaX > 0)
|
|
7713
|
+
newOffset = 0;
|
|
7714
|
+
if (!canSwipeLeft && deltaX < 0)
|
|
7715
|
+
newOffset = 0;
|
|
7716
|
+
// Add resistance when exceeding threshold
|
|
7717
|
+
const maxSwipe = swipeThreshold * 1.5;
|
|
7718
|
+
if (Math.abs(newOffset) > swipeThreshold) {
|
|
7719
|
+
const overflow = Math.abs(newOffset) - swipeThreshold;
|
|
7720
|
+
const resistance = overflow * 0.3;
|
|
7721
|
+
newOffset = newOffset > 0
|
|
7722
|
+
? swipeThreshold + resistance
|
|
7723
|
+
: -(swipeThreshold + resistance);
|
|
7724
|
+
newOffset = Math.max(-maxSwipe, Math.min(maxSwipe, newOffset));
|
|
7725
|
+
}
|
|
7726
|
+
setOffsetX(newOffset);
|
|
7727
|
+
// Check for threshold crossing and trigger haptic
|
|
7728
|
+
const newTriggered = Math.abs(newOffset) >= swipeThreshold
|
|
7729
|
+
? (newOffset > 0 ? 'right' : 'left')
|
|
7730
|
+
: null;
|
|
7731
|
+
if (newTriggered !== isTriggered) {
|
|
7732
|
+
if (newTriggered) {
|
|
7733
|
+
triggerHaptic('medium');
|
|
7734
|
+
}
|
|
7735
|
+
setIsTriggered(newTriggered);
|
|
7736
|
+
}
|
|
7737
|
+
}, [isDragging, disabled, onSwipeRight, onSwipeLeft, swipeThreshold, isTriggered, triggerHaptic]);
|
|
7738
|
+
// Handle drag end
|
|
7739
|
+
const handleDragEnd = React.useCallback(() => {
|
|
7740
|
+
if (!isDragging)
|
|
7741
|
+
return;
|
|
7742
|
+
setIsDragging(false);
|
|
7743
|
+
onSwipeEnd?.();
|
|
7744
|
+
// Check if action should be triggered
|
|
7745
|
+
if (Math.abs(offsetX) >= swipeThreshold) {
|
|
7746
|
+
if (offsetX > 0 && onSwipeRight) {
|
|
7747
|
+
triggerHaptic('heavy');
|
|
7748
|
+
// Animate card away then call handler
|
|
7749
|
+
setOffsetX(window.innerWidth);
|
|
7750
|
+
setTimeout(() => {
|
|
7751
|
+
onSwipeRight();
|
|
7752
|
+
setOffsetX(0);
|
|
7753
|
+
setIsTriggered(null);
|
|
7754
|
+
}, 200);
|
|
7755
|
+
return;
|
|
7756
|
+
}
|
|
7757
|
+
else if (offsetX < 0 && onSwipeLeft) {
|
|
7758
|
+
triggerHaptic('heavy');
|
|
7759
|
+
setOffsetX(-window.innerWidth);
|
|
7760
|
+
setTimeout(() => {
|
|
7761
|
+
onSwipeLeft();
|
|
7762
|
+
setOffsetX(0);
|
|
7763
|
+
setIsTriggered(null);
|
|
7764
|
+
}, 200);
|
|
7765
|
+
return;
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
// Snap back
|
|
7769
|
+
setOffsetX(0);
|
|
7770
|
+
setIsTriggered(null);
|
|
7771
|
+
}, [isDragging, offsetX, swipeThreshold, onSwipeRight, onSwipeLeft, onSwipeEnd, triggerHaptic]);
|
|
7772
|
+
// Touch event handlers
|
|
7773
|
+
const handleTouchStart = (e) => {
|
|
7774
|
+
handleDragStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
7775
|
+
};
|
|
7776
|
+
const handleTouchMove = (e) => {
|
|
7777
|
+
handleDragMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
7778
|
+
// Prevent vertical scroll if horizontal swipe
|
|
7779
|
+
if (isHorizontalSwipe.current === true) {
|
|
7780
|
+
e.preventDefault();
|
|
7781
|
+
}
|
|
7782
|
+
};
|
|
7783
|
+
const handleTouchEnd = () => {
|
|
7784
|
+
handleDragEnd();
|
|
7785
|
+
};
|
|
7786
|
+
// Mouse event handlers (for desktop testing)
|
|
7787
|
+
const handleMouseDown = (e) => {
|
|
7788
|
+
handleDragStart(e.clientX, e.clientY);
|
|
7789
|
+
};
|
|
7790
|
+
React.useEffect(() => {
|
|
7791
|
+
if (!isDragging)
|
|
7792
|
+
return;
|
|
7793
|
+
const handleMouseMove = (e) => {
|
|
7794
|
+
handleDragMove(e.clientX, e.clientY);
|
|
7795
|
+
};
|
|
7796
|
+
const handleMouseUp = () => {
|
|
7797
|
+
handleDragEnd();
|
|
7798
|
+
};
|
|
7799
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
7800
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
7801
|
+
return () => {
|
|
7802
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
7803
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
7804
|
+
};
|
|
7805
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
7806
|
+
// Calculate action opacity based on swipe distance
|
|
7807
|
+
const rightActionOpacity = offsetX > 0 ? Math.min(1, offsetX / swipeThreshold) : 0;
|
|
7808
|
+
const leftActionOpacity = offsetX < 0 ? Math.min(1, Math.abs(offsetX) / swipeThreshold) : 0;
|
|
7809
|
+
return (jsxRuntime.jsxs("div", { className: `relative overflow-hidden rounded-lg ${className}`, children: [onSwipeRight && (jsxRuntime.jsx("div", { className: `
|
|
7810
|
+
absolute inset-y-0 left-0 flex items-center justify-start pl-6
|
|
7811
|
+
${colorClasses[rightAction.color]}
|
|
7812
|
+
transition-opacity duration-100
|
|
7813
|
+
`, style: {
|
|
7814
|
+
opacity: rightActionOpacity,
|
|
7815
|
+
width: Math.abs(offsetX) + 20,
|
|
7816
|
+
}, "aria-hidden": "true", children: jsxRuntime.jsx("div", { className: `
|
|
7817
|
+
text-white transform transition-transform duration-200
|
|
7818
|
+
${isTriggered === 'right' ? 'scale-125' : 'scale-100'}
|
|
7819
|
+
`, children: rightAction.icon }) })), onSwipeLeft && (jsxRuntime.jsx("div", { className: `
|
|
7820
|
+
absolute inset-y-0 right-0 flex items-center justify-end pr-6
|
|
7821
|
+
${colorClasses[leftAction.color]}
|
|
7822
|
+
transition-opacity duration-100
|
|
7823
|
+
`, style: {
|
|
7824
|
+
opacity: leftActionOpacity,
|
|
7825
|
+
width: Math.abs(offsetX) + 20,
|
|
7826
|
+
}, "aria-hidden": "true", children: jsxRuntime.jsx("div", { className: `
|
|
7827
|
+
text-white transform transition-transform duration-200
|
|
7828
|
+
${isTriggered === 'left' ? 'scale-125' : 'scale-100'}
|
|
7829
|
+
`, children: leftAction.icon }) })), jsxRuntime.jsx("div", { ref: cardRef, className: `
|
|
7830
|
+
relative bg-white
|
|
7831
|
+
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
7832
|
+
${disabled ? 'opacity-50 pointer-events-none' : ''}
|
|
7833
|
+
`, style: {
|
|
7834
|
+
transform: `translateX(${offsetX}px)`,
|
|
7835
|
+
}, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, role: "button", "aria-label": `Swipeable card. ${onSwipeRight ? `Swipe right to ${rightAction.label}.` : ''} ${onSwipeLeft ? `Swipe left to ${leftAction.label}.` : ''}`, tabIndex: disabled ? -1 : 0, children: children })] }));
|
|
7836
|
+
}
|
|
7837
|
+
|
|
7838
|
+
/**
|
|
7839
|
+
* NotificationBanner - Dismissible banner for important alerts
|
|
7840
|
+
*
|
|
7841
|
+
* Displays at top of screen for alerts that need attention but aren't blocking:
|
|
7842
|
+
* - Money Found alerts
|
|
7843
|
+
* - System messages
|
|
7844
|
+
* - Promotional info
|
|
7845
|
+
*
|
|
7846
|
+
* @example
|
|
7847
|
+
* ```tsx
|
|
7848
|
+
* <NotificationBanner
|
|
7849
|
+
* variant="warning"
|
|
7850
|
+
* icon={<DollarSign />}
|
|
7851
|
+
* title="Found $33.98 in potential savings"
|
|
7852
|
+
* description="Tap to review"
|
|
7853
|
+
* action={{
|
|
7854
|
+
* label: "Review",
|
|
7855
|
+
* onClick: handleReview
|
|
7856
|
+
* }}
|
|
7857
|
+
* onDismiss={() => setShowBanner(false)}
|
|
7858
|
+
* />
|
|
7859
|
+
* ```
|
|
7860
|
+
*/
|
|
7861
|
+
function NotificationBanner({ variant = 'info', icon, title, description, action, onDismiss, dismissible = true, sticky = false, className = '', }) {
|
|
7862
|
+
const bannerRef = React.useRef(null);
|
|
7863
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
7864
|
+
const [offsetX, setOffsetX] = React.useState(0);
|
|
7865
|
+
const [isDismissed, setIsDismissed] = React.useState(false);
|
|
7866
|
+
const startX = React.useRef(0);
|
|
7867
|
+
// Default icons based on variant
|
|
7868
|
+
const defaultIcons = {
|
|
7869
|
+
info: jsxRuntime.jsx(lucideReact.Info, { className: "h-5 w-5" }),
|
|
7870
|
+
success: jsxRuntime.jsx(lucideReact.CheckCircle, { className: "h-5 w-5" }),
|
|
7871
|
+
warning: jsxRuntime.jsx(lucideReact.AlertTriangle, { className: "h-5 w-5" }),
|
|
7872
|
+
error: jsxRuntime.jsx(lucideReact.AlertCircle, { className: "h-5 w-5" }),
|
|
7873
|
+
};
|
|
7874
|
+
// Color classes
|
|
7875
|
+
const variantClasses = {
|
|
7876
|
+
info: 'bg-gradient-to-r from-primary-50 to-primary-100 border-primary-200 text-primary-900',
|
|
7877
|
+
success: 'bg-gradient-to-r from-success-50 to-success-100 border-success-200 text-success-900',
|
|
7878
|
+
warning: 'bg-gradient-to-r from-warning-50 to-warning-100 border-warning-200 text-warning-900',
|
|
7879
|
+
error: 'bg-gradient-to-r from-error-50 to-error-100 border-error-200 text-error-900',
|
|
7880
|
+
};
|
|
7881
|
+
const iconColorClasses = {
|
|
7882
|
+
info: 'text-primary-600',
|
|
7883
|
+
success: 'text-success-600',
|
|
7884
|
+
warning: 'text-warning-600',
|
|
7885
|
+
error: 'text-error-600',
|
|
7886
|
+
};
|
|
7887
|
+
const buttonClasses = {
|
|
7888
|
+
info: 'bg-primary-600 hover:bg-primary-700 text-white',
|
|
7889
|
+
success: 'bg-success-600 hover:bg-success-700 text-white',
|
|
7890
|
+
warning: 'bg-warning-600 hover:bg-warning-700 text-white',
|
|
7891
|
+
error: 'bg-error-600 hover:bg-error-700 text-white',
|
|
7892
|
+
};
|
|
7893
|
+
// Handle swipe dismiss
|
|
7894
|
+
const handleDragStart = React.useCallback((clientX) => {
|
|
7895
|
+
if (!dismissible)
|
|
7896
|
+
return;
|
|
7897
|
+
setIsDragging(true);
|
|
7898
|
+
startX.current = clientX;
|
|
7899
|
+
}, [dismissible]);
|
|
7900
|
+
const handleDragMove = React.useCallback((clientX) => {
|
|
7901
|
+
if (!isDragging)
|
|
7902
|
+
return;
|
|
7903
|
+
const delta = clientX - startX.current;
|
|
7904
|
+
setOffsetX(delta);
|
|
7905
|
+
}, [isDragging]);
|
|
7906
|
+
const handleDragEnd = React.useCallback(() => {
|
|
7907
|
+
if (!isDragging)
|
|
7908
|
+
return;
|
|
7909
|
+
setIsDragging(false);
|
|
7910
|
+
const threshold = 100;
|
|
7911
|
+
if (Math.abs(offsetX) > threshold) {
|
|
7912
|
+
// Animate out
|
|
7913
|
+
setOffsetX(offsetX > 0 ? window.innerWidth : -window.innerWidth);
|
|
7914
|
+
setIsDismissed(true);
|
|
7915
|
+
setTimeout(() => {
|
|
7916
|
+
onDismiss?.();
|
|
7917
|
+
}, 200);
|
|
7918
|
+
}
|
|
7919
|
+
else {
|
|
7920
|
+
// Snap back
|
|
7921
|
+
setOffsetX(0);
|
|
7922
|
+
}
|
|
7923
|
+
}, [isDragging, offsetX, onDismiss]);
|
|
7924
|
+
// Touch handlers
|
|
7925
|
+
const handleTouchStart = (e) => {
|
|
7926
|
+
handleDragStart(e.touches[0].clientX);
|
|
7927
|
+
};
|
|
7928
|
+
const handleTouchMove = (e) => {
|
|
7929
|
+
handleDragMove(e.touches[0].clientX);
|
|
7930
|
+
};
|
|
7931
|
+
const handleTouchEnd = () => {
|
|
7932
|
+
handleDragEnd();
|
|
7933
|
+
};
|
|
7934
|
+
// Mouse handlers for desktop testing
|
|
7935
|
+
const handleMouseDown = (e) => {
|
|
7936
|
+
if (dismissible) {
|
|
7937
|
+
handleDragStart(e.clientX);
|
|
7938
|
+
}
|
|
7939
|
+
};
|
|
7940
|
+
React.useEffect(() => {
|
|
7941
|
+
if (!isDragging)
|
|
7942
|
+
return;
|
|
7943
|
+
const handleMouseMove = (e) => {
|
|
7944
|
+
handleDragMove(e.clientX);
|
|
7945
|
+
};
|
|
7946
|
+
const handleMouseUp = () => {
|
|
7947
|
+
handleDragEnd();
|
|
7948
|
+
};
|
|
7949
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
7950
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
7951
|
+
return () => {
|
|
7952
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
7953
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
7954
|
+
};
|
|
7955
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
7956
|
+
if (isDismissed)
|
|
7957
|
+
return null;
|
|
7958
|
+
return (jsxRuntime.jsx("div", { ref: bannerRef, className: `
|
|
7959
|
+
w-full border-b
|
|
7960
|
+
${variantClasses[variant]}
|
|
7961
|
+
${sticky ? 'sticky top-0 z-40' : ''}
|
|
7962
|
+
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
7963
|
+
${className}
|
|
7964
|
+
`, style: {
|
|
7965
|
+
transform: `translateX(${offsetX}px)`,
|
|
7966
|
+
opacity: Math.max(0, 1 - Math.abs(offsetX) / 200),
|
|
7967
|
+
}, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, role: "alert", children: jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [jsxRuntime.jsx("div", { className: `flex-shrink-0 ${iconColorClasses[variant]}`, children: icon || defaultIcons[variant] }), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("p", { className: "text-sm font-medium truncate", children: title }), description && (jsxRuntime.jsx("p", { className: "text-xs opacity-80 truncate", children: description }))] }), action && (jsxRuntime.jsx("button", { onClick: action.onClick, className: `
|
|
7968
|
+
flex-shrink-0 px-3 py-1.5 text-xs font-medium rounded-md
|
|
7969
|
+
transition-colors duration-200
|
|
7970
|
+
${buttonClasses[variant]}
|
|
7971
|
+
`, children: action.label })), onDismiss && (jsxRuntime.jsx("button", { onClick: onDismiss, className: "flex-shrink-0 p-1 rounded-full hover:bg-black/10 transition-colors duration-200", "aria-label": "Dismiss notification", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }) }));
|
|
7972
|
+
}
|
|
7973
|
+
|
|
7974
|
+
/**
|
|
7975
|
+
* CompactStat - Single stat display optimized for mobile
|
|
7976
|
+
*
|
|
7977
|
+
* Designed for dashboard stats in 2-column mobile layouts:
|
|
7978
|
+
* - Compact presentation with value, label, and optional trend
|
|
7979
|
+
* - Responsive sizing
|
|
7980
|
+
* - Trend indicators with color coding
|
|
7981
|
+
*
|
|
7982
|
+
* @example
|
|
7983
|
+
* ```tsx
|
|
7984
|
+
* <Grid columns={2} gap="sm">
|
|
7985
|
+
* <CompactStat
|
|
7986
|
+
* value="$62,329"
|
|
7987
|
+
* label="Net Worth"
|
|
7988
|
+
* trend={{
|
|
7989
|
+
* direction: 'up',
|
|
7990
|
+
* value: '+$1,247',
|
|
7991
|
+
* color: 'success'
|
|
7992
|
+
* }}
|
|
7993
|
+
* />
|
|
7994
|
+
* <CompactStat
|
|
7995
|
+
* value="$4,521"
|
|
7996
|
+
* label="Monthly Income"
|
|
7997
|
+
* />
|
|
7998
|
+
* </Grid>
|
|
7999
|
+
* ```
|
|
8000
|
+
*/
|
|
8001
|
+
function CompactStat({ value, label, trend, size = 'md', align = 'left', className = '', }) {
|
|
8002
|
+
// Size classes
|
|
8003
|
+
const sizeClasses = {
|
|
8004
|
+
sm: {
|
|
8005
|
+
value: 'text-lg font-semibold',
|
|
8006
|
+
label: 'text-xs',
|
|
8007
|
+
trend: 'text-xs',
|
|
8008
|
+
icon: 'h-3 w-3',
|
|
8009
|
+
},
|
|
8010
|
+
md: {
|
|
8011
|
+
value: 'text-xl font-semibold',
|
|
8012
|
+
label: 'text-sm',
|
|
8013
|
+
trend: 'text-xs',
|
|
8014
|
+
icon: 'h-3.5 w-3.5',
|
|
8015
|
+
},
|
|
8016
|
+
lg: {
|
|
8017
|
+
value: 'text-2xl font-bold',
|
|
8018
|
+
label: 'text-sm',
|
|
8019
|
+
trend: 'text-sm',
|
|
8020
|
+
icon: 'h-4 w-4',
|
|
8021
|
+
},
|
|
8022
|
+
};
|
|
8023
|
+
// Alignment classes
|
|
8024
|
+
const alignClasses = {
|
|
8025
|
+
left: 'text-left',
|
|
8026
|
+
center: 'text-center',
|
|
8027
|
+
right: 'text-right',
|
|
8028
|
+
};
|
|
8029
|
+
// Trend color classes
|
|
8030
|
+
const getTrendColor = (trend) => {
|
|
8031
|
+
if (trend.color) {
|
|
8032
|
+
const colorMap = {
|
|
8033
|
+
success: 'text-success-600',
|
|
8034
|
+
error: 'text-error-600',
|
|
8035
|
+
warning: 'text-warning-600',
|
|
8036
|
+
neutral: 'text-ink-500',
|
|
8037
|
+
};
|
|
8038
|
+
return colorMap[trend.color];
|
|
8039
|
+
}
|
|
8040
|
+
// Default colors based on direction
|
|
8041
|
+
const directionColors = {
|
|
8042
|
+
up: 'text-success-600',
|
|
8043
|
+
down: 'text-error-600',
|
|
8044
|
+
neutral: 'text-ink-500',
|
|
8045
|
+
};
|
|
8046
|
+
return directionColors[trend.direction];
|
|
8047
|
+
};
|
|
8048
|
+
// Trend icons
|
|
8049
|
+
const TrendIcon = trend ? {
|
|
8050
|
+
up: lucideReact.TrendingUp,
|
|
8051
|
+
down: lucideReact.TrendingDown,
|
|
8052
|
+
neutral: lucideReact.Minus,
|
|
8053
|
+
}[trend.direction] : null;
|
|
8054
|
+
const sizes = sizeClasses[size];
|
|
8055
|
+
return (jsxRuntime.jsxs("div", { className: `${alignClasses[align]} ${className}`, children: [jsxRuntime.jsx("div", { className: `${sizes.value} text-ink-900 tracking-tight`, children: value }), jsxRuntime.jsx("div", { className: `${sizes.label} text-ink-500 mt-0.5`, children: label }), trend && (jsxRuntime.jsxs("div", { className: `
|
|
8056
|
+
flex items-center gap-1 mt-1
|
|
8057
|
+
${align === 'center' ? 'justify-center' : ''}
|
|
8058
|
+
${align === 'right' ? 'justify-end' : ''}
|
|
8059
|
+
${sizes.trend} ${getTrendColor(trend)}
|
|
8060
|
+
`, children: [TrendIcon && jsxRuntime.jsx(TrendIcon, { className: sizes.icon }), jsxRuntime.jsx("span", { children: trend.value })] }))] }));
|
|
8061
|
+
}
|
|
8062
|
+
|
|
7361
8063
|
/**
|
|
7362
8064
|
* Hook to detect breadcrumb navigation and trigger callbacks.
|
|
7363
8065
|
* Use this in host components to reset state when a breadcrumb is clicked.
|
|
@@ -7641,7 +8343,7 @@ function StepIndicator({ steps, currentStep, variant = 'horizontal', onStepClick
|
|
|
7641
8343
|
}) }) }));
|
|
7642
8344
|
}
|
|
7643
8345
|
|
|
7644
|
-
function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, className = '', dot = false, }) {
|
|
8346
|
+
function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, className = '', dot = false, pill = false, }) {
|
|
7645
8347
|
const variantStyles = {
|
|
7646
8348
|
success: 'bg-success-50 text-success-700 border-success-200',
|
|
7647
8349
|
warning: 'bg-warning-50 text-warning-700 border-warning-200',
|
|
@@ -7661,6 +8363,12 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
|
|
|
7661
8363
|
md: 'px-3 py-1 text-xs gap-1.5',
|
|
7662
8364
|
lg: 'px-3 py-1.5 text-sm gap-2',
|
|
7663
8365
|
};
|
|
8366
|
+
// Pill variant has tighter horizontal padding and fully rounded ends
|
|
8367
|
+
const pillSizeStyles = {
|
|
8368
|
+
sm: 'px-1.5 py-0.5 text-xs gap-1',
|
|
8369
|
+
md: 'px-2 py-0.5 text-xs gap-1',
|
|
8370
|
+
lg: 'px-2.5 py-1 text-sm gap-1.5',
|
|
8371
|
+
};
|
|
7664
8372
|
const dotSizeStyles = {
|
|
7665
8373
|
sm: 'h-1.5 w-1.5',
|
|
7666
8374
|
md: 'h-2 w-2',
|
|
@@ -7682,9 +8390,10 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
|
|
|
7682
8390
|
}
|
|
7683
8391
|
// Regular badge
|
|
7684
8392
|
return (jsxRuntime.jsxs("span", { className: `
|
|
7685
|
-
inline-flex items-center
|
|
8393
|
+
inline-flex items-center border font-medium
|
|
8394
|
+
${pill ? 'rounded-full' : 'rounded-full'}
|
|
7686
8395
|
${variantStyles[variant]}
|
|
7687
|
-
${sizeStyles[size]}
|
|
8396
|
+
${pill ? pillSizeStyles[size] : sizeStyles[size]}
|
|
7688
8397
|
${className}
|
|
7689
8398
|
`, children: [icon && jsxRuntime.jsx("span", { className: iconSize[size], children: icon }), jsxRuntime.jsx("span", { children: children }), onRemove && (jsxRuntime.jsx("button", { onClick: onRemove, className: "ml-1 hover:opacity-70 transition-opacity", "aria-label": "Remove badge", children: jsxRuntime.jsx(lucideReact.X, { className: iconSize[size] }) }))] }));
|
|
7690
8399
|
}
|
|
@@ -7792,8 +8501,10 @@ function Progress({ value, variant = 'linear', size = 'md', color = 'primary', s
|
|
|
7792
8501
|
warning: 'bg-warning-100',
|
|
7793
8502
|
error: 'bg-error-100',
|
|
7794
8503
|
};
|
|
8504
|
+
// Normalize 'ring' to 'circular'
|
|
8505
|
+
const normalizedVariant = variant === 'ring' ? 'circular' : variant;
|
|
7795
8506
|
// Linear progress
|
|
7796
|
-
if (
|
|
8507
|
+
if (normalizedVariant === 'linear') {
|
|
7797
8508
|
const heightClasses = {
|
|
7798
8509
|
sm: 'h-1',
|
|
7799
8510
|
md: 'h-2',
|
|
@@ -8725,94 +9436,112 @@ function useFABScroll(threshold = 10) {
|
|
|
8725
9436
|
}
|
|
8726
9437
|
|
|
8727
9438
|
/**
|
|
8728
|
-
* PullToRefresh -
|
|
9439
|
+
* PullToRefresh - Pull-down refresh indicator and handler for mobile lists
|
|
8729
9440
|
*
|
|
8730
|
-
* Wraps content
|
|
8731
|
-
*
|
|
8732
|
-
*
|
|
8733
|
-
*
|
|
8734
|
-
* ```tsx
|
|
8735
|
-
* <PullToRefresh onRefresh={async () => {
|
|
8736
|
-
* await fetchLatestData();
|
|
8737
|
-
* }}>
|
|
8738
|
-
* <div className="min-h-screen">
|
|
8739
|
-
* {content}
|
|
8740
|
-
* </div>
|
|
8741
|
-
* </PullToRefresh>
|
|
8742
|
-
* ```
|
|
9441
|
+
* Wraps content to enable pull-to-refresh behavior on mobile:
|
|
9442
|
+
* - Pull down to trigger refresh
|
|
9443
|
+
* - Visual feedback showing progress
|
|
9444
|
+
* - Custom content for each state
|
|
8743
9445
|
*
|
|
8744
|
-
* @example
|
|
9446
|
+
* @example
|
|
8745
9447
|
* ```tsx
|
|
8746
|
-
* <PullToRefresh
|
|
8747
|
-
*
|
|
8748
|
-
* pullThreshold={100}
|
|
8749
|
-
* maxPull={150}
|
|
8750
|
-
* >
|
|
8751
|
-
* {content}
|
|
9448
|
+
* <PullToRefresh onRefresh={async () => { await syncData(); }}>
|
|
9449
|
+
* <TransactionList transactions={transactions} />
|
|
8752
9450
|
* </PullToRefresh>
|
|
8753
9451
|
* ```
|
|
8754
9452
|
*/
|
|
8755
|
-
function PullToRefresh({ children, onRefresh,
|
|
9453
|
+
function PullToRefresh({ children, onRefresh, threshold = 80, disabled = false, pullingContent, releaseContent, refreshingContent, completeContent, className = '', }) {
|
|
9454
|
+
const containerRef = React.useRef(null);
|
|
8756
9455
|
const [state, setState] = React.useState('idle');
|
|
8757
9456
|
const [pullDistance, setPullDistance] = React.useState(0);
|
|
8758
|
-
const containerRef = React.useRef(null);
|
|
8759
9457
|
const startY = React.useRef(0);
|
|
8760
9458
|
const currentY = React.useRef(0);
|
|
8761
|
-
|
|
9459
|
+
const isDragging = React.useRef(false);
|
|
9460
|
+
// Check if content is at top (can pull to refresh)
|
|
8762
9461
|
const isAtTop = React.useCallback(() => {
|
|
8763
9462
|
const container = containerRef.current;
|
|
8764
9463
|
if (!container)
|
|
8765
9464
|
return false;
|
|
8766
|
-
|
|
9465
|
+
// Check if the scrollable content is at the top
|
|
9466
|
+
const scrollableParent = container.querySelector('[data-ptr-scrollable]') || container;
|
|
9467
|
+
return scrollableParent.scrollTop <= 0;
|
|
8767
9468
|
}, []);
|
|
8768
|
-
// Handle
|
|
9469
|
+
// Handle pull start
|
|
8769
9470
|
const handleTouchStart = React.useCallback((e) => {
|
|
8770
|
-
if (disabled || state === 'refreshing'
|
|
9471
|
+
if (disabled || state === 'refreshing')
|
|
9472
|
+
return;
|
|
9473
|
+
if (!isAtTop())
|
|
8771
9474
|
return;
|
|
9475
|
+
isDragging.current = true;
|
|
8772
9476
|
startY.current = e.touches[0].clientY;
|
|
8773
|
-
currentY.current =
|
|
9477
|
+
currentY.current = e.touches[0].clientY;
|
|
8774
9478
|
}, [disabled, state, isAtTop]);
|
|
8775
|
-
// Handle
|
|
9479
|
+
// Handle pull move
|
|
8776
9480
|
const handleTouchMove = React.useCallback((e) => {
|
|
8777
|
-
if (disabled || state === 'refreshing')
|
|
8778
|
-
return;
|
|
8779
|
-
if (startY.current === 0)
|
|
9481
|
+
if (!isDragging.current || disabled || state === 'refreshing')
|
|
8780
9482
|
return;
|
|
8781
9483
|
currentY.current = e.touches[0].clientY;
|
|
8782
|
-
const
|
|
8783
|
-
// Only
|
|
8784
|
-
if (
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
9484
|
+
const delta = currentY.current - startY.current;
|
|
9485
|
+
// Only activate pull-to-refresh when pulling down
|
|
9486
|
+
if (delta < 0) {
|
|
9487
|
+
isDragging.current = false;
|
|
9488
|
+
setPullDistance(0);
|
|
9489
|
+
setState('idle');
|
|
9490
|
+
return;
|
|
9491
|
+
}
|
|
9492
|
+
// Check if we're at the top before allowing pull
|
|
9493
|
+
if (!isAtTop()) {
|
|
9494
|
+
isDragging.current = false;
|
|
9495
|
+
return;
|
|
9496
|
+
}
|
|
9497
|
+
// Apply resistance to pull
|
|
9498
|
+
const resistance = 0.5;
|
|
9499
|
+
const resistedDelta = delta * resistance;
|
|
9500
|
+
const maxPull = threshold * 2;
|
|
9501
|
+
const clampedDelta = Math.min(resistedDelta, maxPull);
|
|
9502
|
+
setPullDistance(clampedDelta);
|
|
9503
|
+
// Update state based on pull distance
|
|
9504
|
+
if (clampedDelta >= threshold) {
|
|
9505
|
+
setState('ready');
|
|
9506
|
+
}
|
|
9507
|
+
else if (clampedDelta > 0) {
|
|
9508
|
+
setState('pulling');
|
|
9509
|
+
}
|
|
9510
|
+
// Prevent default scroll when pulling
|
|
9511
|
+
if (delta > 0 && isAtTop()) {
|
|
9512
|
+
e.preventDefault();
|
|
8794
9513
|
}
|
|
8795
|
-
}, [disabled, state,
|
|
8796
|
-
// Handle
|
|
9514
|
+
}, [disabled, state, threshold, isAtTop]);
|
|
9515
|
+
// Handle pull end
|
|
8797
9516
|
const handleTouchEnd = React.useCallback(async () => {
|
|
8798
|
-
if (
|
|
9517
|
+
if (!isDragging.current)
|
|
8799
9518
|
return;
|
|
8800
|
-
|
|
9519
|
+
isDragging.current = false;
|
|
9520
|
+
if (state === 'ready' && pullDistance >= threshold) {
|
|
8801
9521
|
setState('refreshing');
|
|
8802
|
-
setPullDistance(
|
|
9522
|
+
setPullDistance(threshold * 0.6); // Settle at a smaller height while refreshing
|
|
8803
9523
|
try {
|
|
8804
9524
|
await onRefresh();
|
|
9525
|
+
setState('complete');
|
|
9526
|
+
// Show complete state briefly
|
|
9527
|
+
setTimeout(() => {
|
|
9528
|
+
setState('idle');
|
|
9529
|
+
setPullDistance(0);
|
|
9530
|
+
}, 500);
|
|
8805
9531
|
}
|
|
8806
9532
|
catch (error) {
|
|
8807
9533
|
console.error('Refresh failed:', error);
|
|
9534
|
+
setState('idle');
|
|
9535
|
+
setPullDistance(0);
|
|
8808
9536
|
}
|
|
9537
|
+
}
|
|
9538
|
+
else {
|
|
9539
|
+
// Snap back
|
|
8809
9540
|
setState('idle');
|
|
9541
|
+
setPullDistance(0);
|
|
8810
9542
|
}
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
currentY.current = 0;
|
|
8814
|
-
}, [disabled, state, pullThreshold, onRefresh]);
|
|
8815
|
-
// Attach touch listeners
|
|
9543
|
+
}, [state, pullDistance, threshold, onRefresh]);
|
|
9544
|
+
// Attach touch event listeners
|
|
8816
9545
|
React.useEffect(() => {
|
|
8817
9546
|
const container = containerRef.current;
|
|
8818
9547
|
if (!container)
|
|
@@ -8826,99 +9555,41 @@ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold =
|
|
|
8826
9555
|
container.removeEventListener('touchend', handleTouchEnd);
|
|
8827
9556
|
};
|
|
8828
9557
|
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
8829
|
-
// Calculate
|
|
8830
|
-
const progress = Math.min(pullDistance /
|
|
8831
|
-
|
|
8832
|
-
|
|
8833
|
-
const
|
|
8834
|
-
|
|
8835
|
-
const
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
|
|
8841
|
-
|
|
8842
|
-
|
|
9558
|
+
// Calculate progress percentage
|
|
9559
|
+
const progress = Math.min(1, pullDistance / threshold);
|
|
9560
|
+
// Default content for each state
|
|
9561
|
+
const defaultPullingContent = (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-5 w-5 text-ink-400 transition-transform duration-200", style: { transform: `rotate(${progress * 180}deg)` } }), jsxRuntime.jsx("span", { className: "text-xs text-ink-500", children: "Pull to refresh" })] }));
|
|
9562
|
+
const defaultReleaseContent = (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-5 w-5 text-accent-500 rotate-180" }), jsxRuntime.jsx("span", { className: "text-xs text-accent-600 font-medium", children: "Release to refresh" })] }));
|
|
9563
|
+
const defaultRefreshingContent = (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 text-accent-500 animate-spin" }), jsxRuntime.jsx("span", { className: "text-xs text-ink-500", children: "Refreshing..." })] }));
|
|
9564
|
+
const defaultCompleteContent = (jsxRuntime.jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsxRuntime.jsx(lucideReact.Check, { className: "h-5 w-5 text-success-500" }), jsxRuntime.jsx("span", { className: "text-xs text-success-600", children: "Done!" })] }));
|
|
9565
|
+
// Get content based on current state
|
|
9566
|
+
const getIndicatorContent = () => {
|
|
9567
|
+
switch (state) {
|
|
9568
|
+
case 'pulling':
|
|
9569
|
+
return pullingContent || defaultPullingContent;
|
|
9570
|
+
case 'ready':
|
|
9571
|
+
return releaseContent || defaultReleaseContent;
|
|
9572
|
+
case 'refreshing':
|
|
9573
|
+
return refreshingContent || defaultRefreshingContent;
|
|
9574
|
+
case 'complete':
|
|
9575
|
+
return completeContent || defaultCompleteContent;
|
|
9576
|
+
default:
|
|
9577
|
+
return null;
|
|
9578
|
+
}
|
|
9579
|
+
};
|
|
9580
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, children: [jsxRuntime.jsx("div", { className: `
|
|
9581
|
+
absolute top-0 left-0 right-0
|
|
9582
|
+
flex items-center justify-center
|
|
9583
|
+
bg-paper-50
|
|
9584
|
+
transition-all duration-200 ease-out
|
|
9585
|
+
${state === 'idle' ? 'opacity-0' : 'opacity-100'}
|
|
8843
9586
|
`, style: {
|
|
8844
|
-
height:
|
|
8845
|
-
|
|
8846
|
-
|
|
8847
|
-
}, children: jsxRuntime.jsx("div", { className: `
|
|
8848
|
-
w-10 h-10 rounded-full bg-white shadow-md
|
|
8849
|
-
flex items-center justify-center
|
|
8850
|
-
transition-transform duration-200
|
|
8851
|
-
${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
|
|
8852
|
-
`, children: state === 'refreshing'
|
|
8853
|
-
? (loadingIndicator || defaultLoadingIndicator)
|
|
8854
|
-
: (pullIndicator || defaultPullIndicator) }) }), jsxRuntime.jsx("div", { className: "transition-transform duration-200", style: {
|
|
9587
|
+
height: pullDistance,
|
|
9588
|
+
transform: state === 'idle' ? 'translateY(-100%)' : 'translateY(0)',
|
|
9589
|
+
}, children: getIndicatorContent() }), jsxRuntime.jsx("div", { className: "transition-transform duration-200 ease-out", style: {
|
|
8855
9590
|
transform: `translateY(${pullDistance}px)`,
|
|
8856
9591
|
}, children: children })] }));
|
|
8857
9592
|
}
|
|
8858
|
-
/**
|
|
8859
|
-
* usePullToRefresh - Hook for custom pull-to-refresh implementations
|
|
8860
|
-
*
|
|
8861
|
-
* @example
|
|
8862
|
-
* ```tsx
|
|
8863
|
-
* const { pullDistance, isRefreshing, bind } = usePullToRefresh({
|
|
8864
|
-
* onRefresh: async () => {
|
|
8865
|
-
* await fetchData();
|
|
8866
|
-
* }
|
|
8867
|
-
* });
|
|
8868
|
-
*
|
|
8869
|
-
* return (
|
|
8870
|
-
* <div {...bind}>
|
|
8871
|
-
* {isRefreshing && <Spinner />}
|
|
8872
|
-
* {content}
|
|
8873
|
-
* </div>
|
|
8874
|
-
* );
|
|
8875
|
-
* ```
|
|
8876
|
-
*/
|
|
8877
|
-
function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
|
|
8878
|
-
const [pullDistance, setPullDistance] = React.useState(0);
|
|
8879
|
-
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
|
8880
|
-
const startY = React.useRef(0);
|
|
8881
|
-
const handleTouchStart = React.useCallback((e) => {
|
|
8882
|
-
if (disabled || isRefreshing)
|
|
8883
|
-
return;
|
|
8884
|
-
startY.current = e.touches[0].clientY;
|
|
8885
|
-
}, [disabled, isRefreshing]);
|
|
8886
|
-
const handleTouchMove = React.useCallback((e) => {
|
|
8887
|
-
if (disabled || isRefreshing || startY.current === 0)
|
|
8888
|
-
return;
|
|
8889
|
-
const diff = e.touches[0].clientY - startY.current;
|
|
8890
|
-
if (diff > 0) {
|
|
8891
|
-
const adjustedPull = Math.min(diff * 0.5, maxPull);
|
|
8892
|
-
setPullDistance(adjustedPull);
|
|
8893
|
-
}
|
|
8894
|
-
}, [disabled, isRefreshing, maxPull]);
|
|
8895
|
-
const handleTouchEnd = React.useCallback(async () => {
|
|
8896
|
-
if (disabled || isRefreshing)
|
|
8897
|
-
return;
|
|
8898
|
-
if (pullDistance >= pullThreshold) {
|
|
8899
|
-
setIsRefreshing(true);
|
|
8900
|
-
try {
|
|
8901
|
-
await onRefresh();
|
|
8902
|
-
}
|
|
8903
|
-
finally {
|
|
8904
|
-
setIsRefreshing(false);
|
|
8905
|
-
}
|
|
8906
|
-
}
|
|
8907
|
-
setPullDistance(0);
|
|
8908
|
-
startY.current = 0;
|
|
8909
|
-
}, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
|
|
8910
|
-
return {
|
|
8911
|
-
pullDistance,
|
|
8912
|
-
isRefreshing,
|
|
8913
|
-
isReady: pullDistance >= pullThreshold,
|
|
8914
|
-
progress: Math.min(pullDistance / pullThreshold, 1),
|
|
8915
|
-
bind: {
|
|
8916
|
-
onTouchStart: handleTouchStart,
|
|
8917
|
-
onTouchMove: handleTouchMove,
|
|
8918
|
-
onTouchEnd: handleTouchEnd,
|
|
8919
|
-
},
|
|
8920
|
-
};
|
|
8921
|
-
}
|
|
8922
9593
|
|
|
8923
9594
|
function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
|
|
8924
9595
|
const sizes = {
|
|
@@ -10421,44 +11092,52 @@ function getAugmentedNamespace(n) {
|
|
|
10421
11092
|
* (A1, A1:C5, ...)
|
|
10422
11093
|
*/
|
|
10423
11094
|
|
|
10424
|
-
|
|
11095
|
+
var collection;
|
|
11096
|
+
var hasRequiredCollection;
|
|
11097
|
+
|
|
11098
|
+
function requireCollection () {
|
|
11099
|
+
if (hasRequiredCollection) return collection;
|
|
11100
|
+
hasRequiredCollection = 1;
|
|
11101
|
+
class Collection {
|
|
10425
11102
|
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
11103
|
+
constructor(data, refs) {
|
|
11104
|
+
if (data == null && refs == null) {
|
|
11105
|
+
this._data = [];
|
|
11106
|
+
this._refs = [];
|
|
11107
|
+
} else {
|
|
11108
|
+
if (data.length !== refs.length)
|
|
11109
|
+
throw Error('Collection: data length should match references length.');
|
|
11110
|
+
this._data = data;
|
|
11111
|
+
this._refs = refs;
|
|
11112
|
+
}
|
|
11113
|
+
}
|
|
10437
11114
|
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
11115
|
+
get data() {
|
|
11116
|
+
return this._data;
|
|
11117
|
+
}
|
|
10441
11118
|
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
11119
|
+
get refs() {
|
|
11120
|
+
return this._refs;
|
|
11121
|
+
}
|
|
10445
11122
|
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
11123
|
+
get length() {
|
|
11124
|
+
return this._data.length;
|
|
11125
|
+
}
|
|
10449
11126
|
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
}
|
|
11127
|
+
/**
|
|
11128
|
+
* Add data and references to this collection.
|
|
11129
|
+
* @param {{}} obj - data
|
|
11130
|
+
* @param {{}} ref - reference
|
|
11131
|
+
*/
|
|
11132
|
+
add(obj, ref) {
|
|
11133
|
+
this._data.push(obj);
|
|
11134
|
+
this._refs.push(ref);
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
10460
11137
|
|
|
10461
|
-
|
|
11138
|
+
collection = Collection;
|
|
11139
|
+
return collection;
|
|
11140
|
+
}
|
|
10462
11141
|
|
|
10463
11142
|
var helpers;
|
|
10464
11143
|
var hasRequiredHelpers;
|
|
@@ -10467,7 +11146,7 @@ function requireHelpers () {
|
|
|
10467
11146
|
if (hasRequiredHelpers) return helpers;
|
|
10468
11147
|
hasRequiredHelpers = 1;
|
|
10469
11148
|
const FormulaError = requireError();
|
|
10470
|
-
const Collection =
|
|
11149
|
+
const Collection = requireCollection();
|
|
10471
11150
|
|
|
10472
11151
|
const Types = {
|
|
10473
11152
|
NUMBER: 0,
|
|
@@ -20121,7 +20800,7 @@ var engineering = EngineeringFunctions;
|
|
|
20121
20800
|
|
|
20122
20801
|
const FormulaError$b = requireError();
|
|
20123
20802
|
const {FormulaHelpers: FormulaHelpers$8, Types: Types$6, WildCard, Address: Address$3} = requireHelpers();
|
|
20124
|
-
const Collection$2 =
|
|
20803
|
+
const Collection$2 = requireCollection();
|
|
20125
20804
|
const H$5 = FormulaHelpers$8;
|
|
20126
20805
|
|
|
20127
20806
|
const ReferenceFunctions$1 = {
|
|
@@ -31749,7 +32428,7 @@ var parsing = {
|
|
|
31749
32428
|
const FormulaError$4 = requireError();
|
|
31750
32429
|
const {Address: Address$1} = requireHelpers();
|
|
31751
32430
|
const {Prefix: Prefix$1, Postfix: Postfix$1, Infix: Infix$1, Operators: Operators$1} = operators;
|
|
31752
|
-
const Collection$1 =
|
|
32431
|
+
const Collection$1 = requireCollection();
|
|
31753
32432
|
const MAX_ROW$1 = 1048576, MAX_COLUMN$1 = 16384;
|
|
31754
32433
|
const {NotAllInputParsedException} = require$$4;
|
|
31755
32434
|
|
|
@@ -32511,7 +33190,7 @@ var hooks$1 = {
|
|
|
32511
33190
|
const FormulaError$2 = requireError();
|
|
32512
33191
|
const {FormulaHelpers: FormulaHelpers$1, Types, Address} = requireHelpers();
|
|
32513
33192
|
const {Prefix, Postfix, Infix, Operators} = operators;
|
|
32514
|
-
const Collection =
|
|
33193
|
+
const Collection = requireCollection();
|
|
32515
33194
|
const MAX_ROW = 1048576, MAX_COLUMN = 16384;
|
|
32516
33195
|
|
|
32517
33196
|
let Utils$1 = class Utils {
|
|
@@ -57018,6 +57697,9 @@ exports.Badge = Badge;
|
|
|
57018
57697
|
exports.BottomNavigation = BottomNavigation;
|
|
57019
57698
|
exports.BottomNavigationSpacer = BottomNavigationSpacer;
|
|
57020
57699
|
exports.BottomSheet = BottomSheet;
|
|
57700
|
+
exports.BottomSheetActions = BottomSheetActions;
|
|
57701
|
+
exports.BottomSheetContent = BottomSheetContent;
|
|
57702
|
+
exports.BottomSheetHeader = BottomSheetHeader;
|
|
57021
57703
|
exports.Box = Box;
|
|
57022
57704
|
exports.Breadcrumbs = Breadcrumbs;
|
|
57023
57705
|
exports.Button = Button;
|
|
@@ -57040,6 +57722,7 @@ exports.ColorPicker = ColorPicker;
|
|
|
57040
57722
|
exports.Combobox = Combobox;
|
|
57041
57723
|
exports.ComingSoon = ComingSoon;
|
|
57042
57724
|
exports.CommandPalette = CommandPalette;
|
|
57725
|
+
exports.CompactStat = CompactStat;
|
|
57043
57726
|
exports.ConfirmDialog = ConfirmDialog;
|
|
57044
57727
|
exports.ContextMenu = ContextMenu;
|
|
57045
57728
|
exports.ControlBar = ControlBar;
|
|
@@ -57086,6 +57769,7 @@ exports.FormWizard = FormWizard;
|
|
|
57086
57769
|
exports.Grid = Grid;
|
|
57087
57770
|
exports.GridItem = GridItem;
|
|
57088
57771
|
exports.Hide = Hide;
|
|
57772
|
+
exports.HorizontalScroll = HorizontalScroll;
|
|
57089
57773
|
exports.HoverCard = HoverCard;
|
|
57090
57774
|
exports.InfiniteScroll = InfiniteScroll;
|
|
57091
57775
|
exports.Input = Input;
|
|
@@ -57106,6 +57790,7 @@ exports.MobileProvider = MobileProvider;
|
|
|
57106
57790
|
exports.Modal = Modal;
|
|
57107
57791
|
exports.ModalFooter = ModalFooter;
|
|
57108
57792
|
exports.MultiSelect = MultiSelect;
|
|
57793
|
+
exports.NotificationBanner = NotificationBanner;
|
|
57109
57794
|
exports.NotificationBar = NotificationBar;
|
|
57110
57795
|
exports.NotificationIndicator = NotificationIndicator;
|
|
57111
57796
|
exports.NumberInput = NumberInput;
|
|
@@ -57146,6 +57831,7 @@ exports.StatusBar = StatusBar;
|
|
|
57146
57831
|
exports.StepIndicator = StepIndicator;
|
|
57147
57832
|
exports.Stepper = Stepper;
|
|
57148
57833
|
exports.SwipeActions = SwipeActions;
|
|
57834
|
+
exports.SwipeableCard = SwipeableCard;
|
|
57149
57835
|
exports.Switch = Switch;
|
|
57150
57836
|
exports.Tabs = Tabs;
|
|
57151
57837
|
exports.Text = Text;
|
|
@@ -57200,7 +57886,6 @@ exports.useMediaQuery = useMediaQuery;
|
|
|
57200
57886
|
exports.useMobileContext = useMobileContext;
|
|
57201
57887
|
exports.useOrientation = useOrientation;
|
|
57202
57888
|
exports.usePrefersMobile = usePrefersMobile;
|
|
57203
|
-
exports.usePullToRefresh = usePullToRefresh;
|
|
57204
57889
|
exports.useResponsiveCallback = useResponsiveCallback;
|
|
57205
57890
|
exports.useSafeAreaInsets = useSafeAreaInsets;
|
|
57206
57891
|
exports.useViewportSize = useViewportSize;
|