@papernote/ui 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/components/BottomNavigation.d.ts +98 -0
  2. package/dist/components/BottomNavigation.d.ts.map +1 -0
  3. package/dist/components/Checkbox.d.ts +2 -0
  4. package/dist/components/Checkbox.d.ts.map +1 -1
  5. package/dist/components/CheckboxList.d.ts +81 -0
  6. package/dist/components/CheckboxList.d.ts.map +1 -0
  7. package/dist/components/Chip.d.ts +92 -1
  8. package/dist/components/Chip.d.ts.map +1 -1
  9. package/dist/components/ConfirmDialog.d.ts +43 -1
  10. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  11. package/dist/components/DataTable.d.ts +10 -1
  12. package/dist/components/DataTable.d.ts.map +1 -1
  13. package/dist/components/DataTableCardView.d.ts +99 -0
  14. package/dist/components/DataTableCardView.d.ts.map +1 -0
  15. package/dist/components/ExpandablePanel.d.ts +142 -0
  16. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  17. package/dist/components/FloatingActionButton.d.ts +98 -0
  18. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  19. package/dist/components/Input.d.ts +45 -1
  20. package/dist/components/Input.d.ts.map +1 -1
  21. package/dist/components/MobileHeader.d.ts +98 -0
  22. package/dist/components/MobileHeader.d.ts.map +1 -0
  23. package/dist/components/MobileLayout.d.ts +121 -0
  24. package/dist/components/MobileLayout.d.ts.map +1 -0
  25. package/dist/components/Modal.d.ts +50 -1
  26. package/dist/components/Modal.d.ts.map +1 -1
  27. package/dist/components/PullToRefresh.d.ts +87 -0
  28. package/dist/components/PullToRefresh.d.ts.map +1 -0
  29. package/dist/components/QueryTransparency.d.ts +1 -1
  30. package/dist/components/QueryTransparency.d.ts.map +1 -1
  31. package/dist/components/SearchableList.d.ts +83 -0
  32. package/dist/components/SearchableList.d.ts.map +1 -0
  33. package/dist/components/Select.d.ts +16 -2
  34. package/dist/components/Select.d.ts.map +1 -1
  35. package/dist/components/Sidebar.d.ts +40 -1
  36. package/dist/components/Sidebar.d.ts.map +1 -1
  37. package/dist/components/SwipeActions.d.ts +93 -0
  38. package/dist/components/SwipeActions.d.ts.map +1 -0
  39. package/dist/components/Switch.d.ts +1 -0
  40. package/dist/components/Switch.d.ts.map +1 -1
  41. package/dist/components/Textarea.d.ts +13 -0
  42. package/dist/components/Textarea.d.ts.map +1 -1
  43. package/dist/components/index.d.ts +27 -3
  44. package/dist/components/index.d.ts.map +1 -1
  45. package/dist/context/MobileContext.d.ts +168 -0
  46. package/dist/context/MobileContext.d.ts.map +1 -0
  47. package/dist/hooks/useResponsive.d.ts +158 -0
  48. package/dist/hooks/useResponsive.d.ts.map +1 -0
  49. package/dist/index.d.ts +1653 -56
  50. package/dist/index.esm.js +2832 -194
  51. package/dist/index.esm.js.map +1 -1
  52. package/dist/index.js +2865 -192
  53. package/dist/index.js.map +1 -1
  54. package/dist/styles.css +404 -1
  55. package/dist/types/index.d.ts +2 -0
  56. package/dist/types/index.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/components/BottomNavigation.stories.tsx +142 -0
  59. package/src/components/BottomNavigation.tsx +225 -0
  60. package/src/components/Checkbox.stories.tsx +162 -0
  61. package/src/components/Checkbox.tsx +22 -6
  62. package/src/components/CheckboxList.stories.tsx +311 -0
  63. package/src/components/CheckboxList.tsx +433 -0
  64. package/src/components/Chip.stories.tsx +389 -0
  65. package/src/components/Chip.tsx +182 -3
  66. package/src/components/ConfirmDialog.tsx +56 -4
  67. package/src/components/DataTable.tsx +60 -1
  68. package/src/components/DataTableCardView.stories.tsx +307 -0
  69. package/src/components/DataTableCardView.tsx +419 -0
  70. package/src/components/ExpandablePanel.stories.tsx +620 -0
  71. package/src/components/ExpandablePanel.tsx +383 -0
  72. package/src/components/FloatingActionButton.stories.tsx +197 -0
  73. package/src/components/FloatingActionButton.tsx +301 -0
  74. package/src/components/Grid.stories.tsx +16 -16
  75. package/src/components/Input.stories.tsx +214 -0
  76. package/src/components/Input.tsx +81 -4
  77. package/src/components/MobileHeader.stories.tsx +205 -0
  78. package/src/components/MobileHeader.tsx +233 -0
  79. package/src/components/MobileLayout.stories.tsx +338 -0
  80. package/src/components/MobileLayout.tsx +313 -0
  81. package/src/components/Modal.stories.tsx +183 -0
  82. package/src/components/Modal.tsx +84 -3
  83. package/src/components/PullToRefresh.stories.tsx +321 -0
  84. package/src/components/PullToRefresh.tsx +294 -0
  85. package/src/components/QueryTransparency.tsx +1 -1
  86. package/src/components/SearchableList.stories.tsx +437 -0
  87. package/src/components/SearchableList.tsx +326 -0
  88. package/src/components/Select.stories.tsx +190 -0
  89. package/src/components/Select.tsx +353 -137
  90. package/src/components/Sidebar.tsx +191 -8
  91. package/src/components/SwipeActions.stories.tsx +327 -0
  92. package/src/components/SwipeActions.tsx +387 -0
  93. package/src/components/Switch.stories.tsx +158 -0
  94. package/src/components/Switch.tsx +12 -3
  95. package/src/components/Textarea.tsx +31 -1
  96. package/src/components/index.ts +63 -3
  97. package/src/context/MobileContext.tsx +296 -0
  98. package/src/hooks/useResponsive.ts +360 -0
  99. package/src/types/index.ts +4 -0
  100. package/tailwind.config.js +56 -1
package/dist/index.esm.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import * as React from 'react';
3
- import React__default, { forwardRef, useState, useRef, useId, useImperativeHandle, useEffect, useCallback, useMemo, cloneElement, isValidElement, Component, createContext as createContext$1, useLayoutEffect, createElement, useContext, useReducer } from 'react';
4
- import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, Circle, ChevronsRight, ChevronsLeft, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, Home, FileText, Image, File as File$1, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Download, Save, XCircle, Filter, BarChart3, MessageSquare } from 'lucide-react';
3
+ import React__default, { forwardRef, useState, useEffect, useCallback, useRef, useId, useImperativeHandle, useMemo, Children, isValidElement, cloneElement, Component, createContext as createContext$1, useLayoutEffect, createElement, useContext, useReducer } from 'react';
4
+ import { Loader2, X, EyeOff, Eye, AlertTriangle, CheckCircle, AlertCircle, ChevronDown, Search, Check, Minus, Star, Calendar as Calendar$1, ChevronLeft, ChevronRight, Clock, ChevronUp, Plus, TrendingUp, TrendingDown, Info, Trash2, Circle, ChevronsRight, ChevronsLeft, MoreVertical, GripVertical, Upload, Bold, Italic, Underline, List, ListOrdered, Code, Link, Home, FileText, Image, File as File$1, Menu as Menu$1, ArrowDown, User, Settings, LogOut, Moon, Sun, Bell, Edit, Trash, Download, Save, XCircle, Filter, BarChart3, MessageSquare } from 'lucide-react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { Link as Link$1 } from 'react-router-dom';
7
7
 
