@papernote/ui 1.3.1 → 1.6.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 (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. 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,191 @@ 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 Scrollable modal for long content
3188
+ * ```tsx
3189
+ * <Modal
3190
+ * isOpen={isOpen}
3191
+ * onClose={handleClose}
3192
+ * title="Terms and Conditions"
3193
+ * scrollable
3194
+ * >
3195
+ * {longContent}
3196
+ * </Modal>
3197
+ * ```
3198
+ *
3199
+ * @example Modal with custom max height
3200
+ * ```tsx
3201
+ * <Modal
3202
+ * isOpen={isOpen}
3203
+ * onClose={handleClose}
3204
+ * title="Document Preview"
3205
+ * maxHeight="70vh"
3206
+ * >
3207
+ * {documentContent}
3208
+ * </Modal>
3209
+ * ```
3210
+ *
3211
+ * @example Force modal on mobile
3212
+ * ```tsx
3213
+ * <Modal
3214
+ * isOpen={isOpen}
3215
+ * onClose={handleClose}
3216
+ * title="Settings"
3217
+ * mobileMode="modal"
3218
+ * >
3219
+ * ...
3220
+ * </Modal>
3221
+ * ```
3222
+ *
3223
+ * @example Always use BottomSheet
3224
+ * ```tsx
3225
+ * <Modal
3226
+ * isOpen={isOpen}
3227
+ * onClose={handleClose}
3228
+ * title="Select Option"
3229
+ * mobileMode="sheet"
3230
+ * mobileHeight="md"
3231
+ * >
3232
+ * ...
3233
+ * </Modal>
3234
+ * ```
3235
+ */
3236
+ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', scrollable = false, maxHeight, mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
2621
3237
  const modalRef = React.useRef(null);
2622
3238
  const mouseDownOnBackdrop = React.useRef(false);
2623
3239
  const titleId = React.useId();
2624
- // Handle escape key
3240
+ const isMobile = useIsMobile();
3241
+ // Determine if we should use BottomSheet
3242
+ const useBottomSheet = mobileMode === 'sheet' ||
3243
+ (mobileMode === 'auto' && isMobile);
3244
+ // Handle escape key (only for modal mode, BottomSheet handles its own)
2625
3245
  React.useEffect(() => {
3246
+ if (useBottomSheet)
3247
+ return; // BottomSheet handles escape
2626
3248
  const handleEscape = (e) => {
2627
3249
  if (e.key === 'Escape' && isOpen) {
2628
3250
  onClose();
@@ -2636,7 +3258,7 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2636
3258
  document.removeEventListener('keydown', handleEscape);
2637
3259
  document.body.style.overflow = 'unset';
2638
3260
  };
2639
- }, [isOpen, onClose]);
3261
+ }, [isOpen, onClose, useBottomSheet]);
2640
3262
  // Track if mousedown originated on the backdrop
2641
3263
  const handleBackdropMouseDown = (e) => {
2642
3264
  if (e.target === e.currentTarget) {
@@ -2672,13 +3294,20 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2672
3294
  };
2673
3295
  if (!isOpen)
2674
3296
  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 })] }) }));
3297
+ // Render as BottomSheet on mobile
3298
+ if (useBottomSheet) {
3299
+ return (jsxRuntime.jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }));
3300
+ }
3301
+ // Render as standard modal on desktop
3302
+ 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 ${scrollable || maxHeight ? 'overflow-y-auto' : ''}`, style: {
3303
+ maxHeight: maxHeight || (scrollable ? 'calc(100vh - 200px)' : undefined),
3304
+ }, children: children })] }) }));
2676
3305
  }
2677
3306
  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 }));
3307
+ 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
3308
  }
2680
3309
 
2681
- const sizeClasses$3 = {
3310
+ const sizeClasses$7 = {
2682
3311
  left: {
2683
3312
  sm: 'w-64',
2684
3313
  md: 'w-96',
@@ -2757,7 +3386,7 @@ function Drawer({ isOpen, onClose, title, children, placement = 'right', size =
2757
3386
  const isHorizontal = placement === 'left' || placement === 'right';
2758
3387
  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
3388
  fixed ${placementClasses[placement]}
2760
- ${sizeClasses$3[placement][size]}
3389
+ ${sizeClasses$7[placement][size]}
2761
3390
  bg-white border-paper-200 shadow-2xl
2762
3391
  ${isHorizontal ? 'border-r' : 'border-b'}
2763
3392
  ${animationClasses[placement].enter}
@@ -2789,14 +3418,49 @@ const variantStyles = {
2789
3418
  button: 'bg-accent-600 hover:bg-accent-700 focus:ring-accent-500',
2790
3419
  },
2791
3420
  };
2792
- function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, }) {
3421
+ /**
3422
+ * ConfirmDialog - Confirmation dialog with mobile support
3423
+ *
3424
+ * @example Basic usage
3425
+ * ```tsx
3426
+ * <ConfirmDialog
3427
+ * isOpen={isOpen}
3428
+ * onClose={handleClose}
3429
+ * onConfirm={handleDelete}
3430
+ * title="Delete Item"
3431
+ * message="Are you sure you want to delete this item? This action cannot be undone."
3432
+ * variant="danger"
3433
+ * />
3434
+ * ```
3435
+ *
3436
+ * @example With useConfirmDialog hook
3437
+ * ```tsx
3438
+ * const confirmDialog = useConfirmDialog();
3439
+ *
3440
+ * const handleDelete = () => {
3441
+ * confirmDialog.show({
3442
+ * title: 'Delete Item',
3443
+ * message: 'Are you sure?',
3444
+ * onConfirm: async () => await deleteItem(),
3445
+ * });
3446
+ * };
3447
+ *
3448
+ * return (
3449
+ * <>
3450
+ * <button onClick={handleDelete}>Delete</button>
3451
+ * <ConfirmDialog {...confirmDialog.props} />
3452
+ * </>
3453
+ * );
3454
+ * ```
3455
+ */
3456
+ function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, mobileMode = 'auto', mobileHeight = 'sm', }) {
2793
3457
  const variantStyle = variantStyles[variant];
2794
3458
  const IconComponent = icon || variantStyle.icon;
2795
3459
  const handleConfirm = async () => {
2796
3460
  await onConfirm();
2797
3461
  // Note: onClose is called by useConfirmDialog hook after onConfirm completes
2798
3462
  };
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)) })] })] }));
3463
+ 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
3464
  }
2801
3465
  /**
2802
3466
  * Hook for managing ConfirmDialog state
@@ -5127,39 +5791,45 @@ function MenuDivider() {
5127
5791
  return { divider: true, id: `divider-${Date.now()}`, label: '' };
5128
5792
  }
5129
5793
 
5130
- const variantClasses = {
5794
+ const variantClasses$4 = {
5131
5795
  primary: {
5132
5796
  default: 'bg-primary-100 text-primary-700 border-primary-200',
5133
5797
  hover: 'hover:bg-primary-200',
5134
5798
  close: 'hover:bg-primary-300 text-primary-600',
5799
+ selected: 'bg-primary-200 border-primary-400 ring-2 ring-primary-300',
5135
5800
  },
5136
5801
  secondary: {
5137
5802
  default: 'bg-ink-100 text-ink-700 border-ink-200',
5138
5803
  hover: 'hover:bg-ink-200',
5139
5804
  close: 'hover:bg-ink-300 text-ink-600',
5805
+ selected: 'bg-ink-200 border-ink-400 ring-2 ring-ink-300',
5140
5806
  },
5141
5807
  success: {
5142
5808
  default: 'bg-success-100 text-success-700 border-success-200',
5143
5809
  hover: 'hover:bg-success-200',
5144
5810
  close: 'hover:bg-success-300 text-success-600',
5811
+ selected: 'bg-success-200 border-success-400 ring-2 ring-success-300',
5145
5812
  },
5146
5813
  warning: {
5147
5814
  default: 'bg-warning-100 text-warning-700 border-warning-200',
5148
5815
  hover: 'hover:bg-warning-200',
5149
5816
  close: 'hover:bg-warning-300 text-warning-600',
5817
+ selected: 'bg-warning-200 border-warning-400 ring-2 ring-warning-300',
5150
5818
  },
5151
5819
  error: {
5152
5820
  default: 'bg-error-100 text-error-700 border-error-200',
5153
5821
  hover: 'hover:bg-error-200',
5154
5822
  close: 'hover:bg-error-300 text-error-600',
5823
+ selected: 'bg-error-200 border-error-400 ring-2 ring-error-300',
5155
5824
  },
5156
5825
  info: {
5157
5826
  default: 'bg-accent-100 text-accent-700 border-accent-200',
5158
5827
  hover: 'hover:bg-accent-200',
5159
5828
  close: 'hover:bg-accent-300 text-accent-600',
5829
+ selected: 'bg-accent-200 border-accent-400 ring-2 ring-accent-300',
5160
5830
  },
5161
5831
  };
5162
- const sizeClasses$2 = {
5832
+ const sizeClasses$6 = {
5163
5833
  sm: {
5164
5834
  container: 'h-6 px-2 text-xs gap-1',
5165
5835
  icon: 'h-3 w-3',
@@ -5176,20 +5846,52 @@ const sizeClasses$2 = {
5176
5846
  close: 'h-4 w-4 ml-2',
5177
5847
  },
5178
5848
  };
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];
5849
+ const gapClasses = {
5850
+ xs: 'gap-1',
5851
+ sm: 'gap-1.5',
5852
+ md: 'gap-2',
5853
+ lg: 'gap-3',
5854
+ };
5855
+ /**
5856
+ * Chip - Compact element for displaying values with optional remove functionality
5857
+ *
5858
+ * @example Basic chip
5859
+ * ```tsx
5860
+ * <Chip>Tag Name</Chip>
5861
+ * ```
5862
+ *
5863
+ * @example Removable chip
5864
+ * ```tsx
5865
+ * <Chip onClose={() => removeTag(tag)}>
5866
+ * {tag.name}
5867
+ * </Chip>
5868
+ * ```
5869
+ *
5870
+ * @example With icon and selected state
5871
+ * ```tsx
5872
+ * <Chip
5873
+ * icon={<Star className="h-3 w-3" />}
5874
+ * selected={isSelected}
5875
+ * onClick={() => toggleSelection()}
5876
+ * >
5877
+ * Favorite
5878
+ * </Chip>
5879
+ * ```
5880
+ */
5881
+ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, selected = false, maxWidth, chipKey, }) {
5882
+ const variantStyle = variantClasses$4[variant];
5883
+ const sizeStyle = sizeClasses$6[size];
5182
5884
  const isClickable = !disabled && (onClick || onClose);
5183
5885
  return (jsxRuntime.jsxs("div", { className: `
5184
5886
  inline-flex items-center rounded-full border font-medium
5185
5887
  transition-colors
5186
- ${variantStyle.default}
5187
- ${isClickable && !disabled ? variantStyle.hover : ''}
5888
+ ${selected ? variantStyle.selected : variantStyle.default}
5889
+ ${isClickable && !disabled && !selected ? variantStyle.hover : ''}
5188
5890
  ${sizeStyle.container}
5189
5891
  ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
5190
5892
  ${onClick && !disabled ? 'cursor-pointer' : ''}
5191
5893
  ${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) => {
5894
+ `, 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
5895
  e.stopPropagation();
5194
5896
  if (!disabled)
5195
5897
  onClose();
@@ -5200,8 +5902,420 @@ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, dis
5200
5902
  ${sizeStyle.close}
5201
5903
  `, "aria-label": "Remove", children: jsxRuntime.jsx(lucideReact.X, { className: "w-full h-full" }) }))] }));
5202
5904
  }
5905
+ /**
5906
+ * ChipGroup - Container for multiple chips with layout and selection support
5907
+ *
5908
+ * @example Basic group
5909
+ * ```tsx
5910
+ * <ChipGroup wrap gap="sm">
5911
+ * {tags.map(tag => (
5912
+ * <Chip key={tag.id} onClose={() => removeTag(tag)}>
5913
+ * {tag.name}
5914
+ * </Chip>
5915
+ * ))}
5916
+ * </ChipGroup>
5917
+ * ```
5918
+ *
5919
+ * @example Selectable group (single)
5920
+ * ```tsx
5921
+ * <ChipGroup
5922
+ * selectionMode="single"
5923
+ * selectedKeys={[selectedCategory]}
5924
+ * onSelectionChange={(keys) => setSelectedCategory(keys[0])}
5925
+ * >
5926
+ * <Chip chipKey="all">All</Chip>
5927
+ * <Chip chipKey="active">Active</Chip>
5928
+ * <Chip chipKey="archived">Archived</Chip>
5929
+ * </ChipGroup>
5930
+ * ```
5931
+ *
5932
+ * @example Multi-select group
5933
+ * ```tsx
5934
+ * <ChipGroup
5935
+ * selectionMode="multiple"
5936
+ * selectedKeys={selectedTags}
5937
+ * onSelectionChange={setSelectedTags}
5938
+ * wrap
5939
+ * >
5940
+ * {availableTags.map(tag => (
5941
+ * <Chip key={tag} chipKey={tag}>{tag}</Chip>
5942
+ * ))}
5943
+ * </ChipGroup>
5944
+ * ```
5945
+ */
5946
+ function ChipGroup({ children, direction = 'horizontal', wrap = false, gap = 'sm', selectionMode = 'none', selectedKeys = [], onSelectionChange, className = '', }) {
5947
+ const handleChipClick = (chipKey) => {
5948
+ if (selectionMode === 'none' || !onSelectionChange)
5949
+ return;
5950
+ if (selectionMode === 'single') {
5951
+ // Toggle single selection
5952
+ if (selectedKeys.includes(chipKey)) {
5953
+ onSelectionChange([]);
5954
+ }
5955
+ else {
5956
+ onSelectionChange([chipKey]);
5957
+ }
5958
+ }
5959
+ else if (selectionMode === 'multiple') {
5960
+ // Toggle in array
5961
+ if (selectedKeys.includes(chipKey)) {
5962
+ onSelectionChange(selectedKeys.filter(k => k !== chipKey));
5963
+ }
5964
+ else {
5965
+ onSelectionChange([...selectedKeys, chipKey]);
5966
+ }
5967
+ }
5968
+ };
5969
+ // Clone children to inject selection props
5970
+ const enhancedChildren = React.Children.map(children, (child) => {
5971
+ if (!React.isValidElement(child))
5972
+ return child;
5973
+ const chipKey = child.props.chipKey;
5974
+ if (!chipKey || selectionMode === 'none')
5975
+ return child;
5976
+ const isSelected = selectedKeys.includes(chipKey);
5977
+ return React.cloneElement(child, {
5978
+ ...child.props,
5979
+ selected: isSelected,
5980
+ onClick: () => {
5981
+ // Call original onClick if exists
5982
+ if (child.props.onClick) {
5983
+ child.props.onClick();
5984
+ }
5985
+ handleChipClick(chipKey);
5986
+ },
5987
+ });
5988
+ });
5989
+ return (jsxRuntime.jsx("div", { className: `
5990
+ flex
5991
+ ${direction === 'vertical' ? 'flex-col' : 'flex-row'}
5992
+ ${wrap ? 'flex-wrap' : ''}
5993
+ ${gapClasses[gap]}
5994
+ ${className}
5995
+ `, role: selectionMode !== 'none' ? 'group' : undefined, "aria-label": selectionMode !== 'none' ? 'Chip selection group' : undefined, children: enhancedChildren }));
5996
+ }
5203
5997
 
