@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.esm.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
import React__default, { forwardRef, useState, useEffect, useCallback, useRef, useId, useImperativeHandle, useMemo, Children, isValidElement, cloneElement, Component, createContext as createContext$1, useLayoutEffect, createElement, useContext, useReducer } from 'react';
|
|
4
|
-
import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, ChevronsLeft, ChevronsRight, Circle, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, Home, FileText, Image, File as File$1, Menu as Menu$1, ArrowDown, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Pin, PinOff, Download, Save, ArrowUpDown, Filter, XCircle, BarChart3, MessageSquare } from 'lucide-react';
|
|
4
|
+
import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, ChevronsLeft, ChevronsRight, Circle, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, MoreHorizontal, Home, FileText, Image, File as File$1, Menu as Menu$1, ArrowDown, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Pin, PinOff, Download, Save, ArrowUpDown, Filter, XCircle, BarChart3, MessageSquare } from 'lucide-react';
|
|
5
5
|
import { createPortal } from 'react-dom';
|
|
6
6
|
import { useInRouterContext, useNavigate, useLocation, Link as Link$1 } from 'react-router-dom';
|
|
7
7
|
|
|
@@ -2507,7 +2507,7 @@ const Card = forwardRef(({ children, variant = 'default', width = 'auto', classN
|
|
|
2507
2507
|
const baseStyles = 'bg-white bg-subtle-grain border-2 border-paper-300 transition-shadow duration-200';
|
|
2508
2508
|
const variantStyles = {
|
|
2509
2509
|
default: 'rounded-xl shadow-lg p-8',
|
|
2510
|
-
compact: 'rounded-lg shadow-md p-
|
|
2510
|
+
compact: 'rounded-lg shadow-md p-3', // 12px padding for mobile-density layouts
|
|
2511
2511
|
flat: 'rounded-lg p-5',
|
|
2512
2512
|
};
|
|
2513
2513
|
const widthStyles = {
|
|
@@ -2623,6 +2623,7 @@ function Separator({ orientation = 'horizontal', className = '', spacing = 'md',
|
|
|
2623
2623
|
*
|
|
2624
2624
|
* Spacing scale (use either `spacing` or `gap` prop - they're aliases):
|
|
2625
2625
|
* - none: 0
|
|
2626
|
+
* - tight: 0.25rem (1) - for mobile-density layouts
|
|
2626
2627
|
* - xs: 0.5rem (2)
|
|
2627
2628
|
* - sm: 0.75rem (3)
|
|
2628
2629
|
* - md: 1.5rem (6)
|
|
@@ -2650,6 +2651,7 @@ const Stack = forwardRef(({ children, direction = 'vertical', spacing, gap, alig
|
|
|
2650
2651
|
const spacingClasses = {
|
|
2651
2652
|
vertical: {
|
|
2652
2653
|
none: '',
|
|
2654
|
+
tight: 'space-y-1', // 4px - for mobile-density layouts
|
|
2653
2655
|
xs: 'space-y-2',
|
|
2654
2656
|
sm: 'space-y-3',
|
|
2655
2657
|
md: 'space-y-6',
|
|
@@ -2658,6 +2660,7 @@ const Stack = forwardRef(({ children, direction = 'vertical', spacing, gap, alig
|
|
|
2658
2660
|
},
|
|
2659
2661
|
horizontal: {
|
|
2660
2662
|
none: '',
|
|
2663
|
+
tight: 'space-x-1', // 4px - for mobile-density layouts
|
|
2661
2664
|
xs: 'space-x-2',
|
|
2662
2665
|
sm: 'space-x-3',
|
|
2663
2666
|
md: 'space-x-6',
|
|
@@ -2955,7 +2958,7 @@ const GridItem = ({ colSpan, rowSpan, children, className = '', ...boxProps }) =
|
|
|
2955
2958
|
* <Text ref={textRef}>Measurable text</Text>
|
|
2956
2959
|
* ```
|
|
2957
2960
|
*/
|
|
2958
|
-
const Text = forwardRef(({ children, as: Component = 'p', size = 'base', weight = 'normal', color = 'primary', align = 'left', truncate = false, lineClamp, transform, className = '', ...htmlProps }, ref) => {
|
|
2961
|
+
const Text = forwardRef(({ children, as: Component = 'p', size = 'base', smSize, mdSize, lgSize, weight = 'normal', color = 'primary', align = 'left', truncate = false, lineClamp, transform, className = '', ...htmlProps }, ref) => {
|
|
2959
2962
|
const sizeClasses = {
|
|
2960
2963
|
xs: 'text-xs',
|
|
2961
2964
|
sm: 'text-sm',
|
|
@@ -2964,6 +2967,31 @@ const Text = forwardRef(({ children, as: Component = 'p', size = 'base', weight
|
|
|
2964
2967
|
xl: 'text-xl',
|
|
2965
2968
|
'2xl': 'text-2xl',
|
|
2966
2969
|
};
|
|
2970
|
+
// Responsive size classes
|
|
2971
|
+
const smSizeClasses = {
|
|
2972
|
+
xs: 'sm:text-xs',
|
|
2973
|
+
sm: 'sm:text-sm',
|
|
2974
|
+
base: 'sm:text-base',
|
|
2975
|
+
lg: 'sm:text-lg',
|
|
2976
|
+
xl: 'sm:text-xl',
|
|
2977
|
+
'2xl': 'sm:text-2xl',
|
|
2978
|
+
};
|
|
2979
|
+
const mdSizeClasses = {
|
|
2980
|
+
xs: 'md:text-xs',
|
|
2981
|
+
sm: 'md:text-sm',
|
|
2982
|
+
base: 'md:text-base',
|
|
2983
|
+
lg: 'md:text-lg',
|
|
2984
|
+
xl: 'md:text-xl',
|
|
2985
|
+
'2xl': 'md:text-2xl',
|
|
2986
|
+
};
|
|
2987
|
+
const lgSizeClasses = {
|
|
2988
|
+
xs: 'lg:text-xs',
|
|
2989
|
+
sm: 'lg:text-sm',
|
|
2990
|
+
base: 'lg:text-base',
|
|
2991
|
+
lg: 'lg:text-lg',
|
|
2992
|
+
xl: 'lg:text-xl',
|
|
2993
|
+
'2xl': 'lg:text-2xl',
|
|
2994
|
+
};
|
|
2967
2995
|
const weightClasses = {
|
|
2968
2996
|
normal: 'font-normal',
|
|
2969
2997
|
medium: 'font-medium',
|
|
@@ -3001,6 +3029,9 @@ const Text = forwardRef(({ children, as: Component = 'p', size = 'base', weight
|
|
|
3001
3029
|
// Build class list
|
|
3002
3030
|
const classes = [
|
|
3003
3031
|
sizeClasses[size],
|
|
3032
|
+
smSize ? smSizeClasses[smSize] : '',
|
|
3033
|
+
mdSize ? mdSizeClasses[mdSize] : '',
|
|
3034
|
+
lgSize ? lgSizeClasses[lgSize] : '',
|
|
3004
3035
|
weightClasses[weight],
|
|
3005
3036
|
colorClasses[color],
|
|
3006
3037
|
alignClasses[align],
|
|
@@ -3107,24 +3138,48 @@ function Alert({ variant = 'info', title, children, onClose, className = '', act
|
|
|
3107
3138
|
return (jsx("div", { className: `rounded-lg border p-4 ${styles.container} ${className}`, role: "alert", children: jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex-shrink-0 mt-0.5", children: styles.icon }), jsxs("div", { className: "flex-1 min-w-0", children: [title && jsx("h4", { className: "text-sm font-medium mb-1", children: title }), jsx("div", { className: "text-sm", children: children }), actions.length > 0 && (jsx("div", { className: "flex gap-2 mt-3", children: actions.map((action, index) => (jsx("button", { onClick: action.onClick, className: getButtonStyles(action.variant), children: action.label }, index))) }))] }), onClose && (jsx("button", { onClick: onClose, className: "flex-shrink-0 text-current opacity-70 hover:opacity-100 transition-opacity", "aria-label": "Close alert", children: jsx(X, { className: "h-4 w-4" }) }))] }) }));
|
|
3108
3139
|
}
|
|
3109
3140
|
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3141
|
+
/**
|
|
3142
|
+
* BottomSheet - Mobile-friendly modal that slides up from the bottom
|
|
3143
|
+
*
|
|
3144
|
+
* Designed for mobile contexts with touch-friendly interactions:
|
|
3145
|
+
* - Drag handle for swipe-to-dismiss
|
|
3146
|
+
* - Snap points for partial expansion
|
|
3147
|
+
* - Sticky action area at thumb zone
|
|
3148
|
+
*
|
|
3149
|
+
* @example
|
|
3150
|
+
* ```tsx
|
|
3151
|
+
* <BottomSheet open={isOpen} onClose={() => setIsOpen(false)}>
|
|
3152
|
+
* <BottomSheetHeader>
|
|
3153
|
+
* <Text weight="bold">Transaction Details</Text>
|
|
3154
|
+
* </BottomSheetHeader>
|
|
3155
|
+
* <BottomSheetContent>
|
|
3156
|
+
* {content}
|
|
3157
|
+
* </BottomSheetContent>
|
|
3158
|
+
* <BottomSheetActions>
|
|
3159
|
+
* <Button fullWidth>Approve</Button>
|
|
3160
|
+
* </BottomSheetActions>
|
|
3161
|
+
* </BottomSheet>
|
|
3162
|
+
* ```
|
|
3163
|
+
*/
|
|
3164
|
+
function BottomSheet({ open, isOpen, onClose, children, title, height = 'auto', maxHeight = '90vh', snapPoints, closeOnOverlayClick = true, closeOnEscape = true, showHandle = true, showCloseButton = true, preventScroll = true, className = '', }) {
|
|
3165
|
+
// Support both 'open' and 'isOpen' props for flexibility
|
|
3166
|
+
const isSheetOpen = open ?? isOpen ?? false;
|
|
3167
|
+
// Height presets for convenience
|
|
3168
|
+
const heightPresets = {
|
|
3169
|
+
sm: '40vh',
|
|
3170
|
+
md: '60vh',
|
|
3171
|
+
lg: '80vh',
|
|
3172
|
+
full: '100vh',
|
|
3173
|
+
};
|
|
3174
|
+
const sheetRef = useRef(null);
|
|
3118
3175
|
const [isDragging, setIsDragging] = useState(false);
|
|
3119
3176
|
const [dragOffset, setDragOffset] = useState(0);
|
|
3120
|
-
const [
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
const startYRef = useRef(0);
|
|
3125
|
-
// Close on Escape
|
|
3177
|
+
const [currentSnapIndex, setCurrentSnapIndex] = useState(snapPoints?.length ? snapPoints.length - 1 : 0);
|
|
3178
|
+
const startY = useRef(0);
|
|
3179
|
+
const startOffset = useRef(0);
|
|
3180
|
+
// Handle escape key
|
|
3126
3181
|
useEffect(() => {
|
|
3127
|
-
if (!
|
|
3182
|
+
if (!isSheetOpen || !closeOnEscape)
|
|
3128
3183
|
return;
|
|
3129
3184
|
const handleEscape = (e) => {
|
|
3130
3185
|
if (e.key === 'Escape') {
|
|
@@ -3133,77 +3188,132 @@ function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHand
|
|
|
3133
3188
|
};
|
|
3134
3189
|
document.addEventListener('keydown', handleEscape);
|
|
3135
3190
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
3136
|
-
}, [
|
|
3137
|
-
// Prevent body scroll
|
|
3191
|
+
}, [open, closeOnEscape, onClose]);
|
|
3192
|
+
// Prevent body scroll
|
|
3138
3193
|
useEffect(() => {
|
|
3139
|
-
if (
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
document.body.style.overflow = '';
|
|
3144
|
-
}
|
|
3194
|
+
if (!isSheetOpen || !preventScroll)
|
|
3195
|
+
return;
|
|
3196
|
+
const originalOverflow = document.body.style.overflow;
|
|
3197
|
+
document.body.style.overflow = 'hidden';
|
|
3145
3198
|
return () => {
|
|
3146
|
-
document.body.style.overflow =
|
|
3199
|
+
document.body.style.overflow = originalOverflow;
|
|
3147
3200
|
};
|
|
3148
|
-
}, [
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
onClose();
|
|
3152
|
-
}
|
|
3153
|
-
};
|
|
3154
|
-
const handleDragStart = (e) => {
|
|
3201
|
+
}, [open, preventScroll]);
|
|
3202
|
+
// Handle drag start
|
|
3203
|
+
const handleDragStart = useCallback((clientY) => {
|
|
3155
3204
|
setIsDragging(true);
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
};
|
|
3159
|
-
|
|
3205
|
+
startY.current = clientY;
|
|
3206
|
+
startOffset.current = dragOffset;
|
|
3207
|
+
}, [dragOffset]);
|
|
3208
|
+
// Handle drag move
|
|
3209
|
+
const handleDragMove = useCallback((clientY) => {
|
|
3210
|
+
if (!isDragging)
|
|
3211
|
+
return;
|
|
3212
|
+
const delta = clientY - startY.current;
|
|
3213
|
+
const newOffset = Math.max(0, startOffset.current + delta);
|
|
3214
|
+
setDragOffset(newOffset);
|
|
3215
|
+
}, [isDragging]);
|
|
3216
|
+
// Handle drag end
|
|
3217
|
+
const handleDragEnd = useCallback(() => {
|
|
3160
3218
|
if (!isDragging)
|
|
3161
3219
|
return;
|
|
3162
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
3163
|
-
const offset = clientY - startYRef.current;
|
|
3164
|
-
// Only allow dragging down
|
|
3165
|
-
if (offset > 0) {
|
|
3166
|
-
setDragOffset(offset);
|
|
3167
|
-
}
|
|
3168
|
-
};
|
|
3169
|
-
const handleDragEnd = () => {
|
|
3170
3220
|
setIsDragging(false);
|
|
3171
|
-
|
|
3172
|
-
if (dragOffset >
|
|
3173
|
-
|
|
3221
|
+
const threshold = 100; // pixels to trigger close
|
|
3222
|
+
if (dragOffset > threshold) {
|
|
3223
|
+
// If we have snap points, snap to next lower point or close
|
|
3224
|
+
if (snapPoints && currentSnapIndex > 0) {
|
|
3225
|
+
setCurrentSnapIndex(currentSnapIndex - 1);
|
|
3226
|
+
setDragOffset(0);
|
|
3227
|
+
}
|
|
3228
|
+
else {
|
|
3229
|
+
onClose();
|
|
3230
|
+
setDragOffset(0);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
else {
|
|
3234
|
+
// Snap back
|
|
3235
|
+
setDragOffset(0);
|
|
3174
3236
|
}
|
|
3175
|
-
|
|
3237
|
+
}, [isDragging, dragOffset, snapPoints, currentSnapIndex, onClose]);
|
|
3238
|
+
// Touch event handlers
|
|
3239
|
+
const handleTouchStart = (e) => {
|
|
3240
|
+
handleDragStart(e.touches[0].clientY);
|
|
3241
|
+
};
|
|
3242
|
+
const handleTouchMove = (e) => {
|
|
3243
|
+
handleDragMove(e.touches[0].clientY);
|
|
3244
|
+
};
|
|
3245
|
+
const handleTouchEnd = () => {
|
|
3246
|
+
handleDragEnd();
|
|
3247
|
+
};
|
|
3248
|
+
// Mouse event handlers (for desktop testing)
|
|
3249
|
+
const handleMouseDown = (e) => {
|
|
3250
|
+
handleDragStart(e.clientY);
|
|
3176
3251
|
};
|
|
3177
3252
|
useEffect(() => {
|
|
3178
3253
|
if (!isDragging)
|
|
3179
3254
|
return;
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3255
|
+
const handleMouseMove = (e) => {
|
|
3256
|
+
handleDragMove(e.clientY);
|
|
3257
|
+
};
|
|
3258
|
+
const handleMouseUp = () => {
|
|
3259
|
+
handleDragEnd();
|
|
3260
|
+
};
|
|
3261
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
3262
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
3186
3263
|
return () => {
|
|
3187
|
-
document.removeEventListener('
|
|
3188
|
-
document.removeEventListener('
|
|
3189
|
-
document.removeEventListener('touchend', handleEnd);
|
|
3190
|
-
document.removeEventListener('mouseup', handleEnd);
|
|
3264
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
3265
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
3191
3266
|
};
|
|
3192
|
-
}, [isDragging,
|
|
3193
|
-
|
|
3267
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
3268
|
+
// Calculate height based on snap points or presets
|
|
3269
|
+
const getSheetHeight = () => {
|
|
3270
|
+
if (snapPoints && snapPoints[currentSnapIndex]) {
|
|
3271
|
+
return snapPoints[currentSnapIndex];
|
|
3272
|
+
}
|
|
3273
|
+
if (typeof height === 'number') {
|
|
3274
|
+
return `${height}px`;
|
|
3275
|
+
}
|
|
3276
|
+
// Check for preset heights
|
|
3277
|
+
if (typeof height === 'string' && heightPresets[height]) {
|
|
3278
|
+
return heightPresets[height];
|
|
3279
|
+
}
|
|
3280
|
+
return height;
|
|
3281
|
+
};
|
|
3282
|
+
if (!isSheetOpen)
|
|
3194
3283
|
return null;
|
|
3195
|
-
|
|
3284
|
+
const sheetContent = (jsxs("div", { className: "fixed inset-0 z-50", children: [jsx("div", { className: `
|
|
3196
3285
|
absolute inset-0 bg-black/50 transition-opacity duration-300
|
|
3197
|
-
${
|
|
3198
|
-
|
|
3199
|
-
|
|
3286
|
+
${isSheetOpen ? 'opacity-100' : 'opacity-0'}
|
|
3287
|
+
`, onClick: closeOnOverlayClick ? onClose : undefined, "aria-hidden": "true" }), jsxs("div", { ref: sheetRef, className: `
|
|
3288
|
+
absolute bottom-0 left-0 right-0
|
|
3289
|
+
bg-white rounded-t-2xl shadow-2xl
|
|
3200
3290
|
transition-transform duration-300 ease-out
|
|
3201
|
-
${
|
|
3291
|
+
${isDragging ? 'transition-none' : ''}
|
|
3202
3292
|
${className}
|
|
3203
3293
|
`, style: {
|
|
3204
|
-
height:
|
|
3294
|
+
height: getSheetHeight(),
|
|
3295
|
+
maxHeight,
|
|
3205
3296
|
transform: `translateY(${dragOffset}px)`,
|
|
3206
|
-
}, role: "dialog", "aria-modal": "true",
|
|
3297
|
+
}, role: "dialog", "aria-modal": "true", children: [showHandle && (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: jsx("div", { className: "w-10 h-1 bg-paper-300 rounded-full" }) })), title && (jsxs("div", { className: "flex items-center justify-between px-4 py-3 border-b border-paper-200", children: [jsx("h2", { className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (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: jsx(X, { className: "h-5 w-5" }) }))] })), jsx("div", { className: "flex flex-col h-full overflow-hidden", children: children })] })] }));
|
|
3298
|
+
return createPortal(sheetContent, document.body);
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* BottomSheetHeader - Header section with title and optional close button
|
|
3302
|
+
*/
|
|
3303
|
+
function BottomSheetHeader({ children, className = '' }) {
|
|
3304
|
+
return (jsx("div", { className: `flex items-center justify-between px-4 py-3 border-b border-paper-200 ${className}`, children: children }));
|
|
3305
|
+
}
|
|
3306
|
+
/**
|
|
3307
|
+
* BottomSheetContent - Scrollable content area
|
|
3308
|
+
*/
|
|
3309
|
+
function BottomSheetContent({ children, className = '' }) {
|
|
3310
|
+
return (jsx("div", { className: `flex-1 overflow-y-auto px-4 py-4 ${className}`, children: children }));
|
|
3311
|
+
}
|
|
3312
|
+
/**
|
|
3313
|
+
* BottomSheetActions - Sticky footer for action buttons (thumb zone)
|
|
3314
|
+
*/
|
|
3315
|
+
function BottomSheetActions({ children, className = '' }) {
|
|
3316
|
+
return (jsx("div", { className: `flex flex-col gap-2 px-4 py-4 border-t border-paper-200 bg-white ${className}`, children: children }));
|
|
3207
3317
|
}
|
|
3208
3318
|
|
|
3209
3319
|
const sizeClasses$8 = {
|
|
@@ -7338,6 +7448,598 @@ function Hide({ children, above, below, only, className = '' }) {
|
|
|
7338
7448
|
return (jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
|
|
7339
7449
|
}
|
|
7340
7450
|
|
|
7451
|
+
/**
|
|
7452
|
+
* HorizontalScroll - Horizontally scrollable container with peek indicators
|
|
7453
|
+
*
|
|
7454
|
+
* Designed for mobile carousels of cards with:
|
|
7455
|
+
* - Touch-friendly momentum scrolling
|
|
7456
|
+
* - Peek hint showing more items exist
|
|
7457
|
+
* - Optional snap scrolling
|
|
7458
|
+
* - Navigation arrows for desktop
|
|
7459
|
+
*
|
|
7460
|
+
* @example
|
|
7461
|
+
* ```tsx
|
|
7462
|
+
* <HorizontalScroll gap="md" peekAmount={24} showIndicators>
|
|
7463
|
+
* <Card>Bill 1</Card>
|
|
7464
|
+
* <Card>Bill 2</Card>
|
|
7465
|
+
* <Card>Bill 3</Card>
|
|
7466
|
+
* </HorizontalScroll>
|
|
7467
|
+
* ```
|
|
7468
|
+
*/
|
|
7469
|
+
function HorizontalScroll({ children, gap = 'md', peekAmount = 24, showIndicators = false, snapToItem = true, showArrows = 'hover', scrollBehavior = 'smooth', className = '', scrollClassName = '', }) {
|
|
7470
|
+
const scrollRef = useRef(null);
|
|
7471
|
+
const [canScrollLeft, setCanScrollLeft] = useState(false);
|
|
7472
|
+
const [canScrollRight, setCanScrollRight] = useState(false);
|
|
7473
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
7474
|
+
const [itemCount, setItemCount] = useState(0);
|
|
7475
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
7476
|
+
// Gap classes
|
|
7477
|
+
const gapClasses = {
|
|
7478
|
+
none: 'gap-0',
|
|
7479
|
+
sm: 'gap-2',
|
|
7480
|
+
md: 'gap-4',
|
|
7481
|
+
lg: 'gap-6',
|
|
7482
|
+
};
|
|
7483
|
+
const gapStyle = typeof gap === 'number' ? { gap: `${gap}px` } : {};
|
|
7484
|
+
const gapClass = typeof gap === 'string' ? gapClasses[gap] : '';
|
|
7485
|
+
// Check scroll position and update state
|
|
7486
|
+
const checkScrollPosition = useCallback(() => {
|
|
7487
|
+
const container = scrollRef.current;
|
|
7488
|
+
if (!container)
|
|
7489
|
+
return;
|
|
7490
|
+
const { scrollLeft, scrollWidth, clientWidth } = container;
|
|
7491
|
+
setCanScrollLeft(scrollLeft > 0);
|
|
7492
|
+
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1);
|
|
7493
|
+
// Calculate active index based on scroll position
|
|
7494
|
+
if (showIndicators && container.children.length > 0) {
|
|
7495
|
+
const children = Array.from(container.children);
|
|
7496
|
+
const containerRect = container.getBoundingClientRect();
|
|
7497
|
+
const containerCenter = containerRect.left + containerRect.width / 2;
|
|
7498
|
+
let closestIndex = 0;
|
|
7499
|
+
let closestDistance = Infinity;
|
|
7500
|
+
children.forEach((child, index) => {
|
|
7501
|
+
const childRect = child.getBoundingClientRect();
|
|
7502
|
+
const childCenter = childRect.left + childRect.width / 2;
|
|
7503
|
+
const distance = Math.abs(childCenter - containerCenter);
|
|
7504
|
+
if (distance < closestDistance) {
|
|
7505
|
+
closestDistance = distance;
|
|
7506
|
+
closestIndex = index;
|
|
7507
|
+
}
|
|
7508
|
+
});
|
|
7509
|
+
setActiveIndex(closestIndex);
|
|
7510
|
+
}
|
|
7511
|
+
}, [showIndicators]);
|
|
7512
|
+
// Initialize and handle resize
|
|
7513
|
+
useEffect(() => {
|
|
7514
|
+
const container = scrollRef.current;
|
|
7515
|
+
if (!container)
|
|
7516
|
+
return;
|
|
7517
|
+
setItemCount(React__default.Children.count(children));
|
|
7518
|
+
checkScrollPosition();
|
|
7519
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
7520
|
+
checkScrollPosition();
|
|
7521
|
+
});
|
|
7522
|
+
resizeObserver.observe(container);
|
|
7523
|
+
return () => {
|
|
7524
|
+
resizeObserver.disconnect();
|
|
7525
|
+
};
|
|
7526
|
+
}, [children, checkScrollPosition]);
|
|
7527
|
+
// Handle scroll event
|
|
7528
|
+
useEffect(() => {
|
|
7529
|
+
const container = scrollRef.current;
|
|
7530
|
+
if (!container)
|
|
7531
|
+
return;
|
|
7532
|
+
const handleScroll = () => {
|
|
7533
|
+
checkScrollPosition();
|
|
7534
|
+
};
|
|
7535
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
7536
|
+
return () => {
|
|
7537
|
+
container.removeEventListener('scroll', handleScroll);
|
|
7538
|
+
};
|
|
7539
|
+
}, [checkScrollPosition]);
|
|
7540
|
+
// Scroll by one item
|
|
7541
|
+
const scrollByItem = (direction) => {
|
|
7542
|
+
const container = scrollRef.current;
|
|
7543
|
+
if (!container)
|
|
7544
|
+
return;
|
|
7545
|
+
const children = Array.from(container.children);
|
|
7546
|
+
if (children.length === 0)
|
|
7547
|
+
return;
|
|
7548
|
+
const firstChild = children[0];
|
|
7549
|
+
const itemWidth = firstChild.offsetWidth;
|
|
7550
|
+
const gapValue = typeof gap === 'number' ? gap :
|
|
7551
|
+
gap === 'sm' ? 8 : gap === 'md' ? 16 : gap === 'lg' ? 24 : 0;
|
|
7552
|
+
const scrollAmount = itemWidth + gapValue;
|
|
7553
|
+
container.scrollBy({
|
|
7554
|
+
left: direction === 'left' ? -scrollAmount : scrollAmount,
|
|
7555
|
+
behavior: scrollBehavior,
|
|
7556
|
+
});
|
|
7557
|
+
};
|
|
7558
|
+
// Scroll to specific index
|
|
7559
|
+
const scrollToIndex = (index) => {
|
|
7560
|
+
const container = scrollRef.current;
|
|
7561
|
+
if (!container)
|
|
7562
|
+
return;
|
|
7563
|
+
const children = Array.from(container.children);
|
|
7564
|
+
if (index < 0 || index >= children.length)
|
|
7565
|
+
return;
|
|
7566
|
+
const child = children[index];
|
|
7567
|
+
child.scrollIntoView({
|
|
7568
|
+
behavior: scrollBehavior,
|
|
7569
|
+
block: 'nearest',
|
|
7570
|
+
inline: 'center',
|
|
7571
|
+
});
|
|
7572
|
+
};
|
|
7573
|
+
const showLeftArrow = showArrows === 'always' || (showArrows === 'hover' && isHovered);
|
|
7574
|
+
const showRightArrow = showArrows === 'always' || (showArrows === 'hover' && isHovered);
|
|
7575
|
+
return (jsxs("div", { className: `relative ${className}`, onMouseEnter: () => setIsHovered(true), onMouseLeave: () => setIsHovered(false), children: [showLeftArrow && canScrollLeft && (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: jsx(ChevronLeft, { className: "h-5 w-5" }) })), jsx("div", { ref: scrollRef, className: `
|
|
7576
|
+
flex overflow-x-auto scrollbar-hide
|
|
7577
|
+
${gapClass}
|
|
7578
|
+
${snapToItem ? 'snap-x snap-mandatory' : ''}
|
|
7579
|
+
${scrollClassName}
|
|
7580
|
+
`, style: {
|
|
7581
|
+
...gapStyle,
|
|
7582
|
+
paddingRight: peekAmount > 0 ? `${peekAmount}px` : undefined,
|
|
7583
|
+
scrollPaddingLeft: '0px',
|
|
7584
|
+
scrollPaddingRight: `${peekAmount}px`,
|
|
7585
|
+
}, children: React__default.Children.map(children, (child, index) => (jsx("div", { className: `flex-shrink-0 ${snapToItem ? 'snap-start' : ''}`, children: child }, index))) }), showRightArrow && canScrollRight && (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: jsx(ChevronRight, { className: "h-5 w-5" }) })), showIndicators && itemCount > 1 && (jsx("div", { className: "flex justify-center gap-1.5 mt-3", children: Array.from({ length: itemCount }).map((_, index) => (jsx("button", { onClick: () => scrollToIndex(index), className: `
|
|
7586
|
+
w-2 h-2 rounded-full transition-all duration-200
|
|
7587
|
+
${index === activeIndex
|
|
7588
|
+
? 'bg-accent-500 w-4'
|
|
7589
|
+
: 'bg-paper-300 hover:bg-paper-400'}
|
|
7590
|
+
`, "aria-label": `Go to item ${index + 1}`, "aria-current": index === activeIndex ? 'true' : 'false' }, index))) }))] }));
|
|
7591
|
+
}
|
|
7592
|
+
|
|
7593
|
+
/**
|
|
7594
|
+
* SwipeableCard - Card component with swipe-to-action functionality
|
|
7595
|
+
*
|
|
7596
|
+
* Designed for mobile approval workflows:
|
|
7597
|
+
* - Swipe right to approve/confirm
|
|
7598
|
+
* - Swipe left to see options/alternatives
|
|
7599
|
+
* - Visual feedback showing action being revealed
|
|
7600
|
+
* - Haptic feedback on mobile devices
|
|
7601
|
+
*
|
|
7602
|
+
* @example
|
|
7603
|
+
* ```tsx
|
|
7604
|
+
* <SwipeableCard
|
|
7605
|
+
* onSwipeRight={() => handleApprove()}
|
|
7606
|
+
* onSwipeLeft={() => handleShowOptions()}
|
|
7607
|
+
* rightAction={{
|
|
7608
|
+
* icon: <Check />,
|
|
7609
|
+
* color: 'success',
|
|
7610
|
+
* label: 'Approve'
|
|
7611
|
+
* }}
|
|
7612
|
+
* leftAction={{
|
|
7613
|
+
* icon: <MoreHorizontal />,
|
|
7614
|
+
* color: 'neutral',
|
|
7615
|
+
* label: 'Options'
|
|
7616
|
+
* }}
|
|
7617
|
+
* >
|
|
7618
|
+
* <TransactionContent />
|
|
7619
|
+
* </SwipeableCard>
|
|
7620
|
+
* ```
|
|
7621
|
+
*/
|
|
7622
|
+
function SwipeableCard({ children, onSwipeRight, onSwipeLeft, rightAction = {
|
|
7623
|
+
icon: jsx(Check, { className: "h-6 w-6" }),
|
|
7624
|
+
color: 'success',
|
|
7625
|
+
label: 'Approve',
|
|
7626
|
+
}, leftAction = {
|
|
7627
|
+
icon: jsx(MoreHorizontal, { className: "h-6 w-6" }),
|
|
7628
|
+
color: 'neutral',
|
|
7629
|
+
label: 'Options',
|
|
7630
|
+
}, swipeThreshold = 100, hapticFeedback = true, disabled = false, onSwipeStart, onSwipeEnd, className = '', }) {
|
|
7631
|
+
const cardRef = useRef(null);
|
|
7632
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
7633
|
+
const [offsetX, setOffsetX] = useState(0);
|
|
7634
|
+
const [isTriggered, setIsTriggered] = useState(null);
|
|
7635
|
+
const startX = useRef(0);
|
|
7636
|
+
const startY = useRef(0);
|
|
7637
|
+
const isHorizontalSwipe = useRef(null);
|
|
7638
|
+
// Color classes for action backgrounds
|
|
7639
|
+
const colorClasses = {
|
|
7640
|
+
success: 'bg-success-500',
|
|
7641
|
+
error: 'bg-error-500',
|
|
7642
|
+
warning: 'bg-warning-500',
|
|
7643
|
+
neutral: 'bg-paper-400',
|
|
7644
|
+
primary: 'bg-accent-500',
|
|
7645
|
+
};
|
|
7646
|
+
// Trigger haptic feedback
|
|
7647
|
+
const triggerHaptic = useCallback((style = 'medium') => {
|
|
7648
|
+
if (!hapticFeedback)
|
|
7649
|
+
return;
|
|
7650
|
+
// Use Vibration API if available
|
|
7651
|
+
if ('vibrate' in navigator) {
|
|
7652
|
+
const patterns = {
|
|
7653
|
+
light: 10,
|
|
7654
|
+
medium: 25,
|
|
7655
|
+
heavy: [50, 30, 50],
|
|
7656
|
+
};
|
|
7657
|
+
navigator.vibrate(patterns[style]);
|
|
7658
|
+
}
|
|
7659
|
+
}, [hapticFeedback]);
|
|
7660
|
+
// Handle drag start
|
|
7661
|
+
const handleDragStart = useCallback((clientX, clientY) => {
|
|
7662
|
+
if (disabled)
|
|
7663
|
+
return;
|
|
7664
|
+
setIsDragging(true);
|
|
7665
|
+
startX.current = clientX;
|
|
7666
|
+
startY.current = clientY;
|
|
7667
|
+
isHorizontalSwipe.current = null;
|
|
7668
|
+
onSwipeStart?.();
|
|
7669
|
+
}, [disabled, onSwipeStart]);
|
|
7670
|
+
// Handle drag move
|
|
7671
|
+
const handleDragMove = useCallback((clientX, clientY) => {
|
|
7672
|
+
if (!isDragging || disabled)
|
|
7673
|
+
return;
|
|
7674
|
+
const deltaX = clientX - startX.current;
|
|
7675
|
+
const deltaY = clientY - startY.current;
|
|
7676
|
+
// Determine if this is a horizontal swipe on first significant movement
|
|
7677
|
+
if (isHorizontalSwipe.current === null) {
|
|
7678
|
+
const absDeltaX = Math.abs(deltaX);
|
|
7679
|
+
const absDeltaY = Math.abs(deltaY);
|
|
7680
|
+
if (absDeltaX > 10 || absDeltaY > 10) {
|
|
7681
|
+
isHorizontalSwipe.current = absDeltaX > absDeltaY;
|
|
7682
|
+
}
|
|
7683
|
+
}
|
|
7684
|
+
// Only process horizontal swipes
|
|
7685
|
+
if (isHorizontalSwipe.current !== true)
|
|
7686
|
+
return;
|
|
7687
|
+
// Check if we should allow this direction
|
|
7688
|
+
const canSwipeRight = onSwipeRight !== undefined;
|
|
7689
|
+
const canSwipeLeft = onSwipeLeft !== undefined;
|
|
7690
|
+
let newOffset = deltaX;
|
|
7691
|
+
// Limit swipe direction based on available actions
|
|
7692
|
+
if (!canSwipeRight && deltaX > 0)
|
|
7693
|
+
newOffset = 0;
|
|
7694
|
+
if (!canSwipeLeft && deltaX < 0)
|
|
7695
|
+
newOffset = 0;
|
|
7696
|
+
// Add resistance when exceeding threshold
|
|
7697
|
+
const maxSwipe = swipeThreshold * 1.5;
|
|
7698
|
+
if (Math.abs(newOffset) > swipeThreshold) {
|
|
7699
|
+
const overflow = Math.abs(newOffset) - swipeThreshold;
|
|
7700
|
+
const resistance = overflow * 0.3;
|
|
7701
|
+
newOffset = newOffset > 0
|
|
7702
|
+
? swipeThreshold + resistance
|
|
7703
|
+
: -(swipeThreshold + resistance);
|
|
7704
|
+
newOffset = Math.max(-maxSwipe, Math.min(maxSwipe, newOffset));
|
|
7705
|
+
}
|
|
7706
|
+
setOffsetX(newOffset);
|
|
7707
|
+
// Check for threshold crossing and trigger haptic
|
|
7708
|
+
const newTriggered = Math.abs(newOffset) >= swipeThreshold
|
|
7709
|
+
? (newOffset > 0 ? 'right' : 'left')
|
|
7710
|
+
: null;
|
|
7711
|
+
if (newTriggered !== isTriggered) {
|
|
7712
|
+
if (newTriggered) {
|
|
7713
|
+
triggerHaptic('medium');
|
|
7714
|
+
}
|
|
7715
|
+
setIsTriggered(newTriggered);
|
|
7716
|
+
}
|
|
7717
|
+
}, [isDragging, disabled, onSwipeRight, onSwipeLeft, swipeThreshold, isTriggered, triggerHaptic]);
|
|
7718
|
+
// Handle drag end
|
|
7719
|
+
const handleDragEnd = useCallback(() => {
|
|
7720
|
+
if (!isDragging)
|
|
7721
|
+
return;
|
|
7722
|
+
setIsDragging(false);
|
|
7723
|
+
onSwipeEnd?.();
|
|
7724
|
+
// Check if action should be triggered
|
|
7725
|
+
if (Math.abs(offsetX) >= swipeThreshold) {
|
|
7726
|
+
if (offsetX > 0 && onSwipeRight) {
|
|
7727
|
+
triggerHaptic('heavy');
|
|
7728
|
+
// Animate card away then call handler
|
|
7729
|
+
setOffsetX(window.innerWidth);
|
|
7730
|
+
setTimeout(() => {
|
|
7731
|
+
onSwipeRight();
|
|
7732
|
+
setOffsetX(0);
|
|
7733
|
+
setIsTriggered(null);
|
|
7734
|
+
}, 200);
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
else if (offsetX < 0 && onSwipeLeft) {
|
|
7738
|
+
triggerHaptic('heavy');
|
|
7739
|
+
setOffsetX(-window.innerWidth);
|
|
7740
|
+
setTimeout(() => {
|
|
7741
|
+
onSwipeLeft();
|
|
7742
|
+
setOffsetX(0);
|
|
7743
|
+
setIsTriggered(null);
|
|
7744
|
+
}, 200);
|
|
7745
|
+
return;
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
// Snap back
|
|
7749
|
+
setOffsetX(0);
|
|
7750
|
+
setIsTriggered(null);
|
|
7751
|
+
}, [isDragging, offsetX, swipeThreshold, onSwipeRight, onSwipeLeft, onSwipeEnd, triggerHaptic]);
|
|
7752
|
+
// Touch event handlers
|
|
7753
|
+
const handleTouchStart = (e) => {
|
|
7754
|
+
handleDragStart(e.touches[0].clientX, e.touches[0].clientY);
|
|
7755
|
+
};
|
|
7756
|
+
const handleTouchMove = (e) => {
|
|
7757
|
+
handleDragMove(e.touches[0].clientX, e.touches[0].clientY);
|
|
7758
|
+
// Prevent vertical scroll if horizontal swipe
|
|
7759
|
+
if (isHorizontalSwipe.current === true) {
|
|
7760
|
+
e.preventDefault();
|
|
7761
|
+
}
|
|
7762
|
+
};
|
|
7763
|
+
const handleTouchEnd = () => {
|
|
7764
|
+
handleDragEnd();
|
|
7765
|
+
};
|
|
7766
|
+
// Mouse event handlers (for desktop testing)
|
|
7767
|
+
const handleMouseDown = (e) => {
|
|
7768
|
+
handleDragStart(e.clientX, e.clientY);
|
|
7769
|
+
};
|
|
7770
|
+
useEffect(() => {
|
|
7771
|
+
if (!isDragging)
|
|
7772
|
+
return;
|
|
7773
|
+
const handleMouseMove = (e) => {
|
|
7774
|
+
handleDragMove(e.clientX, e.clientY);
|
|
7775
|
+
};
|
|
7776
|
+
const handleMouseUp = () => {
|
|
7777
|
+
handleDragEnd();
|
|
7778
|
+
};
|
|
7779
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
7780
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
7781
|
+
return () => {
|
|
7782
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
7783
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
7784
|
+
};
|
|
7785
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
7786
|
+
// Calculate action opacity based on swipe distance
|
|
7787
|
+
const rightActionOpacity = offsetX > 0 ? Math.min(1, offsetX / swipeThreshold) : 0;
|
|
7788
|
+
const leftActionOpacity = offsetX < 0 ? Math.min(1, Math.abs(offsetX) / swipeThreshold) : 0;
|
|
7789
|
+
return (jsxs("div", { className: `relative overflow-hidden rounded-lg ${className}`, children: [onSwipeRight && (jsx("div", { className: `
|
|
7790
|
+
absolute inset-y-0 left-0 flex items-center justify-start pl-6
|
|
7791
|
+
${colorClasses[rightAction.color]}
|
|
7792
|
+
transition-opacity duration-100
|
|
7793
|
+
`, style: {
|
|
7794
|
+
opacity: rightActionOpacity,
|
|
7795
|
+
width: Math.abs(offsetX) + 20,
|
|
7796
|
+
}, "aria-hidden": "true", children: jsx("div", { className: `
|
|
7797
|
+
text-white transform transition-transform duration-200
|
|
7798
|
+
${isTriggered === 'right' ? 'scale-125' : 'scale-100'}
|
|
7799
|
+
`, children: rightAction.icon }) })), onSwipeLeft && (jsx("div", { className: `
|
|
7800
|
+
absolute inset-y-0 right-0 flex items-center justify-end pr-6
|
|
7801
|
+
${colorClasses[leftAction.color]}
|
|
7802
|
+
transition-opacity duration-100
|
|
7803
|
+
`, style: {
|
|
7804
|
+
opacity: leftActionOpacity,
|
|
7805
|
+
width: Math.abs(offsetX) + 20,
|
|
7806
|
+
}, "aria-hidden": "true", children: jsx("div", { className: `
|
|
7807
|
+
text-white transform transition-transform duration-200
|
|
7808
|
+
${isTriggered === 'left' ? 'scale-125' : 'scale-100'}
|
|
7809
|
+
`, children: leftAction.icon }) })), jsx("div", { ref: cardRef, className: `
|
|
7810
|
+
relative bg-white
|
|
7811
|
+
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
7812
|
+
${disabled ? 'opacity-50 pointer-events-none' : ''}
|
|
7813
|
+
`, style: {
|
|
7814
|
+
transform: `translateX(${offsetX}px)`,
|
|
7815
|
+
}, 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 })] }));
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7818
|
+
/**
|
|
7819
|
+
* NotificationBanner - Dismissible banner for important alerts
|
|
7820
|
+
*
|
|
7821
|
+
* Displays at top of screen for alerts that need attention but aren't blocking:
|
|
7822
|
+
* - Money Found alerts
|
|
7823
|
+
* - System messages
|
|
7824
|
+
* - Promotional info
|
|
7825
|
+
*
|
|
7826
|
+
* @example
|
|
7827
|
+
* ```tsx
|
|
7828
|
+
* <NotificationBanner
|
|
7829
|
+
* variant="warning"
|
|
7830
|
+
* icon={<DollarSign />}
|
|
7831
|
+
* title="Found $33.98 in potential savings"
|
|
7832
|
+
* description="Tap to review"
|
|
7833
|
+
* action={{
|
|
7834
|
+
* label: "Review",
|
|
7835
|
+
* onClick: handleReview
|
|
7836
|
+
* }}
|
|
7837
|
+
* onDismiss={() => setShowBanner(false)}
|
|
7838
|
+
* />
|
|
7839
|
+
* ```
|
|
7840
|
+
*/
|
|
7841
|
+
function NotificationBanner({ variant = 'info', icon, title, description, action, onDismiss, dismissible = true, sticky = false, className = '', }) {
|
|
7842
|
+
const bannerRef = useRef(null);
|
|
7843
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
7844
|
+
const [offsetX, setOffsetX] = useState(0);
|
|
7845
|
+
const [isDismissed, setIsDismissed] = useState(false);
|
|
7846
|
+
const startX = useRef(0);
|
|
7847
|
+
// Default icons based on variant
|
|
7848
|
+
const defaultIcons = {
|
|
7849
|
+
info: jsx(Info, { className: "h-5 w-5" }),
|
|
7850
|
+
success: jsx(CheckCircle, { className: "h-5 w-5" }),
|
|
7851
|
+
warning: jsx(AlertTriangle, { className: "h-5 w-5" }),
|
|
7852
|
+
error: jsx(AlertCircle, { className: "h-5 w-5" }),
|
|
7853
|
+
};
|
|
7854
|
+
// Color classes
|
|
7855
|
+
const variantClasses = {
|
|
7856
|
+
info: 'bg-gradient-to-r from-primary-50 to-primary-100 border-primary-200 text-primary-900',
|
|
7857
|
+
success: 'bg-gradient-to-r from-success-50 to-success-100 border-success-200 text-success-900',
|
|
7858
|
+
warning: 'bg-gradient-to-r from-warning-50 to-warning-100 border-warning-200 text-warning-900',
|
|
7859
|
+
error: 'bg-gradient-to-r from-error-50 to-error-100 border-error-200 text-error-900',
|
|
7860
|
+
};
|
|
7861
|
+
const iconColorClasses = {
|
|
7862
|
+
info: 'text-primary-600',
|
|
7863
|
+
success: 'text-success-600',
|
|
7864
|
+
warning: 'text-warning-600',
|
|
7865
|
+
error: 'text-error-600',
|
|
7866
|
+
};
|
|
7867
|
+
const buttonClasses = {
|
|
7868
|
+
info: 'bg-primary-600 hover:bg-primary-700 text-white',
|
|
7869
|
+
success: 'bg-success-600 hover:bg-success-700 text-white',
|
|
7870
|
+
warning: 'bg-warning-600 hover:bg-warning-700 text-white',
|
|
7871
|
+
error: 'bg-error-600 hover:bg-error-700 text-white',
|
|
7872
|
+
};
|
|
7873
|
+
// Handle swipe dismiss
|
|
7874
|
+
const handleDragStart = useCallback((clientX) => {
|
|
7875
|
+
if (!dismissible)
|
|
7876
|
+
return;
|
|
7877
|
+
setIsDragging(true);
|
|
7878
|
+
startX.current = clientX;
|
|
7879
|
+
}, [dismissible]);
|
|
7880
|
+
const handleDragMove = useCallback((clientX) => {
|
|
7881
|
+
if (!isDragging)
|
|
7882
|
+
return;
|
|
7883
|
+
const delta = clientX - startX.current;
|
|
7884
|
+
setOffsetX(delta);
|
|
7885
|
+
}, [isDragging]);
|
|
7886
|
+
const handleDragEnd = useCallback(() => {
|
|
7887
|
+
if (!isDragging)
|
|
7888
|
+
return;
|
|
7889
|
+
setIsDragging(false);
|
|
7890
|
+
const threshold = 100;
|
|
7891
|
+
if (Math.abs(offsetX) > threshold) {
|
|
7892
|
+
// Animate out
|
|
7893
|
+
setOffsetX(offsetX > 0 ? window.innerWidth : -window.innerWidth);
|
|
7894
|
+
setIsDismissed(true);
|
|
7895
|
+
setTimeout(() => {
|
|
7896
|
+
onDismiss?.();
|
|
7897
|
+
}, 200);
|
|
7898
|
+
}
|
|
7899
|
+
else {
|
|
7900
|
+
// Snap back
|
|
7901
|
+
setOffsetX(0);
|
|
7902
|
+
}
|
|
7903
|
+
}, [isDragging, offsetX, onDismiss]);
|
|
7904
|
+
// Touch handlers
|
|
7905
|
+
const handleTouchStart = (e) => {
|
|
7906
|
+
handleDragStart(e.touches[0].clientX);
|
|
7907
|
+
};
|
|
7908
|
+
const handleTouchMove = (e) => {
|
|
7909
|
+
handleDragMove(e.touches[0].clientX);
|
|
7910
|
+
};
|
|
7911
|
+
const handleTouchEnd = () => {
|
|
7912
|
+
handleDragEnd();
|
|
7913
|
+
};
|
|
7914
|
+
// Mouse handlers for desktop testing
|
|
7915
|
+
const handleMouseDown = (e) => {
|
|
7916
|
+
if (dismissible) {
|
|
7917
|
+
handleDragStart(e.clientX);
|
|
7918
|
+
}
|
|
7919
|
+
};
|
|
7920
|
+
useEffect(() => {
|
|
7921
|
+
if (!isDragging)
|
|
7922
|
+
return;
|
|
7923
|
+
const handleMouseMove = (e) => {
|
|
7924
|
+
handleDragMove(e.clientX);
|
|
7925
|
+
};
|
|
7926
|
+
const handleMouseUp = () => {
|
|
7927
|
+
handleDragEnd();
|
|
7928
|
+
};
|
|
7929
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
7930
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
7931
|
+
return () => {
|
|
7932
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
|
7933
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
|
7934
|
+
};
|
|
7935
|
+
}, [isDragging, handleDragMove, handleDragEnd]);
|
|
7936
|
+
if (isDismissed)
|
|
7937
|
+
return null;
|
|
7938
|
+
return (jsx("div", { ref: bannerRef, className: `
|
|
7939
|
+
w-full border-b
|
|
7940
|
+
${variantClasses[variant]}
|
|
7941
|
+
${sticky ? 'sticky top-0 z-40' : ''}
|
|
7942
|
+
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
7943
|
+
${className}
|
|
7944
|
+
`, style: {
|
|
7945
|
+
transform: `translateX(${offsetX}px)`,
|
|
7946
|
+
opacity: Math.max(0, 1 - Math.abs(offsetX) / 200),
|
|
7947
|
+
}, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, role: "alert", children: jsxs("div", { className: "flex items-center gap-3 px-4 py-3", children: [jsx("div", { className: `flex-shrink-0 ${iconColorClasses[variant]}`, children: icon || defaultIcons[variant] }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("p", { className: "text-sm font-medium truncate", children: title }), description && (jsx("p", { className: "text-xs opacity-80 truncate", children: description }))] }), action && (jsx("button", { onClick: action.onClick, className: `
|
|
7948
|
+
flex-shrink-0 px-3 py-1.5 text-xs font-medium rounded-md
|
|
7949
|
+
transition-colors duration-200
|
|
7950
|
+
${buttonClasses[variant]}
|
|
7951
|
+
`, children: action.label })), onDismiss && (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: jsx(X, { className: "h-4 w-4" }) }))] }) }));
|
|
7952
|
+
}
|
|
7953
|
+
|
|
7954
|
+
/**
|
|
7955
|
+
* CompactStat - Single stat display optimized for mobile
|
|
7956
|
+
*
|
|
7957
|
+
* Designed for dashboard stats in 2-column mobile layouts:
|
|
7958
|
+
* - Compact presentation with value, label, and optional trend
|
|
7959
|
+
* - Responsive sizing
|
|
7960
|
+
* - Trend indicators with color coding
|
|
7961
|
+
*
|
|
7962
|
+
* @example
|
|
7963
|
+
* ```tsx
|
|
7964
|
+
* <Grid columns={2} gap="sm">
|
|
7965
|
+
* <CompactStat
|
|
7966
|
+
* value="$62,329"
|
|
7967
|
+
* label="Net Worth"
|
|
7968
|
+
* trend={{
|
|
7969
|
+
* direction: 'up',
|
|
7970
|
+
* value: '+$1,247',
|
|
7971
|
+
* color: 'success'
|
|
7972
|
+
* }}
|
|
7973
|
+
* />
|
|
7974
|
+
* <CompactStat
|
|
7975
|
+
* value="$4,521"
|
|
7976
|
+
* label="Monthly Income"
|
|
7977
|
+
* />
|
|
7978
|
+
* </Grid>
|
|
7979
|
+
* ```
|
|
7980
|
+
*/
|
|
7981
|
+
function CompactStat({ value, label, trend, size = 'md', align = 'left', className = '', }) {
|
|
7982
|
+
// Size classes
|
|
7983
|
+
const sizeClasses = {
|
|
7984
|
+
sm: {
|
|
7985
|
+
value: 'text-lg font-semibold',
|
|
7986
|
+
label: 'text-xs',
|
|
7987
|
+
trend: 'text-xs',
|
|
7988
|
+
icon: 'h-3 w-3',
|
|
7989
|
+
},
|
|
7990
|
+
md: {
|
|
7991
|
+
value: 'text-xl font-semibold',
|
|
7992
|
+
label: 'text-sm',
|
|
7993
|
+
trend: 'text-xs',
|
|
7994
|
+
icon: 'h-3.5 w-3.5',
|
|
7995
|
+
},
|
|
7996
|
+
lg: {
|
|
7997
|
+
value: 'text-2xl font-bold',
|
|
7998
|
+
label: 'text-sm',
|
|
7999
|
+
trend: 'text-sm',
|
|
8000
|
+
icon: 'h-4 w-4',
|
|
8001
|
+
},
|
|
8002
|
+
};
|
|
8003
|
+
// Alignment classes
|
|
8004
|
+
const alignClasses = {
|
|
8005
|
+
left: 'text-left',
|
|
8006
|
+
center: 'text-center',
|
|
8007
|
+
right: 'text-right',
|
|
8008
|
+
};
|
|
8009
|
+
// Trend color classes
|
|
8010
|
+
const getTrendColor = (trend) => {
|
|
8011
|
+
if (trend.color) {
|
|
8012
|
+
const colorMap = {
|
|
8013
|
+
success: 'text-success-600',
|
|
8014
|
+
error: 'text-error-600',
|
|
8015
|
+
warning: 'text-warning-600',
|
|
8016
|
+
neutral: 'text-ink-500',
|
|
8017
|
+
};
|
|
8018
|
+
return colorMap[trend.color];
|
|
8019
|
+
}
|
|
8020
|
+
// Default colors based on direction
|
|
8021
|
+
const directionColors = {
|
|
8022
|
+
up: 'text-success-600',
|
|
8023
|
+
down: 'text-error-600',
|
|
8024
|
+
neutral: 'text-ink-500',
|
|
8025
|
+
};
|
|
8026
|
+
return directionColors[trend.direction];
|
|
8027
|
+
};
|
|
8028
|
+
// Trend icons
|
|
8029
|
+
const TrendIcon = trend ? {
|
|
8030
|
+
up: TrendingUp,
|
|
8031
|
+
down: TrendingDown,
|
|
8032
|
+
neutral: Minus,
|
|
8033
|
+
}[trend.direction] : null;
|
|
8034
|
+
const sizes = sizeClasses[size];
|
|
8035
|
+
return (jsxs("div", { className: `${alignClasses[align]} ${className}`, children: [jsx("div", { className: `${sizes.value} text-ink-900 tracking-tight`, children: value }), jsx("div", { className: `${sizes.label} text-ink-500 mt-0.5`, children: label }), trend && (jsxs("div", { className: `
|
|
8036
|
+
flex items-center gap-1 mt-1
|
|
8037
|
+
${align === 'center' ? 'justify-center' : ''}
|
|
8038
|
+
${align === 'right' ? 'justify-end' : ''}
|
|
8039
|
+
${sizes.trend} ${getTrendColor(trend)}
|
|
8040
|
+
`, children: [TrendIcon && jsx(TrendIcon, { className: sizes.icon }), jsx("span", { children: trend.value })] }))] }));
|
|
8041
|
+
}
|
|
8042
|
+
|
|
7341
8043
|
/**
|
|
7342
8044
|
* Hook to detect breadcrumb navigation and trigger callbacks.
|
|
7343
8045
|
* Use this in host components to reset state when a breadcrumb is clicked.
|
|
@@ -7621,7 +8323,7 @@ function StepIndicator({ steps, currentStep, variant = 'horizontal', onStepClick
|
|
|
7621
8323
|
}) }) }));
|
|
7622
8324
|
}
|
|
7623
8325
|
|
|
7624
|
-
function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, className = '', dot = false, }) {
|
|
8326
|
+
function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, className = '', dot = false, pill = false, }) {
|
|
7625
8327
|
const variantStyles = {
|
|
7626
8328
|
success: 'bg-success-50 text-success-700 border-success-200',
|
|
7627
8329
|
warning: 'bg-warning-50 text-warning-700 border-warning-200',
|
|
@@ -7641,6 +8343,12 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
|
|
|
7641
8343
|
md: 'px-3 py-1 text-xs gap-1.5',
|
|
7642
8344
|
lg: 'px-3 py-1.5 text-sm gap-2',
|
|
7643
8345
|
};
|
|
8346
|
+
// Pill variant has tighter horizontal padding and fully rounded ends
|
|
8347
|
+
const pillSizeStyles = {
|
|
8348
|
+
sm: 'px-1.5 py-0.5 text-xs gap-1',
|
|
8349
|
+
md: 'px-2 py-0.5 text-xs gap-1',
|
|
8350
|
+
lg: 'px-2.5 py-1 text-sm gap-1.5',
|
|
8351
|
+
};
|
|
7644
8352
|
const dotSizeStyles = {
|
|
7645
8353
|
sm: 'h-1.5 w-1.5',
|
|
7646
8354
|
md: 'h-2 w-2',
|
|
@@ -7662,9 +8370,10 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
|
|
|
7662
8370
|
}
|
|
7663
8371
|
// Regular badge
|
|
7664
8372
|
return (jsxs("span", { className: `
|
|
7665
|
-
inline-flex items-center
|
|
8373
|
+
inline-flex items-center border font-medium
|
|
8374
|
+
${pill ? 'rounded-full' : 'rounded-full'}
|
|
7666
8375
|
${variantStyles[variant]}
|
|
7667
|
-
${sizeStyles[size]}
|
|
8376
|
+
${pill ? pillSizeStyles[size] : sizeStyles[size]}
|
|
7668
8377
|
${className}
|
|
7669
8378
|
`, children: [icon && jsx("span", { className: iconSize[size], children: icon }), jsx("span", { children: children }), onRemove && (jsx("button", { onClick: onRemove, className: "ml-1 hover:opacity-70 transition-opacity", "aria-label": "Remove badge", children: jsx(X, { className: iconSize[size] }) }))] }));
|
|
7670
8379
|
}
|
|
@@ -7772,8 +8481,10 @@ function Progress({ value, variant = 'linear', size = 'md', color = 'primary', s
|
|
|
7772
8481
|
warning: 'bg-warning-100',
|
|
7773
8482
|
error: 'bg-error-100',
|
|
7774
8483
|
};
|
|
8484
|
+
// Normalize 'ring' to 'circular'
|
|
8485
|
+
const normalizedVariant = variant === 'ring' ? 'circular' : variant;
|
|
7775
8486
|
// Linear progress
|
|
7776
|
-
if (
|
|
8487
|
+
if (normalizedVariant === 'linear') {
|
|
7777
8488
|
const heightClasses = {
|
|
7778
8489
|
sm: 'h-1',
|
|
7779
8490
|
md: 'h-2',
|
|
@@ -8705,94 +9416,112 @@ function useFABScroll(threshold = 10) {
|
|
|
8705
9416
|
}
|
|
8706
9417
|
|
|
8707
9418
|
/**
|
|
8708
|
-
* PullToRefresh -
|
|
9419
|
+
* PullToRefresh - Pull-down refresh indicator and handler for mobile lists
|
|
8709
9420
|
*
|
|
8710
|
-
* Wraps content
|
|
8711
|
-
*
|
|
8712
|
-
*
|
|
8713
|
-
*
|
|
8714
|
-
* ```tsx
|
|
8715
|
-
* <PullToRefresh onRefresh={async () => {
|
|
8716
|
-
* await fetchLatestData();
|
|
8717
|
-
* }}>
|
|
8718
|
-
* <div className="min-h-screen">
|
|
8719
|
-
* {content}
|
|
8720
|
-
* </div>
|
|
8721
|
-
* </PullToRefresh>
|
|
8722
|
-
* ```
|
|
9421
|
+
* Wraps content to enable pull-to-refresh behavior on mobile:
|
|
9422
|
+
* - Pull down to trigger refresh
|
|
9423
|
+
* - Visual feedback showing progress
|
|
9424
|
+
* - Custom content for each state
|
|
8723
9425
|
*
|
|
8724
|
-
* @example
|
|
9426
|
+
* @example
|
|
8725
9427
|
* ```tsx
|
|
8726
|
-
* <PullToRefresh
|
|
8727
|
-
*
|
|
8728
|
-
* pullThreshold={100}
|
|
8729
|
-
* maxPull={150}
|
|
8730
|
-
* >
|
|
8731
|
-
* {content}
|
|
9428
|
+
* <PullToRefresh onRefresh={async () => { await syncData(); }}>
|
|
9429
|
+
* <TransactionList transactions={transactions} />
|
|
8732
9430
|
* </PullToRefresh>
|
|
8733
9431
|
* ```
|
|
8734
9432
|
*/
|
|
8735
|
-
function PullToRefresh({ children, onRefresh,
|
|
9433
|
+
function PullToRefresh({ children, onRefresh, threshold = 80, disabled = false, pullingContent, releaseContent, refreshingContent, completeContent, className = '', }) {
|
|
9434
|
+
const containerRef = useRef(null);
|
|
8736
9435
|
const [state, setState] = useState('idle');
|
|
8737
9436
|
const [pullDistance, setPullDistance] = useState(0);
|
|
8738
|
-
const containerRef = useRef(null);
|
|
8739
9437
|
const startY = useRef(0);
|
|
8740
9438
|
const currentY = useRef(0);
|
|
8741
|
-
|
|
9439
|
+
const isDragging = useRef(false);
|
|
9440
|
+
// Check if content is at top (can pull to refresh)
|
|
8742
9441
|
const isAtTop = useCallback(() => {
|
|
8743
9442
|
const container = containerRef.current;
|
|
8744
9443
|
if (!container)
|
|
8745
9444
|
return false;
|
|
8746
|
-
|
|
9445
|
+
// Check if the scrollable content is at the top
|
|
9446
|
+
const scrollableParent = container.querySelector('[data-ptr-scrollable]') || container;
|
|
9447
|
+
return scrollableParent.scrollTop <= 0;
|
|
8747
9448
|
}, []);
|
|
8748
|
-
// Handle
|
|
9449
|
+
// Handle pull start
|
|
8749
9450
|
const handleTouchStart = useCallback((e) => {
|
|
8750
|
-
if (disabled || state === 'refreshing'
|
|
9451
|
+
if (disabled || state === 'refreshing')
|
|
9452
|
+
return;
|
|
9453
|
+
if (!isAtTop())
|
|
8751
9454
|
return;
|
|
9455
|
+
isDragging.current = true;
|
|
8752
9456
|
startY.current = e.touches[0].clientY;
|
|
8753
|
-
currentY.current =
|
|
9457
|
+
currentY.current = e.touches[0].clientY;
|
|
8754
9458
|
}, [disabled, state, isAtTop]);
|
|
8755
|
-
// Handle
|
|
9459
|
+
// Handle pull move
|
|
8756
9460
|
const handleTouchMove = useCallback((e) => {
|
|
8757
|
-
if (disabled || state === 'refreshing')
|
|
8758
|
-
return;
|
|
8759
|
-
if (startY.current === 0)
|
|
9461
|
+
if (!isDragging.current || disabled || state === 'refreshing')
|
|
8760
9462
|
return;
|
|
8761
9463
|
currentY.current = e.touches[0].clientY;
|
|
8762
|
-
const
|
|
8763
|
-
// Only
|
|
8764
|
-
if (
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
9464
|
+
const delta = currentY.current - startY.current;
|
|
9465
|
+
// Only activate pull-to-refresh when pulling down
|
|
9466
|
+
if (delta < 0) {
|
|
9467
|
+
isDragging.current = false;
|
|
9468
|
+
setPullDistance(0);
|
|
9469
|
+
setState('idle');
|
|
9470
|
+
return;
|
|
9471
|
+
}
|
|
9472
|
+
// Check if we're at the top before allowing pull
|
|
9473
|
+
if (!isAtTop()) {
|
|
9474
|
+
isDragging.current = false;
|
|
9475
|
+
return;
|
|
9476
|
+
}
|
|
9477
|
+
// Apply resistance to pull
|
|
9478
|
+
const resistance = 0.5;
|
|
9479
|
+
const resistedDelta = delta * resistance;
|
|
9480
|
+
const maxPull = threshold * 2;
|
|
9481
|
+
const clampedDelta = Math.min(resistedDelta, maxPull);
|
|
9482
|
+
setPullDistance(clampedDelta);
|
|
9483
|
+
// Update state based on pull distance
|
|
9484
|
+
if (clampedDelta >= threshold) {
|
|
9485
|
+
setState('ready');
|
|
9486
|
+
}
|
|
9487
|
+
else if (clampedDelta > 0) {
|
|
9488
|
+
setState('pulling');
|
|
9489
|
+
}
|
|
9490
|
+
// Prevent default scroll when pulling
|
|
9491
|
+
if (delta > 0 && isAtTop()) {
|
|
9492
|
+
e.preventDefault();
|
|
8774
9493
|
}
|
|
8775
|
-
}, [disabled, state,
|
|
8776
|
-
// Handle
|
|
9494
|
+
}, [disabled, state, threshold, isAtTop]);
|
|
9495
|
+
// Handle pull end
|
|
8777
9496
|
const handleTouchEnd = useCallback(async () => {
|
|
8778
|
-
if (
|
|
9497
|
+
if (!isDragging.current)
|
|
8779
9498
|
return;
|
|
8780
|
-
|
|
9499
|
+
isDragging.current = false;
|
|
9500
|
+
if (state === 'ready' && pullDistance >= threshold) {
|
|
8781
9501
|
setState('refreshing');
|
|
8782
|
-
setPullDistance(
|
|
9502
|
+
setPullDistance(threshold * 0.6); // Settle at a smaller height while refreshing
|
|
8783
9503
|
try {
|
|
8784
9504
|
await onRefresh();
|
|
9505
|
+
setState('complete');
|
|
9506
|
+
// Show complete state briefly
|
|
9507
|
+
setTimeout(() => {
|
|
9508
|
+
setState('idle');
|
|
9509
|
+
setPullDistance(0);
|
|
9510
|
+
}, 500);
|
|
8785
9511
|
}
|
|
8786
9512
|
catch (error) {
|
|
8787
9513
|
console.error('Refresh failed:', error);
|
|
9514
|
+
setState('idle');
|
|
9515
|
+
setPullDistance(0);
|
|
8788
9516
|
}
|
|
9517
|
+
}
|
|
9518
|
+
else {
|
|
9519
|
+
// Snap back
|
|
8789
9520
|
setState('idle');
|
|
9521
|
+
setPullDistance(0);
|
|
8790
9522
|
}
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
currentY.current = 0;
|
|
8794
|
-
}, [disabled, state, pullThreshold, onRefresh]);
|
|
8795
|
-
// Attach touch listeners
|
|
9523
|
+
}, [state, pullDistance, threshold, onRefresh]);
|
|
9524
|
+
// Attach touch event listeners
|
|
8796
9525
|
useEffect(() => {
|
|
8797
9526
|
const container = containerRef.current;
|
|
8798
9527
|
if (!container)
|
|
@@ -8806,99 +9535,41 @@ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold =
|
|
|
8806
9535
|
container.removeEventListener('touchend', handleTouchEnd);
|
|
8807
9536
|
};
|
|
8808
9537
|
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
8809
|
-
// Calculate
|
|
8810
|
-
const progress = Math.min(pullDistance /
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
const
|
|
8814
|
-
|
|
8815
|
-
const
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
9538
|
+
// Calculate progress percentage
|
|
9539
|
+
const progress = Math.min(1, pullDistance / threshold);
|
|
9540
|
+
// Default content for each state
|
|
9541
|
+
const defaultPullingContent = (jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsx(ArrowDown, { className: "h-5 w-5 text-ink-400 transition-transform duration-200", style: { transform: `rotate(${progress * 180}deg)` } }), jsx("span", { className: "text-xs text-ink-500", children: "Pull to refresh" })] }));
|
|
9542
|
+
const defaultReleaseContent = (jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsx(ArrowDown, { className: "h-5 w-5 text-accent-500 rotate-180" }), jsx("span", { className: "text-xs text-accent-600 font-medium", children: "Release to refresh" })] }));
|
|
9543
|
+
const defaultRefreshingContent = (jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsx(Loader2, { className: "h-5 w-5 text-accent-500 animate-spin" }), jsx("span", { className: "text-xs text-ink-500", children: "Refreshing..." })] }));
|
|
9544
|
+
const defaultCompleteContent = (jsxs("div", { className: "flex flex-col items-center gap-1", children: [jsx(Check, { className: "h-5 w-5 text-success-500" }), jsx("span", { className: "text-xs text-success-600", children: "Done!" })] }));
|
|
9545
|
+
// Get content based on current state
|
|
9546
|
+
const getIndicatorContent = () => {
|
|
9547
|
+
switch (state) {
|
|
9548
|
+
case 'pulling':
|
|
9549
|
+
return pullingContent || defaultPullingContent;
|
|
9550
|
+
case 'ready':
|
|
9551
|
+
return releaseContent || defaultReleaseContent;
|
|
9552
|
+
case 'refreshing':
|
|
9553
|
+
return refreshingContent || defaultRefreshingContent;
|
|
9554
|
+
case 'complete':
|
|
9555
|
+
return completeContent || defaultCompleteContent;
|
|
9556
|
+
default:
|
|
9557
|
+
return null;
|
|
9558
|
+
}
|
|
9559
|
+
};
|
|
9560
|
+
return (jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, children: [jsx("div", { className: `
|
|
9561
|
+
absolute top-0 left-0 right-0
|
|
9562
|
+
flex items-center justify-center
|
|
9563
|
+
bg-paper-50
|
|
9564
|
+
transition-all duration-200 ease-out
|
|
9565
|
+
${state === 'idle' ? 'opacity-0' : 'opacity-100'}
|
|
8823
9566
|
`, style: {
|
|
8824
|
-
height:
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
}, children: jsx("div", { className: `
|
|
8828
|
-
w-10 h-10 rounded-full bg-white shadow-md
|
|
8829
|
-
flex items-center justify-center
|
|
8830
|
-
transition-transform duration-200
|
|
8831
|
-
${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
|
|
8832
|
-
`, children: state === 'refreshing'
|
|
8833
|
-
? (loadingIndicator || defaultLoadingIndicator)
|
|
8834
|
-
: (pullIndicator || defaultPullIndicator) }) }), jsx("div", { className: "transition-transform duration-200", style: {
|
|
9567
|
+
height: pullDistance,
|
|
9568
|
+
transform: state === 'idle' ? 'translateY(-100%)' : 'translateY(0)',
|
|
9569
|
+
}, children: getIndicatorContent() }), jsx("div", { className: "transition-transform duration-200 ease-out", style: {
|
|
8835
9570
|
transform: `translateY(${pullDistance}px)`,
|
|
8836
9571
|
}, children: children })] }));
|
|
8837
9572
|
}
|
|
8838
|
-
/**
|
|
8839
|
-
* usePullToRefresh - Hook for custom pull-to-refresh implementations
|
|
8840
|
-
*
|
|
8841
|
-
* @example
|
|
8842
|
-
* ```tsx
|
|
8843
|
-
* const { pullDistance, isRefreshing, bind } = usePullToRefresh({
|
|
8844
|
-
* onRefresh: async () => {
|
|
8845
|
-
* await fetchData();
|
|
8846
|
-
* }
|
|
8847
|
-
* });
|
|
8848
|
-
*
|
|
8849
|
-
* return (
|
|
8850
|
-
* <div {...bind}>
|
|
8851
|
-
* {isRefreshing && <Spinner />}
|
|
8852
|
-
* {content}
|
|
8853
|
-
* </div>
|
|
8854
|
-
* );
|
|
8855
|
-
* ```
|
|
8856
|
-
*/
|
|
8857
|
-
function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
|
|
8858
|
-
const [pullDistance, setPullDistance] = useState(0);
|
|
8859
|
-
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
8860
|
-
const startY = useRef(0);
|
|
8861
|
-
const handleTouchStart = useCallback((e) => {
|
|
8862
|
-
if (disabled || isRefreshing)
|
|
8863
|
-
return;
|
|
8864
|
-
startY.current = e.touches[0].clientY;
|
|
8865
|
-
}, [disabled, isRefreshing]);
|
|
8866
|
-
const handleTouchMove = useCallback((e) => {
|
|
8867
|
-
if (disabled || isRefreshing || startY.current === 0)
|
|
8868
|
-
return;
|
|
8869
|
-
const diff = e.touches[0].clientY - startY.current;
|
|
8870
|
-
if (diff > 0) {
|
|
8871
|
-
const adjustedPull = Math.min(diff * 0.5, maxPull);
|
|
8872
|
-
setPullDistance(adjustedPull);
|
|
8873
|
-
}
|
|
8874
|
-
}, [disabled, isRefreshing, maxPull]);
|
|
8875
|
-
const handleTouchEnd = useCallback(async () => {
|
|
8876
|
-
if (disabled || isRefreshing)
|
|
8877
|
-
return;
|
|
8878
|
-
if (pullDistance >= pullThreshold) {
|
|
8879
|
-
setIsRefreshing(true);
|
|
8880
|
-
try {
|
|
8881
|
-
await onRefresh();
|
|
8882
|
-
}
|
|
8883
|
-
finally {
|
|
8884
|
-
setIsRefreshing(false);
|
|
8885
|
-
}
|
|
8886
|
-
}
|
|
8887
|
-
setPullDistance(0);
|
|
8888
|
-
startY.current = 0;
|
|
8889
|
-
}, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
|
|
8890
|
-
return {
|
|
8891
|
-
pullDistance,
|
|
8892
|
-
isRefreshing,
|
|
8893
|
-
isReady: pullDistance >= pullThreshold,
|
|
8894
|
-
progress: Math.min(pullDistance / pullThreshold, 1),
|
|
8895
|
-
bind: {
|
|
8896
|
-
onTouchStart: handleTouchStart,
|
|
8897
|
-
onTouchMove: handleTouchMove,
|
|
8898
|
-
onTouchEnd: handleTouchEnd,
|
|
8899
|
-
},
|
|
8900
|
-
};
|
|
8901
|
-
}
|
|
8902
9573
|
|
|
8903
9574
|
function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
|
|
8904
9575
|
const sizes = {
|
|
@@ -10401,44 +11072,52 @@ function getAugmentedNamespace(n) {
|
|
|
10401
11072
|
* (A1, A1:C5, ...)
|
|
10402
11073
|
*/
|
|
10403
11074
|
|
|
10404
|
-
|
|
11075
|
+
var collection;
|
|
11076
|
+
var hasRequiredCollection;
|
|
11077
|
+
|
|
11078
|
+
function requireCollection () {
|
|
11079
|
+
if (hasRequiredCollection) return collection;
|
|
11080
|
+
hasRequiredCollection = 1;
|
|
11081
|
+
class Collection {
|
|
10405
11082
|
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
11083
|
+
constructor(data, refs) {
|
|
11084
|
+
if (data == null && refs == null) {
|
|
11085
|
+
this._data = [];
|
|
11086
|
+
this._refs = [];
|
|
11087
|
+
} else {
|
|
11088
|
+
if (data.length !== refs.length)
|
|
11089
|
+
throw Error('Collection: data length should match references length.');
|
|
11090
|
+
this._data = data;
|
|
11091
|
+
this._refs = refs;
|
|
11092
|
+
}
|
|
11093
|
+
}
|
|
10417
11094
|
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
11095
|
+
get data() {
|
|
11096
|
+
return this._data;
|
|
11097
|
+
}
|
|
10421
11098
|
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
11099
|
+
get refs() {
|
|
11100
|
+
return this._refs;
|
|
11101
|
+
}
|
|
10425
11102
|
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
11103
|
+
get length() {
|
|
11104
|
+
return this._data.length;
|
|
11105
|
+
}
|
|
10429
11106
|
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
}
|
|
11107
|
+
/**
|
|
11108
|
+
* Add data and references to this collection.
|
|
11109
|
+
* @param {{}} obj - data
|
|
11110
|
+
* @param {{}} ref - reference
|
|
11111
|
+
*/
|
|
11112
|
+
add(obj, ref) {
|
|
11113
|
+
this._data.push(obj);
|
|
11114
|
+
this._refs.push(ref);
|
|
11115
|
+
}
|
|
11116
|
+
}
|
|
10440
11117
|
|
|
10441
|
-
|
|
11118
|
+
collection = Collection;
|
|
11119
|
+
return collection;
|
|
11120
|
+
}
|
|
10442
11121
|
|
|
10443
11122
|
var helpers;
|
|
10444
11123
|
var hasRequiredHelpers;
|
|
@@ -10447,7 +11126,7 @@ function requireHelpers () {
|
|
|
10447
11126
|
if (hasRequiredHelpers) return helpers;
|
|
10448
11127
|
hasRequiredHelpers = 1;
|
|
10449
11128
|
const FormulaError = requireError();
|
|
10450
|
-
const Collection =
|
|
11129
|
+
const Collection = requireCollection();
|
|
10451
11130
|
|
|
10452
11131
|
const Types = {
|
|
10453
11132
|
NUMBER: 0,
|
|
@@ -20101,7 +20780,7 @@ var engineering = EngineeringFunctions;
|
|
|
20101
20780
|
|
|
20102
20781
|
const FormulaError$b = requireError();
|
|
20103
20782
|
const {FormulaHelpers: FormulaHelpers$8, Types: Types$6, WildCard, Address: Address$3} = requireHelpers();
|
|
20104
|
-
const Collection$2 =
|
|
20783
|
+
const Collection$2 = requireCollection();
|
|
20105
20784
|
const H$5 = FormulaHelpers$8;
|
|
20106
20785
|
|
|
20107
20786
|
const ReferenceFunctions$1 = {
|
|
@@ -31729,7 +32408,7 @@ var parsing = {
|
|
|
31729
32408
|
const FormulaError$4 = requireError();
|
|
31730
32409
|
const {Address: Address$1} = requireHelpers();
|
|
31731
32410
|
const {Prefix: Prefix$1, Postfix: Postfix$1, Infix: Infix$1, Operators: Operators$1} = operators;
|
|
31732
|
-
const Collection$1 =
|
|
32411
|
+
const Collection$1 = requireCollection();
|
|
31733
32412
|
const MAX_ROW$1 = 1048576, MAX_COLUMN$1 = 16384;
|
|
31734
32413
|
const {NotAllInputParsedException} = require$$4;
|
|
31735
32414
|
|
|
@@ -32491,7 +33170,7 @@ var hooks$1 = {
|
|
|
32491
33170
|
const FormulaError$2 = requireError();
|
|
32492
33171
|
const {FormulaHelpers: FormulaHelpers$1, Types, Address} = requireHelpers();
|
|
32493
33172
|
const {Prefix, Postfix, Infix, Operators} = operators;
|
|
32494
|
-
const Collection =
|
|
33173
|
+
const Collection = requireCollection();
|
|
32495
33174
|
const MAX_ROW = 1048576, MAX_COLUMN = 16384;
|
|
32496
33175
|
|
|
32497
33176
|
let Utils$1 = class Utils {
|
|
@@ -56981,5 +57660,5 @@ function Responsive({ mobile, tablet, desktop, }) {
|
|
|
56981
57660
|
return jsx(Fragment, { children: mobile || tablet || desktop });
|
|
56982
57661
|
}
|
|
56983
57662
|
|
|
56984
|
-
export { Accordion, ActionBar, ActionBarCenter, ActionBarLeft, ActionBarRight, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataGrid, DataTable, DataTableCardView, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, DesktopOnly, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandablePanel, ExpandablePanelContainer, ExpandablePanelSpacer, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FORMULA_CATEGORIES, FORMULA_DEFINITIONS, FORMULA_NAMES, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, FloatingActionButton, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, MobileHeader, MobileHeaderSpacer, MobileLayout, MobileOnly, MobileProvider, Modal, ModalFooter, MultiSelect, NotificationBar, NotificationIndicator, NumberInput, Page, PageHeader, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, getFormula, getFormulasByCategory, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, searchFormulas, statusManager, useBreadcrumbReset, useBreakpoint, useBreakpointValue, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFABScroll, useFormContext, useIsDesktop, useIsMobile, useIsTablet, useIsTouchDevice, useMediaQuery, useMobileContext, useOrientation, usePrefersMobile,
|
|
57663
|
+
export { Accordion, ActionBar, ActionBarCenter, ActionBarLeft, ActionBarRight, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, BottomSheetActions, BottomSheetContent, BottomSheetHeader, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, CompactStat, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataGrid, DataTable, DataTableCardView, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, DesktopOnly, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandablePanel, ExpandablePanelContainer, ExpandablePanelSpacer, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FORMULA_CATEGORIES, FORMULA_DEFINITIONS, FORMULA_NAMES, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, FloatingActionButton, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HorizontalScroll, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, MobileHeader, MobileHeaderSpacer, MobileLayout, MobileOnly, MobileProvider, Modal, ModalFooter, MultiSelect, NotificationBanner, NotificationBar, NotificationIndicator, NumberInput, Page, PageHeader, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, SwipeableCard, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, getFormula, getFormulasByCategory, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, searchFormulas, statusManager, useBreadcrumbReset, useBreakpoint, useBreakpointValue, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFABScroll, useFormContext, useIsDesktop, useIsMobile, useIsTablet, useIsTouchDevice, useMediaQuery, useMobileContext, useOrientation, usePrefersMobile, useResponsiveCallback, useSafeAreaInsets, useViewportSize, withMobileContext };
|
|
56985
57664
|
//# sourceMappingURL=index.esm.js.map
|