@@ -206,12 +206,19 @@ function ButtonGroup({ options, value, values = [], onChange, onChangeMultiple,
206
206
  * A feature-rich text input with support for validation states, character counting,
207
207
  * password visibility toggle, prefix/suffix text and icons, and clearable functionality.
208
208
  *
209
+ * Mobile optimizations:
210
+ * - inputMode prop for appropriate mobile keyboard
211
+ * - enterKeyHint prop for mobile keyboard action button
212
+ * - Size variants with touch-friendly targets (44px for 'lg')
213
+ *
209
214
  * @example Basic input with label
210
215
  * ```tsx
211
216
  * <Input
212
217
  * label="Email"
213
218
  * type="email"
214
219
  * placeholder="Enter your email"
220
+ * inputMode="email"
221
+ * enterKeyHint="next"
215
222
  * />
216
223
  * ```
217
224
  *
@@ -237,18 +244,30 @@ function ButtonGroup({ options, value, values = [], onChange, onChangeMultiple,
237
244
  * />
238
245
  * ```
239
246
  *
247
+ * @example Mobile-optimized phone input
248
+ * ```tsx
249
+ * <Input
250
+ * label="Phone Number"
251
+ * type="tel"
252
+ * inputMode="tel"
253
+ * enterKeyHint="done"
254
+ * size="lg"
255
+ * />
256
+ * ```
257
+ *
240
258
  * @example With prefix/suffix
241
259
  * ```tsx
242
260
  * <Input
243
261
  * label="Amount"
244
262
  * type="number"
263
+ * inputMode="decimal"
245
264
  * prefixIcon={<DollarSign />}
246
265
  * suffix="USD"
247
266
  * clearable
248
267
  * />
249
268
  * ```
250
269
  */
251
- const Input = 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) => {
270
+ const Input = 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) => {
252
271
  const inputId = id || `input-${Math.random().toString(36).substring(2, 9)}`;
253
272
  const [showPassword, setShowPassword] = useState(false);
254
273
  // Handle clear button click
@@ -272,6 +291,28 @@ const Input = forwardRef(({ label, helperText, validationState, validationMessag
272
291
  // Calculate character count
273
292
  const currentLength = value ? String(value).length : 0;
274
293
  const showCounter = showCount && maxLength;
294
+ // Auto-detect inputMode based on type if not specified
295
+ const effectiveInputMode = inputMode || (() => {
296
+ switch (type) {
297
+ case 'email': return 'email';
298
+ case 'tel': return 'tel';
299
+ case 'url': return 'url';
300
+ case 'number': return 'decimal';
301
+ case 'search': return 'search';
302
+ default: return undefined;
303
+ }
304
+ })();
305
+ // Size classes
306
+ const sizeClasses = {
307
+ sm: 'h-8 text-sm',
308
+ md: 'h-10 text-base',
309
+ lg: 'h-12 text-base min-h-touch', // 44px touch target
310
+ };
311
+ const buttonSizeClasses = {
312
+ sm: 'p-1',
313
+ md: 'p-1.5',
314
+ lg: 'p-2 min-w-touch-sm min-h-touch-sm', // 36px touch target for buttons
315
+ };
275
316
  const getValidationIcon = () => {
276
317
  switch (validationState) {
277
318
  case 'error':
@@ -308,8 +349,9 @@ const Input = forwardRef(({ label, helperText, validationState, validationMessag
308
349
  return 'text-ink-600';
309
350
  }
310
351
  };
311
- return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [prefix && (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 && (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 && (jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, className: `
352
+ return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { htmlFor: inputId, className: "label", children: [label, props.required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [prefix && (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 && (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 && (jsx("div", { className: "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-ink-400", children: icon })), jsx("input", { ref: ref, id: inputId, type: actualType, value: value, maxLength: maxLength, inputMode: effectiveInputMode, enterKeyHint: enterKeyHint, className: `
312
353
  input
354
+ ${sizeClasses[size]}
313
355
  ${getValidationClasses()}
314
356
  ${prefix ? 'pl-' + (prefix.length * 8 + 12) : ''}
315
357
  ${prefixIcon && !prefix ? 'pl-10' : ''}
@@ -320,15 +362,323 @@ const Input = forwardRef(({ label, helperText, validationState, validationMessag
320
362
  ${validationState && !suffix && !suffixIcon && !showPasswordToggle ? 'pr-10' : ''}
321
363
  ${(showPasswordToggle && type === 'password') || validationState || suffix || suffixIcon ? 'pr-20' : ''}
322
364
  ${className}
323
- `, ...props }), suffix && (jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-2", children: [loading && (jsx("div", { className: "pointer-events-none text-ink-400", children: jsx(Loader2, { className: "h-5 w-5 animate-spin" }) })), suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (jsx("div", { className: "pointer-events-none text-ink-400", children: suffixIcon })), showClearButton && (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: jsx(X, { className: "h-4 w-4" }) })), type === 'password' && showPasswordToggle && (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 ? jsx(EyeOff, { className: "h-5 w-5" }) : jsx(Eye, { className: "h-5 w-5" }) })), validationState && (jsx("div", { className: "pointer-events-none", children: getValidationIcon() })), icon && iconPosition === 'right' && !suffix && !suffixIcon && !validationState && (jsx("div", { className: "pointer-events-none text-ink-400", children: icon }))] })] }), jsxs("div", { className: "flex justify-between items-center mt-2", children: [(helperText || validationMessage) && (jsx("p", { className: `text-xs ${validationMessage ? getValidationMessageColor() : 'text-ink-600'}`, children: validationMessage || helperText })), showCounter && (jsxs("p", { className: `text-xs ml-auto ${currentLength > maxLength ? 'text-error-600' : 'text-ink-500'}`, children: [currentLength, " / ", maxLength] }))] })] }));
365
+ `, ...props }), suffix && (jsx("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none text-ink-500 text-sm", children: suffix })), jsxs("div", { className: "absolute inset-y-0 right-0 pr-3 flex items-center gap-1", children: [loading && (jsx("div", { className: "pointer-events-none text-ink-400", children: jsx(Loader2, { className: "h-5 w-5 animate-spin" }) })), suffixIcon && !suffix && !validationState && !showPasswordToggle && !showClearButton && !loading && (jsx("div", { className: "pointer-events-none text-ink-400", children: suffixIcon })), showClearButton && (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: jsx(X, { className: "h-4 w-4" }) })), type === 'password' && showPasswordToggle && (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 ? jsx(EyeOff, { className: "h-5 w-5" }) : jsx(Eye, { className: "h-5 w-5" }) })), validationState && (jsx("div", { className: "pointer-events-none", children: getValidationIcon() })), icon && iconPosition === 'right' && !suffix && !suffixIcon && !validationState && (jsx("div", { className: "pointer-events-none text-ink-400", children: icon }))] })] }), jsxs("div", { className: "flex justify-between items-center mt-2", children: [(helperText || validationMessage) && (jsx("p", { className: `text-xs ${validationMessage ? getValidationMessageColor() : 'text-ink-600'}`, children: validationMessage || helperText })), showCounter && (jsxs("p", { className: `text-xs ml-auto ${currentLength > maxLength ? 'text-error-600' : 'text-ink-500'}`, children: [currentLength, " / ", maxLength] }))] })] }));
324
366
  });
325
367
  Input.displayName = 'Input';
326
368
 
327
369
  /**
328
- * Select - Dropdown select component with search, groups, and virtual scrolling
370
+ * Tailwind breakpoint values in pixels
371
+ */
372
+ const BREAKPOINTS = {
373
+ xs: 0,
374
+ sm: 640,
375
+ md: 768,
376
+ lg: 1024,
377
+ xl: 1280,
378
+ '2xl': 1536,
379
+ };
380
+ /**
381
+ * SSR-safe check for window availability
382
+ */
383
+ const isBrowser = typeof window !== 'undefined';
384
+ /**
385
+ * Get initial viewport size (SSR-safe)
386
+ */
387
+ const getInitialViewportSize = () => {
388
+ if (!isBrowser) {
389
+ return { width: 1024, height: 768 }; // Default to desktop for SSR
390
+ }
391
+ return {
392
+ width: window.innerWidth,
393
+ height: window.innerHeight,
394
+ };
395
+ };
396
+ /**
397
+ * useViewportSize - Returns current viewport dimensions
398
+ *
399
+ * Updates on window resize with debouncing for performance.
400
+ * SSR-safe with sensible defaults.
401
+ *
402
+ * @example
403
+ * const { width, height } = useViewportSize();
404
+ * console.log(`Viewport: ${width}x${height}`);
405
+ */
406
+ function useViewportSize() {
407
+ const [size, setSize] = useState(getInitialViewportSize);
408
+ useEffect(() => {
409
+ if (!isBrowser)
410
+ return;
411
+ let timeoutId;
412
+ const handleResize = () => {
413
+ clearTimeout(timeoutId);
414
+ timeoutId = setTimeout(() => {
415
+ setSize({
416
+ width: window.innerWidth,
417
+ height: window.innerHeight,
418
+ });
419
+ }, 100); // Debounce 100ms
420
+ };
421
+ window.addEventListener('resize', handleResize);
422
+ // Set initial size on mount (in case SSR default differs)
423
+ handleResize();
424
+ return () => {
425
+ clearTimeout(timeoutId);
426
+ window.removeEventListener('resize', handleResize);
427
+ };
428
+ }, []);
429
+ return size;
430
+ }
431
+ /**
432
+ * useBreakpoint - Returns the current Tailwind breakpoint
433
+ *
434
+ * Automatically updates when viewport crosses breakpoint thresholds.
435
+ *
436
+ * @example
437
+ * const breakpoint = useBreakpoint();
438
+ * // Returns: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
439
+ */
440
+ function useBreakpoint() {
441
+ const { width } = useViewportSize();
442
+ if (width >= BREAKPOINTS['2xl'])
443
+ return '2xl';
444
+ if (width >= BREAKPOINTS.xl)
445
+ return 'xl';
446
+ if (width >= BREAKPOINTS.lg)
447
+ return 'lg';
448
+ if (width >= BREAKPOINTS.md)
449
+ return 'md';
450
+ if (width >= BREAKPOINTS.sm)
451
+ return 'sm';
452
+ return 'xs';
453
+ }
454
+ /**
455
+ * useMediaQuery - React hook for CSS media queries
456
+ *
457
+ * SSR-safe implementation that returns false during SSR and
458
+ * updates reactively when media query match state changes.
459
+ *
460
+ * @param query - CSS media query string (e.g., '(max-width: 768px)')
461
+ * @returns boolean indicating if the media query matches
462
+ *
463
+ * @example
464
+ * const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
465
+ * const isReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
466
+ * const isPortrait = useMediaQuery('(orientation: portrait)');
467
+ */
468
+ function useMediaQuery(query) {
469
+ const [matches, setMatches] = useState(() => {
470
+ if (!isBrowser)
471
+ return false;
472
+ return window.matchMedia(query).matches;
473
+ });
474
+ useEffect(() => {
475
+ if (!isBrowser)
476
+ return;
477
+ const media = window.matchMedia(query);
478
+ if (media.matches !== matches) {
479
+ setMatches(media.matches);
480
+ }
481
+ const listener = (event) => {
482
+ setMatches(event.matches);
483
+ };
484
+ media.addEventListener('change', listener);
485
+ return () => media.removeEventListener('change', listener);
486
+ }, [query, matches]);
487
+ return matches;
488
+ }
489
+ /**
490
+ * useIsMobile - Returns true when viewport is mobile-sized (< 768px)
491
+ *
492
+ * @example
493
+ * const isMobile = useIsMobile();
494
+ * return isMobile ? <MobileNav /> : <DesktopNav />;
495
+ */
496
+ function useIsMobile() {
497
+ return useMediaQuery(`(max-width: ${BREAKPOINTS.md - 1}px)`);
498
+ }
499
+ /**
500
+ * useIsTablet - Returns true when viewport is tablet-sized (768px - 1023px)
501
+ *
502
+ * @example
503
+ * const isTablet = useIsTablet();
504
+ */
505
+ function useIsTablet() {
506
+ return useMediaQuery(`(min-width: ${BREAKPOINTS.md}px) and (max-width: ${BREAKPOINTS.lg - 1}px)`);
507
+ }
508
+ /**
509
+ * useIsDesktop - Returns true when viewport is desktop-sized (>= 1024px)
510
+ *
511
+ * @example
512
+ * const isDesktop = useIsDesktop();
513
+ */
514
+ function useIsDesktop() {
515
+ return useMediaQuery(`(min-width: ${BREAKPOINTS.lg}px)`);
516
+ }
517
+ /**
518
+ * useIsTouchDevice - Detects if the device supports touch input
519
+ *
520
+ * Uses multiple detection methods for reliability:
521
+ * - Touch event support
522
+ * - Pointer coarse media query
523
+ * - Max touch points
524
+ *
525
+ * @example
526
+ * const isTouchDevice = useIsTouchDevice();
527
+ * // Show swipe hints on touch devices
528
+ */
529
+ function useIsTouchDevice() {
530
+ const [isTouch, setIsTouch] = useState(() => {
531
+ if (!isBrowser)
532
+ return false;
533
+ return ('ontouchstart' in window ||
534
+ navigator.maxTouchPoints > 0 ||
535
+ window.matchMedia('(pointer: coarse)').matches);
536
+ });
537
+ useEffect(() => {
538
+ if (!isBrowser)
539
+ return;
540
+ // Re-check on mount for accuracy
541
+ const touchSupported = 'ontouchstart' in window ||
542
+ navigator.maxTouchPoints > 0 ||
543
+ window.matchMedia('(pointer: coarse)').matches;
544
+ setIsTouch(touchSupported);
545
+ }, []);
546
+ return isTouch;
547
+ }
548
+ /**
549
+ * useOrientation - Returns current screen orientation
550
+ *
551
+ * @returns 'portrait' | 'landscape'
552
+ *
553
+ * @example
554
+ * const orientation = useOrientation();
555
+ * // Adjust layout based on orientation
556
+ */
557
+ function useOrientation() {
558
+ const { width, height } = useViewportSize();
559
+ return height > width ? 'portrait' : 'landscape';
560
+ }
561
+ /**
562
+ * useBreakpointValue - Returns different values based on breakpoint
563
+ *
564
+ * Mobile-first: Returns the value for the current breakpoint or the
565
+ * closest smaller breakpoint that has a value defined.
566
+ *
567
+ * @param values - Object mapping breakpoints to values
568
+ * @param defaultValue - Fallback value if no breakpoint matches
569
+ *
570
+ * @example
571
+ * const columns = useBreakpointValue({ xs: 1, sm: 2, lg: 4 }, 1);
572
+ * // Returns 1 on xs, 2 on sm/md, 4 on lg/xl/2xl
573
+ *
574
+ * const padding = useBreakpointValue({ xs: 'p-2', md: 'p-4', xl: 'p-8' });
575
+ */
576
+ function useBreakpointValue(values, defaultValue) {
577
+ const breakpoint = useBreakpoint();
578
+ // Breakpoints in order from largest to smallest
579
+ const breakpointOrder = ['2xl', 'xl', 'lg', 'md', 'sm', 'xs'];
580
+ // Find the current breakpoint index
581
+ const currentIndex = breakpointOrder.indexOf(breakpoint);
582
+ // Look for value at current breakpoint or smaller (mobile-first)
583
+ for (let i = currentIndex; i < breakpointOrder.length; i++) {
584
+ const bp = breakpointOrder[i];
585
+ if (bp in values && values[bp] !== undefined) {
586
+ return values[bp];
587
+ }
588
+ }
589
+ return defaultValue;
590
+ }
591
+ /**
592
+ * useResponsiveCallback - Returns a memoized callback that receives responsive info
593
+ *
594
+ * Useful for callbacks that need to behave differently based on viewport.
595
+ *
596
+ * @example
597
+ * const handleClick = useResponsiveCallback((isMobile) => {
598
+ * if (isMobile) {
599
+ * openBottomSheet();
600
+ * } else {
601
+ * openModal();
602
+ * }
603
+ * });
604
+ */
605
+ function useResponsiveCallback(callback) {
606
+ const isMobile = useIsMobile();
607
+ const isTablet = useIsTablet();
608
+ const isDesktop = useIsDesktop();
609
+ return useCallback((...args) => callback(isMobile, isTablet, isDesktop)(...args), [callback, isMobile, isTablet, isDesktop]);
610
+ }
611
+ /**
612
+ * useSafeAreaInsets - Returns safe area insets for notched devices
613
+ *
614
+ * Uses CSS environment variables (env(safe-area-inset-*)) to get
615
+ * safe area dimensions for devices with notches or home indicators.
616
+ *
617
+ * @example
618
+ * const { top, bottom } = useSafeAreaInsets();
619
+ * // Add padding-bottom for home indicator
620
+ */
621
+ function useSafeAreaInsets() {
622
+ const [insets, setInsets] = useState({
623
+ top: 0,
624
+ right: 0,
625
+ bottom: 0,
626
+ left: 0,
627
+ });
628
+ useEffect(() => {
629
+ if (!isBrowser)
630
+ return;
631
+ // Create a temporary element to read CSS env() values
632
+ const el = document.createElement('div');
633
+ el.style.position = 'fixed';
634
+ el.style.top = 'env(safe-area-inset-top, 0px)';
635
+ el.style.right = 'env(safe-area-inset-right, 0px)';
636
+ el.style.bottom = 'env(safe-area-inset-bottom, 0px)';
637
+ el.style.left = 'env(safe-area-inset-left, 0px)';
638
+ el.style.visibility = 'hidden';
639
+ el.style.pointerEvents = 'none';
640
+ document.body.appendChild(el);
641
+ const computed = getComputedStyle(el);
642
+ setInsets({
643
+ top: parseInt(computed.top, 10) || 0,
644
+ right: parseInt(computed.right, 10) || 0,
645
+ bottom: parseInt(computed.bottom, 10) || 0,
646
+ left: parseInt(computed.left, 10) || 0,
647
+ });
648
+ document.body.removeChild(el);
649
+ }, []);
650
+ return insets;
651
+ }
652
+ /**
653
+ * usePrefersMobile - Checks if user prefers reduced data/animations (mobile-friendly)
654
+ *
655
+ * Combines multiple preferences that might indicate mobile/low-power usage.
656
+ */
657
+ function usePrefersMobile() {
658
+ const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
659
+ const prefersReducedData = useMediaQuery('(prefers-reduced-data: reduce)');
660
+ const isTouchDevice = useIsTouchDevice();
661
+ const isMobile = useIsMobile();
662
+ return isMobile || isTouchDevice || prefersReducedMotion || prefersReducedData;
663
+ }
664
+
665
+ // Size classes for trigger button
666
+ const sizeClasses$a = {
667
+ sm: 'h-8 text-sm py-1',
668
+ md: 'h-10 text-base py-2',
669
+ lg: 'h-12 text-base py-3 min-h-touch', // 44px touch target
670
+ };
671
+ // Size classes for options
672
+ const optionSizeClasses = {
673
+ sm: 'py-2 text-sm',
674
+ md: 'py-2.5 text-sm',
675
+ lg: 'py-3.5 text-base min-h-touch', // 44px touch target for mobile
676
+ };
677
+ /**
678
+ * Select - Dropdown select component with search, groups, virtual scrolling, and mobile support
329
679
  *
330
680
  * A feature-rich select component supporting flat or grouped options, search/filter,
331
- * option creation, virtual scrolling for large lists, and clear functionality.
681
+ * option creation, virtual scrolling for large lists, and mobile-optimized BottomSheet display.
332
682
  *
333
683
  * @example Basic select
334
684
  * ```tsx
@@ -346,6 +696,16 @@ Input.displayName = 'Input';
346
696
  * />
347
697
  * ```
348
698
  *
699
+ * @example Mobile-optimized with large touch targets
700
+ * ```tsx
701
+ * <Select
702
+ * options={options}
703
+ * size="lg"
704
+ * mobileMode="auto"
705
+ * placeholder="Select..."
706
+ * />
707
+ * ```
708
+ *
349
709
  * @example Searchable with groups
350
710
  * ```tsx
351
711
  * const groups = [
@@ -385,7 +745,7 @@ Input.displayName = 'Input';
385
745
  * ```
386
746
  */
387
747
  const Select = forwardRef((props, ref) => {
388
- 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;
748
+ 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;
389
749
  const [isOpen, setIsOpen] = useState(false);
390
750
  const [searchQuery, setSearchQuery] = useState('');
391
751
  const [scrollTop, setScrollTop] = useState(0);
@@ -393,7 +753,15 @@ const Select = forwardRef((props, ref) => {
393
753
  const selectRef = useRef(null);
394
754
  const buttonRef = useRef(null);
395
755
  const searchInputRef = useRef(null);
756
+ const mobileSearchInputRef = useRef(null);
396
757
  const listRef = useRef(null);
758
+ const nativeSelectRef = useRef(null);
759
+ // Detect mobile viewport
760
+ const isMobile = useIsMobile();
761
+ const useMobileSheet = mobileMode === 'auto' && isMobile;
762
+ const useNativeSelect = mobileMode === 'native' && isMobile;
763
+ // Auto-size for mobile
764
+ const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
397
765
  // Generate unique IDs for ARIA
398
766
  const labelId = useId();
399
767
  const listboxId = useId();
@@ -466,8 +834,10 @@ const Select = forwardRef((props, ref) => {
466
834
  setSearchQuery('');
467
835
  setIsOpen(false);
468
836
  };
469
- // Handle click outside
837
+ // Handle click outside (desktop dropdown only)
470
838
  useEffect(() => {
839
+ if (useMobileSheet)
840
+ return; // Mobile sheet handles its own closing
471
841
  const handleClickOutside = (event) => {
472
842
  if (selectRef.current && !selectRef.current.contains(event.target)) {
473
843
  setIsOpen(false);
@@ -480,50 +850,105 @@ const Select = forwardRef((props, ref) => {
480
850
  return () => {
481
851
  document.removeEventListener('mousedown', handleClickOutside);
482
852
  };
483
- }, [isOpen]);
853
+ }, [isOpen, useMobileSheet]);
484
854
  // Focus search input when opened
485
855
  useEffect(() => {
486
- if (isOpen && searchable && searchInputRef.current) {
487
- searchInputRef.current.focus();
856
+ if (isOpen && searchable) {
857
+ if (useMobileSheet && mobileSearchInputRef.current) {
858
+ // Slight delay for mobile sheet animation
859
+ setTimeout(() => mobileSearchInputRef.current?.focus(), 100);
860
+ }
861
+ else if (searchInputRef.current) {
862
+ searchInputRef.current.focus();
863
+ }
488
864
  }
489
- }, [isOpen, searchable]);
865
+ }, [isOpen, searchable, useMobileSheet]);
866
+ // Lock body scroll when mobile sheet is open
867
+ useEffect(() => {
868
+ if (useMobileSheet && isOpen) {
869
+ document.body.style.overflow = 'hidden';
870
+ }
871
+ else {
872
+ document.body.style.overflow = '';
873
+ }
874
+ return () => {
875
+ document.body.style.overflow = '';
876
+ };
877
+ }, [isOpen, useMobileSheet]);
878
+ // Handle escape key for mobile sheet
879
+ useEffect(() => {
880
+ if (!useMobileSheet || !isOpen)
881
+ return;
882
+ const handleEscape = (e) => {
883
+ if (e.key === 'Escape') {
884
+ setIsOpen(false);
885
+ setSearchQuery('');
886
+ }
887
+ };
888
+ document.addEventListener('keydown', handleEscape);
889
+ return () => document.removeEventListener('keydown', handleEscape);
890
+ }, [isOpen, useMobileSheet]);
490
891
  const handleSelect = (optionValue) => {
491
892
  onChange?.(optionValue);
492
893
  setIsOpen(false);
493
894
  setSearchQuery('');
494
895
  };
896
+ const handleClose = () => {
897
+ setIsOpen(false);
898
+ setSearchQuery('');
899
+ };
900
+ // Render option button (shared between desktop and mobile)
901
+ const renderOption = (option, isSelected, mobile = false) => (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
902
+ w-full flex items-center justify-between px-4 transition-colors
903
+ ${mobile ? optionSizeClasses.lg : optionSizeClasses[effectiveSize]}
904
+ ${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
905
+ ${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 active:bg-paper-100 cursor-pointer'}
906
+ `, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: `${mobile ? 'h-5 w-5' : 'h-4 w-4'} text-accent-600` })] }, option.value));
907
+ // Render options list content (shared between desktop and mobile)
908
+ const renderOptionsContent = (mobile = false) => {
909
+ if (loading) {
910
+ return (jsxs("div", { className: "px-4 py-8 flex items-center justify-center", role: "status", "aria-live": "polite", children: [jsx(Loader2, { className: "h-5 w-5 animate-spin text-ink-500" }), jsx("span", { className: "ml-2 text-sm text-ink-500", children: "Loading..." })] }));
911
+ }
912
+ if (filteredOptions.length === 0 && filteredGroups.length === 0 && !showCreateOption) {
913
+ return (jsx("div", { className: "px-4 py-3 text-sm text-ink-500 text-center", role: "status", "aria-live": "polite", children: "No options found" }));
914
+ }
915
+ return (jsxs(Fragment, { children: [showCreateOption && (jsx("button", { type: "button", onClick: handleCreateOption, className: `
916
+ w-full flex items-center px-4 text-accent-700 hover:bg-accent-50 transition-colors border-b border-paper-200
917
+ ${mobile ? 'py-3.5 text-base' : 'py-2.5 text-sm'}
918
+ `, children: jsxs("span", { className: "font-medium", children: ["Create \"", searchQuery, "\""] }) })), useVirtualScrolling ? (jsx("div", { style: { height: totalHeight, position: 'relative' }, children: jsx("div", { style: { transform: `translateY(${offsetY}px)` }, children: visibleItems.map((item) => {
919
+ const option = item.option;
920
+ const isSelected = option.value === value;
921
+ const key = `${item.type}-${item.groupIndex}-${item.optionIndex}-${option.value}`;
922
+ return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: { height: mobile ? '56px' : `${virtualItemHeight}px` }, className: `
923
+ w-full flex items-center justify-between px-4 transition-colors
924
+ ${mobile ? 'text-base' : 'text-sm'}
925
+ ${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
926
+ ${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
927
+ `, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: `${mobile ? 'h-5 w-5' : 'h-4 w-4'} text-accent-600` })] }, key));
928
+ }) }) })) : (jsxs(Fragment, { children: [filteredOptions.map((option) => renderOption(option, option.value === value, mobile)), filteredGroups.map((group) => (jsxs("div", { children: [jsx("div", { className: `
929
+ px-4 font-semibold text-ink-500 uppercase tracking-wider bg-paper-50 border-t border-b border-paper-200
930
+ ${mobile ? 'py-2.5 text-xs' : 'py-2 text-xs'}
931
+ `, children: group.label }), group.options.map((option) => renderOption(option, option.value === value, mobile))] }, group.label)))] }))] }));
932
+ };
933
+ // Native select for mobile (optional)
934
+ if (useNativeSelect) {
935
+ return (jsxs("div", { className: "w-full", children: [label && (jsx("label", { id: labelId, className: "label", children: label })), jsxs("div", { className: "relative", children: [jsxs("select", { ref: nativeSelectRef, value: value || '', onChange: (e) => onChange?.(e.target.value), disabled: disabled, className: `
936
+ input w-full appearance-none pr-10
937
+ ${sizeClasses$a[effectiveSize]}
938
+ ${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
939
+ ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
940
+ `, "aria-labelledby": label ? labelId : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : (helperText ? helperTextId : undefined), children: [jsx("option", { value: "", disabled: true, children: placeholder }), options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))), groups.map((group) => (jsx("optgroup", { label: group.label, children: group.options.map((opt) => (jsx("option", { value: opt.value, disabled: opt.disabled, children: opt.label }, opt.value))) }, group.label)))] }), jsx(ChevronDown, { className: "absolute right-3 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-500 pointer-events-none" })] }), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
941
+ }
495
942
  return (jsxs("div", { className: "w-full", children: [label && (jsx("label", { id: labelId, className: "label", children: label })), jsxs("div", { ref: selectRef, className: "relative", children: [jsxs("button", { ref: buttonRef, type: "button", onClick: () => !disabled && setIsOpen(!isOpen), disabled: disabled, className: `
496
- input w-full flex items-center justify-between
943
+ input w-full flex items-center justify-between px-3
944
+ ${sizeClasses$a[effectiveSize]}
497
945
  ${error ? 'border-error-400 focus:border-error-400 focus:ring-error-400' : ''}
498
946
  ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
499
947
  `, 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: [jsxs("span", { className: `flex items-center gap-2 ${selectedOption ? 'text-ink-800' : 'text-ink-400'}`, children: [loading && jsx(Loader2, { className: "h-4 w-4 animate-spin text-ink-500" }), !loading && selectedOption?.icon && jsx("span", { children: selectedOption.icon }), selectedOption ? selectedOption.label : placeholder] }), jsxs("div", { className: "flex items-center gap-1", children: [clearable && value && (jsx("button", { type: "button", onClick: (e) => {
500
948
  e.stopPropagation();
501
949
  onChange?.('');
502
950
  setIsOpen(false);
503
- }, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsx(X, { className: "h-4 w-4" }) })), jsx(ChevronDown, { className: `h-4 w-4 text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }), isOpen && (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 && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), 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 })] }) })), 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 ? (jsxs("div", { className: "px-4 py-8 flex items-center justify-center", role: "status", "aria-live": "polite", children: [jsx(Loader2, { className: "h-5 w-5 animate-spin text-ink-500" }), jsx("span", { className: "ml-2 text-sm text-ink-500", children: "Loading..." })] })) : filteredOptions.length === 0 && filteredGroups.length === 0 && !showCreateOption ? (jsx("div", { className: "px-4 py-3 text-sm text-ink-500 text-center", role: "status", "aria-live": "polite", children: "No options found" })) : (jsxs(Fragment, { children: [showCreateOption && (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: jsxs("span", { className: "font-medium", children: ["Create \"", searchQuery, "\""] }) })), useVirtualScrolling ? (jsx("div", { style: { height: totalHeight, position: 'relative' }, children: jsx("div", { style: { transform: `translateY(${offsetY}px)` }, children: visibleItems.map((item) => {
504
- const option = item.option;
505
- const isSelected = option.value === value;
506
- const key = `${item.type}-${item.groupIndex}-${item.optionIndex}-${option.value}`;
507
- return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, style: { height: `${virtualItemHeight}px` }, className: `
508
- w-full flex items-center justify-between px-4 text-sm transition-colors
509
- ${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
510
- ${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
511
- `, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: "h-4 w-4 text-accent-600" })] }, key));
512
- }) }) })) : (jsxs(Fragment, { children: [filteredOptions.map((option) => {
513
- const isSelected = option.value === value;
514
- return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
515
- w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors
516
- ${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
517
- ${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
518
- `, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: "h-4 w-4 text-accent-600" })] }, option.value));
519
- }), filteredGroups.map((group) => (jsxs("div", { children: [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) => {
520
- const isSelected = option.value === value;
521
- return (jsxs("button", { type: "button", onClick: () => !option.disabled && handleSelect(option.value), disabled: option.disabled, className: `
522
- w-full flex items-center justify-between px-4 py-2.5 text-sm transition-colors
523
- ${isSelected ? 'bg-accent-50 text-accent-900' : 'text-ink-700'}
524
- ${option.disabled ? 'opacity-40 cursor-not-allowed' : 'hover:bg-paper-50 cursor-pointer'}
525
- `, role: "option", "aria-selected": isSelected, children: [jsxs("span", { className: "flex items-center gap-2", children: [option.icon && jsx("span", { children: option.icon }), option.label] }), isSelected && jsx(Check, { className: "h-4 w-4 text-accent-600" })] }, option.value));
526
- })] }, group.label)))] }))] })) })] }))] }), error && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
951
+ }, className: "text-ink-400 hover:text-ink-600 transition-colors p-0.5", "aria-label": "Clear selection", children: jsx(X, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'}` }) })), jsx(ChevronDown, { className: `${effectiveSize === 'lg' ? 'h-5 w-5' : 'h-4 w-4'} text-ink-500 transition-transform ${isOpen ? 'rotate-180' : ''}` })] })] }), isOpen && !useMobileSheet && (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 && (jsx("div", { className: "p-2 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-ink-400" }), 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 })] }) })), 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 && createPortal(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: [jsx("div", { className: "absolute inset-0 bg-black/50 animate-fade-in" }), 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: [jsx("div", { className: "py-3 cursor-grab", children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) }), jsxs("div", { className: "px-4 pb-3 border-b border-paper-200 flex items-center justify-between", children: [label && (jsx("h2", { id: `mobile-${labelId}`, className: "text-lg font-semibold text-ink-900", children: label })), !label && (jsx("h2", { className: "text-lg font-semibold text-ink-900", children: placeholder })), jsx("button", { onClick: handleClose, className: "text-ink-400 hover:text-ink-600 transition-colors p-2 -mr-2", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) })] }), searchable && (jsx("div", { className: "p-3 border-b border-paper-200", children: jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-ink-400" }), 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" })] }) })), 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 && (jsx("p", { id: errorId, className: "mt-2 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { id: helperTextId, className: "mt-2 text-xs text-ink-600", children: helperText }))] }));
527
952
  });
528
953
  Select.displayName = 'Select';
529
954
 