5204
- const sizeClasses$1 = {
5998
+ const sizeClasses$5 = {
5999
+ sm: {
6000
+ item: 'py-1.5 px-2',
6001
+ text: 'text-sm',
6002
+ description: 'text-xs',
6003
+ groupHeader: 'py-1.5 px-2 text-xs',
6004
+ },
6005
+ md: {
6006
+ item: 'py-2 px-3',
6007
+ text: 'text-sm',
6008
+ description: 'text-xs',
6009
+ groupHeader: 'py-2 px-3 text-xs',
6010
+ },
6011
+ lg: {
6012
+ item: 'py-3 px-4',
6013
+ text: 'text-base',
6014
+ description: 'text-sm',
6015
+ groupHeader: 'py-2.5 px-4 text-sm',
6016
+ },
6017
+ };
6018
+ const variantClasses$3 = {
6019
+ default: 'bg-white',
6020
+ bordered: 'bg-white border border-paper-300 rounded-lg',
6021
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
6022
+ };
6023
+ /**
6024
+ * CheckboxList - Multi-select list with checkboxes, grouping, and search
6025
+ *
6026
+ * @example Basic usage
6027
+ * ```tsx
6028
+ * <CheckboxList
6029
+ * items={[
6030
+ * { key: '1', label: 'Option 1' },
6031
+ * { key: '2', label: 'Option 2' },
6032
+ * ]}
6033
+ * selectedKeys={selected}
6034
+ * onSelectionChange={setSelected}
6035
+ * />
6036
+ * ```
6037
+ *
6038
+ * @example With grouping and search
6039
+ * ```tsx
6040
+ * <CheckboxList
6041
+ * items={fields}
6042
+ * selectedKeys={selectedFields}
6043
+ * onSelectionChange={setSelectedFields}
6044
+ * groupLabels={{ table1: 'Users', table2: 'Orders' }}
6045
+ * searchable
6046
+ * searchPlaceholder="Search fields..."
6047
+ * showSelectAll
6048
+ * maxHeight="300px"
6049
+ * />
6050
+ * ```
6051
+ */
6052
+ 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 = '', }) {
6053
+ const [searchTerm, setSearchTerm] = React.useState('');
6054
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = React.useState('');
6055
+ const [internalExpandedGroups, setInternalExpandedGroups] = React.useState(new Set(defaultExpandedGroups || []));
6056
+ // Debounce search
6057
+ const handleSearchChange = React.useCallback((value) => {
6058
+ setSearchTerm(value);
6059
+ const timer = setTimeout(() => {
6060
+ setDebouncedSearchTerm(value);
6061
+ }, debounceMs);
6062
+ return () => clearTimeout(timer);
6063
+ }, [debounceMs]);
6064
+ // Filter items based on search
6065
+ const filteredItems = React.useMemo(() => {
6066
+ if (!debouncedSearchTerm)
6067
+ return items;
6068
+ const term = debouncedSearchTerm.toLowerCase();
6069
+ return items.filter(item => {
6070
+ if (filterFn) {
6071
+ return filterFn(item, debouncedSearchTerm);
6072
+ }
6073
+ return (item.label.toLowerCase().includes(term) ||
6074
+ item.description?.toLowerCase().includes(term) ||
6075
+ item.key.toLowerCase().includes(term));
6076
+ });
6077
+ }, [items, debouncedSearchTerm, filterFn]);
6078
+ // Group items
6079
+ const groupedItems = React.useMemo(() => {
6080
+ const groups = new Map();
6081
+ filteredItems.forEach(item => {
6082
+ const groupKey = item.group || null;
6083
+ if (!groups.has(groupKey)) {
6084
+ groups.set(groupKey, []);
6085
+ }
6086
+ groups.get(groupKey).push(item);
6087
+ });
6088
+ return groups;
6089
+ }, [filteredItems]);
6090
+ // Determine expanded groups
6091
+ const expandedGroups = controlledExpandedGroups
6092
+ ? new Set(controlledExpandedGroups)
6093
+ : internalExpandedGroups;
6094
+ const handleGroupToggle = (groupKey) => {
6095
+ const newExpanded = !expandedGroups.has(groupKey);
6096
+ if (!controlledExpandedGroups) {
6097
+ setInternalExpandedGroups(prev => {
6098
+ const next = new Set(prev);
6099
+ if (newExpanded) {
6100
+ next.add(groupKey);
6101
+ }
6102
+ else {
6103
+ next.delete(groupKey);
6104
+ }
6105
+ return next;
6106
+ });
6107
+ }
6108
+ onGroupToggle?.(groupKey, newExpanded);
6109
+ };
6110
+ // Selection handlers
6111
+ const handleItemToggle = (key) => {
6112
+ const newSelected = selectedKeys.includes(key)
6113
+ ? selectedKeys.filter(k => k !== key)
6114
+ : [...selectedKeys, key];
6115
+ onSelectionChange(newSelected);
6116
+ };
6117
+ const handleSelectAll = () => {
6118
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6119
+ const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
6120
+ if (allSelected) {
6121
+ // Deselect all filtered items
6122
+ const filteredKeys = new Set(enabledItems.map(item => item.key));
6123
+ onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
6124
+ }
6125
+ else {
6126
+ // Select all filtered items
6127
+ const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
6128
+ onSelectionChange(Array.from(newKeys));
6129
+ }
6130
+ };
6131
+ const sizeStyle = sizeClasses$5[size];
6132
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6133
+ const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
6134
+ const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
6135
+ // Check if we have groups
6136
+ const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
6137
+ 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: () => {
6138
+ setSearchTerm('');
6139
+ setDebouncedSearchTerm('');
6140
+ } }) })), (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]) => {
6141
+ if (groupKey === null) {
6142
+ // Ungrouped items
6143
+ return groupItems.map(item => (jsxRuntime.jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key)));
6144
+ }
6145
+ const isExpanded = expandedGroups.has(groupKey);
6146
+ const groupLabel = groupLabels[groupKey] || groupKey;
6147
+ const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
6148
+ return (jsxRuntime.jsxs("div", { children: [jsxRuntime.jsxs("button", { type: "button", onClick: () => handleGroupToggle(groupKey), className: `
6149
+ w-full flex items-center justify-between
6150
+ ${sizeStyle.groupHeader}
6151
+ font-medium text-ink-700 bg-paper-50
6152
+ hover:bg-paper-100 transition-colors
6153
+ border-b border-paper-200
6154
+ `, 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));
6155
+ })) : (
6156
+ // Flat list (no groups)
6157
+ filteredItems.map(item => (jsxRuntime.jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key))))] })] }));
6158
+ }
6159
+ // Helper component for rendering individual items
6160
+ function CheckboxListItemRow({ item, selected, onToggle, size, sizeStyle, indented = false, }) {
6161
+ return (jsxRuntime.jsx("div", { className: `
6162
+ ${sizeStyle.item}
6163
+ ${indented ? 'pl-8' : ''}
6164
+ hover:bg-paper-50 transition-colors
6165
+ border-b border-paper-100 last:border-b-0
6166
+ ${item.disabled ? 'opacity-50' : ''}
6167
+ `, 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 }))] })] }) }));
6168
+ }
6169
+
6170
+ const sizeClasses$4 = {
6171
+ sm: {
6172
+ container: 'text-sm',
6173
+ item: 'py-1.5 px-2',
6174
+ searchPadding: 'p-2',
6175
+ statusPadding: 'px-2 py-1.5',
6176
+ },
6177
+ md: {
6178
+ container: 'text-sm',
6179
+ item: 'py-2 px-3',
6180
+ searchPadding: 'p-3',
6181
+ statusPadding: 'px-3 py-2',
6182
+ },
6183
+ lg: {
6184
+ container: 'text-base',
6185
+ item: 'py-3 px-4',
6186
+ searchPadding: 'p-4',
6187
+ statusPadding: 'px-4 py-2.5',
6188
+ },
6189
+ };
6190
+ const variantClasses$2 = {
6191
+ default: 'bg-white',
6192
+ bordered: 'bg-white border border-paper-300 rounded-lg',
6193
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
6194
+ };
6195
+ /**
6196
+ * SearchableList - List component with integrated search/filter functionality
6197
+ *
6198
+ * @example Basic usage
6199
+ * ```tsx
6200
+ * <SearchableList
6201
+ * items={users.map(u => ({ key: u.id, data: u }))}
6202
+ * renderItem={(item) => <div>{item.data.name}</div>}
6203
+ * onSelect={(item) => setSelectedUser(item.data)}
6204
+ * searchable
6205
+ * searchPlaceholder="Search users..."
6206
+ * />
6207
+ * ```
6208
+ *
6209
+ * @example With custom filter and loading
6210
+ * ```tsx
6211
+ * <SearchableList
6212
+ * items={products}
6213
+ * renderItem={(item, index, isSelected) => (
6214
+ * <div className={isSelected ? 'bg-accent-50' : ''}>
6215
+ * {item.data.name} - ${item.data.price}
6216
+ * </div>
6217
+ * )}
6218
+ * filterFn={(item, term) =>
6219
+ * item.data.name.toLowerCase().includes(term.toLowerCase())
6220
+ * }
6221
+ * loading={isLoading}
6222
+ * loadingMessage="Fetching products..."
6223
+ * maxHeight="400px"
6224
+ * />
6225
+ * ```
6226
+ */
6227
+ 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, }) {
6228
+ const [internalSearchValue, setInternalSearchValue] = React.useState('');
6229
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = React.useState('');
6230
+ const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
6231
+ const listRef = React.useRef(null);
6232
+ const itemRefs = React.useRef(new Map());
6233
+ const searchValue = controlledSearchValue !== undefined ? controlledSearchValue : internalSearchValue;
6234
+ // Debounce search
6235
+ React.useEffect(() => {
6236
+ const timer = setTimeout(() => {
6237
+ setDebouncedSearchTerm(searchValue);
6238
+ }, debounceMs);
6239
+ return () => clearTimeout(timer);
6240
+ }, [searchValue, debounceMs]);
6241
+ const handleSearchChange = React.useCallback((value) => {
6242
+ if (controlledSearchValue === undefined) {
6243
+ setInternalSearchValue(value);
6244
+ }
6245
+ onSearchChange?.(value);
6246
+ setHighlightedIndex(-1);
6247
+ }, [controlledSearchValue, onSearchChange]);
6248
+ // Filter items based on search
6249
+ const filteredItems = React.useMemo(() => {
6250
+ if (!debouncedSearchTerm)
6251
+ return items;
6252
+ return items.filter(item => {
6253
+ if (filterFn) {
6254
+ return filterFn(item, debouncedSearchTerm);
6255
+ }
6256
+ // Default filter: check if key includes search term
6257
+ return item.key.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
6258
+ });
6259
+ }, [items, debouncedSearchTerm, filterFn]);
6260
+ // Keyboard navigation
6261
+ const handleKeyDown = React.useCallback((e) => {
6262
+ if (!enableKeyboardNavigation || filteredItems.length === 0)
6263
+ return;
6264
+ switch (e.key) {
6265
+ case 'ArrowDown':
6266
+ e.preventDefault();
6267
+ setHighlightedIndex(prev => prev < filteredItems.length - 1 ? prev + 1 : 0);
6268
+ break;
6269
+ case 'ArrowUp':
6270
+ e.preventDefault();
6271
+ setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredItems.length - 1);
6272
+ break;
6273
+ case 'Enter':
6274
+ e.preventDefault();
6275
+ if (highlightedIndex >= 0 && highlightedIndex < filteredItems.length) {
6276
+ onSelect?.(filteredItems[highlightedIndex]);
6277
+ }
6278
+ break;
6279
+ case 'Escape':
6280
+ setHighlightedIndex(-1);
6281
+ break;
6282
+ }
6283
+ }, [enableKeyboardNavigation, filteredItems, highlightedIndex, onSelect]);
6284
+ // Scroll highlighted item into view
6285
+ React.useEffect(() => {
6286
+ if (highlightedIndex >= 0) {
6287
+ const itemEl = itemRefs.current.get(highlightedIndex);
6288
+ if (itemEl) {
6289
+ itemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
6290
+ }
6291
+ }
6292
+ }, [highlightedIndex]);
6293
+ const sizeStyle = sizeClasses$4[size];
6294
+ const resultCountText = formatResultCount
6295
+ ? formatResultCount(filteredItems.length, items.length)
6296
+ : `${filteredItems.length} of ${items.length}`;
6297
+ 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) => {
6298
+ const isSelected = selectedKey === item.key;
6299
+ const isHighlighted = highlightedIndex === index;
6300
+ return (jsxRuntime.jsx("div", { id: `item-${index}`, ref: (el) => {
6301
+ if (el) {
6302
+ itemRefs.current.set(index, el);
6303
+ }
6304
+ else {
6305
+ itemRefs.current.delete(index);
6306
+ }
6307
+ }, role: "option", "aria-selected": isSelected, onClick: () => onSelect?.(item), className: `
6308
+ ${sizeStyle.item}
6309
+ cursor-pointer transition-colors
6310
+ ${isSelected ? 'bg-accent-50' : ''}
6311
+ ${isHighlighted ? 'bg-paper-100' : ''}
6312
+ ${!isSelected && !isHighlighted ? 'hover:bg-paper-50' : ''}
6313
+ border-b border-paper-100 last:border-b-0
6314
+ `, children: renderItem(item, index, isSelected, isHighlighted) }, item.key));
6315
+ })] })] }));
6316
+ }
6317
+
6318
+ const sizeClasses$3 = {
5205
6319
  sm: {
5206
6320
  input: 'h-8 px-2 text-sm',
5207
6321
  button: 'h-8 w-8',
@@ -5225,7 +6339,7 @@ const NumberInput = React.forwardRef((props, ref) => {
5225
6339
  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
6340
  const [internalValue, setInternalValue] = React.useState(String(value));
5227
6341
  const [isFocused, setIsFocused] = React.useState(false);
5228
- const sizeStyle = sizeClasses$1[size];
6342
+ const sizeStyle = sizeClasses$3[size];
5229
6343
  // Generate unique IDs for ARIA
5230
6344
  const uniqueId = React.useId();
5231
6345
  const inputId = id || uniqueId;
@@ -5655,105 +6769,6 @@ class ErrorBoundary extends React.Component {
5655
6769
  }
5656
6770
  }
5657
6771
 
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
6772
  function Collapsible({ trigger, children, defaultOpen = false, open: controlledOpen, onOpenChange, disabled = false, showIcon = true, className = '', triggerClassName = '', contentClassName = '', }) {
5758
6773
  const isControlled = controlledOpen !== undefined;
5759
6774
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
@@ -5803,6 +6818,231 @@ function Collapsible({ trigger, children, defaultOpen = false, open: controlledO
5803
6818
  ` }))] }), 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
6819
  }
5805
6820
 
6821
+ const sizeClasses$2 = {
6822
+ sm: {
6823
+ header: 'h-10 px-3',
6824
+ text: 'text-sm',
6825
+ icon: 'h-4 w-4',
6826
+ },
6827
+ md: {
6828
+ header: 'h-12 px-4',
6829
+ text: 'text-sm',
6830
+ icon: 'h-5 w-5',
6831
+ },
6832
+ lg: {
6833
+ header: 'h-14 px-5',
6834
+ text: 'text-base',
6835
+ icon: 'h-5 w-5',
6836
+ },
6837
+ };
6838
+ const variantClasses$1 = {
6839
+ default: {
6840
+ container: 'bg-white border-ink-200',
6841
+ header: 'bg-paper-50',
6842
+ },
6843
+ elevated: {
6844
+ container: 'bg-white shadow-lg border-ink-200',
6845
+ header: 'bg-white',
6846
+ },
6847
+ bordered: {
6848
+ container: 'bg-white border-2 border-ink-300',
6849
+ header: 'bg-paper-100',
6850
+ },
6851
+ };
6852
+ /**
6853
+ * ExpandablePanel - A panel that sticks to the bottom (or top) and can expand/collapse
6854
+ *
6855
+ * For bottom position: expands UPWARD (content appears above header)
6856
+ * For top position: expands DOWNWARD (content appears below header)
6857
+ *
6858
+ * Two modes of operation:
6859
+ * - `viewport`: Fixed to the viewport (for standalone pages, covers StatusBar)
6860
+ * - `container`: Sticky within its parent container (for use inside Page/AppLayout, respects StatusBar)
6861
+ *
6862
+ * @example Basic usage (viewport mode - full page)
6863
+ * ```tsx
6864
+ * <ExpandablePanel
6865
+ * mode="viewport"
6866
+ * collapsedContent={<Text>3 items selected</Text>}
6867
+ * expandedHeight="300px"
6868
+ * >
6869
+ * {content}
6870
+ * </ExpandablePanel>
6871
+ * ```
6872
+ *
6873
+ * @example Inside Page/AppLayout (container mode - respects StatusBar)
6874
+ * ```tsx
6875
+ * <Page>
6876
+ * <ExpandablePanelContainer>
6877
+ * <div className="flex-1 overflow-auto">
6878
+ * {pageContent}
6879
+ * </div>
6880
+ * <ExpandablePanel
6881
+ * mode="container"
6882
+ * collapsedContent={<Text>3 items selected</Text>}
6883
+ * expandedHeight="300px"
6884
+ * >
6885
+ * {selectedItemsContent}
6886
+ * </ExpandablePanel>
6887
+ * </ExpandablePanelContainer>
6888
+ * </Page>
6889
+ * ```
6890
+ *
6891
+ * @example With maxWidth to match page content
6892
+ * ```tsx
6893
+ * <ExpandablePanel
6894
+ * mode="container"
6895
+ * maxWidth="1400px"
6896
+ * collapsedContent={<Text>Generated SQL</Text>}
6897
+ * expandedHeight="300px"
6898
+ * >
6899
+ * {sqlContent}
6900
+ * </ExpandablePanel>
6901
+ * ```
6902
+ */
6903
+ 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, }) {
6904
+ const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded);
6905
+ // Determine if controlled or uncontrolled
6906
+ const isControlled = controlledExpanded !== undefined;
6907
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
6908
+ const setExpanded = (value) => {
6909
+ if (!isControlled) {
6910
+ setInternalExpanded(value);
6911
+ }
6912
+ onExpandedChange?.(value);
6913
+ };
6914
+ const toggleExpanded = () => {
6915
+ setExpanded(!expanded);
6916
+ };
6917
+ // Close on Escape
6918
+ React.useEffect(() => {
6919
+ if (!closeOnEscape || !expanded)
6920
+ return;
6921
+ const handleEscape = (e) => {
6922
+ if (e.key === 'Escape') {
6923
+ setExpanded(false);
6924
+ }
6925
+ };
6926
+ document.addEventListener('keydown', handleEscape);
6927
+ return () => document.removeEventListener('keydown', handleEscape);
6928
+ }, [closeOnEscape, expanded]);
6929
+ const sizeStyle = sizeClasses$2[size];
6930
+ const variantStyle = variantClasses$1[variant];
6931
+ const heightValue = typeof expandedHeight === 'number' ? `${expandedHeight}px` : expandedHeight;
6932
+ const maxWidthValue = maxWidth
6933
+ ? (typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)
6934
+ : undefined;
6935
+ // Position classes differ based on mode
6936
+ const getPositionClasses = () => {
6937
+ if (mode === 'viewport') {
6938
+ // Fixed to viewport
6939
+ return position === 'bottom'
6940
+ ? 'fixed bottom-0 left-0 right-0'
6941
+ : 'fixed top-0 left-0 right-0';
6942
+ }
6943
+ else {
6944
+ // Absolute positioning within container - snaps to bottom
6945
+ return position === 'bottom'
6946
+ ? 'absolute bottom-0 left-0 right-0'
6947
+ : 'absolute top-0 left-0 right-0';
6948
+ }
6949
+ };
6950
+ // For bottom panel, we want chevron up to expand (reveal content above)
6951
+ // For top panel, we want chevron down to expand (reveal content below)
6952
+ const ChevronIcon = position === 'bottom'
6953
+ ? (expanded ? lucideReact.ChevronDown : lucideReact.ChevronUp)
6954
+ : (expanded ? lucideReact.ChevronUp : lucideReact.ChevronDown);
6955
+ // Header component
6956
+ const header = (jsxRuntime.jsxs("div", { className: `
6957
+ flex items-center justify-between
6958
+ ${sizeStyle.header}
6959
+ ${variantStyle.header}
6960
+ border-ink-200
6961
+ flex-shrink-0
6962
+ ${headerClassName}
6963
+ `, 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: `
6964
+ flex items-center justify-center
6965
+ p-1.5 rounded-md
6966
+ text-ink-500 hover:text-ink-700
6967
+ hover:bg-ink-100
6968
+ transition-colors
6969
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1
6970
+ `, "aria-expanded": expanded, "aria-label": expanded ? 'Collapse panel' : 'Expand panel', children: toggleContent || jsxRuntime.jsx(ChevronIcon, { className: sizeStyle.icon }) }))] })] }));
6971
+ // Content component
6972
+ const content = (jsxRuntime.jsx("div", { className: `
6973
+ overflow-hidden
6974
+ transition-all duration-300 ease-in-out
6975
+ `, style: {
6976
+ maxHeight: expanded ? heightValue : '0px',
6977
+ opacity: expanded ? 1 : 0,
6978
+ }, children: jsxRuntime.jsx("div", { className: `
6979
+ overflow-y-auto p-4
6980
+ ${contentClassName}
6981
+ `, style: { maxHeight: heightValue }, children: children }) }));
6982
+ // Build container styles
6983
+ const containerStyle = {
6984
+ ...(mode === 'viewport' ? { zIndex } : {}),
6985
+ ...(maxWidthValue ? {
6986
+ maxWidth: maxWidthValue,
6987
+ marginLeft: 'auto',
6988
+ marginRight: 'auto'
6989
+ } : {}),
6990
+ };
6991
+ return (jsxRuntime.jsx("div", { className: `
6992
+ ${getPositionClasses()}
6993
+ ${variantStyle.container}
6994
+ border-t rounded-t-lg
6995
+ transition-all duration-300 ease-in-out
6996
+ flex flex-col
6997
+ ${className}
6998
+ `, style: containerStyle, children: position === 'bottom' ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [content, header] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [header, content] })) }));
6999
+ }
7000
+ /**
7001
+ * ExpandablePanelSpacer - Adds spacing to prevent content from being hidden behind the panel
7002
+ * Only needed in viewport mode. In container mode, the panel is part of the flex layout.
7003
+ *
7004
+ * @example
7005
+ * ```tsx
7006
+ * <div>
7007
+ * <MainContent />
7008
+ * <ExpandablePanelSpacer size="md" />
7009
+ * </div>
7010
+ * <ExpandablePanel mode="viewport" position="bottom" size="md" {...props} />
7011
+ * ```
7012
+ */
7013
+ function ExpandablePanelSpacer({ size = 'md' }) {
7014
+ const heights = {
7015
+ sm: 'h-10',
7016
+ md: 'h-12',
7017
+ lg: 'h-14',
7018
+ };
7019
+ return jsxRuntime.jsx("div", { className: heights[size] });
7020
+ }
7021
+ /**
7022
+ * ExpandablePanelContainer - Wrapper that sets up proper layout for container mode
7023
+ * Use this to wrap your page content when using ExpandablePanel with mode="container"
7024
+ *
7025
+ * This creates a relative container with full height so the panel can position absolutely
7026
+ * at the bottom while the content scrolls above it.
7027
+ *
7028
+ * @example
7029
+ * ```tsx
7030
+ * <Page>
7031
+ * <ExpandablePanelContainer>
7032
+ * <div className="flex-1 overflow-auto p-4">
7033
+ * {pageContent}
7034
+ * </div>
7035
+ * <ExpandablePanel mode="container" {...props}>
7036
+ * {panelContent}
7037
+ * </ExpandablePanel>
7038
+ * </ExpandablePanelContainer>
7039
+ * </Page>
7040
+ * ```
7041
+ */
7042
+ function ExpandablePanelContainer({ children, className = '', }) {
7043
+ return (jsxRuntime.jsx("div", { className: `relative h-full overflow-hidden ${className}`, children: children }));
7044
+ }
7045
+
5806
7046
  // Tailwind breakpoint mappings
5807
7047
  // sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
5808
7048
  /**
@@ -5971,26 +7211,6 @@ function Hide({ children, above, below, only, className = '' }) {
5971
7211
  }
5972
7212
  return (jsxRuntime.jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
5973
7213
  }
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
7214
 
5995
7215
  function Breadcrumbs({ items, showHome = true }) {
5996
7216
  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) => {
@@ -6706,9 +7926,9 @@ function SidebarNavItem({ item, onNavigate, level = 0, currentPath }) {
6706
7926
  // Auto-detect if this item or any child is active based on currentPath
6707
7927
  const isItemActive = currentPath && item.href ? currentPath === item.href : item.active;
6708
7928
  const isChildActive = hasChildren && currentPath
6709
- ? item.children?.some(child => currentPath === child.href || currentPath?.startsWith(child.href || ''))
7929
+ ? item.children?.some(child => child.href && (currentPath === child.href || currentPath.startsWith(child.href)))
6710
7930
  : false;
6711
- const shouldExpandByDefault = isChildActive || (hasChildren && currentPath?.startsWith(item.href || ''));
7931
+ const shouldExpandByDefault = isChildActive || (hasChildren && item.href && currentPath?.startsWith(item.href));
6712
7932
  const [isExpanded, setIsExpanded] = React.useState(shouldExpandByDefault);
6713
7933
  const handleClick = () => {
6714
7934
  if (hasChildren) {
@@ -6748,15 +7968,692 @@ function SidebarGroup({ title, items, onNavigate, defaultExpanded = true, curren
6748
7968
  }
6749
7969
  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
7970
  }
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) => {
7971
+ /**
7972
+ * Sidebar - Navigation sidebar with mobile drawer support
7973
+ *
7974
+ * On desktop: Renders as a fixed-width sidebar
7975
+ * On mobile: Renders as a drawer overlay when mobileOpen is true
7976
+ *
7977
+ * @example Desktop usage (no mobile props)
7978
+ * ```tsx
7979
+ * <Sidebar
7980
+ * items={navItems}
7981
+ * header={<Logo />}
7982
+ * footer={<UserProfile />}
7983
+ * currentPath={location.pathname}
7984
+ * onNavigate={(href) => navigate(href)}
7985
+ * />
7986
+ * ```
7987
+ *
7988
+ * @example With mobile drawer support
7989
+ * ```tsx
7990
+ * const [mobileOpen, setMobileOpen] = useState(false);
7991
+ *
7992
+ * <Sidebar
7993
+ * items={navItems}
7994
+ * header={<Logo />}
7995
+ * mobileOpen={mobileOpen}
7996
+ * onMobileClose={() => setMobileOpen(false)}
7997
+ * onNavigate={(href) => {
7998
+ * navigate(href);
7999
+ * setMobileOpen(false); // Close drawer on navigation
8000
+ * }}
8001
+ * />
8002
+ * ```
8003
+ */
8004
+ function Sidebar({ items, onNavigate, className = '', header, footer, currentPath, mobileOpen, onMobileClose, width = 'w-64', }) {
8005
+ const sidebarRef = React.useRef(null);
8006
+ const [isAnimating, setIsAnimating] = React.useState(false);
8007
+ const [shouldRender, setShouldRender] = React.useState(mobileOpen);
8008
+ // Handle animation states for mobile drawer
8009
+ React.useEffect(() => {
8010
+ if (mobileOpen) {
8011
+ setShouldRender(true);
8012
+ // Small delay to trigger animation
8013
+ requestAnimationFrame(() => {
8014
+ setIsAnimating(true);
8015
+ });
8016
+ return; // No cleanup needed when opening
8017
+ }
8018
+ else {
8019
+ setIsAnimating(false);
8020
+ // Wait for animation to complete before unmounting
8021
+ const timer = setTimeout(() => {
8022
+ setShouldRender(false);
8023
+ }, 300);
8024
+ return () => clearTimeout(timer);
8025
+ }
8026
+ }, [mobileOpen]);
8027
+ // Handle escape key for mobile drawer
8028
+ React.useEffect(() => {
8029
+ if (!mobileOpen)
8030
+ return;
8031
+ const handleEscape = (e) => {
8032
+ if (e.key === 'Escape') {
8033
+ onMobileClose?.();
8034
+ }
8035
+ };
8036
+ document.addEventListener('keydown', handleEscape);
8037
+ return () => document.removeEventListener('keydown', handleEscape);
8038
+ }, [mobileOpen, onMobileClose]);
8039
+ // Lock body scroll when mobile drawer is open
8040
+ React.useEffect(() => {
8041
+ if (mobileOpen) {
8042
+ document.body.style.overflow = 'hidden';
8043
+ }
8044
+ else {
8045
+ document.body.style.overflow = '';
8046
+ }
8047
+ return () => {
8048
+ document.body.style.overflow = '';
8049
+ };
8050
+ }, [mobileOpen]);
8051
+ // Handle navigation with auto-close on mobile
8052
+ const handleNavigate = (href, external) => {
8053
+ onNavigate?.(href, external);
8054
+ // Auto-close mobile drawer on navigation
8055
+ if (mobileOpen) {
8056
+ onMobileClose?.();
8057
+ }
8058
+ };
8059
+ // Sidebar content (shared between desktop and mobile)
8060
+ 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
8061
  // Render separator
6754
8062
  if (item.separator) {
6755
8063
  return jsxRuntime.jsx("div", { className: "my-4 border-t border-paper-300" }, item.id);
6756
8064
  }
6757
8065
  // Render nav item
6758
- return (jsxRuntime.jsx(SidebarNavItem, { item: item, onNavigate: onNavigate, currentPath: currentPath }, item.id));
8066
+ return (jsxRuntime.jsx(SidebarNavItem, { item: item, onNavigate: handleNavigate, currentPath: currentPath }, item.id));
6759
8067
  }) }), footer && (jsxRuntime.jsx("div", { className: "border-t border-paper-300 pl-2 pr-6 py-4 overflow-visible", children: footer }))] }));
8068
+ // If mobileOpen is not defined, render as regular sidebar (desktop mode)
8069
+ if (mobileOpen === undefined) {
8070
+ return sidebarContent;
8071
+ }
8072
+ // Mobile drawer mode
8073
+ if (!shouldRender) {
8074
+ return null;
8075
+ }
8076
+ 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: `
8077
+ absolute inset-0 bg-ink-900/50 backdrop-blur-sm
8078
+ transition-opacity duration-300
8079
+ ${isAnimating ? 'opacity-100' : 'opacity-0'}
8080
+ `, onClick: onMobileClose, "aria-hidden": "true" }), jsxRuntime.jsx("div", { className: `
8081
+ absolute inset-y-0 left-0 flex max-w-full
8082
+ transition-transform duration-300 ease-out
8083
+ ${isAnimating ? 'translate-x-0' : '-translate-x-full'}
8084
+ `, children: sidebarContent })] }), document.body);
8085
+ }
8086
+
8087
+ /**
8088
+ * BottomNavigation - Mobile-style bottom tab bar
8089
+ *
8090
+ * iOS/Android-style fixed bottom navigation with icons, labels, and badges.
8091
+ * Handles safe area insets for notched devices automatically.
8092
+ *
8093
+ * Best practices:
8094
+ * - Use 3-5 items maximum
8095
+ * - Keep labels short (1-2 words)
8096
+ * - Use consistent icon style
8097
+ *
8098
+ * @example Basic usage
8099
+ * ```tsx
8100
+ * import { BottomNavigation } from 'notebook-ui';
8101
+ * import { Home, Search, Bell, User } from 'lucide-react';
8102
+ *
8103
+ * const navItems = [
8104
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8105
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8106
+ * { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3 },
8107
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' },
8108
+ * ];
8109
+ *
8110
+ * <BottomNavigation
8111
+ * items={navItems}
8112
+ * activeId="home"
8113
+ * onNavigate={(id, href) => navigate(href)}
8114
+ * />
8115
+ * ```
8116
+ *
8117
+ * @example With onClick handlers
8118
+ * ```tsx
8119
+ * const navItems = [
8120
+ * { id: 'home', label: 'Home', icon: <Home />, onClick: () => setTab('home') },
8121
+ * { id: 'add', label: 'Add', icon: <Plus />, onClick: openAddModal },
8122
+ * ];
8123
+ *
8124
+ * <BottomNavigation items={navItems} activeId={currentTab} />
8125
+ * ```
8126
+ */
8127
+ function BottomNavigation({ items, activeId, onNavigate, showLabels = true, className = '', safeArea = true, }) {
8128
+ const handleItemClick = (item) => {
8129
+ if (item.disabled)
8130
+ return;
8131
+ if (item.onClick) {
8132
+ item.onClick();
8133
+ }
8134
+ if (onNavigate) {
8135
+ onNavigate(item.id, item.href);
8136
+ }
8137
+ };
8138
+ return (jsxRuntime.jsx("nav", { className: `
8139
+ fixed bottom-0 left-0 right-0 z-40
8140
+ bg-white border-t border-paper-200 shadow-lg
8141
+ ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}
8142
+ ${className}
8143
+ `, 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) => {
8144
+ const isActive = item.id === activeId;
8145
+ return (jsxRuntime.jsxs("button", { onClick: () => handleItemClick(item), disabled: item.disabled, className: `
8146
+ relative flex flex-col items-center justify-center
8147
+ flex-1 h-full min-w-touch-sm
8148
+ transition-colors duration-200
8149
+ ${item.disabled
8150
+ ? 'opacity-40 cursor-not-allowed'
8151
+ : 'active:bg-paper-100'}
8152
+ ${isActive
8153
+ ? 'text-accent-600'
8154
+ : 'text-ink-500 hover:text-ink-700'}
8155
+ `, "aria-current": isActive ? 'page' : undefined, "aria-label": item.label, children: [jsxRuntime.jsxs("div", { className: "relative", children: [jsxRuntime.jsx("div", { className: `
8156
+ w-6 h-6 flex items-center justify-center
8157
+ transition-transform duration-200
8158
+ ${isActive ? 'scale-110' : 'scale-100'}
8159
+ `, children: React.isValidElement(item.icon)
8160
+ ? React.cloneElement(item.icon, {
8161
+ className: 'w-6 h-6',
8162
+ })
8163
+ : item.icon }), item.badge !== undefined && item.badge > 0 && (jsxRuntime.jsx("span", { className: `
8164
+ absolute -top-1 -right-2.5
8165
+ min-w-[18px] h-[18px] px-1
8166
+ flex items-center justify-center
8167
+ text-[10px] font-bold text-white
8168
+ bg-error-500 rounded-full
8169
+ ${item.badge > 99 ? 'text-[8px]' : ''}
8170
+ `, children: item.badge > 99 ? '99+' : item.badge }))] }), showLabels && (jsxRuntime.jsx("span", { className: `
8171
+ mt-1 text-[10px] font-medium leading-none
8172
+ transition-opacity duration-200
8173
+ truncate max-w-full px-1
8174
+ ${isActive ? 'opacity-100' : 'opacity-70'}
8175
+ `, 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));
8176
+ }) }) }));
8177
+ }
8178
+ /**
8179
+ * BottomNavigationSpacer - Spacer to prevent content from being hidden behind BottomNavigation
8180
+ *
8181
+ * Place this at the bottom of your scrollable content when using BottomNavigation.
8182
+ *
8183
+ * @example
8184
+ * ```tsx
8185
+ * <div className="flex flex-col h-screen">
8186
+ * <main className="flex-1 overflow-auto">
8187
+ * {/* Your content *\/}
8188
+ * <BottomNavigationSpacer />
8189
+ * </main>
8190
+ * <BottomNavigation items={navItems} />
8191
+ * </div>
8192
+ * ```
8193
+ */
8194
+ function BottomNavigationSpacer({ safeArea = true }) {
8195
+ return (jsxRuntime.jsx("div", { className: `h-14 ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}`, "aria-hidden": "true" }));
8196
+ }
8197
+
8198
+ /**
8199
+ * MobileHeader - Mobile app header with navigation controls
8200
+ *
8201
+ * A flexible mobile header component with support for:
8202
+ * - Hamburger menu button (default)
8203
+ * - Back navigation arrow
8204
+ * - Close button (X)
8205
+ * - Custom left/right actions
8206
+ * - Sticky positioning
8207
+ * - Blur/transparent variants
8208
+ *
8209
+ * @example Basic with menu button
8210
+ * ```tsx
8211
+ * <MobileHeader
8212
+ * title="Dashboard"
8213
+ * onMenuClick={() => setDrawerOpen(true)}
8214
+ * />
8215
+ * ```
8216
+ *
8217
+ * @example With back button
8218
+ * ```tsx
8219
+ * <MobileHeader
8220
+ * title="User Details"
8221
+ * subtitle="Profile"
8222
+ * onBackClick={() => navigate(-1)}
8223
+ * />
8224
+ * ```
8225
+ *
8226
+ * @example With right action
8227
+ * ```tsx
8228
+ * <MobileHeader
8229
+ * title="Settings"
8230
+ * onMenuClick={openMenu}
8231
+ * rightAction={
8232
+ * <Button variant="ghost" iconOnly onClick={save}>
8233
+ * <Check className="w-5 h-5" />
8234
+ * </Button>
8235
+ * }
8236
+ * />
8237
+ * ```
8238
+ *
8239
+ * @example Transparent with blur
8240
+ * ```tsx
8241
+ * <MobileHeader
8242
+ * title="Photo Gallery"
8243
+ * variant="blur"
8244
+ * onBackClick={goBack}
8245
+ * />
8246
+ * ```
8247
+ */
8248
+ function MobileHeader({ title, subtitle, onMenuClick, onBackClick, onCloseClick, rightAction, leftAction, sticky = true, bordered = true, variant = 'solid', className = '', safeArea = true, }) {
8249
+ // Determine which left button to show
8250
+ const renderLeftButton = () => {
8251
+ // Custom left action takes priority
8252
+ if (leftAction) {
8253
+ return leftAction;
8254
+ }
8255
+ // Close button
8256
+ if (onCloseClick) {
8257
+ 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" }) }));
8258
+ }
8259
+ // Back button
8260
+ if (onBackClick) {
8261
+ 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" }) }));
8262
+ }
8263
+ // Menu button (default)
8264
+ if (onMenuClick) {
8265
+ 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" }) }));
8266
+ }
8267
+ // No left button
8268
+ return jsxRuntime.jsx("div", { className: "w-10 h-10" });
8269
+ };
8270
+ // Background variant styles
8271
+ const variantStyles = {
8272
+ solid: 'bg-white',
8273
+ transparent: 'bg-transparent',
8274
+ blur: 'bg-white/80 backdrop-blur-md',
8275
+ };
8276
+ return (jsxRuntime.jsx("header", { className: `
8277
+ ${sticky ? 'sticky top-0 z-30' : ''}
8278
+ ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}
8279
+ ${variantStyles[variant]}
8280
+ ${bordered ? 'border-b border-paper-200' : ''}
8281
+ ${className}
8282
+ `, 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 })] }) }));
8283
+ }
8284
+ /**
8285
+ * MobileHeaderSpacer - Spacer to prevent content from being hidden behind sticky MobileHeader
8286
+ *
8287
+ * Place this at the top of your content when NOT using sticky header
8288
+ * to maintain consistent spacing.
8289
+ *
8290
+ * @example
8291
+ * ```tsx
8292
+ * <MobileHeader title="Page" sticky={false} />
8293
+ * <MobileHeaderSpacer />
8294
+ * <main>Content here</main>
8295
+ * ```
8296
+ */
8297
+ function MobileHeaderSpacer({ safeArea = true }) {
8298
+ return (jsxRuntime.jsx("div", { className: `h-14 ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}`, "aria-hidden": "true" }));
8299
+ }
8300
+
8301
+ const positionClasses = {
8302
+ 'bottom-right': 'right-4 bottom-4',
8303
+ 'bottom-left': 'left-4 bottom-4',
8304
+ 'bottom-center': 'left-1/2 -translate-x-1/2 bottom-4',
8305
+ };
8306
+ const variantClasses = {
8307
+ primary: 'bg-accent-600 hover:bg-accent-700 text-white shadow-lg',
8308
+ secondary: 'bg-white hover:bg-paper-50 text-ink-700 shadow-lg border border-paper-200',
8309
+ accent: 'bg-accent-500 hover:bg-accent-600 text-white shadow-lg',
8310
+ };
8311
+ const sizeClasses$1 = {
8312
+ md: 'w-14 h-14',
8313
+ lg: 'w-16 h-16',
8314
+ };
8315
+ const iconSizeClasses = {
8316
+ md: 'h-6 w-6',
8317
+ lg: 'h-7 w-7',
8318
+ };
8319
+ /**
8320
+ * FloatingActionButton - Material Design style FAB for mobile
8321
+ *
8322
+ * A prominent button for the primary action on a screen.
8323
+ * Supports single action or expandable menu with multiple actions.
8324
+ *
8325
+ * @example Simple FAB
8326
+ * ```tsx
8327
+ * <FloatingActionButton
8328
+ * onClick={() => openCreateModal()}
8329
+ * label="Create new item"
8330
+ * />
8331
+ * ```
8332
+ *
8333
+ * @example FAB with action menu
8334
+ * ```tsx
8335
+ * <FloatingActionButton
8336
+ * actions={[
8337
+ * { id: 'photo', icon: <Camera />, label: 'Take Photo', onClick: takePhoto },
8338
+ * { id: 'upload', icon: <Upload />, label: 'Upload File', onClick: uploadFile },
8339
+ * { id: 'note', icon: <FileText />, label: 'Create Note', onClick: createNote },
8340
+ * ]}
8341
+ * />
8342
+ * ```
8343
+ *
8344
+ * @example Extended FAB
8345
+ * ```tsx
8346
+ * <FloatingActionButton
8347
+ * extended
8348
+ * extendedLabel="New Task"
8349
+ * icon={<Plus />}
8350
+ * onClick={createTask}
8351
+ * />
8352
+ * ```
8353
+ */
8354
+ function FloatingActionButton({ onClick, icon, actions, position = 'bottom-right', variant = 'primary', size = 'md', label = 'Action button', extended = false, extendedLabel, hidden = false, offset, className = '', }) {
8355
+ const [isMenuOpen, setIsMenuOpen] = React.useState(false);
8356
+ const fabRef = React.useRef(null);
8357
+ const hasMenu = actions && actions.length > 0;
8358
+ // Close menu on escape
8359
+ React.useEffect(() => {
8360
+ if (!isMenuOpen)
8361
+ return;
8362
+ const handleEscape = (e) => {
8363
+ if (e.key === 'Escape') {
8364
+ setIsMenuOpen(false);
8365
+ }
8366
+ };
8367
+ document.addEventListener('keydown', handleEscape);
8368
+ return () => document.removeEventListener('keydown', handleEscape);
8369
+ }, [isMenuOpen]);
8370
+ // Close menu on click outside
8371
+ React.useEffect(() => {
8372
+ if (!isMenuOpen)
8373
+ return;
8374
+ const handleClickOutside = (e) => {
8375
+ if (fabRef.current && !fabRef.current.contains(e.target)) {
8376
+ setIsMenuOpen(false);
8377
+ }
8378
+ };
8379
+ document.addEventListener('mousedown', handleClickOutside);
8380
+ return () => document.removeEventListener('mousedown', handleClickOutside);
8381
+ }, [isMenuOpen]);
8382
+ const handleClick = () => {
8383
+ if (hasMenu) {
8384
+ setIsMenuOpen(!isMenuOpen);
8385
+ }
8386
+ else if (onClick) {
8387
+ onClick();
8388
+ }
8389
+ };
8390
+ const handleActionClick = (action) => {
8391
+ if (!action.disabled) {
8392
+ action.onClick();
8393
+ setIsMenuOpen(false);
8394
+ }
8395
+ };
8396
+ // Custom offset styles
8397
+ const offsetStyle = offset ? {
8398
+ ...(offset.x !== undefined && position.includes('right') ? { right: `${offset.x}px` } : {}),
8399
+ ...(offset.x !== undefined && position.includes('left') ? { left: `${offset.x}px` } : {}),
8400
+ ...(offset.y !== undefined ? { bottom: `${offset.y}px` } : {}),
8401
+ } : {};
8402
+ const fabContent = (jsxRuntime.jsxs("div", { className: `
8403
+ fixed z-40 transition-all duration-300
8404
+ ${positionClasses[position]}
8405
+ ${hidden ? 'translate-y-20 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
8406
+ ${className}
8407
+ `, style: {
8408
+ ...offsetStyle,
8409
+ paddingBottom: 'env(safe-area-inset-bottom)',
8410
+ }, 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: `
8411
+ w-12 h-12 rounded-full flex items-center justify-center
8412
+ transition-all duration-200
8413
+ ${action.disabled
8414
+ ? 'bg-paper-200 text-ink-400 cursor-not-allowed'
8415
+ : 'bg-white text-ink-700 shadow-lg hover:bg-paper-50 active:scale-95'}
8416
+ `, "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: `
8417
+ ${extended ? 'px-6 rounded-full' : 'rounded-full'}
8418
+ ${extended ? 'h-14' : sizeClasses$1[size]}
8419
+ ${variantClasses[variant]}
8420
+ flex items-center justify-center gap-2
8421
+ transition-all duration-200
8422
+ active:scale-95
8423
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
8424
+ `, "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 }))] })] }));
8425
+ // Render via portal to ensure proper stacking
8426
+ return reactDom.createPortal(fabContent, document.body);
8427
+ }
8428
+ /**
8429
+ * Hook for scroll-based FAB visibility
8430
+ *
8431
+ * @example
8432
+ * ```tsx
8433
+ * const { hidden, scrollDirection } = useFABScroll();
8434
+ * <FloatingActionButton hidden={hidden} />
8435
+ * ```
8436
+ */
8437
+ function useFABScroll(threshold = 10) {
8438
+ const [hidden, setHidden] = React.useState(false);
8439
+ const [scrollDirection, setScrollDirection] = React.useState(null);
8440
+ const lastScrollY = React.useRef(0);
8441
+ React.useEffect(() => {
8442
+ const handleScroll = () => {
8443
+ const currentScrollY = window.scrollY;
8444
+ const diff = currentScrollY - lastScrollY.current;
8445
+ if (Math.abs(diff) > threshold) {
8446
+ if (diff > 0) {
8447
+ setHidden(true);
8448
+ setScrollDirection('down');
8449
+ }
8450
+ else {
8451
+ setHidden(false);
8452
+ setScrollDirection('up');
8453
+ }
8454
+ lastScrollY.current = currentScrollY;
8455
+ }
8456
+ };
8457
+ window.addEventListener('scroll', handleScroll, { passive: true });
8458
+ return () => window.removeEventListener('scroll', handleScroll);
8459
+ }, [threshold]);
8460
+ return { hidden, scrollDirection };
8461
+ }
8462
+
8463
+ /**
8464
+ * PullToRefresh - Mobile pull-to-refresh gesture handler
8465
+ *
8466
+ * Wraps content and provides native-feeling pull-to-refresh functionality.
8467
+ * Only activates when scrolled to top of content.
8468
+ *
8469
+ * @example Basic usage
8470
+ * ```tsx
8471
+ * <PullToRefresh onRefresh={async () => {
8472
+ * await fetchLatestData();
8473
+ * }}>
8474
+ * <div className="min-h-screen">
8475
+ * {content}
8476
+ * </div>
8477
+ * </PullToRefresh>
8478
+ * ```
8479
+ *
8480
+ * @example With custom threshold
8481
+ * ```tsx
8482
+ * <PullToRefresh
8483
+ * onRefresh={handleRefresh}
8484
+ * pullThreshold={100}
8485
+ * maxPull={150}
8486
+ * >
8487
+ * {content}
8488
+ * </PullToRefresh>
8489
+ * ```
8490
+ */
8491
+ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold = 80, maxPull = 120, loadingIndicator, pullIndicator, className = '', }) {
8492
+ const [state, setState] = React.useState('idle');
8493
+ const [pullDistance, setPullDistance] = React.useState(0);
8494
+ const containerRef = React.useRef(null);
8495
+ const startY = React.useRef(0);
8496
+ const currentY = React.useRef(0);
8497
+ // Check if at top of scroll container
8498
+ const isAtTop = React.useCallback(() => {
8499
+ const container = containerRef.current;
8500
+ if (!container)
8501
+ return false;
8502
+ return container.scrollTop <= 0;
8503
+ }, []);
8504
+ // Handle touch start
8505
+ const handleTouchStart = React.useCallback((e) => {
8506
+ if (disabled || state === 'refreshing' || !isAtTop())
8507
+ return;
8508
+ startY.current = e.touches[0].clientY;
8509
+ currentY.current = startY.current;
8510
+ }, [disabled, state, isAtTop]);
8511
+ // Handle touch move
8512
+ const handleTouchMove = React.useCallback((e) => {
8513
+ if (disabled || state === 'refreshing')
8514
+ return;
8515
+ if (startY.current === 0)
8516
+ return;
8517
+ currentY.current = e.touches[0].clientY;
8518
+ const diff = currentY.current - startY.current;
8519
+ // Only allow pulling down when at top
8520
+ if (diff > 0 && isAtTop()) {
8521
+ // Apply resistance - pull slows down as distance increases
8522
+ const resistance = 0.5;
8523
+ const adjustedPull = Math.min(diff * resistance, maxPull);
8524
+ setPullDistance(adjustedPull);
8525
+ setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
8526
+ // Prevent default scroll when pulling
8527
+ if (adjustedPull > 0) {
8528
+ e.preventDefault();
8529
+ }
8530
+ }
8531
+ }, [disabled, state, isAtTop, maxPull, pullThreshold]);
8532
+ // Handle touch end
8533
+ const handleTouchEnd = React.useCallback(async () => {
8534
+ if (disabled || state === 'refreshing')
8535
+ return;
8536
+ if (state === 'ready') {
8537
+ setState('refreshing');
8538
+ setPullDistance(pullThreshold); // Hold at threshold while refreshing
8539
+ try {
8540
+ await onRefresh();
8541
+ }
8542
+ catch (error) {
8543
+ console.error('Refresh failed:', error);
8544
+ }
8545
+ setState('idle');
8546
+ }
8547
+ setPullDistance(0);
8548
+ startY.current = 0;
8549
+ currentY.current = 0;
8550
+ }, [disabled, state, pullThreshold, onRefresh]);
8551
+ // Attach touch listeners
8552
+ React.useEffect(() => {
8553
+ const container = containerRef.current;
8554
+ if (!container)
8555
+ return;
8556
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
8557
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
8558
+ container.addEventListener('touchend', handleTouchEnd);
8559
+ return () => {
8560
+ container.removeEventListener('touchstart', handleTouchStart);
8561
+ container.removeEventListener('touchmove', handleTouchMove);
8562
+ container.removeEventListener('touchend', handleTouchEnd);
8563
+ };
8564
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
8565
+ // Calculate indicator opacity and rotation
8566
+ const progress = Math.min(pullDistance / pullThreshold, 1);
8567
+ const rotation = progress * 180;
8568
+ // Default loading indicator
8569
+ const defaultLoadingIndicator = (jsxRuntime.jsx(lucideReact.Loader2, { className: "h-6 w-6 text-accent-600 animate-spin" }));
8570
+ // Default pull indicator
8571
+ const defaultPullIndicator = (jsxRuntime.jsx("div", { className: `
8572
+ transition-transform duration-200
8573
+ ${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
8574
+ `, style: { transform: `rotate(${rotation}deg)` }, children: jsxRuntime.jsx(lucideReact.ArrowDown, { className: "h-6 w-6" }) }));
8575
+ return (jsxRuntime.jsxs("div", { ref: containerRef, className: `relative overflow-auto ${className}`, style: { touchAction: pullDistance > 0 ? 'none' : 'auto' }, children: [jsxRuntime.jsx("div", { className: `
8576
+ absolute left-0 right-0 flex items-center justify-center
8577
+ transition-all duration-200 overflow-hidden
8578
+ ${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
8579
+ `, style: {
8580
+ height: `${pullDistance}px`,
8581
+ top: 0,
8582
+ zIndex: 10,
8583
+ }, children: jsxRuntime.jsx("div", { className: `
8584
+ w-10 h-10 rounded-full bg-white shadow-md
8585
+ flex items-center justify-center
8586
+ transition-transform duration-200
8587
+ ${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
8588
+ `, children: state === 'refreshing'
8589
+ ? (loadingIndicator || defaultLoadingIndicator)
8590
+ : (pullIndicator || defaultPullIndicator) }) }), jsxRuntime.jsx("div", { className: "transition-transform duration-200", style: {
8591
+ transform: `translateY(${pullDistance}px)`,
8592
+ }, children: children })] }));
8593
+ }
8594
+ /**
8595
+ * usePullToRefresh - Hook for custom pull-to-refresh implementations
8596
+ *
8597
+ * @example
8598
+ * ```tsx
8599
+ * const { pullDistance, isRefreshing, bind } = usePullToRefresh({
8600
+ * onRefresh: async () => {
8601
+ * await fetchData();
8602
+ * }
8603
+ * });
8604
+ *
8605
+ * return (
8606
+ * <div {...bind}>
8607
+ * {isRefreshing && <Spinner />}
8608
+ * {content}
8609
+ * </div>
8610
+ * );
8611
+ * ```
8612
+ */
8613
+ function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
8614
+ const [pullDistance, setPullDistance] = React.useState(0);
8615
+ const [isRefreshing, setIsRefreshing] = React.useState(false);
8616
+ const startY = React.useRef(0);
8617
+ const handleTouchStart = React.useCallback((e) => {
8618
+ if (disabled || isRefreshing)
8619
+ return;
8620
+ startY.current = e.touches[0].clientY;
8621
+ }, [disabled, isRefreshing]);
8622
+ const handleTouchMove = React.useCallback((e) => {
8623
+ if (disabled || isRefreshing || startY.current === 0)
8624
+ return;
8625
+ const diff = e.touches[0].clientY - startY.current;
8626
+ if (diff > 0) {
8627
+ const adjustedPull = Math.min(diff * 0.5, maxPull);
8628
+ setPullDistance(adjustedPull);
8629
+ }
8630
+ }, [disabled, isRefreshing, maxPull]);
8631
+ const handleTouchEnd = React.useCallback(async () => {
8632
+ if (disabled || isRefreshing)
8633
+ return;
8634
+ if (pullDistance >= pullThreshold) {
8635
+ setIsRefreshing(true);
8636
+ try {
8637
+ await onRefresh();
8638
+ }
8639
+ finally {
8640
+ setIsRefreshing(false);
8641
+ }
8642
+ }
8643
+ setPullDistance(0);
8644
+ startY.current = 0;
8645
+ }, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
8646
+ return {
8647
+ pullDistance,
8648
+ isRefreshing,
8649
+ isReady: pullDistance >= pullThreshold,
8650
+ progress: Math.min(pullDistance / pullThreshold, 1),
8651
+ bind: {
8652
+ onTouchStart: handleTouchStart,
8653
+ onTouchMove: handleTouchMove,
8654
+ onTouchEnd: handleTouchEnd,
8655
+ },
8656
+ };
6760
8657
  }
6761
8658
 
6762
8659
  function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
@@ -6872,6 +8769,125 @@ const Layout = ({ sidebar, children, statusBar, className = '', sections }) => {
6872
8769
  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
8770
  };
6874
8771
 
8772
+ /**
8773
+ * MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
8774
+ *
8775
+ * This component automatically detects the viewport size and renders the appropriate layout:
8776
+ * - **Desktop** (≥1024px): Standard Layout with sidebar, gutter, and scrollable content
8777
+ * - **Mobile/Tablet** (<1024px): Mobile header, drawer navigation, bottom tab bar
8778
+ *
8779
+ * The mobile layout features:
8780
+ * - Sticky header with hamburger menu to open drawer
8781
+ * - Sidebar rendered as a slide-in drawer
8782
+ * - Bottom navigation bar for primary navigation
8783
+ * - Safe area support for notched devices
8784
+ *
8785
+ * @example Basic usage
8786
+ * ```tsx
8787
+ * <MobileLayout
8788
+ * sidebarItems={[
8789
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8790
+ * { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks' },
8791
+ * { id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' }
8792
+ * ]}
8793
+ * currentPath={location.pathname}
8794
+ * onNavigate={(href) => navigate(href)}
8795
+ * title="My App"
8796
+ * header={<Logo />}
8797
+ * userProfile={<UserProfileButton user={user} />}
8798
+ * >
8799
+ * <Page>
8800
+ * <h1>Dashboard</h1>
8801
+ * </Page>
8802
+ * </MobileLayout>
8803
+ * ```
8804
+ *
8805
+ * @example With custom bottom nav items
8806
+ * ```tsx
8807
+ * <MobileLayout
8808
+ * sidebarItems={fullNavItems}
8809
+ * bottomNavItems={[
8810
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8811
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8812
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' }
8813
+ * ]}
8814
+ * currentPath={location.pathname}
8815
+ * title="My App"
8816
+ * >
8817
+ * {children}
8818
+ * </MobileLayout>
8819
+ * ```
8820
+ *
8821
+ * @example Force mobile layout for testing
8822
+ * ```tsx
8823
+ * <MobileLayout
8824
+ * sidebarItems={items}
8825
+ * title="Mobile Preview"
8826
+ * forceMobile
8827
+ * >
8828
+ * {children}
8829
+ * </MobileLayout>
8830
+ * ```
8831
+ */
8832
+ 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, }) => {
8833
+ const isMobileViewport = useIsMobile();
8834
+ const isTabletViewport = useIsTablet();
8835
+ const [drawerOpen, setDrawerOpen] = React.useState(false);
8836
+ // Determine if we should use mobile layout
8837
+ const useMobileLayout = forceDesktop
8838
+ ? false
8839
+ : forceMobile || isMobileViewport || isTabletViewport;
8840
+ // Open/close drawer
8841
+ const openDrawer = React.useCallback(() => setDrawerOpen(true), []);
8842
+ const closeDrawer = React.useCallback(() => setDrawerOpen(false), []);
8843
+ // Handle navigation from drawer - close drawer after navigation
8844
+ const handleDrawerNavigate = React.useCallback((href) => {
8845
+ closeDrawer();
8846
+ onNavigate?.(href);
8847
+ }, [closeDrawer, onNavigate]);
8848
+ // Handle bottom nav navigation - matches BottomNavigation's onNavigate signature
8849
+ const handleBottomNavNavigate = React.useCallback((id, href) => {
8850
+ if (href) {
8851
+ onNavigate?.(href);
8852
+ }
8853
+ // Also check if there's a custom onClick in the bottom nav items
8854
+ const item = bottomNavItems?.find(i => i.id === id);
8855
+ item?.onClick?.();
8856
+ }, [onNavigate, bottomNavItems]);
8857
+ // Convert sidebar items to bottom nav items if not provided
8858
+ const effectiveBottomNavItems = bottomNavItems || sidebarItems
8859
+ .filter(item => !item.children && item.href) // Only top-level items with href
8860
+ .slice(0, 5) // Max 5 items for bottom nav
8861
+ .map(item => ({
8862
+ id: item.id,
8863
+ label: item.label,
8864
+ icon: item.icon,
8865
+ href: item.href,
8866
+ badge: typeof item.badge === 'number' ? item.badge : undefined,
8867
+ }));
8868
+ // Determine active bottom nav ID
8869
+ const effectiveActiveBottomNavId = activeBottomNavId ||
8870
+ effectiveBottomNavItems.find(item => item.href === currentPath)?.id;
8871
+ // Close drawer on escape key
8872
+ React.useEffect(() => {
8873
+ if (!drawerOpen)
8874
+ return;
8875
+ const handleEscape = (e) => {
8876
+ if (e.key === 'Escape') {
8877
+ closeDrawer();
8878
+ }
8879
+ };
8880
+ window.addEventListener('keydown', handleEscape);
8881
+ return () => window.removeEventListener('keydown', handleEscape);
8882
+ }, [drawerOpen, closeDrawer]);
8883
+ // Desktop Layout
8884
+ if (!useMobileLayout) {
8885
+ 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] }));
8886
+ }
8887
+ // Mobile Layout
8888
+ 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 })] }))] }));
8889
+ };
8890
+
6875
8891
  /**
6876
8892
  * Two-column content layout component that provides:
6877
8893
  * - Sidebar column on the left (takes 1/3 of width)
@@ -7125,6 +9141,185 @@ function NotificationIndicator({ count = 0, onClick, className = '', maxCount =
7125
9141
  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
9142
  }
7127
9143
 
9144
+ /**
9145
+ * Get value from item by key path (supports nested keys like 'user.name')
9146
+ */
9147
+ function getValueByKey(item, key) {
9148
+ if (typeof key !== 'string') {
9149
+ return item[key];
9150
+ }
9151
+ const keys = key.split('.');
9152
+ let value = item;
9153
+ for (const k of keys) {
9154
+ if (value == null)
9155
+ return undefined;
9156
+ value = value[k];
9157
+ }
9158
+ return value;
9159
+ }
9160
+ /**
9161
+ * Skeleton card for loading state
9162
+ */
9163
+ function SkeletonCard({ className = '' }) {
9164
+ 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" })] }) }));
9165
+ }
9166
+ /**
9167
+ * DataTableCardView - Mobile-friendly card view for data tables
9168
+ *
9169
+ * Renders data as cards instead of table rows, optimized for touch interaction.
9170
+ * Automatically uses column render functions for consistent data display.
9171
+ *
9172
+ * @example Basic usage
9173
+ * ```tsx
9174
+ * <DataTableCardView
9175
+ * data={users}
9176
+ * columns={columns}
9177
+ * cardConfig={{
9178
+ * titleKey: 'name',
9179
+ * subtitleKey: 'email',
9180
+ * metadataKeys: ['department', 'role'],
9181
+ * badgeKey: 'status',
9182
+ * }}
9183
+ * onCardClick={(user) => navigate(`/users/${user.id}`)}
9184
+ * />
9185
+ * ```
9186
+ *
9187
+ * @example With selection
9188
+ * ```tsx
9189
+ * <DataTableCardView
9190
+ * data={orders}
9191
+ * columns={columns}
9192
+ * cardConfig={{
9193
+ * titleKey: 'orderNumber',
9194
+ * subtitleKey: 'customer',
9195
+ * badgeKey: 'status',
9196
+ * }}
9197
+ * selectable
9198
+ * selectedRows={selectedOrders}
9199
+ * onSelectionChange={setSelectedOrders}
9200
+ * />
9201
+ * ```
9202
+ */
9203
+ 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', }) {
9204
+ const gapClasses = {
9205
+ sm: 'gap-2',
9206
+ md: 'gap-3',
9207
+ lg: 'gap-4',
9208
+ };
9209
+ // Find column by key to use its render function
9210
+ const getColumn = (key) => {
9211
+ return columns.find(col => col.key === key);
9212
+ };
9213
+ // Render a value using column's render function if available
9214
+ const renderValue = (item, key) => {
9215
+ const column = getColumn(key);
9216
+ const value = getValueByKey(item, key);
9217
+ if (column?.render) {
9218
+ return column.render(item, value);
9219
+ }
9220
+ if (value == null)
9221
+ return '-';
9222
+ if (typeof value === 'boolean')
9223
+ return value ? 'Yes' : 'No';
9224
+ if (value instanceof Date)
9225
+ return value.toLocaleDateString();
9226
+ return String(value);
9227
+ };
9228
+ // Handle card selection toggle
9229
+ const handleSelectionToggle = (item, event) => {
9230
+ event.stopPropagation();
9231
+ const key = keyExtractor(item);
9232
+ const newSelected = new Set(selectedRows);
9233
+ if (newSelected.has(key)) {
9234
+ newSelected.delete(key);
9235
+ }
9236
+ else {
9237
+ newSelected.add(key);
9238
+ }
9239
+ onSelectionChange?.(Array.from(newSelected));
9240
+ };
9241
+ // Handle card click
9242
+ const handleCardClick = (item) => {
9243
+ if (selectable && selectedRows.size > 0) {
9244
+ // If in selection mode, toggle selection instead
9245
+ const key = keyExtractor(item);
9246
+ const newSelected = new Set(selectedRows);
9247
+ if (newSelected.has(key)) {
9248
+ newSelected.delete(key);
9249
+ }
9250
+ else {
9251
+ newSelected.add(key);
9252
+ }
9253
+ onSelectionChange?.(Array.from(newSelected));
9254
+ }
9255
+ else {
9256
+ onCardClick?.(item);
9257
+ }
9258
+ };
9259
+ // Handle long press for context actions
9260
+ const handleLongPress = (item, event) => {
9261
+ onCardLongPress?.(item, event);
9262
+ };
9263
+ // Loading state
9264
+ if (loading) {
9265
+ return (jsxRuntime.jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: Array.from({ length: loadingRows }).map((_, i) => (jsxRuntime.jsx(SkeletonCard, { className: cardClassName }, i))) }));
9266
+ }
9267
+ // Empty state
9268
+ if (data.length === 0) {
9269
+ 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 }) }));
9270
+ }
9271
+ // Determine default card config if not provided
9272
+ const config = cardConfig || {
9273
+ titleKey: columns[0]?.key || 'id',
9274
+ subtitleKey: columns[1]?.key,
9275
+ metadataKeys: columns.slice(2, 5).map(c => c.key),
9276
+ };
9277
+ return (jsxRuntime.jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: data.map((item) => {
9278
+ const key = keyExtractor(item);
9279
+ const isSelected = selectedRows.has(key);
9280
+ // Custom card render
9281
+ if (config.renderCard) {
9282
+ return (jsxRuntime.jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9283
+ e.preventDefault();
9284
+ handleLongPress(item, e);
9285
+ }, className: `
9286
+ cursor-pointer transition-all duration-200
9287
+ ${isSelected ? 'ring-2 ring-accent-500' : ''}
9288
+ ${cardClassName}
9289
+ `, children: config.renderCard(item, columns) }, key));
9290
+ }
9291
+ // Default card layout
9292
+ const titleColumn = getColumn(config.titleKey);
9293
+ const titleValue = getValueByKey(item, config.titleKey);
9294
+ return (jsxRuntime.jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9295
+ e.preventDefault();
9296
+ handleLongPress(item, e);
9297
+ }, className: `
9298
+ bg-white rounded-lg border border-paper-200 p-4
9299
+ transition-all duration-200 cursor-pointer
9300
+ active:scale-[0.98] active:bg-paper-50
9301
+ ${isSelected ? 'ring-2 ring-accent-500 bg-accent-50/30' : 'hover:shadow-md hover:border-paper-300'}
9302
+ ${cardClassName}
9303
+ `, 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: (() => {
9304
+ const avatarValue = getValueByKey(item, config.avatarKey);
9305
+ if (typeof avatarValue === 'string' && avatarValue.startsWith('http')) {
9306
+ return (jsxRuntime.jsx("img", { src: avatarValue, alt: "", className: "w-10 h-10 rounded-full object-cover" }));
9307
+ }
9308
+ // Render initials or placeholder
9309
+ const initials = String(titleValue || '').slice(0, 2).toUpperCase();
9310
+ 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 }));
9311
+ })() })), jsxRuntime.jsxs("div", { className: "flex-1 min-w-0", children: [jsxRuntime.jsx("div", { className: "font-medium text-ink-900 truncate", children: titleColumn?.render
9312
+ ? titleColumn.render(item, titleValue)
9313
+ : 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) => {
9314
+ const column = getColumn(metaKey);
9315
+ 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)));
9316
+ }) }))] }), 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) => {
9317
+ e.stopPropagation();
9318
+ handleLongPress(item, e);
9319
+ }, 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));
9320
+ }) }));
9321
+ }
9322
+
7128
9323
  /**
7129
9324
  * ActionMenu - Inline dropdown menu for row actions
7130
9325
  */
@@ -7283,7 +9478,11 @@ function DataTable({ data, columns, loading = false, error = null, emptyMessage
7283
9478
  // Visual customization props
7284
9479
  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
9480
  // Pagination props
7286
- paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true, }) {
9481
+ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true,
9482
+ // Mobile view props
9483
+ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
9484
+ // Mobile detection for auto mode
9485
+ const isMobileViewport = useIsMobile();
7287
9486
  // Column resizing state
7288
9487
  const [columnWidths, setColumnWidths] = React.useState({});
7289
9488
  const [resizingColumn, setResizingColumn] = React.useState(null);
@@ -7902,8 +10101,274 @@ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pag
7902
10101
  return null;
7903
10102
  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
10103
  };
10104
+ // Determine if we should show card view
10105
+ const shouldShowCardView = mobileView === 'card' ||
10106
+ (mobileView === 'auto' && isMobileViewport);
10107
+ // Card view content
10108
+ const cardViewContent = shouldShowCardView ? (jsxRuntime.jsx(DataTableCardView, { data: data, columns: visibleColumns, cardConfig: cardConfig, loading: loading, loadingRows: loadingRows, emptyMessage: emptyMessage, onCardClick: onRowClick, onCardLongPress: (item, event) => {
10109
+ if (enableContextMenu && allActions.length > 0) {
10110
+ event.preventDefault();
10111
+ const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
10112
+ const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
10113
+ setContextMenuState({
10114
+ isOpen: true,
10115
+ position: { x: clientX, y: clientY },
10116
+ item,
10117
+ });
10118
+ }
10119
+ }, 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
10120
  // 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 }) }))] }));
10121
+ 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 }) }))] }));
10122
+ }
10123
+
10124
+ // Color mapping for action buttons
10125
+ const colorClasses = {
10126
+ primary: 'bg-accent-500 text-white',
10127
+ success: 'bg-success-500 text-white',
10128
+ warning: 'bg-warning-500 text-white',
10129
+ error: 'bg-error-500 text-white',
10130
+ default: 'bg-paper-500 text-white',
10131
+ };
10132
+ /**
10133
+ * SwipeActions - Touch-based swipe actions for list items
10134
+ *
10135
+ * Wraps any content with swipe-to-reveal actions, commonly used in mobile
10136
+ * list items for quick actions like delete, archive, edit, etc.
10137
+ *
10138
+ * Features:
10139
+ * - Left and right swipe actions
10140
+ * - Full swipe to trigger primary action
10141
+ * - Spring-back animation
10142
+ * - Touch and mouse support
10143
+ * - Customizable thresholds
10144
+ *
10145
+ * @example Basic delete action
10146
+ * ```tsx
10147
+ * <SwipeActions
10148
+ * leftActions={[
10149
+ * {
10150
+ * id: 'delete',
10151
+ * label: 'Delete',
10152
+ * icon: <Trash className="h-5 w-5" />,
10153
+ * color: 'error',
10154
+ * onClick: () => handleDelete(item),
10155
+ * primary: true,
10156
+ * },
10157
+ * ]}
10158
+ * >
10159
+ * <div className="p-4 bg-white">
10160
+ * List item content
10161
+ * </div>
10162
+ * </SwipeActions>
10163
+ * ```
10164
+ *
10165
+ * @example Multiple actions on both sides
10166
+ * ```tsx
10167
+ * <SwipeActions
10168
+ * leftActions={[
10169
+ * { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
10170
+ * { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
10171
+ * ]}
10172
+ * rightActions={[
10173
+ * { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
10174
+ * ]}
10175
+ * fullSwipe
10176
+ * >
10177
+ * <ListItem />
10178
+ * </SwipeActions>
10179
+ * ```
10180
+ */
10181
+ function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
10182
+ const containerRef = React.useRef(null);
10183
+ const contentRef = React.useRef(null);
10184
+ // Swipe state
10185
+ const [translateX, setTranslateX] = React.useState(0);
10186
+ const [isDragging, setIsDragging] = React.useState(false);
10187
+ const [activeDirection, setActiveDirection] = React.useState(null);
10188
+ // Touch/mouse tracking
10189
+ const startX = React.useRef(0);
10190
+ const currentX = React.useRef(0);
10191
+ const startTime = React.useRef(0);
10192
+ // Calculate action widths
10193
+ const leftActionsWidth = leftActions.length * 72; // 72px per action
10194
+ const rightActionsWidth = rightActions.length * 72;
10195
+ // Reset position
10196
+ const resetPosition = React.useCallback(() => {
10197
+ setTranslateX(0);
10198
+ setActiveDirection(null);
10199
+ onSwipeChange?.(null);
10200
+ }, [onSwipeChange]);
10201
+ // Handle touch/mouse start
10202
+ const handleStart = React.useCallback((clientX) => {
10203
+ if (disabled)
10204
+ return;
10205
+ startX.current = clientX;
10206
+ currentX.current = clientX;
10207
+ startTime.current = Date.now();
10208
+ setIsDragging(true);
10209
+ }, [disabled]);
10210
+ // Handle touch/mouse move
10211
+ const handleMove = React.useCallback((clientX) => {
10212
+ if (!isDragging || disabled)
10213
+ return;
10214
+ const deltaX = clientX - startX.current;
10215
+ currentX.current = clientX;
10216
+ // Determine direction and apply resistance at boundaries
10217
+ let newTranslateX = deltaX;
10218
+ // Swiping left (reveals left actions on right side)
10219
+ if (deltaX < 0) {
10220
+ if (leftActions.length === 0) {
10221
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10222
+ }
10223
+ else {
10224
+ const maxSwipe = fullSwipe
10225
+ ? -(containerRef.current?.offsetWidth || 300)
10226
+ : -leftActionsWidth;
10227
+ newTranslateX = Math.max(maxSwipe, deltaX);
10228
+ // Apply resistance past the action buttons
10229
+ if (newTranslateX < -leftActionsWidth) {
10230
+ const overSwipe = newTranslateX + leftActionsWidth;
10231
+ newTranslateX = -leftActionsWidth + overSwipe * 0.3;
10232
+ }
10233
+ }
10234
+ setActiveDirection('left');
10235
+ onSwipeChange?.('left');
10236
+ }
10237
+ // Swiping right (reveals right actions on left side)
10238
+ else if (deltaX > 0) {
10239
+ if (rightActions.length === 0) {
10240
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10241
+ }
10242
+ else {
10243
+ const maxSwipe = fullSwipe
10244
+ ? (containerRef.current?.offsetWidth || 300)
10245
+ : rightActionsWidth;
10246
+ newTranslateX = Math.min(maxSwipe, deltaX);
10247
+ // Apply resistance past the action buttons
10248
+ if (newTranslateX > rightActionsWidth) {
10249
+ const overSwipe = newTranslateX - rightActionsWidth;
10250
+ newTranslateX = rightActionsWidth + overSwipe * 0.3;
10251
+ }
10252
+ }
10253
+ setActiveDirection('right');
10254
+ onSwipeChange?.('right');
10255
+ }
10256
+ setTranslateX(newTranslateX);
10257
+ }, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
10258
+ // Handle touch/mouse end
10259
+ const handleEnd = React.useCallback(() => {
10260
+ if (!isDragging)
10261
+ return;
10262
+ setIsDragging(false);
10263
+ const deltaX = currentX.current - startX.current;
10264
+ const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
10265
+ const containerWidth = containerRef.current?.offsetWidth || 300;
10266
+ // Check for full swipe trigger
10267
+ if (fullSwipe) {
10268
+ const swipePercentage = Math.abs(translateX) / containerWidth;
10269
+ if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
10270
+ // Find primary action and trigger it
10271
+ if (translateX < 0 && leftActions.length > 0) {
10272
+ const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
10273
+ primaryAction.onClick();
10274
+ resetPosition();
10275
+ return;
10276
+ }
10277
+ else if (translateX > 0 && rightActions.length > 0) {
10278
+ const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
10279
+ primaryAction.onClick();
10280
+ resetPosition();
10281
+ return;
10282
+ }
10283
+ }
10284
+ }
10285
+ // Snap to open or closed position
10286
+ if (Math.abs(translateX) >= threshold || velocity > 0.3) {
10287
+ // Snap open
10288
+ if (translateX < 0 && leftActions.length > 0) {
10289
+ setTranslateX(-leftActionsWidth);
10290
+ setActiveDirection('left');
10291
+ onSwipeChange?.('left');
10292
+ }
10293
+ else if (translateX > 0 && rightActions.length > 0) {
10294
+ setTranslateX(rightActionsWidth);
10295
+ setActiveDirection('right');
10296
+ onSwipeChange?.('right');
10297
+ }
10298
+ else {
10299
+ resetPosition();
10300
+ }
10301
+ }
10302
+ else {
10303
+ // Snap closed
10304
+ resetPosition();
10305
+ }
10306
+ }, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
10307
+ // Touch event handlers
10308
+ const handleTouchStart = (e) => {
10309
+ handleStart(e.touches[0].clientX);
10310
+ };
10311
+ const handleTouchMove = (e) => {
10312
+ handleMove(e.touches[0].clientX);
10313
+ };
10314
+ const handleTouchEnd = () => {
10315
+ handleEnd();
10316
+ };
10317
+ // Mouse event handlers (for testing/desktop)
10318
+ const handleMouseDown = (e) => {
10319
+ handleStart(e.clientX);
10320
+ };
10321
+ const handleMouseMove = (e) => {
10322
+ handleMove(e.clientX);
10323
+ };
10324
+ const handleMouseUp = () => {
10325
+ handleEnd();
10326
+ };
10327
+ // Close on outside click
10328
+ React.useEffect(() => {
10329
+ if (activeDirection === null)
10330
+ return;
10331
+ const handleClickOutside = (e) => {
10332
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
10333
+ resetPosition();
10334
+ }
10335
+ };
10336
+ document.addEventListener('mousedown', handleClickOutside);
10337
+ return () => document.removeEventListener('mousedown', handleClickOutside);
10338
+ }, [activeDirection, resetPosition]);
10339
+ // Handle mouse leave during drag
10340
+ React.useEffect(() => {
10341
+ if (!isDragging)
10342
+ return;
10343
+ const handleMouseLeave = () => {
10344
+ handleEnd();
10345
+ };
10346
+ document.addEventListener('mouseup', handleMouseLeave);
10347
+ return () => document.removeEventListener('mouseup', handleMouseLeave);
10348
+ }, [isDragging, handleEnd]);
10349
+ // Render action button
10350
+ const renderActionButton = (action) => {
10351
+ const colorClass = colorClasses[action.color || 'default'];
10352
+ return (jsxRuntime.jsxs("button", { onClick: (e) => {
10353
+ e.stopPropagation();
10354
+ action.onClick();
10355
+ resetPosition();
10356
+ }, className: `
10357
+ flex flex-col items-center justify-center
10358
+ w-18 h-full min-w-[72px]
10359
+ ${colorClass}
10360
+ transition-transform duration-150
10361
+ `, style: {
10362
+ transform: isDragging ? 'scale(1)' : 'scale(1)',
10363
+ }, 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));
10364
+ };
10365
+ 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: `
10366
+ relative bg-white
10367
+ ${isDragging ? '' : 'transition-transform duration-200 ease-out'}
10368
+ `, style: {
10369
+ transform: `translateX(${translateX}px)`,
10370
+ touchAction: 'pan-y', // Allow vertical scrolling
10371
+ }, children: children })] }));
7907
10372
  }
7908
10373
 
7909
10374
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -51391,6 +53856,171 @@ function PageLayout({ title, description, children, className = '', headerConten
51391
53856
  return (jsxRuntime.jsxs(Page, { padding: "none", maxWidth: maxWidth, fixed: fixed, children: [headerContent, jsxRuntime.jsxs("div", { className: `${paddingClasses} ${maxWidthClasses[maxWidth]} mx-auto ${className}`, children: [jsxRuntime.jsxs("div", { className: "mb-8", children: [jsxRuntime.jsx("h1", { className: "text-3xl font-bold text-ink-900 mb-2", children: title }), description && (jsxRuntime.jsx("p", { className: "text-ink-600", children: description }))] }), children] })] }));
51392
53857
  }
51393
53858
 
53859
+ /**
53860
+ * PageHeader - Standard page header with title, breadcrumbs, and actions
53861
+ *
53862
+ * A consistent header component for pages that provides:
53863
+ * - Page title and optional subtitle
53864
+ * - Breadcrumb navigation
53865
+ * - Action buttons (Create, Export, etc.)
53866
+ * - Optional back button
53867
+ * - Sticky positioning option
53868
+ *
53869
+ * @example Basic usage
53870
+ * ```tsx
53871
+ * <PageHeader
53872
+ * title="Products"
53873
+ * subtitle="Manage your product catalog"
53874
+ * breadcrumbs={[{ label: 'Inventory' }, { label: 'Products' }]}
53875
+ * actions={[
53876
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport, variant: 'ghost' },
53877
+ * { id: 'add', label: 'Add Product', icon: <Plus />, onClick: handleAdd, variant: 'primary' },
53878
+ * ]}
53879
+ * />
53880
+ * ```
53881
+ *
53882
+ * @example With back button
53883
+ * ```tsx
53884
+ * <PageHeader
53885
+ * title="Edit Product"
53886
+ * backButton={{ label: 'Back to Products', onClick: () => navigate('/products') }}
53887
+ * />
53888
+ * ```
53889
+ *
53890
+ * @example With custom right content
53891
+ * ```tsx
53892
+ * <PageHeader
53893
+ * title="Dashboard"
53894
+ * rightContent={<DateRangePicker value={range} onChange={setRange} />}
53895
+ * />
53896
+ * ```
53897
+ */
53898
+ function PageHeader({ title, subtitle, breadcrumbs, showHomeBreadcrumb = true, actions, rightContent, belowTitle, className = '', sticky = false, backButton, }) {
53899
+ const variantStyles = {
53900
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
53901
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
53902
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
53903
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
53904
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
53905
+ };
53906
+ return (jsxRuntime.jsx("div", { className: `
53907
+ bg-white border-b border-paper-200
53908
+ ${sticky ? 'sticky top-0 z-40' : ''}
53909
+ ${className}
53910
+ `, children: jsxRuntime.jsxs("div", { className: "px-6 py-4", children: [breadcrumbs && breadcrumbs.length > 0 && (jsxRuntime.jsx("div", { className: "mb-3", children: jsxRuntime.jsx(Breadcrumbs, { items: breadcrumbs, showHome: showHomeBreadcrumb }) })), backButton && (jsxRuntime.jsx("div", { className: "mb-3", children: jsxRuntime.jsxs("button", { onClick: backButton.onClick, className: "inline-flex items-center gap-1.5 text-sm text-ink-500 hover:text-ink-700 transition-colors", children: [jsxRuntime.jsx("svg", { className: "h-4 w-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), jsxRuntime.jsx("span", { children: backButton.label || 'Back' })] }) })), jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4", children: [jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [jsxRuntime.jsx("h1", { className: "text-2xl font-bold text-ink-900 truncate", children: title }), subtitle && (jsxRuntime.jsx("p", { className: "mt-1 text-sm text-ink-500", children: subtitle }))] }), (actions || rightContent) && (jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxRuntime.jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
53911
+ inline-flex items-center justify-center gap-2
53912
+ px-4 py-2 text-sm font-medium rounded-lg border
53913
+ transition-all duration-200
53914
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
53915
+ disabled:opacity-40 disabled:cursor-not-allowed
53916
+ ${variantStyles[action.variant || 'secondary']}
53917
+ ${action.hideOnMobile ? 'hidden sm:inline-flex' : ''}
53918
+ `, children: [action.loading ? (jsxRuntime.jsxs("svg", { className: "h-4 w-4 animate-spin", 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" })] })) : action.icon ? (jsxRuntime.jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsxRuntime.jsx("span", { children: action.label })] }, action.id)))] }))] }), belowTitle && (jsxRuntime.jsx("div", { className: "mt-4", children: belowTitle }))] }) }));
53919
+ }
53920
+
53921
+ /**
53922
+ * ActionBar - Flexible toolbar for page-level and contextual actions
53923
+ *
53924
+ * A versatile action container that can be used for:
53925
+ * - Bulk actions when rows are selected
53926
+ * - Page-level actions and controls
53927
+ * - Form action buttons (Save/Cancel)
53928
+ * - Contextual toolbars
53929
+ *
53930
+ * @example Basic bulk actions bar
53931
+ * ```tsx
53932
+ * <ActionBar
53933
+ * visible={selectedIds.length > 0}
53934
+ * leftContent={<Text weight="medium">{selectedIds.length} selected</Text>}
53935
+ * actions={[
53936
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport },
53937
+ * { id: 'delete', label: 'Delete', icon: <Trash />, onClick: handleDelete, variant: 'danger' },
53938
+ * ]}
53939
+ * onDismiss={() => setSelectedIds([])}
53940
+ * showDismiss
53941
+ * />
53942
+ * ```
53943
+ *
53944
+ * @example Sticky bottom form actions
53945
+ * ```tsx
53946
+ * <ActionBar
53947
+ * position="bottom"
53948
+ * sticky
53949
+ * rightContent={
53950
+ * <>
53951
+ * <Button variant="ghost" onClick={handleCancel}>Cancel</Button>
53952
+ * <Button variant="primary" onClick={handleSave} loading={isSaving}>Save Changes</Button>
53953
+ * </>
53954
+ * }
53955
+ * />
53956
+ * ```
53957
+ *
53958
+ * @example Info bar with center content
53959
+ * ```tsx
53960
+ * <ActionBar
53961
+ * variant="info"
53962
+ * centerContent={
53963
+ * <Text size="sm">Showing results for "search term" - 42 items found</Text>
53964
+ * }
53965
+ * onDismiss={clearSearch}
53966
+ * showDismiss
53967
+ * />
53968
+ * ```
53969
+ */
53970
+ function ActionBar({ leftContent, centerContent, rightContent, actions, position = 'top', sticky = false, visible = true, onDismiss, showDismiss = false, className = '', variant = 'default', compact = false, }) {
53971
+ if (!visible) {
53972
+ return null;
53973
+ }
53974
+ const variantStyles = {
53975
+ default: 'bg-white border-paper-200',
53976
+ primary: 'bg-accent-50 border-accent-200',
53977
+ warning: 'bg-warning-50 border-warning-200',
53978
+ info: 'bg-blue-50 border-blue-200',
53979
+ };
53980
+ const buttonVariantStyles = {
53981
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
53982
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
53983
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
53984
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
53985
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
53986
+ };
53987
+ const positionStyles = {
53988
+ top: sticky ? 'sticky top-0 z-40 border-b' : 'border-b',
53989
+ bottom: sticky ? 'sticky bottom-0 z-40 border-t' : 'border-t',
53990
+ };
53991
+ return (jsxRuntime.jsx("div", { className: `
53992
+ ${variantStyles[variant]}
53993
+ ${positionStyles[position]}
53994
+ ${compact ? 'px-4 py-2' : 'px-6 py-3'}
53995
+ ${className}
53996
+ `, role: "toolbar", "aria-label": "Action bar", children: jsxRuntime.jsxs("div", { className: "flex items-center justify-between gap-4", children: [jsxRuntime.jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-shrink-0", children: [showDismiss && onDismiss && (jsxRuntime.jsx("button", { onClick: onDismiss, className: "\n flex items-center justify-center\n w-8 h-8 rounded-full\n text-ink-400 hover:text-ink-600\n hover:bg-paper-100\n transition-colors\n ", "aria-label": "Dismiss", children: jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })), leftContent] }), centerContent && (jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center min-w-0", children: centerContent })), jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxRuntime.jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
53997
+ inline-flex items-center justify-center gap-2
53998
+ px-3 py-1.5 text-sm font-medium rounded-lg border
53999
+ transition-all duration-200
54000
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
54001
+ disabled:opacity-40 disabled:cursor-not-allowed
54002
+ ${buttonVariantStyles[action.variant || 'secondary']}
54003
+ `, children: [action.loading ? (jsxRuntime.jsxs("svg", { className: "h-4 w-4 animate-spin", 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" })] })) : action.icon ? (jsxRuntime.jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsxRuntime.jsx("span", { children: action.label })] }, action.id)))] })] }) }));
54004
+ }
54005
+ /**
54006
+ * ActionBar.Left - Semantic wrapper for left content
54007
+ */
54008
+ function ActionBarLeft({ children }) {
54009
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
54010
+ }
54011
+ /**
54012
+ * ActionBar.Center - Semantic wrapper for center content
54013
+ */
54014
+ function ActionBarCenter({ children }) {
54015
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
54016
+ }
54017
+ /**
54018
+ * ActionBar.Right - Semantic wrapper for right content
54019
+ */
54020
+ function ActionBarRight({ children }) {
54021
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
54022
+ }
54023
+
51394
54024
  const sizeClasses = {
51395
54025
  sm: 'max-w-sm',
51396
54026
  md: 'max-w-md',
@@ -51983,7 +54613,210 @@ function useColumnReorder(initialColumns, options = {}) {
51983
54613
  };
51984
54614
  }
51985
54615
 
54616
+ /**
54617
+ * Default context value (SSR-safe defaults)
54618
+ */
54619
+ const defaultContextValue = {
54620
+ isMobile: false,
54621
+ isTablet: false,
54622
+ isDesktop: true,
54623
+ isTouchDevice: false,
54624
+ breakpoint: 'lg',
54625
+ orientation: 'landscape',
54626
+ viewport: { width: 1024, height: 768 },
54627
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
54628
+ useMobileUI: false,
54629
+ };
54630
+ /**
54631
+ * Mobile context
54632
+ */
54633
+ const MobileContext = React.createContext(defaultContextValue);
54634
+ /**
54635
+ * MobileProvider - Provides responsive state to the entire application
54636
+ *
54637
+ * Wrap your application with MobileProvider to enable auto-responsive
54638
+ * behavior in notebook-ui components.
54639
+ *
54640
+ * @example Basic usage
54641
+ * ```tsx
54642
+ * import { MobileProvider } from 'notebook-ui';
54643
+ *
54644
+ * function App() {
54645
+ * return (
54646
+ * <MobileProvider>
54647
+ * <YourApplication />
54648
+ * </MobileProvider>
54649
+ * );
54650
+ * }
54651
+ * ```
54652
+ *
54653
+ * @example Force mobile UI for testing
54654
+ * ```tsx
54655
+ * <MobileProvider forceMobileUI>
54656
+ * <YourApplication />
54657
+ * </MobileProvider>
54658
+ * ```
54659
+ */
54660
+ function MobileProvider({ children, forceMobileUI = false, forceDesktopUI = false, }) {
54661
+ const isMobile = useIsMobile();
54662
+ const isTablet = useIsTablet();
54663
+ const isDesktop = useIsDesktop();
54664
+ const isTouchDevice = useIsTouchDevice();
54665
+ const breakpoint = useBreakpoint();
54666
+ const orientation = useOrientation();
54667
+ const viewport = useViewportSize();
54668
+ const safeAreaInsets = useSafeAreaInsets();
54669
+ const value = React.useMemo(() => {
54670
+ // Calculate effective mobile UI state
54671
+ let useMobileUI = isMobile || isTouchDevice;
54672
+ // Apply force overrides
54673
+ if (forceMobileUI) {
54674
+ useMobileUI = true;
54675
+ }
54676
+ else if (forceDesktopUI) {
54677
+ useMobileUI = false;
54678
+ }
54679
+ return {
54680
+ isMobile: forceMobileUI ? true : forceDesktopUI ? false : isMobile,
54681
+ isTablet: forceMobileUI || forceDesktopUI ? false : isTablet,
54682
+ isDesktop: forceDesktopUI ? true : forceMobileUI ? false : isDesktop,
54683
+ isTouchDevice,
54684
+ breakpoint: forceMobileUI ? 'xs' : forceDesktopUI ? 'lg' : breakpoint,
54685
+ orientation,
54686
+ viewport,
54687
+ safeAreaInsets,
54688
+ useMobileUI,
54689
+ };
54690
+ }, [
54691
+ isMobile,
54692
+ isTablet,
54693
+ isDesktop,
54694
+ isTouchDevice,
54695
+ breakpoint,
54696
+ orientation,
54697
+ viewport,
54698
+ safeAreaInsets,
54699
+ forceMobileUI,
54700
+ forceDesktopUI,
54701
+ ]);
54702
+ return (jsxRuntime.jsx(MobileContext.Provider, { value: value, children: children }));
54703
+ }
54704
+ /**
54705
+ * useMobileContext - Hook to access mobile responsive state
54706
+ *
54707
+ * Must be used within a MobileProvider. Returns comprehensive
54708
+ * responsive state for making UI decisions.
54709
+ *
54710
+ * @example
54711
+ * ```tsx
54712
+ * function MyComponent() {
54713
+ * const { isMobile, useMobileUI, breakpoint } = useMobileContext();
54714
+ *
54715
+ * return useMobileUI ? <MobileView /> : <DesktopView />;
54716
+ * }
54717
+ * ```
54718
+ */
54719
+ function useMobileContext() {
54720
+ const context = React.useContext(MobileContext);
54721
+ if (context === undefined) {
54722
+ // Return default value if used outside provider (graceful degradation)
54723
+ console.warn('useMobileContext was used outside of MobileProvider. ' +
54724
+ 'Wrap your app with <MobileProvider> for full mobile support.');
54725
+ return defaultContextValue;
54726
+ }
54727
+ return context;
54728
+ }
54729
+ /**
54730
+ * withMobileContext - HOC to inject mobile context as props
54731
+ *
54732
+ * For class components or when you prefer props over hooks.
54733
+ *
54734
+ * @example
54735
+ * ```tsx
54736
+ * interface Props {
54737
+ * mobile: MobileContextValue;
54738
+ * }
54739
+ *
54740
+ * class MyComponent extends React.Component<Props> {
54741
+ * render() {
54742
+ * const { isMobile } = this.props.mobile;
54743
+ * return isMobile ? <Mobile /> : <Desktop />;
54744
+ * }
54745
+ * }
54746
+ *
54747
+ * export default withMobileContext(MyComponent);
54748
+ * ```
54749
+ */
54750
+ function withMobileContext(Component) {
54751
+ const displayName = Component.displayName || Component.name || 'Component';
54752
+ const WrappedComponent = (props) => {
54753
+ const mobile = useMobileContext();
54754
+ return jsxRuntime.jsx(Component, { ...props, mobile: mobile });
54755
+ };
54756
+ WrappedComponent.displayName = `withMobileContext(${displayName})`;
54757
+ return WrappedComponent;
54758
+ }
54759
+ /**
54760
+ * MobileOnly - Renders children only on mobile devices
54761
+ *
54762
+ * @example
54763
+ * ```tsx
54764
+ * <MobileOnly>
54765
+ * <BottomNavigation items={navItems} />
54766
+ * </MobileOnly>
54767
+ * ```
54768
+ */
54769
+ function MobileOnly({ children }) {
54770
+ const { useMobileUI } = useMobileContext();
54771
+ return useMobileUI ? jsxRuntime.jsx(jsxRuntime.Fragment, { children: children }) : null;
54772
+ }
54773
+ /**
54774
+ * DesktopOnly - Renders children only on desktop devices
54775
+ *
54776
+ * @example
54777
+ * ```tsx
54778
+ * <DesktopOnly>
54779
+ * <Sidebar items={navItems} />
54780
+ * </DesktopOnly>
54781
+ * ```
54782
+ */
54783
+ function DesktopOnly({ children }) {
54784
+ const { useMobileUI } = useMobileContext();
54785
+ return useMobileUI ? null : jsxRuntime.jsx(jsxRuntime.Fragment, { children: children });
54786
+ }
54787
+ /**
54788
+ * Responsive - Renders different content based on device type
54789
+ *
54790
+ * @example
54791
+ * ```tsx
54792
+ * <Responsive
54793
+ * mobile={<MobileNavigation />}
54794
+ * tablet={<TabletNavigation />}
54795
+ * desktop={<DesktopNavigation />}
54796
+ * />
54797
+ * ```
54798
+ */
54799
+ function Responsive({ mobile, tablet, desktop, }) {
54800
+ const { isMobile, isTablet, isDesktop } = useMobileContext();
54801
+ if (isMobile && mobile)
54802
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: mobile });
54803
+ if (isTablet && tablet)
54804
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: tablet });
54805
+ if (isDesktop && desktop)
54806
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: desktop });
54807
+ // Fallback: desktop -> tablet -> mobile
54808
+ if (isDesktop)
54809
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: desktop || tablet || mobile });
54810
+ if (isTablet)
54811
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: tablet || mobile || desktop });
54812
+ return jsxRuntime.jsx(jsxRuntime.Fragment, { children: mobile || tablet || desktop });
54813
+ }
54814
+
51986
54815
  exports.Accordion = Accordion;
54816
+ exports.ActionBar = ActionBar;
54817
+ exports.ActionBarCenter = ActionBarCenter;
54818
+ exports.ActionBarLeft = ActionBarLeft;
54819
+ exports.ActionBarRight = ActionBarRight;
51987
54820
  exports.ActionButton = ActionButton;
51988
54821
  exports.AdminModal = AdminModal;
51989
54822
  exports.Alert = Alert;
@@ -51991,7 +54824,10 @@ exports.AlertDialog = AlertDialog;
51991
54824
  exports.AppLayout = AppLayout;
51992
54825
  exports.Autocomplete = Autocomplete;
51993
54826
  exports.Avatar = Avatar;
54827
+ exports.BREAKPOINTS = BREAKPOINTS;
51994
54828
  exports.Badge = Badge;
54829
+ exports.BottomNavigation = BottomNavigation;
54830
+ exports.BottomNavigationSpacer = BottomNavigationSpacer;
51995
54831
  exports.BottomSheet = BottomSheet;
51996
54832
  exports.Box = Box;
51997
54833
  exports.Breadcrumbs = Breadcrumbs;
@@ -52007,7 +54843,9 @@ exports.CardTitle = CardTitle;
52007
54843
  exports.CardView = CardView;
52008
54844
  exports.Carousel = Carousel;
52009
54845
  exports.Checkbox = Checkbox;
54846
+ exports.CheckboxList = CheckboxList;
52010
54847
  exports.Chip = Chip;
54848
+ exports.ChipGroup = ChipGroup;
52011
54849
  exports.Collapsible = Collapsible;
52012
54850
  exports.ColorPicker = ColorPicker;
52013
54851
  exports.Combobox = Combobox;
@@ -52022,10 +54860,12 @@ exports.Dashboard = Dashboard;
52022
54860
  exports.DashboardContent = DashboardContent;
52023
54861
  exports.DashboardHeader = DashboardHeader;
52024
54862
  exports.DataTable = DataTable;
54863
+ exports.DataTableCardView = DataTableCardView;
52025
54864
  exports.DateDisplay = DateDisplay;
52026
54865
  exports.DatePicker = DatePicker;
52027
54866
  exports.DateRangePicker = DateRangePicker;
52028
54867
  exports.DateTimePicker = DateTimePicker;
54868
+ exports.DesktopOnly = DesktopOnly;
52029
54869
  exports.Drawer = Drawer;
52030
54870
  exports.DrawerFooter = DrawerFooter;
52031
54871
  exports.DropZone = DropZone;
@@ -52033,6 +54873,9 @@ exports.Dropdown = Dropdown;
52033
54873
  exports.DropdownTrigger = DropdownTrigger;
52034
54874
  exports.EmptyState = EmptyState;
52035
54875
  exports.ErrorBoundary = ErrorBoundary;
54876
+ exports.ExpandablePanel = ExpandablePanel;
54877
+ exports.ExpandablePanelContainer = ExpandablePanelContainer;
54878
+ exports.ExpandablePanelSpacer = ExpandablePanelSpacer;
52036
54879
  exports.ExpandableRowButton = ExpandableRowButton;
52037
54880
  exports.ExpandableToolbar = ExpandableToolbar;
52038
54881
  exports.ExpandedRowEditForm = ExpandedRowEditForm;
@@ -52042,6 +54885,7 @@ exports.FileUpload = FileUpload;
52042
54885
  exports.FilterBar = FilterBar;
52043
54886
  exports.FilterControls = FilterControls;
52044
54887
  exports.FilterStatusBanner = FilterStatusBanner;
54888
+ exports.FloatingActionButton = FloatingActionButton;
52045
54889
  exports.Form = Form;
52046
54890
  exports.FormContext = FormContext;
52047
54891
  exports.FormControl = FormControl;
@@ -52061,6 +54905,11 @@ exports.MarkdownEditor = MarkdownEditor;
52061
54905
  exports.MaskedInput = MaskedInput;
52062
54906
  exports.Menu = Menu;
52063
54907
  exports.MenuDivider = MenuDivider;
54908
+ exports.MobileHeader = MobileHeader;
54909
+ exports.MobileHeaderSpacer = MobileHeaderSpacer;
54910
+ exports.MobileLayout = MobileLayout;
54911
+ exports.MobileOnly = MobileOnly;
54912
+ exports.MobileProvider = MobileProvider;
52064
54913
  exports.Modal = Modal;
52065
54914
  exports.ModalFooter = ModalFooter;
52066
54915
  exports.MultiSelect = MultiSelect;
@@ -52068,24 +54917,28 @@ exports.NotificationBar = NotificationBar;
52068
54917
  exports.NotificationIndicator = NotificationIndicator;
52069
54918
  exports.NumberInput = NumberInput;
52070
54919
  exports.Page = Page;
54920
+ exports.PageHeader = PageHeader;
52071
54921
  exports.PageLayout = PageLayout;
52072
54922
  exports.PageNavigation = PageNavigation;
52073
54923
  exports.Pagination = Pagination;
52074
54924
  exports.PasswordInput = PasswordInput;
52075
54925
  exports.Popover = Popover;
52076
54926
  exports.Progress = Progress;
54927
+ exports.PullToRefresh = PullToRefresh;
52077
54928
  exports.QueryTransparency = QueryTransparency;
52078
54929
  exports.RadioGroup = RadioGroup;
52079
54930
  exports.Rating = Rating;
54931
+ exports.Responsive = Responsive;
52080
54932
  exports.RichTextEditor = RichTextEditor;
52081
54933
  exports.SearchBar = SearchBar;
54934
+ exports.SearchableList = SearchableList;
52082
54935
  exports.Select = Select;
52083
54936
  exports.Separator = Separator;
52084
54937
  exports.Show = Show;
52085
54938
  exports.Sidebar = Sidebar;
52086
54939
  exports.SidebarGroup = SidebarGroup;
52087
54940
  exports.Skeleton = Skeleton;
52088
- exports.SkeletonCard = SkeletonCard;
54941
+ exports.SkeletonCard = SkeletonCard$1;
52089
54942
  exports.SkeletonTable = SkeletonTable;
52090
54943
  exports.Slider = Slider;
52091
54944
  exports.Spreadsheet = Spreadsheet;
@@ -52099,6 +54952,7 @@ exports.StatusBadge = StatusBadge;
52099
54952
  exports.StatusBar = StatusBar;
52100
54953
  exports.StepIndicator = StepIndicator;
52101
54954
  exports.Stepper = Stepper;
54955
+ exports.SwipeActions = SwipeActions;
52102
54956
  exports.Switch = Switch;
52103
54957
  exports.Tabs = Tabs;
52104
54958
  exports.Text = Text;
@@ -52133,10 +54987,25 @@ exports.reorderArray = reorderArray;
52133
54987
  exports.saveColumnOrder = saveColumnOrder;
52134
54988
  exports.saveColumnWidths = saveColumnWidths;
52135
54989
  exports.statusManager = statusManager;
54990
+ exports.useBreakpoint = useBreakpoint;
54991
+ exports.useBreakpointValue = useBreakpointValue;
52136
54992
  exports.useColumnReorder = useColumnReorder;
52137
54993
  exports.useColumnResize = useColumnResize;
52138
54994
  exports.useCommandPalette = useCommandPalette;
52139
54995
  exports.useConfirmDialog = useConfirmDialog;
54996
+ exports.useFABScroll = useFABScroll;
52140
54997
  exports.useFormContext = useFormContext;
54998
+ exports.useIsDesktop = useIsDesktop;
54999
+ exports.useIsMobile = useIsMobile;
55000
+ exports.useIsTablet = useIsTablet;
55001
+ exports.useIsTouchDevice = useIsTouchDevice;
52141
55002
  exports.useMediaQuery = useMediaQuery;
55003
+ exports.useMobileContext = useMobileContext;
55004
+ exports.useOrientation = useOrientation;
55005
+ exports.usePrefersMobile = usePrefersMobile;
55006
+ exports.usePullToRefresh = usePullToRefresh;
55007
+ exports.useResponsiveCallback = useResponsiveCallback;
55008
+ exports.useSafeAreaInsets = useSafeAreaInsets;
55009
+ exports.useViewportSize = useViewportSize;
55010
+ exports.withMobileContext = withMobileContext;
52142
55011
  //# sourceMappingURL=index.js.map