@papernote/ui 1.7.6 → 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.
Files changed (45) hide show
  1. package/dist/components/Autocomplete.d.ts +4 -0
  2. package/dist/components/Autocomplete.d.ts.map +1 -1
  3. package/dist/components/Badge.d.ts +3 -1
  4. package/dist/components/Badge.d.ts.map +1 -1
  5. package/dist/components/BottomSheet.d.ts +72 -8
  6. package/dist/components/BottomSheet.d.ts.map +1 -1
  7. package/dist/components/CompactStat.d.ts +52 -0
  8. package/dist/components/CompactStat.d.ts.map +1 -0
  9. package/dist/components/HorizontalScroll.d.ts +43 -0
  10. package/dist/components/HorizontalScroll.d.ts.map +1 -0
  11. package/dist/components/NotificationBanner.d.ts +53 -0
  12. package/dist/components/NotificationBanner.d.ts.map +1 -0
  13. package/dist/components/Progress.d.ts +2 -2
  14. package/dist/components/Progress.d.ts.map +1 -1
  15. package/dist/components/PullToRefresh.d.ts +23 -71
  16. package/dist/components/PullToRefresh.d.ts.map +1 -1
  17. package/dist/components/Stack.d.ts +2 -1
  18. package/dist/components/Stack.d.ts.map +1 -1
  19. package/dist/components/SwipeableCard.d.ts +65 -0
  20. package/dist/components/SwipeableCard.d.ts.map +1 -0
  21. package/dist/components/Text.d.ts +9 -2
  22. package/dist/components/Text.d.ts.map +1 -1
  23. package/dist/components/index.d.ts +11 -3
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/index.d.ts +321 -86
  26. package/dist/index.esm.js +999 -267
  27. package/dist/index.esm.js.map +1 -1
  28. package/dist/index.js +1004 -266
  29. package/dist/index.js.map +1 -1
  30. package/dist/styles.css +191 -8
  31. package/package.json +1 -1
  32. package/src/components/Autocomplete.tsx +95 -32
  33. package/src/components/Badge.tsx +13 -2
  34. package/src/components/BottomSheet.tsx +227 -98
  35. package/src/components/Card.tsx +1 -1
  36. package/src/components/CompactStat.tsx +150 -0
  37. package/src/components/HorizontalScroll.tsx +275 -0
  38. package/src/components/NotificationBanner.tsx +238 -0
  39. package/src/components/Progress.tsx +6 -3
  40. package/src/components/PullToRefresh.tsx +158 -196
  41. package/src/components/Stack.tsx +4 -1
  42. package/src/components/SwipeableCard.tsx +347 -0
  43. package/src/components/Text.tsx +45 -3
  44. package/src/components/index.ts +16 -3
  45. 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-5',
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
- const heightPresets = {
3111
- sm: '33vh',
3112
- md: '50vh',
3113
- lg: '75vh',
3114
- full: '90vh',
3115
- };
3116
- function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHandle = true, showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className = '', }) {
3117
- const titleId = useId();
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 [currentHeight] = useState(typeof height === 'string' && height in heightPresets
3121
- ? heightPresets[height]
3122
- : height);
3123
- const sheetRef = useRef(null);
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 (!isOpen || !closeOnEscape)
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
- }, [isOpen, closeOnEscape, onClose]);
3137
- // Prevent body scroll when open
3191
+ }, [open, closeOnEscape, onClose]);
3192
+ // Prevent body scroll
3138
3193
  useEffect(() => {
3139
- if (isOpen) {
3140
- document.body.style.overflow = 'hidden';
3141
- }
3142
- else {
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
- }, [isOpen]);
3149
- const handleOverlayClick = (e) => {
3150
- if (closeOnOverlayClick && e.target === e.currentTarget) {
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
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
3157
- startYRef.current = clientY;
3158
- };
3159
- const handleDragMove = (e) => {
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
- // Close if dragged down more than 150px
3172
- if (dragOffset > 150) {
3173
- onClose();
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
+ }
3174
3232
  }
3175
- setDragOffset(0);
3233
+ else {
3234
+ // Snap back
3235
+ setDragOffset(0);
3236
+ }
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 handleMove = (e) => handleDragMove(e);
3181
- const handleEnd = () => handleDragEnd();
3182
- document.addEventListener('touchmove', handleMove);
3183
- document.addEventListener('mousemove', handleMove);
3184
- document.addEventListener('touchend', handleEnd);
3185
- document.addEventListener('mouseup', handleEnd);
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('touchmove', handleMove);
3188
- document.removeEventListener('mousemove', handleMove);
3189
- document.removeEventListener('touchend', handleEnd);
3190
- document.removeEventListener('mouseup', handleEnd);
3264
+ document.removeEventListener('mousemove', handleMouseMove);
3265
+ document.removeEventListener('mouseup', handleMouseUp);
3191
3266
  };
3192
- }, [isDragging, dragOffset]);
3193
- if (!isOpen)
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
- return (jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: handleOverlayClick, children: [jsx("div", { className: `
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
- ${isOpen ? 'opacity-100' : 'opacity-0'}
3198
- ` }), jsxs("div", { ref: sheetRef, className: `
3199
- relative w-full bg-white rounded-t-2xl shadow-2xl
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
- ${isOpen ? 'translate-y-0' : 'translate-y-full'}
3291
+ ${isDragging ? 'transition-none' : ''}
3202
3292
  ${className}
3203
3293
  `, style: {
3204
- height: currentHeight,
3294
+ height: getSheetHeight(),
3295
+ maxHeight,
3205
3296
  transform: `translateY(${dragOffset}px)`,
3206
- }, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, children: [showHandle && (jsx("div", { className: "py-3 cursor-grab active:cursor-grabbing", onTouchStart: handleDragStart, onMouseDown: handleDragStart, children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) })), (title || showCloseButton) && (jsxs("div", { className: "px-6 py-4 border-b border-ink-200 flex items-center justify-between", children: [title && (jsx("h2", { id: titleId, className: "text-lg font-semibold text-ink-900", children: title })), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors ml-auto", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) }))] })), jsx("div", { className: "overflow-y-auto flex-1 p-6", children: children })] })] }));
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 = {
@@ -5007,7 +5117,7 @@ const MaskedInput = forwardRef(({ value, onChange, maskType = 'phone', customMas
5007
5117
  });
5008
5118
  MaskedInput.displayName = 'MaskedInput';
5009
5119
 
5010
- const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', }, ref) => {
5120
+ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', showOptionsOnFocus = true, }, ref) => {
5011
5121
  const [isOpen, setIsOpen] = useState(false);
5012
5122
  const [filteredOptions, setFilteredOptions] = useState([]);
5013
5123
  const [loading, setLoading] = useState(false);
@@ -5019,6 +5129,30 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5019
5129
  const labelId = useId();
5020
5130
  const listboxId = useId();
5021
5131
  const errorId = useId();
5132
+ // Helper to find next selectable (non-header) index
5133
+ const findNextSelectableIndex = (currentIndex, optionsList) => {
5134
+ for (let i = currentIndex + 1; i < optionsList.length; i++) {
5135
+ if (!optionsList[i].isHeader)
5136
+ return i;
5137
+ }
5138
+ return currentIndex; // Stay at current if no next selectable
5139
+ };
5140
+ // Helper to find previous selectable (non-header) index
5141
+ const findPrevSelectableIndex = (currentIndex, optionsList) => {
5142
+ for (let i = currentIndex - 1; i >= 0; i--) {
5143
+ if (!optionsList[i].isHeader)
5144
+ return i;
5145
+ }
5146
+ return -1; // Go to -1 if no previous selectable
5147
+ };
5148
+ // Helper to find first selectable (non-header) index
5149
+ const findFirstSelectableIndex = (optionsList) => {
5150
+ for (let i = 0; i < optionsList.length; i++) {
5151
+ if (!optionsList[i].isHeader)
5152
+ return i;
5153
+ }
5154
+ return -1;
5155
+ };
5022
5156
  // Expose methods via ref
5023
5157
  useImperativeHandle(ref, () => ({
5024
5158
  focus: () => inputRef.current?.focus(),
@@ -5048,8 +5182,8 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5048
5182
  const results = await onSearch(query);
5049
5183
  setFilteredOptions(results.slice(0, maxResults));
5050
5184
  setIsOpen(results.length > 0);
5051
- // Auto-highlight first result for keyboard navigation
5052
- setHighlightedIndex(results.length > 0 ? 0 : -1);
5185
+ // Auto-highlight first selectable (non-header) result
5186
+ setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
5053
5187
  }
5054
5188
  catch (err) {
5055
5189
  console.error('Autocomplete search error:', err);
@@ -5066,8 +5200,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5066
5200
  const filtered = filterOptions(query);
5067
5201
  setFilteredOptions(filtered);
5068
5202
  setIsOpen(filtered.length > 0);
5069
- // Auto-highlight first result for keyboard navigation
5070
- setHighlightedIndex(filtered.length > 0 ? 0 : -1);
5203
+ // Auto-highlight first selectable (non-header) result
5204
+ setHighlightedIndex(findFirstSelectableIndex(filtered));
5205
+ }
5206
+ };
5207
+ // Show static options (for focus/arrow down when input is empty)
5208
+ const showStaticOptions = () => {
5209
+ if (options.length > 0) {
5210
+ setFilteredOptions(options.slice(0, maxResults));
5211
+ setIsOpen(true);
5212
+ setHighlightedIndex(findFirstSelectableIndex(options.slice(0, maxResults)));
5071
5213
  }
5072
5214
  };
5073
5215
  // Debounced search
@@ -5108,7 +5250,11 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5108
5250
  // If we have cached results from a previous search, show them
5109
5251
  if (filteredOptions.length > 0) {
5110
5252
  setIsOpen(true);
5111
- setHighlightedIndex(0);
5253
+ setHighlightedIndex(findFirstSelectableIndex(filteredOptions));
5254
+ }
5255
+ else if (value.length < minChars && options.length > 0) {
5256
+ // Show static options when input is empty/below minChars
5257
+ showStaticOptions();
5112
5258
  }
5113
5259
  else if (value.length >= minChars) {
5114
5260
  // Otherwise trigger a new search
@@ -5120,16 +5266,20 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5120
5266
  switch (e.key) {
5121
5267
  case 'ArrowDown':
5122
5268
  e.preventDefault();
5123
- setHighlightedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev);
5269
+ setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
5124
5270
  break;
5125
5271
  case 'ArrowUp':
5126
5272
  e.preventDefault();
5127
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
5273
+ setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
5128
5274
  break;
5129
5275
  case 'Enter':
5130
5276
  e.preventDefault();
5131
5277
  if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
5132
- handleSelect(filteredOptions[highlightedIndex]);
5278
+ const option = filteredOptions[highlightedIndex];
5279
+ // Don't select headers
5280
+ if (!option.isHeader) {
5281
+ handleSelect(option);
5282
+ }
5133
5283
  }
5134
5284
  break;
5135
5285
  case 'Escape':
@@ -5159,7 +5309,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5159
5309
  }
5160
5310
  };
5161
5311
  }, []);
5162
- return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => value.length >= minChars && handleSearch(value), placeholder: placeholder, disabled: disabled, className: `
5312
+ return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => {
5313
+ if (showOptionsOnFocus && value.length < minChars && options.length > 0) {
5314
+ // Show static options when input is empty/below minChars
5315
+ showStaticOptions();
5316
+ }
5317
+ else if (value.length >= minChars) {
5318
+ // Trigger search if we have enough chars
5319
+ handleSearch(value);
5320
+ }
5321
+ }, placeholder: placeholder, disabled: disabled, className: `
5163
5322
  w-full pl-9 pr-9 py-2
5164
5323
  text-sm text-ink-900 placeholder-ink-400
5165
5324
  bg-white border rounded-lg
@@ -5169,12 +5328,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5169
5328
  ${error
5170
5329
  ? 'border-error-500 focus:ring-error-400 focus:border-error-400'
5171
5330
  : 'border-paper-300'}
5172
- `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
5173
- w-full text-left px-3 py-2 transition-colors
5174
- ${highlightedIndex === index
5331
+ `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (option.isHeader ? (
5332
+ // Render section header (non-selectable)
5333
+ jsx("div", { className: "px-3 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wide bg-paper-50 border-t border-paper-200 first:border-t-0 first:rounded-t-lg cursor-default", role: "presentation", children: option.label }, `header-${option.value}`)) : (
5334
+ // Render selectable option
5335
+ jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
5336
+ w-full text-left px-3 py-2 transition-colors
5337
+ ${highlightedIndex === index
5175
5338
  ? 'bg-accent-50'
5176
5339
  : 'hover:bg-paper-50'}
5177
- `, children: [jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (jsx("div", { className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg p-3", role: "status", "aria-live": "polite", children: jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
5340
+ `, children: [jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value)))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (jsx("div", { className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg p-3", role: "status", "aria-live": "polite", children: jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
5178
5341
  });
5179
5342
  Autocomplete.displayName = 'Autocomplete';
5180
5343
 
@@ -7285,6 +7448,598 @@ function Hide({ children, above, below, only, className = '' }) {
7285
7448
  return (jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
7286
7449
  }
7287
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
+
7288
8043
  /**
7289
8044
  * Hook to detect breadcrumb navigation and trigger callbacks.
7290
8045
  * Use this in host components to reset state when a breadcrumb is clicked.
@@ -7568,7 +8323,7 @@ function StepIndicator({ steps, currentStep, variant = 'horizontal', onStepClick
7568
8323
  }) }) }));
7569
8324
  }
7570
8325
 
7571
- 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, }) {
7572
8327
  const variantStyles = {
7573
8328
  success: 'bg-success-50 text-success-700 border-success-200',
7574
8329
  warning: 'bg-warning-50 text-warning-700 border-warning-200',
@@ -7588,6 +8343,12 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
7588
8343
  md: 'px-3 py-1 text-xs gap-1.5',
7589
8344
  lg: 'px-3 py-1.5 text-sm gap-2',
7590
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
+ };
7591
8352
  const dotSizeStyles = {
7592
8353
  sm: 'h-1.5 w-1.5',
7593
8354
  md: 'h-2 w-2',
@@ -7609,9 +8370,10 @@ function Badge({ children, variant = 'neutral', size = 'md', icon, onRemove, cla
7609
8370
  }
7610
8371
  // Regular badge
7611
8372
  return (jsxs("span", { className: `
7612
- inline-flex items-center rounded-full border font-medium
8373
+ inline-flex items-center border font-medium
8374
+ ${pill ? 'rounded-full' : 'rounded-full'}
7613
8375
  ${variantStyles[variant]}
7614
- ${sizeStyles[size]}
8376
+ ${pill ? pillSizeStyles[size] : sizeStyles[size]}
7615
8377
  ${className}
7616
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] }) }))] }));
7617
8379
  }
@@ -7719,8 +8481,10 @@ function Progress({ value, variant = 'linear', size = 'md', color = 'primary', s
7719
8481
  warning: 'bg-warning-100',
7720
8482
  error: 'bg-error-100',
7721
8483
  };
8484
+ // Normalize 'ring' to 'circular'
8485
+ const normalizedVariant = variant === 'ring' ? 'circular' : variant;
7722
8486
  // Linear progress
7723
- if (variant === 'linear') {
8487
+ if (normalizedVariant === 'linear') {
7724
8488
  const heightClasses = {
7725
8489
  sm: 'h-1',
7726
8490
  md: 'h-2',
@@ -8652,94 +9416,112 @@ function useFABScroll(threshold = 10) {
8652
9416
  }
8653
9417
 
8654
9418
  /**
8655
- * PullToRefresh - Mobile pull-to-refresh gesture handler
9419
+ * PullToRefresh - Pull-down refresh indicator and handler for mobile lists
8656
9420
  *
8657
- * Wraps content and provides native-feeling pull-to-refresh functionality.
8658
- * Only activates when scrolled to top of content.
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
8659
9425
  *
8660
- * @example Basic usage
8661
- * ```tsx
8662
- * <PullToRefresh onRefresh={async () => {
8663
- * await fetchLatestData();
8664
- * }}>
8665
- * <div className="min-h-screen">
8666
- * {content}
8667
- * </div>
8668
- * </PullToRefresh>
8669
- * ```
8670
- *
8671
- * @example With custom threshold
9426
+ * @example
8672
9427
  * ```tsx
8673
- * <PullToRefresh
8674
- * onRefresh={handleRefresh}
8675
- * pullThreshold={100}
8676
- * maxPull={150}
8677
- * >
8678
- * {content}
9428
+ * <PullToRefresh onRefresh={async () => { await syncData(); }}>
9429
+ * <TransactionList transactions={transactions} />
8679
9430
  * </PullToRefresh>
8680
9431
  * ```
8681
9432
  */
8682
- function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold = 80, maxPull = 120, loadingIndicator, pullIndicator, className = '', }) {
9433
+ function PullToRefresh({ children, onRefresh, threshold = 80, disabled = false, pullingContent, releaseContent, refreshingContent, completeContent, className = '', }) {
9434
+ const containerRef = useRef(null);
8683
9435
  const [state, setState] = useState('idle');
8684
9436
  const [pullDistance, setPullDistance] = useState(0);
8685
- const containerRef = useRef(null);
8686
9437
  const startY = useRef(0);
8687
9438
  const currentY = useRef(0);
8688
- // Check if at top of scroll container
9439
+ const isDragging = useRef(false);
9440
+ // Check if content is at top (can pull to refresh)
8689
9441
  const isAtTop = useCallback(() => {
8690
9442
  const container = containerRef.current;
8691
9443
  if (!container)
8692
9444
  return false;
8693
- return container.scrollTop <= 0;
9445
+ // Check if the scrollable content is at the top
9446
+ const scrollableParent = container.querySelector('[data-ptr-scrollable]') || container;
9447
+ return scrollableParent.scrollTop <= 0;
8694
9448
  }, []);
8695
- // Handle touch start
9449
+ // Handle pull start
8696
9450
  const handleTouchStart = useCallback((e) => {
8697
- if (disabled || state === 'refreshing' || !isAtTop())
9451
+ if (disabled || state === 'refreshing')
8698
9452
  return;
9453
+ if (!isAtTop())
9454
+ return;
9455
+ isDragging.current = true;
8699
9456
  startY.current = e.touches[0].clientY;
8700
- currentY.current = startY.current;
9457
+ currentY.current = e.touches[0].clientY;
8701
9458
  }, [disabled, state, isAtTop]);
8702
- // Handle touch move
9459
+ // Handle pull move
8703
9460
  const handleTouchMove = useCallback((e) => {
8704
- if (disabled || state === 'refreshing')
8705
- return;
8706
- if (startY.current === 0)
9461
+ if (!isDragging.current || disabled || state === 'refreshing')
8707
9462
  return;
8708
9463
  currentY.current = e.touches[0].clientY;
8709
- const diff = currentY.current - startY.current;
8710
- // Only allow pulling down when at top
8711
- if (diff > 0 && isAtTop()) {
8712
- // Apply resistance - pull slows down as distance increases
8713
- const resistance = 0.5;
8714
- const adjustedPull = Math.min(diff * resistance, maxPull);
8715
- setPullDistance(adjustedPull);
8716
- setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
8717
- // Prevent default scroll when pulling
8718
- if (adjustedPull > 0) {
8719
- e.preventDefault();
8720
- }
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();
8721
9493
  }
8722
- }, [disabled, state, isAtTop, maxPull, pullThreshold]);
8723
- // Handle touch end
9494
+ }, [disabled, state, threshold, isAtTop]);
9495
+ // Handle pull end
8724
9496
  const handleTouchEnd = useCallback(async () => {
8725
- if (disabled || state === 'refreshing')
9497
+ if (!isDragging.current)
8726
9498
  return;
8727
- if (state === 'ready') {
9499
+ isDragging.current = false;
9500
+ if (state === 'ready' && pullDistance >= threshold) {
8728
9501
  setState('refreshing');
8729
- setPullDistance(pullThreshold); // Hold at threshold while refreshing
9502
+ setPullDistance(threshold * 0.6); // Settle at a smaller height while refreshing
8730
9503
  try {
8731
9504
  await onRefresh();
9505
+ setState('complete');
9506
+ // Show complete state briefly
9507
+ setTimeout(() => {
9508
+ setState('idle');
9509
+ setPullDistance(0);
9510
+ }, 500);
8732
9511
  }
8733
9512
  catch (error) {
8734
9513
  console.error('Refresh failed:', error);
9514
+ setState('idle');
9515
+ setPullDistance(0);
8735
9516
  }
9517
+ }
9518
+ else {
9519
+ // Snap back
8736
9520
  setState('idle');
9521
+ setPullDistance(0);
8737
9522
  }
8738
- setPullDistance(0);
8739
- startY.current = 0;
8740
- currentY.current = 0;
8741
- }, [disabled, state, pullThreshold, onRefresh]);
8742
- // Attach touch listeners
9523
+ }, [state, pullDistance, threshold, onRefresh]);
9524
+ // Attach touch event listeners
8743
9525
  useEffect(() => {
8744
9526
  const container = containerRef.current;
8745
9527
  if (!container)
@@ -8753,99 +9535,41 @@ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold =
8753
9535
  container.removeEventListener('touchend', handleTouchEnd);
8754
9536
  };
8755
9537
  }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
8756
- // Calculate indicator opacity and rotation
8757
- const progress = Math.min(pullDistance / pullThreshold, 1);
8758
- const rotation = progress * 180;
8759
- // Default loading indicator
8760
- const defaultLoadingIndicator = (jsx(Loader2, { className: "h-6 w-6 text-accent-600 animate-spin" }));
8761
- // Default pull indicator
8762
- const defaultPullIndicator = (jsx("div", { className: `
8763
- transition-transform duration-200
8764
- ${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
8765
- `, style: { transform: `rotate(${rotation}deg)` }, children: jsx(ArrowDown, { className: "h-6 w-6" }) }));
8766
- return (jsxs("div", { ref: containerRef, className: `relative overflow-auto ${className}`, style: { touchAction: pullDistance > 0 ? 'none' : 'auto' }, children: [jsx("div", { className: `
8767
- absolute left-0 right-0 flex items-center justify-center
8768
- transition-all duration-200 overflow-hidden
8769
- ${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
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'}
8770
9566
  `, style: {
8771
- height: `${pullDistance}px`,
8772
- top: 0,
8773
- zIndex: 10,
8774
- }, children: jsx("div", { className: `
8775
- w-10 h-10 rounded-full bg-white shadow-md
8776
- flex items-center justify-center
8777
- transition-transform duration-200
8778
- ${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
8779
- `, children: state === 'refreshing'
8780
- ? (loadingIndicator || defaultLoadingIndicator)
8781
- : (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: {
8782
9570
  transform: `translateY(${pullDistance}px)`,
8783
9571
  }, children: children })] }));
8784
9572
  }
8785
- /**
8786
- * usePullToRefresh - Hook for custom pull-to-refresh implementations
8787
- *
8788
- * @example
8789
- * ```tsx
8790
- * const { pullDistance, isRefreshing, bind } = usePullToRefresh({
8791
- * onRefresh: async () => {
8792
- * await fetchData();
8793
- * }
8794
- * });
8795
- *
8796
- * return (
8797
- * <div {...bind}>
8798
- * {isRefreshing && <Spinner />}
8799
- * {content}
8800
- * </div>
8801
- * );
8802
- * ```
8803
- */
8804
- function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
8805
- const [pullDistance, setPullDistance] = useState(0);
8806
- const [isRefreshing, setIsRefreshing] = useState(false);
8807
- const startY = useRef(0);
8808
- const handleTouchStart = useCallback((e) => {
8809
- if (disabled || isRefreshing)
8810
- return;
8811
- startY.current = e.touches[0].clientY;
8812
- }, [disabled, isRefreshing]);
8813
- const handleTouchMove = useCallback((e) => {
8814
- if (disabled || isRefreshing || startY.current === 0)
8815
- return;
8816
- const diff = e.touches[0].clientY - startY.current;
8817
- if (diff > 0) {
8818
- const adjustedPull = Math.min(diff * 0.5, maxPull);
8819
- setPullDistance(adjustedPull);
8820
- }
8821
- }, [disabled, isRefreshing, maxPull]);
8822
- const handleTouchEnd = useCallback(async () => {
8823
- if (disabled || isRefreshing)
8824
- return;
8825
- if (pullDistance >= pullThreshold) {
8826
- setIsRefreshing(true);
8827
- try {
8828
- await onRefresh();
8829
- }
8830
- finally {
8831
- setIsRefreshing(false);
8832
- }
8833
- }
8834
- setPullDistance(0);
8835
- startY.current = 0;
8836
- }, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
8837
- return {
8838
- pullDistance,
8839
- isRefreshing,
8840
- isReady: pullDistance >= pullThreshold,
8841
- progress: Math.min(pullDistance / pullThreshold, 1),
8842
- bind: {
8843
- onTouchStart: handleTouchStart,
8844
- onTouchMove: handleTouchMove,
8845
- onTouchEnd: handleTouchEnd,
8846
- },
8847
- };
8848
- }
8849
9573
 
8850
9574
  function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
8851
9575
  const sizes = {
@@ -10348,44 +11072,52 @@ function getAugmentedNamespace(n) {
10348
11072
  * (A1, A1:C5, ...)
10349
11073
  */
10350
11074
 
10351
- let Collection$3 = class Collection {
11075
+ var collection;
11076
+ var hasRequiredCollection;
11077
+
11078
+ function requireCollection () {
11079
+ if (hasRequiredCollection) return collection;
11080
+ hasRequiredCollection = 1;
11081
+ class Collection {
10352
11082
 
10353
- constructor(data, refs) {
10354
- if (data == null && refs == null) {
10355
- this._data = [];
10356
- this._refs = [];
10357
- } else {
10358
- if (data.length !== refs.length)
10359
- throw Error('Collection: data length should match references length.');
10360
- this._data = data;
10361
- this._refs = refs;
10362
- }
10363
- }
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
+ }
10364
11094
 
10365
- get data() {
10366
- return this._data;
10367
- }
11095
+ get data() {
11096
+ return this._data;
11097
+ }
10368
11098
 
10369
- get refs() {
10370
- return this._refs;
10371
- }
11099
+ get refs() {
11100
+ return this._refs;
11101
+ }
10372
11102
 
10373
- get length() {
10374
- return this._data.length;
10375
- }
11103
+ get length() {
11104
+ return this._data.length;
11105
+ }
10376
11106
 
10377
- /**
10378
- * Add data and references to this collection.
10379
- * @param {{}} obj - data
10380
- * @param {{}} ref - reference
10381
- */
10382
- add(obj, ref) {
10383
- this._data.push(obj);
10384
- this._refs.push(ref);
10385
- }
10386
- };
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
+ }
10387
11117
 
10388
- var collection = Collection$3;
11118
+ collection = Collection;
11119
+ return collection;
11120
+ }
10389
11121
 
10390
11122
  var helpers;
10391
11123
  var hasRequiredHelpers;
@@ -10394,7 +11126,7 @@ function requireHelpers () {
10394
11126
  if (hasRequiredHelpers) return helpers;
10395
11127
  hasRequiredHelpers = 1;
10396
11128
  const FormulaError = requireError();
10397
- const Collection = collection;
11129
+ const Collection = requireCollection();
10398
11130
 
10399
11131
  const Types = {
10400
11132
  NUMBER: 0,
@@ -20048,7 +20780,7 @@ var engineering = EngineeringFunctions;
20048
20780
 
20049
20781
  const FormulaError$b = requireError();
20050
20782
  const {FormulaHelpers: FormulaHelpers$8, Types: Types$6, WildCard, Address: Address$3} = requireHelpers();
20051
- const Collection$2 = collection;
20783
+ const Collection$2 = requireCollection();
20052
20784
  const H$5 = FormulaHelpers$8;
20053
20785
 
20054
20786
  const ReferenceFunctions$1 = {
@@ -31676,7 +32408,7 @@ var parsing = {
31676
32408
  const FormulaError$4 = requireError();
31677
32409
  const {Address: Address$1} = requireHelpers();
31678
32410
  const {Prefix: Prefix$1, Postfix: Postfix$1, Infix: Infix$1, Operators: Operators$1} = operators;
31679
- const Collection$1 = collection;
32411
+ const Collection$1 = requireCollection();
31680
32412
  const MAX_ROW$1 = 1048576, MAX_COLUMN$1 = 16384;
31681
32413
  const {NotAllInputParsedException} = require$$4;
31682
32414
 
@@ -32438,7 +33170,7 @@ var hooks$1 = {
32438
33170
  const FormulaError$2 = requireError();
32439
33171
  const {FormulaHelpers: FormulaHelpers$1, Types, Address} = requireHelpers();
32440
33172
  const {Prefix, Postfix, Infix, Operators} = operators;
32441
- const Collection = collection;
33173
+ const Collection = requireCollection();
32442
33174
  const MAX_ROW = 1048576, MAX_COLUMN = 16384;
32443
33175
 
32444
33176
  let Utils$1 = class Utils {
@@ -56928,5 +57660,5 @@ function Responsive({ mobile, tablet, desktop, }) {
56928
57660
  return jsx(Fragment, { children: mobile || tablet || desktop });
56929
57661
  }
56930
57662
 
56931
- 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, usePullToRefresh, useResponsiveCallback, useSafeAreaInsets, useViewportSize, withMobileContext };
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 };
56932
57664
  //# sourceMappingURL=index.esm.js.map