@@ -609,6 +1034,9 @@ const Switch = forwardRef(({ checked, onChange, label, description, disabled = f
609
1034
  const switchId = useId();
610
1035
  const labelId = label ? `${switchId}-label` : undefined;
611
1036
  const descId = description ? `${switchId}-desc` : undefined;
1037
+ // Auto-size for mobile
1038
+ const isMobile = useIsMobile();
1039
+ const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
612
1040
  const sizeStyles = {
613
1041
  sm: {
614
1042
  switch: 'w-9 h-5',
@@ -629,14 +1057,16 @@ const Switch = forwardRef(({ checked, onChange, label, description, disabled = f
629
1057
  spinner: 'h-5 w-5',
630
1058
  },
631
1059
  };
632
- const styles = sizeStyles[size];
1060
+ const styles = sizeStyles[effectiveSize];
633
1061
  const isDisabled = disabled || loading;
634
1062
  const handleChange = () => {
635
1063
  if (!isDisabled) {
636
1064
  onChange(!checked);
637
1065
  }
638
1066
  };
639
- return (jsxs("label", { htmlFor: switchId, className: `flex items-center gap-3 ${isDisabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`, children: [jsxs("div", { className: "relative inline-block flex-shrink-0", children: [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" }), jsx("div", { className: `
1067
+ // Touch target padding for mobile
1068
+ const touchTargetClass = effectiveSize === 'lg' ? 'min-h-touch py-1' : '';
1069
+ return (jsxs("label", { htmlFor: switchId, className: `flex items-center gap-3 ${touchTargetClass} ${isDisabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}`, children: [jsxs("div", { className: "relative inline-block flex-shrink-0", children: [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" }), jsx("div", { className: `
640
1070
  ${styles.switch}
641
1071
  rounded-full transition-all duration-200
642
1072
  ${checked ? 'bg-accent-500' : 'bg-paper-300'}
@@ -645,11 +1075,20 @@ const Switch = forwardRef(({ checked, onChange, label, description, disabled = f
645
1075
  ${styles.slider}
646
1076
  absolute left-0.5 top-0.5 bg-white rounded-full shadow-sm transition-transform duration-200 flex items-center justify-center
647
1077
  ${checked ? styles.translate : ''}
648
- `, children: loading && jsx(Loader2, { className: `${styles.spinner} animate-spin text-accent-600` }) }) })] }), (label || description) && (jsxs("div", { className: "flex-1", children: [label && jsx("p", { id: labelId, className: "text-sm font-medium text-ink-900", children: label }), description && jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description })] }))] }));
1078
+ `, children: loading && jsx(Loader2, { className: `${styles.spinner} animate-spin text-accent-600` }) }) })] }), (label || description) && (jsxs("div", { className: "flex-1", children: [label && jsx("p", { id: labelId, className: `${effectiveSize === 'lg' ? 'text-base' : 'text-sm'} font-medium text-ink-900`, children: label }), description && jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description })] }))] }));
649
1079
  });
650
1080
  Switch.displayName = 'Switch';
651
1081
 
652
- const Textarea = 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) => {
1082
+ // Size classes for textarea
1083
+ const sizeClasses$9 = {
1084
+ sm: 'px-3 py-2 text-sm',
1085
+ md: 'px-4 py-3 text-sm',
1086
+ lg: 'px-4 py-3.5 text-base', // Larger padding and text for mobile
1087
+ };
1088
+ const Textarea = 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) => {
1089
+ // Detect mobile and auto-size
1090
+ const isMobile = useIsMobile();
1091
+ const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
653
1092
  const textareaId = id || `textarea-${Math.random().toString(36).substring(2, 9)}`;
654
1093
  const currentLength = typeof value === 'string' ? value.length : 0;
655
1094
  const internalRef = useRef(null);
@@ -724,11 +1163,12 @@ const Textarea = forwardRef(({ label, helperText, validationState, validationMes
724
1163
  return 'text-ink-600';
725
1164
  }
726
1165
  };
727
- return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { htmlFor: textareaId, className: "label", children: [label, props.required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("textarea", { ref: textareaRef, id: textareaId, value: value, maxLength: maxLength, rows: autoExpand ? minRows : rows, className: `
728
- block w-full px-4 py-3 border rounded-lg text-sm text-ink-800 placeholder-ink-400
1166
+ return (jsxs("div", { className: "w-full", children: [label && (jsxs("label", { htmlFor: textareaId, className: "label", children: [label, props.required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("textarea", { ref: textareaRef, id: textareaId, value: value, maxLength: maxLength, rows: autoExpand ? minRows : rows, enterKeyHint: enterKeyHint, className: `
1167
+ block w-full border rounded-lg text-ink-800 placeholder-ink-400
729
1168
  bg-white bg-subtle-grain transition-all duration-200
730
1169
  focus:outline-none focus:ring-2 ${getResizeClass()}
731
1170
  disabled:bg-paper-100 disabled:text-ink-400 disabled:cursor-not-allowed disabled:opacity-60
1171
+ ${sizeClasses$9[effectiveSize]}
732
1172
  ${getValidationClasses()}
733
1173
  ${loading ? 'pr-10' : ''}
734
1174
  ${className}
@@ -736,10 +1176,20 @@ const Textarea = forwardRef(({ label, helperText, validationState, validationMes
736
1176
  });
737
1177
  Textarea.displayName = 'Textarea';
738
1178
 
739
- const Checkbox = forwardRef(({ checked, onChange, label, description, disabled = false, indeterminate = false, className = '', id, name, icon, }, ref) => {
1179
+ // Size classes for checkbox box and touch target
1180
+ const sizeConfig$4 = {
1181
+ sm: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-2' },
1182
+ md: { box: 'w-4 h-4', icon: 'h-3 w-3', text: 'text-sm', gap: 'gap-3' },
1183
+ 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
1184
+ };
1185
+ const Checkbox = forwardRef(({ checked, onChange, label, description, disabled = false, indeterminate = false, className = '', id, name, icon, size = 'md', }, ref) => {
740
1186
  const generatedId = useId();
741
1187
  const checkboxId = id || generatedId;
742
1188
  const descId = description ? `${checkboxId}-desc` : undefined;
1189
+ // Auto-size for mobile
1190
+ const isMobile = useIsMobile();
1191
+ const effectiveSize = isMobile && size === 'md' ? 'lg' : size;
1192
+ const config = sizeConfig$4[effectiveSize];
743
1193
  const handleChange = () => {
744
1194
  if (!disabled) {
745
1195
  onChange(!checked);
@@ -751,14 +1201,14 @@ const Checkbox = forwardRef(({ checked, onChange, label, description, disabled =
751
1201
  onChange(!checked);
752
1202
  }
753
1203
  };
754
- return (jsxs("label", { htmlFor: checkboxId, className: `flex items-start gap-3 ${disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} ${className}`, children: [jsxs("div", { className: "relative inline-block flex-shrink-0 mt-0.5", children: [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 }), jsx("div", { className: `
755
- w-4 h-4 rounded border transition-all duration-200
1204
+ return (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: [jsxs("div", { className: "relative inline-block flex-shrink-0 mt-0.5", children: [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 }), jsx("div", { className: `
1205
+ ${config.box} rounded border transition-all duration-200
756
1206
  flex items-center justify-center
757
1207
  ${checked || indeterminate
758
1208
  ? 'bg-accent-600 border-accent-600'
759
1209
  : 'bg-white border-paper-300 hover:border-paper-400'}
760
1210
  ${!disabled && 'focus-within:ring-2 focus-within:ring-accent-400 focus-within:ring-offset-2'}
761
- `, children: indeterminate ? (jsx(Minus, { className: "h-3 w-3 text-white" })) : checked ? (jsx(Check, { className: "h-3 w-3 text-white" })) : null })] }), (label || description) && (jsxs("div", { className: "flex-1", children: [label && (jsxs("div", { className: "flex items-center gap-2", children: [icon && jsx("span", { className: "text-ink-700", children: icon }), jsx("p", { className: "text-sm font-medium text-ink-900", children: label })] })), description && (jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description }))] }))] }));
1211
+ `, children: indeterminate ? (jsx(Minus, { className: `${config.icon} text-white` })) : checked ? (jsx(Check, { className: `${config.icon} text-white` })) : null })] }), (label || description) && (jsxs("div", { className: "flex-1", children: [label && (jsxs("div", { className: "flex items-center gap-2", children: [icon && jsx("span", { className: "text-ink-700", children: icon }), jsx("p", { className: `${config.text} font-medium text-ink-900`, children: label })] })), description && (jsx("p", { id: descId, className: "text-xs text-ink-600 mt-0.5", children: description }))] }))] }));
762
1212
  });
763
1213
  Checkbox.displayName = 'Checkbox';
764
1214
 
@@ -1938,7 +2388,7 @@ function Loading({ variant = 'spinner', size = 'md', text }) {
1938
2388
  function Skeleton({ className = '', ...props }) {
1939
2389
  return (jsx("div", { className: `animate-pulse bg-paper-200 rounded ${className}`, ...props }));
1940
2390
  }
1941
- function SkeletonCard() {
2391
+ function SkeletonCard$1() {
1942
2392
  return (jsxs("div", { className: "card", children: [jsx(Skeleton, { className: "h-6 w-3/4 mb-4" }), jsx(Skeleton, { className: "h-4 w-full mb-2" }), jsx(Skeleton, { className: "h-4 w-5/6 mb-2" }), jsx(Skeleton, { className: "h-4 w-4/6" })] }));
1943
2393
  }
1944
2394
  function SkeletonTable({ rows = 5, columns = 4 }) {
@@ -2590,19 +3040,167 @@ function Alert({ variant = 'info', title, children, onClose, className = '', act
2590
3040
  return (jsx("div", { className: `rounded-lg border p-4 ${styles.container} ${className}`, role: "alert", children: jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex-shrink-0 mt-0.5", children: styles.icon }), jsxs("div", { className: "flex-1 min-w-0", children: [title && jsx("h4", { className: "text-sm font-medium mb-1", children: title }), jsx("div", { className: "text-sm", children: children }), actions.length > 0 && (jsx("div", { className: "flex gap-2 mt-3", children: actions.map((action, index) => (jsx("button", { onClick: action.onClick, className: getButtonStyles(action.variant), children: action.label }, index))) }))] }), onClose && (jsx("button", { onClick: onClose, className: "flex-shrink-0 text-current opacity-70 hover:opacity-100 transition-opacity", "aria-label": "Close alert", children: jsx(X, { className: "h-4 w-4" }) }))] }) }));
2591
3041
  }
2592
3042
 
2593
- const sizeClasses$4 = {
3043
+ const heightPresets = {
3044
+ sm: '33vh',
3045
+ md: '50vh',
3046
+ lg: '75vh',
3047
+ full: '90vh',
3048
+ };
3049
+ function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHandle = true, showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className = '', }) {
3050
+ const titleId = useId();
3051
+ const [isDragging, setIsDragging] = useState(false);
3052
+ const [dragOffset, setDragOffset] = useState(0);
3053
+ const [currentHeight] = useState(typeof height === 'string' && height in heightPresets
3054
+ ? heightPresets[height]
3055
+ : height);
3056
+ const sheetRef = useRef(null);
3057
+ const startYRef = useRef(0);
3058
+ // Close on Escape
3059
+ useEffect(() => {
3060
+ if (!isOpen || !closeOnEscape)
3061
+ return;
3062
+ const handleEscape = (e) => {
3063
+ if (e.key === 'Escape') {
3064
+ onClose();
3065
+ }
3066
+ };
3067
+ document.addEventListener('keydown', handleEscape);
3068
+ return () => document.removeEventListener('keydown', handleEscape);
3069
+ }, [isOpen, closeOnEscape, onClose]);
3070
+ // Prevent body scroll when open
3071
+ useEffect(() => {
3072
+ if (isOpen) {
3073
+ document.body.style.overflow = 'hidden';
3074
+ }
3075
+ else {
3076
+ document.body.style.overflow = '';
3077
+ }
3078
+ return () => {
3079
+ document.body.style.overflow = '';
3080
+ };
3081
+ }, [isOpen]);
3082
+ const handleOverlayClick = (e) => {
3083
+ if (closeOnOverlayClick && e.target === e.currentTarget) {
3084
+ onClose();
3085
+ }
3086
+ };
3087
+ const handleDragStart = (e) => {
3088
+ setIsDragging(true);
3089
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
3090
+ startYRef.current = clientY;
3091
+ };
3092
+ const handleDragMove = (e) => {
3093
+ if (!isDragging)
3094
+ return;
3095
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
3096
+ const offset = clientY - startYRef.current;
3097
+ // Only allow dragging down
3098
+ if (offset > 0) {
3099
+ setDragOffset(offset);
3100
+ }
3101
+ };
3102
+ const handleDragEnd = () => {
3103
+ setIsDragging(false);
3104
+ // Close if dragged down more than 150px
3105
+ if (dragOffset > 150) {
3106
+ onClose();
3107
+ }
3108
+ setDragOffset(0);
3109
+ };
3110
+ useEffect(() => {
3111
+ if (!isDragging)
3112
+ return;
3113
+ const handleMove = (e) => handleDragMove(e);
3114
+ const handleEnd = () => handleDragEnd();
3115
+ document.addEventListener('touchmove', handleMove);
3116
+ document.addEventListener('mousemove', handleMove);
3117
+ document.addEventListener('touchend', handleEnd);
3118
+ document.addEventListener('mouseup', handleEnd);
3119
+ return () => {
3120
+ document.removeEventListener('touchmove', handleMove);
3121
+ document.removeEventListener('mousemove', handleMove);
3122
+ document.removeEventListener('touchend', handleEnd);
3123
+ document.removeEventListener('mouseup', handleEnd);
3124
+ };
3125
+ }, [isDragging, dragOffset]);
3126
+ if (!isOpen)
3127
+ return null;
3128
+ return (jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: handleOverlayClick, children: [jsx("div", { className: `
3129
+ absolute inset-0 bg-black/50 transition-opacity duration-300
3130
+ ${isOpen ? 'opacity-100' : 'opacity-0'}
3131
+ ` }), jsxs("div", { ref: sheetRef, className: `
3132
+ relative w-full bg-white rounded-t-2xl shadow-2xl
3133
+ transition-transform duration-300 ease-out
3134
+ ${isOpen ? 'translate-y-0' : 'translate-y-full'}
3135
+ ${className}
3136
+ `, style: {
3137
+ height: currentHeight,
3138
+ transform: `translateY(${dragOffset}px)`,
3139
+ }, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, children: [showHandle && (jsx("div", { className: "py-3 cursor-grab active:cursor-grabbing", onTouchStart: handleDragStart, onMouseDown: handleDragStart, children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) })), (title || showCloseButton) && (jsxs("div", { className: "px-6 py-4 border-b border-ink-200 flex items-center justify-between", children: [title && (jsx("h2", { id: titleId, className: "text-lg font-semibold text-ink-900", children: title })), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors ml-auto", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) }))] })), jsx("div", { className: "overflow-y-auto flex-1 p-6", children: children })] })] }));
3140
+ }
3141
+
3142
+ const sizeClasses$8 = {
2594
3143
  sm: 'max-w-md',
2595
3144
  md: 'max-w-lg',
2596
3145
  lg: 'max-w-2xl',
2597
3146
  xl: 'max-w-4xl',
2598
3147
  full: 'max-w-7xl',
2599
3148
  };
2600
- function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', }) {
3149
+ /**
3150
+ * Modal - Adaptive dialog component
3151
+ *
3152
+ * On desktop, renders as a centered modal dialog.
3153
+ * On mobile (when mobileMode='auto'), automatically renders as a BottomSheet
3154
+ * for better touch interaction and visibility.
3155
+ *
3156
+ * @example Basic modal
3157
+ * ```tsx
3158
+ * <Modal isOpen={isOpen} onClose={handleClose} title="Edit User">
3159
+ * <form>...</form>
3160
+ * <ModalFooter>
3161
+ * <Button onClick={handleClose}>Cancel</Button>
3162
+ * <Button variant="primary" onClick={handleSave}>Save</Button>
3163
+ * </ModalFooter>
3164
+ * </Modal>
3165
+ * ```
3166
+ *
3167
+ * @example Force modal on mobile
3168
+ * ```tsx
3169
+ * <Modal
3170
+ * isOpen={isOpen}
3171
+ * onClose={handleClose}
3172
+ * title="Settings"
3173
+ * mobileMode="modal"
3174
+ * >
3175
+ * ...
3176
+ * </Modal>
3177
+ * ```
3178
+ *
3179
+ * @example Always use BottomSheet
3180
+ * ```tsx
3181
+ * <Modal
3182
+ * isOpen={isOpen}
3183
+ * onClose={handleClose}
3184
+ * title="Select Option"
3185
+ * mobileMode="sheet"
3186
+ * mobileHeight="md"
3187
+ * >
3188
+ * ...
3189
+ * </Modal>
3190
+ * ```
3191
+ */
3192
+ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
2601
3193
  const modalRef = useRef(null);
2602
3194
  const mouseDownOnBackdrop = useRef(false);
2603
3195
  const titleId = useId();
2604
- // Handle escape key
3196
+ const isMobile = useIsMobile();
3197
+ // Determine if we should use BottomSheet
3198
+ const useBottomSheet = mobileMode === 'sheet' ||
3199
+ (mobileMode === 'auto' && isMobile);
3200
+ // Handle escape key (only for modal mode, BottomSheet handles its own)
2605
3201
  useEffect(() => {
3202
+ if (useBottomSheet)
3203
+ return; // BottomSheet handles escape
2606
3204
  const handleEscape = (e) => {
2607
3205
  if (e.key === 'Escape' && isOpen) {
2608
3206
  onClose();
@@ -2616,7 +3214,7 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2616
3214
  document.removeEventListener('keydown', handleEscape);
2617
3215
  document.body.style.overflow = 'unset';
2618
3216
  };
2619
- }, [isOpen, onClose]);
3217
+ }, [isOpen, onClose, useBottomSheet]);
2620
3218
  // Track if mousedown originated on the backdrop
2621
3219
  const handleBackdropMouseDown = (e) => {
2622
3220
  if (e.target === e.currentTarget) {
@@ -2652,13 +3250,18 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2652
3250
  };
2653
3251
  if (!isOpen)
2654
3252
  return null;
2655
- return (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: 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: [jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }))] }), jsx("div", { className: "px-6 py-4", children: children })] }) }));
3253
+ // Render as BottomSheet on mobile
3254
+ if (useBottomSheet) {
3255
+ return (jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }));
3256
+ }
3257
+ // Render as standard modal on desktop
3258
+ return (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: 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: [jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-paper-200", children: [jsx("h3", { id: titleId, className: "text-lg font-medium text-ink-900", children: title }), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Close modal", children: jsx(X, { className: "h-5 w-5" }) }))] }), jsx("div", { className: "px-6 py-4", children: children })] }) }));
2656
3259
  }
2657
3260
  function ModalFooter({ children }) {
2658
- return (jsx("div", { className: "flex items-center justify-end gap-3 px-6 py-4 border-t border-paper-200 bg-paper-50", children: children }));
3261
+ return (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 }));
2659
3262
  }
2660
3263
 
2661
- const sizeClasses$3 = {
3264
+ const sizeClasses$7 = {
2662
3265
  left: {
2663
3266
  sm: 'w-64',
2664
3267
  md: 'w-96',
@@ -2737,7 +3340,7 @@ function Drawer({ isOpen, onClose, title, children, placement = 'right', size =
2737
3340
  const isHorizontal = placement === 'left' || placement === 'right';
2738
3341
  return (jsxs("div", { className: "fixed inset-0 z-50 flex", children: [showOverlay && (jsx("div", { className: "fixed inset-0 bg-ink-900 bg-opacity-50 backdrop-blur-sm animate-fade-in", onClick: handleOverlayClick, "aria-hidden": "true" })), jsxs("div", { className: `
2739
3342
  fixed ${placementClasses[placement]}
2740
- ${sizeClasses$3[placement][size]}
3343
+ ${sizeClasses$7[placement][size]}
2741
3344
  bg-white border-paper-200 shadow-2xl
2742
3345
  ${isHorizontal ? 'border-r' : 'border-b'}
2743
3346
  ${animationClasses[placement].enter}
@@ -2769,14 +3372,49 @@ const variantStyles = {
2769
3372
  button: 'bg-accent-600 hover:bg-accent-700 focus:ring-accent-500',
2770
3373
  },
2771
3374
  };
2772
- function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, }) {
3375
+ /**
3376
+ * ConfirmDialog - Confirmation dialog with mobile support
3377
+ *
3378
+ * @example Basic usage
3379
+ * ```tsx
3380
+ * <ConfirmDialog
3381
+ * isOpen={isOpen}
3382
+ * onClose={handleClose}
3383
+ * onConfirm={handleDelete}
3384
+ * title="Delete Item"
3385
+ * message="Are you sure you want to delete this item? This action cannot be undone."
3386
+ * variant="danger"
3387
+ * />
3388
+ * ```
3389
+ *
3390
+ * @example With useConfirmDialog hook
3391
+ * ```tsx
3392
+ * const confirmDialog = useConfirmDialog();
3393
+ *
3394
+ * const handleDelete = () => {
3395
+ * confirmDialog.show({
3396
+ * title: 'Delete Item',
3397
+ * message: 'Are you sure?',
3398
+ * onConfirm: async () => await deleteItem(),
3399
+ * });
3400
+ * };
3401
+ *
3402
+ * return (
3403
+ * <>
3404
+ * <button onClick={handleDelete}>Delete</button>
3405
+ * <ConfirmDialog {...confirmDialog.props} />
3406
+ * </>
3407
+ * );
3408
+ * ```
3409
+ */
3410
+ function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, mobileMode = 'auto', mobileHeight = 'sm', }) {
2773
3411
  const variantStyle = variantStyles[variant];
2774
3412
  const IconComponent = icon || variantStyle.icon;
2775
3413
  const handleConfirm = async () => {
2776
3414
  await onConfirm();
2777
3415
  // Note: onClose is called by useConfirmDialog hook after onConfirm completes
2778
3416
  };
2779
- return (jsxs(Modal, { isOpen: isOpen, onClose: onClose, title: typeof title === 'string' ? title : String(title), size: "sm", showCloseButton: false, children: [jsxs("div", { className: "flex items-start gap-4", children: [jsx("div", { className: `flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-full ${variantStyle.iconBg}`, children: typeof IconComponent === 'function' ? (jsx(IconComponent, { className: `h-6 w-6 ${variantStyle.iconColor}` })) : React__default.isValidElement(IconComponent) ? (IconComponent) : null }), jsx("div", { className: "flex-1 pt-1", children: jsx("p", { className: "text-sm text-ink-700 leading-relaxed whitespace-pre-line", children: typeof message === 'string' ? message : String(message) }) })] }), jsxs(ModalFooter, { children: [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) }), 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 ? (jsxs("span", { className: "flex items-center gap-2", children: [jsxs("svg", { className: "animate-spin h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), 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)) })] })] }));
3417
+ return (jsxs(Modal, { isOpen: isOpen, onClose: onClose, title: typeof title === 'string' ? title : String(title), size: "sm", showCloseButton: false, mobileMode: mobileMode, mobileHeight: mobileHeight, mobileShowHandle: false, children: [jsxs("div", { className: "flex items-start gap-4", children: [jsx("div", { className: `flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-full ${variantStyle.iconBg}`, children: typeof IconComponent === 'function' ? (jsx(IconComponent, { className: `h-6 w-6 ${variantStyle.iconColor}` })) : React__default.isValidElement(IconComponent) ? (IconComponent) : null }), jsx("div", { className: "flex-1 pt-1", children: jsx("p", { className: "text-sm text-ink-700 leading-relaxed whitespace-pre-line", children: typeof message === 'string' ? message : String(message) }) })] }), jsxs(ModalFooter, { children: [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) }), 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 ? (jsxs("span", { className: "flex items-center gap-2", children: [jsxs("svg", { className: "animate-spin h-4 w-4", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [jsx("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), 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)) })] })] }));
2780
3418
  }
2781
3419
  /**
2782
3420
  * Hook for managing ConfirmDialog state
@@ -5107,39 +5745,45 @@ function MenuDivider() {
5107
5745
  return { divider: true, id: `divider-${Date.now()}`, label: '' };
5108
5746
  }
5109
5747
 
5110
- const variantClasses = {
5748
+ const variantClasses$4 = {
5111
5749
  primary: {
5112
5750
  default: 'bg-primary-100 text-primary-700 border-primary-200',
5113
5751
  hover: 'hover:bg-primary-200',
5114
5752
  close: 'hover:bg-primary-300 text-primary-600',
5753
+ selected: 'bg-primary-200 border-primary-400 ring-2 ring-primary-300',
5115
5754
  },
5116
5755
  secondary: {
5117
5756
  default: 'bg-ink-100 text-ink-700 border-ink-200',
5118
5757
  hover: 'hover:bg-ink-200',
5119
5758
  close: 'hover:bg-ink-300 text-ink-600',
5759
+ selected: 'bg-ink-200 border-ink-400 ring-2 ring-ink-300',
5120
5760
  },
5121
5761
  success: {
5122
5762
  default: 'bg-success-100 text-success-700 border-success-200',
5123
5763
  hover: 'hover:bg-success-200',
5124
5764
  close: 'hover:bg-success-300 text-success-600',
5765
+ selected: 'bg-success-200 border-success-400 ring-2 ring-success-300',
5125
5766
  },
5126
5767
  warning: {
5127
5768
  default: 'bg-warning-100 text-warning-700 border-warning-200',
5128
5769
  hover: 'hover:bg-warning-200',
5129
5770
  close: 'hover:bg-warning-300 text-warning-600',
5771
+ selected: 'bg-warning-200 border-warning-400 ring-2 ring-warning-300',
5130
5772
  },
5131
5773
  error: {
5132
5774
  default: 'bg-error-100 text-error-700 border-error-200',
5133
5775
  hover: 'hover:bg-error-200',
5134
5776
  close: 'hover:bg-error-300 text-error-600',
5777
+ selected: 'bg-error-200 border-error-400 ring-2 ring-error-300',
5135
5778
  },
5136
5779
  info: {
5137
5780
  default: 'bg-accent-100 text-accent-700 border-accent-200',
5138
5781
  hover: 'hover:bg-accent-200',
5139
5782
  close: 'hover:bg-accent-300 text-accent-600',
5783
+ selected: 'bg-accent-200 border-accent-400 ring-2 ring-accent-300',
5140
5784
  },
5141
5785
  };
5142
- const sizeClasses$2 = {
5786
+ const sizeClasses$6 = {
5143
5787
  sm: {
5144
5788
  container: 'h-6 px-2 text-xs gap-1',
5145
5789
  icon: 'h-3 w-3',
@@ -5156,20 +5800,52 @@ const sizeClasses$2 = {
5156
5800
  close: 'h-4 w-4 ml-2',
5157
5801
  },
5158
5802
  };
5159
- function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, }) {
5160
- const variantStyle = variantClasses[variant];
5161
- const sizeStyle = sizeClasses$2[size];
5803
+ const gapClasses = {
5804
+ xs: 'gap-1',
5805
+ sm: 'gap-1.5',
5806
+ md: 'gap-2',
5807
+ lg: 'gap-3',
5808
+ };
5809
+ /**
5810
+ * Chip - Compact element for displaying values with optional remove functionality
5811
+ *
5812
+ * @example Basic chip
5813
+ * ```tsx
5814
+ * <Chip>Tag Name</Chip>
5815
+ * ```
5816
+ *
5817
+ * @example Removable chip
5818
+ * ```tsx
5819
+ * <Chip onClose={() => removeTag(tag)}>
5820
+ * {tag.name}
5821
+ * </Chip>
5822
+ * ```
5823
+ *
5824
+ * @example With icon and selected state
5825
+ * ```tsx
5826
+ * <Chip
5827
+ * icon={<Star className="h-3 w-3" />}
5828
+ * selected={isSelected}
5829
+ * onClick={() => toggleSelection()}
5830
+ * >
5831
+ * Favorite
5832
+ * </Chip>
5833
+ * ```
5834
+ */
5835
+ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, selected = false, maxWidth, chipKey, }) {
5836
+ const variantStyle = variantClasses$4[variant];
5837
+ const sizeStyle = sizeClasses$6[size];
5162
5838
  const isClickable = !disabled && (onClick || onClose);
5163
5839
  return (jsxs("div", { className: `
5164
5840
  inline-flex items-center rounded-full border font-medium
5165
5841
  transition-colors
5166
- ${variantStyle.default}
5167
- ${isClickable && !disabled ? variantStyle.hover : ''}
5842
+ ${selected ? variantStyle.selected : variantStyle.default}
5843
+ ${isClickable && !disabled && !selected ? variantStyle.hover : ''}
5168
5844
  ${sizeStyle.container}
5169
5845
  ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
5170
5846
  ${onClick && !disabled ? 'cursor-pointer' : ''}
5171
5847
  ${className}
5172
- `, onClick: onClick && !disabled ? onClick : undefined, role: onClick ? 'button' : undefined, "aria-disabled": disabled, children: [icon && (jsx("span", { className: `flex-shrink-0 ${sizeStyle.icon}`, children: icon })), jsx("span", { className: "truncate", children: children }), onClose && (jsx("button", { type: "button", onClick: (e) => {
5848
+ `, 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 && (jsx("span", { className: `flex-shrink-0 ${sizeStyle.icon}`, children: icon })), jsx("span", { className: "truncate", children: children }), onClose && (jsx("button", { type: "button", onClick: (e) => {
5173
5849
  e.stopPropagation();
5174
5850
  if (!disabled)
5175
5851
  onClose();
@@ -5180,8 +5856,420 @@ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, dis
5180
5856
  ${sizeStyle.close}
5181
5857
  `, "aria-label": "Remove", children: jsx(X, { className: "w-full h-full" }) }))] }));
5182
5858
  }
5859
+ /**
5860
+ * ChipGroup - Container for multiple chips with layout and selection support
5861
+ *
5862
+ * @example Basic group
5863
+ * ```tsx
5864
+ * <ChipGroup wrap gap="sm">
5865
+ * {tags.map(tag => (
5866
+ * <Chip key={tag.id} onClose={() => removeTag(tag)}>
5867
+ * {tag.name}
5868
+ * </Chip>
5869
+ * ))}
5870
+ * </ChipGroup>
5871
+ * ```
5872
+ *
5873
+ * @example Selectable group (single)
5874
+ * ```tsx
5875
+ * <ChipGroup
5876
+ * selectionMode="single"
5877
+ * selectedKeys={[selectedCategory]}
5878
+ * onSelectionChange={(keys) => setSelectedCategory(keys[0])}
5879
+ * >
5880
+ * <Chip chipKey="all">All</Chip>
5881
+ * <Chip chipKey="active">Active</Chip>
5882
+ * <Chip chipKey="archived">Archived</Chip>
5883
+ * </ChipGroup>
5884
+ * ```
5885
+ *
5886
+ * @example Multi-select group
5887
+ * ```tsx
5888
+ * <ChipGroup
5889
+ * selectionMode="multiple"
5890
+ * selectedKeys={selectedTags}
5891
+ * onSelectionChange={setSelectedTags}
5892
+ * wrap
5893
+ * >
5894
+ * {availableTags.map(tag => (
5895
+ * <Chip key={tag} chipKey={tag}>{tag}</Chip>
5896
+ * ))}
5897
+ * </ChipGroup>
5898
+ * ```
5899
+ */
5900
+ function ChipGroup({ children, direction = 'horizontal', wrap = false, gap = 'sm', selectionMode = 'none', selectedKeys = [], onSelectionChange, className = '', }) {
5901
+ const handleChipClick = (chipKey) => {
5902
+ if (selectionMode === 'none' || !onSelectionChange)
5903
+ return;
5904
+ if (selectionMode === 'single') {
5905
+ // Toggle single selection
5906
+ if (selectedKeys.includes(chipKey)) {
5907
+ onSelectionChange([]);
5908
+ }
5909
+ else {
5910
+ onSelectionChange([chipKey]);
5911
+ }
5912
+ }
5913
+ else if (selectionMode === 'multiple') {
5914
+ // Toggle in array
5915
+ if (selectedKeys.includes(chipKey)) {
5916
+ onSelectionChange(selectedKeys.filter(k => k !== chipKey));
5917
+ }
5918
+ else {
5919
+ onSelectionChange([...selectedKeys, chipKey]);
5920
+ }
5921
+ }
5922
+ };
5923
+ // Clone children to inject selection props
5924
+ const enhancedChildren = Children.map(children, (child) => {
5925
+ if (!isValidElement(child))
5926
+ return child;
5927
+ const chipKey = child.props.chipKey;
5928
+ if (!chipKey || selectionMode === 'none')
5929
+ return child;
5930
+ const isSelected = selectedKeys.includes(chipKey);
5931
+ return cloneElement(child, {
5932
+ ...child.props,
5933
+ selected: isSelected,
5934
+ onClick: () => {
5935
+ // Call original onClick if exists
5936
+ if (child.props.onClick) {
5937
+ child.props.onClick();
5938
+ }
5939
+ handleChipClick(chipKey);
5940
+ },
5941
+ });
5942
+ });
5943
+ return (jsx("div", { className: `
5944
+ flex
5945
+ ${direction === 'vertical' ? 'flex-col' : 'flex-row'}
5946
+ ${wrap ? 'flex-wrap' : ''}
5947
+ ${gapClasses[gap]}
5948
+ ${className}
5949
+ `, role: selectionMode !== 'none' ? 'group' : undefined, "aria-label": selectionMode !== 'none' ? 'Chip selection group' : undefined, children: enhancedChildren }));
5950
+ }
5183
5951
 
5184
- const sizeClasses$1 = {
5952
+ const sizeClasses$5 = {
5953
+ sm: {
5954
+ item: 'py-1.5 px-2',
5955
+ text: 'text-sm',
5956
+ description: 'text-xs',
5957
+ groupHeader: 'py-1.5 px-2 text-xs',
5958
+ },
5959
+ md: {
5960
+ item: 'py-2 px-3',
5961
+ text: 'text-sm',
5962
+ description: 'text-xs',
5963
+ groupHeader: 'py-2 px-3 text-xs',
5964
+ },
5965
+ lg: {
5966
+ item: 'py-3 px-4',
5967
+ text: 'text-base',
5968
+ description: 'text-sm',
5969
+ groupHeader: 'py-2.5 px-4 text-sm',
5970
+ },
5971
+ };
5972
+ const variantClasses$3 = {
5973
+ default: 'bg-white',
5974
+ bordered: 'bg-white border border-paper-300 rounded-lg',
5975
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
5976
+ };
5977
+ /**
5978
+ * CheckboxList - Multi-select list with checkboxes, grouping, and search
5979
+ *
5980
+ * @example Basic usage
5981
+ * ```tsx
5982
+ * <CheckboxList
5983
+ * items={[
5984
+ * { key: '1', label: 'Option 1' },
5985
+ * { key: '2', label: 'Option 2' },
5986
+ * ]}
5987
+ * selectedKeys={selected}
5988
+ * onSelectionChange={setSelected}
5989
+ * />
5990
+ * ```
5991
+ *
5992
+ * @example With grouping and search
5993
+ * ```tsx
5994
+ * <CheckboxList
5995
+ * items={fields}
5996
+ * selectedKeys={selectedFields}
5997
+ * onSelectionChange={setSelectedFields}
5998
+ * groupLabels={{ table1: 'Users', table2: 'Orders' }}
5999
+ * searchable
6000
+ * searchPlaceholder="Search fields..."
6001
+ * showSelectAll
6002
+ * maxHeight="300px"
6003
+ * />
6004
+ * ```
6005
+ */
6006
+ 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 = '', }) {
6007
+ const [searchTerm, setSearchTerm] = useState('');
6008
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
6009
+ const [internalExpandedGroups, setInternalExpandedGroups] = useState(new Set(defaultExpandedGroups || []));
6010
+ // Debounce search
6011
+ const handleSearchChange = useCallback((value) => {
6012
+ setSearchTerm(value);
6013
+ const timer = setTimeout(() => {
6014
+ setDebouncedSearchTerm(value);
6015
+ }, debounceMs);
6016
+ return () => clearTimeout(timer);
6017
+ }, [debounceMs]);
6018
+ // Filter items based on search
6019
+ const filteredItems = useMemo(() => {
6020
+ if (!debouncedSearchTerm)
6021
+ return items;
6022
+ const term = debouncedSearchTerm.toLowerCase();
6023
+ return items.filter(item => {
6024
+ if (filterFn) {
6025
+ return filterFn(item, debouncedSearchTerm);
6026
+ }
6027
+ return (item.label.toLowerCase().includes(term) ||
6028
+ item.description?.toLowerCase().includes(term) ||
6029
+ item.key.toLowerCase().includes(term));
6030
+ });
6031
+ }, [items, debouncedSearchTerm, filterFn]);
6032
+ // Group items
6033
+ const groupedItems = useMemo(() => {
6034
+ const groups = new Map();
6035
+ filteredItems.forEach(item => {
6036
+ const groupKey = item.group || null;
6037
+ if (!groups.has(groupKey)) {
6038
+ groups.set(groupKey, []);
6039
+ }
6040
+ groups.get(groupKey).push(item);
6041
+ });
6042
+ return groups;
6043
+ }, [filteredItems]);
6044
+ // Determine expanded groups
6045
+ const expandedGroups = controlledExpandedGroups
6046
+ ? new Set(controlledExpandedGroups)
6047
+ : internalExpandedGroups;
6048
+ const handleGroupToggle = (groupKey) => {
6049
+ const newExpanded = !expandedGroups.has(groupKey);
6050
+ if (!controlledExpandedGroups) {
6051
+ setInternalExpandedGroups(prev => {
6052
+ const next = new Set(prev);
6053
+ if (newExpanded) {
6054
+ next.add(groupKey);
6055
+ }
6056
+ else {
6057
+ next.delete(groupKey);
6058
+ }
6059
+ return next;
6060
+ });
6061
+ }
6062
+ onGroupToggle?.(groupKey, newExpanded);
6063
+ };
6064
+ // Selection handlers
6065
+ const handleItemToggle = (key) => {
6066
+ const newSelected = selectedKeys.includes(key)
6067
+ ? selectedKeys.filter(k => k !== key)
6068
+ : [...selectedKeys, key];
6069
+ onSelectionChange(newSelected);
6070
+ };
6071
+ const handleSelectAll = () => {
6072
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6073
+ const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
6074
+ if (allSelected) {
6075
+ // Deselect all filtered items
6076
+ const filteredKeys = new Set(enabledItems.map(item => item.key));
6077
+ onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
6078
+ }
6079
+ else {
6080
+ // Select all filtered items
6081
+ const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
6082
+ onSelectionChange(Array.from(newKeys));
6083
+ }
6084
+ };
6085
+ const sizeStyle = sizeClasses$5[size];
6086
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6087
+ const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
6088
+ const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
6089
+ // Check if we have groups
6090
+ const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
6091
+ return (jsxs("div", { className: `${variantClasses$3[variant]} ${className}`, children: [searchable && (jsx("div", { className: `${sizeStyle.item} border-b border-paper-200`, children: jsx(Input, { value: searchTerm, onChange: (e) => handleSearchChange(e.target.value), placeholder: searchPlaceholder, prefixIcon: jsx(Search, { className: "h-4 w-4" }), size: size === 'lg' ? 'md' : 'sm', clearable: true, onClear: () => {
6092
+ setSearchTerm('');
6093
+ setDebouncedSearchTerm('');
6094
+ } }) })), (showSelectAll || showSelectedCount) && filteredItems.length > 0 && (jsxs("div", { className: `${sizeStyle.item} border-b border-paper-200 flex items-center justify-between`, children: [showSelectAll && (jsx(Checkbox, { checked: allSelected, indeterminate: someSelected, onChange: handleSelectAll, label: selectAllLabel, size: size })), showSelectedCount && (jsxs("span", { className: `${sizeStyle.description} text-ink-500`, children: [selectedKeys.length, " selected"] }))] })), jsxs("div", { className: "overflow-y-auto", style: { maxHeight: maxHeight || undefined }, children: [items.length === 0 && (jsx("div", { className: `${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`, children: emptyMessage })), items.length > 0 && filteredItems.length === 0 && (jsx("div", { className: `${sizeStyle.item} text-ink-500 ${sizeStyle.text} text-center`, children: noResultsMessage })), hasGroups ? (Array.from(groupedItems.entries()).map(([groupKey, groupItems]) => {
6095
+ if (groupKey === null) {
6096
+ // Ungrouped items
6097
+ return groupItems.map(item => (jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key)));
6098
+ }
6099
+ const isExpanded = expandedGroups.has(groupKey);
6100
+ const groupLabel = groupLabels[groupKey] || groupKey;
6101
+ const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
6102
+ return (jsxs("div", { children: [jsxs("button", { type: "button", onClick: () => handleGroupToggle(groupKey), className: `
6103
+ w-full flex items-center justify-between
6104
+ ${sizeStyle.groupHeader}
6105
+ font-medium text-ink-700 bg-paper-50
6106
+ hover:bg-paper-100 transition-colors
6107
+ border-b border-paper-200
6108
+ `, children: [jsxs("div", { className: "flex items-center gap-2", children: [isExpanded ? (jsx(ChevronDown, { className: "h-4 w-4 text-ink-400" })) : (jsx(ChevronRight, { className: "h-4 w-4 text-ink-400" })), jsx("span", { children: groupLabel })] }), jsxs("span", { className: "text-ink-400 font-normal", children: [groupSelectedCount > 0 && `${groupSelectedCount}/`, groupItems.length] })] }), isExpanded && (jsx("div", { children: groupItems.map(item => (jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle, indented: true }, item.key))) }))] }, groupKey));
6109
+ })) : (
6110
+ // Flat list (no groups)
6111
+ filteredItems.map(item => (jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key))))] })] }));
6112
+ }
6113
+ // Helper component for rendering individual items
6114
+ function CheckboxListItemRow({ item, selected, onToggle, size, sizeStyle, indented = false, }) {
6115
+ return (jsx("div", { className: `
6116
+ ${sizeStyle.item}
6117
+ ${indented ? 'pl-8' : ''}
6118
+ hover:bg-paper-50 transition-colors
6119
+ border-b border-paper-100 last:border-b-0
6120
+ ${item.disabled ? 'opacity-50' : ''}
6121
+ `, children: jsxs("div", { className: "flex items-start gap-3", children: [jsx(Checkbox, { checked: selected, onChange: onToggle, disabled: item.disabled, size: size }), jsxs("div", { className: "flex flex-col flex-1 min-w-0", children: [jsx("span", { className: sizeStyle.text, children: item.label }), item.description && (jsx("span", { className: `${sizeStyle.description} text-ink-500`, children: item.description }))] })] }) }));
6122
+ }
6123
+
6124
+ const sizeClasses$4 = {
6125
+ sm: {
6126
+ container: 'text-sm',
6127
+ item: 'py-1.5 px-2',
6128
+ searchPadding: 'p-2',
6129
+ statusPadding: 'px-2 py-1.5',
6130
+ },
6131
+ md: {
6132
+ container: 'text-sm',
6133
+ item: 'py-2 px-3',
6134
+ searchPadding: 'p-3',
6135
+ statusPadding: 'px-3 py-2',
6136
+ },
6137
+ lg: {
6138
+ container: 'text-base',
6139
+ item: 'py-3 px-4',
6140
+ searchPadding: 'p-4',
6141
+ statusPadding: 'px-4 py-2.5',
6142
+ },
6143
+ };
6144
+ const variantClasses$2 = {
6145
+ default: 'bg-white',
6146
+ bordered: 'bg-white border border-paper-300 rounded-lg',
6147
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
6148
+ };
6149
+ /**
6150
+ * SearchableList - List component with integrated search/filter functionality
6151
+ *
6152
+ * @example Basic usage
6153
+ * ```tsx
6154
+ * <SearchableList
6155
+ * items={users.map(u => ({ key: u.id, data: u }))}
6156
+ * renderItem={(item) => <div>{item.data.name}</div>}
6157
+ * onSelect={(item) => setSelectedUser(item.data)}
6158
+ * searchable
6159
+ * searchPlaceholder="Search users..."
6160
+ * />
6161
+ * ```
6162
+ *
6163
+ * @example With custom filter and loading
6164
+ * ```tsx
6165
+ * <SearchableList
6166
+ * items={products}
6167
+ * renderItem={(item, index, isSelected) => (
6168
+ * <div className={isSelected ? 'bg-accent-50' : ''}>
6169
+ * {item.data.name} - ${item.data.price}
6170
+ * </div>
6171
+ * )}
6172
+ * filterFn={(item, term) =>
6173
+ * item.data.name.toLowerCase().includes(term.toLowerCase())
6174
+ * }
6175
+ * loading={isLoading}
6176
+ * loadingMessage="Fetching products..."
6177
+ * maxHeight="400px"
6178
+ * />
6179
+ * ```
6180
+ */
6181
+ 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, }) {
6182
+ const [internalSearchValue, setInternalSearchValue] = useState('');
6183
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
6184
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
6185
+ const listRef = useRef(null);
6186
+ const itemRefs = useRef(new Map());
6187
+ const searchValue = controlledSearchValue !== undefined ? controlledSearchValue : internalSearchValue;
6188
+ // Debounce search
6189
+ useEffect(() => {
6190
+ const timer = setTimeout(() => {
6191
+ setDebouncedSearchTerm(searchValue);
6192
+ }, debounceMs);
6193
+ return () => clearTimeout(timer);
6194
+ }, [searchValue, debounceMs]);
6195
+ const handleSearchChange = useCallback((value) => {
6196
+ if (controlledSearchValue === undefined) {
6197
+ setInternalSearchValue(value);
6198
+ }
6199
+ onSearchChange?.(value);
6200
+ setHighlightedIndex(-1);
6201
+ }, [controlledSearchValue, onSearchChange]);
6202
+ // Filter items based on search
6203
+ const filteredItems = useMemo(() => {
6204
+ if (!debouncedSearchTerm)
6205
+ return items;
6206
+ return items.filter(item => {
6207
+ if (filterFn) {
6208
+ return filterFn(item, debouncedSearchTerm);
6209
+ }
6210
+ // Default filter: check if key includes search term
6211
+ return item.key.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
6212
+ });
6213
+ }, [items, debouncedSearchTerm, filterFn]);
6214
+ // Keyboard navigation
6215
+ const handleKeyDown = useCallback((e) => {
6216
+ if (!enableKeyboardNavigation || filteredItems.length === 0)
6217
+ return;
6218
+ switch (e.key) {
6219
+ case 'ArrowDown':
6220
+ e.preventDefault();
6221
+ setHighlightedIndex(prev => prev < filteredItems.length - 1 ? prev + 1 : 0);
6222
+ break;
6223
+ case 'ArrowUp':
6224
+ e.preventDefault();
6225
+ setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredItems.length - 1);
6226
+ break;
6227
+ case 'Enter':
6228
+ e.preventDefault();
6229
+ if (highlightedIndex >= 0 && highlightedIndex < filteredItems.length) {
6230
+ onSelect?.(filteredItems[highlightedIndex]);
6231
+ }
6232
+ break;
6233
+ case 'Escape':
6234
+ setHighlightedIndex(-1);
6235
+ break;
6236
+ }
6237
+ }, [enableKeyboardNavigation, filteredItems, highlightedIndex, onSelect]);
6238
+ // Scroll highlighted item into view
6239
+ useEffect(() => {
6240
+ if (highlightedIndex >= 0) {
6241
+ const itemEl = itemRefs.current.get(highlightedIndex);
6242
+ if (itemEl) {
6243
+ itemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
6244
+ }
6245
+ }
6246
+ }, [highlightedIndex]);
6247
+ const sizeStyle = sizeClasses$4[size];
6248
+ const resultCountText = formatResultCount
6249
+ ? formatResultCount(filteredItems.length, items.length)
6250
+ : `${filteredItems.length} of ${items.length}`;
6251
+ return (jsxs("div", { className: `${variantClasses$2[variant]} ${sizeStyle.container} ${className}`, onKeyDown: handleKeyDown, children: [jsx("div", { className: `${sizeStyle.searchPadding} border-b border-paper-200`, children: jsx(Input, { value: searchValue, onChange: (e) => handleSearchChange(e.target.value), placeholder: searchPlaceholder, prefixIcon: jsx(Search, { className: "h-4 w-4" }), size: size === 'lg' ? 'md' : 'sm', clearable: true, onClear: () => handleSearchChange(''), autoFocus: autoFocus }) }), showResultCount && items.length > 0 && !loading && (jsx("div", { className: `${sizeStyle.statusPadding} text-ink-500 text-xs border-b border-paper-100`, children: resultCountText })), jsxs("div", { ref: listRef, className: "overflow-y-auto", style: { maxHeight: maxHeight || undefined }, role: "listbox", "aria-activedescendant": highlightedIndex >= 0 ? `item-${highlightedIndex}` : undefined, children: [loading && (jsxs("div", { className: `${sizeStyle.item} flex items-center justify-center gap-2 text-ink-500`, children: [jsx(Loader2, { className: "h-4 w-4 animate-spin" }), jsx("span", { children: loadingMessage })] })), !loading && items.length === 0 && (jsx("div", { className: `${sizeStyle.item} text-ink-500 text-center`, children: emptyMessage })), !loading && items.length > 0 && filteredItems.length === 0 && (jsx("div", { className: `${sizeStyle.item} text-ink-500 text-center`, children: noResultsMessage })), !loading && filteredItems.map((item, index) => {
6252
+ const isSelected = selectedKey === item.key;
6253
+ const isHighlighted = highlightedIndex === index;
6254
+ return (jsx("div", { id: `item-${index}`, ref: (el) => {
6255
+ if (el) {
6256
+ itemRefs.current.set(index, el);
6257
+ }
6258
+ else {
6259
+ itemRefs.current.delete(index);
6260
+ }
6261
+ }, role: "option", "aria-selected": isSelected, onClick: () => onSelect?.(item), className: `
6262
+ ${sizeStyle.item}
6263
+ cursor-pointer transition-colors
6264
+ ${isSelected ? 'bg-accent-50' : ''}
6265
+ ${isHighlighted ? 'bg-paper-100' : ''}
6266
+ ${!isSelected && !isHighlighted ? 'hover:bg-paper-50' : ''}
6267
+ border-b border-paper-100 last:border-b-0
6268
+ `, children: renderItem(item, index, isSelected, isHighlighted) }, item.key));
6269
+ })] })] }));
6270
+ }
6271
+
6272
+ const sizeClasses$3 = {
5185
6273
  sm: {
5186
6274
  input: 'h-8 px-2 text-sm',
5187
6275
  button: 'h-8 w-8',
@@ -5205,7 +6293,7 @@ const NumberInput = forwardRef((props, ref) => {
5205
6293
  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;
5206
6294
  const [internalValue, setInternalValue] = useState(String(value));
5207
6295
  const [isFocused, setIsFocused] = useState(false);
5208
- const sizeStyle = sizeClasses$1[size];
6296
+ const sizeStyle = sizeClasses$3[size];
5209
6297
  // Generate unique IDs for ARIA
5210
6298
  const uniqueId = useId();
5211
6299
  const inputId = id || uniqueId;
@@ -5635,105 +6723,6 @@ class ErrorBoundary extends Component {
5635
6723
  }
5636
6724
  }
5637
6725
 
5638
- const heightPresets = {
5639
- sm: '33vh',
5640
- md: '50vh',
5641
- lg: '75vh',
5642
- full: '90vh',
5643
- };
5644
- function BottomSheet({ isOpen, onClose, children, title, height = 'md', showHandle = true, showCloseButton = true, closeOnOverlayClick = true, closeOnEscape = true, className = '', }) {
5645
- const titleId = useId();
5646
- const [isDragging, setIsDragging] = useState(false);
5647
- const [dragOffset, setDragOffset] = useState(0);
5648
- const [currentHeight] = useState(typeof height === 'string' && height in heightPresets
5649
- ? heightPresets[height]
5650
- : height);
5651
- const sheetRef = useRef(null);
5652
- const startYRef = useRef(0);
5653
- // Close on Escape
5654
- useEffect(() => {
5655
- if (!isOpen || !closeOnEscape)
5656
- return;
5657
- const handleEscape = (e) => {
5658
- if (e.key === 'Escape') {
5659
- onClose();
5660
- }
5661
- };
5662
- document.addEventListener('keydown', handleEscape);
5663
- return () => document.removeEventListener('keydown', handleEscape);
5664
- }, [isOpen, closeOnEscape, onClose]);
5665
- // Prevent body scroll when open
5666
- useEffect(() => {
5667
- if (isOpen) {
5668
- document.body.style.overflow = 'hidden';
5669
- }
5670
- else {
5671
- document.body.style.overflow = '';
5672
- }
5673
- return () => {
5674
- document.body.style.overflow = '';
5675
- };
5676
- }, [isOpen]);
5677
- const handleOverlayClick = (e) => {
5678
- if (closeOnOverlayClick && e.target === e.currentTarget) {
5679
- onClose();
5680
- }
5681
- };
5682
- const handleDragStart = (e) => {
5683
- setIsDragging(true);
5684
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
5685
- startYRef.current = clientY;
5686
- };
5687
- const handleDragMove = (e) => {
5688
- if (!isDragging)
5689
- return;
5690
- const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY;
5691
- const offset = clientY - startYRef.current;
5692
- // Only allow dragging down
5693
- if (offset > 0) {
5694
- setDragOffset(offset);
5695
- }
5696
- };
5697
- const handleDragEnd = () => {
5698
- setIsDragging(false);
5699
- // Close if dragged down more than 150px
5700
- if (dragOffset > 150) {
5701
- onClose();
5702
- }
5703
- setDragOffset(0);
5704
- };
5705
- useEffect(() => {
5706
- if (!isDragging)
5707
- return;
5708
- const handleMove = (e) => handleDragMove(e);
5709
- const handleEnd = () => handleDragEnd();
5710
- document.addEventListener('touchmove', handleMove);
5711
- document.addEventListener('mousemove', handleMove);
5712
- document.addEventListener('touchend', handleEnd);
5713
- document.addEventListener('mouseup', handleEnd);
5714
- return () => {
5715
- document.removeEventListener('touchmove', handleMove);
5716
- document.removeEventListener('mousemove', handleMove);
5717
- document.removeEventListener('touchend', handleEnd);
5718
- document.removeEventListener('mouseup', handleEnd);
5719
- };
5720
- }, [isDragging, dragOffset]);
5721
- if (!isOpen)
5722
- return null;
5723
- return (jsxs("div", { className: "fixed inset-0 z-50 flex items-end", onClick: handleOverlayClick, children: [jsx("div", { className: `
5724
- absolute inset-0 bg-black/50 transition-opacity duration-300
5725
- ${isOpen ? 'opacity-100' : 'opacity-0'}
5726
- ` }), jsxs("div", { ref: sheetRef, className: `
5727
- relative w-full bg-white rounded-t-2xl shadow-2xl
5728
- transition-transform duration-300 ease-out
5729
- ${isOpen ? 'translate-y-0' : 'translate-y-full'}
5730
- ${className}
5731
- `, style: {
5732
- height: currentHeight,
5733
- transform: `translateY(${dragOffset}px)`,
5734
- }, role: "dialog", "aria-modal": "true", "aria-labelledby": title ? titleId : undefined, children: [showHandle && (jsx("div", { className: "py-3 cursor-grab active:cursor-grabbing", onTouchStart: handleDragStart, onMouseDown: handleDragStart, children: jsx("div", { className: "w-12 h-1.5 bg-ink-300 rounded-full mx-auto" }) })), (title || showCloseButton) && (jsxs("div", { className: "px-6 py-4 border-b border-ink-200 flex items-center justify-between", children: [title && (jsx("h2", { id: titleId, className: "text-lg font-semibold text-ink-900", children: title })), showCloseButton && (jsx("button", { onClick: onClose, className: "text-ink-400 hover:text-ink-600 transition-colors ml-auto", "aria-label": "Close", children: jsx(X, { className: "h-5 w-5" }) }))] })), jsx("div", { className: "overflow-y-auto flex-1 p-6", children: children })] })] }));
5735
- }
5736
-
5737
6726
  function Collapsible({ trigger, children, defaultOpen = false, open: controlledOpen, onOpenChange, disabled = false, showIcon = true, className = '', triggerClassName = '', contentClassName = '', }) {
5738
6727
  const isControlled = controlledOpen !== undefined;
5739
6728
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
@@ -5783,6 +6772,231 @@ function Collapsible({ trigger, children, defaultOpen = false, open: controlledO
5783
6772
  ` }))] }), 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: jsx("div", { className: contentClassName, children: children }) })] }));
5784
6773
  }
5785
6774
 
6775
+ const sizeClasses$2 = {
6776
+ sm: {
6777
+ header: 'h-10 px-3',
6778
+ text: 'text-sm',
6779
+ icon: 'h-4 w-4',
6780
+ },
6781
+ md: {
6782
+ header: 'h-12 px-4',
6783
+ text: 'text-sm',
6784
+ icon: 'h-5 w-5',
6785
+ },
6786
+ lg: {
6787
+ header: 'h-14 px-5',
6788
+ text: 'text-base',
6789
+ icon: 'h-5 w-5',
6790
+ },
6791
+ };
6792
+ const variantClasses$1 = {
6793
+ default: {
6794
+ container: 'bg-white border-ink-200',
6795
+ header: 'bg-paper-50',
6796
+ },
6797
+ elevated: {
6798
+ container: 'bg-white shadow-lg border-ink-200',
6799
+ header: 'bg-white',
6800
+ },
6801
+ bordered: {
6802
+ container: 'bg-white border-2 border-ink-300',
6803
+ header: 'bg-paper-100',
6804
+ },
6805
+ };
6806
+ /**
6807
+ * ExpandablePanel - A panel that sticks to the bottom (or top) and can expand/collapse
6808
+ *
6809
+ * For bottom position: expands UPWARD (content appears above header)
6810
+ * For top position: expands DOWNWARD (content appears below header)
6811
+ *
6812
+ * Two modes of operation:
6813
+ * - `viewport`: Fixed to the viewport (for standalone pages, covers StatusBar)
6814
+ * - `container`: Sticky within its parent container (for use inside Page/AppLayout, respects StatusBar)
6815
+ *
6816
+ * @example Basic usage (viewport mode - full page)
6817
+ * ```tsx
6818
+ * <ExpandablePanel
6819
+ * mode="viewport"
6820
+ * collapsedContent={<Text>3 items selected</Text>}
6821
+ * expandedHeight="300px"
6822
+ * >
6823
+ * {content}
6824
+ * </ExpandablePanel>
6825
+ * ```
6826
+ *
6827
+ * @example Inside Page/AppLayout (container mode - respects StatusBar)
6828
+ * ```tsx
6829
+ * <Page>
6830
+ * <ExpandablePanelContainer>
6831
+ * <div className="flex-1 overflow-auto">
6832
+ * {pageContent}
6833
+ * </div>
6834
+ * <ExpandablePanel
6835
+ * mode="container"
6836
+ * collapsedContent={<Text>3 items selected</Text>}
6837
+ * expandedHeight="300px"
6838
+ * >
6839
+ * {selectedItemsContent}
6840
+ * </ExpandablePanel>
6841
+ * </ExpandablePanelContainer>
6842
+ * </Page>
6843
+ * ```
6844
+ *
6845
+ * @example With maxWidth to match page content
6846
+ * ```tsx
6847
+ * <ExpandablePanel
6848
+ * mode="container"
6849
+ * maxWidth="1400px"
6850
+ * collapsedContent={<Text>Generated SQL</Text>}
6851
+ * expandedHeight="300px"
6852
+ * >
6853
+ * {sqlContent}
6854
+ * </ExpandablePanel>
6855
+ * ```
6856
+ */
6857
+ 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, }) {
6858
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
6859
+ // Determine if controlled or uncontrolled
6860
+ const isControlled = controlledExpanded !== undefined;
6861
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
6862
+ const setExpanded = (value) => {
6863
+ if (!isControlled) {
6864
+ setInternalExpanded(value);
6865
+ }
6866
+ onExpandedChange?.(value);
6867
+ };
6868
+ const toggleExpanded = () => {
6869
+ setExpanded(!expanded);
6870
+ };
6871
+ // Close on Escape
6872
+ useEffect(() => {
6873
+ if (!closeOnEscape || !expanded)
6874
+ return;
6875
+ const handleEscape = (e) => {
6876
+ if (e.key === 'Escape') {
6877
+ setExpanded(false);
6878
+ }
6879
+ };
6880
+ document.addEventListener('keydown', handleEscape);
6881
+ return () => document.removeEventListener('keydown', handleEscape);
6882
+ }, [closeOnEscape, expanded]);
6883
+ const sizeStyle = sizeClasses$2[size];
6884
+ const variantStyle = variantClasses$1[variant];
6885
+ const heightValue = typeof expandedHeight === 'number' ? `${expandedHeight}px` : expandedHeight;
6886
+ const maxWidthValue = maxWidth
6887
+ ? (typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)
6888
+ : undefined;
6889
+ // Position classes differ based on mode
6890
+ const getPositionClasses = () => {
6891
+ if (mode === 'viewport') {
6892
+ // Fixed to viewport
6893
+ return position === 'bottom'
6894
+ ? 'fixed bottom-0 left-0 right-0'
6895
+ : 'fixed top-0 left-0 right-0';
6896
+ }
6897
+ else {
6898
+ // Absolute positioning within container - snaps to bottom
6899
+ return position === 'bottom'
6900
+ ? 'absolute bottom-0 left-0 right-0'
6901
+ : 'absolute top-0 left-0 right-0';
6902
+ }
6903
+ };
6904
+ // For bottom panel, we want chevron up to expand (reveal content above)
6905
+ // For top panel, we want chevron down to expand (reveal content below)
6906
+ const ChevronIcon = position === 'bottom'
6907
+ ? (expanded ? ChevronDown : ChevronUp)
6908
+ : (expanded ? ChevronUp : ChevronDown);
6909
+ // Header component
6910
+ const header = (jsxs("div", { className: `
6911
+ flex items-center justify-between
6912
+ ${sizeStyle.header}
6913
+ ${variantStyle.header}
6914
+ border-ink-200
6915
+ flex-shrink-0
6916
+ ${headerClassName}
6917
+ `, children: [jsx("div", { className: `flex-1 flex items-center ${sizeStyle.text}`, children: collapsedContent }), jsxs("div", { className: "flex items-center gap-2", children: [headerActions, showToggle && (jsx("button", { type: "button", onClick: toggleExpanded, className: `
6918
+ flex items-center justify-center
6919
+ p-1.5 rounded-md
6920
+ text-ink-500 hover:text-ink-700
6921
+ hover:bg-ink-100
6922
+ transition-colors
6923
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1
6924
+ `, "aria-expanded": expanded, "aria-label": expanded ? 'Collapse panel' : 'Expand panel', children: toggleContent || jsx(ChevronIcon, { className: sizeStyle.icon }) }))] })] }));
6925
+ // Content component
6926
+ const content = (jsx("div", { className: `
6927
+ overflow-hidden
6928
+ transition-all duration-300 ease-in-out
6929
+ `, style: {
6930
+ maxHeight: expanded ? heightValue : '0px',
6931
+ opacity: expanded ? 1 : 0,
6932
+ }, children: jsx("div", { className: `
6933
+ overflow-y-auto p-4
6934
+ ${contentClassName}
6935
+ `, style: { maxHeight: heightValue }, children: children }) }));
6936
+ // Build container styles
6937
+ const containerStyle = {
6938
+ ...(mode === 'viewport' ? { zIndex } : {}),
6939
+ ...(maxWidthValue ? {
6940
+ maxWidth: maxWidthValue,
6941
+ marginLeft: 'auto',
6942
+ marginRight: 'auto'
6943
+ } : {}),
6944
+ };
6945
+ return (jsx("div", { className: `
6946
+ ${getPositionClasses()}
6947
+ ${variantStyle.container}
6948
+ border-t rounded-t-lg
6949
+ transition-all duration-300 ease-in-out
6950
+ flex flex-col
6951
+ ${className}
6952
+ `, style: containerStyle, children: position === 'bottom' ? (jsxs(Fragment, { children: [content, header] })) : (jsxs(Fragment, { children: [header, content] })) }));
6953
+ }
6954
+ /**
6955
+ * ExpandablePanelSpacer - Adds spacing to prevent content from being hidden behind the panel
6956
+ * Only needed in viewport mode. In container mode, the panel is part of the flex layout.
6957
+ *
6958
+ * @example
6959
+ * ```tsx
6960
+ * <div>
6961
+ * <MainContent />
6962
+ * <ExpandablePanelSpacer size="md" />
6963
+ * </div>
6964
+ * <ExpandablePanel mode="viewport" position="bottom" size="md" {...props} />
6965
+ * ```
6966
+ */
6967
+ function ExpandablePanelSpacer({ size = 'md' }) {
6968
+ const heights = {
6969
+ sm: 'h-10',
6970
+ md: 'h-12',
6971
+ lg: 'h-14',
6972
+ };
6973
+ return jsx("div", { className: heights[size] });
6974
+ }
6975
+ /**
6976
+ * ExpandablePanelContainer - Wrapper that sets up proper layout for container mode
6977
+ * Use this to wrap your page content when using ExpandablePanel with mode="container"
6978
+ *
6979
+ * This creates a relative container with full height so the panel can position absolutely
6980
+ * at the bottom while the content scrolls above it.
6981
+ *
6982
+ * @example
6983
+ * ```tsx
6984
+ * <Page>
6985
+ * <ExpandablePanelContainer>
6986
+ * <div className="flex-1 overflow-auto p-4">
6987
+ * {pageContent}
6988
+ * </div>
6989
+ * <ExpandablePanel mode="container" {...props}>
6990
+ * {panelContent}
6991
+ * </ExpandablePanel>
6992
+ * </ExpandablePanelContainer>
6993
+ * </Page>
6994
+ * ```
6995
+ */
6996
+ function ExpandablePanelContainer({ children, className = '', }) {
6997
+ return (jsx("div", { className: `relative h-full overflow-hidden ${className}`, children: children }));
6998
+ }
6999
+
5786
7000
  // Tailwind breakpoint mappings
5787
7001
  // sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
5788
7002
  /**
@@ -5951,26 +7165,6 @@ function Hide({ children, above, below, only, className = '' }) {
5951
7165
  }
5952
7166
  return (jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
5953
7167
  }
5954
- /**
5955
- * useMediaQuery hook - React hook for responsive logic in components
5956
- *
5957
- * @example
5958
- * const isMobile = useMediaQuery('(max-width: 768px)');
5959
- * const isDesktop = useMediaQuery('(min-width: 1024px)');
5960
- */
5961
- function useMediaQuery(query) {
5962
- const [matches, setMatches] = React__default.useState(false);
5963
- React__default.useEffect(() => {
5964
- const media = window.matchMedia(query);
5965
- if (media.matches !== matches) {
5966
- setMatches(media.matches);
5967
- }
5968
- const listener = () => setMatches(media.matches);
5969
- media.addEventListener('change', listener);
5970
- return () => media.removeEventListener('change', listener);
5971
- }, [matches, query]);
5972
- return matches;
5973
- }
5974
7168
 
5975
7169
  function Breadcrumbs({ items, showHome = true }) {
5976
7170
  return (jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center space-x-2 text-sm", children: [showHome && (jsxs(Fragment, { children: [jsx(Link$1, { to: "/", className: "text-ink-500 hover:text-ink-900 transition-colors", "aria-label": "Home", children: jsx(Home, { className: "h-4 w-4" }) }), items.length > 0 && jsx(ChevronRight, { className: "h-4 w-4 text-ink-400" })] })), items.map((item, index) => {
@@ -6728,15 +7922,692 @@ function SidebarGroup({ title, items, onNavigate, defaultExpanded = true, curren
6728
7922
  }
6729
7923
  return (jsxs("div", { children: [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: [jsx("span", { children: title }), isExpanded ? (jsx(ChevronDown, { className: "h-4 w-4" })) : (jsx(ChevronRight, { className: "h-4 w-4" }))] }), isExpanded && (jsx("div", { className: "mt-1 space-y-1", children: items.map((item) => (jsx(SidebarNavItem, { item: item, onNavigate: onNavigate, currentPath: currentPath }, item.id))) }))] }));
6730
7924
  }
6731
- function Sidebar({ items, onNavigate, className = '', header, footer, currentPath }) {
6732
- return (jsxs("div", { className: `flex flex-col h-full bg-white border-r border-paper-300 notebook-binding ${className}`, children: [header && (jsx("div", { className: "px-6 pt-6 pb-4", children: header })), jsx("nav", { className: "flex-1 px-3 py-2 space-y-1 overflow-y-auto", children: items.map((item) => {
7925
+ /**
7926
+ * Sidebar - Navigation sidebar with mobile drawer support
7927
+ *
7928
+ * On desktop: Renders as a fixed-width sidebar
7929
+ * On mobile: Renders as a drawer overlay when mobileOpen is true
7930
+ *
7931
+ * @example Desktop usage (no mobile props)
7932
+ * ```tsx
7933
+ * <Sidebar
7934
+ * items={navItems}
7935
+ * header={<Logo />}
7936
+ * footer={<UserProfile />}
7937
+ * currentPath={location.pathname}
7938
+ * onNavigate={(href) => navigate(href)}
7939
+ * />
7940
+ * ```
7941
+ *
7942
+ * @example With mobile drawer support
7943
+ * ```tsx
7944
+ * const [mobileOpen, setMobileOpen] = useState(false);
7945
+ *
7946
+ * <Sidebar
7947
+ * items={navItems}
7948
+ * header={<Logo />}
7949
+ * mobileOpen={mobileOpen}
7950
+ * onMobileClose={() => setMobileOpen(false)}
7951
+ * onNavigate={(href) => {
7952
+ * navigate(href);
7953
+ * setMobileOpen(false); // Close drawer on navigation
7954
+ * }}
7955
+ * />
7956
+ * ```
7957
+ */
7958
+ function Sidebar({ items, onNavigate, className = '', header, footer, currentPath, mobileOpen, onMobileClose, width = 'w-64', }) {
7959
+ const sidebarRef = useRef(null);
7960
+ const [isAnimating, setIsAnimating] = useState(false);
7961
+ const [shouldRender, setShouldRender] = useState(mobileOpen);
7962
+ // Handle animation states for mobile drawer
7963
+ useEffect(() => {
7964
+ if (mobileOpen) {
7965
+ setShouldRender(true);
7966
+ // Small delay to trigger animation
7967
+ requestAnimationFrame(() => {
7968
+ setIsAnimating(true);
7969
+ });
7970
+ return; // No cleanup needed when opening
7971
+ }
7972
+ else {
7973
+ setIsAnimating(false);
7974
+ // Wait for animation to complete before unmounting
7975
+ const timer = setTimeout(() => {
7976
+ setShouldRender(false);
7977
+ }, 300);
7978
+ return () => clearTimeout(timer);
7979
+ }
7980
+ }, [mobileOpen]);
7981
+ // Handle escape key for mobile drawer
7982
+ useEffect(() => {
7983
+ if (!mobileOpen)
7984
+ return;
7985
+ const handleEscape = (e) => {
7986
+ if (e.key === 'Escape') {
7987
+ onMobileClose?.();
7988
+ }
7989
+ };
7990
+ document.addEventListener('keydown', handleEscape);
7991
+ return () => document.removeEventListener('keydown', handleEscape);
7992
+ }, [mobileOpen, onMobileClose]);
7993
+ // Lock body scroll when mobile drawer is open
7994
+ useEffect(() => {
7995
+ if (mobileOpen) {
7996
+ document.body.style.overflow = 'hidden';
7997
+ }
7998
+ else {
7999
+ document.body.style.overflow = '';
8000
+ }
8001
+ return () => {
8002
+ document.body.style.overflow = '';
8003
+ };
8004
+ }, [mobileOpen]);
8005
+ // Handle navigation with auto-close on mobile
8006
+ const handleNavigate = (href, external) => {
8007
+ onNavigate?.(href, external);
8008
+ // Auto-close mobile drawer on navigation
8009
+ if (mobileOpen) {
8010
+ onMobileClose?.();
8011
+ }
8012
+ };
8013
+ // Sidebar content (shared between desktop and mobile)
8014
+ const sidebarContent = (jsxs("div", { ref: sidebarRef, className: `flex flex-col h-full bg-white border-r border-paper-300 notebook-binding ${width} ${className}`, children: [mobileOpen !== undefined && (jsxs("div", { className: "flex items-center justify-between px-4 pt-4 md:hidden", children: [jsx("div", { className: "flex-1", children: header }), 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: jsx(X, { className: "w-5 h-5" }) })] })), header && mobileOpen === undefined && (jsx("div", { className: "px-6 pt-6 pb-4", children: header })), header && mobileOpen !== undefined && (jsx("div", { className: "px-6 pt-2 pb-4 hidden md:block", children: header })), jsx("nav", { className: "flex-1 px-3 py-2 space-y-1 overflow-y-auto", children: items.map((item) => {
6733
8015
  // Render separator
6734
8016
  if (item.separator) {
6735
8017
  return jsx("div", { className: "my-4 border-t border-paper-300" }, item.id);
6736
8018
  }
6737
8019
  // Render nav item
6738
- return (jsx(SidebarNavItem, { item: item, onNavigate: onNavigate, currentPath: currentPath }, item.id));
8020
+ return (jsx(SidebarNavItem, { item: item, onNavigate: handleNavigate, currentPath: currentPath }, item.id));
6739
8021
  }) }), footer && (jsx("div", { className: "border-t border-paper-300 pl-2 pr-6 py-4 overflow-visible", children: footer }))] }));
8022
+ // If mobileOpen is not defined, render as regular sidebar (desktop mode)
8023
+ if (mobileOpen === undefined) {
8024
+ return sidebarContent;
8025
+ }
8026
+ // Mobile drawer mode
8027
+ if (!shouldRender) {
8028
+ return null;
8029
+ }
8030
+ return createPortal(jsxs("div", { className: "fixed inset-0 z-50 md:hidden", role: "dialog", "aria-modal": "true", "aria-label": "Navigation menu", children: [jsx("div", { className: `
8031
+ absolute inset-0 bg-ink-900/50 backdrop-blur-sm
8032
+ transition-opacity duration-300
8033
+ ${isAnimating ? 'opacity-100' : 'opacity-0'}
8034
+ `, onClick: onMobileClose, "aria-hidden": "true" }), jsx("div", { className: `
8035
+ absolute inset-y-0 left-0 flex max-w-full
8036
+ transition-transform duration-300 ease-out
8037
+ ${isAnimating ? 'translate-x-0' : '-translate-x-full'}
8038
+ `, children: sidebarContent })] }), document.body);
8039
+ }
8040
+
8041
+ /**
8042
+ * BottomNavigation - Mobile-style bottom tab bar
8043
+ *
8044
+ * iOS/Android-style fixed bottom navigation with icons, labels, and badges.
8045
+ * Handles safe area insets for notched devices automatically.
8046
+ *
8047
+ * Best practices:
8048
+ * - Use 3-5 items maximum
8049
+ * - Keep labels short (1-2 words)
8050
+ * - Use consistent icon style
8051
+ *
8052
+ * @example Basic usage
8053
+ * ```tsx
8054
+ * import { BottomNavigation } from 'notebook-ui';
8055
+ * import { Home, Search, Bell, User } from 'lucide-react';
8056
+ *
8057
+ * const navItems = [
8058
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8059
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8060
+ * { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3 },
8061
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' },
8062
+ * ];
8063
+ *
8064
+ * <BottomNavigation
8065
+ * items={navItems}
8066
+ * activeId="home"
8067
+ * onNavigate={(id, href) => navigate(href)}
8068
+ * />
8069
+ * ```
8070
+ *
8071
+ * @example With onClick handlers
8072
+ * ```tsx
8073
+ * const navItems = [
8074
+ * { id: 'home', label: 'Home', icon: <Home />, onClick: () => setTab('home') },
8075
+ * { id: 'add', label: 'Add', icon: <Plus />, onClick: openAddModal },
8076
+ * ];
8077
+ *
8078
+ * <BottomNavigation items={navItems} activeId={currentTab} />
8079
+ * ```
8080
+ */
8081
+ function BottomNavigation({ items, activeId, onNavigate, showLabels = true, className = '', safeArea = true, }) {
8082
+ const handleItemClick = (item) => {
8083
+ if (item.disabled)
8084
+ return;
8085
+ if (item.onClick) {
8086
+ item.onClick();
8087
+ }
8088
+ if (onNavigate) {
8089
+ onNavigate(item.id, item.href);
8090
+ }
8091
+ };
8092
+ return (jsx("nav", { className: `
8093
+ fixed bottom-0 left-0 right-0 z-40
8094
+ bg-white border-t border-paper-200 shadow-lg
8095
+ ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}
8096
+ ${className}
8097
+ `, role: "navigation", "aria-label": "Bottom navigation", children: jsx("div", { className: "flex items-center justify-around h-14 max-w-lg mx-auto px-2", children: items.map((item) => {
8098
+ const isActive = item.id === activeId;
8099
+ return (jsxs("button", { onClick: () => handleItemClick(item), disabled: item.disabled, className: `
8100
+ relative flex flex-col items-center justify-center
8101
+ flex-1 h-full min-w-touch-sm
8102
+ transition-colors duration-200
8103
+ ${item.disabled
8104
+ ? 'opacity-40 cursor-not-allowed'
8105
+ : 'active:bg-paper-100'}
8106
+ ${isActive
8107
+ ? 'text-accent-600'
8108
+ : 'text-ink-500 hover:text-ink-700'}
8109
+ `, "aria-current": isActive ? 'page' : undefined, "aria-label": item.label, children: [jsxs("div", { className: "relative", children: [jsx("div", { className: `
8110
+ w-6 h-6 flex items-center justify-center
8111
+ transition-transform duration-200
8112
+ ${isActive ? 'scale-110' : 'scale-100'}
8113
+ `, children: React__default.isValidElement(item.icon)
8114
+ ? React__default.cloneElement(item.icon, {
8115
+ className: 'w-6 h-6',
8116
+ })
8117
+ : item.icon }), item.badge !== undefined && item.badge > 0 && (jsx("span", { className: `
8118
+ absolute -top-1 -right-2.5
8119
+ min-w-[18px] h-[18px] px-1
8120
+ flex items-center justify-center
8121
+ text-[10px] font-bold text-white
8122
+ bg-error-500 rounded-full
8123
+ ${item.badge > 99 ? 'text-[8px]' : ''}
8124
+ `, children: item.badge > 99 ? '99+' : item.badge }))] }), showLabels && (jsx("span", { className: `
8125
+ mt-1 text-[10px] font-medium leading-none
8126
+ transition-opacity duration-200
8127
+ truncate max-w-full px-1
8128
+ ${isActive ? 'opacity-100' : 'opacity-70'}
8129
+ `, children: item.label })), isActive && (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));
8130
+ }) }) }));
8131
+ }
8132
+ /**
8133
+ * BottomNavigationSpacer - Spacer to prevent content from being hidden behind BottomNavigation
8134
+ *
8135
+ * Place this at the bottom of your scrollable content when using BottomNavigation.
8136
+ *
8137
+ * @example
8138
+ * ```tsx
8139
+ * <div className="flex flex-col h-screen">
8140
+ * <main className="flex-1 overflow-auto">
8141
+ * {/* Your content *\/}
8142
+ * <BottomNavigationSpacer />
8143
+ * </main>
8144
+ * <BottomNavigation items={navItems} />
8145
+ * </div>
8146
+ * ```
8147
+ */
8148
+ function BottomNavigationSpacer({ safeArea = true }) {
8149
+ return (jsx("div", { className: `h-14 ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}`, "aria-hidden": "true" }));
8150
+ }
8151
+
8152
+ /**
8153
+ * MobileHeader - Mobile app header with navigation controls
8154
+ *
8155
+ * A flexible mobile header component with support for:
8156
+ * - Hamburger menu button (default)
8157
+ * - Back navigation arrow
8158
+ * - Close button (X)
8159
+ * - Custom left/right actions
8160
+ * - Sticky positioning
8161
+ * - Blur/transparent variants
8162
+ *
8163
+ * @example Basic with menu button
8164
+ * ```tsx
8165
+ * <MobileHeader
8166
+ * title="Dashboard"
8167
+ * onMenuClick={() => setDrawerOpen(true)}
8168
+ * />
8169
+ * ```
8170
+ *
8171
+ * @example With back button
8172
+ * ```tsx
8173
+ * <MobileHeader
8174
+ * title="User Details"
8175
+ * subtitle="Profile"
8176
+ * onBackClick={() => navigate(-1)}
8177
+ * />
8178
+ * ```
8179
+ *
8180
+ * @example With right action
8181
+ * ```tsx
8182
+ * <MobileHeader
8183
+ * title="Settings"
8184
+ * onMenuClick={openMenu}
8185
+ * rightAction={
8186
+ * <Button variant="ghost" iconOnly onClick={save}>
8187
+ * <Check className="w-5 h-5" />
8188
+ * </Button>
8189
+ * }
8190
+ * />
8191
+ * ```
8192
+ *
8193
+ * @example Transparent with blur
8194
+ * ```tsx
8195
+ * <MobileHeader
8196
+ * title="Photo Gallery"
8197
+ * variant="blur"
8198
+ * onBackClick={goBack}
8199
+ * />
8200
+ * ```
8201
+ */
8202
+ function MobileHeader({ title, subtitle, onMenuClick, onBackClick, onCloseClick, rightAction, leftAction, sticky = true, bordered = true, variant = 'solid', className = '', safeArea = true, }) {
8203
+ // Determine which left button to show
8204
+ const renderLeftButton = () => {
8205
+ // Custom left action takes priority
8206
+ if (leftAction) {
8207
+ return leftAction;
8208
+ }
8209
+ // Close button
8210
+ if (onCloseClick) {
8211
+ return (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: jsx(X, { className: "w-6 h-6" }) }));
8212
+ }
8213
+ // Back button
8214
+ if (onBackClick) {
8215
+ return (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: jsx(ChevronLeft, { className: "w-6 h-6" }) }));
8216
+ }
8217
+ // Menu button (default)
8218
+ if (onMenuClick) {
8219
+ return (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: jsx(Menu$1, { className: "w-6 h-6" }) }));
8220
+ }
8221
+ // No left button
8222
+ return jsx("div", { className: "w-10 h-10" });
8223
+ };
8224
+ // Background variant styles
8225
+ const variantStyles = {
8226
+ solid: 'bg-white',
8227
+ transparent: 'bg-transparent',
8228
+ blur: 'bg-white/80 backdrop-blur-md',
8229
+ };
8230
+ return (jsx("header", { className: `
8231
+ ${sticky ? 'sticky top-0 z-30' : ''}
8232
+ ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}
8233
+ ${variantStyles[variant]}
8234
+ ${bordered ? 'border-b border-paper-200' : ''}
8235
+ ${className}
8236
+ `, children: jsxs("div", { className: "flex items-center justify-between h-14 px-4", children: [jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [renderLeftButton(), jsxs("div", { className: "flex flex-col min-w-0 flex-1", children: [subtitle && (jsx("span", { className: "text-xs text-ink-500 truncate", children: subtitle })), jsx("h1", { className: "text-lg font-semibold text-ink-900 truncate leading-tight", children: title })] })] }), jsx("div", { className: "flex items-center gap-1 ml-2 flex-shrink-0", children: rightAction })] }) }));
8237
+ }
8238
+ /**
8239
+ * MobileHeaderSpacer - Spacer to prevent content from being hidden behind sticky MobileHeader
8240
+ *
8241
+ * Place this at the top of your content when NOT using sticky header
8242
+ * to maintain consistent spacing.
8243
+ *
8244
+ * @example
8245
+ * ```tsx
8246
+ * <MobileHeader title="Page" sticky={false} />
8247
+ * <MobileHeaderSpacer />
8248
+ * <main>Content here</main>
8249
+ * ```
8250
+ */
8251
+ function MobileHeaderSpacer({ safeArea = true }) {
8252
+ return (jsx("div", { className: `h-14 ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}`, "aria-hidden": "true" }));
8253
+ }
8254
+
8255
+ const positionClasses = {
8256
+ 'bottom-right': 'right-4 bottom-4',
8257
+ 'bottom-left': 'left-4 bottom-4',
8258
+ 'bottom-center': 'left-1/2 -translate-x-1/2 bottom-4',
8259
+ };
8260
+ const variantClasses = {
8261
+ primary: 'bg-accent-600 hover:bg-accent-700 text-white shadow-lg',
8262
+ secondary: 'bg-white hover:bg-paper-50 text-ink-700 shadow-lg border border-paper-200',
8263
+ accent: 'bg-accent-500 hover:bg-accent-600 text-white shadow-lg',
8264
+ };
8265
+ const sizeClasses$1 = {
8266
+ md: 'w-14 h-14',
8267
+ lg: 'w-16 h-16',
8268
+ };
8269
+ const iconSizeClasses = {
8270
+ md: 'h-6 w-6',
8271
+ lg: 'h-7 w-7',
8272
+ };
8273
+ /**
8274
+ * FloatingActionButton - Material Design style FAB for mobile
8275
+ *
8276
+ * A prominent button for the primary action on a screen.
8277
+ * Supports single action or expandable menu with multiple actions.
8278
+ *
8279
+ * @example Simple FAB
8280
+ * ```tsx
8281
+ * <FloatingActionButton
8282
+ * onClick={() => openCreateModal()}
8283
+ * label="Create new item"
8284
+ * />
8285
+ * ```
8286
+ *
8287
+ * @example FAB with action menu
8288
+ * ```tsx
8289
+ * <FloatingActionButton
8290
+ * actions={[
8291
+ * { id: 'photo', icon: <Camera />, label: 'Take Photo', onClick: takePhoto },
8292
+ * { id: 'upload', icon: <Upload />, label: 'Upload File', onClick: uploadFile },
8293
+ * { id: 'note', icon: <FileText />, label: 'Create Note', onClick: createNote },
8294
+ * ]}
8295
+ * />
8296
+ * ```
8297
+ *
8298
+ * @example Extended FAB
8299
+ * ```tsx
8300
+ * <FloatingActionButton
8301
+ * extended
8302
+ * extendedLabel="New Task"
8303
+ * icon={<Plus />}
8304
+ * onClick={createTask}
8305
+ * />
8306
+ * ```
8307
+ */
8308
+ function FloatingActionButton({ onClick, icon, actions, position = 'bottom-right', variant = 'primary', size = 'md', label = 'Action button', extended = false, extendedLabel, hidden = false, offset, className = '', }) {
8309
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
8310
+ const fabRef = useRef(null);
8311
+ const hasMenu = actions && actions.length > 0;
8312
+ // Close menu on escape
8313
+ useEffect(() => {
8314
+ if (!isMenuOpen)
8315
+ return;
8316
+ const handleEscape = (e) => {
8317
+ if (e.key === 'Escape') {
8318
+ setIsMenuOpen(false);
8319
+ }
8320
+ };
8321
+ document.addEventListener('keydown', handleEscape);
8322
+ return () => document.removeEventListener('keydown', handleEscape);
8323
+ }, [isMenuOpen]);
8324
+ // Close menu on click outside
8325
+ useEffect(() => {
8326
+ if (!isMenuOpen)
8327
+ return;
8328
+ const handleClickOutside = (e) => {
8329
+ if (fabRef.current && !fabRef.current.contains(e.target)) {
8330
+ setIsMenuOpen(false);
8331
+ }
8332
+ };
8333
+ document.addEventListener('mousedown', handleClickOutside);
8334
+ return () => document.removeEventListener('mousedown', handleClickOutside);
8335
+ }, [isMenuOpen]);
8336
+ const handleClick = () => {
8337
+ if (hasMenu) {
8338
+ setIsMenuOpen(!isMenuOpen);
8339
+ }
8340
+ else if (onClick) {
8341
+ onClick();
8342
+ }
8343
+ };
8344
+ const handleActionClick = (action) => {
8345
+ if (!action.disabled) {
8346
+ action.onClick();
8347
+ setIsMenuOpen(false);
8348
+ }
8349
+ };
8350
+ // Custom offset styles
8351
+ const offsetStyle = offset ? {
8352
+ ...(offset.x !== undefined && position.includes('right') ? { right: `${offset.x}px` } : {}),
8353
+ ...(offset.x !== undefined && position.includes('left') ? { left: `${offset.x}px` } : {}),
8354
+ ...(offset.y !== undefined ? { bottom: `${offset.y}px` } : {}),
8355
+ } : {};
8356
+ const fabContent = (jsxs("div", { className: `
8357
+ fixed z-40 transition-all duration-300
8358
+ ${positionClasses[position]}
8359
+ ${hidden ? 'translate-y-20 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
8360
+ ${className}
8361
+ `, style: {
8362
+ ...offsetStyle,
8363
+ paddingBottom: 'env(safe-area-inset-bottom)',
8364
+ }, children: [hasMenu && isMenuOpen && (jsx("div", { className: "absolute bottom-full mb-3 flex flex-col-reverse gap-3 items-center", children: actions.map((action, index) => (jsxs("div", { className: "flex items-center gap-3 animate-fade-in", style: { animationDelay: `${index * 50}ms` }, children: [jsx("span", { className: "bg-ink-900/80 text-white text-sm px-3 py-1.5 rounded-lg whitespace-nowrap", children: action.label }), jsx("button", { onClick: () => handleActionClick(action), disabled: action.disabled, className: `
8365
+ w-12 h-12 rounded-full flex items-center justify-center
8366
+ transition-all duration-200
8367
+ ${action.disabled
8368
+ ? 'bg-paper-200 text-ink-400 cursor-not-allowed'
8369
+ : 'bg-white text-ink-700 shadow-lg hover:bg-paper-50 active:scale-95'}
8370
+ `, "aria-label": action.label, children: action.icon })] }, action.id))) })), hasMenu && isMenuOpen && (jsx("div", { className: "fixed inset-0 bg-black/20 -z-10 animate-fade-in", onClick: () => setIsMenuOpen(false) })), jsxs("button", { ref: fabRef, onClick: handleClick, className: `
8371
+ ${extended ? 'px-6 rounded-full' : 'rounded-full'}
8372
+ ${extended ? 'h-14' : sizeClasses$1[size]}
8373
+ ${variantClasses[variant]}
8374
+ flex items-center justify-center gap-2
8375
+ transition-all duration-200
8376
+ active:scale-95
8377
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
8378
+ `, "aria-label": label, "aria-expanded": hasMenu ? isMenuOpen : undefined, "aria-haspopup": hasMenu ? 'menu' : undefined, children: [hasMenu && isMenuOpen ? (jsx(X, { className: iconSizeClasses[size] })) : (icon || jsx(Plus, { className: iconSizeClasses[size] })), extended && extendedLabel && (jsx("span", { className: "font-medium", children: extendedLabel }))] })] }));
8379
+ // Render via portal to ensure proper stacking
8380
+ return createPortal(fabContent, document.body);
8381
+ }
8382
+ /**
8383
+ * Hook for scroll-based FAB visibility
8384
+ *
8385
+ * @example
8386
+ * ```tsx
8387
+ * const { hidden, scrollDirection } = useFABScroll();
8388
+ * <FloatingActionButton hidden={hidden} />
8389
+ * ```
8390
+ */
8391
+ function useFABScroll(threshold = 10) {
8392
+ const [hidden, setHidden] = useState(false);
8393
+ const [scrollDirection, setScrollDirection] = useState(null);
8394
+ const lastScrollY = useRef(0);
8395
+ useEffect(() => {
8396
+ const handleScroll = () => {
8397
+ const currentScrollY = window.scrollY;
8398
+ const diff = currentScrollY - lastScrollY.current;
8399
+ if (Math.abs(diff) > threshold) {
8400
+ if (diff > 0) {
8401
+ setHidden(true);
8402
+ setScrollDirection('down');
8403
+ }
8404
+ else {
8405
+ setHidden(false);
8406
+ setScrollDirection('up');
8407
+ }
8408
+ lastScrollY.current = currentScrollY;
8409
+ }
8410
+ };
8411
+ window.addEventListener('scroll', handleScroll, { passive: true });
8412
+ return () => window.removeEventListener('scroll', handleScroll);
8413
+ }, [threshold]);
8414
+ return { hidden, scrollDirection };
8415
+ }
8416
+
8417
+ /**
8418
+ * PullToRefresh - Mobile pull-to-refresh gesture handler
8419
+ *
8420
+ * Wraps content and provides native-feeling pull-to-refresh functionality.
8421
+ * Only activates when scrolled to top of content.
8422
+ *
8423
+ * @example Basic usage
8424
+ * ```tsx
8425
+ * <PullToRefresh onRefresh={async () => {
8426
+ * await fetchLatestData();
8427
+ * }}>
8428
+ * <div className="min-h-screen">
8429
+ * {content}
8430
+ * </div>
8431
+ * </PullToRefresh>
8432
+ * ```
8433
+ *
8434
+ * @example With custom threshold
8435
+ * ```tsx
8436
+ * <PullToRefresh
8437
+ * onRefresh={handleRefresh}
8438
+ * pullThreshold={100}
8439
+ * maxPull={150}
8440
+ * >
8441
+ * {content}
8442
+ * </PullToRefresh>
8443
+ * ```
8444
+ */
8445
+ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold = 80, maxPull = 120, loadingIndicator, pullIndicator, className = '', }) {
8446
+ const [state, setState] = useState('idle');
8447
+ const [pullDistance, setPullDistance] = useState(0);
8448
+ const containerRef = useRef(null);
8449
+ const startY = useRef(0);
8450
+ const currentY = useRef(0);
8451
+ // Check if at top of scroll container
8452
+ const isAtTop = useCallback(() => {
8453
+ const container = containerRef.current;
8454
+ if (!container)
8455
+ return false;
8456
+ return container.scrollTop <= 0;
8457
+ }, []);
8458
+ // Handle touch start
8459
+ const handleTouchStart = useCallback((e) => {
8460
+ if (disabled || state === 'refreshing' || !isAtTop())
8461
+ return;
8462
+ startY.current = e.touches[0].clientY;
8463
+ currentY.current = startY.current;
8464
+ }, [disabled, state, isAtTop]);
8465
+ // Handle touch move
8466
+ const handleTouchMove = useCallback((e) => {
8467
+ if (disabled || state === 'refreshing')
8468
+ return;
8469
+ if (startY.current === 0)
8470
+ return;
8471
+ currentY.current = e.touches[0].clientY;
8472
+ const diff = currentY.current - startY.current;
8473
+ // Only allow pulling down when at top
8474
+ if (diff > 0 && isAtTop()) {
8475
+ // Apply resistance - pull slows down as distance increases
8476
+ const resistance = 0.5;
8477
+ const adjustedPull = Math.min(diff * resistance, maxPull);
8478
+ setPullDistance(adjustedPull);
8479
+ setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
8480
+ // Prevent default scroll when pulling
8481
+ if (adjustedPull > 0) {
8482
+ e.preventDefault();
8483
+ }
8484
+ }
8485
+ }, [disabled, state, isAtTop, maxPull, pullThreshold]);
8486
+ // Handle touch end
8487
+ const handleTouchEnd = useCallback(async () => {
8488
+ if (disabled || state === 'refreshing')
8489
+ return;
8490
+ if (state === 'ready') {
8491
+ setState('refreshing');
8492
+ setPullDistance(pullThreshold); // Hold at threshold while refreshing
8493
+ try {
8494
+ await onRefresh();
8495
+ }
8496
+ catch (error) {
8497
+ console.error('Refresh failed:', error);
8498
+ }
8499
+ setState('idle');
8500
+ }
8501
+ setPullDistance(0);
8502
+ startY.current = 0;
8503
+ currentY.current = 0;
8504
+ }, [disabled, state, pullThreshold, onRefresh]);
8505
+ // Attach touch listeners
8506
+ useEffect(() => {
8507
+ const container = containerRef.current;
8508
+ if (!container)
8509
+ return;
8510
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
8511
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
8512
+ container.addEventListener('touchend', handleTouchEnd);
8513
+ return () => {
8514
+ container.removeEventListener('touchstart', handleTouchStart);
8515
+ container.removeEventListener('touchmove', handleTouchMove);
8516
+ container.removeEventListener('touchend', handleTouchEnd);
8517
+ };
8518
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
8519
+ // Calculate indicator opacity and rotation
8520
+ const progress = Math.min(pullDistance / pullThreshold, 1);
8521
+ const rotation = progress * 180;
8522
+ // Default loading indicator
8523
+ const defaultLoadingIndicator = (jsx(Loader2, { className: "h-6 w-6 text-accent-600 animate-spin" }));
8524
+ // Default pull indicator
8525
+ const defaultPullIndicator = (jsx("div", { className: `
8526
+ transition-transform duration-200
8527
+ ${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
8528
+ `, style: { transform: `rotate(${rotation}deg)` }, children: jsx(ArrowDown, { className: "h-6 w-6" }) }));
8529
+ return (jsxs("div", { ref: containerRef, className: `relative overflow-auto ${className}`, style: { touchAction: pullDistance > 0 ? 'none' : 'auto' }, children: [jsx("div", { className: `
8530
+ absolute left-0 right-0 flex items-center justify-center
8531
+ transition-all duration-200 overflow-hidden
8532
+ ${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
8533
+ `, style: {
8534
+ height: `${pullDistance}px`,
8535
+ top: 0,
8536
+ zIndex: 10,
8537
+ }, children: jsx("div", { className: `
8538
+ w-10 h-10 rounded-full bg-white shadow-md
8539
+ flex items-center justify-center
8540
+ transition-transform duration-200
8541
+ ${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
8542
+ `, children: state === 'refreshing'
8543
+ ? (loadingIndicator || defaultLoadingIndicator)
8544
+ : (pullIndicator || defaultPullIndicator) }) }), jsx("div", { className: "transition-transform duration-200", style: {
8545
+ transform: `translateY(${pullDistance}px)`,
8546
+ }, children: children })] }));
8547
+ }
8548
+ /**
8549
+ * usePullToRefresh - Hook for custom pull-to-refresh implementations
8550
+ *
8551
+ * @example
8552
+ * ```tsx
8553
+ * const { pullDistance, isRefreshing, bind } = usePullToRefresh({
8554
+ * onRefresh: async () => {
8555
+ * await fetchData();
8556
+ * }
8557
+ * });
8558
+ *
8559
+ * return (
8560
+ * <div {...bind}>
8561
+ * {isRefreshing && <Spinner />}
8562
+ * {content}
8563
+ * </div>
8564
+ * );
8565
+ * ```
8566
+ */
8567
+ function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
8568
+ const [pullDistance, setPullDistance] = useState(0);
8569
+ const [isRefreshing, setIsRefreshing] = useState(false);
8570
+ const startY = useRef(0);
8571
+ const handleTouchStart = useCallback((e) => {
8572
+ if (disabled || isRefreshing)
8573
+ return;
8574
+ startY.current = e.touches[0].clientY;
8575
+ }, [disabled, isRefreshing]);
8576
+ const handleTouchMove = useCallback((e) => {
8577
+ if (disabled || isRefreshing || startY.current === 0)
8578
+ return;
8579
+ const diff = e.touches[0].clientY - startY.current;
8580
+ if (diff > 0) {
8581
+ const adjustedPull = Math.min(diff * 0.5, maxPull);
8582
+ setPullDistance(adjustedPull);
8583
+ }
8584
+ }, [disabled, isRefreshing, maxPull]);
8585
+ const handleTouchEnd = useCallback(async () => {
8586
+ if (disabled || isRefreshing)
8587
+ return;
8588
+ if (pullDistance >= pullThreshold) {
8589
+ setIsRefreshing(true);
8590
+ try {
8591
+ await onRefresh();
8592
+ }
8593
+ finally {
8594
+ setIsRefreshing(false);
8595
+ }
8596
+ }
8597
+ setPullDistance(0);
8598
+ startY.current = 0;
8599
+ }, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
8600
+ return {
8601
+ pullDistance,
8602
+ isRefreshing,
8603
+ isReady: pullDistance >= pullThreshold,
8604
+ progress: Math.min(pullDistance / pullThreshold, 1),
8605
+ bind: {
8606
+ onTouchStart: handleTouchStart,
8607
+ onTouchMove: handleTouchMove,
8608
+ onTouchEnd: handleTouchEnd,
8609
+ },
8610
+ };
6740
8611
  }
6741
8612
 
6742
8613
  function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
@@ -6852,6 +8723,125 @@ const Layout = ({ sidebar, children, statusBar, className = '', sections }) => {
6852
8723
  return (jsxs("div", { className: `h-screen flex flex-col bg-paper-100 ${className}`, children: [jsxs("div", { className: "flex flex-1 overflow-hidden relative", children: [sidebar, jsx("div", { className: "w-8 h-full bg-paper-100 flex-shrink-0 relative flex items-center justify-center", children: jsx(PageNavigation, { sections: sections }) }), jsx("div", { className: "flex-1 overflow-auto", children: children })] }), statusBar] }));
6853
8724
  };
6854
8725
 
8726
+ /**
8727
+ * MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
8728
+ *
8729
+ * This component automatically detects the viewport size and renders the appropriate layout:
8730
+ * - **Desktop** (≥1024px): Standard Layout with sidebar, gutter, and scrollable content
8731
+ * - **Mobile/Tablet** (<1024px): Mobile header, drawer navigation, bottom tab bar
8732
+ *
8733
+ * The mobile layout features:
8734
+ * - Sticky header with hamburger menu to open drawer
8735
+ * - Sidebar rendered as a slide-in drawer
8736
+ * - Bottom navigation bar for primary navigation
8737
+ * - Safe area support for notched devices
8738
+ *
8739
+ * @example Basic usage
8740
+ * ```tsx
8741
+ * <MobileLayout
8742
+ * sidebarItems={[
8743
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8744
+ * { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks' },
8745
+ * { id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' }
8746
+ * ]}
8747
+ * currentPath={location.pathname}
8748
+ * onNavigate={(href) => navigate(href)}
8749
+ * title="My App"
8750
+ * header={<Logo />}
8751
+ * userProfile={<UserProfileButton user={user} />}
8752
+ * >
8753
+ * <Page>
8754
+ * <h1>Dashboard</h1>
8755
+ * </Page>
8756
+ * </MobileLayout>
8757
+ * ```
8758
+ *
8759
+ * @example With custom bottom nav items
8760
+ * ```tsx
8761
+ * <MobileLayout
8762
+ * sidebarItems={fullNavItems}
8763
+ * bottomNavItems={[
8764
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8765
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8766
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' }
8767
+ * ]}
8768
+ * currentPath={location.pathname}
8769
+ * title="My App"
8770
+ * >
8771
+ * {children}
8772
+ * </MobileLayout>
8773
+ * ```
8774
+ *
8775
+ * @example Force mobile layout for testing
8776
+ * ```tsx
8777
+ * <MobileLayout
8778
+ * sidebarItems={items}
8779
+ * title="Mobile Preview"
8780
+ * forceMobile
8781
+ * >
8782
+ * {children}
8783
+ * </MobileLayout>
8784
+ * ```
8785
+ */
8786
+ 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, }) => {
8787
+ const isMobileViewport = useIsMobile();
8788
+ const isTabletViewport = useIsTablet();
8789
+ const [drawerOpen, setDrawerOpen] = useState(false);
8790
+ // Determine if we should use mobile layout
8791
+ const useMobileLayout = forceDesktop
8792
+ ? false
8793
+ : forceMobile || isMobileViewport || isTabletViewport;
8794
+ // Open/close drawer
8795
+ const openDrawer = useCallback(() => setDrawerOpen(true), []);
8796
+ const closeDrawer = useCallback(() => setDrawerOpen(false), []);
8797
+ // Handle navigation from drawer - close drawer after navigation
8798
+ const handleDrawerNavigate = useCallback((href) => {
8799
+ closeDrawer();
8800
+ onNavigate?.(href);
8801
+ }, [closeDrawer, onNavigate]);
8802
+ // Handle bottom nav navigation - matches BottomNavigation's onNavigate signature
8803
+ const handleBottomNavNavigate = useCallback((id, href) => {
8804
+ if (href) {
8805
+ onNavigate?.(href);
8806
+ }
8807
+ // Also check if there's a custom onClick in the bottom nav items
8808
+ const item = bottomNavItems?.find(i => i.id === id);
8809
+ item?.onClick?.();
8810
+ }, [onNavigate, bottomNavItems]);
8811
+ // Convert sidebar items to bottom nav items if not provided
8812
+ const effectiveBottomNavItems = bottomNavItems || sidebarItems
8813
+ .filter(item => !item.children && item.href) // Only top-level items with href
8814
+ .slice(0, 5) // Max 5 items for bottom nav
8815
+ .map(item => ({
8816
+ id: item.id,
8817
+ label: item.label,
8818
+ icon: item.icon,
8819
+ href: item.href,
8820
+ badge: typeof item.badge === 'number' ? item.badge : undefined,
8821
+ }));
8822
+ // Determine active bottom nav ID
8823
+ const effectiveActiveBottomNavId = activeBottomNavId ||
8824
+ effectiveBottomNavItems.find(item => item.href === currentPath)?.id;
8825
+ // Close drawer on escape key
8826
+ useEffect(() => {
8827
+ if (!drawerOpen)
8828
+ return;
8829
+ const handleEscape = (e) => {
8830
+ if (e.key === 'Escape') {
8831
+ closeDrawer();
8832
+ }
8833
+ };
8834
+ window.addEventListener('keydown', handleEscape);
8835
+ return () => window.removeEventListener('keydown', handleEscape);
8836
+ }, [drawerOpen, closeDrawer]);
8837
+ // Desktop Layout
8838
+ if (!useMobileLayout) {
8839
+ return (jsxs("div", { className: `h-screen flex flex-col bg-paper-100 ${className}`, children: [jsxs("div", { className: "flex flex-1 overflow-hidden relative", children: [jsx(Sidebar, { items: sidebarItems, currentPath: currentPath, onNavigate: onNavigate, header: header, footer: jsxs(Fragment, { children: [userProfile, sidebarFooter] }) }), jsx("div", { className: "w-8 h-full bg-paper-100 flex-shrink-0 relative flex items-center justify-center", children: jsx(PageNavigation, { sections: sections }) }), jsx("div", { className: "flex-1 overflow-auto", children: children })] }), statusBar] }));
8840
+ }
8841
+ // Mobile Layout
8842
+ return (jsxs("div", { className: `min-h-screen flex flex-col bg-paper-100 ${className}`, children: [!hideMobileHeader && (jsx(MobileHeader, { title: title, subtitle: subtitle, onMenuClick: headerLeftAction ? undefined : openDrawer, leftAction: headerLeftAction, rightAction: headerRightAction, variant: headerVariant, sticky: true, bordered: true, safeArea: safeArea })), jsx(Sidebar, { items: sidebarItems, currentPath: currentPath, onNavigate: handleDrawerNavigate, header: header, footer: jsxs(Fragment, { children: [userProfile, sidebarFooter] }), mobileOpen: drawerOpen, onMobileClose: closeDrawer }), jsx("div", { className: "flex-1 overflow-auto", children: children }), !hideBottomNav && effectiveBottomNavItems.length > 0 && (jsxs(Fragment, { children: [jsx(BottomNavigationSpacer, {}), jsx(BottomNavigation, { items: effectiveBottomNavItems, activeId: effectiveActiveBottomNavId, onNavigate: handleBottomNavNavigate, showLabels: showBottomNavLabels, safeArea: safeArea })] }))] }));
8843
+ };
8844
+
6855
8845
  /**
6856
8846
  * Two-column content layout component that provides:
6857
8847
  * - Sidebar column on the left (takes 1/3 of width)
@@ -7105,6 +9095,185 @@ function NotificationIndicator({ count = 0, onClick, className = '', maxCount =
7105
9095
  return (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: [jsx(Bell, { className: "h-5 w-5" }), showBadge && (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 }))] }));
7106
9096
  }
7107
9097
 
9098
+ /**
9099
+ * Get value from item by key path (supports nested keys like 'user.name')
9100
+ */
9101
+ function getValueByKey(item, key) {
9102
+ if (typeof key !== 'string') {
9103
+ return item[key];
9104
+ }
9105
+ const keys = key.split('.');
9106
+ let value = item;
9107
+ for (const k of keys) {
9108
+ if (value == null)
9109
+ return undefined;
9110
+ value = value[k];
9111
+ }
9112
+ return value;
9113
+ }
9114
+ /**
9115
+ * Skeleton card for loading state
9116
+ */
9117
+ function SkeletonCard({ className = '' }) {
9118
+ return (jsx("div", { className: `bg-white rounded-lg border border-paper-200 p-4 animate-pulse ${className}`, children: jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "w-10 h-10 rounded-full bg-paper-200 flex-shrink-0" }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "h-5 bg-paper-200 rounded w-3/4 mb-2" }), jsx("div", { className: "h-4 bg-paper-100 rounded w-1/2 mb-3" }), jsxs("div", { className: "space-y-2", children: [jsx("div", { className: "h-3 bg-paper-100 rounded w-2/3" }), jsx("div", { className: "h-3 bg-paper-100 rounded w-1/2" })] })] }), jsx("div", { className: "h-6 w-16 bg-paper-200 rounded-full" })] }) }));
9119
+ }
9120
+ /**
9121
+ * DataTableCardView - Mobile-friendly card view for data tables
9122
+ *
9123
+ * Renders data as cards instead of table rows, optimized for touch interaction.
9124
+ * Automatically uses column render functions for consistent data display.
9125
+ *
9126
+ * @example Basic usage
9127
+ * ```tsx
9128
+ * <DataTableCardView
9129
+ * data={users}
9130
+ * columns={columns}
9131
+ * cardConfig={{
9132
+ * titleKey: 'name',
9133
+ * subtitleKey: 'email',
9134
+ * metadataKeys: ['department', 'role'],
9135
+ * badgeKey: 'status',
9136
+ * }}
9137
+ * onCardClick={(user) => navigate(`/users/${user.id}`)}
9138
+ * />
9139
+ * ```
9140
+ *
9141
+ * @example With selection
9142
+ * ```tsx
9143
+ * <DataTableCardView
9144
+ * data={orders}
9145
+ * columns={columns}
9146
+ * cardConfig={{
9147
+ * titleKey: 'orderNumber',
9148
+ * subtitleKey: 'customer',
9149
+ * badgeKey: 'status',
9150
+ * }}
9151
+ * selectable
9152
+ * selectedRows={selectedOrders}
9153
+ * onSelectionChange={setSelectedOrders}
9154
+ * />
9155
+ * ```
9156
+ */
9157
+ 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', }) {
9158
+ const gapClasses = {
9159
+ sm: 'gap-2',
9160
+ md: 'gap-3',
9161
+ lg: 'gap-4',
9162
+ };
9163
+ // Find column by key to use its render function
9164
+ const getColumn = (key) => {
9165
+ return columns.find(col => col.key === key);
9166
+ };
9167
+ // Render a value using column's render function if available
9168
+ const renderValue = (item, key) => {
9169
+ const column = getColumn(key);
9170
+ const value = getValueByKey(item, key);
9171
+ if (column?.render) {
9172
+ return column.render(item, value);
9173
+ }
9174
+ if (value == null)
9175
+ return '-';
9176
+ if (typeof value === 'boolean')
9177
+ return value ? 'Yes' : 'No';
9178
+ if (value instanceof Date)
9179
+ return value.toLocaleDateString();
9180
+ return String(value);
9181
+ };
9182
+ // Handle card selection toggle
9183
+ const handleSelectionToggle = (item, event) => {
9184
+ event.stopPropagation();
9185
+ const key = keyExtractor(item);
9186
+ const newSelected = new Set(selectedRows);
9187
+ if (newSelected.has(key)) {
9188
+ newSelected.delete(key);
9189
+ }
9190
+ else {
9191
+ newSelected.add(key);
9192
+ }
9193
+ onSelectionChange?.(Array.from(newSelected));
9194
+ };
9195
+ // Handle card click
9196
+ const handleCardClick = (item) => {
9197
+ if (selectable && selectedRows.size > 0) {
9198
+ // If in selection mode, toggle selection instead
9199
+ const key = keyExtractor(item);
9200
+ const newSelected = new Set(selectedRows);
9201
+ if (newSelected.has(key)) {
9202
+ newSelected.delete(key);
9203
+ }
9204
+ else {
9205
+ newSelected.add(key);
9206
+ }
9207
+ onSelectionChange?.(Array.from(newSelected));
9208
+ }
9209
+ else {
9210
+ onCardClick?.(item);
9211
+ }
9212
+ };
9213
+ // Handle long press for context actions
9214
+ const handleLongPress = (item, event) => {
9215
+ onCardLongPress?.(item, event);
9216
+ };
9217
+ // Loading state
9218
+ if (loading) {
9219
+ return (jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: Array.from({ length: loadingRows }).map((_, i) => (jsx(SkeletonCard, { className: cardClassName }, i))) }));
9220
+ }
9221
+ // Empty state
9222
+ if (data.length === 0) {
9223
+ return (jsx("div", { className: `flex items-center justify-center py-12 px-4 ${className}`, children: jsx("p", { className: "text-ink-500 text-center", children: emptyMessage }) }));
9224
+ }
9225
+ // Determine default card config if not provided
9226
+ const config = cardConfig || {
9227
+ titleKey: columns[0]?.key || 'id',
9228
+ subtitleKey: columns[1]?.key,
9229
+ metadataKeys: columns.slice(2, 5).map(c => c.key),
9230
+ };
9231
+ return (jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: data.map((item) => {
9232
+ const key = keyExtractor(item);
9233
+ const isSelected = selectedRows.has(key);
9234
+ // Custom card render
9235
+ if (config.renderCard) {
9236
+ return (jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9237
+ e.preventDefault();
9238
+ handleLongPress(item, e);
9239
+ }, className: `
9240
+ cursor-pointer transition-all duration-200
9241
+ ${isSelected ? 'ring-2 ring-accent-500' : ''}
9242
+ ${cardClassName}
9243
+ `, children: config.renderCard(item, columns) }, key));
9244
+ }
9245
+ // Default card layout
9246
+ const titleColumn = getColumn(config.titleKey);
9247
+ const titleValue = getValueByKey(item, config.titleKey);
9248
+ return (jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9249
+ e.preventDefault();
9250
+ handleLongPress(item, e);
9251
+ }, className: `
9252
+ bg-white rounded-lg border border-paper-200 p-4
9253
+ transition-all duration-200 cursor-pointer
9254
+ active:scale-[0.98] active:bg-paper-50
9255
+ ${isSelected ? 'ring-2 ring-accent-500 bg-accent-50/30' : 'hover:shadow-md hover:border-paper-300'}
9256
+ ${cardClassName}
9257
+ `, children: jsxs("div", { className: "flex items-start gap-3", children: [selectable && (jsx("div", { className: "flex-shrink-0 pt-0.5", onClick: (e) => handleSelectionToggle(item, e), children: jsx(Checkbox, { checked: isSelected, onChange: () => { } }) })), config.avatarKey && (jsx("div", { className: "flex-shrink-0", children: (() => {
9258
+ const avatarValue = getValueByKey(item, config.avatarKey);
9259
+ if (typeof avatarValue === 'string' && avatarValue.startsWith('http')) {
9260
+ return (jsx("img", { src: avatarValue, alt: "", className: "w-10 h-10 rounded-full object-cover" }));
9261
+ }
9262
+ // Render initials or placeholder
9263
+ const initials = String(titleValue || '').slice(0, 2).toUpperCase();
9264
+ return (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 }));
9265
+ })() })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-ink-900 truncate", children: titleColumn?.render
9266
+ ? titleColumn.render(item, titleValue)
9267
+ : String(titleValue || '-') }), config.subtitleKey && (jsx("div", { className: "text-sm text-ink-500 truncate mt-0.5", children: renderValue(item, config.subtitleKey) })), config.metadataKeys && config.metadataKeys.length > 0 && (jsx("div", { className: "mt-2 space-y-1", children: config.metadataKeys.map((metaKey) => {
9268
+ const column = getColumn(metaKey);
9269
+ return (jsxs("div", { className: "flex items-center text-xs", children: [jsxs("span", { className: "text-ink-400 w-20 flex-shrink-0 truncate", children: [column?.header || String(metaKey), ":"] }), jsx("span", { className: "text-ink-600 truncate", children: renderValue(item, metaKey) })] }, String(metaKey)));
9270
+ }) }))] }), jsxs("div", { className: "flex flex-col items-end gap-2 flex-shrink-0", children: [config.badgeKey && (jsx("div", { children: renderValue(item, config.badgeKey) })), config.showChevron && onCardClick && (jsx(ChevronRight, { className: "h-5 w-5 text-ink-300" })), (actions || onEdit || onDelete) && (jsx("button", { onClick: (e) => {
9271
+ e.stopPropagation();
9272
+ handleLongPress(item, e);
9273
+ }, className: "p-1 rounded hover:bg-paper-100 text-ink-400 hover:text-ink-600 -mr-1", children: jsx(MoreVertical, { className: "h-5 w-5" }) }))] })] }) }, key));
9274
+ }) }));
9275
+ }
9276
+
7108
9277
  /**
7109
9278
  * ActionMenu - Inline dropdown menu for row actions
7110
9279
  */
