@papernote/ui 1.3.1 → 1.5.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/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +50 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1653 -56
- package/dist/index.esm.js +2832 -194
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2865 -192
- package/dist/index.js.map +1 -1
- package/dist/styles.css +404 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +183 -0
- package/src/components/Modal.tsx +84 -3
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +191 -8
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +63 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- package/tailwind.config.js +56 -1
package/dist/index.js
CHANGED
|
@@ -226,12 +226,19 @@ function ButtonGroup({ options, value, values = [], onChange, onChangeMultiple,
|
|
|
226
226
|
* A feature-rich text input with support for validation states, character counting,
|
|
227
227
|
* password visibility toggle, prefix/suffix text and icons, and clearable functionality.
|
|
228
228
|
*
|
|
229
|
+
* Mobile optimizations:
|
|
230
|
+
* - inputMode prop for appropriate mobile keyboard
|
|
231
|
+
* - enterKeyHint prop for mobile keyboard action button
|
|
232
|
+
* - Size variants with touch-friendly targets (44px for 'lg')
|
|
233
|
+
*
|
|
229
234
|
* @example Basic input with label
|
|
230
235
|
* ```tsx
|
|
231
236
|
* <Input
|
|
232
237
|
* label="Email"
|
|
233
238
|
* type="email"
|
|
234
239
|
* placeholder="Enter your email"
|
|
240
|
+
* inputMode="email"
|
|
241
|
+
* enterKeyHint="next"
|
|
235
242
|
* />
|
|
236
243
|
* ```
|
|
237
244
|
*
|
|
@@ -257,18 +264,30 @@ function ButtonGroup({ options, value, values = [], onChange, onChangeMultiple,
|
|
|
257
264
|
* />
|
|
258
265
|
* ```
|
|
259
266
|
*
|
|
267
|
+
* @example Mobile-optimized phone input
|
|
268
|
+
* ```tsx
|
|
269
|
+
* <Input
|
|
270
|
+
* label="Phone Number"
|
|
271
|
+
* type="tel"
|
|
272
|
+
* inputMode="tel"
|
|
273
|
+
* enterKeyHint="done"
|
|
274
|
+
* size="lg"
|
|
275
|
+
* />
|
|
276
|
+
* ```
|
|
277
|
+
*
|
|
260
278
|
* @example With prefix/suffix
|
|
261
279
|
* ```tsx
|
|
262
280
|
* <Input
|
|
263
281
|
* label="Amount"
|
|
264
282
|
* type="number"
|
|
283
|
+
* inputMode="decimal"
|
|
265
284
|
* prefixIcon={<DollarSign />}
|
|
266
285
|
* suffix="USD"
|
|
267
286
|
* clearable
|
|
268
287
|
* />
|
|
269
288
|
* ```
|
|
270
289
|
*/
|
|
271
|
-
const Input = React.forwardRef(({ label, helperText, validationState, validationMessage, icon, iconPosition = 'left', showCount = false, prefix, suffix, prefixIcon, suffixIcon, showPasswordToggle = false, clearable = false, onClear, loading = false, className = '', id, type = 'text', value, maxLength, ...props }, ref) => {
|
|
290
|
+
const Input = React.forwardRef(({ label, helperText, validationState, validationMessage, icon, iconPosition = 'left', showCount = false, prefix, suffix, prefixIcon, suffixIcon, showPasswordToggle = false, clearable = false, onClear, loading = false, className = '', id, type = 'text', value, maxLength, inputMode, enterKeyHint, size = 'md', ...props }, ref) => {
|
|
272
291
|
const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
|
|
273
292
|
const [showPassword, setShowPassword] = React.useState(false);
|
|
274
293
|
// Handle clear button click
|
|
@@ -292,6 +311,28 @@ const Input = React.forwardRef(({ label, helperText, validationState, validation
|
|
|
292
311
|
// Calculate character count
|
|
293
312
|
const currentLength = value ? String(value).length : 0;
|
|
294
313
|
const showCounter = showCount && maxLength;
|
|
314
|
+
// Auto-detect inputMode based on type if not specified
|
|
315
|
+
const effectiveInputMode = inputMode || (() => {
|
|
316
|
+
switch (type) {
|
|
317
|
+
case 'email': return 'email';
|
|
318
|
+
case 'tel': return 'tel';
|
|
319
|
+
case 'url': return 'url';
|
|
320
|
+
case 'number': return 'decimal';
|
|
321
|
+
case 'search': return 'search';
|
|
322
|
+
default: return undefined;
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
// Size classes
|
|
326
|
+
const sizeClasses = {
|
|
327
|
+
sm: 'h-8 text-sm',
|
|
328
|
+
md: 'h-10 text-base',
|
|
329
|
+
lg: 'h-12 text-base min-h-touch', // 44px touch target
|
|
330
|
+
};
|
|
331
|
+
const buttonSizeClasses = {
|
|
332
|
+
sm: 'p-1',
|
|
333
|
+
md: 'p-1.5',
|
|
334
|
+
lg: 'p-2 min-w-touch-sm min-h-touch-sm', // 36px touch target for buttons
|
|
335
|
+
};
|
|
295
336
|
const getValidationIcon = () => {
|
|
296
337
|
switch (validationState) {
|
|
297
338
|
case 'error':
|
|
@@ -328,8 +369,9 @@ const Input = React.forwardRef(({ label, helperText, validationState, validation
|
|
|
328
369
|
return 'text-ink-600';
|
|
329
370
|
}
|
|
330
371
|
};
|
|
331
|
-
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-500 text-sm", children: prefix })), prefixIcon && !prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: prefixIcon })), icon && iconPosition === 'left' && !prefix && !prefixIcon && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsxRuntime.jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, className: `
|
|
372
|
+
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-500 text-sm", children: prefix })), prefixIcon && !prefix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: prefixIcon })), icon && iconPosition === 'left' && !prefix && !prefixIcon && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsxRuntime.jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, inputMode: effectiveInputMode, enterKeyHint: enterKeyHint, className: `
|
|
332
373
|
input
|
|
374
|
+
${sizeClasses[size]}
|
|
333
375
|
${getValidationClasses()}
|
|
334
376
|
${prefix ? 'pl-' + (prefix.length * 8 + 12) : ''}
|
|
335
377
|
${prefixIcon && !prefix ? 'pl-10' : ''}
|
|
@@ -340,15 +382,323 @@ const Input = React.forwardRef(({ label, helperText, validationState, validation
|
|
|
340
382
|
${validationState && !suffix && !suffixIcon && !showPasswordToggle ? 'pr-10' : ''}
|
|
341
383
|
${(showPasswordToggle && type === 'password') || validationState || suffix || suffixIcon ? 'pr-20' : ''}
|
|
342
384
|
${className}
|
|
343
|
-
`, ...props }), suffix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-
|
|
385
|
+
`, ...props }), suffix && (jsxRuntime.jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxRuntime.jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-1", children: [loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin" }) })), suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: suffixIcon })), showClearButton && (jsxRuntime.jsx("button", { type: "button", onClick: handleClear, className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": "Clear input", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })), type === 'password' && showPasswordToggle && (jsxRuntime.jsx("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: `text-ink-400 hover:text-ink-600 focus:outline-none cursor-pointer pointer-events-auto rounded-full hover:bg-paper-100 flex items-center justify-center ${buttonSizeClasses[size]}`, "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? jsxRuntime.jsx(lucideReact.EyeOff, { className: "h-5 w-5" }) : jsxRuntime.jsx(lucideReact.Eye, { className: "h-5 w-5" }) })), validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none", children: getValidationIcon() })), icon && iconPosition === 'right' && !suffix && !suffixIcon && !validationState && (jsxRuntime.jsx("div", { className: "pointer-events-none text-ink-400", children: icon }))] })] }), jsxRuntime.jsxs("div", { className: "flex justify-between items-center mt-2", children: [(helperText || validationMessage) && (jsxRuntime.jsx("p", { className: `text-xs ${validationMessage ? getValidationMessageColor() : 'text-ink-600'}`, children: validationMessage || helperText })), showCounter && (jsxRuntime.jsxs("p", { className: `text-xs ml-auto ${currentLength > maxLength ? 'text-error-600' : 'text-ink-500'}`, children: [currentLength, " / ", maxLength] }))] })] }));
|
|
344
386
|
});
|
|
345
387
|
Input.displayName = 'Input';
|
|
346
388
|
|
|
347
389
|
/**
|
|
348
|
-
*
|
|
390
|
+
* Tailwind breakpoint values in pixels
|
|
391
|
+
*/
|
|
392
|
+
const BREAKPOINTS = {
|
|
393
|
+
xs: 0,
|
|
394
|
+
sm: 640,
|
|
395
|
+
md: 768,
|
|
396
|
+
lg: 1024,
|
|
397
|
+
xl: 1280,
|
|
398
|
+
'2xl': 1536,
|
|
399
|
+
};
|
|
400
|
+
/**
|
|
401
|
+
* SSR-safe check for window availability
|
|
402
|
+
*/
|
|
403
|
+
const isBrowser = typeof window !== 'undefined';
|
|
404
|
+
/**
|
|
405
|
+
* Get initial viewport size (SSR-safe)
|
|
406
|
+
*/
|
|
407
|
+
const getInitialViewportSize = () => {
|
|
408
|
+
if (!isBrowser) {
|
|
409
|
+
return { width: 1024, height: 768 }; // Default to desktop for SSR
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
width: window.innerWidth,
|
|
413
|
+
height: window.innerHeight,
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
/**
|
|
417
|
+
* useViewportSize - Returns current viewport dimensions
|
|
418
|
+
*
|
|
419
|
+
* Updates on window resize with debouncing for performance.
|
|
420
|
+
* SSR-safe with sensible defaults.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* const { width, height } = useViewportSize();
|
|
424
|
+
* console.log(`Viewport: ${width}x${height}`);
|
|
425
|
+
*/
|
|
426
|
+
function useViewportSize() {
|
|
427
|
+
const [size, setSize] = React.useState(getInitialViewportSize);
|
|
428
|
+
React.useEffect(() => {
|
|
429
|
+
if (!isBrowser)
|
|
430
|
+
return;
|
|
431
|
+
let timeoutId;
|
|
432
|
+
const handleResize = () => {
|
|
433
|
+
clearTimeout(timeoutId);
|
|
434
|
+
timeoutId = setTimeout(() => {
|
|
435
|
+
setSize({
|
|
436
|
+
width: window.innerWidth,
|
|
437
|
+
height: window.innerHeight,
|
|
438
|
+
});
|
|
439
|
+
}, 100); // Debounce 100ms
|
|
440
|
+
};
|
|
441
|
+
window.addEventListener('resize', handleResize);
|
|
442
|
+
// Set initial size on mount (in case SSR default differs)
|
|
443
|
+
handleResize();
|
|
444
|
+
return () => {
|
|
445
|
+
clearTimeout(timeoutId);
|
|
446
|
+
window.removeEventListener('resize', handleResize);
|
|
447
|
+
};
|
|
448
|
+
}, []);
|
|
449
|
+
return size;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* useBreakpoint - Returns the current Tailwind breakpoint
|
|
453
|
+
*
|
|
454
|
+
* Automatically updates when viewport crosses breakpoint thresholds.
|
|
455
|
+
*
|
|
456
|
+
* @example
|
|
457
|
+
* const breakpoint = useBreakpoint();
|
|
458
|
+
* // Returns: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
459
|
+
*/
|
|
460
|
+
function useBreakpoint() {
|
|
461
|
+
const { width } = useViewportSize();
|
|
462
|
+
if (width >= BREAKPOINTS['2xl'])
|
|
463
|
+
return '2xl';
|
|
464
|
+
if (width >= BREAKPOINTS.xl)
|
|
465
|
+
return 'xl';
|
|
466
|
+
if (width >= BREAKPOINTS.lg)
|
|
467
|
+
return 'lg';
|
|
468
|
+
if (width >= BREAKPOINTS.md)
|
|
469
|
+
return 'md';
|
|
470
|
+
if (width >= BREAKPOINTS.sm)
|
|
471
|
+
return 'sm';
|
|
472
|
+
return 'xs';
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* useMediaQuery - React hook for CSS media queries
|
|
476
|
+
*
|
|
477
|
+
* SSR-safe implementation that returns false during SSR and
|
|
478
|
+
* updates reactively when media query match state changes.
|
|
479
|
+
*
|
|
480
|
+
* @param query - CSS media query string (e.g., '(max-width: 768px)')
|
|
481
|
+
* @returns boolean indicating if the media query matches
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
485
|
+
* const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
486
|
+
* const isPortrait = useMediaQuery('(orientation: portrait)');
|
|
487
|
+
*/
|
|
488
|
+
function useMediaQuery(query) {
|
|
489
|
+
const [matches, setMatches] = React.useState(() => {
|
|
490
|
+
if (!isBrowser)
|
|
491
|
+
return false;
|
|
492
|
+
return window.matchMedia(query).matches;
|
|
493
|
+
});
|
|
494
|
+
React.useEffect(() => {
|
|
495
|
+
if (!isBrowser)
|
|
496
|
+
return;
|
|
497
|
+
const media = window.matchMedia(query);
|
|
498
|
+
if (media.matches !== matches) {
|
|
499
|
+
setMatches(media.matches);
|
|
500
|
+
}
|
|
501
|
+
const listener = (event) => {
|
|
502
|
+
setMatches(event.matches);
|
|
503
|
+
};
|
|
504
|
+
media.addEventListener('change', listener);
|
|
505
|
+
return () => media.removeEventListener('change', listener);
|
|
506
|
+
}, [query, matches]);
|
|
507
|
+
return matches;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* useIsMobile - Returns true when viewport is mobile-sized (< 768px)
|
|
511
|
+
*
|
|
512
|
+
* @example
|
|
513
|
+
* const isMobile = useIsMobile();
|
|
514
|
+
* return isMobile ? <MobileNav /> : <DesktopNav />;
|
|
515
|
+
*/
|
|
516
|
+
function useIsMobile() {
|
|
517
|
+
return useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* useIsTablet - Returns true when viewport is tablet-sized (768px - 1023px)
|
|
521
|
+
*
|
|
522
|
+
* @example
|
|
523
|
+
* const isTablet = useIsTablet();
|
|
524
|
+
*/
|
|
525
|
+
function useIsTablet() {
|
|
526
|
+
return useMediaQuery(`(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)`);
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* useIsDesktop - Returns true when viewport is desktop-sized (>= 1024px)
|
|
530
|
+
*
|
|
531
|
+
* @example
|
|
532
|
+
* const isDesktop = useIsDesktop();
|
|
533
|
+
*/
|
|
534
|
+
function useIsDesktop() {
|
|
535
|
+
return useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* useIsTouchDevice - Detects if the device supports touch input
|
|
539
|
+
*
|
|
540
|
+
* Uses multiple detection methods for reliability:
|
|
541
|
+
* - Touch event support
|
|
542
|
+
* - Pointer coarse media query
|
|
543
|
+
* - Max touch points
|
|
544
|
+
*
|
|
545
|
+
* @example
|
|
546
|
+
* const isTouchDevice = useIsTouchDevice();
|
|
547
|
+
* // Show swipe hints on touch devices
|
|
548
|
+
*/
|
|
549
|
+
function useIsTouchDevice() {
|
|
550
|
+
const [isTouch, setIsTouch] = React.useState(() => {
|
|
551
|
+
if (!isBrowser)
|
|
552
|
+
return false;
|
|
553
|
+
return ('ontouchstart' in window ||
|
|
554
|
+
navigator.maxTouchPoints > 0 ||
|
|
555
|
+
window.matchMedia('(pointer: coarse)').matches);
|
|
556
|
+
});
|
|
557
|
+
React.useEffect(() => {
|
|
558
|
+
if (!isBrowser)
|
|
559
|
+
return;
|
|
560
|
+
// Re-check on mount for accuracy
|
|
561
|
+
const touchSupported = 'ontouchstart' in window ||
|
|
562
|
+
navigator.maxTouchPoints > 0 ||
|
|
563
|
+
window.matchMedia('(pointer: coarse)').matches;
|
|
564
|
+
setIsTouch(touchSupported);
|
|
565
|
+
}, []);
|
|
566
|
+
return isTouch;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* useOrientation - Returns current screen orientation
|
|
570
|
+
*
|
|
571
|
+
* @returns 'portrait' | 'landscape'
|
|
572
|
+
*
|
|
573
|
+
* @example
|
|
574
|
+
* const orientation = useOrientation();
|
|
575
|
+
* // Adjust layout based on orientation
|
|
576
|
+
*/
|
|
577
|
+
function useOrientation() {
|
|
578
|
+
const { width, height } = useViewportSize();
|
|
579
|
+
return height > width ? 'portrait' : 'landscape';
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* useBreakpointValue - Returns different values based on breakpoint
|
|
583
|
+
*
|
|
584
|
+
* Mobile-first: Returns the value for the current breakpoint or the
|
|
585
|
+
* closest smaller breakpoint that has a value defined.
|
|
586
|
+
*
|
|
587
|
+
* @param values - Object mapping breakpoints to values
|
|
588
|
+
* @param defaultValue - Fallback value if no breakpoint matches
|
|
589
|
+
*
|
|
590
|
+
* @example
|
|
591
|
+
* const columns = useBreakpointValue({ xs: 1, sm: 2, lg: 4 }, 1);
|
|
592
|
+
* // Returns 1 on xs, 2 on sm/md, 4 on lg/xl/2xl
|
|
593
|
+
*
|
|
594
|
+
* const padding = useBreakpointValue({ xs: 'p-2', md: 'p-4', xl: 'p-8' });
|
|
595
|
+
*/
|
|
596
|
+
function useBreakpointValue(values, defaultValue) {
|
|
597
|
+
const breakpoint = useBreakpoint();
|
|
598
|
+
// Breakpoints in order from largest to smallest
|
|
599
|
+
const breakpointOrder = ['2xl', 'xl', 'lg', 'md', 'sm', 'xs'];
|
|
600
|
+
// Find the current breakpoint index
|
|
601
|
+
const currentIndex = breakpointOrder.indexOf(breakpoint);
|
|
602
|
+
// Look for value at current breakpoint or smaller (mobile-first)
|
|
603
|
+
for (let i = currentIndex; i < breakpointOrder.length; i++) {
|
|
604
|
+
const bp = breakpointOrder[i];
|
|
605
|
+
if (bp in values && values[bp] !== undefined) {
|
|
606
|
+
return values[bp];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return defaultValue;
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* useResponsiveCallback - Returns a memoized callback that receives responsive info
|
|
613
|
+
*
|
|
614
|
+
* Useful for callbacks that need to behave differently based on viewport.
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* const handleClick = useResponsiveCallback((isMobile) => {
|
|
618
|
+
* if (isMobile) {
|
|
619
|
+
* openBottomSheet();
|
|
620
|
+
* } else {
|
|
621
|
+
* openModal();
|
|
622
|
+
* }
|
|
623
|
+
* });
|
|
624
|
+
*/
|
|
625
|
+
function useResponsiveCallback(callback) {
|
|
626
|
+
const isMobile = useIsMobile();
|
|
627
|
+
const isTablet = useIsTablet();
|
|
628
|
+
const isDesktop = useIsDesktop();
|
|
629
|
+
return React.useCallback((...args) => callback(isMobile, isTablet, isDesktop)(...args), [callback, isMobile, isTablet, isDesktop]);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* useSafeAreaInsets - Returns safe area insets for notched devices
|
|
633
|
+
*
|
|
634
|
+
* Uses CSS environment variables (env(safe-area-inset-*)) to get
|
|
635
|
+
* safe area dimensions for devices with notches or home indicators.
|
|
636
|
+
*
|
|
637
|
+
* @example
|
|
638
|
+
* const { top, bottom } = useSafeAreaInsets();
|
|
639
|
+
* // Add padding-bottom for home indicator
|
|
640
|
+
*/
|
|
641
|
+
function useSafeAreaInsets() {
|
|
642
|
+
const [insets, setInsets] = React.useState({
|
|
643
|
+
top: 0,
|
|
644
|
+
right: 0,
|
|
645
|
+
bottom: 0,
|
|
646
|
+
left: 0,
|
|
647
|
+
});
|
|
648
|
+
React.useEffect(() => {
|
|
649
|
+
if (!isBrowser)
|
|
650
|
+
return;
|
|
651
|
+
// Create a temporary element to read CSS env() values
|
|
652
|
+
const el = document.createElement('div');
|
|
653
|
+
el.style.position = 'fixed';
|
|
654
|
+
el.style.top = 'env(safe-area-inset-top, 0px)';
|
|
655
|
+
el.style.right = 'env(safe-area-inset-right, 0px)';
|
|
656
|
+
el.style.bottom = 'env(safe-area-inset-bottom, 0px)';
|
|
657
|
+
el.style.left = 'env(safe-area-inset-left, 0px)';
|
|
658
|
+
el.style.visibility = 'hidden';
|
|
659
|
+
el.style.pointerEvents = 'none';
|
|
660
|
+
document.body.appendChild(el);
|
|
661
|
+
const computed = getComputedStyle(el);
|
|
662
|
+
setInsets({
|
|
663
|
+
top: parseInt(computed.top, 10) || 0,
|
|
664
|
+
right: parseInt(computed.right, 10) || 0,
|
|
665
|
+
bottom: parseInt(computed.bottom, 10) || 0,
|
|
666
|
+
left: parseInt(computed.left, 10) || 0,
|
|
667
|
+
});
|
|
668
|
+
document.body.removeChild(el);
|
|
669
|
+
}, []);
|
|
670
|
+
return insets;
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* usePrefersMobile - Checks if user prefers reduced data/animations (mobile-friendly)
|
|
674
|
+
*
|
|
675
|
+
* Combines multiple preferences that might indicate mobile/low-power usage.
|
|
676
|
+
*/
|
|
677
|
+
function usePrefersMobile() {
|
|
678
|
+
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
|
|
679
|
+
const prefersReducedData = useMediaQuery('(prefers-reduced-data: reduce)');
|
|
680
|
+
const isTouchDevice = useIsTouchDevice();
|
|
681
|
+
const isMobile = useIsMobile();
|
|
682
|
+
return isMobile || isTouchDevice || prefersReducedMotion || prefersReducedData;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Size classes for trigger button
|
|
686
|
+
const sizeClasses$a = {
|
|
687
|
+
sm: 'h-8 text-sm py-1',
|
|
688
|
+
md: 'h-10 text-base py-2',
|
|
689
|
+
lg: 'h-12 text-base py-3 min-h-touch', // 44px touch target
|
|
690
|
+
};
|
|
691
|
+
// Size classes for options
|
|
692
|
+
const optionSizeClasses = {
|
|
693
|
+
sm: 'py-2 text-sm',
|
|
694
|
+
md: 'py-2.5 text-sm',
|
|
695
|
+
lg: 'py-3.5 text-base min-h-touch', // 44px touch target for mobile
|
|
696
|
+
};
|
|
697
|
+
/**
|
|
698
|
+
* Select - Dropdown select component with search, groups, virtual scrolling, and mobile support
|
|
349
699
|
*
|
|
350
700
|
* A feature-rich select component supporting flat or grouped options, search/filter,
|
|
351
|
-
* option creation, virtual scrolling for large lists, and
|
|
701
|
+
* option creation, virtual scrolling for large lists, and mobile-optimized BottomSheet display.
|
|
352
702
|
*
|
|
353
703
|
* @example Basic select
|
|
354
704
|
* ```tsx
|
|
@@ -366,6 +716,16 @@ Input.displayName = 'Input';
|
|
|
366
716
|
* />
|
|
367
717
|
* ```
|
|
368
718
|
*
|
|
719
|
+
* @example Mobile-optimized with large touch targets
|
|
720
|
+
* ```tsx
|
|
721
|
+
* <Select
|
|
722
|
+
* options={options}
|
|
723
|
+
* size="lg"
|
|
724
|
+
* mobileMode="auto"
|
|
725
|
+
* placeholder="Select..."
|
|
726
|
+
* />
|
|
727
|
+
* ```
|
|
728
|
+
*
|
|
369
729
|
* @example Searchable with groups
|
|
370
730
|
* ```tsx
|
|
371
731
|
* const groups = [
|
|
@@ -405,7 +765,7 @@ Input.displayName = 'Input';
|
|
|
405
765
|
* ```
|
|
406
766
|
*/
|
|
407
767
|
const Select = React.forwardRef((props, ref) => {
|
|
408
|
-
const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, } = props;
|
|
768
|
+
const { options = [], groups = [], value, onChange, placeholder = 'Select an option', searchable = false, disabled = false, label, helperText, error, loading = false, clearable = false, creatable = false, onCreateOption, virtualized = false, virtualHeight = '300px', virtualItemHeight = 42, size = 'md', mobileMode = 'auto', } = props;
|
|
409
769
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
410
770
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
411
771
|
const [scrollTop, setScrollTop] = React.useState(0);
|
|
@@ -413,7 +773,15 @@ const Select = React.forwardRef((props, ref) => {
|
|
|
413
773
|
const selectRef = React.useRef(null);
|
|
414
774
|
const buttonRef = React.useRef(null);
|
|
415
775
|
const searchInputRef = React.useRef(null);
|
|
776
|
+
const mobileSearchInputRef = React.useRef(null);
|
|
416
777
|
const listRef = React.useRef(null);
|
|
778
|
+
const nativeSelectRef = React.useRef(null);
|
|
779
|
+
// Detect mobile viewport
|
|
780
|
+
const isMobile = useIsMobile();
|
|
781
|
+
const useMobileSheet = mobileMode === 'auto' && isMobile;
|
|
782
|
+
const useNativeSelect = mobileMode === 'native' && isMobile;
|
|
783
|
+
// Auto-size for mobile
|
|
784
|
+
const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
|
|
417
785
|
// Generate unique IDs for ARIA
|
|
418
786
|
const labelId = React.useId();
|
|
419
787
|
const listboxId = React.useId();
|
|
@@ -486,8 +854,10 @@ const Select = React.forwardRef((props, ref) => {
|
|
|
486
854
|
setSearchQuery('');
|
|
487
855
|
setIsOpen(false);
|
|
488
856
|
};
|
|
489
|
-
// Handle click outside
|
|
857
|
+
// Handle click outside (desktop dropdown only)
|
|
490
858
|
React.useEffect(() => {
|
|
859
|
+
if (useMobileSheet)
|
|
860
|
+
return; // Mobile sheet handles its own closing
|
|
491
861
|
const handleClickOutside = (event) => {
|
|
492
862
|
if (selectRef.current && !selectRef.current.contains(event.target)) {
|
|
493
863
|
setIsOpen(false);
|
|
@@ -500,50 +870,105 @@ const Select = React.forwardRef((props, ref) => {
|
|
|
500
870
|
return () => {
|
|
501
871
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
502
872
|
};
|
|
503
|
-
}, [isOpen]);
|
|
873
|
+
}, [isOpen, useMobileSheet]);
|
|
504
874
|
// Focus search input when opened
|
|
505
875
|
React.useEffect(() => {
|
|
506
|
-
if (isOpen && searchable
|
|
507
|
-
|
|
876
|
+
if (isOpen && searchable) {
|
|
877
|
+
if (useMobileSheet && mobileSearchInputRef.current) {
|
|
878
|
+
// Slight delay for mobile sheet animation
|
|
879
|
+
setTimeout(() => mobileSearchInputRef.current?.focus(), 100);
|
|
880
|
+
}
|
|
881
|
+
else if (searchInputRef.current) {
|
|
882
|
+
searchInputRef.current.focus();
|
|
883
|
+
}
|
|
508
884
|
}
|
|
509
|
-
}, [isOpen, searchable]);
|
|
885
|
+
}, [isOpen, searchable, useMobileSheet]);
|
|
886
|
+
// Lock body scroll when mobile sheet is open
|
|
887
|
+
React.useEffect(() => {
|
|
888
|
+
if (useMobileSheet && isOpen) {
|
|
889
|
+
document.body.style.overflow = 'hidden';
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
document.body.style.overflow = '';
|
|
893
|
+
}
|
|
894
|
+
return () => {
|
|
895
|
+
document.body.style.overflow = '';
|
|
896
|
+
};
|
|
897
|
+
}, [isOpen, useMobileSheet]);
|
|
898
|
+
// Handle escape key for mobile sheet
|
|
899
|
+
React.useEffect(() => {
|
|
900
|
+
if (!useMobileSheet || !isOpen)
|
|
901
|
+
return;
|
|
902
|
+
const handleEscape = (e) => {
|
|
903
|
+
if (e.key === 'Escape') {
|
|
904
|
+
setIsOpen(false);
|
|
905
|
+
setSearchQuery('');
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
document.addEventListener('keydown', handleEscape);
|
|
909
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
910
|
+
}, [isOpen, useMobileSheet]);
|
|
510
911
|
const handleSelect = (optionValue) => {
|
|
511
912
|
onChange?.(optionValue);
|
|
512
913
|
setIsOpen(false);
|
|
513
914
|
setSearchQuery('');
|
|
514
915
|
};
|
|
916
|
+
const handleClose = () => {
|
|
917
|
+
setIsOpen(false);
|
|
918
|
+
setSearchQuery('');
|
|
919
|
+
};
|
|
920
|
+
// Render option button (shared between desktop and mobile)
|
|
921
|
+
const renderOption = (option, isSelected, mobile = false) => (jsxRuntime.jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
|
|
922
|
+
w-full flex items-center justify-between px-4 transition-colors
|
|
923
|
+
${mobile ? optionSizeClasses.lg : optionSizeClasses[effectiveSize]}
|
|
924
|
+
${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
|
|
925
|
+
${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 active:bg-paper-100 cursor-pointer'}
|
|
926
|
+
`, role: "option", "aria-selected": isSelected, children: [jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsxRuntime.jsx("span", { children: option.icon }), option.label] }), isSelected && jsxRuntime.jsx(lucideReact.Check, { className: `${mobile ? 'h-5 w-5' : 'h-4 w-4'} text-accent-600` })] }, option.value));
|
|
927
|
+
// Render options list content (shared between desktop and mobile)
|
|
928
|
+
const renderOptionsContent = (mobile = false) => {
|
|
929
|
+
if (loading) {
|
|
930
|
+
return (jsxRuntime.jsxs("div", { className: "px-4 py-8 flex items-center justify-center", role: "status", "aria-live": "polite", children: [jsxRuntime.jsx(lucideReact.Loader2, { className: "h-5 w-5 animate-spin text-ink-500" }), jsxRuntime.jsx("span", { className: "ml-2 text-sm text-ink-500", children: "Loading..." })] }));
|
|
931
|
+
}
|
|
932
|
+
if (filteredOptions.length === 0 && filteredGroups.length === 0 && !showCreateOption) {
|
|
933
|
+
return (jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-ink-500 text-center", role: "status", "aria-live": "polite", children: "No options found" }));
|
|
934
|
+
}
|
|
935
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [showCreateOption && (jsxRuntime.jsx("button", { type: "button", onClick: handleCreateOption, className: `
|
|
936
|
+
w-full flex items-center px-4 text-accent-700 hover:bg-accent-50 transition-colors border-b border-paper-200
|
|
937
|
+
${mobile ? 'py-3.5 text-base' : 'py-2.5 text-sm'}
|
|
938
|
+
`, children: jsxRuntime.jsxs("span", { className: "font-medium", children: ["Create \"", searchQuery, "\""] }) })), useVirtualScrolling ? (jsxRuntime.jsx("div", { style: { height: totalHeight, position: 'relative' }, children: jsxRuntime.jsx("div", { style: { transform: `translateY(${offsetY}px)` }, children: visibleItems.map((item) => {
|
|
939
|
+
const option = item.option;
|
|
940
|
+
const isSelected = option.value === value;
|
|
941
|
+
const key = `${item.type}-${item.groupIndex}-${item.optionIndex}-${option.value}`;
|
|
942
|
+
return (jsxRuntime.jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: { height: mobile ? '56px' : `${virtualItemHeight}px` }, className: `
|
|
943
|
+
w-full flex items-center justify-between px-4 transition-colors
|
|
944
|
+
${mobile ? 'text-base' : 'text-sm'}
|
|
945
|
+
${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
|
|
946
|
+
${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
|
|
947
|
+
`, role: "option", "aria-selected": isSelected, children: [jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsxRuntime.jsx("span", { children: option.icon }), option.label] }), isSelected && jsxRuntime.jsx(lucideReact.Check, { className: `${mobile ? 'h-5 w-5' : 'h-4 w-4'} text-accent-600` })] }, key));
|
|
948
|
+
}) }) })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [filteredOptions.map((option) => renderOption(option, option.value === value, mobile)), filteredGroups.map((group) => (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: `
|
|
949
|
+
px-4 font-semibold text-ink-500 uppercase tracking-wider bg-paper-50 border-t border-b border-paper-200
|
|
950
|
+
${mobile ? 'py-2.5 text-xs' : 'py-2 text-xs'}
|
|
951
|
+
`, children: group.label }), group.options.map((option) => renderOption(option, option.value === value, mobile))] }, group.label)))] }))] }));
|
|
952
|
+
};
|
|
953
|
+
// Native select for mobile (optional)
|
|
954
|
+
if (useNativeSelect) {
|
|
955
|
+
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsx("label", { id: labelId, className: "label", children: label })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsxs("select", { ref: nativeSelectRef, value: value || '', onChange: (e) => onChange?.(e.target.value), disabled: disabled, className: `
|
|
956
|
+
input w-full appearance-none pr-10
|
|
957
|
+
${sizeClasses$a[effectiveSize]}
|
|
958
|
+
${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
|
|
959
|
+
${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
|
960
|
+
`, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), children: [jsxRuntime.jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsxRuntime.jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsxRuntime.jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsxRuntime.jsx(lucideReact.ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
|
|
961
|
+
}
|
|
515
962
|
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsx("label", { id: labelId, className: "label", children: label })), jsxRuntime.jsxs("div", { ref: selectRef, className: "relative", children: [jsxRuntime.jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
|
|
516
|
-
input w-full flex items-center justify-between
|
|
963
|
+
input w-full flex items-center justify-between px-3
|
|
964
|
+
${sizeClasses$a[effectiveSize]}
|
|
517
965
|
${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
|
|
518
966
|
${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
|
519
967
|
`, role: "combobox", "aria-haspopup": "listbox", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? placeholder : undefined, "aria-activedescendant": activeDescendant, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), "aria-disabled": disabled, children: [jsxRuntime.jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? 'text-ink-800' : 'text-ink-400'}`, children: [loading && jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin text-ink-500" }), !loading && selectedOption?.icon && jsxRuntime.jsx("span", { children: selectedOption.icon }), selectedOption ? selectedOption.label : placeholder] }), jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
|
|
520
968
|
e.stopPropagation();
|
|
521
969
|
onChange?.('');
|
|
522
970
|
setIsOpen(false);
|
|
523
|
-
}, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsxRuntime.jsx(lucideReact.X, { className:
|
|
524
|
-
const option = item.option;
|
|
525
|
-
const isSelected = option.value === value;
|
|
526
|
-
const key = `${item.type}-${item.groupIndex}-${item.optionIndex}-${option.value}`;
|
|
527
|
-
return (jsxRuntime.jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: { height: `${virtualItemHeight}px` }, className: `
|
|
528
|
-
w-full flex items-center justify-between px-4 text-sm transition-colors
|
|
529
|
-
${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
|
|
530
|
-
${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
|
|
531
|
-
`, role: "option", "aria-selected": isSelected, children: [jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsxRuntime.jsx("span", { children: option.icon }), option.label] }), isSelected && jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 text-accent-600" })] }, key));
|
|
532
|
-
}) }) })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [filteredOptions.map((option) => {
|
|
533
|
-
const isSelected = option.value === value;
|
|
534
|
-
return (jsxRuntime.jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
|
|
535
|
-
w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors
|
|
536
|
-
${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
|
|
537
|
-
${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
|
|
538
|
-
`, role: "option", "aria-selected": isSelected, children: [jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsxRuntime.jsx("span", { children: option.icon }), option.label] }), isSelected && jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 text-accent-600" })] }, option.value));
|
|
539
|
-
}), filteredGroups.map((group) => (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsx("div", { className: "px-4 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wider bg-paper-50 border-t border-b border-paper-200", children: group.label }), group.options.map((option) => {
|
|
540
|
-
const isSelected = option.value === value;
|
|
541
|
-
return (jsxRuntime.jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
|
|
542
|
-
w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors
|
|
543
|
-
${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
|
|
544
|
-
${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
|
|
545
|
-
`, role: "option", "aria-selected": isSelected, children: [jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsxRuntime.jsx("span", { children: option.icon }), option.label] }), isSelected && jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4 text-accent-600" })] }, option.value));
|
|
546
|
-
})] }, group.label)))] }))] })) })] }))] }), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
|
|
971
|
+
}, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsxRuntime.jsx(lucideReact.X, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'}` }) })), jsxRuntime.jsx(lucideReact.ChevronDown, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }), isOpen && !useMobileSheet && (jsxRuntime.jsxs("div", { className: "absolute z-50 w-full mt-2 bg-white bg-subtle-grain rounded-lg shadow-lg border border-paper-200 max-h-60 overflow-hidden animate-fade-in", children: [searchable && (jsxRuntime.jsx("div", { className: "p-2 border-b border-paper-200", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(lucideReact.Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), jsxRuntime.jsx("input", { ref: searchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", className: "w-full pl-9 pr-3 py-2 text-sm border border-paper-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options", "aria-autocomplete": "list", "aria-controls": listboxId })] }) })), jsxRuntime.jsx("div", { ref: listRef, id: listboxId, className: "overflow-y-auto", style: { maxHeight: useVirtualScrolling ? virtualHeight : '12rem' }, onScroll: (e) => useVirtualScrolling && setScrollTop(e.currentTarget.scrollTop), role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(false) })] }))] }), isOpen && useMobileSheet && reactDom.createPortal(jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: (e) => e.target === e.currentTarget && handleClose(), role: "dialog", "aria-modal": "true", "aria-labelledby": label ? `mobile-${labelId}` : undefined, children: [jsxRuntime.jsx("div", { className: "absolute inset-0 bg-black/50 animate-fade-in" }), jsxRuntime.jsxs("div", { className: "relative w-full bg-white rounded-t-2xl shadow-2xl animate-slide-up max-h-[85vh] flex flex-col", style: { paddingBottom: 'env(safe-area-inset-bottom)' }, children: [jsxRuntime.jsx("div", { className: "py-3 cursor-grab", children: jsxRuntime.jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) }), jsxRuntime.jsxs("div", { className: "px-4 pb-3 border-b border-paper-200 flex items-center justify-between", children: [label && (jsxRuntime.jsx("h2", { id: `mobile-${labelId}`, className: "text-lg font-semibold text-ink-900", children: label })), !label && (jsxRuntime.jsx("h2", { className: "text-lg font-semibold text-ink-900", children: placeholder })), jsxRuntime.jsx("button", { onClick: handleClose, className: "text-ink-400 hover:text-ink-600 transition-colors p-2 -mr-2", "aria-label": "Close", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) })] }), searchable && (jsxRuntime.jsx("div", { className: "p-3 border-b border-paper-200", children: jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx(lucideReact.Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-400" }), jsxRuntime.jsx("input", { ref: mobileSearchInputRef, type: "text", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: "Search...", inputMode: "search", enterKeyHint: "search", className: "w-full pl-12 pr-4 py-3 text-base border border-paper-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent-400 focus:border-accent-400", role: "searchbox", "aria-label": "Search options" })] }) })), jsxRuntime.jsx("div", { id: listboxId, className: "overflow-y-auto flex-1", role: "listbox", "aria-label": "Available options", "aria-multiselectable": "false", children: renderOptionsContent(true) })] })] }), document.body), error && (jsxRuntime.jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsxRuntime.jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
|
|
547
972
|
});
|
|
548
973
|
Select.displayName = 'Select';
|
|
549
974
|
|
|
@@ -629,6 +1054,9 @@ const Switch = React.forwardRef(({ checked, onChange, label, description, disabl
|
|
|
629
1054
|
const switchId = React.useId();
|
|
630
1055
|
const labelId = label ? `${switchId}-label` : undefined;
|
|
631
1056
|
const descId = description ? `${switchId}-desc` : undefined;
|
|
1057
|
+
// Auto-size for mobile
|
|
1058
|
+
const isMobile = useIsMobile();
|
|
1059
|
+
const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
|
|
632
1060
|
const sizeStyles = {
|
|
633
1061
|
sm: {
|
|
634
1062
|
switch: 'w-9 h-5',
|
|
@@ -649,14 +1077,16 @@ const Switch = React.forwardRef(({ checked, onChange, label, description, disabl
|
|
|
649
1077
|
spinner: 'h-5 w-5',
|
|
650
1078
|
},
|
|
651
1079
|
};
|
|
652
|
-
const styles = sizeStyles[
|
|
1080
|
+
const styles = sizeStyles[effectiveSize];
|
|
653
1081
|
const isDisabled = disabled || loading;
|
|
654
1082
|
const handleChange = () => {
|
|
655
1083
|
if (!isDisabled) {
|
|
656
1084
|
onChange(!checked);
|
|
657
1085
|
}
|
|
658
1086
|
};
|
|
659
|
-
|
|
1087
|
+
// Touch target padding for mobile
|
|
1088
|
+
const touchTargetClass = effectiveSize === 'lg' ? 'min-h-touch py-1' : '';
|
|
1089
|
+
return (jsxRuntime.jsxs("label", { htmlFor: switchId, className: `flex items-center gap-3 ${touchTargetClass} ${isDisabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`, children: [jsxRuntime.jsxs("div", { className: "relative inline-block flex-shrink-0", children: [jsxRuntime.jsx("input", { ref: ref, id: switchId, type: "checkbox", role: "switch", checked: checked, onChange: handleChange, disabled: isDisabled, "aria-checked": checked, "aria-labelledby": labelId, "aria-describedby": descId, "aria-disabled": isDisabled, "aria-busy": loading, className: "sr-only" }), jsxRuntime.jsx("div", { className: `
|
|
660
1090
|
${styles.switch}
|
|
661
1091
|
rounded-full transition-all duration-200
|
|
662
1092
|
${checked ? 'bg-accent-500' : 'bg-paper-300'}
|
|
@@ -665,11 +1095,20 @@ const Switch = React.forwardRef(({ checked, onChange, label, description, disabl
|
|
|
665
1095
|
${styles.slider}
|
|
666
1096
|
absolute left-0.5 top-0.5 bg-white rounded-full shadow-sm transition-transform duration-200 flex items-center justify-center
|
|
667
1097
|
${checked ? styles.translate : ''}
|
|
668
|
-
`, children: loading && jsxRuntime.jsx(lucideReact.Loader2, { className: `${styles.spinner} animate-spin text-accent-600` }) }) })] }), (label || description) && (jsxRuntime.jsxs("div", { className: "flex-1", children: [label && jsxRuntime.jsx("p", { id: labelId, className:
|
|
1098
|
+
`, children: loading && jsxRuntime.jsx(lucideReact.Loader2, { className: `${styles.spinner} animate-spin text-accent-600` }) }) })] }), (label || description) && (jsxRuntime.jsxs("div", { className: "flex-1", children: [label && jsxRuntime.jsx("p", { id: labelId, className: `${effectiveSize === 'lg' ? 'text-base' : 'text-sm'} font-medium text-ink-900`, children: label }), description && jsxRuntime.jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description })] }))] }));
|
|
669
1099
|
});
|
|
670
1100
|
Switch.displayName = 'Switch';
|
|
671
1101
|
|
|
672
|
-
|
|
1102
|
+
// Size classes for textarea
|
|
1103
|
+
const sizeClasses$9 = {
|
|
1104
|
+
sm: 'px-3 py-2 text-sm',
|
|
1105
|
+
md: 'px-4 py-3 text-sm',
|
|
1106
|
+
lg: 'px-4 py-3.5 text-base', // Larger padding and text for mobile
|
|
1107
|
+
};
|
|
1108
|
+
const Textarea = React.forwardRef(({ label, helperText, validationState, validationMessage, maxLength, showCharCount = false, autoExpand = false, minRows = 2, maxRows = 10, resize = 'vertical', loading = false, size = 'md', enterKeyHint, className = '', id, value, rows = 4, ...props }, ref) => {
|
|
1109
|
+
// Detect mobile and auto-size
|
|
1110
|
+
const isMobile = useIsMobile();
|
|
1111
|
+
const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
|
|
673
1112
|
const textareaId = id || `textarea-${Math.random().toString(36).substring(2, 9)}`;
|
|
674
1113
|
const currentLength = typeof value === 'string' ? value.length : 0;
|
|
675
1114
|
const internalRef = React.useRef(null);
|
|
@@ -744,11 +1183,12 @@ const Textarea = React.forwardRef(({ label, helperText, validationState, validat
|
|
|
744
1183
|
return 'text-ink-600';
|
|
745
1184
|
}
|
|
746
1185
|
};
|
|
747
|
-
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: textareaId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("textarea", { ref: textareaRef, id: textareaId, value: value, maxLength: maxLength, rows: autoExpand ? minRows : rows, className: `
|
|
748
|
-
block w-full
|
|
1186
|
+
return (jsxRuntime.jsxs("div", { className: "w-full", children: [label && (jsxRuntime.jsxs("label", { htmlFor: textareaId, className: "label", children: [label, props.required && jsxRuntime.jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("textarea", { ref: textareaRef, id: textareaId, value: value, maxLength: maxLength, rows: autoExpand ? minRows : rows, enterKeyHint: enterKeyHint, className: `
|
|
1187
|
+
block w-full border rounded-lg text-ink-800 placeholder-ink-400
|
|
749
1188
|
bg-white bg-subtle-grain transition-all duration-200
|
|
750
1189
|
focus:outline-none focus:ring-2 ${getResizeClass()}
|
|
751
1190
|
disabled:bg-paper-100 disabled:text-ink-400 disabled:cursor-not-allowed disabled:opacity-60
|
|
1191
|
+
${sizeClasses$9[effectiveSize]}
|
|
752
1192
|
${getValidationClasses()}
|
|
753
1193
|
${loading ? 'pr-10' : ''}
|
|
754
1194
|
${className}
|
|
@@ -756,10 +1196,20 @@ const Textarea = React.forwardRef(({ label, helperText, validationState, validat
|
|
|
756
1196
|
});
|
|
757
1197
|
Textarea.displayName = 'Textarea';
|
|
758
1198
|
|
|
759
|
-
|
|
1199
|
+
// Size classes for checkbox box and touch target
|
|
1200
|
+
const sizeConfig$4 = {
|
|
1201
|
+
sm: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-2' },
|
|
1202
|
+
md: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-3' },
|
|
1203
|
+
lg: { box: 'w-5 h-5', icon: 'h-4 w-4', text: 'text-base', gap: 'gap-3', touchTarget: 'min-h-touch py-2' }, // 44px touch target
|
|
1204
|
+
};
|
|
1205
|
+
const Checkbox = React.forwardRef(({ checked, onChange, label, description, disabled = false, indeterminate = false, className = '', id, name, icon, size = 'md', }, ref) => {
|
|
760
1206
|
const generatedId = React.useId();
|
|
761
1207
|
const checkboxId = id || generatedId;
|
|
762
1208
|
const descId = description ? `${checkboxId}-desc` : undefined;
|
|
1209
|
+
// Auto-size for mobile
|
|
1210
|
+
const isMobile = useIsMobile();
|
|
1211
|
+
const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
|
|
1212
|
+
const config = sizeConfig$4[effectiveSize];
|
|
763
1213
|
const handleChange = () => {
|
|
764
1214
|
if (!disabled) {
|
|
765
1215
|
onChange(!checked);
|
|
@@ -771,14 +1221,14 @@ const Checkbox = React.forwardRef(({ checked, onChange, label, description, disa
|
|
|
771
1221
|
onChange(!checked);
|
|
772
1222
|
}
|
|
773
1223
|
};
|
|
774
|
-
return (jsxRuntime.jsxs("label", { htmlFor: checkboxId, className: `flex items-start gap
|
|
775
|
-
|
|
1224
|
+
return (jsxRuntime.jsxs("label", { htmlFor: checkboxId, className: `flex items-start ${config.gap} ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} ${'touchTarget' in config ? config.touchTarget : ''} ${className}`, children: [jsxRuntime.jsxs("div", { className: "relative inline-block flex-shrink-0 mt-0.5", children: [jsxRuntime.jsx("input", { ref: ref, type: "checkbox", id: checkboxId, name: name, checked: checked, onChange: handleChange, onKeyDown: handleKeyDown, disabled: disabled, className: "sr-only", "aria-checked": indeterminate ? 'mixed' : checked, "aria-describedby": descId, "aria-disabled": disabled }), jsxRuntime.jsx("div", { className: `
|
|
1225
|
+
${config.box} rounded border transition-all duration-200
|
|
776
1226
|
flex items-center justify-center
|
|
777
1227
|
${checked || indeterminate
|
|
778
1228
|
? 'bg-accent-600 border-accent-600'
|
|
779
1229
|
: 'bg-white border-paper-300 hover:border-paper-400'}
|
|
780
1230
|
${!disabled && 'focus-within:ring-2 focus-within:ring-accent-400 focus-within:ring-offset-2'}
|
|
781
|
-
`, children: indeterminate ? (jsxRuntime.jsx(lucideReact.Minus, { className:
|
|
1231
|
+
`, children: indeterminate ? (jsxRuntime.jsx(lucideReact.Minus, { className: `${config.icon} text-white` })) : checked ? (jsxRuntime.jsx(lucideReact.Check, { className: `${config.icon} text-white` })) : null })] }), (label || description) && (jsxRuntime.jsxs("div", { className: "flex-1", children: [label && (jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [icon && jsxRuntime.jsx("span", { className: "text-ink-700", children: icon }), jsxRuntime.jsx("p", { className: `${config.text} font-medium text-ink-900`, children: label })] })), description && (jsxRuntime.jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description }))] }))] }));
|
|
782
1232
|
});
|
|
783
1233
|
Checkbox.displayName = 'Checkbox';
|
|
784
1234
|
|
|
@@ -1958,7 +2408,7 @@ function Loading({ variant = 'spinner', size = 'md', text }) {
|
|
|
1958
2408
|
function Skeleton({ className = '', ...props }) {
|
|
1959
2409
|
return (jsxRuntime.jsx("div", { className: `animate-pulse bg-paper-200 rounded ${className}`, ...props }));
|
|
1960
2410
|
}
|
|
1961
|
-
function SkeletonCard() {
|
|
2411
|
+
function SkeletonCard$1() {
|
|
1962
2412
|
return (jsxRuntime.jsxs("div", { className: "card", children: [jsxRuntime.jsx(Skeleton, { className: "h-6 w-3/4 mb-4" }), jsxRuntime.jsx(Skeleton, { className: "h-4 w-full mb-2" }), jsxRuntime.jsx(Skeleton, { className: "h-4 w-5/6 mb-2" }), jsxRuntime.jsx(Skeleton, { className: "h-4 w-4/6" })] }));
|
|
1963
2413
|
}
|
|
1964
2414
|
function SkeletonTable({ rows = 5, columns = 4 }) {
|
|
@@ -2610,19 +3060,167 @@ function Alert({ variant = 'info', title, children, onClose, className = '', act
|
|
|
2610
3060
|
return (jsxRuntime.jsx("div", { className: `rounded-lg border p-4 ${styles.container} ${className}`, role: "alert", children: jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [jsxRuntime.jsx("div", { className: "flex-shrink-0 mt-0.5", children: styles.icon }), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [title && jsxRuntime.jsx("h4", { className: "text-sm font-medium mb-1", children: title }), jsxRuntime.jsx("div", { className: "text-sm", children: children }), actions.length > 0 && (jsxRuntime.jsx("div", { className: "flex gap-2 mt-3", children: actions.map((action, index) => (jsxRuntime.jsx("button", { onClick: action.onClick, className: getButtonStyles(action.variant), children: action.label }, index))) }))] }), onClose && (jsxRuntime.jsx("button", { onClick: onClose, className: "flex-shrink-0 text-current opacity-70 hover:opacity-100 transition-opacity", "aria-label": "Close alert", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) }))] }) }));
|
|
2611
3061
|
}
|
|
2612
3062
|
|
|
2613
|
-
const
|
|
3063
|
+
const heightPresets = {
|
|
3064
|
+
sm: '33vh',
|
|
3065
|
+
md: '50vh',
|
|
3066
|
+
lg: '75vh',
|
|
3067
|
+
full: '90vh',
|
|
3068
|
+
};
|
|
3069
|
+
function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHandle = true, showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className = '', }) {
|
|
3070
|
+
const titleId = React.useId();
|
|
3071
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
3072
|
+
const [dragOffset, setDragOffset] = React.useState(0);
|
|
3073
|
+
const [currentHeight] = React.useState(typeof height === 'string' && height in heightPresets
|
|
3074
|
+
? heightPresets[height]
|
|
3075
|
+
: height);
|
|
3076
|
+
const sheetRef = React.useRef(null);
|
|
3077
|
+
const startYRef = React.useRef(0);
|
|
3078
|
+
// Close on Escape
|
|
3079
|
+
React.useEffect(() => {
|
|
3080
|
+
if (!isOpen || !closeOnEscape)
|
|
3081
|
+
return;
|
|
3082
|
+
const handleEscape = (e) => {
|
|
3083
|
+
if (e.key === 'Escape') {
|
|
3084
|
+
onClose();
|
|
3085
|
+
}
|
|
3086
|
+
};
|
|
3087
|
+
document.addEventListener('keydown', handleEscape);
|
|
3088
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
3089
|
+
}, [isOpen, closeOnEscape, onClose]);
|
|
3090
|
+
// Prevent body scroll when open
|
|
3091
|
+
React.useEffect(() => {
|
|
3092
|
+
if (isOpen) {
|
|
3093
|
+
document.body.style.overflow = 'hidden';
|
|
3094
|
+
}
|
|
3095
|
+
else {
|
|
3096
|
+
document.body.style.overflow = '';
|
|
3097
|
+
}
|
|
3098
|
+
return () => {
|
|
3099
|
+
document.body.style.overflow = '';
|
|
3100
|
+
};
|
|
3101
|
+
}, [isOpen]);
|
|
3102
|
+
const handleOverlayClick = (e) => {
|
|
3103
|
+
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
3104
|
+
onClose();
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
const handleDragStart = (e) => {
|
|
3108
|
+
setIsDragging(true);
|
|
3109
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
3110
|
+
startYRef.current = clientY;
|
|
3111
|
+
};
|
|
3112
|
+
const handleDragMove = (e) => {
|
|
3113
|
+
if (!isDragging)
|
|
3114
|
+
return;
|
|
3115
|
+
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
3116
|
+
const offset = clientY - startYRef.current;
|
|
3117
|
+
// Only allow dragging down
|
|
3118
|
+
if (offset > 0) {
|
|
3119
|
+
setDragOffset(offset);
|
|
3120
|
+
}
|
|
3121
|
+
};
|
|
3122
|
+
const handleDragEnd = () => {
|
|
3123
|
+
setIsDragging(false);
|
|
3124
|
+
// Close if dragged down more than 150px
|
|
3125
|
+
if (dragOffset > 150) {
|
|
3126
|
+
onClose();
|
|
3127
|
+
}
|
|
3128
|
+
setDragOffset(0);
|
|
3129
|
+
};
|
|
3130
|
+
React.useEffect(() => {
|
|
3131
|
+
if (!isDragging)
|
|
3132
|
+
return;
|
|
3133
|
+
const handleMove = (e) => handleDragMove(e);
|
|
3134
|
+
const handleEnd = () => handleDragEnd();
|
|
3135
|
+
document.addEventListener('touchmove', handleMove);
|
|
3136
|
+
document.addEventListener('mousemove', handleMove);
|
|
3137
|
+
document.addEventListener('touchend', handleEnd);
|
|
3138
|
+
document.addEventListener('mouseup', handleEnd);
|
|
3139
|
+
return () => {
|
|
3140
|
+
document.removeEventListener('touchmove', handleMove);
|
|
3141
|
+
document.removeEventListener('mousemove', handleMove);
|
|
3142
|
+
document.removeEventListener('touchend', handleEnd);
|
|
3143
|
+
document.removeEventListener('mouseup', handleEnd);
|
|
3144
|
+
};
|
|
3145
|
+
}, [isDragging, dragOffset]);
|
|
3146
|
+
if (!isOpen)
|
|
3147
|
+
return null;
|
|
3148
|
+
return (jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: handleOverlayClick, children: [jsxRuntime.jsx("div", { className: `
|
|
3149
|
+
absolute inset-0 bg-black/50 transition-opacity duration-300
|
|
3150
|
+
${isOpen ? 'opacity-100' : 'opacity-0'}
|
|
3151
|
+
` }), jsxRuntime.jsxs("div", { ref: sheetRef, className: `
|
|
3152
|
+
relative w-full bg-white rounded-t-2xl shadow-2xl
|
|
3153
|
+
transition-transform duration-300 ease-out
|
|
3154
|
+
${isOpen ? 'translate-y-0' : 'translate-y-full'}
|
|
3155
|
+
${className}
|
|
3156
|
+
`, style: {
|
|
3157
|
+
height: currentHeight,
|
|
3158
|
+
transform: `translateY(${dragOffset}px)`,
|
|
3159
|
+
}, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, children: [showHandle && (jsxRuntime.jsx("div", { className: "py-3 cursor-grab active:cursor-grabbing", onTouchStart: handleDragStart, onMouseDown: handleDragStart, children: jsxRuntime.jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) })), (title || showCloseButton) && (jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-ink-200 flex items-center justify-between", children: [title && (jsxRuntime.jsx("h2", { id: titleId, className: "text-lg font-semibold text-ink-900", children: title })), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors ml-auto", "aria-label": "Close", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] })), jsxRuntime.jsx("div", { className: "overflow-y-auto flex-1 p-6", children: children })] })] }));
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const sizeClasses$8 = {
|
|
2614
3163
|
sm: 'max-w-md',
|
|
2615
3164
|
md: 'max-w-lg',
|
|
2616
3165
|
lg: 'max-w-2xl',
|
|
2617
3166
|
xl: 'max-w-4xl',
|
|
2618
3167
|
full: 'max-w-7xl',
|
|
2619
3168
|
};
|
|
2620
|
-
|
|
3169
|
+
/**
|
|
3170
|
+
* Modal - Adaptive dialog component
|
|
3171
|
+
*
|
|
3172
|
+
* On desktop, renders as a centered modal dialog.
|
|
3173
|
+
* On mobile (when mobileMode='auto'), automatically renders as a BottomSheet
|
|
3174
|
+
* for better touch interaction and visibility.
|
|
3175
|
+
*
|
|
3176
|
+
* @example Basic modal
|
|
3177
|
+
* ```tsx
|
|
3178
|
+
* <Modal isOpen={isOpen} onClose={handleClose} title="Edit User">
|
|
3179
|
+
* <form>...</form>
|
|
3180
|
+
* <ModalFooter>
|
|
3181
|
+
* <Button onClick={handleClose}>Cancel</Button>
|
|
3182
|
+
* <Button variant="primary" onClick={handleSave}>Save</Button>
|
|
3183
|
+
* </ModalFooter>
|
|
3184
|
+
* </Modal>
|
|
3185
|
+
* ```
|
|
3186
|
+
*
|
|
3187
|
+
* @example Force modal on mobile
|
|
3188
|
+
* ```tsx
|
|
3189
|
+
* <Modal
|
|
3190
|
+
* isOpen={isOpen}
|
|
3191
|
+
* onClose={handleClose}
|
|
3192
|
+
* title="Settings"
|
|
3193
|
+
* mobileMode="modal"
|
|
3194
|
+
* >
|
|
3195
|
+
* ...
|
|
3196
|
+
* </Modal>
|
|
3197
|
+
* ```
|
|
3198
|
+
*
|
|
3199
|
+
* @example Always use BottomSheet
|
|
3200
|
+
* ```tsx
|
|
3201
|
+
* <Modal
|
|
3202
|
+
* isOpen={isOpen}
|
|
3203
|
+
* onClose={handleClose}
|
|
3204
|
+
* title="Select Option"
|
|
3205
|
+
* mobileMode="sheet"
|
|
3206
|
+
* mobileHeight="md"
|
|
3207
|
+
* >
|
|
3208
|
+
* ...
|
|
3209
|
+
* </Modal>
|
|
3210
|
+
* ```
|
|
3211
|
+
*/
|
|
3212
|
+
function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
|
|
2621
3213
|
const modalRef = React.useRef(null);
|
|
2622
3214
|
const mouseDownOnBackdrop = React.useRef(false);
|
|
2623
3215
|
const titleId = React.useId();
|
|
2624
|
-
|
|
3216
|
+
const isMobile = useIsMobile();
|
|
3217
|
+
// Determine if we should use BottomSheet
|
|
3218
|
+
const useBottomSheet = mobileMode === 'sheet' ||
|
|
3219
|
+
(mobileMode === 'auto' && isMobile);
|
|
3220
|
+
// Handle escape key (only for modal mode, BottomSheet handles its own)
|
|
2625
3221
|
React.useEffect(() => {
|
|
3222
|
+
if (useBottomSheet)
|
|
3223
|
+
return; // BottomSheet handles escape
|
|
2626
3224
|
const handleEscape = (e) => {
|
|
2627
3225
|
if (e.key === 'Escape' && isOpen) {
|
|
2628
3226
|
onClose();
|
|
@@ -2636,7 +3234,7 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
|
|
|
2636
3234
|
document.removeEventListener('keydown', handleEscape);
|
|
2637
3235
|
document.body.style.overflow = 'unset';
|
|
2638
3236
|
};
|
|
2639
|
-
}, [isOpen, onClose]);
|
|
3237
|
+
}, [isOpen, onClose, useBottomSheet]);
|
|
2640
3238
|
// Track if mousedown originated on the backdrop
|
|
2641
3239
|
const handleBackdropMouseDown = (e) => {
|
|
2642
3240
|
if (e.target === e.currentTarget) {
|
|
@@ -2672,13 +3270,18 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
|
|
|
2672
3270
|
};
|
|
2673
3271
|
if (!isOpen)
|
|
2674
3272
|
return null;
|
|
2675
|
-
|
|
3273
|
+
// Render as BottomSheet on mobile
|
|
3274
|
+
if (useBottomSheet) {
|
|
3275
|
+
return (jsxRuntime.jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }));
|
|
3276
|
+
}
|
|
3277
|
+
// Render as standard modal on desktop
|
|
3278
|
+
return (jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onMouseDown: handleBackdropMouseDown, onClick: handleBackdropClick, children: jsxRuntime.jsxs("div", { ref: modalRef, className: `${sizeClasses$8[size]} w-full bg-white bg-subtle-grain rounded-xl shadow-2xl border border-paper-200 ${getAnimationClass()}`, role: "dialog", "aria-modal": "true", "aria-labelledby": titleId, children: [jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsxRuntime.jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] }), jsxRuntime.jsx("div", { className: "px-6 py-4", children: children })] }) }));
|
|
2676
3279
|
}
|
|
2677
3280
|
function ModalFooter({ children }) {
|
|
2678
|
-
return (jsxRuntime.jsx("div", { className: "flex items-center justify-end gap-3 px-6 py-4 border-t border-paper-200 bg-paper-50", children: children }));
|
|
3281
|
+
return (jsxRuntime.jsx("div", { className: "flex items-center justify-end gap-3 px-6 py-4 border-t border-paper-200 bg-paper-50 -mx-6 -mb-4 mt-4 rounded-b-xl", children: children }));
|
|
2679
3282
|
}
|
|
2680
3283
|
|
|
2681
|
-
const sizeClasses$
|
|
3284
|
+
const sizeClasses$7 = {
|
|
2682
3285
|
left: {
|
|
2683
3286
|
sm: 'w-64',
|
|
2684
3287
|
md: 'w-96',
|
|
@@ -2757,7 +3360,7 @@ function Drawer({ isOpen, onClose, title, children, placement = 'right', size =
|
|
|
2757
3360
|
const isHorizontal = placement === 'left' || placement === 'right';
|
|
2758
3361
|
return (jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex", children: [showOverlay && (jsxRuntime.jsx("div", { className: "fixed inset-0 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onClick: handleOverlayClick, "aria-hidden": "true" })), jsxRuntime.jsxs("div", { className: `
|
|
2759
3362
|
fixed ${placementClasses[placement]}
|
|
2760
|
-
${sizeClasses$
|
|
3363
|
+
${sizeClasses$7[placement][size]}
|
|
2761
3364
|
bg-white border-paper-200 shadow-2xl
|
|
2762
3365
|
${isHorizontal ? 'border-r' : 'border-b'}
|
|
2763
3366
|
${animationClasses[placement].enter}
|
|
@@ -2789,14 +3392,49 @@ const variantStyles = {
|
|
|
2789
3392
|
button: 'bg-accent-600 hover:bg-accent-700 focus:ring-accent-500',
|
|
2790
3393
|
},
|
|
2791
3394
|
};
|
|
2792
|
-
|
|
3395
|
+
/**
|
|
3396
|
+
* ConfirmDialog - Confirmation dialog with mobile support
|
|
3397
|
+
*
|
|
3398
|
+
* @example Basic usage
|
|
3399
|
+
* ```tsx
|
|
3400
|
+
* <ConfirmDialog
|
|
3401
|
+
* isOpen={isOpen}
|
|
3402
|
+
* onClose={handleClose}
|
|
3403
|
+
* onConfirm={handleDelete}
|
|
3404
|
+
* title="Delete Item"
|
|
3405
|
+
* message="Are you sure you want to delete this item? This action cannot be undone."
|
|
3406
|
+
* variant="danger"
|
|
3407
|
+
* />
|
|
3408
|
+
* ```
|
|
3409
|
+
*
|
|
3410
|
+
* @example With useConfirmDialog hook
|
|
3411
|
+
* ```tsx
|
|
3412
|
+
* const confirmDialog = useConfirmDialog();
|
|
3413
|
+
*
|
|
3414
|
+
* const handleDelete = () => {
|
|
3415
|
+
* confirmDialog.show({
|
|
3416
|
+
* title: 'Delete Item',
|
|
3417
|
+
* message: 'Are you sure?',
|
|
3418
|
+
* onConfirm: async () => await deleteItem(),
|
|
3419
|
+
* });
|
|
3420
|
+
* };
|
|
3421
|
+
*
|
|
3422
|
+
* return (
|
|
3423
|
+
* <>
|
|
3424
|
+
* <button onClick={handleDelete}>Delete</button>
|
|
3425
|
+
* <ConfirmDialog {...confirmDialog.props} />
|
|
3426
|
+
* </>
|
|
3427
|
+
* );
|
|
3428
|
+
* ```
|
|
3429
|
+
*/
|
|
3430
|
+
function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, mobileMode = 'auto', mobileHeight = 'sm', }) {
|
|
2793
3431
|
const variantStyle = variantStyles[variant];
|
|
2794
3432
|
const IconComponent = icon || variantStyle.icon;
|
|
2795
3433
|
const handleConfirm = async () => {
|
|
2796
3434
|
await onConfirm();
|
|
2797
3435
|
// Note: onClose is called by useConfirmDialog hook after onConfirm completes
|
|
2798
3436
|
};
|
|
2799
|
-
return (jsxRuntime.jsxs(Modal, { isOpen: isOpen, onClose: onClose, title: typeof title === 'string' ? title : String(title), size: "sm", showCloseButton: false, children: [jsxRuntime.jsxs("div", { className: "flex items-start gap-4", children: [jsxRuntime.jsx("div", { className: `flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-full ${variantStyle.iconBg}`, children: typeof IconComponent === 'function' ? (jsxRuntime.jsx(IconComponent, { className: `h-6 w-6 ${variantStyle.iconColor}` })) : React.isValidElement(IconComponent) ? (IconComponent) : null }), jsxRuntime.jsx("div", { className: "flex-1 pt-1", children: jsxRuntime.jsx("p", { className: "text-sm text-ink-700 leading-relaxed whitespace-pre-line", children: typeof message === 'string' ? message : String(message) }) })] }), jsxRuntime.jsxs(ModalFooter, { children: [jsxRuntime.jsx("button", { onClick: onClose, disabled: isLoading, className: "px-4 py-2 text-sm font-medium text-ink-700 bg-white border border-paper-300 rounded-lg hover:bg-paper-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", children: typeof cancelLabel === 'string' ? cancelLabel : String(cancelLabel) }), jsxRuntime.jsx("button", { onClick: handleConfirm, disabled: isLoading, className: `px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${variantStyle.button}`, children: isLoading ? (jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("svg", { className: "animate-spin h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsxRuntime.jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsxRuntime.jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Processing..."] })) : (typeof confirmLabel === 'string' ? confirmLabel : String(confirmLabel)) })] })] }));
|
|
3437
|
+
return (jsxRuntime.jsxs(Modal, { isOpen: isOpen, onClose: onClose, title: typeof title === 'string' ? title : String(title), size: "sm", showCloseButton: false, mobileMode: mobileMode, mobileHeight: mobileHeight, mobileShowHandle: false, children: [jsxRuntime.jsxs("div", { className: "flex items-start gap-4", children: [jsxRuntime.jsx("div", { className: `flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-full ${variantStyle.iconBg}`, children: typeof IconComponent === 'function' ? (jsxRuntime.jsx(IconComponent, { className: `h-6 w-6 ${variantStyle.iconColor}` })) : React.isValidElement(IconComponent) ? (IconComponent) : null }), jsxRuntime.jsx("div", { className: "flex-1 pt-1", children: jsxRuntime.jsx("p", { className: "text-sm text-ink-700 leading-relaxed whitespace-pre-line", children: typeof message === 'string' ? message : String(message) }) })] }), jsxRuntime.jsxs(ModalFooter, { children: [jsxRuntime.jsx("button", { onClick: onClose, disabled: isLoading, className: "px-4 py-2 text-sm font-medium text-ink-700 bg-white border border-paper-300 rounded-lg hover:bg-paper-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-h-touch-sm", children: typeof cancelLabel === 'string' ? cancelLabel : String(cancelLabel) }), jsxRuntime.jsx("button", { onClick: handleConfirm, disabled: isLoading, className: `px-4 py-2 text-sm font-medium text-white rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors min-h-touch-sm ${variantStyle.button}`, children: isLoading ? (jsxRuntime.jsxs("span", { className: "flex items-center gap-2", children: [jsxRuntime.jsxs("svg", { className: "animate-spin h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsxRuntime.jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), jsxRuntime.jsx("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Processing..."] })) : (typeof confirmLabel === 'string' ? confirmLabel : String(confirmLabel)) })] })] }));
|
|
2800
3438
|
}
|
|
2801
3439
|
/**
|
|
2802
3440
|
* Hook for managing ConfirmDialog state
|
|
@@ -5127,39 +5765,45 @@ function MenuDivider() {
|
|
|
5127
5765
|
return { divider: true, id: `divider-${Date.now()}`, label: '' };
|
|
5128
5766
|
}
|
|
5129
5767
|
|
|
5130
|
-
const variantClasses = {
|
|
5768
|
+
const variantClasses$4 = {
|
|
5131
5769
|
primary: {
|
|
5132
5770
|
default: 'bg-primary-100 text-primary-700 border-primary-200',
|
|
5133
5771
|
hover: 'hover:bg-primary-200',
|
|
5134
5772
|
close: 'hover:bg-primary-300 text-primary-600',
|
|
5773
|
+
selected: 'bg-primary-200 border-primary-400 ring-2 ring-primary-300',
|
|
5135
5774
|
},
|
|
5136
5775
|
secondary: {
|
|
5137
5776
|
default: 'bg-ink-100 text-ink-700 border-ink-200',
|
|
5138
5777
|
hover: 'hover:bg-ink-200',
|
|
5139
5778
|
close: 'hover:bg-ink-300 text-ink-600',
|
|
5779
|
+
selected: 'bg-ink-200 border-ink-400 ring-2 ring-ink-300',
|
|
5140
5780
|
},
|
|
5141
5781
|
success: {
|
|
5142
5782
|
default: 'bg-success-100 text-success-700 border-success-200',
|
|
5143
5783
|
hover: 'hover:bg-success-200',
|
|
5144
5784
|
close: 'hover:bg-success-300 text-success-600',
|
|
5785
|
+
selected: 'bg-success-200 border-success-400 ring-2 ring-success-300',
|
|
5145
5786
|
},
|
|
5146
5787
|
warning: {
|
|
5147
5788
|
default: 'bg-warning-100 text-warning-700 border-warning-200',
|
|
5148
5789
|
hover: 'hover:bg-warning-200',
|
|
5149
5790
|
close: 'hover:bg-warning-300 text-warning-600',
|
|
5791
|
+
selected: 'bg-warning-200 border-warning-400 ring-2 ring-warning-300',
|
|
5150
5792
|
},
|
|
5151
5793
|
error: {
|
|
5152
5794
|
default: 'bg-error-100 text-error-700 border-error-200',
|
|
5153
5795
|
hover: 'hover:bg-error-200',
|
|
5154
5796
|
close: 'hover:bg-error-300 text-error-600',
|
|
5797
|
+
selected: 'bg-error-200 border-error-400 ring-2 ring-error-300',
|
|
5155
5798
|
},
|
|
5156
5799
|
info: {
|
|
5157
5800
|
default: 'bg-accent-100 text-accent-700 border-accent-200',
|
|
5158
5801
|
hover: 'hover:bg-accent-200',
|
|
5159
5802
|
close: 'hover:bg-accent-300 text-accent-600',
|
|
5803
|
+
selected: 'bg-accent-200 border-accent-400 ring-2 ring-accent-300',
|
|
5160
5804
|
},
|
|
5161
5805
|
};
|
|
5162
|
-
const sizeClasses$
|
|
5806
|
+
const sizeClasses$6 = {
|
|
5163
5807
|
sm: {
|
|
5164
5808
|
container: 'h-6 px-2 text-xs gap-1',
|
|
5165
5809
|
icon: 'h-3 w-3',
|
|
@@ -5176,20 +5820,52 @@ const sizeClasses$2 = {
|
|
|
5176
5820
|
close: 'h-4 w-4 ml-2',
|
|
5177
5821
|
},
|
|
5178
5822
|
};
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5823
|
+
const gapClasses = {
|
|
5824
|
+
xs: 'gap-1',
|
|
5825
|
+
sm: 'gap-1.5',
|
|
5826
|
+
md: 'gap-2',
|
|
5827
|
+
lg: 'gap-3',
|
|
5828
|
+
};
|
|
5829
|
+
/**
|
|
5830
|
+
* Chip - Compact element for displaying values with optional remove functionality
|
|
5831
|
+
*
|
|
5832
|
+
* @example Basic chip
|
|
5833
|
+
* ```tsx
|
|
5834
|
+
* <Chip>Tag Name</Chip>
|
|
5835
|
+
* ```
|
|
5836
|
+
*
|
|
5837
|
+
* @example Removable chip
|
|
5838
|
+
* ```tsx
|
|
5839
|
+
* <Chip onClose={() => removeTag(tag)}>
|
|
5840
|
+
* {tag.name}
|
|
5841
|
+
* </Chip>
|
|
5842
|
+
* ```
|
|
5843
|
+
*
|
|
5844
|
+
* @example With icon and selected state
|
|
5845
|
+
* ```tsx
|
|
5846
|
+
* <Chip
|
|
5847
|
+
* icon={<Star className="h-3 w-3" />}
|
|
5848
|
+
* selected={isSelected}
|
|
5849
|
+
* onClick={() => toggleSelection()}
|
|
5850
|
+
* >
|
|
5851
|
+
* Favorite
|
|
5852
|
+
* </Chip>
|
|
5853
|
+
* ```
|
|
5854
|
+
*/
|
|
5855
|
+
function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, selected = false, maxWidth, chipKey, }) {
|
|
5856
|
+
const variantStyle = variantClasses$4[variant];
|
|
5857
|
+
const sizeStyle = sizeClasses$6[size];
|
|
5182
5858
|
const isClickable = !disabled && (onClick || onClose);
|
|
5183
5859
|
return (jsxRuntime.jsxs("div", { className: `
|
|
5184
5860
|
inline-flex items-center rounded-full border font-medium
|
|
5185
5861
|
transition-colors
|
|
5186
|
-
${variantStyle.default}
|
|
5187
|
-
${isClickable && !disabled ? variantStyle.hover : ''}
|
|
5862
|
+
${selected ? variantStyle.selected : variantStyle.default}
|
|
5863
|
+
${isClickable && !disabled && !selected ? variantStyle.hover : ''}
|
|
5188
5864
|
${sizeStyle.container}
|
|
5189
5865
|
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
|
|
5190
5866
|
${onClick && !disabled ? 'cursor-pointer' : ''}
|
|
5191
5867
|
${className}
|
|
5192
|
-
`, onClick: onClick && !disabled ? onClick : undefined, role: onClick ? 'button' : undefined, "aria-disabled": disabled, children: [icon && (jsxRuntime.jsx("span", { className: `flex-shrink-0 ${sizeStyle.icon}`, children: icon })), jsxRuntime.jsx("span", { className: "truncate", children: children }), onClose && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
|
|
5868
|
+
`, onClick: onClick && !disabled ? onClick : undefined, role: onClick ? 'button' : undefined, "aria-disabled": disabled, "aria-pressed": onClick ? selected : undefined, "data-chip-key": chipKey, style: { maxWidth: maxWidth || undefined }, children: [icon && (jsxRuntime.jsx("span", { className: `flex-shrink-0 ${sizeStyle.icon}`, children: icon })), jsxRuntime.jsx("span", { className: "truncate", children: children }), onClose && (jsxRuntime.jsx("button", { type: "button", onClick: (e) => {
|
|
5193
5869
|
e.stopPropagation();
|
|
5194
5870
|
if (!disabled)
|
|
5195
5871
|
onClose();
|
|
@@ -5200,8 +5876,420 @@ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, dis
|
|
|
5200
5876
|
${sizeStyle.close}
|
|
5201
5877
|
`, "aria-label": "Remove", children: jsxRuntime.jsx(lucideReact.X, { className: "w-full h-full" }) }))] }));
|
|
5202
5878
|
}
|
|
5879
|
+
/**
|
|
5880
|
+
* ChipGroup - Container for multiple chips with layout and selection support
|
|
5881
|
+
*
|
|
5882
|
+
* @example Basic group
|
|
5883
|
+
* ```tsx
|
|
5884
|
+
* <ChipGroup wrap gap="sm">
|
|
5885
|
+
* {tags.map(tag => (
|
|
5886
|
+
* <Chip key={tag.id} onClose={() => removeTag(tag)}>
|
|
5887
|
+
* {tag.name}
|
|
5888
|
+
* </Chip>
|
|
5889
|
+
* ))}
|
|
5890
|
+
* </ChipGroup>
|
|
5891
|
+
* ```
|
|
5892
|
+
*
|
|
5893
|
+
* @example Selectable group (single)
|
|
5894
|
+
* ```tsx
|
|
5895
|
+
* <ChipGroup
|
|
5896
|
+
* selectionMode="single"
|
|
5897
|
+
* selectedKeys={[selectedCategory]}
|
|
5898
|
+
* onSelectionChange={(keys) => setSelectedCategory(keys[0])}
|
|
5899
|
+
* >
|
|
5900
|
+
* <Chip chipKey="all">All</Chip>
|
|
5901
|
+
* <Chip chipKey="active">Active</Chip>
|
|
5902
|
+
* <Chip chipKey="archived">Archived</Chip>
|
|
5903
|
+
* </ChipGroup>
|
|
5904
|
+
* ```
|
|
5905
|
+
*
|
|
5906
|
+
* @example Multi-select group
|
|
5907
|
+
* ```tsx
|
|
5908
|
+
* <ChipGroup
|
|
5909
|
+
* selectionMode="multiple"
|
|
5910
|
+
* selectedKeys={selectedTags}
|
|
5911
|
+
* onSelectionChange={setSelectedTags}
|
|
5912
|
+
* wrap
|
|
5913
|
+
* >
|
|
5914
|
+
* {availableTags.map(tag => (
|
|
5915
|
+
* <Chip key={tag} chipKey={tag}>{tag}</Chip>
|
|
5916
|
+
* ))}
|
|
5917
|
+
* </ChipGroup>
|
|
5918
|
+
* ```
|
|
5919
|
+
*/
|
|
5920
|
+
function ChipGroup({ children, direction = 'horizontal', wrap = false, gap = 'sm', selectionMode = 'none', selectedKeys = [], onSelectionChange, className = '', }) {
|
|
5921
|
+
const handleChipClick = (chipKey) => {
|
|
5922
|
+
if (selectionMode === 'none' || !onSelectionChange)
|
|
5923
|
+
return;
|
|
5924
|
+
if (selectionMode === 'single') {
|
|
5925
|
+
// Toggle single selection
|
|
5926
|
+
if (selectedKeys.includes(chipKey)) {
|
|
5927
|
+
onSelectionChange([]);
|
|
5928
|
+
}
|
|
5929
|
+
else {
|
|
5930
|
+
onSelectionChange([chipKey]);
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
else if (selectionMode === 'multiple') {
|
|
5934
|
+
// Toggle in array
|
|
5935
|
+
if (selectedKeys.includes(chipKey)) {
|
|
5936
|
+
onSelectionChange(selectedKeys.filter(k => k !== chipKey));
|
|
5937
|
+
}
|
|
5938
|
+
else {
|
|
5939
|
+
onSelectionChange([...selectedKeys, chipKey]);
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
};
|
|
5943
|
+
// Clone children to inject selection props
|
|
5944
|
+
const enhancedChildren = React.Children.map(children, (child) => {
|
|
5945
|
+
if (!React.isValidElement(child))
|
|
5946
|
+
return child;
|
|
5947
|
+
const chipKey = child.props.chipKey;
|
|
5948
|
+
if (!chipKey || selectionMode === 'none')
|
|
5949
|
+
return child;
|
|
5950
|
+
const isSelected = selectedKeys.includes(chipKey);
|
|
5951
|
+
return React.cloneElement(child, {
|
|
5952
|
+
...child.props,
|
|
5953
|
+
selected: isSelected,
|
|
5954
|
+
onClick: () => {
|
|
5955
|
+
// Call original onClick if exists
|
|
5956
|
+
if (child.props.onClick) {
|
|
5957
|
+
child.props.onClick();
|
|
5958
|
+
}
|
|
5959
|
+
handleChipClick(chipKey);
|
|
5960
|
+
},
|
|
5961
|
+
});
|
|
5962
|
+
});
|
|
5963
|
+
return (jsxRuntime.jsx("div", { className: `
|
|
5964
|
+
flex
|
|
5965
|
+
${direction === 'vertical' ? 'flex-col' : 'flex-row'}
|
|
5966
|
+
${wrap ? 'flex-wrap' : ''}
|
|
5967
|
+
${gapClasses[gap]}
|
|
5968
|
+
${className}
|
|
5969
|
+
`, role: selectionMode !== 'none' ? 'group' : undefined, "aria-label": selectionMode !== 'none' ? 'Chip selection group' : undefined, children: enhancedChildren }));
|
|
5970
|
+
}
|
|
5203
5971
|
|
|
5204
|
-
const sizeClasses$
|
|
5972
|
+
const sizeClasses$5 = {
|
|
5973
|
+
sm: {
|
|
5974
|
+
item: 'py-1.5 px-2',
|
|
5975
|
+
text: 'text-sm',
|
|
5976
|
+
description: 'text-xs',
|
|
5977
|
+
groupHeader: 'py-1.5 px-2 text-xs',
|
|
5978
|
+
},
|
|
5979
|
+
md: {
|
|
5980
|
+
item: 'py-2 px-3',
|
|
5981
|
+
text: 'text-sm',
|
|
5982
|
+
description: 'text-xs',
|
|
5983
|
+
groupHeader: 'py-2 px-3 text-xs',
|
|
5984
|
+
},
|
|
5985
|
+
lg: {
|
|
5986
|
+
item: 'py-3 px-4',
|
|
5987
|
+
text: 'text-base',
|
|
5988
|
+
description: 'text-sm',
|
|
5989
|
+
groupHeader: 'py-2.5 px-4 text-sm',
|
|
5990
|
+
},
|
|
5991
|
+
};
|
|
5992
|
+
const variantClasses$3 = {
|
|
5993
|
+
default: 'bg-white',
|
|
5994
|
+
bordered: 'bg-white border border-paper-300 rounded-lg',
|
|
5995
|
+
card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
|
|
5996
|
+
};
|
|
5997
|
+
/**
|
|
5998
|
+
* CheckboxList - Multi-select list with checkboxes, grouping, and search
|
|
5999
|
+
*
|
|
6000
|
+
* @example Basic usage
|
|
6001
|
+
* ```tsx
|
|
6002
|
+
* <CheckboxList
|
|
6003
|
+
* items={[
|
|
6004
|
+
* { key: '1', label: 'Option 1' },
|
|
6005
|
+
* { key: '2', label: 'Option 2' },
|
|
6006
|
+
* ]}
|
|
6007
|
+
* selectedKeys={selected}
|
|
6008
|
+
* onSelectionChange={setSelected}
|
|
6009
|
+
* />
|
|
6010
|
+
* ```
|
|
6011
|
+
*
|
|
6012
|
+
* @example With grouping and search
|
|
6013
|
+
* ```tsx
|
|
6014
|
+
* <CheckboxList
|
|
6015
|
+
* items={fields}
|
|
6016
|
+
* selectedKeys={selectedFields}
|
|
6017
|
+
* onSelectionChange={setSelectedFields}
|
|
6018
|
+
* groupLabels={{ table1: 'Users', table2: 'Orders' }}
|
|
6019
|
+
* searchable
|
|
6020
|
+
* searchPlaceholder="Search fields..."
|
|
6021
|
+
* showSelectAll
|
|
6022
|
+
* maxHeight="300px"
|
|
6023
|
+
* />
|
|
6024
|
+
* ```
|
|
6025
|
+
*/
|
|
6026
|
+
function CheckboxList({ items, selectedKeys, onSelectionChange, groupLabels = {}, expandedGroups: controlledExpandedGroups, defaultExpandedGroups, onGroupToggle, searchable = false, searchPlaceholder = 'Search...', filterFn, debounceMs = 150, maxHeight, showSelectAll = false, selectAllLabel = 'Select All', showSelectedCount = false, emptyMessage = 'No items available', noResultsMessage = 'No items match your search', size = 'md', variant = 'default', className = '', }) {
|
|
6027
|
+
const [searchTerm, setSearchTerm] = React.useState('');
|
|
6028
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = React.useState('');
|
|
6029
|
+
const [internalExpandedGroups, setInternalExpandedGroups] = React.useState(new Set(defaultExpandedGroups || []));
|
|
6030
|
+
// Debounce search
|
|
6031
|
+
const handleSearchChange = React.useCallback((value) => {
|
|
6032
|
+
setSearchTerm(value);
|
|
6033
|
+
const timer = setTimeout(() => {
|
|
6034
|
+
setDebouncedSearchTerm(value);
|
|
6035
|
+
}, debounceMs);
|
|
6036
|
+
return () => clearTimeout(timer);
|
|
6037
|
+
}, [debounceMs]);
|
|
6038
|
+
// Filter items based on search
|
|
6039
|
+
const filteredItems = React.useMemo(() => {
|
|
6040
|
+
if (!debouncedSearchTerm)
|
|
6041
|
+
return items;
|
|
6042
|
+
const term = debouncedSearchTerm.toLowerCase();
|
|
6043
|
+
return items.filter(item => {
|
|
6044
|
+
if (filterFn) {
|
|
6045
|
+
return filterFn(item, debouncedSearchTerm);
|
|
6046
|
+
}
|
|
6047
|
+
return (item.label.toLowerCase().includes(term) ||
|
|
6048
|
+
item.description?.toLowerCase().includes(term) ||
|
|
6049
|
+
item.key.toLowerCase().includes(term));
|
|
6050
|
+
});
|
|
6051
|
+
}, [items, debouncedSearchTerm, filterFn]);
|
|
6052
|
+
// Group items
|
|
6053
|
+
const groupedItems = React.useMemo(() => {
|
|
6054
|
+
const groups = new Map();
|
|
6055
|
+
filteredItems.forEach(item => {
|
|
6056
|
+
const groupKey = item.group || null;
|
|
6057
|
+
if (!groups.has(groupKey)) {
|
|
6058
|
+
groups.set(groupKey, []);
|
|
6059
|
+
}
|
|
6060
|
+
groups.get(groupKey).push(item);
|
|
6061
|
+
});
|
|
6062
|
+
return groups;
|
|
6063
|
+
}, [filteredItems]);
|
|
6064
|
+
// Determine expanded groups
|
|
6065
|
+
const expandedGroups = controlledExpandedGroups
|
|
6066
|
+
? new Set(controlledExpandedGroups)
|
|
6067
|
+
: internalExpandedGroups;
|
|
6068
|
+
const handleGroupToggle = (groupKey) => {
|
|
6069
|
+
const newExpanded = !expandedGroups.has(groupKey);
|
|
6070
|
+
if (!controlledExpandedGroups) {
|
|
6071
|
+
setInternalExpandedGroups(prev => {
|
|
6072
|
+
const next = new Set(prev);
|
|
6073
|
+
if (newExpanded) {
|
|
6074
|
+
next.add(groupKey);
|
|
6075
|
+
}
|
|
6076
|
+
else {
|
|
6077
|
+
next.delete(groupKey);
|
|
6078
|
+
}
|
|
6079
|
+
return next;
|
|
6080
|
+
});
|
|
6081
|
+
}
|
|
6082
|
+
onGroupToggle?.(groupKey, newExpanded);
|
|
6083
|
+
};
|
|
6084
|
+
// Selection handlers
|
|
6085
|
+
const handleItemToggle = (key) => {
|
|
6086
|
+
const newSelected = selectedKeys.includes(key)
|
|
6087
|
+
? selectedKeys.filter(k => k !== key)
|
|
6088
|
+
: [...selectedKeys, key];
|
|
6089
|
+
onSelectionChange(newSelected);
|
|
6090
|
+
};
|
|
6091
|
+
const handleSelectAll = () => {
|
|
6092
|
+
const enabledItems = filteredItems.filter(item => !item.disabled);
|
|
6093
|
+
const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
|
|
6094
|
+
if (allSelected) {
|
|
6095
|
+
// Deselect all filtered items
|
|
6096
|
+
const filteredKeys = new Set(enabledItems.map(item => item.key));
|
|
6097
|
+
onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
|
|
6098
|
+
}
|
|
6099
|
+
else {
|
|
6100
|
+
// Select all filtered items
|
|
6101
|
+
const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
|
|
6102
|
+
onSelectionChange(Array.from(newKeys));
|
|
6103
|
+
}
|
|
6104
|
+
};
|
|
6105
|
+
const sizeStyle = sizeClasses$5[size];
|
|
6106
|
+
const enabledItems = filteredItems.filter(item => !item.disabled);
|
|
6107
|
+
const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
|
|
6108
|
+
const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
|
|
6109
|
+
// Check if we have groups
|
|
6110
|
+
const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
|
|
6111
|
+
return (jsxRuntime.jsxs("div", { className: `${variantClasses$3[variant]} ${className}`, children: [searchable && (jsxRuntime.jsx("div", { className: `${sizeStyle.item} border-b border-paper-200`, children: jsxRuntime.jsx(Input, { value: searchTerm, onChange: (e) => handleSearchChange(e.target.value), placeholder: searchPlaceholder, prefixIcon: jsxRuntime.jsx(lucideReact.Search, { className: "h-4 w-4" }), size: size === 'lg' ? 'md' : 'sm', clearable: true, onClear: () => {
|
|
6112
|
+
setSearchTerm('');
|
|
6113
|
+
setDebouncedSearchTerm('');
|
|
6114
|
+
} }) })), (showSelectAll || showSelectedCount) && filteredItems.length > 0 && (jsxRuntime.jsxs("div", { className: `${sizeStyle.item} border-b border-paper-200 flex items-center justify-between`, children: [showSelectAll && (jsxRuntime.jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: handleSelectAll, label: selectAllLabel, size: size })), showSelectedCount && (jsxRuntime.jsxs("span", { className: `${sizeStyle.description} text-ink-500`, children: [selectedKeys.length, " selected"] }))] })), jsxRuntime.jsxs("div", { className: "overflow-y-auto", style: { maxHeight: maxHeight || undefined }, children: [items.length === 0 && (jsxRuntime.jsx("div", { className: `${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`, children: emptyMessage })), items.length > 0 && filteredItems.length === 0 && (jsxRuntime.jsx("div", { className: `${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`, children: noResultsMessage })), hasGroups ? (Array.from(groupedItems.entries()).map(([groupKey, groupItems]) => {
|
|
6115
|
+
if (groupKey === null) {
|
|
6116
|
+
// Ungrouped items
|
|
6117
|
+
return groupItems.map(item => (jsxRuntime.jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key)));
|
|
6118
|
+
}
|
|
6119
|
+
const isExpanded = expandedGroups.has(groupKey);
|
|
6120
|
+
const groupLabel = groupLabels[groupKey] || groupKey;
|
|
6121
|
+
const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
|
|
6122
|
+
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("button", { type: "button", onClick: () => handleGroupToggle(groupKey), className: `
|
|
6123
|
+
w-full flex items-center justify-between
|
|
6124
|
+
${sizeStyle.groupHeader}
|
|
6125
|
+
font-medium text-ink-700 bg-paper-50
|
|
6126
|
+
hover:bg-paper-100 transition-colors
|
|
6127
|
+
border-b border-paper-200
|
|
6128
|
+
`, children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [isExpanded ? (jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4 text-ink-400" })) : (jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4 text-ink-400" })), jsxRuntime.jsx("span", { children: groupLabel })] }), jsxRuntime.jsxs("span", { className: "text-ink-400 font-normal", children: [groupSelectedCount > 0 && `${groupSelectedCount}/`, groupItems.length] })] }), isExpanded && (jsxRuntime.jsx("div", { children: groupItems.map(item => (jsxRuntime.jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle, indented: true }, item.key))) }))] }, groupKey));
|
|
6129
|
+
})) : (
|
|
6130
|
+
// Flat list (no groups)
|
|
6131
|
+
filteredItems.map(item => (jsxRuntime.jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key))))] })] }));
|
|
6132
|
+
}
|
|
6133
|
+
// Helper component for rendering individual items
|
|
6134
|
+
function CheckboxListItemRow({ item, selected, onToggle, size, sizeStyle, indented = false, }) {
|
|
6135
|
+
return (jsxRuntime.jsx("div", { className: `
|
|
6136
|
+
${sizeStyle.item}
|
|
6137
|
+
${indented ? 'pl-8' : ''}
|
|
6138
|
+
hover:bg-paper-50 transition-colors
|
|
6139
|
+
border-b border-paper-100 last:border-b-0
|
|
6140
|
+
${item.disabled ? 'opacity-50' : ''}
|
|
6141
|
+
`, children: jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [jsxRuntime.jsx(Checkbox, { checked: selected, onChange: onToggle, disabled: item.disabled, size: size }), jsxRuntime.jsxs("div", { className: "flex flex-col flex-1 min-w-0", children: [jsxRuntime.jsx("span", { className: sizeStyle.text, children: item.label }), item.description && (jsxRuntime.jsx("span", { className: `${sizeStyle.description} text-ink-500`, children: item.description }))] })] }) }));
|
|
6142
|
+
}
|
|
6143
|
+
|
|
6144
|
+
const sizeClasses$4 = {
|
|
6145
|
+
sm: {
|
|
6146
|
+
container: 'text-sm',
|
|
6147
|
+
item: 'py-1.5 px-2',
|
|
6148
|
+
searchPadding: 'p-2',
|
|
6149
|
+
statusPadding: 'px-2 py-1.5',
|
|
6150
|
+
},
|
|
6151
|
+
md: {
|
|
6152
|
+
container: 'text-sm',
|
|
6153
|
+
item: 'py-2 px-3',
|
|
6154
|
+
searchPadding: 'p-3',
|
|
6155
|
+
statusPadding: 'px-3 py-2',
|
|
6156
|
+
},
|
|
6157
|
+
lg: {
|
|
6158
|
+
container: 'text-base',
|
|
6159
|
+
item: 'py-3 px-4',
|
|
6160
|
+
searchPadding: 'p-4',
|
|
6161
|
+
statusPadding: 'px-4 py-2.5',
|
|
6162
|
+
},
|
|
6163
|
+
};
|
|
6164
|
+
const variantClasses$2 = {
|
|
6165
|
+
default: 'bg-white',
|
|
6166
|
+
bordered: 'bg-white border border-paper-300 rounded-lg',
|
|
6167
|
+
card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
|
|
6168
|
+
};
|
|
6169
|
+
/**
|
|
6170
|
+
* SearchableList - List component with integrated search/filter functionality
|
|
6171
|
+
*
|
|
6172
|
+
* @example Basic usage
|
|
6173
|
+
* ```tsx
|
|
6174
|
+
* <SearchableList
|
|
6175
|
+
* items={users.map(u => ({ key: u.id, data: u }))}
|
|
6176
|
+
* renderItem={(item) => <div>{item.data.name}</div>}
|
|
6177
|
+
* onSelect={(item) => setSelectedUser(item.data)}
|
|
6178
|
+
* searchable
|
|
6179
|
+
* searchPlaceholder="Search users..."
|
|
6180
|
+
* />
|
|
6181
|
+
* ```
|
|
6182
|
+
*
|
|
6183
|
+
* @example With custom filter and loading
|
|
6184
|
+
* ```tsx
|
|
6185
|
+
* <SearchableList
|
|
6186
|
+
* items={products}
|
|
6187
|
+
* renderItem={(item, index, isSelected) => (
|
|
6188
|
+
* <div className={isSelected ? 'bg-accent-50' : ''}>
|
|
6189
|
+
* {item.data.name} - ${item.data.price}
|
|
6190
|
+
* </div>
|
|
6191
|
+
* )}
|
|
6192
|
+
* filterFn={(item, term) =>
|
|
6193
|
+
* item.data.name.toLowerCase().includes(term.toLowerCase())
|
|
6194
|
+
* }
|
|
6195
|
+
* loading={isLoading}
|
|
6196
|
+
* loadingMessage="Fetching products..."
|
|
6197
|
+
* maxHeight="400px"
|
|
6198
|
+
* />
|
|
6199
|
+
* ```
|
|
6200
|
+
*/
|
|
6201
|
+
function SearchableList({ items, searchPlaceholder = 'Search...', searchValue: controlledSearchValue, onSearchChange, filterFn, debounceMs = 150, renderItem, selectedKey, onSelect, maxHeight, showResultCount = false, formatResultCount, emptyMessage = 'No items available', noResultsMessage = 'No items match your search', loading = false, loadingMessage = 'Loading...', size = 'md', variant = 'default', className = '', enableKeyboardNavigation = true, autoFocus = false, }) {
|
|
6202
|
+
const [internalSearchValue, setInternalSearchValue] = React.useState('');
|
|
6203
|
+
const [debouncedSearchTerm, setDebouncedSearchTerm] = React.useState('');
|
|
6204
|
+
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
|
|
6205
|
+
const listRef = React.useRef(null);
|
|
6206
|
+
const itemRefs = React.useRef(new Map());
|
|
6207
|
+
const searchValue = controlledSearchValue !== undefined ? controlledSearchValue : internalSearchValue;
|
|
6208
|
+
// Debounce search
|
|
6209
|
+
React.useEffect(() => {
|
|
6210
|
+
const timer = setTimeout(() => {
|
|
6211
|
+
setDebouncedSearchTerm(searchValue);
|
|
6212
|
+
}, debounceMs);
|
|
6213
|
+
return () => clearTimeout(timer);
|
|
6214
|
+
}, [searchValue, debounceMs]);
|
|
6215
|
+
const handleSearchChange = React.useCallback((value) => {
|
|
6216
|
+
if (controlledSearchValue === undefined) {
|
|
6217
|
+
setInternalSearchValue(value);
|
|
6218
|
+
}
|
|
6219
|
+
onSearchChange?.(value);
|
|
6220
|
+
setHighlightedIndex(-1);
|
|
6221
|
+
}, [controlledSearchValue, onSearchChange]);
|
|
6222
|
+
// Filter items based on search
|
|
6223
|
+
const filteredItems = React.useMemo(() => {
|
|
6224
|
+
if (!debouncedSearchTerm)
|
|
6225
|
+
return items;
|
|
6226
|
+
return items.filter(item => {
|
|
6227
|
+
if (filterFn) {
|
|
6228
|
+
return filterFn(item, debouncedSearchTerm);
|
|
6229
|
+
}
|
|
6230
|
+
// Default filter: check if key includes search term
|
|
6231
|
+
return item.key.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
|
|
6232
|
+
});
|
|
6233
|
+
}, [items, debouncedSearchTerm, filterFn]);
|
|
6234
|
+
// Keyboard navigation
|
|
6235
|
+
const handleKeyDown = React.useCallback((e) => {
|
|
6236
|
+
if (!enableKeyboardNavigation || filteredItems.length === 0)
|
|
6237
|
+
return;
|
|
6238
|
+
switch (e.key) {
|
|
6239
|
+
case 'ArrowDown':
|
|
6240
|
+
e.preventDefault();
|
|
6241
|
+
setHighlightedIndex(prev => prev < filteredItems.length - 1 ? prev + 1 : 0);
|
|
6242
|
+
break;
|
|
6243
|
+
case 'ArrowUp':
|
|
6244
|
+
e.preventDefault();
|
|
6245
|
+
setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredItems.length - 1);
|
|
6246
|
+
break;
|
|
6247
|
+
case 'Enter':
|
|
6248
|
+
e.preventDefault();
|
|
6249
|
+
if (highlightedIndex >= 0 && highlightedIndex < filteredItems.length) {
|
|
6250
|
+
onSelect?.(filteredItems[highlightedIndex]);
|
|
6251
|
+
}
|
|
6252
|
+
break;
|
|
6253
|
+
case 'Escape':
|
|
6254
|
+
setHighlightedIndex(-1);
|
|
6255
|
+
break;
|
|
6256
|
+
}
|
|
6257
|
+
}, [enableKeyboardNavigation, filteredItems, highlightedIndex, onSelect]);
|
|
6258
|
+
// Scroll highlighted item into view
|
|
6259
|
+
React.useEffect(() => {
|
|
6260
|
+
if (highlightedIndex >= 0) {
|
|
6261
|
+
const itemEl = itemRefs.current.get(highlightedIndex);
|
|
6262
|
+
if (itemEl) {
|
|
6263
|
+
itemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}, [highlightedIndex]);
|
|
6267
|
+
const sizeStyle = sizeClasses$4[size];
|
|
6268
|
+
const resultCountText = formatResultCount
|
|
6269
|
+
? formatResultCount(filteredItems.length, items.length)
|
|
6270
|
+
: `${filteredItems.length} of ${items.length}`;
|
|
6271
|
+
return (jsxRuntime.jsxs("div", { className: `${variantClasses$2[variant]} ${sizeStyle.container} ${className}`, onKeyDown: handleKeyDown, children: [jsxRuntime.jsx("div", { className: `${sizeStyle.searchPadding} border-b border-paper-200`, children: jsxRuntime.jsx(Input, { value: searchValue, onChange: (e) => handleSearchChange(e.target.value), placeholder: searchPlaceholder, prefixIcon: jsxRuntime.jsx(lucideReact.Search, { className: "h-4 w-4" }), size: size === 'lg' ? 'md' : 'sm', clearable: true, onClear: () => handleSearchChange(''), autoFocus: autoFocus }) }), showResultCount && items.length > 0 && !loading && (jsxRuntime.jsx("div", { className: `${sizeStyle.statusPadding} text-ink-500 text-xs border-b border-paper-100`, children: resultCountText })), jsxRuntime.jsxs("div", { ref: listRef, className: "overflow-y-auto", style: { maxHeight: maxHeight || undefined }, role: "listbox", "aria-activedescendant": highlightedIndex >= 0 ? `item-${highlightedIndex}` : undefined, children: [loading && (jsxRuntime.jsxs("div", { className: `${sizeStyle.item} flex items-center justify-center gap-2 text-ink-500`, children: [jsxRuntime.jsx(lucideReact.Loader2, { className: "h-4 w-4 animate-spin" }), jsxRuntime.jsx("span", { children: loadingMessage })] })), !loading && items.length === 0 && (jsxRuntime.jsx("div", { className: `${sizeStyle.item} text-ink-500 text-center`, children: emptyMessage })), !loading && items.length > 0 && filteredItems.length === 0 && (jsxRuntime.jsx("div", { className: `${sizeStyle.item} text-ink-500 text-center`, children: noResultsMessage })), !loading && filteredItems.map((item, index) => {
|
|
6272
|
+
const isSelected = selectedKey === item.key;
|
|
6273
|
+
const isHighlighted = highlightedIndex === index;
|
|
6274
|
+
return (jsxRuntime.jsx("div", { id: `item-${index}`, ref: (el) => {
|
|
6275
|
+
if (el) {
|
|
6276
|
+
itemRefs.current.set(index, el);
|
|
6277
|
+
}
|
|
6278
|
+
else {
|
|
6279
|
+
itemRefs.current.delete(index);
|
|
6280
|
+
}
|
|
6281
|
+
}, role: "option", "aria-selected": isSelected, onClick: () => onSelect?.(item), className: `
|
|
6282
|
+
${sizeStyle.item}
|
|
6283
|
+
cursor-pointer transition-colors
|
|
6284
|
+
${isSelected ? 'bg-accent-50' : ''}
|
|
6285
|
+
${isHighlighted ? 'bg-paper-100' : ''}
|
|
6286
|
+
${!isSelected && !isHighlighted ? 'hover:bg-paper-50' : ''}
|
|
6287
|
+
border-b border-paper-100 last:border-b-0
|
|
6288
|
+
`, children: renderItem(item, index, isSelected, isHighlighted) }, item.key));
|
|
6289
|
+
})] })] }));
|
|
6290
|
+
}
|
|
6291
|
+
|
|
6292
|
+
const sizeClasses$3 = {
|
|
5205
6293
|
sm: {
|
|
5206
6294
|
input: 'h-8 px-2 text-sm',
|
|
5207
6295
|
button: 'h-8 w-8',
|
|
@@ -5225,7 +6313,7 @@ const NumberInput = React.forwardRef((props, ref) => {
|
|
|
5225
6313
|
const { value = 0, onChange, min, max, step = 1, disabled = false, readOnly = false, label, helperText, error, required = false, size = 'md', className = '', placeholder, id, name, precision, formatValue, } = props;
|
|
5226
6314
|
const [internalValue, setInternalValue] = React.useState(String(value));
|
|
5227
6315
|
const [isFocused, setIsFocused] = React.useState(false);
|
|
5228
|
-
const sizeStyle = sizeClasses$
|
|
6316
|
+
const sizeStyle = sizeClasses$3[size];
|
|
5229
6317
|
// Generate unique IDs for ARIA
|
|
5230
6318
|
const uniqueId = React.useId();
|
|
5231
6319
|
const inputId = id || uniqueId;
|
|
@@ -5655,105 +6743,6 @@ class ErrorBoundary extends React.Component {
|
|
|
5655
6743
|
}
|
|
5656
6744
|
}
|
|
5657
6745
|
|
|
5658
|
-
const heightPresets = {
|
|
5659
|
-
sm: '33vh',
|
|
5660
|
-
md: '50vh',
|
|
5661
|
-
lg: '75vh',
|
|
5662
|
-
full: '90vh',
|
|
5663
|
-
};
|
|
5664
|
-
function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHandle = true, showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className = '', }) {
|
|
5665
|
-
const titleId = React.useId();
|
|
5666
|
-
const [isDragging, setIsDragging] = React.useState(false);
|
|
5667
|
-
const [dragOffset, setDragOffset] = React.useState(0);
|
|
5668
|
-
const [currentHeight] = React.useState(typeof height === 'string' && height in heightPresets
|
|
5669
|
-
? heightPresets[height]
|
|
5670
|
-
: height);
|
|
5671
|
-
const sheetRef = React.useRef(null);
|
|
5672
|
-
const startYRef = React.useRef(0);
|
|
5673
|
-
// Close on Escape
|
|
5674
|
-
React.useEffect(() => {
|
|
5675
|
-
if (!isOpen || !closeOnEscape)
|
|
5676
|
-
return;
|
|
5677
|
-
const handleEscape = (e) => {
|
|
5678
|
-
if (e.key === 'Escape') {
|
|
5679
|
-
onClose();
|
|
5680
|
-
}
|
|
5681
|
-
};
|
|
5682
|
-
document.addEventListener('keydown', handleEscape);
|
|
5683
|
-
return () => document.removeEventListener('keydown', handleEscape);
|
|
5684
|
-
}, [isOpen, closeOnEscape, onClose]);
|
|
5685
|
-
// Prevent body scroll when open
|
|
5686
|
-
React.useEffect(() => {
|
|
5687
|
-
if (isOpen) {
|
|
5688
|
-
document.body.style.overflow = 'hidden';
|
|
5689
|
-
}
|
|
5690
|
-
else {
|
|
5691
|
-
document.body.style.overflow = '';
|
|
5692
|
-
}
|
|
5693
|
-
return () => {
|
|
5694
|
-
document.body.style.overflow = '';
|
|
5695
|
-
};
|
|
5696
|
-
}, [isOpen]);
|
|
5697
|
-
const handleOverlayClick = (e) => {
|
|
5698
|
-
if (closeOnOverlayClick && e.target === e.currentTarget) {
|
|
5699
|
-
onClose();
|
|
5700
|
-
}
|
|
5701
|
-
};
|
|
5702
|
-
const handleDragStart = (e) => {
|
|
5703
|
-
setIsDragging(true);
|
|
5704
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
5705
|
-
startYRef.current = clientY;
|
|
5706
|
-
};
|
|
5707
|
-
const handleDragMove = (e) => {
|
|
5708
|
-
if (!isDragging)
|
|
5709
|
-
return;
|
|
5710
|
-
const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
|
|
5711
|
-
const offset = clientY - startYRef.current;
|
|
5712
|
-
// Only allow dragging down
|
|
5713
|
-
if (offset > 0) {
|
|
5714
|
-
setDragOffset(offset);
|
|
5715
|
-
}
|
|
5716
|
-
};
|
|
5717
|
-
const handleDragEnd = () => {
|
|
5718
|
-
setIsDragging(false);
|
|
5719
|
-
// Close if dragged down more than 150px
|
|
5720
|
-
if (dragOffset > 150) {
|
|
5721
|
-
onClose();
|
|
5722
|
-
}
|
|
5723
|
-
setDragOffset(0);
|
|
5724
|
-
};
|
|
5725
|
-
React.useEffect(() => {
|
|
5726
|
-
if (!isDragging)
|
|
5727
|
-
return;
|
|
5728
|
-
const handleMove = (e) => handleDragMove(e);
|
|
5729
|
-
const handleEnd = () => handleDragEnd();
|
|
5730
|
-
document.addEventListener('touchmove', handleMove);
|
|
5731
|
-
document.addEventListener('mousemove', handleMove);
|
|
5732
|
-
document.addEventListener('touchend', handleEnd);
|
|
5733
|
-
document.addEventListener('mouseup', handleEnd);
|
|
5734
|
-
return () => {
|
|
5735
|
-
document.removeEventListener('touchmove', handleMove);
|
|
5736
|
-
document.removeEventListener('mousemove', handleMove);
|
|
5737
|
-
document.removeEventListener('touchend', handleEnd);
|
|
5738
|
-
document.removeEventListener('mouseup', handleEnd);
|
|
5739
|
-
};
|
|
5740
|
-
}, [isDragging, dragOffset]);
|
|
5741
|
-
if (!isOpen)
|
|
5742
|
-
return null;
|
|
5743
|
-
return (jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: handleOverlayClick, children: [jsxRuntime.jsx("div", { className: `
|
|
5744
|
-
absolute inset-0 bg-black/50 transition-opacity duration-300
|
|
5745
|
-
${isOpen ? 'opacity-100' : 'opacity-0'}
|
|
5746
|
-
` }), jsxRuntime.jsxs("div", { ref: sheetRef, className: `
|
|
5747
|
-
relative w-full bg-white rounded-t-2xl shadow-2xl
|
|
5748
|
-
transition-transform duration-300 ease-out
|
|
5749
|
-
${isOpen ? 'translate-y-0' : 'translate-y-full'}
|
|
5750
|
-
${className}
|
|
5751
|
-
`, style: {
|
|
5752
|
-
height: currentHeight,
|
|
5753
|
-
transform: `translateY(${dragOffset}px)`,
|
|
5754
|
-
}, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, children: [showHandle && (jsxRuntime.jsx("div", { className: "py-3 cursor-grab active:cursor-grabbing", onTouchStart: handleDragStart, onMouseDown: handleDragStart, children: jsxRuntime.jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) })), (title || showCloseButton) && (jsxRuntime.jsxs("div", { className: "px-6 py-4 border-b border-ink-200 flex items-center justify-between", children: [title && (jsxRuntime.jsx("h2", { id: titleId, className: "text-lg font-semibold text-ink-900", children: title })), showCloseButton && (jsxRuntime.jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors ml-auto", "aria-label": "Close", children: jsxRuntime.jsx(lucideReact.X, { className: "h-5 w-5" }) }))] })), jsxRuntime.jsx("div", { className: "overflow-y-auto flex-1 p-6", children: children })] })] }));
|
|
5755
|
-
}
|
|
5756
|
-
|
|
5757
6746
|
function Collapsible({ trigger, children, defaultOpen = false, open: controlledOpen, onOpenChange, disabled = false, showIcon = true, className = '', triggerClassName = '', contentClassName = '', }) {
|
|
5758
6747
|
const isControlled = controlledOpen !== undefined;
|
|
5759
6748
|
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
|
|
@@ -5803,6 +6792,231 @@ function Collapsible({ trigger, children, defaultOpen = false, open: controlledO
|
|
|
5803
6792
|
` }))] }), jsxRuntime.jsx("div", { id: "collapsible-content", ref: contentRef, className: "overflow-hidden transition-all duration-300 ease-in-out", style: { height: `${height}px` }, "aria-hidden": !isOpen, children: jsxRuntime.jsx("div", { className: contentClassName, children: children }) })] }));
|
|
5804
6793
|
}
|
|
5805
6794
|
|
|
6795
|
+
const sizeClasses$2 = {
|
|
6796
|
+
sm: {
|
|
6797
|
+
header: 'h-10 px-3',
|
|
6798
|
+
text: 'text-sm',
|
|
6799
|
+
icon: 'h-4 w-4',
|
|
6800
|
+
},
|
|
6801
|
+
md: {
|
|
6802
|
+
header: 'h-12 px-4',
|
|
6803
|
+
text: 'text-sm',
|
|
6804
|
+
icon: 'h-5 w-5',
|
|
6805
|
+
},
|
|
6806
|
+
lg: {
|
|
6807
|
+
header: 'h-14 px-5',
|
|
6808
|
+
text: 'text-base',
|
|
6809
|
+
icon: 'h-5 w-5',
|
|
6810
|
+
},
|
|
6811
|
+
};
|
|
6812
|
+
const variantClasses$1 = {
|
|
6813
|
+
default: {
|
|
6814
|
+
container: 'bg-white border-ink-200',
|
|
6815
|
+
header: 'bg-paper-50',
|
|
6816
|
+
},
|
|
6817
|
+
elevated: {
|
|
6818
|
+
container: 'bg-white shadow-lg border-ink-200',
|
|
6819
|
+
header: 'bg-white',
|
|
6820
|
+
},
|
|
6821
|
+
bordered: {
|
|
6822
|
+
container: 'bg-white border-2 border-ink-300',
|
|
6823
|
+
header: 'bg-paper-100',
|
|
6824
|
+
},
|
|
6825
|
+
};
|
|
6826
|
+
/**
|
|
6827
|
+
* ExpandablePanel - A panel that sticks to the bottom (or top) and can expand/collapse
|
|
6828
|
+
*
|
|
6829
|
+
* For bottom position: expands UPWARD (content appears above header)
|
|
6830
|
+
* For top position: expands DOWNWARD (content appears below header)
|
|
6831
|
+
*
|
|
6832
|
+
* Two modes of operation:
|
|
6833
|
+
* - `viewport`: Fixed to the viewport (for standalone pages, covers StatusBar)
|
|
6834
|
+
* - `container`: Sticky within its parent container (for use inside Page/AppLayout, respects StatusBar)
|
|
6835
|
+
*
|
|
6836
|
+
* @example Basic usage (viewport mode - full page)
|
|
6837
|
+
* ```tsx
|
|
6838
|
+
* <ExpandablePanel
|
|
6839
|
+
* mode="viewport"
|
|
6840
|
+
* collapsedContent={<Text>3 items selected</Text>}
|
|
6841
|
+
* expandedHeight="300px"
|
|
6842
|
+
* >
|
|
6843
|
+
* {content}
|
|
6844
|
+
* </ExpandablePanel>
|
|
6845
|
+
* ```
|
|
6846
|
+
*
|
|
6847
|
+
* @example Inside Page/AppLayout (container mode - respects StatusBar)
|
|
6848
|
+
* ```tsx
|
|
6849
|
+
* <Page>
|
|
6850
|
+
* <ExpandablePanelContainer>
|
|
6851
|
+
* <div className="flex-1 overflow-auto">
|
|
6852
|
+
* {pageContent}
|
|
6853
|
+
* </div>
|
|
6854
|
+
* <ExpandablePanel
|
|
6855
|
+
* mode="container"
|
|
6856
|
+
* collapsedContent={<Text>3 items selected</Text>}
|
|
6857
|
+
* expandedHeight="300px"
|
|
6858
|
+
* >
|
|
6859
|
+
* {selectedItemsContent}
|
|
6860
|
+
* </ExpandablePanel>
|
|
6861
|
+
* </ExpandablePanelContainer>
|
|
6862
|
+
* </Page>
|
|
6863
|
+
* ```
|
|
6864
|
+
*
|
|
6865
|
+
* @example With maxWidth to match page content
|
|
6866
|
+
* ```tsx
|
|
6867
|
+
* <ExpandablePanel
|
|
6868
|
+
* mode="container"
|
|
6869
|
+
* maxWidth="1400px"
|
|
6870
|
+
* collapsedContent={<Text>Generated SQL</Text>}
|
|
6871
|
+
* expandedHeight="300px"
|
|
6872
|
+
* >
|
|
6873
|
+
* {sqlContent}
|
|
6874
|
+
* </ExpandablePanel>
|
|
6875
|
+
* ```
|
|
6876
|
+
*/
|
|
6877
|
+
function ExpandablePanel({ collapsedContent, children, position = 'bottom', mode = 'viewport', expanded: controlledExpanded, defaultExpanded = false, onExpandedChange, expandedHeight = '300px', maxWidth, showToggle = true, toggleContent, headerActions, closeOnEscape = true, variant = 'elevated', size = 'md', className = '', headerClassName = '', contentClassName = '', zIndex = 40, }) {
|
|
6878
|
+
const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded);
|
|
6879
|
+
// Determine if controlled or uncontrolled
|
|
6880
|
+
const isControlled = controlledExpanded !== undefined;
|
|
6881
|
+
const expanded = isControlled ? controlledExpanded : internalExpanded;
|
|
6882
|
+
const setExpanded = (value) => {
|
|
6883
|
+
if (!isControlled) {
|
|
6884
|
+
setInternalExpanded(value);
|
|
6885
|
+
}
|
|
6886
|
+
onExpandedChange?.(value);
|
|
6887
|
+
};
|
|
6888
|
+
const toggleExpanded = () => {
|
|
6889
|
+
setExpanded(!expanded);
|
|
6890
|
+
};
|
|
6891
|
+
// Close on Escape
|
|
6892
|
+
React.useEffect(() => {
|
|
6893
|
+
if (!closeOnEscape || !expanded)
|
|
6894
|
+
return;
|
|
6895
|
+
const handleEscape = (e) => {
|
|
6896
|
+
if (e.key === 'Escape') {
|
|
6897
|
+
setExpanded(false);
|
|
6898
|
+
}
|
|
6899
|
+
};
|
|
6900
|
+
document.addEventListener('keydown', handleEscape);
|
|
6901
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
6902
|
+
}, [closeOnEscape, expanded]);
|
|
6903
|
+
const sizeStyle = sizeClasses$2[size];
|
|
6904
|
+
const variantStyle = variantClasses$1[variant];
|
|
6905
|
+
const heightValue = typeof expandedHeight === 'number' ? `${expandedHeight}px` : expandedHeight;
|
|
6906
|
+
const maxWidthValue = maxWidth
|
|
6907
|
+
? (typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)
|
|
6908
|
+
: undefined;
|
|
6909
|
+
// Position classes differ based on mode
|
|
6910
|
+
const getPositionClasses = () => {
|
|
6911
|
+
if (mode === 'viewport') {
|
|
6912
|
+
// Fixed to viewport
|
|
6913
|
+
return position === 'bottom'
|
|
6914
|
+
? 'fixed bottom-0 left-0 right-0'
|
|
6915
|
+
: 'fixed top-0 left-0 right-0';
|
|
6916
|
+
}
|
|
6917
|
+
else {
|
|
6918
|
+
// Absolute positioning within container - snaps to bottom
|
|
6919
|
+
return position === 'bottom'
|
|
6920
|
+
? 'absolute bottom-0 left-0 right-0'
|
|
6921
|
+
: 'absolute top-0 left-0 right-0';
|
|
6922
|
+
}
|
|
6923
|
+
};
|
|
6924
|
+
// For bottom panel, we want chevron up to expand (reveal content above)
|
|
6925
|
+
// For top panel, we want chevron down to expand (reveal content below)
|
|
6926
|
+
const ChevronIcon = position === 'bottom'
|
|
6927
|
+
? (expanded ? lucideReact.ChevronDown : lucideReact.ChevronUp)
|
|
6928
|
+
: (expanded ? lucideReact.ChevronUp : lucideReact.ChevronDown);
|
|
6929
|
+
// Header component
|
|
6930
|
+
const header = (jsxRuntime.jsxs("div", { className: `
|
|
6931
|
+
flex items-center justify-between
|
|
6932
|
+
${sizeStyle.header}
|
|
6933
|
+
${variantStyle.header}
|
|
6934
|
+
border-ink-200
|
|
6935
|
+
flex-shrink-0
|
|
6936
|
+
${headerClassName}
|
|
6937
|
+
`, children: [jsxRuntime.jsx("div", { className: `flex-1 flex items-center ${sizeStyle.text}`, children: collapsedContent }), jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [headerActions, showToggle && (jsxRuntime.jsx("button", { type: "button", onClick: toggleExpanded, className: `
|
|
6938
|
+
flex items-center justify-center
|
|
6939
|
+
p-1.5 rounded-md
|
|
6940
|
+
text-ink-500 hover:text-ink-700
|
|
6941
|
+
hover:bg-ink-100
|
|
6942
|
+
transition-colors
|
|
6943
|
+
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1
|
|
6944
|
+
`, "aria-expanded": expanded, "aria-label": expanded ? 'Collapse panel' : 'Expand panel', children: toggleContent || jsxRuntime.jsx(ChevronIcon, { className: sizeStyle.icon }) }))] })] }));
|
|
6945
|
+
// Content component
|
|
6946
|
+
const content = (jsxRuntime.jsx("div", { className: `
|
|
6947
|
+
overflow-hidden
|
|
6948
|
+
transition-all duration-300 ease-in-out
|
|
6949
|
+
`, style: {
|
|
6950
|
+
maxHeight: expanded ? heightValue : '0px',
|
|
6951
|
+
opacity: expanded ? 1 : 0,
|
|
6952
|
+
}, children: jsxRuntime.jsx("div", { className: `
|
|
6953
|
+
overflow-y-auto p-4
|
|
6954
|
+
${contentClassName}
|
|
6955
|
+
`, style: { maxHeight: heightValue }, children: children }) }));
|
|
6956
|
+
// Build container styles
|
|
6957
|
+
const containerStyle = {
|
|
6958
|
+
...(mode === 'viewport' ? { zIndex } : {}),
|
|
6959
|
+
...(maxWidthValue ? {
|
|
6960
|
+
maxWidth: maxWidthValue,
|
|
6961
|
+
marginLeft: 'auto',
|
|
6962
|
+
marginRight: 'auto'
|
|
6963
|
+
} : {}),
|
|
6964
|
+
};
|
|
6965
|
+
return (jsxRuntime.jsx("div", { className: `
|
|
6966
|
+
${getPositionClasses()}
|
|
6967
|
+
${variantStyle.container}
|
|
6968
|
+
border-t rounded-t-lg
|
|
6969
|
+
transition-all duration-300 ease-in-out
|
|
6970
|
+
flex flex-col
|
|
6971
|
+
${className}
|
|
6972
|
+
`, style: containerStyle, children: position === 'bottom' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, header] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [header, content] })) }));
|
|
6973
|
+
}
|
|
6974
|
+
/**
|
|
6975
|
+
* ExpandablePanelSpacer - Adds spacing to prevent content from being hidden behind the panel
|
|
6976
|
+
* Only needed in viewport mode. In container mode, the panel is part of the flex layout.
|
|
6977
|
+
*
|
|
6978
|
+
* @example
|
|
6979
|
+
* ```tsx
|
|
6980
|
+
* <div>
|
|
6981
|
+
* <MainContent />
|
|
6982
|
+
* <ExpandablePanelSpacer size="md" />
|
|
6983
|
+
* </div>
|
|
6984
|
+
* <ExpandablePanel mode="viewport" position="bottom" size="md" {...props} />
|
|
6985
|
+
* ```
|
|
6986
|
+
*/
|
|
6987
|
+
function ExpandablePanelSpacer({ size = 'md' }) {
|
|
6988
|
+
const heights = {
|
|
6989
|
+
sm: 'h-10',
|
|
6990
|
+
md: 'h-12',
|
|
6991
|
+
lg: 'h-14',
|
|
6992
|
+
};
|
|
6993
|
+
return jsxRuntime.jsx("div", { className: heights[size] });
|
|
6994
|
+
}
|
|
6995
|
+
/**
|
|
6996
|
+
* ExpandablePanelContainer - Wrapper that sets up proper layout for container mode
|
|
6997
|
+
* Use this to wrap your page content when using ExpandablePanel with mode="container"
|
|
6998
|
+
*
|
|
6999
|
+
* This creates a relative container with full height so the panel can position absolutely
|
|
7000
|
+
* at the bottom while the content scrolls above it.
|
|
7001
|
+
*
|
|
7002
|
+
* @example
|
|
7003
|
+
* ```tsx
|
|
7004
|
+
* <Page>
|
|
7005
|
+
* <ExpandablePanelContainer>
|
|
7006
|
+
* <div className="flex-1 overflow-auto p-4">
|
|
7007
|
+
* {pageContent}
|
|
7008
|
+
* </div>
|
|
7009
|
+
* <ExpandablePanel mode="container" {...props}>
|
|
7010
|
+
* {panelContent}
|
|
7011
|
+
* </ExpandablePanel>
|
|
7012
|
+
* </ExpandablePanelContainer>
|
|
7013
|
+
* </Page>
|
|
7014
|
+
* ```
|
|
7015
|
+
*/
|
|
7016
|
+
function ExpandablePanelContainer({ children, className = '', }) {
|
|
7017
|
+
return (jsxRuntime.jsx("div", { className: `relative h-full overflow-hidden ${className}`, children: children }));
|
|
7018
|
+
}
|
|
7019
|
+
|
|
5806
7020
|
// Tailwind breakpoint mappings
|
|
5807
7021
|
// sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
|
|
5808
7022
|
/**
|
|
@@ -5971,26 +7185,6 @@ function Hide({ children, above, below, only, className = '' }) {
|
|
|
5971
7185
|
}
|
|
5972
7186
|
return (jsxRuntime.jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
|
|
5973
7187
|
}
|
|
5974
|
-
/**
|
|
5975
|
-
* useMediaQuery hook - React hook for responsive logic in components
|
|
5976
|
-
*
|
|
5977
|
-
* @example
|
|
5978
|
-
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
5979
|
-
* const isDesktop = useMediaQuery('(min-width: 1024px)');
|
|
5980
|
-
*/
|
|
5981
|
-
function useMediaQuery(query) {
|
|
5982
|
-
const [matches, setMatches] = React.useState(false);
|
|
5983
|
-
React.useEffect(() => {
|
|
5984
|
-
const media = window.matchMedia(query);
|
|
5985
|
-
if (media.matches !== matches) {
|
|
5986
|
-
setMatches(media.matches);
|
|
5987
|
-
}
|
|
5988
|
-
const listener = () => setMatches(media.matches);
|
|
5989
|
-
media.addEventListener('change', listener);
|
|
5990
|
-
return () => media.removeEventListener('change', listener);
|
|
5991
|
-
}, [matches, query]);
|
|
5992
|
-
return matches;
|
|
5993
|
-
}
|
|
5994
7188
|
|
|
5995
7189
|
function Breadcrumbs({ items, showHome = true }) {
|
|
5996
7190
|
return (jsxRuntime.jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center space-x-2 text-sm", children: [showHome && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactRouterDom.Link, { to: "/", className: "text-ink-500 hover:text-ink-900 transition-colors", "aria-label": "Home", children: jsxRuntime.jsx(lucideReact.Home, { className: "h-4 w-4" }) }), items.length > 0 && jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4 text-ink-400" })] })), items.map((item, index) => {
|
|
@@ -6748,15 +7942,692 @@ function SidebarGroup({ title, items, onNavigate, defaultExpanded = true, curren
|
|
|
6748
7942
|
}
|
|
6749
7943
|
return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: "w-full flex items-center justify-between px-3 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wider hover:text-ink-700 transition-colors", children: [jsxRuntime.jsx("span", { children: title }), isExpanded ? (jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" })) : (jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }))] }), isExpanded && (jsxRuntime.jsx("div", { className: "mt-1 space-y-1", children: items.map((item) => (jsxRuntime.jsx(SidebarNavItem, { item: item, onNavigate: onNavigate, currentPath: currentPath }, item.id))) }))] }));
|
|
6750
7944
|
}
|
|
6751
|
-
|
|
6752
|
-
|
|
7945
|
+
/**
|
|
7946
|
+
* Sidebar - Navigation sidebar with mobile drawer support
|
|
7947
|
+
*
|
|
7948
|
+
* On desktop: Renders as a fixed-width sidebar
|
|
7949
|
+
* On mobile: Renders as a drawer overlay when mobileOpen is true
|
|
7950
|
+
*
|
|
7951
|
+
* @example Desktop usage (no mobile props)
|
|
7952
|
+
* ```tsx
|
|
7953
|
+
* <Sidebar
|
|
7954
|
+
* items={navItems}
|
|
7955
|
+
* header={<Logo />}
|
|
7956
|
+
* footer={<UserProfile />}
|
|
7957
|
+
* currentPath={location.pathname}
|
|
7958
|
+
* onNavigate={(href) => navigate(href)}
|
|
7959
|
+
* />
|
|
7960
|
+
* ```
|
|
7961
|
+
*
|
|
7962
|
+
* @example With mobile drawer support
|
|
7963
|
+
* ```tsx
|
|
7964
|
+
* const [mobileOpen, setMobileOpen] = useState(false);
|
|
7965
|
+
*
|
|
7966
|
+
* <Sidebar
|
|
7967
|
+
* items={navItems}
|
|
7968
|
+
* header={<Logo />}
|
|
7969
|
+
* mobileOpen={mobileOpen}
|
|
7970
|
+
* onMobileClose={() => setMobileOpen(false)}
|
|
7971
|
+
* onNavigate={(href) => {
|
|
7972
|
+
* navigate(href);
|
|
7973
|
+
* setMobileOpen(false); // Close drawer on navigation
|
|
7974
|
+
* }}
|
|
7975
|
+
* />
|
|
7976
|
+
* ```
|
|
7977
|
+
*/
|
|
7978
|
+
function Sidebar({ items, onNavigate, className = '', header, footer, currentPath, mobileOpen, onMobileClose, width = 'w-64', }) {
|
|
7979
|
+
const sidebarRef = React.useRef(null);
|
|
7980
|
+
const [isAnimating, setIsAnimating] = React.useState(false);
|
|
7981
|
+
const [shouldRender, setShouldRender] = React.useState(mobileOpen);
|
|
7982
|
+
// Handle animation states for mobile drawer
|
|
7983
|
+
React.useEffect(() => {
|
|
7984
|
+
if (mobileOpen) {
|
|
7985
|
+
setShouldRender(true);
|
|
7986
|
+
// Small delay to trigger animation
|
|
7987
|
+
requestAnimationFrame(() => {
|
|
7988
|
+
setIsAnimating(true);
|
|
7989
|
+
});
|
|
7990
|
+
return; // No cleanup needed when opening
|
|
7991
|
+
}
|
|
7992
|
+
else {
|
|
7993
|
+
setIsAnimating(false);
|
|
7994
|
+
// Wait for animation to complete before unmounting
|
|
7995
|
+
const timer = setTimeout(() => {
|
|
7996
|
+
setShouldRender(false);
|
|
7997
|
+
}, 300);
|
|
7998
|
+
return () => clearTimeout(timer);
|
|
7999
|
+
}
|
|
8000
|
+
}, [mobileOpen]);
|
|
8001
|
+
// Handle escape key for mobile drawer
|
|
8002
|
+
React.useEffect(() => {
|
|
8003
|
+
if (!mobileOpen)
|
|
8004
|
+
return;
|
|
8005
|
+
const handleEscape = (e) => {
|
|
8006
|
+
if (e.key === 'Escape') {
|
|
8007
|
+
onMobileClose?.();
|
|
8008
|
+
}
|
|
8009
|
+
};
|
|
8010
|
+
document.addEventListener('keydown', handleEscape);
|
|
8011
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
8012
|
+
}, [mobileOpen, onMobileClose]);
|
|
8013
|
+
// Lock body scroll when mobile drawer is open
|
|
8014
|
+
React.useEffect(() => {
|
|
8015
|
+
if (mobileOpen) {
|
|
8016
|
+
document.body.style.overflow = 'hidden';
|
|
8017
|
+
}
|
|
8018
|
+
else {
|
|
8019
|
+
document.body.style.overflow = '';
|
|
8020
|
+
}
|
|
8021
|
+
return () => {
|
|
8022
|
+
document.body.style.overflow = '';
|
|
8023
|
+
};
|
|
8024
|
+
}, [mobileOpen]);
|
|
8025
|
+
// Handle navigation with auto-close on mobile
|
|
8026
|
+
const handleNavigate = (href, external) => {
|
|
8027
|
+
onNavigate?.(href, external);
|
|
8028
|
+
// Auto-close mobile drawer on navigation
|
|
8029
|
+
if (mobileOpen) {
|
|
8030
|
+
onMobileClose?.();
|
|
8031
|
+
}
|
|
8032
|
+
};
|
|
8033
|
+
// Sidebar content (shared between desktop and mobile)
|
|
8034
|
+
const sidebarContent = (jsxRuntime.jsxs("div", { ref: sidebarRef, className: `flex flex-col h-full bg-white border-r border-paper-300 notebook-binding ${width} ${className}`, children: [mobileOpen !== undefined && (jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 pt-4 md:hidden", children: [jsxRuntime.jsx("div", { className: "flex-1", children: header }), jsxRuntime.jsx("button", { onClick: onMobileClose, className: "\n flex items-center justify-center\n w-10 h-10 -mr-2\n text-ink-500 hover:text-ink-700\n hover:bg-paper-100 rounded-full\n transition-colors\n ", "aria-label": "Close sidebar", children: jsxRuntime.jsx(lucideReact.X, { className: "w-5 h-5" }) })] })), header && mobileOpen === undefined && (jsxRuntime.jsx("div", { className: "px-6 pt-6 pb-4", children: header })), header && mobileOpen !== undefined && (jsxRuntime.jsx("div", { className: "px-6 pt-2 pb-4 hidden md:block", children: header })), jsxRuntime.jsx("nav", { className: "flex-1 px-3 py-2 space-y-1 overflow-y-auto", children: items.map((item) => {
|
|
6753
8035
|
// Render separator
|
|
6754
8036
|
if (item.separator) {
|
|
6755
8037
|
return jsxRuntime.jsx("div", { className: "my-4 border-t border-paper-300" }, item.id);
|
|
6756
8038
|
}
|
|
6757
8039
|
// Render nav item
|
|
6758
|
-
return (jsxRuntime.jsx(SidebarNavItem, { item: item, onNavigate:
|
|
8040
|
+
return (jsxRuntime.jsx(SidebarNavItem, { item: item, onNavigate: handleNavigate, currentPath: currentPath }, item.id));
|
|
6759
8041
|
}) }), footer && (jsxRuntime.jsx("div", { className: "border-t border-paper-300 pl-2 pr-6 py-4 overflow-visible", children: footer }))] }));
|
|
8042
|
+
// If mobileOpen is not defined, render as regular sidebar (desktop mode)
|
|
8043
|
+
if (mobileOpen === undefined) {
|
|
8044
|
+
return sidebarContent;
|
|
8045
|
+
}
|
|
8046
|
+
// Mobile drawer mode
|
|
8047
|
+
if (!shouldRender) {
|
|
8048
|
+
return null;
|
|
8049
|
+
}
|
|
8050
|
+
return reactDom.createPortal(jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 md:hidden", role: "dialog", "aria-modal": "true", "aria-label": "Navigation menu", children: [jsxRuntime.jsx("div", { className: `
|
|
8051
|
+
absolute inset-0 bg-ink-900/50 backdrop-blur-sm
|
|
8052
|
+
transition-opacity duration-300
|
|
8053
|
+
${isAnimating ? 'opacity-100' : 'opacity-0'}
|
|
8054
|
+
`, onClick: onMobileClose, "aria-hidden": "true" }), jsxRuntime.jsx("div", { className: `
|
|
8055
|
+
absolute inset-y-0 left-0 flex max-w-full
|
|
8056
|
+
transition-transform duration-300 ease-out
|
|
8057
|
+
${isAnimating ? 'translate-x-0' : '-translate-x-full'}
|
|
8058
|
+
`, children: sidebarContent })] }), document.body);
|
|
8059
|
+
}
|
|
8060
|
+
|
|
8061
|
+
/**
|
|
8062
|
+
* BottomNavigation - Mobile-style bottom tab bar
|
|
8063
|
+
*
|
|
8064
|
+
* iOS/Android-style fixed bottom navigation with icons, labels, and badges.
|
|
8065
|
+
* Handles safe area insets for notched devices automatically.
|
|
8066
|
+
*
|
|
8067
|
+
* Best practices:
|
|
8068
|
+
* - Use 3-5 items maximum
|
|
8069
|
+
* - Keep labels short (1-2 words)
|
|
8070
|
+
* - Use consistent icon style
|
|
8071
|
+
*
|
|
8072
|
+
* @example Basic usage
|
|
8073
|
+
* ```tsx
|
|
8074
|
+
* import { BottomNavigation } from 'notebook-ui';
|
|
8075
|
+
* import { Home, Search, Bell, User } from 'lucide-react';
|
|
8076
|
+
*
|
|
8077
|
+
* const navItems = [
|
|
8078
|
+
* { id: 'home', label: 'Home', icon: <Home />, href: '/' },
|
|
8079
|
+
* { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
|
|
8080
|
+
* { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3 },
|
|
8081
|
+
* { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' },
|
|
8082
|
+
* ];
|
|
8083
|
+
*
|
|
8084
|
+
* <BottomNavigation
|
|
8085
|
+
* items={navItems}
|
|
8086
|
+
* activeId="home"
|
|
8087
|
+
* onNavigate={(id, href) => navigate(href)}
|
|
8088
|
+
* />
|
|
8089
|
+
* ```
|
|
8090
|
+
*
|
|
8091
|
+
* @example With onClick handlers
|
|
8092
|
+
* ```tsx
|
|
8093
|
+
* const navItems = [
|
|
8094
|
+
* { id: 'home', label: 'Home', icon: <Home />, onClick: () => setTab('home') },
|
|
8095
|
+
* { id: 'add', label: 'Add', icon: <Plus />, onClick: openAddModal },
|
|
8096
|
+
* ];
|
|
8097
|
+
*
|
|
8098
|
+
* <BottomNavigation items={navItems} activeId={currentTab} />
|
|
8099
|
+
* ```
|
|
8100
|
+
*/
|
|
8101
|
+
function BottomNavigation({ items, activeId, onNavigate, showLabels = true, className = '', safeArea = true, }) {
|
|
8102
|
+
const handleItemClick = (item) => {
|
|
8103
|
+
if (item.disabled)
|
|
8104
|
+
return;
|
|
8105
|
+
if (item.onClick) {
|
|
8106
|
+
item.onClick();
|
|
8107
|
+
}
|
|
8108
|
+
if (onNavigate) {
|
|
8109
|
+
onNavigate(item.id, item.href);
|
|
8110
|
+
}
|
|
8111
|
+
};
|
|
8112
|
+
return (jsxRuntime.jsx("nav", { className: `
|
|
8113
|
+
fixed bottom-0 left-0 right-0 z-40
|
|
8114
|
+
bg-white border-t border-paper-200 shadow-lg
|
|
8115
|
+
${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}
|
|
8116
|
+
${className}
|
|
8117
|
+
`, role: "navigation", "aria-label": "Bottom navigation", children: jsxRuntime.jsx("div", { className: "flex items-center justify-around h-14 max-w-lg mx-auto px-2", children: items.map((item) => {
|
|
8118
|
+
const isActive = item.id === activeId;
|
|
8119
|
+
return (jsxRuntime.jsxs("button", { onClick: () => handleItemClick(item), disabled: item.disabled, className: `
|
|
8120
|
+
relative flex flex-col items-center justify-center
|
|
8121
|
+
flex-1 h-full min-w-touch-sm
|
|
8122
|
+
transition-colors duration-200
|
|
8123
|
+
${item.disabled
|
|
8124
|
+
? 'opacity-40 cursor-not-allowed'
|
|
8125
|
+
: 'active:bg-paper-100'}
|
|
8126
|
+
${isActive
|
|
8127
|
+
? 'text-accent-600'
|
|
8128
|
+
: 'text-ink-500 hover:text-ink-700'}
|
|
8129
|
+
`, "aria-current": isActive ? 'page' : undefined, "aria-label": item.label, children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("div", { className: `
|
|
8130
|
+
w-6 h-6 flex items-center justify-center
|
|
8131
|
+
transition-transform duration-200
|
|
8132
|
+
${isActive ? 'scale-110' : 'scale-100'}
|
|
8133
|
+
`, children: React.isValidElement(item.icon)
|
|
8134
|
+
? React.cloneElement(item.icon, {
|
|
8135
|
+
className: 'w-6 h-6',
|
|
8136
|
+
})
|
|
8137
|
+
: item.icon }), item.badge !== undefined && item.badge > 0 && (jsxRuntime.jsx("span", { className: `
|
|
8138
|
+
absolute -top-1 -right-2.5
|
|
8139
|
+
min-w-[18px] h-[18px] px-1
|
|
8140
|
+
flex items-center justify-center
|
|
8141
|
+
text-[10px] font-bold text-white
|
|
8142
|
+
bg-error-500 rounded-full
|
|
8143
|
+
${item.badge > 99 ? 'text-[8px]' : ''}
|
|
8144
|
+
`, children: item.badge > 99 ? '99+' : item.badge }))] }), showLabels && (jsxRuntime.jsx("span", { className: `
|
|
8145
|
+
mt-1 text-[10px] font-medium leading-none
|
|
8146
|
+
transition-opacity duration-200
|
|
8147
|
+
truncate max-w-full px-1
|
|
8148
|
+
${isActive ? 'opacity-100' : 'opacity-70'}
|
|
8149
|
+
`, children: item.label })), isActive && (jsxRuntime.jsx("div", { className: "\n absolute top-0 left-1/2 -translate-x-1/2\n w-8 h-0.5 bg-accent-500 rounded-full\n " }))] }, item.id));
|
|
8150
|
+
}) }) }));
|
|
8151
|
+
}
|
|
8152
|
+
/**
|
|
8153
|
+
* BottomNavigationSpacer - Spacer to prevent content from being hidden behind BottomNavigation
|
|
8154
|
+
*
|
|
8155
|
+
* Place this at the bottom of your scrollable content when using BottomNavigation.
|
|
8156
|
+
*
|
|
8157
|
+
* @example
|
|
8158
|
+
* ```tsx
|
|
8159
|
+
* <div className="flex flex-col h-screen">
|
|
8160
|
+
* <main className="flex-1 overflow-auto">
|
|
8161
|
+
* {/* Your content *\/}
|
|
8162
|
+
* <BottomNavigationSpacer />
|
|
8163
|
+
* </main>
|
|
8164
|
+
* <BottomNavigation items={navItems} />
|
|
8165
|
+
* </div>
|
|
8166
|
+
* ```
|
|
8167
|
+
*/
|
|
8168
|
+
function BottomNavigationSpacer({ safeArea = true }) {
|
|
8169
|
+
return (jsxRuntime.jsx("div", { className: `h-14 ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}`, "aria-hidden": "true" }));
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
/**
|
|
8173
|
+
* MobileHeader - Mobile app header with navigation controls
|
|
8174
|
+
*
|
|
8175
|
+
* A flexible mobile header component with support for:
|
|
8176
|
+
* - Hamburger menu button (default)
|
|
8177
|
+
* - Back navigation arrow
|
|
8178
|
+
* - Close button (X)
|
|
8179
|
+
* - Custom left/right actions
|
|
8180
|
+
* - Sticky positioning
|
|
8181
|
+
* - Blur/transparent variants
|
|
8182
|
+
*
|
|
8183
|
+
* @example Basic with menu button
|
|
8184
|
+
* ```tsx
|
|
8185
|
+
* <MobileHeader
|
|
8186
|
+
* title="Dashboard"
|
|
8187
|
+
* onMenuClick={() => setDrawerOpen(true)}
|
|
8188
|
+
* />
|
|
8189
|
+
* ```
|
|
8190
|
+
*
|
|
8191
|
+
* @example With back button
|
|
8192
|
+
* ```tsx
|
|
8193
|
+
* <MobileHeader
|
|
8194
|
+
* title="User Details"
|
|
8195
|
+
* subtitle="Profile"
|
|
8196
|
+
* onBackClick={() => navigate(-1)}
|
|
8197
|
+
* />
|
|
8198
|
+
* ```
|
|
8199
|
+
*
|
|
8200
|
+
* @example With right action
|
|
8201
|
+
* ```tsx
|
|
8202
|
+
* <MobileHeader
|
|
8203
|
+
* title="Settings"
|
|
8204
|
+
* onMenuClick={openMenu}
|
|
8205
|
+
* rightAction={
|
|
8206
|
+
* <Button variant="ghost" iconOnly onClick={save}>
|
|
8207
|
+
* <Check className="w-5 h-5" />
|
|
8208
|
+
* </Button>
|
|
8209
|
+
* }
|
|
8210
|
+
* />
|
|
8211
|
+
* ```
|
|
8212
|
+
*
|
|
8213
|
+
* @example Transparent with blur
|
|
8214
|
+
* ```tsx
|
|
8215
|
+
* <MobileHeader
|
|
8216
|
+
* title="Photo Gallery"
|
|
8217
|
+
* variant="blur"
|
|
8218
|
+
* onBackClick={goBack}
|
|
8219
|
+
* />
|
|
8220
|
+
* ```
|
|
8221
|
+
*/
|
|
8222
|
+
function MobileHeader({ title, subtitle, onMenuClick, onBackClick, onCloseClick, rightAction, leftAction, sticky = true, bordered = true, variant = 'solid', className = '', safeArea = true, }) {
|
|
8223
|
+
// Determine which left button to show
|
|
8224
|
+
const renderLeftButton = () => {
|
|
8225
|
+
// Custom left action takes priority
|
|
8226
|
+
if (leftAction) {
|
|
8227
|
+
return leftAction;
|
|
8228
|
+
}
|
|
8229
|
+
// Close button
|
|
8230
|
+
if (onCloseClick) {
|
|
8231
|
+
return (jsxRuntime.jsx("button", { onClick: onCloseClick, className: "\n flex items-center justify-center\n w-10 h-10 -ml-2\n text-ink-600 hover:text-ink-900\n hover:bg-paper-100 rounded-full\n transition-colors duration-200\n active:bg-paper-200\n ", "aria-label": "Close", children: jsxRuntime.jsx(lucideReact.X, { className: "w-6 h-6" }) }));
|
|
8232
|
+
}
|
|
8233
|
+
// Back button
|
|
8234
|
+
if (onBackClick) {
|
|
8235
|
+
return (jsxRuntime.jsx("button", { onClick: onBackClick, className: "\n flex items-center justify-center\n w-10 h-10 -ml-2\n text-ink-600 hover:text-ink-900\n hover:bg-paper-100 rounded-full\n transition-colors duration-200\n active:bg-paper-200\n ", "aria-label": "Go back", children: jsxRuntime.jsx(lucideReact.ChevronLeft, { className: "w-6 h-6" }) }));
|
|
8236
|
+
}
|
|
8237
|
+
// Menu button (default)
|
|
8238
|
+
if (onMenuClick) {
|
|
8239
|
+
return (jsxRuntime.jsx("button", { onClick: onMenuClick, className: "\n flex items-center justify-center\n w-10 h-10 -ml-2\n text-ink-600 hover:text-ink-900\n hover:bg-paper-100 rounded-full\n transition-colors duration-200\n active:bg-paper-200\n ", "aria-label": "Open menu", children: jsxRuntime.jsx(lucideReact.Menu, { className: "w-6 h-6" }) }));
|
|
8240
|
+
}
|
|
8241
|
+
// No left button
|
|
8242
|
+
return jsxRuntime.jsx("div", { className: "w-10 h-10" });
|
|
8243
|
+
};
|
|
8244
|
+
// Background variant styles
|
|
8245
|
+
const variantStyles = {
|
|
8246
|
+
solid: 'bg-white',
|
|
8247
|
+
transparent: 'bg-transparent',
|
|
8248
|
+
blur: 'bg-white/80 backdrop-blur-md',
|
|
8249
|
+
};
|
|
8250
|
+
return (jsxRuntime.jsx("header", { className: `
|
|
8251
|
+
${sticky ? 'sticky top-0 z-30' : ''}
|
|
8252
|
+
${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}
|
|
8253
|
+
${variantStyles[variant]}
|
|
8254
|
+
${bordered ? 'border-b border-paper-200' : ''}
|
|
8255
|
+
${className}
|
|
8256
|
+
`, children: jsxRuntime.jsxs("div", { className: "flex items-center justify-between h-14 px-4", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [renderLeftButton(), jsxRuntime.jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [subtitle && (jsxRuntime.jsx("span", { className: "text-xs text-ink-500 truncate", children: subtitle })), jsxRuntime.jsx("h1", { className: "text-lg font-semibold text-ink-900 truncate leading-tight", children: title })] })] }), jsxRuntime.jsx("div", { className: "flex items-center gap-1 ml-2 flex-shrink-0", children: rightAction })] }) }));
|
|
8257
|
+
}
|
|
8258
|
+
/**
|
|
8259
|
+
* MobileHeaderSpacer - Spacer to prevent content from being hidden behind sticky MobileHeader
|
|
8260
|
+
*
|
|
8261
|
+
* Place this at the top of your content when NOT using sticky header
|
|
8262
|
+
* to maintain consistent spacing.
|
|
8263
|
+
*
|
|
8264
|
+
* @example
|
|
8265
|
+
* ```tsx
|
|
8266
|
+
* <MobileHeader title="Page" sticky={false} />
|
|
8267
|
+
* <MobileHeaderSpacer />
|
|
8268
|
+
* <main>Content here</main>
|
|
8269
|
+
* ```
|
|
8270
|
+
*/
|
|
8271
|
+
function MobileHeaderSpacer({ safeArea = true }) {
|
|
8272
|
+
return (jsxRuntime.jsx("div", { className: `h-14 ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}`, "aria-hidden": "true" }));
|
|
8273
|
+
}
|
|
8274
|
+
|
|
8275
|
+
const positionClasses = {
|
|
8276
|
+
'bottom-right': 'right-4 bottom-4',
|
|
8277
|
+
'bottom-left': 'left-4 bottom-4',
|
|
8278
|
+
'bottom-center': 'left-1/2 -translate-x-1/2 bottom-4',
|
|
8279
|
+
};
|
|
8280
|
+
const variantClasses = {
|
|
8281
|
+
primary: 'bg-accent-600 hover:bg-accent-700 text-white shadow-lg',
|
|
8282
|
+
secondary: 'bg-white hover:bg-paper-50 text-ink-700 shadow-lg border border-paper-200',
|
|
8283
|
+
accent: 'bg-accent-500 hover:bg-accent-600 text-white shadow-lg',
|
|
8284
|
+
};
|
|
8285
|
+
const sizeClasses$1 = {
|
|
8286
|
+
md: 'w-14 h-14',
|
|
8287
|
+
lg: 'w-16 h-16',
|
|
8288
|
+
};
|
|
8289
|
+
const iconSizeClasses = {
|
|
8290
|
+
md: 'h-6 w-6',
|
|
8291
|
+
lg: 'h-7 w-7',
|
|
8292
|
+
};
|
|
8293
|
+
/**
|
|
8294
|
+
* FloatingActionButton - Material Design style FAB for mobile
|
|
8295
|
+
*
|
|
8296
|
+
* A prominent button for the primary action on a screen.
|
|
8297
|
+
* Supports single action or expandable menu with multiple actions.
|
|
8298
|
+
*
|
|
8299
|
+
* @example Simple FAB
|
|
8300
|
+
* ```tsx
|
|
8301
|
+
* <FloatingActionButton
|
|
8302
|
+
* onClick={() => openCreateModal()}
|
|
8303
|
+
* label="Create new item"
|
|
8304
|
+
* />
|
|
8305
|
+
* ```
|
|
8306
|
+
*
|
|
8307
|
+
* @example FAB with action menu
|
|
8308
|
+
* ```tsx
|
|
8309
|
+
* <FloatingActionButton
|
|
8310
|
+
* actions={[
|
|
8311
|
+
* { id: 'photo', icon: <Camera />, label: 'Take Photo', onClick: takePhoto },
|
|
8312
|
+
* { id: 'upload', icon: <Upload />, label: 'Upload File', onClick: uploadFile },
|
|
8313
|
+
* { id: 'note', icon: <FileText />, label: 'Create Note', onClick: createNote },
|
|
8314
|
+
* ]}
|
|
8315
|
+
* />
|
|
8316
|
+
* ```
|
|
8317
|
+
*
|
|
8318
|
+
* @example Extended FAB
|
|
8319
|
+
* ```tsx
|
|
8320
|
+
* <FloatingActionButton
|
|
8321
|
+
* extended
|
|
8322
|
+
* extendedLabel="New Task"
|
|
8323
|
+
* icon={<Plus />}
|
|
8324
|
+
* onClick={createTask}
|
|
8325
|
+
* />
|
|
8326
|
+
* ```
|
|
8327
|
+
*/
|
|
8328
|
+
function FloatingActionButton({ onClick, icon, actions, position = 'bottom-right', variant = 'primary', size = 'md', label = 'Action button', extended = false, extendedLabel, hidden = false, offset, className = '', }) {
|
|
8329
|
+
const [isMenuOpen, setIsMenuOpen] = React.useState(false);
|
|
8330
|
+
const fabRef = React.useRef(null);
|
|
8331
|
+
const hasMenu = actions && actions.length > 0;
|
|
8332
|
+
// Close menu on escape
|
|
8333
|
+
React.useEffect(() => {
|
|
8334
|
+
if (!isMenuOpen)
|
|
8335
|
+
return;
|
|
8336
|
+
const handleEscape = (e) => {
|
|
8337
|
+
if (e.key === 'Escape') {
|
|
8338
|
+
setIsMenuOpen(false);
|
|
8339
|
+
}
|
|
8340
|
+
};
|
|
8341
|
+
document.addEventListener('keydown', handleEscape);
|
|
8342
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
8343
|
+
}, [isMenuOpen]);
|
|
8344
|
+
// Close menu on click outside
|
|
8345
|
+
React.useEffect(() => {
|
|
8346
|
+
if (!isMenuOpen)
|
|
8347
|
+
return;
|
|
8348
|
+
const handleClickOutside = (e) => {
|
|
8349
|
+
if (fabRef.current && !fabRef.current.contains(e.target)) {
|
|
8350
|
+
setIsMenuOpen(false);
|
|
8351
|
+
}
|
|
8352
|
+
};
|
|
8353
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
8354
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
8355
|
+
}, [isMenuOpen]);
|
|
8356
|
+
const handleClick = () => {
|
|
8357
|
+
if (hasMenu) {
|
|
8358
|
+
setIsMenuOpen(!isMenuOpen);
|
|
8359
|
+
}
|
|
8360
|
+
else if (onClick) {
|
|
8361
|
+
onClick();
|
|
8362
|
+
}
|
|
8363
|
+
};
|
|
8364
|
+
const handleActionClick = (action) => {
|
|
8365
|
+
if (!action.disabled) {
|
|
8366
|
+
action.onClick();
|
|
8367
|
+
setIsMenuOpen(false);
|
|
8368
|
+
}
|
|
8369
|
+
};
|
|
8370
|
+
// Custom offset styles
|
|
8371
|
+
const offsetStyle = offset ? {
|
|
8372
|
+
...(offset.x !== undefined && position.includes('right') ? { right: `${offset.x}px` } : {}),
|
|
8373
|
+
...(offset.x !== undefined && position.includes('left') ? { left: `${offset.x}px` } : {}),
|
|
8374
|
+
...(offset.y !== undefined ? { bottom: `${offset.y}px` } : {}),
|
|
8375
|
+
} : {};
|
|
8376
|
+
const fabContent = (jsxRuntime.jsxs("div", { className: `
|
|
8377
|
+
fixed z-40 transition-all duration-300
|
|
8378
|
+
${positionClasses[position]}
|
|
8379
|
+
${hidden ? 'translate-y-20 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
|
|
8380
|
+
${className}
|
|
8381
|
+
`, style: {
|
|
8382
|
+
...offsetStyle,
|
|
8383
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
8384
|
+
}, children: [hasMenu && isMenuOpen && (jsxRuntime.jsx("div", { className: "absolute bottom-full mb-3 flex flex-col-reverse gap-3 items-center", children: actions.map((action, index) => (jsxRuntime.jsxs("div", { className: "flex items-center gap-3 animate-fade-in", style: { animationDelay: `${index * 50}ms` }, children: [jsxRuntime.jsx("span", { className: "bg-ink-900/80 text-white text-sm px-3 py-1.5 rounded-lg whitespace-nowrap", children: action.label }), jsxRuntime.jsx("button", { onClick: () => handleActionClick(action), disabled: action.disabled, className: `
|
|
8385
|
+
w-12 h-12 rounded-full flex items-center justify-center
|
|
8386
|
+
transition-all duration-200
|
|
8387
|
+
${action.disabled
|
|
8388
|
+
? 'bg-paper-200 text-ink-400 cursor-not-allowed'
|
|
8389
|
+
: 'bg-white text-ink-700 shadow-lg hover:bg-paper-50 active:scale-95'}
|
|
8390
|
+
`, "aria-label": action.label, children: action.icon })] }, action.id))) })), hasMenu && isMenuOpen && (jsxRuntime.jsx("div", { className: "fixed inset-0 bg-black/20 -z-10 animate-fade-in", onClick: () => setIsMenuOpen(false) })), jsxRuntime.jsxs("button", { ref: fabRef, onClick: handleClick, className: `
|
|
8391
|
+
${extended ? 'px-6 rounded-full' : 'rounded-full'}
|
|
8392
|
+
${extended ? 'h-14' : sizeClasses$1[size]}
|
|
8393
|
+
${variantClasses[variant]}
|
|
8394
|
+
flex items-center justify-center gap-2
|
|
8395
|
+
transition-all duration-200
|
|
8396
|
+
active:scale-95
|
|
8397
|
+
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
|
|
8398
|
+
`, "aria-label": label, "aria-expanded": hasMenu ? isMenuOpen : undefined, "aria-haspopup": hasMenu ? 'menu' : undefined, children: [hasMenu && isMenuOpen ? (jsxRuntime.jsx(lucideReact.X, { className: iconSizeClasses[size] })) : (icon || jsxRuntime.jsx(lucideReact.Plus, { className: iconSizeClasses[size] })), extended && extendedLabel && (jsxRuntime.jsx("span", { className: "font-medium", children: extendedLabel }))] })] }));
|
|
8399
|
+
// Render via portal to ensure proper stacking
|
|
8400
|
+
return reactDom.createPortal(fabContent, document.body);
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8403
|
+
* Hook for scroll-based FAB visibility
|
|
8404
|
+
*
|
|
8405
|
+
* @example
|
|
8406
|
+
* ```tsx
|
|
8407
|
+
* const { hidden, scrollDirection } = useFABScroll();
|
|
8408
|
+
* <FloatingActionButton hidden={hidden} />
|
|
8409
|
+
* ```
|
|
8410
|
+
*/
|
|
8411
|
+
function useFABScroll(threshold = 10) {
|
|
8412
|
+
const [hidden, setHidden] = React.useState(false);
|
|
8413
|
+
const [scrollDirection, setScrollDirection] = React.useState(null);
|
|
8414
|
+
const lastScrollY = React.useRef(0);
|
|
8415
|
+
React.useEffect(() => {
|
|
8416
|
+
const handleScroll = () => {
|
|
8417
|
+
const currentScrollY = window.scrollY;
|
|
8418
|
+
const diff = currentScrollY - lastScrollY.current;
|
|
8419
|
+
if (Math.abs(diff) > threshold) {
|
|
8420
|
+
if (diff > 0) {
|
|
8421
|
+
setHidden(true);
|
|
8422
|
+
setScrollDirection('down');
|
|
8423
|
+
}
|
|
8424
|
+
else {
|
|
8425
|
+
setHidden(false);
|
|
8426
|
+
setScrollDirection('up');
|
|
8427
|
+
}
|
|
8428
|
+
lastScrollY.current = currentScrollY;
|
|
8429
|
+
}
|
|
8430
|
+
};
|
|
8431
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
8432
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
8433
|
+
}, [threshold]);
|
|
8434
|
+
return { hidden, scrollDirection };
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
/**
|
|
8438
|
+
* PullToRefresh - Mobile pull-to-refresh gesture handler
|
|
8439
|
+
*
|
|
8440
|
+
* Wraps content and provides native-feeling pull-to-refresh functionality.
|
|
8441
|
+
* Only activates when scrolled to top of content.
|
|
8442
|
+
*
|
|
8443
|
+
* @example Basic usage
|
|
8444
|
+
* ```tsx
|
|
8445
|
+
* <PullToRefresh onRefresh={async () => {
|
|
8446
|
+
* await fetchLatestData();
|
|
8447
|
+
* }}>
|
|
8448
|
+
* <div className="min-h-screen">
|
|
8449
|
+
* {content}
|
|
8450
|
+
* </div>
|
|
8451
|
+
* </PullToRefresh>
|
|
8452
|
+
* ```
|
|
8453
|
+
*
|
|
8454
|
+
* @example With custom threshold
|
|
8455
|
+
* ```tsx
|
|
8456
|
+
* <PullToRefresh
|
|
8457
|
+
* onRefresh={handleRefresh}
|
|
8458
|
+
* pullThreshold={100}
|
|
8459
|
+
* maxPull={150}
|
|
8460
|
+
* >
|
|
8461
|
+
* {content}
|
|
8462
|
+
* </PullToRefresh>
|
|
8463
|
+
* ```
|
|
8464
|
+
*/
|
|
8465
|
+
function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold = 80, maxPull = 120, loadingIndicator, pullIndicator, className = '', }) {
|
|
8466
|
+
const [state, setState] = React.useState('idle');
|
|
8467
|
+
const [pullDistance, setPullDistance] = React.useState(0);
|
|
8468
|
+
const containerRef = React.useRef(null);
|
|
8469
|
+
const startY = React.useRef(0);
|
|
8470
|
+
const currentY = React.useRef(0);
|
|
8471
|
+
// Check if at top of scroll container
|
|
8472
|
+
const isAtTop = React.useCallback(() => {
|
|
8473
|
+
const container = containerRef.current;
|
|
8474
|
+
if (!container)
|
|
8475
|
+
return false;
|
|
8476
|
+
return container.scrollTop <= 0;
|
|
8477
|
+
}, []);
|
|
8478
|
+
// Handle touch start
|
|
8479
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
8480
|
+
if (disabled || state === 'refreshing' || !isAtTop())
|
|
8481
|
+
return;
|
|
8482
|
+
startY.current = e.touches[0].clientY;
|
|
8483
|
+
currentY.current = startY.current;
|
|
8484
|
+
}, [disabled, state, isAtTop]);
|
|
8485
|
+
// Handle touch move
|
|
8486
|
+
const handleTouchMove = React.useCallback((e) => {
|
|
8487
|
+
if (disabled || state === 'refreshing')
|
|
8488
|
+
return;
|
|
8489
|
+
if (startY.current === 0)
|
|
8490
|
+
return;
|
|
8491
|
+
currentY.current = e.touches[0].clientY;
|
|
8492
|
+
const diff = currentY.current - startY.current;
|
|
8493
|
+
// Only allow pulling down when at top
|
|
8494
|
+
if (diff > 0 && isAtTop()) {
|
|
8495
|
+
// Apply resistance - pull slows down as distance increases
|
|
8496
|
+
const resistance = 0.5;
|
|
8497
|
+
const adjustedPull = Math.min(diff * resistance, maxPull);
|
|
8498
|
+
setPullDistance(adjustedPull);
|
|
8499
|
+
setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
|
|
8500
|
+
// Prevent default scroll when pulling
|
|
8501
|
+
if (adjustedPull > 0) {
|
|
8502
|
+
e.preventDefault();
|
|
8503
|
+
}
|
|
8504
|
+
}
|
|
8505
|
+
}, [disabled, state, isAtTop, maxPull, pullThreshold]);
|
|
8506
|
+
// Handle touch end
|
|
8507
|
+
const handleTouchEnd = React.useCallback(async () => {
|
|
8508
|
+
if (disabled || state === 'refreshing')
|
|
8509
|
+
return;
|
|
8510
|
+
if (state === 'ready') {
|
|
8511
|
+
setState('refreshing');
|
|
8512
|
+
setPullDistance(pullThreshold); // Hold at threshold while refreshing
|
|
8513
|
+
try {
|
|
8514
|
+
await onRefresh();
|
|
8515
|
+
}
|
|
8516
|
+
catch (error) {
|
|
8517
|
+
console.error('Refresh failed:', error);
|
|
8518
|
+
}
|
|
8519
|
+
setState('idle');
|
|
8520
|
+
}
|
|
8521
|
+
setPullDistance(0);
|
|
8522
|
+
startY.current = 0;
|
|
8523
|
+
currentY.current = 0;
|
|
8524
|
+
}, [disabled, state, pullThreshold, onRefresh]);
|
|
8525
|
+
// Attach touch listeners
|
|
8526
|
+
React.useEffect(() => {
|
|
8527
|
+
const container = containerRef.current;
|
|
8528
|
+
if (!container)
|
|
8529
|
+
return;
|
|
8530
|
+
container.addEventListener('touchstart', handleTouchStart, { passive: true });
|
|
8531
|
+
container.addEventListener('touchmove', handleTouchMove, { passive: false });
|
|
8532
|
+
container.addEventListener('touchend', handleTouchEnd);
|
|
8533
|
+
return () => {
|
|
8534
|
+
container.removeEventListener('touchstart', handleTouchStart);
|
|
8535
|
+
container.removeEventListener('touchmove', handleTouchMove);
|
|
8536
|
+
container.removeEventListener('touchend', handleTouchEnd);
|
|
8537
|
+
};
|
|
8538
|
+
}, [handleTouchStart, handleTouchMove, handleTouchEnd]);
|
|
8539
|
+
// Calculate indicator opacity and rotation
|
|
8540
|
+
const progress = Math.min(pullDistance / pullThreshold, 1);
|
|
8541
|
+
const rotation = progress * 180;
|
|
8542
|
+
// Default loading indicator
|
|
8543
|
+
const defaultLoadingIndicator = (jsxRuntime.jsx(lucideReact.Loader2, { className: "h-6 w-6 text-accent-600 animate-spin" }));
|
|
8544
|
+
// Default pull indicator
|
|
8545
|
+
const defaultPullIndicator = (jsxRuntime.jsx("div", { className: `
|
|
8546
|
+
transition-transform duration-200
|
|
8547
|
+
${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
|
|
8548
|
+
`, style: { transform: `rotate(${rotation}deg)` }, children: jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-6 w-6" }) }));
|
|
8549
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, className: `relative overflow-auto ${className}`, style: { touchAction: pullDistance > 0 ? 'none' : 'auto' }, children: [jsxRuntime.jsx("div", { className: `
|
|
8550
|
+
absolute left-0 right-0 flex items-center justify-center
|
|
8551
|
+
transition-all duration-200 overflow-hidden
|
|
8552
|
+
${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
|
|
8553
|
+
`, style: {
|
|
8554
|
+
height: `${pullDistance}px`,
|
|
8555
|
+
top: 0,
|
|
8556
|
+
zIndex: 10,
|
|
8557
|
+
}, children: jsxRuntime.jsx("div", { className: `
|
|
8558
|
+
w-10 h-10 rounded-full bg-white shadow-md
|
|
8559
|
+
flex items-center justify-center
|
|
8560
|
+
transition-transform duration-200
|
|
8561
|
+
${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
|
|
8562
|
+
`, children: state === 'refreshing'
|
|
8563
|
+
? (loadingIndicator || defaultLoadingIndicator)
|
|
8564
|
+
: (pullIndicator || defaultPullIndicator) }) }), jsxRuntime.jsx("div", { className: "transition-transform duration-200", style: {
|
|
8565
|
+
transform: `translateY(${pullDistance}px)`,
|
|
8566
|
+
}, children: children })] }));
|
|
8567
|
+
}
|
|
8568
|
+
/**
|
|
8569
|
+
* usePullToRefresh - Hook for custom pull-to-refresh implementations
|
|
8570
|
+
*
|
|
8571
|
+
* @example
|
|
8572
|
+
* ```tsx
|
|
8573
|
+
* const { pullDistance, isRefreshing, bind } = usePullToRefresh({
|
|
8574
|
+
* onRefresh: async () => {
|
|
8575
|
+
* await fetchData();
|
|
8576
|
+
* }
|
|
8577
|
+
* });
|
|
8578
|
+
*
|
|
8579
|
+
* return (
|
|
8580
|
+
* <div {...bind}>
|
|
8581
|
+
* {isRefreshing && <Spinner />}
|
|
8582
|
+
* {content}
|
|
8583
|
+
* </div>
|
|
8584
|
+
* );
|
|
8585
|
+
* ```
|
|
8586
|
+
*/
|
|
8587
|
+
function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
|
|
8588
|
+
const [pullDistance, setPullDistance] = React.useState(0);
|
|
8589
|
+
const [isRefreshing, setIsRefreshing] = React.useState(false);
|
|
8590
|
+
const startY = React.useRef(0);
|
|
8591
|
+
const handleTouchStart = React.useCallback((e) => {
|
|
8592
|
+
if (disabled || isRefreshing)
|
|
8593
|
+
return;
|
|
8594
|
+
startY.current = e.touches[0].clientY;
|
|
8595
|
+
}, [disabled, isRefreshing]);
|
|
8596
|
+
const handleTouchMove = React.useCallback((e) => {
|
|
8597
|
+
if (disabled || isRefreshing || startY.current === 0)
|
|
8598
|
+
return;
|
|
8599
|
+
const diff = e.touches[0].clientY - startY.current;
|
|
8600
|
+
if (diff > 0) {
|
|
8601
|
+
const adjustedPull = Math.min(diff * 0.5, maxPull);
|
|
8602
|
+
setPullDistance(adjustedPull);
|
|
8603
|
+
}
|
|
8604
|
+
}, [disabled, isRefreshing, maxPull]);
|
|
8605
|
+
const handleTouchEnd = React.useCallback(async () => {
|
|
8606
|
+
if (disabled || isRefreshing)
|
|
8607
|
+
return;
|
|
8608
|
+
if (pullDistance >= pullThreshold) {
|
|
8609
|
+
setIsRefreshing(true);
|
|
8610
|
+
try {
|
|
8611
|
+
await onRefresh();
|
|
8612
|
+
}
|
|
8613
|
+
finally {
|
|
8614
|
+
setIsRefreshing(false);
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
setPullDistance(0);
|
|
8618
|
+
startY.current = 0;
|
|
8619
|
+
}, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
|
|
8620
|
+
return {
|
|
8621
|
+
pullDistance,
|
|
8622
|
+
isRefreshing,
|
|
8623
|
+
isReady: pullDistance >= pullThreshold,
|
|
8624
|
+
progress: Math.min(pullDistance / pullThreshold, 1),
|
|
8625
|
+
bind: {
|
|
8626
|
+
onTouchStart: handleTouchStart,
|
|
8627
|
+
onTouchMove: handleTouchMove,
|
|
8628
|
+
onTouchEnd: handleTouchEnd,
|
|
8629
|
+
},
|
|
8630
|
+
};
|
|
6760
8631
|
}
|
|
6761
8632
|
|
|
6762
8633
|
function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
|
|
@@ -6872,6 +8743,125 @@ const Layout = ({ sidebar, children, statusBar, className = '', sections }) => {
|
|
|
6872
8743
|
return (jsxRuntime.jsxs("div", { className: `h-screen flex flex-col bg-paper-100 ${className}`, children: [jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden relative", children: [sidebar, jsxRuntime.jsx("div", { className: "w-8 h-full bg-paper-100 flex-shrink-0 relative flex items-center justify-center", children: jsxRuntime.jsx(PageNavigation, { sections: sections }) }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: children })] }), statusBar] }));
|
|
6873
8744
|
};
|
|
6874
8745
|
|
|
8746
|
+
/**
|
|
8747
|
+
* MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
|
|
8748
|
+
*
|
|
8749
|
+
* This component automatically detects the viewport size and renders the appropriate layout:
|
|
8750
|
+
* - **Desktop** (≥1024px): Standard Layout with sidebar, gutter, and scrollable content
|
|
8751
|
+
* - **Mobile/Tablet** (<1024px): Mobile header, drawer navigation, bottom tab bar
|
|
8752
|
+
*
|
|
8753
|
+
* The mobile layout features:
|
|
8754
|
+
* - Sticky header with hamburger menu to open drawer
|
|
8755
|
+
* - Sidebar rendered as a slide-in drawer
|
|
8756
|
+
* - Bottom navigation bar for primary navigation
|
|
8757
|
+
* - Safe area support for notched devices
|
|
8758
|
+
*
|
|
8759
|
+
* @example Basic usage
|
|
8760
|
+
* ```tsx
|
|
8761
|
+
* <MobileLayout
|
|
8762
|
+
* sidebarItems={[
|
|
8763
|
+
* { id: 'home', label: 'Home', icon: <Home />, href: '/' },
|
|
8764
|
+
* { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks' },
|
|
8765
|
+
* { id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' }
|
|
8766
|
+
* ]}
|
|
8767
|
+
* currentPath={location.pathname}
|
|
8768
|
+
* onNavigate={(href) => navigate(href)}
|
|
8769
|
+
* title="My App"
|
|
8770
|
+
* header={<Logo />}
|
|
8771
|
+
* userProfile={<UserProfileButton user={user} />}
|
|
8772
|
+
* >
|
|
8773
|
+
* <Page>
|
|
8774
|
+
* <h1>Dashboard</h1>
|
|
8775
|
+
* </Page>
|
|
8776
|
+
* </MobileLayout>
|
|
8777
|
+
* ```
|
|
8778
|
+
*
|
|
8779
|
+
* @example With custom bottom nav items
|
|
8780
|
+
* ```tsx
|
|
8781
|
+
* <MobileLayout
|
|
8782
|
+
* sidebarItems={fullNavItems}
|
|
8783
|
+
* bottomNavItems={[
|
|
8784
|
+
* { id: 'home', label: 'Home', icon: <Home />, href: '/' },
|
|
8785
|
+
* { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
|
|
8786
|
+
* { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' }
|
|
8787
|
+
* ]}
|
|
8788
|
+
* currentPath={location.pathname}
|
|
8789
|
+
* title="My App"
|
|
8790
|
+
* >
|
|
8791
|
+
* {children}
|
|
8792
|
+
* </MobileLayout>
|
|
8793
|
+
* ```
|
|
8794
|
+
*
|
|
8795
|
+
* @example Force mobile layout for testing
|
|
8796
|
+
* ```tsx
|
|
8797
|
+
* <MobileLayout
|
|
8798
|
+
* sidebarItems={items}
|
|
8799
|
+
* title="Mobile Preview"
|
|
8800
|
+
* forceMobile
|
|
8801
|
+
* >
|
|
8802
|
+
* {children}
|
|
8803
|
+
* </MobileLayout>
|
|
8804
|
+
* ```
|
|
8805
|
+
*/
|
|
8806
|
+
const MobileLayout = ({ children, sidebarItems, currentPath, onNavigate, header, userProfile, sidebarFooter, title, subtitle, headerRightAction, headerLeftAction, headerVariant = 'solid', bottomNavItems, activeBottomNavId, showBottomNavLabels = true, statusBar, className = '', sections, forceMobile = false, forceDesktop = false, hideBottomNav = false, hideMobileHeader = false, safeArea = true, }) => {
|
|
8807
|
+
const isMobileViewport = useIsMobile();
|
|
8808
|
+
const isTabletViewport = useIsTablet();
|
|
8809
|
+
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
|
8810
|
+
// Determine if we should use mobile layout
|
|
8811
|
+
const useMobileLayout = forceDesktop
|
|
8812
|
+
? false
|
|
8813
|
+
: forceMobile || isMobileViewport || isTabletViewport;
|
|
8814
|
+
// Open/close drawer
|
|
8815
|
+
const openDrawer = React.useCallback(() => setDrawerOpen(true), []);
|
|
8816
|
+
const closeDrawer = React.useCallback(() => setDrawerOpen(false), []);
|
|
8817
|
+
// Handle navigation from drawer - close drawer after navigation
|
|
8818
|
+
const handleDrawerNavigate = React.useCallback((href) => {
|
|
8819
|
+
closeDrawer();
|
|
8820
|
+
onNavigate?.(href);
|
|
8821
|
+
}, [closeDrawer, onNavigate]);
|
|
8822
|
+
// Handle bottom nav navigation - matches BottomNavigation's onNavigate signature
|
|
8823
|
+
const handleBottomNavNavigate = React.useCallback((id, href) => {
|
|
8824
|
+
if (href) {
|
|
8825
|
+
onNavigate?.(href);
|
|
8826
|
+
}
|
|
8827
|
+
// Also check if there's a custom onClick in the bottom nav items
|
|
8828
|
+
const item = bottomNavItems?.find(i => i.id === id);
|
|
8829
|
+
item?.onClick?.();
|
|
8830
|
+
}, [onNavigate, bottomNavItems]);
|
|
8831
|
+
// Convert sidebar items to bottom nav items if not provided
|
|
8832
|
+
const effectiveBottomNavItems = bottomNavItems || sidebarItems
|
|
8833
|
+
.filter(item => !item.children && item.href) // Only top-level items with href
|
|
8834
|
+
.slice(0, 5) // Max 5 items for bottom nav
|
|
8835
|
+
.map(item => ({
|
|
8836
|
+
id: item.id,
|
|
8837
|
+
label: item.label,
|
|
8838
|
+
icon: item.icon,
|
|
8839
|
+
href: item.href,
|
|
8840
|
+
badge: typeof item.badge === 'number' ? item.badge : undefined,
|
|
8841
|
+
}));
|
|
8842
|
+
// Determine active bottom nav ID
|
|
8843
|
+
const effectiveActiveBottomNavId = activeBottomNavId ||
|
|
8844
|
+
effectiveBottomNavItems.find(item => item.href === currentPath)?.id;
|
|
8845
|
+
// Close drawer on escape key
|
|
8846
|
+
React.useEffect(() => {
|
|
8847
|
+
if (!drawerOpen)
|
|
8848
|
+
return;
|
|
8849
|
+
const handleEscape = (e) => {
|
|
8850
|
+
if (e.key === 'Escape') {
|
|
8851
|
+
closeDrawer();
|
|
8852
|
+
}
|
|
8853
|
+
};
|
|
8854
|
+
window.addEventListener('keydown', handleEscape);
|
|
8855
|
+
return () => window.removeEventListener('keydown', handleEscape);
|
|
8856
|
+
}, [drawerOpen, closeDrawer]);
|
|
8857
|
+
// Desktop Layout
|
|
8858
|
+
if (!useMobileLayout) {
|
|
8859
|
+
return (jsxRuntime.jsxs("div", { className: `h-screen flex flex-col bg-paper-100 ${className}`, children: [jsxRuntime.jsxs("div", { className: "flex flex-1 overflow-hidden relative", children: [jsxRuntime.jsx(Sidebar, { items: sidebarItems, currentPath: currentPath, onNavigate: onNavigate, header: header, footer: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [userProfile, sidebarFooter] }) }), jsxRuntime.jsx("div", { className: "w-8 h-full bg-paper-100 flex-shrink-0 relative flex items-center justify-center", children: jsxRuntime.jsx(PageNavigation, { sections: sections }) }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: children })] }), statusBar] }));
|
|
8860
|
+
}
|
|
8861
|
+
// Mobile Layout
|
|
8862
|
+
return (jsxRuntime.jsxs("div", { className: `min-h-screen flex flex-col bg-paper-100 ${className}`, children: [!hideMobileHeader && (jsxRuntime.jsx(MobileHeader, { title: title, subtitle: subtitle, onMenuClick: headerLeftAction ? undefined : openDrawer, leftAction: headerLeftAction, rightAction: headerRightAction, variant: headerVariant, sticky: true, bordered: true, safeArea: safeArea })), jsxRuntime.jsx(Sidebar, { items: sidebarItems, currentPath: currentPath, onNavigate: handleDrawerNavigate, header: header, footer: jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [userProfile, sidebarFooter] }), mobileOpen: drawerOpen, onMobileClose: closeDrawer }), jsxRuntime.jsx("div", { className: "flex-1 overflow-auto", children: children }), !hideBottomNav && effectiveBottomNavItems.length > 0 && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(BottomNavigationSpacer, {}), jsxRuntime.jsx(BottomNavigation, { items: effectiveBottomNavItems, activeId: effectiveActiveBottomNavId, onNavigate: handleBottomNavNavigate, showLabels: showBottomNavLabels, safeArea: safeArea })] }))] }));
|
|
8863
|
+
};
|
|
8864
|
+
|
|
6875
8865
|
/**
|
|
6876
8866
|
* Two-column content layout component that provides:
|
|
6877
8867
|
* - Sidebar column on the left (takes 1/3 of width)
|
|
@@ -7125,6 +9115,185 @@ function NotificationIndicator({ count = 0, onClick, className = '', maxCount =
|
|
|
7125
9115
|
return (jsxRuntime.jsxs("button", { onClick: onClick, className: `relative bg-white p-2.5 rounded-lg text-ink-400 hover:text-ink-600 hover:bg-paper-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400 transition-all shadow-xs dark:bg-slate-800 dark:text-slate-400 dark:hover:text-slate-100 dark:hover:bg-slate-700 ${className}`, "aria-label": "View notifications", children: [jsxRuntime.jsx(lucideReact.Bell, { className: "h-5 w-5" }), showBadge && (jsxRuntime.jsx("span", { className: `absolute -top-1 -right-1 ${variantClasses[variant]} text-white text-xs font-semibold rounded-full h-5 min-w-5 px-1 flex items-center justify-center`, children: displayCount }))] }));
|
|
7126
9116
|
}
|
|
7127
9117
|
|
|
9118
|
+
/**
|
|
9119
|
+
* Get value from item by key path (supports nested keys like 'user.name')
|
|
9120
|
+
*/
|
|
9121
|
+
function getValueByKey(item, key) {
|
|
9122
|
+
if (typeof key !== 'string') {
|
|
9123
|
+
return item[key];
|
|
9124
|
+
}
|
|
9125
|
+
const keys = key.split('.');
|
|
9126
|
+
let value = item;
|
|
9127
|
+
for (const k of keys) {
|
|
9128
|
+
if (value == null)
|
|
9129
|
+
return undefined;
|
|
9130
|
+
value = value[k];
|
|
9131
|
+
}
|
|
9132
|
+
return value;
|
|
9133
|
+
}
|
|
9134
|
+
/**
|
|
9135
|
+
* Skeleton card for loading state
|
|
9136
|
+
*/
|
|
9137
|
+
function SkeletonCard({ className = '' }) {
|
|
9138
|
+
return (jsxRuntime.jsx("div", { className: `bg-white rounded-lg border border-paper-200 p-4 animate-pulse ${className}`, children: jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [jsxRuntime.jsx("div", { className: "w-10 h-10 rounded-full bg-paper-200 flex-shrink-0" }), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "h-5 bg-paper-200 rounded w-3/4 mb-2" }), jsxRuntime.jsx("div", { className: "h-4 bg-paper-100 rounded w-1/2 mb-3" }), jsxRuntime.jsxs("div", { className: "space-y-2", children: [jsxRuntime.jsx("div", { className: "h-3 bg-paper-100 rounded w-2/3" }), jsxRuntime.jsx("div", { className: "h-3 bg-paper-100 rounded w-1/2" })] })] }), jsxRuntime.jsx("div", { className: "h-6 w-16 bg-paper-200 rounded-full" })] }) }));
|
|
9139
|
+
}
|
|
9140
|
+
/**
|
|
9141
|
+
* DataTableCardView - Mobile-friendly card view for data tables
|
|
9142
|
+
*
|
|
9143
|
+
* Renders data as cards instead of table rows, optimized for touch interaction.
|
|
9144
|
+
* Automatically uses column render functions for consistent data display.
|
|
9145
|
+
*
|
|
9146
|
+
* @example Basic usage
|
|
9147
|
+
* ```tsx
|
|
9148
|
+
* <DataTableCardView
|
|
9149
|
+
* data={users}
|
|
9150
|
+
* columns={columns}
|
|
9151
|
+
* cardConfig={{
|
|
9152
|
+
* titleKey: 'name',
|
|
9153
|
+
* subtitleKey: 'email',
|
|
9154
|
+
* metadataKeys: ['department', 'role'],
|
|
9155
|
+
* badgeKey: 'status',
|
|
9156
|
+
* }}
|
|
9157
|
+
* onCardClick={(user) => navigate(`/users/${user.id}`)}
|
|
9158
|
+
* />
|
|
9159
|
+
* ```
|
|
9160
|
+
*
|
|
9161
|
+
* @example With selection
|
|
9162
|
+
* ```tsx
|
|
9163
|
+
* <DataTableCardView
|
|
9164
|
+
* data={orders}
|
|
9165
|
+
* columns={columns}
|
|
9166
|
+
* cardConfig={{
|
|
9167
|
+
* titleKey: 'orderNumber',
|
|
9168
|
+
* subtitleKey: 'customer',
|
|
9169
|
+
* badgeKey: 'status',
|
|
9170
|
+
* }}
|
|
9171
|
+
* selectable
|
|
9172
|
+
* selectedRows={selectedOrders}
|
|
9173
|
+
* onSelectionChange={setSelectedOrders}
|
|
9174
|
+
* />
|
|
9175
|
+
* ```
|
|
9176
|
+
*/
|
|
9177
|
+
function DataTableCardView({ data, columns, cardConfig, loading = false, loadingRows = 5, emptyMessage = 'No items to display', onCardClick, onCardLongPress, selectable = false, selectedRows = new Set(), onSelectionChange, keyExtractor = (row) => String(row.id), actions, onEdit, onDelete, className = '', cardClassName = '', gap = 'md', }) {
|
|
9178
|
+
const gapClasses = {
|
|
9179
|
+
sm: 'gap-2',
|
|
9180
|
+
md: 'gap-3',
|
|
9181
|
+
lg: 'gap-4',
|
|
9182
|
+
};
|
|
9183
|
+
// Find column by key to use its render function
|
|
9184
|
+
const getColumn = (key) => {
|
|
9185
|
+
return columns.find(col => col.key === key);
|
|
9186
|
+
};
|
|
9187
|
+
// Render a value using column's render function if available
|
|
9188
|
+
const renderValue = (item, key) => {
|
|
9189
|
+
const column = getColumn(key);
|
|
9190
|
+
const value = getValueByKey(item, key);
|
|
9191
|
+
if (column?.render) {
|
|
9192
|
+
return column.render(item, value);
|
|
9193
|
+
}
|
|
9194
|
+
if (value == null)
|
|
9195
|
+
return '-';
|
|
9196
|
+
if (typeof value === 'boolean')
|
|
9197
|
+
return value ? 'Yes' : 'No';
|
|
9198
|
+
if (value instanceof Date)
|
|
9199
|
+
return value.toLocaleDateString();
|
|
9200
|
+
return String(value);
|
|
9201
|
+
};
|
|
9202
|
+
// Handle card selection toggle
|
|
9203
|
+
const handleSelectionToggle = (item, event) => {
|
|
9204
|
+
event.stopPropagation();
|
|
9205
|
+
const key = keyExtractor(item);
|
|
9206
|
+
const newSelected = new Set(selectedRows);
|
|
9207
|
+
if (newSelected.has(key)) {
|
|
9208
|
+
newSelected.delete(key);
|
|
9209
|
+
}
|
|
9210
|
+
else {
|
|
9211
|
+
newSelected.add(key);
|
|
9212
|
+
}
|
|
9213
|
+
onSelectionChange?.(Array.from(newSelected));
|
|
9214
|
+
};
|
|
9215
|
+
// Handle card click
|
|
9216
|
+
const handleCardClick = (item) => {
|
|
9217
|
+
if (selectable && selectedRows.size > 0) {
|
|
9218
|
+
// If in selection mode, toggle selection instead
|
|
9219
|
+
const key = keyExtractor(item);
|
|
9220
|
+
const newSelected = new Set(selectedRows);
|
|
9221
|
+
if (newSelected.has(key)) {
|
|
9222
|
+
newSelected.delete(key);
|
|
9223
|
+
}
|
|
9224
|
+
else {
|
|
9225
|
+
newSelected.add(key);
|
|
9226
|
+
}
|
|
9227
|
+
onSelectionChange?.(Array.from(newSelected));
|
|
9228
|
+
}
|
|
9229
|
+
else {
|
|
9230
|
+
onCardClick?.(item);
|
|
9231
|
+
}
|
|
9232
|
+
};
|
|
9233
|
+
// Handle long press for context actions
|
|
9234
|
+
const handleLongPress = (item, event) => {
|
|
9235
|
+
onCardLongPress?.(item, event);
|
|
9236
|
+
};
|
|
9237
|
+
// Loading state
|
|
9238
|
+
if (loading) {
|
|
9239
|
+
return (jsxRuntime.jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: Array.from({ length: loadingRows }).map((_, i) => (jsxRuntime.jsx(SkeletonCard, { className: cardClassName }, i))) }));
|
|
9240
|
+
}
|
|
9241
|
+
// Empty state
|
|
9242
|
+
if (data.length === 0) {
|
|
9243
|
+
return (jsxRuntime.jsx("div", { className: `flex items-center justify-center py-12 px-4 ${className}`, children: jsxRuntime.jsx("p", { className: "text-ink-500 text-center", children: emptyMessage }) }));
|
|
9244
|
+
}
|
|
9245
|
+
// Determine default card config if not provided
|
|
9246
|
+
const config = cardConfig || {
|
|
9247
|
+
titleKey: columns[0]?.key || 'id',
|
|
9248
|
+
subtitleKey: columns[1]?.key,
|
|
9249
|
+
metadataKeys: columns.slice(2, 5).map(c => c.key),
|
|
9250
|
+
};
|
|
9251
|
+
return (jsxRuntime.jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: data.map((item) => {
|
|
9252
|
+
const key = keyExtractor(item);
|
|
9253
|
+
const isSelected = selectedRows.has(key);
|
|
9254
|
+
// Custom card render
|
|
9255
|
+
if (config.renderCard) {
|
|
9256
|
+
return (jsxRuntime.jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
|
|
9257
|
+
e.preventDefault();
|
|
9258
|
+
handleLongPress(item, e);
|
|
9259
|
+
}, className: `
|
|
9260
|
+
cursor-pointer transition-all duration-200
|
|
9261
|
+
${isSelected ? 'ring-2 ring-accent-500' : ''}
|
|
9262
|
+
${cardClassName}
|
|
9263
|
+
`, children: config.renderCard(item, columns) }, key));
|
|
9264
|
+
}
|
|
9265
|
+
// Default card layout
|
|
9266
|
+
const titleColumn = getColumn(config.titleKey);
|
|
9267
|
+
const titleValue = getValueByKey(item, config.titleKey);
|
|
9268
|
+
return (jsxRuntime.jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
|
|
9269
|
+
e.preventDefault();
|
|
9270
|
+
handleLongPress(item, e);
|
|
9271
|
+
}, className: `
|
|
9272
|
+
bg-white rounded-lg border border-paper-200 p-4
|
|
9273
|
+
transition-all duration-200 cursor-pointer
|
|
9274
|
+
active:scale-[0.98] active:bg-paper-50
|
|
9275
|
+
${isSelected ? 'ring-2 ring-accent-500 bg-accent-50/30' : 'hover:shadow-md hover:border-paper-300'}
|
|
9276
|
+
${cardClassName}
|
|
9277
|
+
`, children: jsxRuntime.jsxs("div", { className: "flex items-start gap-3", children: [selectable && (jsxRuntime.jsx("div", { className: "flex-shrink-0 pt-0.5", onClick: (e) => handleSelectionToggle(item, e), children: jsxRuntime.jsx(Checkbox, { checked: isSelected, onChange: () => { } }) })), config.avatarKey && (jsxRuntime.jsx("div", { className: "flex-shrink-0", children: (() => {
|
|
9278
|
+
const avatarValue = getValueByKey(item, config.avatarKey);
|
|
9279
|
+
if (typeof avatarValue === 'string' && avatarValue.startsWith('http')) {
|
|
9280
|
+
return (jsxRuntime.jsx("img", { src: avatarValue, alt: "", className: "w-10 h-10 rounded-full object-cover" }));
|
|
9281
|
+
}
|
|
9282
|
+
// Render initials or placeholder
|
|
9283
|
+
const initials = String(titleValue || '').slice(0, 2).toUpperCase();
|
|
9284
|
+
return (jsxRuntime.jsx("div", { className: "w-10 h-10 rounded-full bg-accent-100 flex items-center justify-center text-accent-700 font-medium text-sm", children: initials }));
|
|
9285
|
+
})() })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-ink-900 truncate", children: titleColumn?.render
|
|
9286
|
+
? titleColumn.render(item, titleValue)
|
|
9287
|
+
: String(titleValue || '-') }), config.subtitleKey && (jsxRuntime.jsx("div", { className: "text-sm text-ink-500 truncate mt-0.5", children: renderValue(item, config.subtitleKey) })), config.metadataKeys && config.metadataKeys.length > 0 && (jsxRuntime.jsx("div", { className: "mt-2 space-y-1", children: config.metadataKeys.map((metaKey) => {
|
|
9288
|
+
const column = getColumn(metaKey);
|
|
9289
|
+
return (jsxRuntime.jsxs("div", { className: "flex items-center text-xs", children: [jsxRuntime.jsxs("span", { className: "text-ink-400 w-20 flex-shrink-0 truncate", children: [column?.header || String(metaKey), ":"] }), jsxRuntime.jsx("span", { className: "text-ink-600 truncate", children: renderValue(item, metaKey) })] }, String(metaKey)));
|
|
9290
|
+
}) }))] }), jsxRuntime.jsxs("div", { className: "flex flex-col items-end gap-2 flex-shrink-0", children: [config.badgeKey && (jsxRuntime.jsx("div", { children: renderValue(item, config.badgeKey) })), config.showChevron && onCardClick && (jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-5 w-5 text-ink-300" })), (actions || onEdit || onDelete) && (jsxRuntime.jsx("button", { onClick: (e) => {
|
|
9291
|
+
e.stopPropagation();
|
|
9292
|
+
handleLongPress(item, e);
|
|
9293
|
+
}, className: "p-1 rounded hover:bg-paper-100 text-ink-400 hover:text-ink-600 -mr-1", children: jsxRuntime.jsx(lucideReact.MoreVertical, { className: "h-5 w-5" }) }))] })] }) }, key));
|
|
9294
|
+
}) }));
|
|
9295
|
+
}
|
|
9296
|
+
|
|
7128
9297
|
/**
|
|
7129
9298
|
* ActionMenu - Inline dropdown menu for row actions
|
|
7130
9299
|
*/
|
|
@@ -7283,7 +9452,11 @@ function DataTable({ data, columns, loading = false, error = null, emptyMessage
|
|
|
7283
9452
|
// Visual customization props
|
|
7284
9453
|
striped = false, stripedColor, density = 'normal', rowClassName, rowHighlight, highlightedRowId, bordered = false, borderColor = 'border-paper-200', disableHover = false, hiddenColumns = [], headerClassName = '', renderEmptyState: customRenderEmptyState, resizable = false, onColumnResize, reorderable = false, onColumnReorder, virtualized = false, virtualHeight = '600px', virtualRowHeight = 60,
|
|
7285
9454
|
// Pagination props
|
|
7286
|
-
paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true,
|
|
9455
|
+
paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true,
|
|
9456
|
+
// Mobile view props
|
|
9457
|
+
mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
|
|
9458
|
+
// Mobile detection for auto mode
|
|
9459
|
+
const isMobileViewport = useIsMobile();
|
|
7287
9460
|
// Column resizing state
|
|
7288
9461
|
const [columnWidths, setColumnWidths] = React.useState({});
|
|
7289
9462
|
const [resizingColumn, setResizingColumn] = React.useState(null);
|
|
@@ -7902,8 +10075,274 @@ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pag
|
|
|
7902
10075
|
return null;
|
|
7903
10076
|
return (jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-4 px-1", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-4", children: [showPageSizeSelector && onPageSizeChange && (jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [jsxRuntime.jsx("span", { className: "text-sm text-ink-600", children: "Show:" }), jsxRuntime.jsx(Select, { options: pageSizeSelectOptions, value: String(pageSize), onChange: (value) => onPageSizeChange?.(Number(value)) })] })), jsxRuntime.jsx("span", { className: "text-sm text-ink-600", children: effectiveTotalItems > 0 ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: ["Showing ", ((currentPage - 1) * pageSize) + 1, " - ", Math.min(currentPage * pageSize, effectiveTotalItems), " of ", effectiveTotalItems] })) : ('No items') })] }), totalPages > 1 && onPageChange && (jsxRuntime.jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }))] }));
|
|
7904
10077
|
};
|
|
10078
|
+
// Determine if we should show card view
|
|
10079
|
+
const shouldShowCardView = mobileView === 'card' ||
|
|
10080
|
+
(mobileView === 'auto' && isMobileViewport);
|
|
10081
|
+
// Card view content
|
|
10082
|
+
const cardViewContent = shouldShowCardView ? (jsxRuntime.jsx(DataTableCardView, { data: data, columns: visibleColumns, cardConfig: cardConfig, loading: loading, loadingRows: loadingRows, emptyMessage: emptyMessage, onCardClick: onRowClick, onCardLongPress: (item, event) => {
|
|
10083
|
+
if (enableContextMenu && allActions.length > 0) {
|
|
10084
|
+
event.preventDefault();
|
|
10085
|
+
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
|
10086
|
+
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
|
10087
|
+
setContextMenuState({
|
|
10088
|
+
isOpen: true,
|
|
10089
|
+
position: { x: clientX, y: clientY },
|
|
10090
|
+
item,
|
|
10091
|
+
});
|
|
10092
|
+
}
|
|
10093
|
+
}, selectable: selectable, selectedRows: selectedRowsSet, onSelectionChange: onRowSelect ? (rows) => onRowSelect(rows) : undefined, keyExtractor: getRowKey, actions: allActions, onEdit: onEdit, onDelete: onDelete, className: className, cardClassName: cardClassName, gap: cardGap })) : null;
|
|
7905
10094
|
// Render with context menu
|
|
7906
|
-
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderPaginationControls(), finalContent, contextMenuState.isOpen && contextMenuState.item && (jsxRuntime.jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
|
|
10095
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [renderPaginationControls(), shouldShowCardView ? cardViewContent : finalContent, contextMenuState.isOpen && contextMenuState.item && (jsxRuntime.jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
|
|
10096
|
+
}
|
|
10097
|
+
|
|
10098
|
+
// Color mapping for action buttons
|
|
10099
|
+
const colorClasses = {
|
|
10100
|
+
primary: 'bg-accent-500 text-white',
|
|
10101
|
+
success: 'bg-success-500 text-white',
|
|
10102
|
+
warning: 'bg-warning-500 text-white',
|
|
10103
|
+
error: 'bg-error-500 text-white',
|
|
10104
|
+
default: 'bg-paper-500 text-white',
|
|
10105
|
+
};
|
|
10106
|
+
/**
|
|
10107
|
+
* SwipeActions - Touch-based swipe actions for list items
|
|
10108
|
+
*
|
|
10109
|
+
* Wraps any content with swipe-to-reveal actions, commonly used in mobile
|
|
10110
|
+
* list items for quick actions like delete, archive, edit, etc.
|
|
10111
|
+
*
|
|
10112
|
+
* Features:
|
|
10113
|
+
* - Left and right swipe actions
|
|
10114
|
+
* - Full swipe to trigger primary action
|
|
10115
|
+
* - Spring-back animation
|
|
10116
|
+
* - Touch and mouse support
|
|
10117
|
+
* - Customizable thresholds
|
|
10118
|
+
*
|
|
10119
|
+
* @example Basic delete action
|
|
10120
|
+
* ```tsx
|
|
10121
|
+
* <SwipeActions
|
|
10122
|
+
* leftActions={[
|
|
10123
|
+
* {
|
|
10124
|
+
* id: 'delete',
|
|
10125
|
+
* label: 'Delete',
|
|
10126
|
+
* icon: <Trash className="h-5 w-5" />,
|
|
10127
|
+
* color: 'error',
|
|
10128
|
+
* onClick: () => handleDelete(item),
|
|
10129
|
+
* primary: true,
|
|
10130
|
+
* },
|
|
10131
|
+
* ]}
|
|
10132
|
+
* >
|
|
10133
|
+
* <div className="p-4 bg-white">
|
|
10134
|
+
* List item content
|
|
10135
|
+
* </div>
|
|
10136
|
+
* </SwipeActions>
|
|
10137
|
+
* ```
|
|
10138
|
+
*
|
|
10139
|
+
* @example Multiple actions on both sides
|
|
10140
|
+
* ```tsx
|
|
10141
|
+
* <SwipeActions
|
|
10142
|
+
* leftActions={[
|
|
10143
|
+
* { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
|
|
10144
|
+
* { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
|
|
10145
|
+
* ]}
|
|
10146
|
+
* rightActions={[
|
|
10147
|
+
* { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
|
|
10148
|
+
* ]}
|
|
10149
|
+
* fullSwipe
|
|
10150
|
+
* >
|
|
10151
|
+
* <ListItem />
|
|
10152
|
+
* </SwipeActions>
|
|
10153
|
+
* ```
|
|
10154
|
+
*/
|
|
10155
|
+
function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
|
|
10156
|
+
const containerRef = React.useRef(null);
|
|
10157
|
+
const contentRef = React.useRef(null);
|
|
10158
|
+
// Swipe state
|
|
10159
|
+
const [translateX, setTranslateX] = React.useState(0);
|
|
10160
|
+
const [isDragging, setIsDragging] = React.useState(false);
|
|
10161
|
+
const [activeDirection, setActiveDirection] = React.useState(null);
|
|
10162
|
+
// Touch/mouse tracking
|
|
10163
|
+
const startX = React.useRef(0);
|
|
10164
|
+
const currentX = React.useRef(0);
|
|
10165
|
+
const startTime = React.useRef(0);
|
|
10166
|
+
// Calculate action widths
|
|
10167
|
+
const leftActionsWidth = leftActions.length * 72; // 72px per action
|
|
10168
|
+
const rightActionsWidth = rightActions.length * 72;
|
|
10169
|
+
// Reset position
|
|
10170
|
+
const resetPosition = React.useCallback(() => {
|
|
10171
|
+
setTranslateX(0);
|
|
10172
|
+
setActiveDirection(null);
|
|
10173
|
+
onSwipeChange?.(null);
|
|
10174
|
+
}, [onSwipeChange]);
|
|
10175
|
+
// Handle touch/mouse start
|
|
10176
|
+
const handleStart = React.useCallback((clientX) => {
|
|
10177
|
+
if (disabled)
|
|
10178
|
+
return;
|
|
10179
|
+
startX.current = clientX;
|
|
10180
|
+
currentX.current = clientX;
|
|
10181
|
+
startTime.current = Date.now();
|
|
10182
|
+
setIsDragging(true);
|
|
10183
|
+
}, [disabled]);
|
|
10184
|
+
// Handle touch/mouse move
|
|
10185
|
+
const handleMove = React.useCallback((clientX) => {
|
|
10186
|
+
if (!isDragging || disabled)
|
|
10187
|
+
return;
|
|
10188
|
+
const deltaX = clientX - startX.current;
|
|
10189
|
+
currentX.current = clientX;
|
|
10190
|
+
// Determine direction and apply resistance at boundaries
|
|
10191
|
+
let newTranslateX = deltaX;
|
|
10192
|
+
// Swiping left (reveals left actions on right side)
|
|
10193
|
+
if (deltaX < 0) {
|
|
10194
|
+
if (leftActions.length === 0) {
|
|
10195
|
+
newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
|
|
10196
|
+
}
|
|
10197
|
+
else {
|
|
10198
|
+
const maxSwipe = fullSwipe
|
|
10199
|
+
? -(containerRef.current?.offsetWidth || 300)
|
|
10200
|
+
: -leftActionsWidth;
|
|
10201
|
+
newTranslateX = Math.max(maxSwipe, deltaX);
|
|
10202
|
+
// Apply resistance past the action buttons
|
|
10203
|
+
if (newTranslateX < -leftActionsWidth) {
|
|
10204
|
+
const overSwipe = newTranslateX + leftActionsWidth;
|
|
10205
|
+
newTranslateX = -leftActionsWidth + overSwipe * 0.3;
|
|
10206
|
+
}
|
|
10207
|
+
}
|
|
10208
|
+
setActiveDirection('left');
|
|
10209
|
+
onSwipeChange?.('left');
|
|
10210
|
+
}
|
|
10211
|
+
// Swiping right (reveals right actions on left side)
|
|
10212
|
+
else if (deltaX > 0) {
|
|
10213
|
+
if (rightActions.length === 0) {
|
|
10214
|
+
newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
|
|
10215
|
+
}
|
|
10216
|
+
else {
|
|
10217
|
+
const maxSwipe = fullSwipe
|
|
10218
|
+
? (containerRef.current?.offsetWidth || 300)
|
|
10219
|
+
: rightActionsWidth;
|
|
10220
|
+
newTranslateX = Math.min(maxSwipe, deltaX);
|
|
10221
|
+
// Apply resistance past the action buttons
|
|
10222
|
+
if (newTranslateX > rightActionsWidth) {
|
|
10223
|
+
const overSwipe = newTranslateX - rightActionsWidth;
|
|
10224
|
+
newTranslateX = rightActionsWidth + overSwipe * 0.3;
|
|
10225
|
+
}
|
|
10226
|
+
}
|
|
10227
|
+
setActiveDirection('right');
|
|
10228
|
+
onSwipeChange?.('right');
|
|
10229
|
+
}
|
|
10230
|
+
setTranslateX(newTranslateX);
|
|
10231
|
+
}, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
|
|
10232
|
+
// Handle touch/mouse end
|
|
10233
|
+
const handleEnd = React.useCallback(() => {
|
|
10234
|
+
if (!isDragging)
|
|
10235
|
+
return;
|
|
10236
|
+
setIsDragging(false);
|
|
10237
|
+
const deltaX = currentX.current - startX.current;
|
|
10238
|
+
const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
|
|
10239
|
+
const containerWidth = containerRef.current?.offsetWidth || 300;
|
|
10240
|
+
// Check for full swipe trigger
|
|
10241
|
+
if (fullSwipe) {
|
|
10242
|
+
const swipePercentage = Math.abs(translateX) / containerWidth;
|
|
10243
|
+
if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
|
|
10244
|
+
// Find primary action and trigger it
|
|
10245
|
+
if (translateX < 0 && leftActions.length > 0) {
|
|
10246
|
+
const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
|
|
10247
|
+
primaryAction.onClick();
|
|
10248
|
+
resetPosition();
|
|
10249
|
+
return;
|
|
10250
|
+
}
|
|
10251
|
+
else if (translateX > 0 && rightActions.length > 0) {
|
|
10252
|
+
const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
|
|
10253
|
+
primaryAction.onClick();
|
|
10254
|
+
resetPosition();
|
|
10255
|
+
return;
|
|
10256
|
+
}
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
// Snap to open or closed position
|
|
10260
|
+
if (Math.abs(translateX) >= threshold || velocity > 0.3) {
|
|
10261
|
+
// Snap open
|
|
10262
|
+
if (translateX < 0 && leftActions.length > 0) {
|
|
10263
|
+
setTranslateX(-leftActionsWidth);
|
|
10264
|
+
setActiveDirection('left');
|
|
10265
|
+
onSwipeChange?.('left');
|
|
10266
|
+
}
|
|
10267
|
+
else if (translateX > 0 && rightActions.length > 0) {
|
|
10268
|
+
setTranslateX(rightActionsWidth);
|
|
10269
|
+
setActiveDirection('right');
|
|
10270
|
+
onSwipeChange?.('right');
|
|
10271
|
+
}
|
|
10272
|
+
else {
|
|
10273
|
+
resetPosition();
|
|
10274
|
+
}
|
|
10275
|
+
}
|
|
10276
|
+
else {
|
|
10277
|
+
// Snap closed
|
|
10278
|
+
resetPosition();
|
|
10279
|
+
}
|
|
10280
|
+
}, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
|
|
10281
|
+
// Touch event handlers
|
|
10282
|
+
const handleTouchStart = (e) => {
|
|
10283
|
+
handleStart(e.touches[0].clientX);
|
|
10284
|
+
};
|
|
10285
|
+
const handleTouchMove = (e) => {
|
|
10286
|
+
handleMove(e.touches[0].clientX);
|
|
10287
|
+
};
|
|
10288
|
+
const handleTouchEnd = () => {
|
|
10289
|
+
handleEnd();
|
|
10290
|
+
};
|
|
10291
|
+
// Mouse event handlers (for testing/desktop)
|
|
10292
|
+
const handleMouseDown = (e) => {
|
|
10293
|
+
handleStart(e.clientX);
|
|
10294
|
+
};
|
|
10295
|
+
const handleMouseMove = (e) => {
|
|
10296
|
+
handleMove(e.clientX);
|
|
10297
|
+
};
|
|
10298
|
+
const handleMouseUp = () => {
|
|
10299
|
+
handleEnd();
|
|
10300
|
+
};
|
|
10301
|
+
// Close on outside click
|
|
10302
|
+
React.useEffect(() => {
|
|
10303
|
+
if (activeDirection === null)
|
|
10304
|
+
return;
|
|
10305
|
+
const handleClickOutside = (e) => {
|
|
10306
|
+
if (containerRef.current && !containerRef.current.contains(e.target)) {
|
|
10307
|
+
resetPosition();
|
|
10308
|
+
}
|
|
10309
|
+
};
|
|
10310
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
10311
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
10312
|
+
}, [activeDirection, resetPosition]);
|
|
10313
|
+
// Handle mouse leave during drag
|
|
10314
|
+
React.useEffect(() => {
|
|
10315
|
+
if (!isDragging)
|
|
10316
|
+
return;
|
|
10317
|
+
const handleMouseLeave = () => {
|
|
10318
|
+
handleEnd();
|
|
10319
|
+
};
|
|
10320
|
+
document.addEventListener('mouseup', handleMouseLeave);
|
|
10321
|
+
return () => document.removeEventListener('mouseup', handleMouseLeave);
|
|
10322
|
+
}, [isDragging, handleEnd]);
|
|
10323
|
+
// Render action button
|
|
10324
|
+
const renderActionButton = (action) => {
|
|
10325
|
+
const colorClass = colorClasses[action.color || 'default'];
|
|
10326
|
+
return (jsxRuntime.jsxs("button", { onClick: (e) => {
|
|
10327
|
+
e.stopPropagation();
|
|
10328
|
+
action.onClick();
|
|
10329
|
+
resetPosition();
|
|
10330
|
+
}, className: `
|
|
10331
|
+
flex flex-col items-center justify-center
|
|
10332
|
+
w-18 h-full min-w-[72px]
|
|
10333
|
+
${colorClass}
|
|
10334
|
+
transition-transform duration-150
|
|
10335
|
+
`, style: {
|
|
10336
|
+
transform: isDragging ? 'scale(1)' : 'scale(1)',
|
|
10337
|
+
}, children: [action.icon && (jsxRuntime.jsx("div", { className: "mb-1", children: action.icon })), jsxRuntime.jsx("span", { className: "text-xs font-medium", children: action.label })] }, action.id));
|
|
10338
|
+
};
|
|
10339
|
+
return (jsxRuntime.jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onMouseDown: handleMouseDown, onMouseMove: isDragging ? handleMouseMove : undefined, onMouseUp: handleMouseUp, onMouseLeave: isDragging ? handleEnd : undefined, children: [rightActions.length > 0 && (jsxRuntime.jsx("div", { className: "absolute left-0 top-0 bottom-0 flex", style: { width: rightActionsWidth }, children: rightActions.map((action) => renderActionButton(action)) })), leftActions.length > 0 && (jsxRuntime.jsx("div", { className: "absolute right-0 top-0 bottom-0 flex", style: { width: leftActionsWidth }, children: leftActions.map((action) => renderActionButton(action)) })), jsxRuntime.jsx("div", { ref: contentRef, className: `
|
|
10340
|
+
relative bg-white
|
|
10341
|
+
${isDragging ? '' : 'transition-transform duration-200 ease-out'}
|
|
10342
|
+
`, style: {
|
|
10343
|
+
transform: `translateX(${translateX}px)`,
|
|
10344
|
+
touchAction: 'pan-y', // Allow vertical scrolling
|
|
10345
|
+
}, children: children })] }));
|
|
7907
10346
|
}
|
|
7908
10347
|
|
|
7909
10348
|
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
|
|
@@ -51983,6 +54422,205 @@ function useColumnReorder(initialColumns, options = {}) {
|
|
|
51983
54422
|
};
|
|
51984
54423
|
}
|
|
51985
54424
|
|
|
54425
|
+
/**
|
|
54426
|
+
* Default context value (SSR-safe defaults)
|
|
54427
|
+
*/
|
|
54428
|
+
const defaultContextValue = {
|
|
54429
|
+
isMobile: false,
|
|
54430
|
+
isTablet: false,
|
|
54431
|
+
isDesktop: true,
|
|
54432
|
+
isTouchDevice: false,
|
|
54433
|
+
breakpoint: 'lg',
|
|
54434
|
+
orientation: 'landscape',
|
|
54435
|
+
viewport: { width: 1024, height: 768 },
|
|
54436
|
+
safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
54437
|
+
useMobileUI: false,
|
|
54438
|
+
};
|
|
54439
|
+
/**
|
|
54440
|
+
* Mobile context
|
|
54441
|
+
*/
|
|
54442
|
+
const MobileContext = React.createContext(defaultContextValue);
|
|
54443
|
+
/**
|
|
54444
|
+
* MobileProvider - Provides responsive state to the entire application
|
|
54445
|
+
*
|
|
54446
|
+
* Wrap your application with MobileProvider to enable auto-responsive
|
|
54447
|
+
* behavior in notebook-ui components.
|
|
54448
|
+
*
|
|
54449
|
+
* @example Basic usage
|
|
54450
|
+
* ```tsx
|
|
54451
|
+
* import { MobileProvider } from 'notebook-ui';
|
|
54452
|
+
*
|
|
54453
|
+
* function App() {
|
|
54454
|
+
* return (
|
|
54455
|
+
* <MobileProvider>
|
|
54456
|
+
* <YourApplication />
|
|
54457
|
+
* </MobileProvider>
|
|
54458
|
+
* );
|
|
54459
|
+
* }
|
|
54460
|
+
* ```
|
|
54461
|
+
*
|
|
54462
|
+
* @example Force mobile UI for testing
|
|
54463
|
+
* ```tsx
|
|
54464
|
+
* <MobileProvider forceMobileUI>
|
|
54465
|
+
* <YourApplication />
|
|
54466
|
+
* </MobileProvider>
|
|
54467
|
+
* ```
|
|
54468
|
+
*/
|
|
54469
|
+
function MobileProvider({ children, forceMobileUI = false, forceDesktopUI = false, }) {
|
|
54470
|
+
const isMobile = useIsMobile();
|
|
54471
|
+
const isTablet = useIsTablet();
|
|
54472
|
+
const isDesktop = useIsDesktop();
|
|
54473
|
+
const isTouchDevice = useIsTouchDevice();
|
|
54474
|
+
const breakpoint = useBreakpoint();
|
|
54475
|
+
const orientation = useOrientation();
|
|
54476
|
+
const viewport = useViewportSize();
|
|
54477
|
+
const safeAreaInsets = useSafeAreaInsets();
|
|
54478
|
+
const value = React.useMemo(() => {
|
|
54479
|
+
// Calculate effective mobile UI state
|
|
54480
|
+
let useMobileUI = isMobile || isTouchDevice;
|
|
54481
|
+
// Apply force overrides
|
|
54482
|
+
if (forceMobileUI) {
|
|
54483
|
+
useMobileUI = true;
|
|
54484
|
+
}
|
|
54485
|
+
else if (forceDesktopUI) {
|
|
54486
|
+
useMobileUI = false;
|
|
54487
|
+
}
|
|
54488
|
+
return {
|
|
54489
|
+
isMobile: forceMobileUI ? true : forceDesktopUI ? false : isMobile,
|
|
54490
|
+
isTablet: forceMobileUI || forceDesktopUI ? false : isTablet,
|
|
54491
|
+
isDesktop: forceDesktopUI ? true : forceMobileUI ? false : isDesktop,
|
|
54492
|
+
isTouchDevice,
|
|
54493
|
+
breakpoint: forceMobileUI ? 'xs' : forceDesktopUI ? 'lg' : breakpoint,
|
|
54494
|
+
orientation,
|
|
54495
|
+
viewport,
|
|
54496
|
+
safeAreaInsets,
|
|
54497
|
+
useMobileUI,
|
|
54498
|
+
};
|
|
54499
|
+
}, [
|
|
54500
|
+
isMobile,
|
|
54501
|
+
isTablet,
|
|
54502
|
+
isDesktop,
|
|
54503
|
+
isTouchDevice,
|
|
54504
|
+
breakpoint,
|
|
54505
|
+
orientation,
|
|
54506
|
+
viewport,
|
|
54507
|
+
safeAreaInsets,
|
|
54508
|
+
forceMobileUI,
|
|
54509
|
+
forceDesktopUI,
|
|
54510
|
+
]);
|
|
54511
|
+
return (jsxRuntime.jsx(MobileContext.Provider, { value: value, children: children }));
|
|
54512
|
+
}
|
|
54513
|
+
/**
|
|
54514
|
+
* useMobileContext - Hook to access mobile responsive state
|
|
54515
|
+
*
|
|
54516
|
+
* Must be used within a MobileProvider. Returns comprehensive
|
|
54517
|
+
* responsive state for making UI decisions.
|
|
54518
|
+
*
|
|
54519
|
+
* @example
|
|
54520
|
+
* ```tsx
|
|
54521
|
+
* function MyComponent() {
|
|
54522
|
+
* const { isMobile, useMobileUI, breakpoint } = useMobileContext();
|
|
54523
|
+
*
|
|
54524
|
+
* return useMobileUI ? <MobileView /> : <DesktopView />;
|
|
54525
|
+
* }
|
|
54526
|
+
* ```
|
|
54527
|
+
*/
|
|
54528
|
+
function useMobileContext() {
|
|
54529
|
+
const context = React.useContext(MobileContext);
|
|
54530
|
+
if (context === undefined) {
|
|
54531
|
+
// Return default value if used outside provider (graceful degradation)
|
|
54532
|
+
console.warn('useMobileContext was used outside of MobileProvider. ' +
|
|
54533
|
+
'Wrap your app with <MobileProvider> for full mobile support.');
|
|
54534
|
+
return defaultContextValue;
|
|
54535
|
+
}
|
|
54536
|
+
return context;
|
|
54537
|
+
}
|
|
54538
|
+
/**
|
|
54539
|
+
* withMobileContext - HOC to inject mobile context as props
|
|
54540
|
+
*
|
|
54541
|
+
* For class components or when you prefer props over hooks.
|
|
54542
|
+
*
|
|
54543
|
+
* @example
|
|
54544
|
+
* ```tsx
|
|
54545
|
+
* interface Props {
|
|
54546
|
+
* mobile: MobileContextValue;
|
|
54547
|
+
* }
|
|
54548
|
+
*
|
|
54549
|
+
* class MyComponent extends React.Component<Props> {
|
|
54550
|
+
* render() {
|
|
54551
|
+
* const { isMobile } = this.props.mobile;
|
|
54552
|
+
* return isMobile ? <Mobile /> : <Desktop />;
|
|
54553
|
+
* }
|
|
54554
|
+
* }
|
|
54555
|
+
*
|
|
54556
|
+
* export default withMobileContext(MyComponent);
|
|
54557
|
+
* ```
|
|
54558
|
+
*/
|
|
54559
|
+
function withMobileContext(Component) {
|
|
54560
|
+
const displayName = Component.displayName || Component.name || 'Component';
|
|
54561
|
+
const WrappedComponent = (props) => {
|
|
54562
|
+
const mobile = useMobileContext();
|
|
54563
|
+
return jsxRuntime.jsx(Component, { ...props, mobile: mobile });
|
|
54564
|
+
};
|
|
54565
|
+
WrappedComponent.displayName = `withMobileContext(${displayName})`;
|
|
54566
|
+
return WrappedComponent;
|
|
54567
|
+
}
|
|
54568
|
+
/**
|
|
54569
|
+
* MobileOnly - Renders children only on mobile devices
|
|
54570
|
+
*
|
|
54571
|
+
* @example
|
|
54572
|
+
* ```tsx
|
|
54573
|
+
* <MobileOnly>
|
|
54574
|
+
* <BottomNavigation items={navItems} />
|
|
54575
|
+
* </MobileOnly>
|
|
54576
|
+
* ```
|
|
54577
|
+
*/
|
|
54578
|
+
function MobileOnly({ children }) {
|
|
54579
|
+
const { useMobileUI } = useMobileContext();
|
|
54580
|
+
return useMobileUI ? jsxRuntime.jsx(jsxRuntime.Fragment, { children: children }) : null;
|
|
54581
|
+
}
|
|
54582
|
+
/**
|
|
54583
|
+
* DesktopOnly - Renders children only on desktop devices
|
|
54584
|
+
*
|
|
54585
|
+
* @example
|
|
54586
|
+
* ```tsx
|
|
54587
|
+
* <DesktopOnly>
|
|
54588
|
+
* <Sidebar items={navItems} />
|
|
54589
|
+
* </DesktopOnly>
|
|
54590
|
+
* ```
|
|
54591
|
+
*/
|
|
54592
|
+
function DesktopOnly({ children }) {
|
|
54593
|
+
const { useMobileUI } = useMobileContext();
|
|
54594
|
+
return useMobileUI ? null : jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
|
|
54595
|
+
}
|
|
54596
|
+
/**
|
|
54597
|
+
* Responsive - Renders different content based on device type
|
|
54598
|
+
*
|
|
54599
|
+
* @example
|
|
54600
|
+
* ```tsx
|
|
54601
|
+
* <Responsive
|
|
54602
|
+
* mobile={<MobileNavigation />}
|
|
54603
|
+
* tablet={<TabletNavigation />}
|
|
54604
|
+
* desktop={<DesktopNavigation />}
|
|
54605
|
+
* />
|
|
54606
|
+
* ```
|
|
54607
|
+
*/
|
|
54608
|
+
function Responsive({ mobile, tablet, desktop, }) {
|
|
54609
|
+
const { isMobile, isTablet, isDesktop } = useMobileContext();
|
|
54610
|
+
if (isMobile && mobile)
|
|
54611
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: mobile });
|
|
54612
|
+
if (isTablet && tablet)
|
|
54613
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: tablet });
|
|
54614
|
+
if (isDesktop && desktop)
|
|
54615
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: desktop });
|
|
54616
|
+
// Fallback: desktop -> tablet -> mobile
|
|
54617
|
+
if (isDesktop)
|
|
54618
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: desktop || tablet || mobile });
|
|
54619
|
+
if (isTablet)
|
|
54620
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: tablet || mobile || desktop });
|
|
54621
|
+
return jsxRuntime.jsx(jsxRuntime.Fragment, { children: mobile || tablet || desktop });
|
|
54622
|
+
}
|
|
54623
|
+
|
|
51986
54624
|
exports.Accordion = Accordion;
|
|
51987
54625
|
exports.ActionButton = ActionButton;
|
|
51988
54626
|
exports.AdminModal = AdminModal;
|
|
@@ -51991,7 +54629,10 @@ exports.AlertDialog = AlertDialog;
|
|
|
51991
54629
|
exports.AppLayout = AppLayout;
|
|
51992
54630
|
exports.Autocomplete = Autocomplete;
|
|
51993
54631
|
exports.Avatar = Avatar;
|
|
54632
|
+
exports.BREAKPOINTS = BREAKPOINTS;
|
|
51994
54633
|
exports.Badge = Badge;
|
|
54634
|
+
exports.BottomNavigation = BottomNavigation;
|
|
54635
|
+
exports.BottomNavigationSpacer = BottomNavigationSpacer;
|
|
51995
54636
|
exports.BottomSheet = BottomSheet;
|
|
51996
54637
|
exports.Box = Box;
|
|
51997
54638
|
exports.Breadcrumbs = Breadcrumbs;
|
|
@@ -52007,7 +54648,9 @@ exports.CardTitle = CardTitle;
|
|
|
52007
54648
|
exports.CardView = CardView;
|
|
52008
54649
|
exports.Carousel = Carousel;
|
|
52009
54650
|
exports.Checkbox = Checkbox;
|
|
54651
|
+
exports.CheckboxList = CheckboxList;
|
|
52010
54652
|
exports.Chip = Chip;
|
|
54653
|
+
exports.ChipGroup = ChipGroup;
|
|
52011
54654
|
exports.Collapsible = Collapsible;
|
|
52012
54655
|
exports.ColorPicker = ColorPicker;
|
|
52013
54656
|
exports.Combobox = Combobox;
|
|
@@ -52022,10 +54665,12 @@ exports.Dashboard = Dashboard;
|
|
|
52022
54665
|
exports.DashboardContent = DashboardContent;
|
|
52023
54666
|
exports.DashboardHeader = DashboardHeader;
|
|
52024
54667
|
exports.DataTable = DataTable;
|
|
54668
|
+
exports.DataTableCardView = DataTableCardView;
|
|
52025
54669
|
exports.DateDisplay = DateDisplay;
|
|
52026
54670
|
exports.DatePicker = DatePicker;
|
|
52027
54671
|
exports.DateRangePicker = DateRangePicker;
|
|
52028
54672
|
exports.DateTimePicker = DateTimePicker;
|
|
54673
|
+
exports.DesktopOnly = DesktopOnly;
|
|
52029
54674
|
exports.Drawer = Drawer;
|
|
52030
54675
|
exports.DrawerFooter = DrawerFooter;
|
|
52031
54676
|
exports.DropZone = DropZone;
|
|
@@ -52033,6 +54678,9 @@ exports.Dropdown = Dropdown;
|
|
|
52033
54678
|
exports.DropdownTrigger = DropdownTrigger;
|
|
52034
54679
|
exports.EmptyState = EmptyState;
|
|
52035
54680
|
exports.ErrorBoundary = ErrorBoundary;
|
|
54681
|
+
exports.ExpandablePanel = ExpandablePanel;
|
|
54682
|
+
exports.ExpandablePanelContainer = ExpandablePanelContainer;
|
|
54683
|
+
exports.ExpandablePanelSpacer = ExpandablePanelSpacer;
|
|
52036
54684
|
exports.ExpandableRowButton = ExpandableRowButton;
|
|
52037
54685
|
exports.ExpandableToolbar = ExpandableToolbar;
|
|
52038
54686
|
exports.ExpandedRowEditForm = ExpandedRowEditForm;
|
|
@@ -52042,6 +54690,7 @@ exports.FileUpload = FileUpload;
|
|
|
52042
54690
|
exports.FilterBar = FilterBar;
|
|
52043
54691
|
exports.FilterControls = FilterControls;
|
|
52044
54692
|
exports.FilterStatusBanner = FilterStatusBanner;
|
|
54693
|
+
exports.FloatingActionButton = FloatingActionButton;
|
|
52045
54694
|
exports.Form = Form;
|
|
52046
54695
|
exports.FormContext = FormContext;
|
|
52047
54696
|
exports.FormControl = FormControl;
|
|
@@ -52061,6 +54710,11 @@ exports.MarkdownEditor = MarkdownEditor;
|
|
|
52061
54710
|
exports.MaskedInput = MaskedInput;
|
|
52062
54711
|
exports.Menu = Menu;
|
|
52063
54712
|
exports.MenuDivider = MenuDivider;
|
|
54713
|
+
exports.MobileHeader = MobileHeader;
|
|
54714
|
+
exports.MobileHeaderSpacer = MobileHeaderSpacer;
|
|
54715
|
+
exports.MobileLayout = MobileLayout;
|
|
54716
|
+
exports.MobileOnly = MobileOnly;
|
|
54717
|
+
exports.MobileProvider = MobileProvider;
|
|
52064
54718
|
exports.Modal = Modal;
|
|
52065
54719
|
exports.ModalFooter = ModalFooter;
|
|
52066
54720
|
exports.MultiSelect = MultiSelect;
|
|
@@ -52074,18 +54728,21 @@ exports.Pagination = Pagination;
|
|
|
52074
54728
|
exports.PasswordInput = PasswordInput;
|
|
52075
54729
|
exports.Popover = Popover;
|
|
52076
54730
|
exports.Progress = Progress;
|
|
54731
|
+
exports.PullToRefresh = PullToRefresh;
|
|
52077
54732
|
exports.QueryTransparency = QueryTransparency;
|
|
52078
54733
|
exports.RadioGroup = RadioGroup;
|
|
52079
54734
|
exports.Rating = Rating;
|
|
54735
|
+
exports.Responsive = Responsive;
|
|
52080
54736
|
exports.RichTextEditor = RichTextEditor;
|
|
52081
54737
|
exports.SearchBar = SearchBar;
|
|
54738
|
+
exports.SearchableList = SearchableList;
|
|
52082
54739
|
exports.Select = Select;
|
|
52083
54740
|
exports.Separator = Separator;
|
|
52084
54741
|
exports.Show = Show;
|
|
52085
54742
|
exports.Sidebar = Sidebar;
|
|
52086
54743
|
exports.SidebarGroup = SidebarGroup;
|
|
52087
54744
|
exports.Skeleton = Skeleton;
|
|
52088
|
-
exports.SkeletonCard = SkeletonCard;
|
|
54745
|
+
exports.SkeletonCard = SkeletonCard$1;
|
|
52089
54746
|
exports.SkeletonTable = SkeletonTable;
|
|
52090
54747
|
exports.Slider = Slider;
|
|
52091
54748
|
exports.Spreadsheet = Spreadsheet;
|
|
@@ -52099,6 +54756,7 @@ exports.StatusBadge = StatusBadge;
|
|
|
52099
54756
|
exports.StatusBar = StatusBar;
|
|
52100
54757
|
exports.StepIndicator = StepIndicator;
|
|
52101
54758
|
exports.Stepper = Stepper;
|
|
54759
|
+
exports.SwipeActions = SwipeActions;
|
|
52102
54760
|
exports.Switch = Switch;
|
|
52103
54761
|
exports.Tabs = Tabs;
|
|
52104
54762
|
exports.Text = Text;
|
|
@@ -52133,10 +54791,25 @@ exports.reorderArray = reorderArray;
|
|
|
52133
54791
|
exports.saveColumnOrder = saveColumnOrder;
|
|
52134
54792
|
exports.saveColumnWidths = saveColumnWidths;
|
|
52135
54793
|
exports.statusManager = statusManager;
|
|
54794
|
+
exports.useBreakpoint = useBreakpoint;
|
|
54795
|
+
exports.useBreakpointValue = useBreakpointValue;
|
|
52136
54796
|
exports.useColumnReorder = useColumnReorder;
|
|
52137
54797
|
exports.useColumnResize = useColumnResize;
|
|
52138
54798
|
exports.useCommandPalette = useCommandPalette;
|
|
52139
54799
|
exports.useConfirmDialog = useConfirmDialog;
|
|
54800
|
+
exports.useFABScroll = useFABScroll;
|
|
52140
54801
|
exports.useFormContext = useFormContext;
|
|
54802
|
+
exports.useIsDesktop = useIsDesktop;
|
|
54803
|
+
exports.useIsMobile = useIsMobile;
|
|
54804
|
+
exports.useIsTablet = useIsTablet;
|
|
54805
|
+
exports.useIsTouchDevice = useIsTouchDevice;
|
|
52141
54806
|
exports.useMediaQuery = useMediaQuery;
|
|
54807
|
+
exports.useMobileContext = useMobileContext;
|
|
54808
|
+
exports.useOrientation = useOrientation;
|
|
54809
|
+
exports.usePrefersMobile = usePrefersMobile;
|
|
54810
|
+
exports.usePullToRefresh = usePullToRefresh;
|
|
54811
|
+
exports.useResponsiveCallback = useResponsiveCallback;
|
|
54812
|
+
exports.useSafeAreaInsets = useSafeAreaInsets;
|
|
54813
|
+
exports.useViewportSize = useViewportSize;
|
|
54814
|
+
exports.withMobileContext = withMobileContext;
|
|
52142
54815
|
//# sourceMappingURL=index.js.map
|