@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.
Files changed (100) hide show
  1. package/dist/components/BottomNavigation.d.ts +98 -0
  2. package/dist/components/BottomNavigation.d.ts.map +1 -0
  3. package/dist/components/Checkbox.d.ts +2 -0
  4. package/dist/components/Checkbox.d.ts.map +1 -1
  5. package/dist/components/CheckboxList.d.ts +81 -0
  6. package/dist/components/CheckboxList.d.ts.map +1 -0
  7. package/dist/components/Chip.d.ts +92 -1
  8. package/dist/components/Chip.d.ts.map +1 -1
  9. package/dist/components/ConfirmDialog.d.ts +43 -1
  10. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  11. package/dist/components/DataTable.d.ts +10 -1
  12. package/dist/components/DataTable.d.ts.map +1 -1
  13. package/dist/components/DataTableCardView.d.ts +99 -0
  14. package/dist/components/DataTableCardView.d.ts.map +1 -0
  15. package/dist/components/ExpandablePanel.d.ts +142 -0
  16. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  17. package/dist/components/FloatingActionButton.d.ts +98 -0
  18. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  19. package/dist/components/Input.d.ts +45 -1
  20. package/dist/components/Input.d.ts.map +1 -1
  21. package/dist/components/MobileHeader.d.ts +98 -0
  22. package/dist/components/MobileHeader.d.ts.map +1 -0
  23. package/dist/components/MobileLayout.d.ts +121 -0
  24. package/dist/components/MobileLayout.d.ts.map +1 -0
  25. package/dist/components/Modal.d.ts +50 -1
  26. package/dist/components/Modal.d.ts.map +1 -1
  27. package/dist/components/PullToRefresh.d.ts +87 -0
  28. package/dist/components/PullToRefresh.d.ts.map +1 -0
  29. package/dist/components/QueryTransparency.d.ts +1 -1
  30. package/dist/components/QueryTransparency.d.ts.map +1 -1
  31. package/dist/components/SearchableList.d.ts +83 -0
  32. package/dist/components/SearchableList.d.ts.map +1 -0
  33. package/dist/components/Select.d.ts +16 -2
  34. package/dist/components/Select.d.ts.map +1 -1
  35. package/dist/components/Sidebar.d.ts +40 -1
  36. package/dist/components/Sidebar.d.ts.map +1 -1
  37. package/dist/components/SwipeActions.d.ts +93 -0
  38. package/dist/components/SwipeActions.d.ts.map +1 -0
  39. package/dist/components/Switch.d.ts +1 -0
  40. package/dist/components/Switch.d.ts.map +1 -1
  41. package/dist/components/Textarea.d.ts +13 -0
  42. package/dist/components/Textarea.d.ts.map +1 -1
  43. package/dist/components/index.d.ts +27 -3
  44. package/dist/components/index.d.ts.map +1 -1
  45. package/dist/context/MobileContext.d.ts +168 -0
  46. package/dist/context/MobileContext.d.ts.map +1 -0
  47. package/dist/hooks/useResponsive.d.ts +158 -0
  48. package/dist/hooks/useResponsive.d.ts.map +1 -0
  49. package/dist/index.d.ts +1653 -56
  50. package/dist/index.esm.js +2832 -194
  51. package/dist/index.esm.js.map +1 -1
  52. package/dist/index.js +2865 -192
  53. package/dist/index.js.map +1 -1
  54. package/dist/styles.css +404 -1
  55. package/dist/types/index.d.ts +2 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/components/BottomNavigation.stories.tsx +142 -0
  59. package/src/components/BottomNavigation.tsx +225 -0
  60. package/src/components/Checkbox.stories.tsx +162 -0
  61. package/src/components/Checkbox.tsx +22 -6
  62. package/src/components/CheckboxList.stories.tsx +311 -0
  63. package/src/components/CheckboxList.tsx +433 -0
  64. package/src/components/Chip.stories.tsx +389 -0
  65. package/src/components/Chip.tsx +182 -3
  66. package/src/components/ConfirmDialog.tsx +56 -4
  67. package/src/components/DataTable.tsx +60 -1
  68. package/src/components/DataTableCardView.stories.tsx +307 -0
  69. package/src/components/DataTableCardView.tsx +419 -0
  70. package/src/components/ExpandablePanel.stories.tsx +620 -0
  71. package/src/components/ExpandablePanel.tsx +383 -0
  72. package/src/components/FloatingActionButton.stories.tsx +197 -0
  73. package/src/components/FloatingActionButton.tsx +301 -0
  74. package/src/components/Grid.stories.tsx +16 -16
  75. package/src/components/Input.stories.tsx +214 -0
  76. package/src/components/Input.tsx +81 -4
  77. package/src/components/MobileHeader.stories.tsx +205 -0
  78. package/src/components/MobileHeader.tsx +233 -0
  79. package/src/components/MobileLayout.stories.tsx +338 -0
  80. package/src/components/MobileLayout.tsx +313 -0
  81. package/src/components/Modal.stories.tsx +183 -0
  82. package/src/components/Modal.tsx +84 -3
  83. package/src/components/PullToRefresh.stories.tsx +321 -0
  84. package/src/components/PullToRefresh.tsx +294 -0
  85. package/src/components/QueryTransparency.tsx +1 -1
  86. package/src/components/SearchableList.stories.tsx +437 -0
  87. package/src/components/SearchableList.tsx +326 -0
  88. package/src/components/Select.stories.tsx +190 -0
  89. package/src/components/Select.tsx +353 -137
  90. package/src/components/Sidebar.tsx +191 -8
  91. package/src/components/SwipeActions.stories.tsx +327 -0
  92. package/src/components/SwipeActions.tsx +387 -0
  93. package/src/components/Switch.stories.tsx +158 -0
  94. package/src/components/Switch.tsx +12 -3
  95. package/src/components/Textarea.tsx +31 -1
  96. package/src/components/index.ts +63 -3
  97. package/src/context/MobileContext.tsx +296 -0
  98. package/src/hooks/useResponsive.ts +360 -0
  99. package/src/types/index.ts +4 -0
  100. 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-2", 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", "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", "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] }))] })] }));
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
- * Select - Dropdown select component with search, groups, and virtual scrolling
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 clear functionality.
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 && searchInputRef.current) {
507
- searchInputRef.current.focus();
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: "h-4 w-4" }) })), jsxRuntime.jsx(lucideReact.ChevronDown, { className: `h-4 w-4 text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }), isOpen && (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: loading ? (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..." })] })) : filteredOptions.length === 0 && filteredGroups.length === 0 && !showCreateOption ? (jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-ink-500 text-center", role: "status", "aria-live": "polite", children: "No options found" })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [showCreateOption && (jsxRuntime.jsx("button", { type: "button", onClick: handleCreateOption, className: "w-full flex items-center px-4 py-2.5 text-sm text-accent-700 hover:bg-accent-50 transition-colors border-b border-paper-200", 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) => {
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[size];
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
- return (jsxRuntime.jsxs("label", { htmlFor: switchId, className: `flex items-center gap-3 ${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: `
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: "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 })] }))] }));
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
- const Textarea = React.forwardRef(({ label, helperText, validationState, validationMessage, maxLength, showCharCount = false, autoExpand = false, minRows = 2, maxRows = 10, resize = 'vertical', loading = false, className = '', id, value, rows = 4, ...props }, ref) => {
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 px-4 py-3 border rounded-lg text-sm text-ink-800 placeholder-ink-400
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
- const Checkbox = React.forwardRef(({ checked, onChange, label, description, disabled = false, indeterminate = false, className = '', id, name, icon, }, ref) => {
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-3 ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} ${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: `
775
- w-4 h-4 rounded border transition-all duration-200
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: "h-3 w-3 text-white" })) : checked ? (jsxRuntime.jsx(lucideReact.Check, { className: "h-3 w-3 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: "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 }))] }))] }));
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 sizeClasses$4 = {
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
- function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', }) {
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
- // Handle escape key
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
- 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$4[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 })] }) }));
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$3 = {
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$3[placement][size]}
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
- function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, }) {
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$2 = {
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
- function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, }) {
5180
- const variantStyle = variantClasses[variant];
5181
- const sizeStyle = sizeClasses$2[size];
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$1 = {
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$1[size];
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
- function Sidebar({ items, onNavigate, className = '', header, footer, currentPath }) {
6752
- return (jsxRuntime.jsxs("div", { className: `flex flex-col h-full bg-white border-r border-paper-300 notebook-binding ${className}`, children: [header && (jsxRuntime.jsx("div", { className: "px-6 pt-6 pb-4", children: header })), jsxRuntime.jsx("nav", { className: "flex-1 px-3 py-2 space-y-1 overflow-y-auto", children: items.map((item) => {
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: onNavigate, currentPath: currentPath }, item.id));
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