@@ -7263,7 +9432,11 @@ function DataTable({ data, columns, loading = false, error = null, emptyMessage
7263
9432
  // Visual customization props
7264
9433
  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,
7265
9434
  // Pagination props
7266
- paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true, }) {
9435
+ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true,
9436
+ // Mobile view props
9437
+ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
9438
+ // Mobile detection for auto mode
9439
+ const isMobileViewport = useIsMobile();
7267
9440
  // Column resizing state
7268
9441
  const [columnWidths, setColumnWidths] = useState({});
7269
9442
  const [resizingColumn, setResizingColumn] = useState(null);
@@ -7882,8 +10055,274 @@ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pag
7882
10055
  return null;
7883
10056
  return (jsxs("div", { className: "flex items-center justify-between mb-4 px-1", children: [jsxs("div", { className: "flex items-center gap-4", children: [showPageSizeSelector && onPageSizeChange && (jsxs("div", { className: "flex items-center gap-2", children: [jsx("span", { className: "text-sm text-ink-600", children: "Show:" }), jsx(Select, { options: pageSizeSelectOptions, value: String(pageSize), onChange: (value) => onPageSizeChange?.(Number(value)) })] })), jsx("span", { className: "text-sm text-ink-600", children: effectiveTotalItems > 0 ? (jsxs(Fragment, { children: ["Showing ", ((currentPage - 1) * pageSize) + 1, " - ", Math.min(currentPage * pageSize, effectiveTotalItems), " of ", effectiveTotalItems] })) : ('No items') })] }), totalPages > 1 && onPageChange && (jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: onPageChange }))] }));
7884
10057
  };
10058
+ // Determine if we should show card view
10059
+ const shouldShowCardView = mobileView === 'card' ||
10060
+ (mobileView === 'auto' && isMobileViewport);
10061
+ // Card view content
10062
+ const cardViewContent = shouldShowCardView ? (jsx(DataTableCardView, { data: data, columns: visibleColumns, cardConfig: cardConfig, loading: loading, loadingRows: loadingRows, emptyMessage: emptyMessage, onCardClick: onRowClick, onCardLongPress: (item, event) => {
10063
+ if (enableContextMenu && allActions.length > 0) {
10064
+ event.preventDefault();
10065
+ const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
10066
+ const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
10067
+ setContextMenuState({
10068
+ isOpen: true,
10069
+ position: { x: clientX, y: clientY },
10070
+ item,
10071
+ });
10072
+ }
10073
+ }, 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;
7885
10074
  // Render with context menu
7886
- return (jsxs(Fragment, { children: [renderPaginationControls(), finalContent, contextMenuState.isOpen && contextMenuState.item && (jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
10075
+ return (jsxs(Fragment, { children: [renderPaginationControls(), shouldShowCardView ? cardViewContent : finalContent, contextMenuState.isOpen && contextMenuState.item && (jsx(Menu, { items: convertActionsToMenuItems(contextMenuState.item), position: contextMenuState.position, onClose: () => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null }) }))] }));
10076
+ }
10077
+
10078
+ // Color mapping for action buttons
10079
+ const colorClasses = {
10080
+ primary: 'bg-accent-500 text-white',
10081
+ success: 'bg-success-500 text-white',
10082
+ warning: 'bg-warning-500 text-white',
10083
+ error: 'bg-error-500 text-white',
10084
+ default: 'bg-paper-500 text-white',
10085
+ };
10086
+ /**
10087
+ * SwipeActions - Touch-based swipe actions for list items
10088
+ *
10089
+ * Wraps any content with swipe-to-reveal actions, commonly used in mobile
10090
+ * list items for quick actions like delete, archive, edit, etc.
10091
+ *
10092
+ * Features:
10093
+ * - Left and right swipe actions
10094
+ * - Full swipe to trigger primary action
10095
+ * - Spring-back animation
10096
+ * - Touch and mouse support
10097
+ * - Customizable thresholds
10098
+ *
10099
+ * @example Basic delete action
10100
+ * ```tsx
10101
+ * <SwipeActions
10102
+ * leftActions={[
10103
+ * {
10104
+ * id: 'delete',
10105
+ * label: 'Delete',
10106
+ * icon: <Trash className="h-5 w-5" />,
10107
+ * color: 'error',
10108
+ * onClick: () => handleDelete(item),
10109
+ * primary: true,
10110
+ * },
10111
+ * ]}
10112
+ * >
10113
+ * <div className="p-4 bg-white">
10114
+ * List item content
10115
+ * </div>
10116
+ * </SwipeActions>
10117
+ * ```
10118
+ *
10119
+ * @example Multiple actions on both sides
10120
+ * ```tsx
10121
+ * <SwipeActions
10122
+ * leftActions={[
10123
+ * { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
10124
+ * { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
10125
+ * ]}
10126
+ * rightActions={[
10127
+ * { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
10128
+ * ]}
10129
+ * fullSwipe
10130
+ * >
10131
+ * <ListItem />
10132
+ * </SwipeActions>
10133
+ * ```
10134
+ */
10135
+ function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
10136
+ const containerRef = useRef(null);
10137
+ const contentRef = useRef(null);
10138
+ // Swipe state
10139
+ const [translateX, setTranslateX] = useState(0);
10140
+ const [isDragging, setIsDragging] = useState(false);
10141
+ const [activeDirection, setActiveDirection] = useState(null);
10142
+ // Touch/mouse tracking
10143
+ const startX = useRef(0);
10144
+ const currentX = useRef(0);
10145
+ const startTime = useRef(0);
10146
+ // Calculate action widths
10147
+ const leftActionsWidth = leftActions.length * 72; // 72px per action
10148
+ const rightActionsWidth = rightActions.length * 72;
10149
+ // Reset position
10150
+ const resetPosition = useCallback(() => {
10151
+ setTranslateX(0);
10152
+ setActiveDirection(null);
10153
+ onSwipeChange?.(null);
10154
+ }, [onSwipeChange]);
10155
+ // Handle touch/mouse start
10156
+ const handleStart = useCallback((clientX) => {
10157
+ if (disabled)
10158
+ return;
10159
+ startX.current = clientX;
10160
+ currentX.current = clientX;
10161
+ startTime.current = Date.now();
10162
+ setIsDragging(true);
10163
+ }, [disabled]);
10164
+ // Handle touch/mouse move
10165
+ const handleMove = useCallback((clientX) => {
10166
+ if (!isDragging || disabled)
10167
+ return;
10168
+ const deltaX = clientX - startX.current;
10169
+ currentX.current = clientX;
10170
+ // Determine direction and apply resistance at boundaries
10171
+ let newTranslateX = deltaX;
10172
+ // Swiping left (reveals left actions on right side)
10173
+ if (deltaX < 0) {
10174
+ if (leftActions.length === 0) {
10175
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10176
+ }
10177
+ else {
10178
+ const maxSwipe = fullSwipe
10179
+ ? -(containerRef.current?.offsetWidth || 300)
10180
+ : -leftActionsWidth;
10181
+ newTranslateX = Math.max(maxSwipe, deltaX);
10182
+ // Apply resistance past the action buttons
10183
+ if (newTranslateX < -leftActionsWidth) {
10184
+ const overSwipe = newTranslateX + leftActionsWidth;
10185
+ newTranslateX = -leftActionsWidth + overSwipe * 0.3;
10186
+ }
10187
+ }
10188
+ setActiveDirection('left');
10189
+ onSwipeChange?.('left');
10190
+ }
10191
+ // Swiping right (reveals right actions on left side)
10192
+ else if (deltaX > 0) {
10193
+ if (rightActions.length === 0) {
10194
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10195
+ }
10196
+ else {
10197
+ const maxSwipe = fullSwipe
10198
+ ? (containerRef.current?.offsetWidth || 300)
10199
+ : rightActionsWidth;
10200
+ newTranslateX = Math.min(maxSwipe, deltaX);
10201
+ // Apply resistance past the action buttons
10202
+ if (newTranslateX > rightActionsWidth) {
10203
+ const overSwipe = newTranslateX - rightActionsWidth;
10204
+ newTranslateX = rightActionsWidth + overSwipe * 0.3;
10205
+ }
10206
+ }
10207
+ setActiveDirection('right');
10208
+ onSwipeChange?.('right');
10209
+ }
10210
+ setTranslateX(newTranslateX);
10211
+ }, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
10212
+ // Handle touch/mouse end
10213
+ const handleEnd = useCallback(() => {
10214
+ if (!isDragging)
10215
+ return;
10216
+ setIsDragging(false);
10217
+ const deltaX = currentX.current - startX.current;
10218
+ const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
10219
+ const containerWidth = containerRef.current?.offsetWidth || 300;
10220
+ // Check for full swipe trigger
10221
+ if (fullSwipe) {
10222
+ const swipePercentage = Math.abs(translateX) / containerWidth;
10223
+ if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
10224
+ // Find primary action and trigger it
10225
+ if (translateX < 0 && leftActions.length > 0) {
10226
+ const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
10227
+ primaryAction.onClick();
10228
+ resetPosition();
10229
+ return;
10230
+ }
10231
+ else if (translateX > 0 && rightActions.length > 0) {
10232
+ const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
10233
+ primaryAction.onClick();
10234
+ resetPosition();
10235
+ return;
10236
+ }
10237
+ }
10238
+ }
10239
+ // Snap to open or closed position
10240
+ if (Math.abs(translateX) >= threshold || velocity > 0.3) {
10241
+ // Snap open
10242
+ if (translateX < 0 && leftActions.length > 0) {
10243
+ setTranslateX(-leftActionsWidth);
10244
+ setActiveDirection('left');
10245
+ onSwipeChange?.('left');
10246
+ }
10247
+ else if (translateX > 0 && rightActions.length > 0) {
10248
+ setTranslateX(rightActionsWidth);
10249
+ setActiveDirection('right');
10250
+ onSwipeChange?.('right');
10251
+ }
10252
+ else {
10253
+ resetPosition();
10254
+ }
10255
+ }
10256
+ else {
10257
+ // Snap closed
10258
+ resetPosition();
10259
+ }
10260
+ }, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
10261
+ // Touch event handlers
10262
+ const handleTouchStart = (e) => {
10263
+ handleStart(e.touches[0].clientX);
10264
+ };
10265
+ const handleTouchMove = (e) => {
10266
+ handleMove(e.touches[0].clientX);
10267
+ };
10268
+ const handleTouchEnd = () => {
10269
+ handleEnd();
10270
+ };
10271
+ // Mouse event handlers (for testing/desktop)
10272
+ const handleMouseDown = (e) => {
10273
+ handleStart(e.clientX);
10274
+ };
10275
+ const handleMouseMove = (e) => {
10276
+ handleMove(e.clientX);
10277
+ };
10278
+ const handleMouseUp = () => {
10279
+ handleEnd();
10280
+ };
10281
+ // Close on outside click
10282
+ useEffect(() => {
10283
+ if (activeDirection === null)
10284
+ return;
10285
+ const handleClickOutside = (e) => {
10286
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
10287
+ resetPosition();
10288
+ }
10289
+ };
10290
+ document.addEventListener('mousedown', handleClickOutside);
10291
+ return () => document.removeEventListener('mousedown', handleClickOutside);
10292
+ }, [activeDirection, resetPosition]);
10293
+ // Handle mouse leave during drag
10294
+ useEffect(() => {
10295
+ if (!isDragging)
10296
+ return;
10297
+ const handleMouseLeave = () => {
10298
+ handleEnd();
10299
+ };
10300
+ document.addEventListener('mouseup', handleMouseLeave);
10301
+ return () => document.removeEventListener('mouseup', handleMouseLeave);
10302
+ }, [isDragging, handleEnd]);
10303
+ // Render action button
10304
+ const renderActionButton = (action) => {
10305
+ const colorClass = colorClasses[action.color || 'default'];
10306
+ return (jsxs("button", { onClick: (e) => {
10307
+ e.stopPropagation();
10308
+ action.onClick();
10309
+ resetPosition();
10310
+ }, className: `
10311
+ flex flex-col items-center justify-center
10312
+ w-18 h-full min-w-[72px]
10313
+ ${colorClass}
10314
+ transition-transform duration-150
10315
+ `, style: {
10316
+ transform: isDragging ? 'scale(1)' : 'scale(1)',
10317
+ }, children: [action.icon && (jsx("div", { className: "mb-1", children: action.icon })), jsx("span", { className: "text-xs font-medium", children: action.label })] }, action.id));
10318
+ };
10319
+ return (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 && (jsx("div", { className: "absolute left-0 top-0 bottom-0 flex", style: { width: rightActionsWidth }, children: rightActions.map((action) => renderActionButton(action)) })), leftActions.length > 0 && (jsx("div", { className: "absolute right-0 top-0 bottom-0 flex", style: { width: leftActionsWidth }, children: leftActions.map((action) => renderActionButton(action)) })), jsx("div", { ref: contentRef, className: `
10320
+ relative bg-white
10321
+ ${isDragging ? '' : 'transition-transform duration-200 ease-out'}
10322
+ `, style: {
10323
+ transform: `translateX(${translateX}px)`,
10324
+ touchAction: 'pan-y', // Allow vertical scrolling
10325
+ }, children: children })] }));
7887
10326
  }
7888
10327
 
7889
10328
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -51963,5 +54402,204 @@ function useColumnReorder(initialColumns, options = {}) {
51963
54402
  };
51964
54403
  }
51965
54404
 
51966
- export { Accordion, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, Badge, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, Chip, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataTable, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, Modal, ModalFooter, MultiSelect, NotificationBar, NotificationIndicator, NumberInput, Page, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, QueryTransparency, RadioGroup, Rating, RichTextEditor, SearchBar, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, statusManager, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFormContext, useMediaQuery };
54405
+ /**
54406
+ * Default context value (SSR-safe defaults)
54407
+ */
54408
+ const defaultContextValue = {
54409
+ isMobile: false,
54410
+ isTablet: false,
54411
+ isDesktop: true,
54412
+ isTouchDevice: false,
54413
+ breakpoint: 'lg',
54414
+ orientation: 'landscape',
54415
+ viewport: { width: 1024, height: 768 },
54416
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
54417
+ useMobileUI: false,
54418
+ };
54419
+ /**
54420
+ * Mobile context
54421
+ */
54422
+ const MobileContext = createContext$1(defaultContextValue);
54423
+ /**
54424
+ * MobileProvider - Provides responsive state to the entire application
54425
+ *
54426
+ * Wrap your application with MobileProvider to enable auto-responsive
54427
+ * behavior in notebook-ui components.
54428
+ *
54429
+ * @example Basic usage
54430
+ * ```tsx
54431
+ * import { MobileProvider } from 'notebook-ui';
54432
+ *
54433
+ * function App() {
54434
+ * return (
54435
+ * <MobileProvider>
54436
+ * <YourApplication />
54437
+ * </MobileProvider>
54438
+ * );
54439
+ * }
54440
+ * ```
54441
+ *
54442
+ * @example Force mobile UI for testing
54443
+ * ```tsx
54444
+ * <MobileProvider forceMobileUI>
54445
+ * <YourApplication />
54446
+ * </MobileProvider>
54447
+ * ```
54448
+ */
54449
+ function MobileProvider({ children, forceMobileUI = false, forceDesktopUI = false, }) {
54450
+ const isMobile = useIsMobile();
54451
+ const isTablet = useIsTablet();
54452
+ const isDesktop = useIsDesktop();
54453
+ const isTouchDevice = useIsTouchDevice();
54454
+ const breakpoint = useBreakpoint();
54455
+ const orientation = useOrientation();
54456
+ const viewport = useViewportSize();
54457
+ const safeAreaInsets = useSafeAreaInsets();
54458
+ const value = useMemo(() => {
54459
+ // Calculate effective mobile UI state
54460
+ let useMobileUI = isMobile || isTouchDevice;
54461
+ // Apply force overrides
54462
+ if (forceMobileUI) {
54463
+ useMobileUI = true;
54464
+ }
54465
+ else if (forceDesktopUI) {
54466
+ useMobileUI = false;
54467
+ }
54468
+ return {
54469
+ isMobile: forceMobileUI ? true : forceDesktopUI ? false : isMobile,
54470
+ isTablet: forceMobileUI || forceDesktopUI ? false : isTablet,
54471
+ isDesktop: forceDesktopUI ? true : forceMobileUI ? false : isDesktop,
54472
+ isTouchDevice,
54473
+ breakpoint: forceMobileUI ? 'xs' : forceDesktopUI ? 'lg' : breakpoint,
54474
+ orientation,
54475
+ viewport,
54476
+ safeAreaInsets,
54477
+ useMobileUI,
54478
+ };
54479
+ }, [
54480
+ isMobile,
54481
+ isTablet,
54482
+ isDesktop,
54483
+ isTouchDevice,
54484
+ breakpoint,
54485
+ orientation,
54486
+ viewport,
54487
+ safeAreaInsets,
54488
+ forceMobileUI,
54489
+ forceDesktopUI,
54490
+ ]);
54491
+ return (jsx(MobileContext.Provider, { value: value, children: children }));
54492
+ }
54493
+ /**
54494
+ * useMobileContext - Hook to access mobile responsive state
54495
+ *
54496
+ * Must be used within a MobileProvider. Returns comprehensive
54497
+ * responsive state for making UI decisions.
54498
+ *
54499
+ * @example
54500
+ * ```tsx
54501
+ * function MyComponent() {
54502
+ * const { isMobile, useMobileUI, breakpoint } = useMobileContext();
54503
+ *
54504
+ * return useMobileUI ? <MobileView /> : <DesktopView />;
54505
+ * }
54506
+ * ```
54507
+ */
54508
+ function useMobileContext() {
54509
+ const context = useContext(MobileContext);
54510
+ if (context === undefined) {
54511
+ // Return default value if used outside provider (graceful degradation)
54512
+ console.warn('useMobileContext was used outside of MobileProvider. ' +
54513
+ 'Wrap your app with <MobileProvider> for full mobile support.');
54514
+ return defaultContextValue;
54515
+ }
54516
+ return context;
54517
+ }
54518
+ /**
54519
+ * withMobileContext - HOC to inject mobile context as props
54520
+ *
54521
+ * For class components or when you prefer props over hooks.
54522
+ *
54523
+ * @example
54524
+ * ```tsx
54525
+ * interface Props {
54526
+ * mobile: MobileContextValue;
54527
+ * }
54528
+ *
54529
+ * class MyComponent extends React.Component<Props> {
54530
+ * render() {
54531
+ * const { isMobile } = this.props.mobile;
54532
+ * return isMobile ? <Mobile /> : <Desktop />;
54533
+ * }
54534
+ * }
54535
+ *
54536
+ * export default withMobileContext(MyComponent);
54537
+ * ```
54538
+ */
54539
+ function withMobileContext(Component) {
54540
+ const displayName = Component.displayName || Component.name || 'Component';
54541
+ const WrappedComponent = (props) => {
54542
+ const mobile = useMobileContext();
54543
+ return jsx(Component, { ...props, mobile: mobile });
54544
+ };
54545
+ WrappedComponent.displayName = `withMobileContext(${displayName})`;
54546
+ return WrappedComponent;
54547
+ }
54548
+ /**
54549
+ * MobileOnly - Renders children only on mobile devices
54550
+ *
54551
+ * @example
54552
+ * ```tsx
54553
+ * <MobileOnly>
54554
+ * <BottomNavigation items={navItems} />
54555
+ * </MobileOnly>
54556
+ * ```
54557
+ */
54558
+ function MobileOnly({ children }) {
54559
+ const { useMobileUI } = useMobileContext();
54560
+ return useMobileUI ? jsx(Fragment, { children: children }) : null;
54561
+ }
54562
+ /**
54563
+ * DesktopOnly - Renders children only on desktop devices
54564
+ *
54565
+ * @example
54566
+ * ```tsx
54567
+ * <DesktopOnly>
54568
+ * <Sidebar items={navItems} />
54569
+ * </DesktopOnly>
54570
+ * ```
54571
+ */
54572
+ function DesktopOnly({ children }) {
54573
+ const { useMobileUI } = useMobileContext();
54574
+ return useMobileUI ? null : jsx(Fragment, { children: children });
54575
+ }
54576
+ /**
54577
+ * Responsive - Renders different content based on device type
54578
+ *
54579
+ * @example
54580
+ * ```tsx
54581
+ * <Responsive
54582
+ * mobile={<MobileNavigation />}
54583
+ * tablet={<TabletNavigation />}
54584
+ * desktop={<DesktopNavigation />}
54585
+ * />
54586
+ * ```
54587
+ */
54588
+ function Responsive({ mobile, tablet, desktop, }) {
54589
+ const { isMobile, isTablet, isDesktop } = useMobileContext();
54590
+ if (isMobile && mobile)
54591
+ return jsx(Fragment, { children: mobile });
54592
+ if (isTablet && tablet)
54593
+ return jsx(Fragment, { children: tablet });
54594
+ if (isDesktop && desktop)
54595
+ return jsx(Fragment, { children: desktop });
54596
+ // Fallback: desktop -> tablet -> mobile
54597
+ if (isDesktop)
54598
+ return jsx(Fragment, { children: desktop || tablet || mobile });
54599
+ if (isTablet)
54600
+ return jsx(Fragment, { children: tablet || mobile || desktop });
54601
+ return jsx(Fragment, { children: mobile || tablet || desktop });
54602
+ }
54603
+
54604
+ export { Accordion, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, DataTable, DataTableCardView, DateDisplay, DatePicker, DateRangePicker, DateTimePicker, DesktopOnly, Drawer, DrawerFooter, DropZone, Dropdown, DropdownTrigger, EmptyState, ErrorBoundary, ExpandablePanel, ExpandablePanelContainer, ExpandablePanelSpacer, ExpandableRowButton, ExpandableToolbar, ExpandedRowEditForm, ExportButton, FieldArray, FileUpload, FilterBar, FilterControls, FilterStatusBanner, FloatingActionButton, Form, FormContext, FormControl, FormWizard, Grid, GridItem, Hide, HoverCard, InfiniteScroll, Input, KanbanBoard, Layout, Loading, LoadingOverlay, Logo, MarkdownEditor, MaskedInput, Menu, MenuDivider, MobileHeader, MobileHeaderSpacer, MobileLayout, MobileOnly, MobileProvider, Modal, ModalFooter, MultiSelect, NotificationBar, NotificationIndicator, NumberInput, Page, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, loadColumnOrder, loadColumnWidths, reorderArray, saveColumnOrder, saveColumnWidths, statusManager, useBreakpoint, useBreakpointValue, useColumnReorder, useColumnResize, useCommandPalette, useConfirmDialog, useFABScroll, useFormContext, useIsDesktop, useIsMobile, useIsTablet, useIsTouchDevice, useMediaQuery, useMobileContext, useOrientation, usePrefersMobile, usePullToRefresh, useResponsiveCallback, useSafeAreaInsets, useViewportSize, withMobileContext };
51967
54605
  //# sourceMappingURL=index.esm.js.map