@papernote/ui 1.3.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/dist/components/ActionBar.d.ts +112 -0
  2. package/dist/components/ActionBar.d.ts.map +1 -0
  3. package/dist/components/BottomNavigation.d.ts +98 -0
  4. package/dist/components/BottomNavigation.d.ts.map +1 -0
  5. package/dist/components/Checkbox.d.ts +2 -0
  6. package/dist/components/Checkbox.d.ts.map +1 -1
  7. package/dist/components/CheckboxList.d.ts +81 -0
  8. package/dist/components/CheckboxList.d.ts.map +1 -0
  9. package/dist/components/Chip.d.ts +92 -1
  10. package/dist/components/Chip.d.ts.map +1 -1
  11. package/dist/components/ConfirmDialog.d.ts +43 -1
  12. package/dist/components/ConfirmDialog.d.ts.map +1 -1
  13. package/dist/components/DataTable.d.ts +10 -1
  14. package/dist/components/DataTable.d.ts.map +1 -1
  15. package/dist/components/DataTableCardView.d.ts +99 -0
  16. package/dist/components/DataTableCardView.d.ts.map +1 -0
  17. package/dist/components/ExpandablePanel.d.ts +142 -0
  18. package/dist/components/ExpandablePanel.d.ts.map +1 -0
  19. package/dist/components/FloatingActionButton.d.ts +98 -0
  20. package/dist/components/FloatingActionButton.d.ts.map +1 -0
  21. package/dist/components/Input.d.ts +45 -1
  22. package/dist/components/Input.d.ts.map +1 -1
  23. package/dist/components/MobileHeader.d.ts +98 -0
  24. package/dist/components/MobileHeader.d.ts.map +1 -0
  25. package/dist/components/MobileLayout.d.ts +121 -0
  26. package/dist/components/MobileLayout.d.ts.map +1 -0
  27. package/dist/components/Modal.d.ts +78 -1
  28. package/dist/components/Modal.d.ts.map +1 -1
  29. package/dist/components/PageHeader.d.ts +86 -0
  30. package/dist/components/PageHeader.d.ts.map +1 -0
  31. package/dist/components/PullToRefresh.d.ts +87 -0
  32. package/dist/components/PullToRefresh.d.ts.map +1 -0
  33. package/dist/components/QueryTransparency.d.ts +1 -1
  34. package/dist/components/QueryTransparency.d.ts.map +1 -1
  35. package/dist/components/SearchableList.d.ts +83 -0
  36. package/dist/components/SearchableList.d.ts.map +1 -0
  37. package/dist/components/Select.d.ts +16 -2
  38. package/dist/components/Select.d.ts.map +1 -1
  39. package/dist/components/Sidebar.d.ts +40 -1
  40. package/dist/components/Sidebar.d.ts.map +1 -1
  41. package/dist/components/SwipeActions.d.ts +93 -0
  42. package/dist/components/SwipeActions.d.ts.map +1 -0
  43. package/dist/components/Switch.d.ts +1 -0
  44. package/dist/components/Switch.d.ts.map +1 -1
  45. package/dist/components/Textarea.d.ts +13 -0
  46. package/dist/components/Textarea.d.ts.map +1 -1
  47. package/dist/components/index.d.ts +31 -3
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/context/MobileContext.d.ts +168 -0
  50. package/dist/context/MobileContext.d.ts.map +1 -0
  51. package/dist/hooks/useResponsive.d.ts +158 -0
  52. package/dist/hooks/useResponsive.d.ts.map +1 -0
  53. package/dist/index.d.ts +1871 -51
  54. package/dist/index.esm.js +3025 -196
  55. package/dist/index.esm.js.map +1 -1
  56. package/dist/index.js +3063 -194
  57. package/dist/index.js.map +1 -1
  58. package/dist/styles.css +434 -1
  59. package/dist/types/index.d.ts +2 -0
  60. package/dist/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/components/ActionBar.stories.tsx +246 -0
  63. package/src/components/ActionBar.tsx +242 -0
  64. package/src/components/BottomNavigation.stories.tsx +142 -0
  65. package/src/components/BottomNavigation.tsx +225 -0
  66. package/src/components/Checkbox.stories.tsx +162 -0
  67. package/src/components/Checkbox.tsx +22 -6
  68. package/src/components/CheckboxList.stories.tsx +311 -0
  69. package/src/components/CheckboxList.tsx +433 -0
  70. package/src/components/Chip.stories.tsx +389 -0
  71. package/src/components/Chip.tsx +182 -3
  72. package/src/components/ConfirmDialog.tsx +56 -4
  73. package/src/components/DataTable.tsx +60 -1
  74. package/src/components/DataTableCardView.stories.tsx +307 -0
  75. package/src/components/DataTableCardView.tsx +419 -0
  76. package/src/components/ExpandablePanel.stories.tsx +620 -0
  77. package/src/components/ExpandablePanel.tsx +383 -0
  78. package/src/components/FloatingActionButton.stories.tsx +197 -0
  79. package/src/components/FloatingActionButton.tsx +301 -0
  80. package/src/components/Grid.stories.tsx +16 -16
  81. package/src/components/Input.stories.tsx +214 -0
  82. package/src/components/Input.tsx +81 -4
  83. package/src/components/MobileHeader.stories.tsx +205 -0
  84. package/src/components/MobileHeader.tsx +233 -0
  85. package/src/components/MobileLayout.stories.tsx +338 -0
  86. package/src/components/MobileLayout.tsx +313 -0
  87. package/src/components/Modal.stories.tsx +388 -0
  88. package/src/components/Modal.tsx +122 -4
  89. package/src/components/PageHeader.stories.tsx +198 -0
  90. package/src/components/PageHeader.tsx +217 -0
  91. package/src/components/PullToRefresh.stories.tsx +321 -0
  92. package/src/components/PullToRefresh.tsx +294 -0
  93. package/src/components/QueryTransparency.tsx +1 -1
  94. package/src/components/SearchableList.stories.tsx +437 -0
  95. package/src/components/SearchableList.tsx +326 -0
  96. package/src/components/Select.stories.tsx +190 -0
  97. package/src/components/Select.tsx +353 -137
  98. package/src/components/Sidebar.tsx +193 -10
  99. package/src/components/SwipeActions.stories.tsx +327 -0
  100. package/src/components/SwipeActions.tsx +387 -0
  101. package/src/components/Switch.stories.tsx +158 -0
  102. package/src/components/Switch.tsx +12 -3
  103. package/src/components/Textarea.tsx +31 -1
  104. package/src/components/index.ts +69 -3
  105. package/src/context/MobileContext.tsx +296 -0
  106. package/src/hooks/useResponsive.ts +360 -0
  107. package/src/types/index.ts +4 -0
  108. package/tailwind.config.js +56 -1
package/dist/index.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,191 @@ 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 Scrollable modal for long content
3168
+ * ```tsx
3169
+ * <Modal
3170
+ * isOpen={isOpen}
3171
+ * onClose={handleClose}
3172
+ * title="Terms and Conditions"
3173
+ * scrollable
3174
+ * >
3175
+ * {longContent}
3176
+ * </Modal>
3177
+ * ```
3178
+ *
3179
+ * @example Modal with custom max height
3180
+ * ```tsx
3181
+ * <Modal
3182
+ * isOpen={isOpen}
3183
+ * onClose={handleClose}
3184
+ * title="Document Preview"
3185
+ * maxHeight="70vh"
3186
+ * >
3187
+ * {documentContent}
3188
+ * </Modal>
3189
+ * ```
3190
+ *
3191
+ * @example Force modal on mobile
3192
+ * ```tsx
3193
+ * <Modal
3194
+ * isOpen={isOpen}
3195
+ * onClose={handleClose}
3196
+ * title="Settings"
3197
+ * mobileMode="modal"
3198
+ * >
3199
+ * ...
3200
+ * </Modal>
3201
+ * ```
3202
+ *
3203
+ * @example Always use BottomSheet
3204
+ * ```tsx
3205
+ * <Modal
3206
+ * isOpen={isOpen}
3207
+ * onClose={handleClose}
3208
+ * title="Select Option"
3209
+ * mobileMode="sheet"
3210
+ * mobileHeight="md"
3211
+ * >
3212
+ * ...
3213
+ * </Modal>
3214
+ * ```
3215
+ */
3216
+ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton = true, animation = 'scale', scrollable = false, maxHeight, mobileMode = 'auto', mobileHeight = 'lg', mobileShowHandle = true, }) {
2601
3217
  const modalRef = useRef(null);
2602
3218
  const mouseDownOnBackdrop = useRef(false);
2603
3219
  const titleId = useId();
2604
- // Handle escape key
3220
+ const isMobile = useIsMobile();
3221
+ // Determine if we should use BottomSheet
3222
+ const useBottomSheet = mobileMode === 'sheet' ||
3223
+ (mobileMode === 'auto' && isMobile);
3224
+ // Handle escape key (only for modal mode, BottomSheet handles its own)
2605
3225
  useEffect(() => {
3226
+ if (useBottomSheet)
3227
+ return; // BottomSheet handles escape
2606
3228
  const handleEscape = (e) => {
2607
3229
  if (e.key === 'Escape' && isOpen) {
2608
3230
  onClose();
@@ -2616,7 +3238,7 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2616
3238
  document.removeEventListener('keydown', handleEscape);
2617
3239
  document.body.style.overflow = 'unset';
2618
3240
  };
2619
- }, [isOpen, onClose]);
3241
+ }, [isOpen, onClose, useBottomSheet]);
2620
3242
  // Track if mousedown originated on the backdrop
2621
3243
  const handleBackdropMouseDown = (e) => {
2622
3244
  if (e.target === e.currentTarget) {
@@ -2652,13 +3274,20 @@ function Modal({ isOpen, onClose, title, children, size = 'md', showCloseButton
2652
3274
  };
2653
3275
  if (!isOpen)
2654
3276
  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 })] }) }));
3277
+ // Render as BottomSheet on mobile
3278
+ if (useBottomSheet) {
3279
+ return (jsx(BottomSheet, { isOpen: isOpen, onClose: onClose, title: title, height: mobileHeight, showHandle: mobileShowHandle, showCloseButton: showCloseButton, children: children }));
3280
+ }
3281
+ // Render as standard modal on desktop
3282
+ 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 ${scrollable || maxHeight ? 'overflow-y-auto' : ''}`, style: {
3283
+ maxHeight: maxHeight || (scrollable ? 'calc(100vh - 200px)' : undefined),
3284
+ }, children: children })] }) }));
2656
3285
  }
2657
3286
  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 }));
3287
+ 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
3288
  }
2660
3289
 
2661
- const sizeClasses$3 = {
3290
+ const sizeClasses$7 = {
2662
3291
  left: {
2663
3292
  sm: 'w-64',
2664
3293
  md: 'w-96',
@@ -2737,7 +3366,7 @@ function Drawer({ isOpen, onClose, title, children, placement = 'right', size =
2737
3366
  const isHorizontal = placement === 'left' || placement === 'right';
2738
3367
  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
3368
  fixed ${placementClasses[placement]}
2740
- ${sizeClasses$3[placement][size]}
3369
+ ${sizeClasses$7[placement][size]}
2741
3370
  bg-white border-paper-200 shadow-2xl
2742
3371
  ${isHorizontal ? 'border-r' : 'border-b'}
2743
3372
  ${animationClasses[placement].enter}
@@ -2769,14 +3398,49 @@ const variantStyles = {
2769
3398
  button: 'bg-accent-600 hover:bg-accent-700 focus:ring-accent-500',
2770
3399
  },
2771
3400
  };
2772
- function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, }) {
3401
+ /**
3402
+ * ConfirmDialog - Confirmation dialog with mobile support
3403
+ *
3404
+ * @example Basic usage
3405
+ * ```tsx
3406
+ * <ConfirmDialog
3407
+ * isOpen={isOpen}
3408
+ * onClose={handleClose}
3409
+ * onConfirm={handleDelete}
3410
+ * title="Delete Item"
3411
+ * message="Are you sure you want to delete this item? This action cannot be undone."
3412
+ * variant="danger"
3413
+ * />
3414
+ * ```
3415
+ *
3416
+ * @example With useConfirmDialog hook
3417
+ * ```tsx
3418
+ * const confirmDialog = useConfirmDialog();
3419
+ *
3420
+ * const handleDelete = () => {
3421
+ * confirmDialog.show({
3422
+ * title: 'Delete Item',
3423
+ * message: 'Are you sure?',
3424
+ * onConfirm: async () => await deleteItem(),
3425
+ * });
3426
+ * };
3427
+ *
3428
+ * return (
3429
+ * <>
3430
+ * <button onClick={handleDelete}>Delete</button>
3431
+ * <ConfirmDialog {...confirmDialog.props} />
3432
+ * </>
3433
+ * );
3434
+ * ```
3435
+ */
3436
+ function ConfirmDialog({ isOpen, onClose, onConfirm, title, message, confirmLabel = 'Confirm', cancelLabel = 'Cancel', variant = 'danger', icon, isLoading = false, mobileMode = 'auto', mobileHeight = 'sm', }) {
2773
3437
  const variantStyle = variantStyles[variant];
2774
3438
  const IconComponent = icon || variantStyle.icon;
2775
3439
  const handleConfirm = async () => {
2776
3440
  await onConfirm();
2777
3441
  // Note: onClose is called by useConfirmDialog hook after onConfirm completes
2778
3442
  };
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)) })] })] }));
3443
+ 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
3444
  }
2781
3445
  /**
2782
3446
  * Hook for managing ConfirmDialog state
@@ -5107,39 +5771,45 @@ function MenuDivider() {
5107
5771
  return { divider: true, id: `divider-${Date.now()}`, label: '' };
5108
5772
  }
5109
5773
 
5110
- const variantClasses = {
5774
+ const variantClasses$4 = {
5111
5775
  primary: {
5112
5776
  default: 'bg-primary-100 text-primary-700 border-primary-200',
5113
5777
  hover: 'hover:bg-primary-200',
5114
5778
  close: 'hover:bg-primary-300 text-primary-600',
5779
+ selected: 'bg-primary-200 border-primary-400 ring-2 ring-primary-300',
5115
5780
  },
5116
5781
  secondary: {
5117
5782
  default: 'bg-ink-100 text-ink-700 border-ink-200',
5118
5783
  hover: 'hover:bg-ink-200',
5119
5784
  close: 'hover:bg-ink-300 text-ink-600',
5785
+ selected: 'bg-ink-200 border-ink-400 ring-2 ring-ink-300',
5120
5786
  },
5121
5787
  success: {
5122
5788
  default: 'bg-success-100 text-success-700 border-success-200',
5123
5789
  hover: 'hover:bg-success-200',
5124
5790
  close: 'hover:bg-success-300 text-success-600',
5791
+ selected: 'bg-success-200 border-success-400 ring-2 ring-success-300',
5125
5792
  },
5126
5793
  warning: {
5127
5794
  default: 'bg-warning-100 text-warning-700 border-warning-200',
5128
5795
  hover: 'hover:bg-warning-200',
5129
5796
  close: 'hover:bg-warning-300 text-warning-600',
5797
+ selected: 'bg-warning-200 border-warning-400 ring-2 ring-warning-300',
5130
5798
  },
5131
5799
  error: {
5132
5800
  default: 'bg-error-100 text-error-700 border-error-200',
5133
5801
  hover: 'hover:bg-error-200',
5134
5802
  close: 'hover:bg-error-300 text-error-600',
5803
+ selected: 'bg-error-200 border-error-400 ring-2 ring-error-300',
5135
5804
  },
5136
5805
  info: {
5137
5806
  default: 'bg-accent-100 text-accent-700 border-accent-200',
5138
5807
  hover: 'hover:bg-accent-200',
5139
5808
  close: 'hover:bg-accent-300 text-accent-600',
5809
+ selected: 'bg-accent-200 border-accent-400 ring-2 ring-accent-300',
5140
5810
  },
5141
5811
  };
5142
- const sizeClasses$2 = {
5812
+ const sizeClasses$6 = {
5143
5813
  sm: {
5144
5814
  container: 'h-6 px-2 text-xs gap-1',
5145
5815
  icon: 'h-3 w-3',
@@ -5156,20 +5826,52 @@ const sizeClasses$2 = {
5156
5826
  close: 'h-4 w-4 ml-2',
5157
5827
  },
5158
5828
  };
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];
5829
+ const gapClasses = {
5830
+ xs: 'gap-1',
5831
+ sm: 'gap-1.5',
5832
+ md: 'gap-2',
5833
+ lg: 'gap-3',
5834
+ };
5835
+ /**
5836
+ * Chip - Compact element for displaying values with optional remove functionality
5837
+ *
5838
+ * @example Basic chip
5839
+ * ```tsx
5840
+ * <Chip>Tag Name</Chip>
5841
+ * ```
5842
+ *
5843
+ * @example Removable chip
5844
+ * ```tsx
5845
+ * <Chip onClose={() => removeTag(tag)}>
5846
+ * {tag.name}
5847
+ * </Chip>
5848
+ * ```
5849
+ *
5850
+ * @example With icon and selected state
5851
+ * ```tsx
5852
+ * <Chip
5853
+ * icon={<Star className="h-3 w-3" />}
5854
+ * selected={isSelected}
5855
+ * onClick={() => toggleSelection()}
5856
+ * >
5857
+ * Favorite
5858
+ * </Chip>
5859
+ * ```
5860
+ */
5861
+ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, disabled = false, className = '', onClick, selected = false, maxWidth, chipKey, }) {
5862
+ const variantStyle = variantClasses$4[variant];
5863
+ const sizeStyle = sizeClasses$6[size];
5162
5864
  const isClickable = !disabled && (onClick || onClose);
5163
5865
  return (jsxs("div", { className: `
5164
5866
  inline-flex items-center rounded-full border font-medium
5165
5867
  transition-colors
5166
- ${variantStyle.default}
5167
- ${isClickable && !disabled ? variantStyle.hover : ''}
5868
+ ${selected ? variantStyle.selected : variantStyle.default}
5869
+ ${isClickable && !disabled && !selected ? variantStyle.hover : ''}
5168
5870
  ${sizeStyle.container}
5169
5871
  ${disabled ? 'opacity-50 cursor-not-allowed' : ''}
5170
5872
  ${onClick && !disabled ? 'cursor-pointer' : ''}
5171
5873
  ${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) => {
5874
+ `, 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
5875
  e.stopPropagation();
5174
5876
  if (!disabled)
5175
5877
  onClose();
@@ -5180,8 +5882,420 @@ function Chip({ children, variant = 'secondary', size = 'md', onClose, icon, dis
5180
5882
  ${sizeStyle.close}
5181
5883
  `, "aria-label": "Remove", children: jsx(X, { className: "w-full h-full" }) }))] }));
5182
5884
  }
5885
+ /**
5886
+ * ChipGroup - Container for multiple chips with layout and selection support
5887
+ *
5888
+ * @example Basic group
5889
+ * ```tsx
5890
+ * <ChipGroup wrap gap="sm">
5891
+ * {tags.map(tag => (
5892
+ * <Chip key={tag.id} onClose={() => removeTag(tag)}>
5893
+ * {tag.name}
5894
+ * </Chip>
5895
+ * ))}
5896
+ * </ChipGroup>
5897
+ * ```
5898
+ *
5899
+ * @example Selectable group (single)
5900
+ * ```tsx
5901
+ * <ChipGroup
5902
+ * selectionMode="single"
5903
+ * selectedKeys={[selectedCategory]}
5904
+ * onSelectionChange={(keys) => setSelectedCategory(keys[0])}
5905
+ * >
5906
+ * <Chip chipKey="all">All</Chip>
5907
+ * <Chip chipKey="active">Active</Chip>
5908
+ * <Chip chipKey="archived">Archived</Chip>
5909
+ * </ChipGroup>
5910
+ * ```
5911
+ *
5912
+ * @example Multi-select group
5913
+ * ```tsx
5914
+ * <ChipGroup
5915
+ * selectionMode="multiple"
5916
+ * selectedKeys={selectedTags}
5917
+ * onSelectionChange={setSelectedTags}
5918
+ * wrap
5919
+ * >
5920
+ * {availableTags.map(tag => (
5921
+ * <Chip key={tag} chipKey={tag}>{tag}</Chip>
5922
+ * ))}
5923
+ * </ChipGroup>
5924
+ * ```
5925
+ */
5926
+ function ChipGroup({ children, direction = 'horizontal', wrap = false, gap = 'sm', selectionMode = 'none', selectedKeys = [], onSelectionChange, className = '', }) {
5927
+ const handleChipClick = (chipKey) => {
5928
+ if (selectionMode === 'none' || !onSelectionChange)
5929
+ return;
5930
+ if (selectionMode === 'single') {
5931
+ // Toggle single selection
5932
+ if (selectedKeys.includes(chipKey)) {
5933
+ onSelectionChange([]);
5934
+ }
5935
+ else {
5936
+ onSelectionChange([chipKey]);
5937
+ }
5938
+ }
5939
+ else if (selectionMode === 'multiple') {
5940
+ // Toggle in array
5941
+ if (selectedKeys.includes(chipKey)) {
5942
+ onSelectionChange(selectedKeys.filter(k => k !== chipKey));
5943
+ }
5944
+ else {
5945
+ onSelectionChange([...selectedKeys, chipKey]);
5946
+ }
5947
+ }
5948
+ };
5949
+ // Clone children to inject selection props
5950
+ const enhancedChildren = Children.map(children, (child) => {
5951
+ if (!isValidElement(child))
5952
+ return child;
5953
+ const chipKey = child.props.chipKey;
5954
+ if (!chipKey || selectionMode === 'none')
5955
+ return child;
5956
+ const isSelected = selectedKeys.includes(chipKey);
5957
+ return cloneElement(child, {
5958
+ ...child.props,
5959
+ selected: isSelected,
5960
+ onClick: () => {
5961
+ // Call original onClick if exists
5962
+ if (child.props.onClick) {
5963
+ child.props.onClick();
5964
+ }
5965
+ handleChipClick(chipKey);
5966
+ },
5967
+ });
5968
+ });
5969
+ return (jsx("div", { className: `
5970
+ flex
5971
+ ${direction === 'vertical' ? 'flex-col' : 'flex-row'}
5972
+ ${wrap ? 'flex-wrap' : ''}
5973
+ ${gapClasses[gap]}
5974
+ ${className}
5975
+ `, role: selectionMode !== 'none' ? 'group' : undefined, "aria-label": selectionMode !== 'none' ? 'Chip selection group' : undefined, children: enhancedChildren }));
5976
+ }
5183
5977
 
5184
- const sizeClasses$1 = {
5978
+ const sizeClasses$5 = {
5979
+ sm: {
5980
+ item: 'py-1.5 px-2',
5981
+ text: 'text-sm',
5982
+ description: 'text-xs',
5983
+ groupHeader: 'py-1.5 px-2 text-xs',
5984
+ },
5985
+ md: {
5986
+ item: 'py-2 px-3',
5987
+ text: 'text-sm',
5988
+ description: 'text-xs',
5989
+ groupHeader: 'py-2 px-3 text-xs',
5990
+ },
5991
+ lg: {
5992
+ item: 'py-3 px-4',
5993
+ text: 'text-base',
5994
+ description: 'text-sm',
5995
+ groupHeader: 'py-2.5 px-4 text-sm',
5996
+ },
5997
+ };
5998
+ const variantClasses$3 = {
5999
+ default: 'bg-white',
6000
+ bordered: 'bg-white border border-paper-300 rounded-lg',
6001
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
6002
+ };
6003
+ /**
6004
+ * CheckboxList - Multi-select list with checkboxes, grouping, and search
6005
+ *
6006
+ * @example Basic usage
6007
+ * ```tsx
6008
+ * <CheckboxList
6009
+ * items={[
6010
+ * { key: '1', label: 'Option 1' },
6011
+ * { key: '2', label: 'Option 2' },
6012
+ * ]}
6013
+ * selectedKeys={selected}
6014
+ * onSelectionChange={setSelected}
6015
+ * />
6016
+ * ```
6017
+ *
6018
+ * @example With grouping and search
6019
+ * ```tsx
6020
+ * <CheckboxList
6021
+ * items={fields}
6022
+ * selectedKeys={selectedFields}
6023
+ * onSelectionChange={setSelectedFields}
6024
+ * groupLabels={{ table1: 'Users', table2: 'Orders' }}
6025
+ * searchable
6026
+ * searchPlaceholder="Search fields..."
6027
+ * showSelectAll
6028
+ * maxHeight="300px"
6029
+ * />
6030
+ * ```
6031
+ */
6032
+ 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 = '', }) {
6033
+ const [searchTerm, setSearchTerm] = useState('');
6034
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
6035
+ const [internalExpandedGroups, setInternalExpandedGroups] = useState(new Set(defaultExpandedGroups || []));
6036
+ // Debounce search
6037
+ const handleSearchChange = useCallback((value) => {
6038
+ setSearchTerm(value);
6039
+ const timer = setTimeout(() => {
6040
+ setDebouncedSearchTerm(value);
6041
+ }, debounceMs);
6042
+ return () => clearTimeout(timer);
6043
+ }, [debounceMs]);
6044
+ // Filter items based on search
6045
+ const filteredItems = useMemo(() => {
6046
+ if (!debouncedSearchTerm)
6047
+ return items;
6048
+ const term = debouncedSearchTerm.toLowerCase();
6049
+ return items.filter(item => {
6050
+ if (filterFn) {
6051
+ return filterFn(item, debouncedSearchTerm);
6052
+ }
6053
+ return (item.label.toLowerCase().includes(term) ||
6054
+ item.description?.toLowerCase().includes(term) ||
6055
+ item.key.toLowerCase().includes(term));
6056
+ });
6057
+ }, [items, debouncedSearchTerm, filterFn]);
6058
+ // Group items
6059
+ const groupedItems = useMemo(() => {
6060
+ const groups = new Map();
6061
+ filteredItems.forEach(item => {
6062
+ const groupKey = item.group || null;
6063
+ if (!groups.has(groupKey)) {
6064
+ groups.set(groupKey, []);
6065
+ }
6066
+ groups.get(groupKey).push(item);
6067
+ });
6068
+ return groups;
6069
+ }, [filteredItems]);
6070
+ // Determine expanded groups
6071
+ const expandedGroups = controlledExpandedGroups
6072
+ ? new Set(controlledExpandedGroups)
6073
+ : internalExpandedGroups;
6074
+ const handleGroupToggle = (groupKey) => {
6075
+ const newExpanded = !expandedGroups.has(groupKey);
6076
+ if (!controlledExpandedGroups) {
6077
+ setInternalExpandedGroups(prev => {
6078
+ const next = new Set(prev);
6079
+ if (newExpanded) {
6080
+ next.add(groupKey);
6081
+ }
6082
+ else {
6083
+ next.delete(groupKey);
6084
+ }
6085
+ return next;
6086
+ });
6087
+ }
6088
+ onGroupToggle?.(groupKey, newExpanded);
6089
+ };
6090
+ // Selection handlers
6091
+ const handleItemToggle = (key) => {
6092
+ const newSelected = selectedKeys.includes(key)
6093
+ ? selectedKeys.filter(k => k !== key)
6094
+ : [...selectedKeys, key];
6095
+ onSelectionChange(newSelected);
6096
+ };
6097
+ const handleSelectAll = () => {
6098
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6099
+ const allSelected = enabledItems.every(item => selectedKeys.includes(item.key));
6100
+ if (allSelected) {
6101
+ // Deselect all filtered items
6102
+ const filteredKeys = new Set(enabledItems.map(item => item.key));
6103
+ onSelectionChange(selectedKeys.filter(key => !filteredKeys.has(key)));
6104
+ }
6105
+ else {
6106
+ // Select all filtered items
6107
+ const newKeys = new Set([...selectedKeys, ...enabledItems.map(item => item.key)]);
6108
+ onSelectionChange(Array.from(newKeys));
6109
+ }
6110
+ };
6111
+ const sizeStyle = sizeClasses$5[size];
6112
+ const enabledItems = filteredItems.filter(item => !item.disabled);
6113
+ const allSelected = enabledItems.length > 0 && enabledItems.every(item => selectedKeys.includes(item.key));
6114
+ const someSelected = enabledItems.some(item => selectedKeys.includes(item.key)) && !allSelected;
6115
+ // Check if we have groups
6116
+ const hasGroups = groupedItems.size > 1 || (groupedItems.size === 1 && !groupedItems.has(null));
6117
+ 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: () => {
6118
+ setSearchTerm('');
6119
+ setDebouncedSearchTerm('');
6120
+ } }) })), (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]) => {
6121
+ if (groupKey === null) {
6122
+ // Ungrouped items
6123
+ return groupItems.map(item => (jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key)));
6124
+ }
6125
+ const isExpanded = expandedGroups.has(groupKey);
6126
+ const groupLabel = groupLabels[groupKey] || groupKey;
6127
+ const groupSelectedCount = groupItems.filter(item => selectedKeys.includes(item.key)).length;
6128
+ return (jsxs("div", { children: [jsxs("button", { type: "button", onClick: () => handleGroupToggle(groupKey), className: `
6129
+ w-full flex items-center justify-between
6130
+ ${sizeStyle.groupHeader}
6131
+ font-medium text-ink-700 bg-paper-50
6132
+ hover:bg-paper-100 transition-colors
6133
+ border-b border-paper-200
6134
+ `, 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));
6135
+ })) : (
6136
+ // Flat list (no groups)
6137
+ filteredItems.map(item => (jsx(CheckboxListItemRow, { item: item, selected: selectedKeys.includes(item.key), onToggle: () => handleItemToggle(item.key), size: size, sizeStyle: sizeStyle }, item.key))))] })] }));
6138
+ }
6139
+ // Helper component for rendering individual items
6140
+ function CheckboxListItemRow({ item, selected, onToggle, size, sizeStyle, indented = false, }) {
6141
+ return (jsx("div", { className: `
6142
+ ${sizeStyle.item}
6143
+ ${indented ? 'pl-8' : ''}
6144
+ hover:bg-paper-50 transition-colors
6145
+ border-b border-paper-100 last:border-b-0
6146
+ ${item.disabled ? 'opacity-50' : ''}
6147
+ `, 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 }))] })] }) }));
6148
+ }
6149
+
6150
+ const sizeClasses$4 = {
6151
+ sm: {
6152
+ container: 'text-sm',
6153
+ item: 'py-1.5 px-2',
6154
+ searchPadding: 'p-2',
6155
+ statusPadding: 'px-2 py-1.5',
6156
+ },
6157
+ md: {
6158
+ container: 'text-sm',
6159
+ item: 'py-2 px-3',
6160
+ searchPadding: 'p-3',
6161
+ statusPadding: 'px-3 py-2',
6162
+ },
6163
+ lg: {
6164
+ container: 'text-base',
6165
+ item: 'py-3 px-4',
6166
+ searchPadding: 'p-4',
6167
+ statusPadding: 'px-4 py-2.5',
6168
+ },
6169
+ };
6170
+ const variantClasses$2 = {
6171
+ default: 'bg-white',
6172
+ bordered: 'bg-white border border-paper-300 rounded-lg',
6173
+ card: 'bg-white border border-paper-300 rounded-lg shadow-sm',
6174
+ };
6175
+ /**
6176
+ * SearchableList - List component with integrated search/filter functionality
6177
+ *
6178
+ * @example Basic usage
6179
+ * ```tsx
6180
+ * <SearchableList
6181
+ * items={users.map(u => ({ key: u.id, data: u }))}
6182
+ * renderItem={(item) => <div>{item.data.name}</div>}
6183
+ * onSelect={(item) => setSelectedUser(item.data)}
6184
+ * searchable
6185
+ * searchPlaceholder="Search users..."
6186
+ * />
6187
+ * ```
6188
+ *
6189
+ * @example With custom filter and loading
6190
+ * ```tsx
6191
+ * <SearchableList
6192
+ * items={products}
6193
+ * renderItem={(item, index, isSelected) => (
6194
+ * <div className={isSelected ? 'bg-accent-50' : ''}>
6195
+ * {item.data.name} - ${item.data.price}
6196
+ * </div>
6197
+ * )}
6198
+ * filterFn={(item, term) =>
6199
+ * item.data.name.toLowerCase().includes(term.toLowerCase())
6200
+ * }
6201
+ * loading={isLoading}
6202
+ * loadingMessage="Fetching products..."
6203
+ * maxHeight="400px"
6204
+ * />
6205
+ * ```
6206
+ */
6207
+ 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, }) {
6208
+ const [internalSearchValue, setInternalSearchValue] = useState('');
6209
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
6210
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
6211
+ const listRef = useRef(null);
6212
+ const itemRefs = useRef(new Map());
6213
+ const searchValue = controlledSearchValue !== undefined ? controlledSearchValue : internalSearchValue;
6214
+ // Debounce search
6215
+ useEffect(() => {
6216
+ const timer = setTimeout(() => {
6217
+ setDebouncedSearchTerm(searchValue);
6218
+ }, debounceMs);
6219
+ return () => clearTimeout(timer);
6220
+ }, [searchValue, debounceMs]);
6221
+ const handleSearchChange = useCallback((value) => {
6222
+ if (controlledSearchValue === undefined) {
6223
+ setInternalSearchValue(value);
6224
+ }
6225
+ onSearchChange?.(value);
6226
+ setHighlightedIndex(-1);
6227
+ }, [controlledSearchValue, onSearchChange]);
6228
+ // Filter items based on search
6229
+ const filteredItems = useMemo(() => {
6230
+ if (!debouncedSearchTerm)
6231
+ return items;
6232
+ return items.filter(item => {
6233
+ if (filterFn) {
6234
+ return filterFn(item, debouncedSearchTerm);
6235
+ }
6236
+ // Default filter: check if key includes search term
6237
+ return item.key.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
6238
+ });
6239
+ }, [items, debouncedSearchTerm, filterFn]);
6240
+ // Keyboard navigation
6241
+ const handleKeyDown = useCallback((e) => {
6242
+ if (!enableKeyboardNavigation || filteredItems.length === 0)
6243
+ return;
6244
+ switch (e.key) {
6245
+ case 'ArrowDown':
6246
+ e.preventDefault();
6247
+ setHighlightedIndex(prev => prev < filteredItems.length - 1 ? prev + 1 : 0);
6248
+ break;
6249
+ case 'ArrowUp':
6250
+ e.preventDefault();
6251
+ setHighlightedIndex(prev => prev > 0 ? prev - 1 : filteredItems.length - 1);
6252
+ break;
6253
+ case 'Enter':
6254
+ e.preventDefault();
6255
+ if (highlightedIndex >= 0 && highlightedIndex < filteredItems.length) {
6256
+ onSelect?.(filteredItems[highlightedIndex]);
6257
+ }
6258
+ break;
6259
+ case 'Escape':
6260
+ setHighlightedIndex(-1);
6261
+ break;
6262
+ }
6263
+ }, [enableKeyboardNavigation, filteredItems, highlightedIndex, onSelect]);
6264
+ // Scroll highlighted item into view
6265
+ useEffect(() => {
6266
+ if (highlightedIndex >= 0) {
6267
+ const itemEl = itemRefs.current.get(highlightedIndex);
6268
+ if (itemEl) {
6269
+ itemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
6270
+ }
6271
+ }
6272
+ }, [highlightedIndex]);
6273
+ const sizeStyle = sizeClasses$4[size];
6274
+ const resultCountText = formatResultCount
6275
+ ? formatResultCount(filteredItems.length, items.length)
6276
+ : `${filteredItems.length} of ${items.length}`;
6277
+ 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) => {
6278
+ const isSelected = selectedKey === item.key;
6279
+ const isHighlighted = highlightedIndex === index;
6280
+ return (jsx("div", { id: `item-${index}`, ref: (el) => {
6281
+ if (el) {
6282
+ itemRefs.current.set(index, el);
6283
+ }
6284
+ else {
6285
+ itemRefs.current.delete(index);
6286
+ }
6287
+ }, role: "option", "aria-selected": isSelected, onClick: () => onSelect?.(item), className: `
6288
+ ${sizeStyle.item}
6289
+ cursor-pointer transition-colors
6290
+ ${isSelected ? 'bg-accent-50' : ''}
6291
+ ${isHighlighted ? 'bg-paper-100' : ''}
6292
+ ${!isSelected && !isHighlighted ? 'hover:bg-paper-50' : ''}
6293
+ border-b border-paper-100 last:border-b-0
6294
+ `, children: renderItem(item, index, isSelected, isHighlighted) }, item.key));
6295
+ })] })] }));
6296
+ }
6297
+
6298
+ const sizeClasses$3 = {
5185
6299
  sm: {
5186
6300
  input: 'h-8 px-2 text-sm',
5187
6301
  button: 'h-8 w-8',
@@ -5205,7 +6319,7 @@ const NumberInput = forwardRef((props, ref) => {
5205
6319
  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
6320
  const [internalValue, setInternalValue] = useState(String(value));
5207
6321
  const [isFocused, setIsFocused] = useState(false);
5208
- const sizeStyle = sizeClasses$1[size];
6322
+ const sizeStyle = sizeClasses$3[size];
5209
6323
  // Generate unique IDs for ARIA
5210
6324
  const uniqueId = useId();
5211
6325
  const inputId = id || uniqueId;
@@ -5635,105 +6749,6 @@ class ErrorBoundary extends Component {
5635
6749
  }
5636
6750
  }
5637
6751
 
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
6752
  function Collapsible({ trigger, children, defaultOpen = false, open: controlledOpen, onOpenChange, disabled = false, showIcon = true, className = '', triggerClassName = '', contentClassName = '', }) {
5738
6753
  const isControlled = controlledOpen !== undefined;
5739
6754
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
@@ -5783,6 +6798,231 @@ function Collapsible({ trigger, children, defaultOpen = false, open: controlledO
5783
6798
  ` }))] }), 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
6799
  }
5785
6800
 
6801
+ const sizeClasses$2 = {
6802
+ sm: {
6803
+ header: 'h-10 px-3',
6804
+ text: 'text-sm',
6805
+ icon: 'h-4 w-4',
6806
+ },
6807
+ md: {
6808
+ header: 'h-12 px-4',
6809
+ text: 'text-sm',
6810
+ icon: 'h-5 w-5',
6811
+ },
6812
+ lg: {
6813
+ header: 'h-14 px-5',
6814
+ text: 'text-base',
6815
+ icon: 'h-5 w-5',
6816
+ },
6817
+ };
6818
+ const variantClasses$1 = {
6819
+ default: {
6820
+ container: 'bg-white border-ink-200',
6821
+ header: 'bg-paper-50',
6822
+ },
6823
+ elevated: {
6824
+ container: 'bg-white shadow-lg border-ink-200',
6825
+ header: 'bg-white',
6826
+ },
6827
+ bordered: {
6828
+ container: 'bg-white border-2 border-ink-300',
6829
+ header: 'bg-paper-100',
6830
+ },
6831
+ };
6832
+ /**
6833
+ * ExpandablePanel - A panel that sticks to the bottom (or top) and can expand/collapse
6834
+ *
6835
+ * For bottom position: expands UPWARD (content appears above header)
6836
+ * For top position: expands DOWNWARD (content appears below header)
6837
+ *
6838
+ * Two modes of operation:
6839
+ * - `viewport`: Fixed to the viewport (for standalone pages, covers StatusBar)
6840
+ * - `container`: Sticky within its parent container (for use inside Page/AppLayout, respects StatusBar)
6841
+ *
6842
+ * @example Basic usage (viewport mode - full page)
6843
+ * ```tsx
6844
+ * <ExpandablePanel
6845
+ * mode="viewport"
6846
+ * collapsedContent={<Text>3 items selected</Text>}
6847
+ * expandedHeight="300px"
6848
+ * >
6849
+ * {content}
6850
+ * </ExpandablePanel>
6851
+ * ```
6852
+ *
6853
+ * @example Inside Page/AppLayout (container mode - respects StatusBar)
6854
+ * ```tsx
6855
+ * <Page>
6856
+ * <ExpandablePanelContainer>
6857
+ * <div className="flex-1 overflow-auto">
6858
+ * {pageContent}
6859
+ * </div>
6860
+ * <ExpandablePanel
6861
+ * mode="container"
6862
+ * collapsedContent={<Text>3 items selected</Text>}
6863
+ * expandedHeight="300px"
6864
+ * >
6865
+ * {selectedItemsContent}
6866
+ * </ExpandablePanel>
6867
+ * </ExpandablePanelContainer>
6868
+ * </Page>
6869
+ * ```
6870
+ *
6871
+ * @example With maxWidth to match page content
6872
+ * ```tsx
6873
+ * <ExpandablePanel
6874
+ * mode="container"
6875
+ * maxWidth="1400px"
6876
+ * collapsedContent={<Text>Generated SQL</Text>}
6877
+ * expandedHeight="300px"
6878
+ * >
6879
+ * {sqlContent}
6880
+ * </ExpandablePanel>
6881
+ * ```
6882
+ */
6883
+ 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, }) {
6884
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
6885
+ // Determine if controlled or uncontrolled
6886
+ const isControlled = controlledExpanded !== undefined;
6887
+ const expanded = isControlled ? controlledExpanded : internalExpanded;
6888
+ const setExpanded = (value) => {
6889
+ if (!isControlled) {
6890
+ setInternalExpanded(value);
6891
+ }
6892
+ onExpandedChange?.(value);
6893
+ };
6894
+ const toggleExpanded = () => {
6895
+ setExpanded(!expanded);
6896
+ };
6897
+ // Close on Escape
6898
+ useEffect(() => {
6899
+ if (!closeOnEscape || !expanded)
6900
+ return;
6901
+ const handleEscape = (e) => {
6902
+ if (e.key === 'Escape') {
6903
+ setExpanded(false);
6904
+ }
6905
+ };
6906
+ document.addEventListener('keydown', handleEscape);
6907
+ return () => document.removeEventListener('keydown', handleEscape);
6908
+ }, [closeOnEscape, expanded]);
6909
+ const sizeStyle = sizeClasses$2[size];
6910
+ const variantStyle = variantClasses$1[variant];
6911
+ const heightValue = typeof expandedHeight === 'number' ? `${expandedHeight}px` : expandedHeight;
6912
+ const maxWidthValue = maxWidth
6913
+ ? (typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)
6914
+ : undefined;
6915
+ // Position classes differ based on mode
6916
+ const getPositionClasses = () => {
6917
+ if (mode === 'viewport') {
6918
+ // Fixed to viewport
6919
+ return position === 'bottom'
6920
+ ? 'fixed bottom-0 left-0 right-0'
6921
+ : 'fixed top-0 left-0 right-0';
6922
+ }
6923
+ else {
6924
+ // Absolute positioning within container - snaps to bottom
6925
+ return position === 'bottom'
6926
+ ? 'absolute bottom-0 left-0 right-0'
6927
+ : 'absolute top-0 left-0 right-0';
6928
+ }
6929
+ };
6930
+ // For bottom panel, we want chevron up to expand (reveal content above)
6931
+ // For top panel, we want chevron down to expand (reveal content below)
6932
+ const ChevronIcon = position === 'bottom'
6933
+ ? (expanded ? ChevronDown : ChevronUp)
6934
+ : (expanded ? ChevronUp : ChevronDown);
6935
+ // Header component
6936
+ const header = (jsxs("div", { className: `
6937
+ flex items-center justify-between
6938
+ ${sizeStyle.header}
6939
+ ${variantStyle.header}
6940
+ border-ink-200
6941
+ flex-shrink-0
6942
+ ${headerClassName}
6943
+ `, 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: `
6944
+ flex items-center justify-center
6945
+ p-1.5 rounded-md
6946
+ text-ink-500 hover:text-ink-700
6947
+ hover:bg-ink-100
6948
+ transition-colors
6949
+ focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-1
6950
+ `, "aria-expanded": expanded, "aria-label": expanded ? 'Collapse panel' : 'Expand panel', children: toggleContent || jsx(ChevronIcon, { className: sizeStyle.icon }) }))] })] }));
6951
+ // Content component
6952
+ const content = (jsx("div", { className: `
6953
+ overflow-hidden
6954
+ transition-all duration-300 ease-in-out
6955
+ `, style: {
6956
+ maxHeight: expanded ? heightValue : '0px',
6957
+ opacity: expanded ? 1 : 0,
6958
+ }, children: jsx("div", { className: `
6959
+ overflow-y-auto p-4
6960
+ ${contentClassName}
6961
+ `, style: { maxHeight: heightValue }, children: children }) }));
6962
+ // Build container styles
6963
+ const containerStyle = {
6964
+ ...(mode === 'viewport' ? { zIndex } : {}),
6965
+ ...(maxWidthValue ? {
6966
+ maxWidth: maxWidthValue,
6967
+ marginLeft: 'auto',
6968
+ marginRight: 'auto'
6969
+ } : {}),
6970
+ };
6971
+ return (jsx("div", { className: `
6972
+ ${getPositionClasses()}
6973
+ ${variantStyle.container}
6974
+ border-t rounded-t-lg
6975
+ transition-all duration-300 ease-in-out
6976
+ flex flex-col
6977
+ ${className}
6978
+ `, style: containerStyle, children: position === 'bottom' ? (jsxs(Fragment, { children: [content, header] })) : (jsxs(Fragment, { children: [header, content] })) }));
6979
+ }
6980
+ /**
6981
+ * ExpandablePanelSpacer - Adds spacing to prevent content from being hidden behind the panel
6982
+ * Only needed in viewport mode. In container mode, the panel is part of the flex layout.
6983
+ *
6984
+ * @example
6985
+ * ```tsx
6986
+ * <div>
6987
+ * <MainContent />
6988
+ * <ExpandablePanelSpacer size="md" />
6989
+ * </div>
6990
+ * <ExpandablePanel mode="viewport" position="bottom" size="md" {...props} />
6991
+ * ```
6992
+ */
6993
+ function ExpandablePanelSpacer({ size = 'md' }) {
6994
+ const heights = {
6995
+ sm: 'h-10',
6996
+ md: 'h-12',
6997
+ lg: 'h-14',
6998
+ };
6999
+ return jsx("div", { className: heights[size] });
7000
+ }
7001
+ /**
7002
+ * ExpandablePanelContainer - Wrapper that sets up proper layout for container mode
7003
+ * Use this to wrap your page content when using ExpandablePanel with mode="container"
7004
+ *
7005
+ * This creates a relative container with full height so the panel can position absolutely
7006
+ * at the bottom while the content scrolls above it.
7007
+ *
7008
+ * @example
7009
+ * ```tsx
7010
+ * <Page>
7011
+ * <ExpandablePanelContainer>
7012
+ * <div className="flex-1 overflow-auto p-4">
7013
+ * {pageContent}
7014
+ * </div>
7015
+ * <ExpandablePanel mode="container" {...props}>
7016
+ * {panelContent}
7017
+ * </ExpandablePanel>
7018
+ * </ExpandablePanelContainer>
7019
+ * </Page>
7020
+ * ```
7021
+ */
7022
+ function ExpandablePanelContainer({ children, className = '', }) {
7023
+ return (jsx("div", { className: `relative h-full overflow-hidden ${className}`, children: children }));
7024
+ }
7025
+
5786
7026
  // Tailwind breakpoint mappings
5787
7027
  // sm: 640px, md: 768px, lg: 1024px, xl: 1280px, 2xl: 1536px
5788
7028
  /**
@@ -5951,26 +7191,6 @@ function Hide({ children, above, below, only, className = '' }) {
5951
7191
  }
5952
7192
  return (jsx("div", { className: `${visibilityClasses} ${className}`, children: children }));
5953
7193
  }
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
7194
 
5975
7195
  function Breadcrumbs({ items, showHome = true }) {
5976
7196
  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) => {
@@ -6686,9 +7906,9 @@ function SidebarNavItem({ item, onNavigate, level = 0, currentPath }) {
6686
7906
  // Auto-detect if this item or any child is active based on currentPath
6687
7907
  const isItemActive = currentPath && item.href ? currentPath === item.href : item.active;
6688
7908
  const isChildActive = hasChildren && currentPath
6689
- ? item.children?.some(child => currentPath === child.href || currentPath?.startsWith(child.href || ''))
7909
+ ? item.children?.some(child => child.href && (currentPath === child.href || currentPath.startsWith(child.href)))
6690
7910
  : false;
6691
- const shouldExpandByDefault = isChildActive || (hasChildren && currentPath?.startsWith(item.href || ''));
7911
+ const shouldExpandByDefault = isChildActive || (hasChildren && item.href && currentPath?.startsWith(item.href));
6692
7912
  const [isExpanded, setIsExpanded] = useState(shouldExpandByDefault);
6693
7913
  const handleClick = () => {
6694
7914
  if (hasChildren) {
@@ -6728,15 +7948,692 @@ function SidebarGroup({ title, items, onNavigate, defaultExpanded = true, curren
6728
7948
  }
6729
7949
  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
7950
  }
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) => {
7951
+ /**
7952
+ * Sidebar - Navigation sidebar with mobile drawer support
7953
+ *
7954
+ * On desktop: Renders as a fixed-width sidebar
7955
+ * On mobile: Renders as a drawer overlay when mobileOpen is true
7956
+ *
7957
+ * @example Desktop usage (no mobile props)
7958
+ * ```tsx
7959
+ * <Sidebar
7960
+ * items={navItems}
7961
+ * header={<Logo />}
7962
+ * footer={<UserProfile />}
7963
+ * currentPath={location.pathname}
7964
+ * onNavigate={(href) => navigate(href)}
7965
+ * />
7966
+ * ```
7967
+ *
7968
+ * @example With mobile drawer support
7969
+ * ```tsx
7970
+ * const [mobileOpen, setMobileOpen] = useState(false);
7971
+ *
7972
+ * <Sidebar
7973
+ * items={navItems}
7974
+ * header={<Logo />}
7975
+ * mobileOpen={mobileOpen}
7976
+ * onMobileClose={() => setMobileOpen(false)}
7977
+ * onNavigate={(href) => {
7978
+ * navigate(href);
7979
+ * setMobileOpen(false); // Close drawer on navigation
7980
+ * }}
7981
+ * />
7982
+ * ```
7983
+ */
7984
+ function Sidebar({ items, onNavigate, className = '', header, footer, currentPath, mobileOpen, onMobileClose, width = 'w-64', }) {
7985
+ const sidebarRef = useRef(null);
7986
+ const [isAnimating, setIsAnimating] = useState(false);
7987
+ const [shouldRender, setShouldRender] = useState(mobileOpen);
7988
+ // Handle animation states for mobile drawer
7989
+ useEffect(() => {
7990
+ if (mobileOpen) {
7991
+ setShouldRender(true);
7992
+ // Small delay to trigger animation
7993
+ requestAnimationFrame(() => {
7994
+ setIsAnimating(true);
7995
+ });
7996
+ return; // No cleanup needed when opening
7997
+ }
7998
+ else {
7999
+ setIsAnimating(false);
8000
+ // Wait for animation to complete before unmounting
8001
+ const timer = setTimeout(() => {
8002
+ setShouldRender(false);
8003
+ }, 300);
8004
+ return () => clearTimeout(timer);
8005
+ }
8006
+ }, [mobileOpen]);
8007
+ // Handle escape key for mobile drawer
8008
+ useEffect(() => {
8009
+ if (!mobileOpen)
8010
+ return;
8011
+ const handleEscape = (e) => {
8012
+ if (e.key === 'Escape') {
8013
+ onMobileClose?.();
8014
+ }
8015
+ };
8016
+ document.addEventListener('keydown', handleEscape);
8017
+ return () => document.removeEventListener('keydown', handleEscape);
8018
+ }, [mobileOpen, onMobileClose]);
8019
+ // Lock body scroll when mobile drawer is open
8020
+ useEffect(() => {
8021
+ if (mobileOpen) {
8022
+ document.body.style.overflow = 'hidden';
8023
+ }
8024
+ else {
8025
+ document.body.style.overflow = '';
8026
+ }
8027
+ return () => {
8028
+ document.body.style.overflow = '';
8029
+ };
8030
+ }, [mobileOpen]);
8031
+ // Handle navigation with auto-close on mobile
8032
+ const handleNavigate = (href, external) => {
8033
+ onNavigate?.(href, external);
8034
+ // Auto-close mobile drawer on navigation
8035
+ if (mobileOpen) {
8036
+ onMobileClose?.();
8037
+ }
8038
+ };
8039
+ // Sidebar content (shared between desktop and mobile)
8040
+ 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
8041
  // Render separator
6734
8042
  if (item.separator) {
6735
8043
  return jsx("div", { className: "my-4 border-t border-paper-300" }, item.id);
6736
8044
  }
6737
8045
  // Render nav item
6738
- return (jsx(SidebarNavItem, { item: item, onNavigate: onNavigate, currentPath: currentPath }, item.id));
8046
+ return (jsx(SidebarNavItem, { item: item, onNavigate: handleNavigate, currentPath: currentPath }, item.id));
6739
8047
  }) }), footer && (jsx("div", { className: "border-t border-paper-300 pl-2 pr-6 py-4 overflow-visible", children: footer }))] }));
8048
+ // If mobileOpen is not defined, render as regular sidebar (desktop mode)
8049
+ if (mobileOpen === undefined) {
8050
+ return sidebarContent;
8051
+ }
8052
+ // Mobile drawer mode
8053
+ if (!shouldRender) {
8054
+ return null;
8055
+ }
8056
+ 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: `
8057
+ absolute inset-0 bg-ink-900/50 backdrop-blur-sm
8058
+ transition-opacity duration-300
8059
+ ${isAnimating ? 'opacity-100' : 'opacity-0'}
8060
+ `, onClick: onMobileClose, "aria-hidden": "true" }), jsx("div", { className: `
8061
+ absolute inset-y-0 left-0 flex max-w-full
8062
+ transition-transform duration-300 ease-out
8063
+ ${isAnimating ? 'translate-x-0' : '-translate-x-full'}
8064
+ `, children: sidebarContent })] }), document.body);
8065
+ }
8066
+
8067
+ /**
8068
+ * BottomNavigation - Mobile-style bottom tab bar
8069
+ *
8070
+ * iOS/Android-style fixed bottom navigation with icons, labels, and badges.
8071
+ * Handles safe area insets for notched devices automatically.
8072
+ *
8073
+ * Best practices:
8074
+ * - Use 3-5 items maximum
8075
+ * - Keep labels short (1-2 words)
8076
+ * - Use consistent icon style
8077
+ *
8078
+ * @example Basic usage
8079
+ * ```tsx
8080
+ * import { BottomNavigation } from 'notebook-ui';
8081
+ * import { Home, Search, Bell, User } from 'lucide-react';
8082
+ *
8083
+ * const navItems = [
8084
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8085
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8086
+ * { id: 'notifications', label: 'Alerts', icon: <Bell />, badge: 3 },
8087
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' },
8088
+ * ];
8089
+ *
8090
+ * <BottomNavigation
8091
+ * items={navItems}
8092
+ * activeId="home"
8093
+ * onNavigate={(id, href) => navigate(href)}
8094
+ * />
8095
+ * ```
8096
+ *
8097
+ * @example With onClick handlers
8098
+ * ```tsx
8099
+ * const navItems = [
8100
+ * { id: 'home', label: 'Home', icon: <Home />, onClick: () => setTab('home') },
8101
+ * { id: 'add', label: 'Add', icon: <Plus />, onClick: openAddModal },
8102
+ * ];
8103
+ *
8104
+ * <BottomNavigation items={navItems} activeId={currentTab} />
8105
+ * ```
8106
+ */
8107
+ function BottomNavigation({ items, activeId, onNavigate, showLabels = true, className = '', safeArea = true, }) {
8108
+ const handleItemClick = (item) => {
8109
+ if (item.disabled)
8110
+ return;
8111
+ if (item.onClick) {
8112
+ item.onClick();
8113
+ }
8114
+ if (onNavigate) {
8115
+ onNavigate(item.id, item.href);
8116
+ }
8117
+ };
8118
+ return (jsx("nav", { className: `
8119
+ fixed bottom-0 left-0 right-0 z-40
8120
+ bg-white border-t border-paper-200 shadow-lg
8121
+ ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}
8122
+ ${className}
8123
+ `, 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) => {
8124
+ const isActive = item.id === activeId;
8125
+ return (jsxs("button", { onClick: () => handleItemClick(item), disabled: item.disabled, className: `
8126
+ relative flex flex-col items-center justify-center
8127
+ flex-1 h-full min-w-touch-sm
8128
+ transition-colors duration-200
8129
+ ${item.disabled
8130
+ ? 'opacity-40 cursor-not-allowed'
8131
+ : 'active:bg-paper-100'}
8132
+ ${isActive
8133
+ ? 'text-accent-600'
8134
+ : 'text-ink-500 hover:text-ink-700'}
8135
+ `, "aria-current": isActive ? 'page' : undefined, "aria-label": item.label, children: [jsxs("div", { className: "relative", children: [jsx("div", { className: `
8136
+ w-6 h-6 flex items-center justify-center
8137
+ transition-transform duration-200
8138
+ ${isActive ? 'scale-110' : 'scale-100'}
8139
+ `, children: React__default.isValidElement(item.icon)
8140
+ ? React__default.cloneElement(item.icon, {
8141
+ className: 'w-6 h-6',
8142
+ })
8143
+ : item.icon }), item.badge !== undefined && item.badge > 0 && (jsx("span", { className: `
8144
+ absolute -top-1 -right-2.5
8145
+ min-w-[18px] h-[18px] px-1
8146
+ flex items-center justify-center
8147
+ text-[10px] font-bold text-white
8148
+ bg-error-500 rounded-full
8149
+ ${item.badge > 99 ? 'text-[8px]' : ''}
8150
+ `, children: item.badge > 99 ? '99+' : item.badge }))] }), showLabels && (jsx("span", { className: `
8151
+ mt-1 text-[10px] font-medium leading-none
8152
+ transition-opacity duration-200
8153
+ truncate max-w-full px-1
8154
+ ${isActive ? 'opacity-100' : 'opacity-70'}
8155
+ `, 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));
8156
+ }) }) }));
8157
+ }
8158
+ /**
8159
+ * BottomNavigationSpacer - Spacer to prevent content from being hidden behind BottomNavigation
8160
+ *
8161
+ * Place this at the bottom of your scrollable content when using BottomNavigation.
8162
+ *
8163
+ * @example
8164
+ * ```tsx
8165
+ * <div className="flex flex-col h-screen">
8166
+ * <main className="flex-1 overflow-auto">
8167
+ * {/* Your content *\/}
8168
+ * <BottomNavigationSpacer />
8169
+ * </main>
8170
+ * <BottomNavigation items={navItems} />
8171
+ * </div>
8172
+ * ```
8173
+ */
8174
+ function BottomNavigationSpacer({ safeArea = true }) {
8175
+ return (jsx("div", { className: `h-14 ${safeArea ? 'pb-[env(safe-area-inset-bottom)]' : ''}`, "aria-hidden": "true" }));
8176
+ }
8177
+
8178
+ /**
8179
+ * MobileHeader - Mobile app header with navigation controls
8180
+ *
8181
+ * A flexible mobile header component with support for:
8182
+ * - Hamburger menu button (default)
8183
+ * - Back navigation arrow
8184
+ * - Close button (X)
8185
+ * - Custom left/right actions
8186
+ * - Sticky positioning
8187
+ * - Blur/transparent variants
8188
+ *
8189
+ * @example Basic with menu button
8190
+ * ```tsx
8191
+ * <MobileHeader
8192
+ * title="Dashboard"
8193
+ * onMenuClick={() => setDrawerOpen(true)}
8194
+ * />
8195
+ * ```
8196
+ *
8197
+ * @example With back button
8198
+ * ```tsx
8199
+ * <MobileHeader
8200
+ * title="User Details"
8201
+ * subtitle="Profile"
8202
+ * onBackClick={() => navigate(-1)}
8203
+ * />
8204
+ * ```
8205
+ *
8206
+ * @example With right action
8207
+ * ```tsx
8208
+ * <MobileHeader
8209
+ * title="Settings"
8210
+ * onMenuClick={openMenu}
8211
+ * rightAction={
8212
+ * <Button variant="ghost" iconOnly onClick={save}>
8213
+ * <Check className="w-5 h-5" />
8214
+ * </Button>
8215
+ * }
8216
+ * />
8217
+ * ```
8218
+ *
8219
+ * @example Transparent with blur
8220
+ * ```tsx
8221
+ * <MobileHeader
8222
+ * title="Photo Gallery"
8223
+ * variant="blur"
8224
+ * onBackClick={goBack}
8225
+ * />
8226
+ * ```
8227
+ */
8228
+ function MobileHeader({ title, subtitle, onMenuClick, onBackClick, onCloseClick, rightAction, leftAction, sticky = true, bordered = true, variant = 'solid', className = '', safeArea = true, }) {
8229
+ // Determine which left button to show
8230
+ const renderLeftButton = () => {
8231
+ // Custom left action takes priority
8232
+ if (leftAction) {
8233
+ return leftAction;
8234
+ }
8235
+ // Close button
8236
+ if (onCloseClick) {
8237
+ 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" }) }));
8238
+ }
8239
+ // Back button
8240
+ if (onBackClick) {
8241
+ 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" }) }));
8242
+ }
8243
+ // Menu button (default)
8244
+ if (onMenuClick) {
8245
+ 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" }) }));
8246
+ }
8247
+ // No left button
8248
+ return jsx("div", { className: "w-10 h-10" });
8249
+ };
8250
+ // Background variant styles
8251
+ const variantStyles = {
8252
+ solid: 'bg-white',
8253
+ transparent: 'bg-transparent',
8254
+ blur: 'bg-white/80 backdrop-blur-md',
8255
+ };
8256
+ return (jsx("header", { className: `
8257
+ ${sticky ? 'sticky top-0 z-30' : ''}
8258
+ ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}
8259
+ ${variantStyles[variant]}
8260
+ ${bordered ? 'border-b border-paper-200' : ''}
8261
+ ${className}
8262
+ `, 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 })] }) }));
8263
+ }
8264
+ /**
8265
+ * MobileHeaderSpacer - Spacer to prevent content from being hidden behind sticky MobileHeader
8266
+ *
8267
+ * Place this at the top of your content when NOT using sticky header
8268
+ * to maintain consistent spacing.
8269
+ *
8270
+ * @example
8271
+ * ```tsx
8272
+ * <MobileHeader title="Page" sticky={false} />
8273
+ * <MobileHeaderSpacer />
8274
+ * <main>Content here</main>
8275
+ * ```
8276
+ */
8277
+ function MobileHeaderSpacer({ safeArea = true }) {
8278
+ return (jsx("div", { className: `h-14 ${safeArea ? 'pt-[env(safe-area-inset-top)]' : ''}`, "aria-hidden": "true" }));
8279
+ }
8280
+
8281
+ const positionClasses = {
8282
+ 'bottom-right': 'right-4 bottom-4',
8283
+ 'bottom-left': 'left-4 bottom-4',
8284
+ 'bottom-center': 'left-1/2 -translate-x-1/2 bottom-4',
8285
+ };
8286
+ const variantClasses = {
8287
+ primary: 'bg-accent-600 hover:bg-accent-700 text-white shadow-lg',
8288
+ secondary: 'bg-white hover:bg-paper-50 text-ink-700 shadow-lg border border-paper-200',
8289
+ accent: 'bg-accent-500 hover:bg-accent-600 text-white shadow-lg',
8290
+ };
8291
+ const sizeClasses$1 = {
8292
+ md: 'w-14 h-14',
8293
+ lg: 'w-16 h-16',
8294
+ };
8295
+ const iconSizeClasses = {
8296
+ md: 'h-6 w-6',
8297
+ lg: 'h-7 w-7',
8298
+ };
8299
+ /**
8300
+ * FloatingActionButton - Material Design style FAB for mobile
8301
+ *
8302
+ * A prominent button for the primary action on a screen.
8303
+ * Supports single action or expandable menu with multiple actions.
8304
+ *
8305
+ * @example Simple FAB
8306
+ * ```tsx
8307
+ * <FloatingActionButton
8308
+ * onClick={() => openCreateModal()}
8309
+ * label="Create new item"
8310
+ * />
8311
+ * ```
8312
+ *
8313
+ * @example FAB with action menu
8314
+ * ```tsx
8315
+ * <FloatingActionButton
8316
+ * actions={[
8317
+ * { id: 'photo', icon: <Camera />, label: 'Take Photo', onClick: takePhoto },
8318
+ * { id: 'upload', icon: <Upload />, label: 'Upload File', onClick: uploadFile },
8319
+ * { id: 'note', icon: <FileText />, label: 'Create Note', onClick: createNote },
8320
+ * ]}
8321
+ * />
8322
+ * ```
8323
+ *
8324
+ * @example Extended FAB
8325
+ * ```tsx
8326
+ * <FloatingActionButton
8327
+ * extended
8328
+ * extendedLabel="New Task"
8329
+ * icon={<Plus />}
8330
+ * onClick={createTask}
8331
+ * />
8332
+ * ```
8333
+ */
8334
+ function FloatingActionButton({ onClick, icon, actions, position = 'bottom-right', variant = 'primary', size = 'md', label = 'Action button', extended = false, extendedLabel, hidden = false, offset, className = '', }) {
8335
+ const [isMenuOpen, setIsMenuOpen] = useState(false);
8336
+ const fabRef = useRef(null);
8337
+ const hasMenu = actions && actions.length > 0;
8338
+ // Close menu on escape
8339
+ useEffect(() => {
8340
+ if (!isMenuOpen)
8341
+ return;
8342
+ const handleEscape = (e) => {
8343
+ if (e.key === 'Escape') {
8344
+ setIsMenuOpen(false);
8345
+ }
8346
+ };
8347
+ document.addEventListener('keydown', handleEscape);
8348
+ return () => document.removeEventListener('keydown', handleEscape);
8349
+ }, [isMenuOpen]);
8350
+ // Close menu on click outside
8351
+ useEffect(() => {
8352
+ if (!isMenuOpen)
8353
+ return;
8354
+ const handleClickOutside = (e) => {
8355
+ if (fabRef.current && !fabRef.current.contains(e.target)) {
8356
+ setIsMenuOpen(false);
8357
+ }
8358
+ };
8359
+ document.addEventListener('mousedown', handleClickOutside);
8360
+ return () => document.removeEventListener('mousedown', handleClickOutside);
8361
+ }, [isMenuOpen]);
8362
+ const handleClick = () => {
8363
+ if (hasMenu) {
8364
+ setIsMenuOpen(!isMenuOpen);
8365
+ }
8366
+ else if (onClick) {
8367
+ onClick();
8368
+ }
8369
+ };
8370
+ const handleActionClick = (action) => {
8371
+ if (!action.disabled) {
8372
+ action.onClick();
8373
+ setIsMenuOpen(false);
8374
+ }
8375
+ };
8376
+ // Custom offset styles
8377
+ const offsetStyle = offset ? {
8378
+ ...(offset.x !== undefined && position.includes('right') ? { right: `${offset.x}px` } : {}),
8379
+ ...(offset.x !== undefined && position.includes('left') ? { left: `${offset.x}px` } : {}),
8380
+ ...(offset.y !== undefined ? { bottom: `${offset.y}px` } : {}),
8381
+ } : {};
8382
+ const fabContent = (jsxs("div", { className: `
8383
+ fixed z-40 transition-all duration-300
8384
+ ${positionClasses[position]}
8385
+ ${hidden ? 'translate-y-20 opacity-0 pointer-events-none' : 'translate-y-0 opacity-100'}
8386
+ ${className}
8387
+ `, style: {
8388
+ ...offsetStyle,
8389
+ paddingBottom: 'env(safe-area-inset-bottom)',
8390
+ }, 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: `
8391
+ w-12 h-12 rounded-full flex items-center justify-center
8392
+ transition-all duration-200
8393
+ ${action.disabled
8394
+ ? 'bg-paper-200 text-ink-400 cursor-not-allowed'
8395
+ : 'bg-white text-ink-700 shadow-lg hover:bg-paper-50 active:scale-95'}
8396
+ `, "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: `
8397
+ ${extended ? 'px-6 rounded-full' : 'rounded-full'}
8398
+ ${extended ? 'h-14' : sizeClasses$1[size]}
8399
+ ${variantClasses[variant]}
8400
+ flex items-center justify-center gap-2
8401
+ transition-all duration-200
8402
+ active:scale-95
8403
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
8404
+ `, "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 }))] })] }));
8405
+ // Render via portal to ensure proper stacking
8406
+ return createPortal(fabContent, document.body);
8407
+ }
8408
+ /**
8409
+ * Hook for scroll-based FAB visibility
8410
+ *
8411
+ * @example
8412
+ * ```tsx
8413
+ * const { hidden, scrollDirection } = useFABScroll();
8414
+ * <FloatingActionButton hidden={hidden} />
8415
+ * ```
8416
+ */
8417
+ function useFABScroll(threshold = 10) {
8418
+ const [hidden, setHidden] = useState(false);
8419
+ const [scrollDirection, setScrollDirection] = useState(null);
8420
+ const lastScrollY = useRef(0);
8421
+ useEffect(() => {
8422
+ const handleScroll = () => {
8423
+ const currentScrollY = window.scrollY;
8424
+ const diff = currentScrollY - lastScrollY.current;
8425
+ if (Math.abs(diff) > threshold) {
8426
+ if (diff > 0) {
8427
+ setHidden(true);
8428
+ setScrollDirection('down');
8429
+ }
8430
+ else {
8431
+ setHidden(false);
8432
+ setScrollDirection('up');
8433
+ }
8434
+ lastScrollY.current = currentScrollY;
8435
+ }
8436
+ };
8437
+ window.addEventListener('scroll', handleScroll, { passive: true });
8438
+ return () => window.removeEventListener('scroll', handleScroll);
8439
+ }, [threshold]);
8440
+ return { hidden, scrollDirection };
8441
+ }
8442
+
8443
+ /**
8444
+ * PullToRefresh - Mobile pull-to-refresh gesture handler
8445
+ *
8446
+ * Wraps content and provides native-feeling pull-to-refresh functionality.
8447
+ * Only activates when scrolled to top of content.
8448
+ *
8449
+ * @example Basic usage
8450
+ * ```tsx
8451
+ * <PullToRefresh onRefresh={async () => {
8452
+ * await fetchLatestData();
8453
+ * }}>
8454
+ * <div className="min-h-screen">
8455
+ * {content}
8456
+ * </div>
8457
+ * </PullToRefresh>
8458
+ * ```
8459
+ *
8460
+ * @example With custom threshold
8461
+ * ```tsx
8462
+ * <PullToRefresh
8463
+ * onRefresh={handleRefresh}
8464
+ * pullThreshold={100}
8465
+ * maxPull={150}
8466
+ * >
8467
+ * {content}
8468
+ * </PullToRefresh>
8469
+ * ```
8470
+ */
8471
+ function PullToRefresh({ children, onRefresh, disabled = false, pullThreshold = 80, maxPull = 120, loadingIndicator, pullIndicator, className = '', }) {
8472
+ const [state, setState] = useState('idle');
8473
+ const [pullDistance, setPullDistance] = useState(0);
8474
+ const containerRef = useRef(null);
8475
+ const startY = useRef(0);
8476
+ const currentY = useRef(0);
8477
+ // Check if at top of scroll container
8478
+ const isAtTop = useCallback(() => {
8479
+ const container = containerRef.current;
8480
+ if (!container)
8481
+ return false;
8482
+ return container.scrollTop <= 0;
8483
+ }, []);
8484
+ // Handle touch start
8485
+ const handleTouchStart = useCallback((e) => {
8486
+ if (disabled || state === 'refreshing' || !isAtTop())
8487
+ return;
8488
+ startY.current = e.touches[0].clientY;
8489
+ currentY.current = startY.current;
8490
+ }, [disabled, state, isAtTop]);
8491
+ // Handle touch move
8492
+ const handleTouchMove = useCallback((e) => {
8493
+ if (disabled || state === 'refreshing')
8494
+ return;
8495
+ if (startY.current === 0)
8496
+ return;
8497
+ currentY.current = e.touches[0].clientY;
8498
+ const diff = currentY.current - startY.current;
8499
+ // Only allow pulling down when at top
8500
+ if (diff > 0 && isAtTop()) {
8501
+ // Apply resistance - pull slows down as distance increases
8502
+ const resistance = 0.5;
8503
+ const adjustedPull = Math.min(diff * resistance, maxPull);
8504
+ setPullDistance(adjustedPull);
8505
+ setState(adjustedPull >= pullThreshold ? 'ready' : 'pulling');
8506
+ // Prevent default scroll when pulling
8507
+ if (adjustedPull > 0) {
8508
+ e.preventDefault();
8509
+ }
8510
+ }
8511
+ }, [disabled, state, isAtTop, maxPull, pullThreshold]);
8512
+ // Handle touch end
8513
+ const handleTouchEnd = useCallback(async () => {
8514
+ if (disabled || state === 'refreshing')
8515
+ return;
8516
+ if (state === 'ready') {
8517
+ setState('refreshing');
8518
+ setPullDistance(pullThreshold); // Hold at threshold while refreshing
8519
+ try {
8520
+ await onRefresh();
8521
+ }
8522
+ catch (error) {
8523
+ console.error('Refresh failed:', error);
8524
+ }
8525
+ setState('idle');
8526
+ }
8527
+ setPullDistance(0);
8528
+ startY.current = 0;
8529
+ currentY.current = 0;
8530
+ }, [disabled, state, pullThreshold, onRefresh]);
8531
+ // Attach touch listeners
8532
+ useEffect(() => {
8533
+ const container = containerRef.current;
8534
+ if (!container)
8535
+ return;
8536
+ container.addEventListener('touchstart', handleTouchStart, { passive: true });
8537
+ container.addEventListener('touchmove', handleTouchMove, { passive: false });
8538
+ container.addEventListener('touchend', handleTouchEnd);
8539
+ return () => {
8540
+ container.removeEventListener('touchstart', handleTouchStart);
8541
+ container.removeEventListener('touchmove', handleTouchMove);
8542
+ container.removeEventListener('touchend', handleTouchEnd);
8543
+ };
8544
+ }, [handleTouchStart, handleTouchMove, handleTouchEnd]);
8545
+ // Calculate indicator opacity and rotation
8546
+ const progress = Math.min(pullDistance / pullThreshold, 1);
8547
+ const rotation = progress * 180;
8548
+ // Default loading indicator
8549
+ const defaultLoadingIndicator = (jsx(Loader2, { className: "h-6 w-6 text-accent-600 animate-spin" }));
8550
+ // Default pull indicator
8551
+ const defaultPullIndicator = (jsx("div", { className: `
8552
+ transition-transform duration-200
8553
+ ${state === 'ready' ? 'text-accent-600' : 'text-ink-400'}
8554
+ `, style: { transform: `rotate(${rotation}deg)` }, children: jsx(ArrowDown, { className: "h-6 w-6" }) }));
8555
+ return (jsxs("div", { ref: containerRef, className: `relative overflow-auto ${className}`, style: { touchAction: pullDistance > 0 ? 'none' : 'auto' }, children: [jsx("div", { className: `
8556
+ absolute left-0 right-0 flex items-center justify-center
8557
+ transition-all duration-200 overflow-hidden
8558
+ ${state === 'idle' && pullDistance === 0 ? 'opacity-0' : 'opacity-100'}
8559
+ `, style: {
8560
+ height: `${pullDistance}px`,
8561
+ top: 0,
8562
+ zIndex: 10,
8563
+ }, children: jsx("div", { className: `
8564
+ w-10 h-10 rounded-full bg-white shadow-md
8565
+ flex items-center justify-center
8566
+ transition-transform duration-200
8567
+ ${state === 'refreshing' ? 'scale-100' : progress < 0.3 ? 'scale-75' : 'scale-100'}
8568
+ `, children: state === 'refreshing'
8569
+ ? (loadingIndicator || defaultLoadingIndicator)
8570
+ : (pullIndicator || defaultPullIndicator) }) }), jsx("div", { className: "transition-transform duration-200", style: {
8571
+ transform: `translateY(${pullDistance}px)`,
8572
+ }, children: children })] }));
8573
+ }
8574
+ /**
8575
+ * usePullToRefresh - Hook for custom pull-to-refresh implementations
8576
+ *
8577
+ * @example
8578
+ * ```tsx
8579
+ * const { pullDistance, isRefreshing, bind } = usePullToRefresh({
8580
+ * onRefresh: async () => {
8581
+ * await fetchData();
8582
+ * }
8583
+ * });
8584
+ *
8585
+ * return (
8586
+ * <div {...bind}>
8587
+ * {isRefreshing && <Spinner />}
8588
+ * {content}
8589
+ * </div>
8590
+ * );
8591
+ * ```
8592
+ */
8593
+ function usePullToRefresh({ onRefresh, pullThreshold = 80, maxPull = 120, disabled = false, }) {
8594
+ const [pullDistance, setPullDistance] = useState(0);
8595
+ const [isRefreshing, setIsRefreshing] = useState(false);
8596
+ const startY = useRef(0);
8597
+ const handleTouchStart = useCallback((e) => {
8598
+ if (disabled || isRefreshing)
8599
+ return;
8600
+ startY.current = e.touches[0].clientY;
8601
+ }, [disabled, isRefreshing]);
8602
+ const handleTouchMove = useCallback((e) => {
8603
+ if (disabled || isRefreshing || startY.current === 0)
8604
+ return;
8605
+ const diff = e.touches[0].clientY - startY.current;
8606
+ if (diff > 0) {
8607
+ const adjustedPull = Math.min(diff * 0.5, maxPull);
8608
+ setPullDistance(adjustedPull);
8609
+ }
8610
+ }, [disabled, isRefreshing, maxPull]);
8611
+ const handleTouchEnd = useCallback(async () => {
8612
+ if (disabled || isRefreshing)
8613
+ return;
8614
+ if (pullDistance >= pullThreshold) {
8615
+ setIsRefreshing(true);
8616
+ try {
8617
+ await onRefresh();
8618
+ }
8619
+ finally {
8620
+ setIsRefreshing(false);
8621
+ }
8622
+ }
8623
+ setPullDistance(0);
8624
+ startY.current = 0;
8625
+ }, [disabled, isRefreshing, pullDistance, pullThreshold, onRefresh]);
8626
+ return {
8627
+ pullDistance,
8628
+ isRefreshing,
8629
+ isReady: pullDistance >= pullThreshold,
8630
+ progress: Math.min(pullDistance / pullThreshold, 1),
8631
+ bind: {
8632
+ onTouchStart: handleTouchStart,
8633
+ onTouchMove: handleTouchMove,
8634
+ onTouchEnd: handleTouchEnd,
8635
+ },
8636
+ };
6740
8637
  }
6741
8638
 
6742
8639
  function Logo({ size = 'md', showText = true, text = 'Commora', className = '', }) {
@@ -6852,6 +8749,125 @@ const Layout = ({ sidebar, children, statusBar, className = '', sections }) => {
6852
8749
  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
8750
  };
6854
8751
 
8752
+ /**
8753
+ * MobileLayout - Auto-responsive layout that switches between desktop and mobile patterns
8754
+ *
8755
+ * This component automatically detects the viewport size and renders the appropriate layout:
8756
+ * - **Desktop** (≥1024px): Standard Layout with sidebar, gutter, and scrollable content
8757
+ * - **Mobile/Tablet** (<1024px): Mobile header, drawer navigation, bottom tab bar
8758
+ *
8759
+ * The mobile layout features:
8760
+ * - Sticky header with hamburger menu to open drawer
8761
+ * - Sidebar rendered as a slide-in drawer
8762
+ * - Bottom navigation bar for primary navigation
8763
+ * - Safe area support for notched devices
8764
+ *
8765
+ * @example Basic usage
8766
+ * ```tsx
8767
+ * <MobileLayout
8768
+ * sidebarItems={[
8769
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8770
+ * { id: 'tasks', label: 'Tasks', icon: <CheckSquare />, href: '/tasks' },
8771
+ * { id: 'settings', label: 'Settings', icon: <Settings />, href: '/settings' }
8772
+ * ]}
8773
+ * currentPath={location.pathname}
8774
+ * onNavigate={(href) => navigate(href)}
8775
+ * title="My App"
8776
+ * header={<Logo />}
8777
+ * userProfile={<UserProfileButton user={user} />}
8778
+ * >
8779
+ * <Page>
8780
+ * <h1>Dashboard</h1>
8781
+ * </Page>
8782
+ * </MobileLayout>
8783
+ * ```
8784
+ *
8785
+ * @example With custom bottom nav items
8786
+ * ```tsx
8787
+ * <MobileLayout
8788
+ * sidebarItems={fullNavItems}
8789
+ * bottomNavItems={[
8790
+ * { id: 'home', label: 'Home', icon: <Home />, href: '/' },
8791
+ * { id: 'search', label: 'Search', icon: <Search />, href: '/search' },
8792
+ * { id: 'profile', label: 'Profile', icon: <User />, href: '/profile' }
8793
+ * ]}
8794
+ * currentPath={location.pathname}
8795
+ * title="My App"
8796
+ * >
8797
+ * {children}
8798
+ * </MobileLayout>
8799
+ * ```
8800
+ *
8801
+ * @example Force mobile layout for testing
8802
+ * ```tsx
8803
+ * <MobileLayout
8804
+ * sidebarItems={items}
8805
+ * title="Mobile Preview"
8806
+ * forceMobile
8807
+ * >
8808
+ * {children}
8809
+ * </MobileLayout>
8810
+ * ```
8811
+ */
8812
+ 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, }) => {
8813
+ const isMobileViewport = useIsMobile();
8814
+ const isTabletViewport = useIsTablet();
8815
+ const [drawerOpen, setDrawerOpen] = useState(false);
8816
+ // Determine if we should use mobile layout
8817
+ const useMobileLayout = forceDesktop
8818
+ ? false
8819
+ : forceMobile || isMobileViewport || isTabletViewport;
8820
+ // Open/close drawer
8821
+ const openDrawer = useCallback(() => setDrawerOpen(true), []);
8822
+ const closeDrawer = useCallback(() => setDrawerOpen(false), []);
8823
+ // Handle navigation from drawer - close drawer after navigation
8824
+ const handleDrawerNavigate = useCallback((href) => {
8825
+ closeDrawer();
8826
+ onNavigate?.(href);
8827
+ }, [closeDrawer, onNavigate]);
8828
+ // Handle bottom nav navigation - matches BottomNavigation's onNavigate signature
8829
+ const handleBottomNavNavigate = useCallback((id, href) => {
8830
+ if (href) {
8831
+ onNavigate?.(href);
8832
+ }
8833
+ // Also check if there's a custom onClick in the bottom nav items
8834
+ const item = bottomNavItems?.find(i => i.id === id);
8835
+ item?.onClick?.();
8836
+ }, [onNavigate, bottomNavItems]);
8837
+ // Convert sidebar items to bottom nav items if not provided
8838
+ const effectiveBottomNavItems = bottomNavItems || sidebarItems
8839
+ .filter(item => !item.children && item.href) // Only top-level items with href
8840
+ .slice(0, 5) // Max 5 items for bottom nav
8841
+ .map(item => ({
8842
+ id: item.id,
8843
+ label: item.label,
8844
+ icon: item.icon,
8845
+ href: item.href,
8846
+ badge: typeof item.badge === 'number' ? item.badge : undefined,
8847
+ }));
8848
+ // Determine active bottom nav ID
8849
+ const effectiveActiveBottomNavId = activeBottomNavId ||
8850
+ effectiveBottomNavItems.find(item => item.href === currentPath)?.id;
8851
+ // Close drawer on escape key
8852
+ useEffect(() => {
8853
+ if (!drawerOpen)
8854
+ return;
8855
+ const handleEscape = (e) => {
8856
+ if (e.key === 'Escape') {
8857
+ closeDrawer();
8858
+ }
8859
+ };
8860
+ window.addEventListener('keydown', handleEscape);
8861
+ return () => window.removeEventListener('keydown', handleEscape);
8862
+ }, [drawerOpen, closeDrawer]);
8863
+ // Desktop Layout
8864
+ if (!useMobileLayout) {
8865
+ 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] }));
8866
+ }
8867
+ // Mobile Layout
8868
+ 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 })] }))] }));
8869
+ };
8870
+
6855
8871
  /**
6856
8872
  * Two-column content layout component that provides:
6857
8873
  * - Sidebar column on the left (takes 1/3 of width)
@@ -7105,6 +9121,185 @@ function NotificationIndicator({ count = 0, onClick, className = '', maxCount =
7105
9121
  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
9122
  }
7107
9123
 
9124
+ /**
9125
+ * Get value from item by key path (supports nested keys like 'user.name')
9126
+ */
9127
+ function getValueByKey(item, key) {
9128
+ if (typeof key !== 'string') {
9129
+ return item[key];
9130
+ }
9131
+ const keys = key.split('.');
9132
+ let value = item;
9133
+ for (const k of keys) {
9134
+ if (value == null)
9135
+ return undefined;
9136
+ value = value[k];
9137
+ }
9138
+ return value;
9139
+ }
9140
+ /**
9141
+ * Skeleton card for loading state
9142
+ */
9143
+ function SkeletonCard({ className = '' }) {
9144
+ 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" })] }) }));
9145
+ }
9146
+ /**
9147
+ * DataTableCardView - Mobile-friendly card view for data tables
9148
+ *
9149
+ * Renders data as cards instead of table rows, optimized for touch interaction.
9150
+ * Automatically uses column render functions for consistent data display.
9151
+ *
9152
+ * @example Basic usage
9153
+ * ```tsx
9154
+ * <DataTableCardView
9155
+ * data={users}
9156
+ * columns={columns}
9157
+ * cardConfig={{
9158
+ * titleKey: 'name',
9159
+ * subtitleKey: 'email',
9160
+ * metadataKeys: ['department', 'role'],
9161
+ * badgeKey: 'status',
9162
+ * }}
9163
+ * onCardClick={(user) => navigate(`/users/${user.id}`)}
9164
+ * />
9165
+ * ```
9166
+ *
9167
+ * @example With selection
9168
+ * ```tsx
9169
+ * <DataTableCardView
9170
+ * data={orders}
9171
+ * columns={columns}
9172
+ * cardConfig={{
9173
+ * titleKey: 'orderNumber',
9174
+ * subtitleKey: 'customer',
9175
+ * badgeKey: 'status',
9176
+ * }}
9177
+ * selectable
9178
+ * selectedRows={selectedOrders}
9179
+ * onSelectionChange={setSelectedOrders}
9180
+ * />
9181
+ * ```
9182
+ */
9183
+ 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', }) {
9184
+ const gapClasses = {
9185
+ sm: 'gap-2',
9186
+ md: 'gap-3',
9187
+ lg: 'gap-4',
9188
+ };
9189
+ // Find column by key to use its render function
9190
+ const getColumn = (key) => {
9191
+ return columns.find(col => col.key === key);
9192
+ };
9193
+ // Render a value using column's render function if available
9194
+ const renderValue = (item, key) => {
9195
+ const column = getColumn(key);
9196
+ const value = getValueByKey(item, key);
9197
+ if (column?.render) {
9198
+ return column.render(item, value);
9199
+ }
9200
+ if (value == null)
9201
+ return '-';
9202
+ if (typeof value === 'boolean')
9203
+ return value ? 'Yes' : 'No';
9204
+ if (value instanceof Date)
9205
+ return value.toLocaleDateString();
9206
+ return String(value);
9207
+ };
9208
+ // Handle card selection toggle
9209
+ const handleSelectionToggle = (item, event) => {
9210
+ event.stopPropagation();
9211
+ const key = keyExtractor(item);
9212
+ const newSelected = new Set(selectedRows);
9213
+ if (newSelected.has(key)) {
9214
+ newSelected.delete(key);
9215
+ }
9216
+ else {
9217
+ newSelected.add(key);
9218
+ }
9219
+ onSelectionChange?.(Array.from(newSelected));
9220
+ };
9221
+ // Handle card click
9222
+ const handleCardClick = (item) => {
9223
+ if (selectable && selectedRows.size > 0) {
9224
+ // If in selection mode, toggle selection instead
9225
+ const key = keyExtractor(item);
9226
+ const newSelected = new Set(selectedRows);
9227
+ if (newSelected.has(key)) {
9228
+ newSelected.delete(key);
9229
+ }
9230
+ else {
9231
+ newSelected.add(key);
9232
+ }
9233
+ onSelectionChange?.(Array.from(newSelected));
9234
+ }
9235
+ else {
9236
+ onCardClick?.(item);
9237
+ }
9238
+ };
9239
+ // Handle long press for context actions
9240
+ const handleLongPress = (item, event) => {
9241
+ onCardLongPress?.(item, event);
9242
+ };
9243
+ // Loading state
9244
+ if (loading) {
9245
+ return (jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: Array.from({ length: loadingRows }).map((_, i) => (jsx(SkeletonCard, { className: cardClassName }, i))) }));
9246
+ }
9247
+ // Empty state
9248
+ if (data.length === 0) {
9249
+ 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 }) }));
9250
+ }
9251
+ // Determine default card config if not provided
9252
+ const config = cardConfig || {
9253
+ titleKey: columns[0]?.key || 'id',
9254
+ subtitleKey: columns[1]?.key,
9255
+ metadataKeys: columns.slice(2, 5).map(c => c.key),
9256
+ };
9257
+ return (jsx("div", { className: `flex flex-col ${gapClasses[gap]} ${className}`, children: data.map((item) => {
9258
+ const key = keyExtractor(item);
9259
+ const isSelected = selectedRows.has(key);
9260
+ // Custom card render
9261
+ if (config.renderCard) {
9262
+ return (jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9263
+ e.preventDefault();
9264
+ handleLongPress(item, e);
9265
+ }, className: `
9266
+ cursor-pointer transition-all duration-200
9267
+ ${isSelected ? 'ring-2 ring-accent-500' : ''}
9268
+ ${cardClassName}
9269
+ `, children: config.renderCard(item, columns) }, key));
9270
+ }
9271
+ // Default card layout
9272
+ const titleColumn = getColumn(config.titleKey);
9273
+ const titleValue = getValueByKey(item, config.titleKey);
9274
+ return (jsx("div", { onClick: () => handleCardClick(item), onContextMenu: (e) => {
9275
+ e.preventDefault();
9276
+ handleLongPress(item, e);
9277
+ }, className: `
9278
+ bg-white rounded-lg border border-paper-200 p-4
9279
+ transition-all duration-200 cursor-pointer
9280
+ active:scale-[0.98] active:bg-paper-50
9281
+ ${isSelected ? 'ring-2 ring-accent-500 bg-accent-50/30' : 'hover:shadow-md hover:border-paper-300'}
9282
+ ${cardClassName}
9283
+ `, 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: (() => {
9284
+ const avatarValue = getValueByKey(item, config.avatarKey);
9285
+ if (typeof avatarValue === 'string' && avatarValue.startsWith('http')) {
9286
+ return (jsx("img", { src: avatarValue, alt: "", className: "w-10 h-10 rounded-full object-cover" }));
9287
+ }
9288
+ // Render initials or placeholder
9289
+ const initials = String(titleValue || '').slice(0, 2).toUpperCase();
9290
+ 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 }));
9291
+ })() })), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("div", { className: "font-medium text-ink-900 truncate", children: titleColumn?.render
9292
+ ? titleColumn.render(item, titleValue)
9293
+ : 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) => {
9294
+ const column = getColumn(metaKey);
9295
+ 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)));
9296
+ }) }))] }), 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) => {
9297
+ e.stopPropagation();
9298
+ handleLongPress(item, e);
9299
+ }, 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));
9300
+ }) }));
9301
+ }
9302
+
7108
9303
  /**
7109
9304
  * ActionMenu - Inline dropdown menu for row actions
7110
9305
  */
@@ -7263,7 +9458,11 @@ function DataTable({ data, columns, loading = false, error = null, emptyMessage
7263
9458
  // Visual customization props
7264
9459
  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
9460
  // Pagination props
7266
- paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true, }) {
9461
+ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pageSizeOptions = [10, 25, 50, 100], onPageSizeChange, showPageSizeSelector = true,
9462
+ // Mobile view props
9463
+ mobileView = 'auto', cardConfig, cardGap = 'md', cardClassName, }) {
9464
+ // Mobile detection for auto mode
9465
+ const isMobileViewport = useIsMobile();
7267
9466
  // Column resizing state
7268
9467
  const [columnWidths, setColumnWidths] = useState({});
7269
9468
  const [resizingColumn, setResizingColumn] = useState(null);
@@ -7882,8 +10081,274 @@ paginated = false, currentPage = 1, pageSize = 10, totalItems, onPageChange, pag
7882
10081
  return null;
7883
10082
  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
10083
  };
10084
+ // Determine if we should show card view
10085
+ const shouldShowCardView = mobileView === 'card' ||
10086
+ (mobileView === 'auto' && isMobileViewport);
10087
+ // Card view content
10088
+ const cardViewContent = shouldShowCardView ? (jsx(DataTableCardView, { data: data, columns: visibleColumns, cardConfig: cardConfig, loading: loading, loadingRows: loadingRows, emptyMessage: emptyMessage, onCardClick: onRowClick, onCardLongPress: (item, event) => {
10089
+ if (enableContextMenu && allActions.length > 0) {
10090
+ event.preventDefault();
10091
+ const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
10092
+ const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
10093
+ setContextMenuState({
10094
+ isOpen: true,
10095
+ position: { x: clientX, y: clientY },
10096
+ item,
10097
+ });
10098
+ }
10099
+ }, 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
10100
  // 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 }) }))] }));
10101
+ 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 }) }))] }));
10102
+ }
10103
+
10104
+ // Color mapping for action buttons
10105
+ const colorClasses = {
10106
+ primary: 'bg-accent-500 text-white',
10107
+ success: 'bg-success-500 text-white',
10108
+ warning: 'bg-warning-500 text-white',
10109
+ error: 'bg-error-500 text-white',
10110
+ default: 'bg-paper-500 text-white',
10111
+ };
10112
+ /**
10113
+ * SwipeActions - Touch-based swipe actions for list items
10114
+ *
10115
+ * Wraps any content with swipe-to-reveal actions, commonly used in mobile
10116
+ * list items for quick actions like delete, archive, edit, etc.
10117
+ *
10118
+ * Features:
10119
+ * - Left and right swipe actions
10120
+ * - Full swipe to trigger primary action
10121
+ * - Spring-back animation
10122
+ * - Touch and mouse support
10123
+ * - Customizable thresholds
10124
+ *
10125
+ * @example Basic delete action
10126
+ * ```tsx
10127
+ * <SwipeActions
10128
+ * leftActions={[
10129
+ * {
10130
+ * id: 'delete',
10131
+ * label: 'Delete',
10132
+ * icon: <Trash className="h-5 w-5" />,
10133
+ * color: 'error',
10134
+ * onClick: () => handleDelete(item),
10135
+ * primary: true,
10136
+ * },
10137
+ * ]}
10138
+ * >
10139
+ * <div className="p-4 bg-white">
10140
+ * List item content
10141
+ * </div>
10142
+ * </SwipeActions>
10143
+ * ```
10144
+ *
10145
+ * @example Multiple actions on both sides
10146
+ * ```tsx
10147
+ * <SwipeActions
10148
+ * leftActions={[
10149
+ * { id: 'delete', label: 'Delete', icon: <Trash />, color: 'error', onClick: handleDelete },
10150
+ * { id: 'archive', label: 'Archive', icon: <Archive />, color: 'warning', onClick: handleArchive },
10151
+ * ]}
10152
+ * rightActions={[
10153
+ * { id: 'edit', label: 'Edit', icon: <Edit />, color: 'primary', onClick: handleEdit },
10154
+ * ]}
10155
+ * fullSwipe
10156
+ * >
10157
+ * <ListItem />
10158
+ * </SwipeActions>
10159
+ * ```
10160
+ */
10161
+ function SwipeActions({ children, leftActions = [], rightActions = [], threshold = 80, fullSwipeThreshold = 0.5, fullSwipe = false, disabled = false, onSwipeChange, className = '', }) {
10162
+ const containerRef = useRef(null);
10163
+ const contentRef = useRef(null);
10164
+ // Swipe state
10165
+ const [translateX, setTranslateX] = useState(0);
10166
+ const [isDragging, setIsDragging] = useState(false);
10167
+ const [activeDirection, setActiveDirection] = useState(null);
10168
+ // Touch/mouse tracking
10169
+ const startX = useRef(0);
10170
+ const currentX = useRef(0);
10171
+ const startTime = useRef(0);
10172
+ // Calculate action widths
10173
+ const leftActionsWidth = leftActions.length * 72; // 72px per action
10174
+ const rightActionsWidth = rightActions.length * 72;
10175
+ // Reset position
10176
+ const resetPosition = useCallback(() => {
10177
+ setTranslateX(0);
10178
+ setActiveDirection(null);
10179
+ onSwipeChange?.(null);
10180
+ }, [onSwipeChange]);
10181
+ // Handle touch/mouse start
10182
+ const handleStart = useCallback((clientX) => {
10183
+ if (disabled)
10184
+ return;
10185
+ startX.current = clientX;
10186
+ currentX.current = clientX;
10187
+ startTime.current = Date.now();
10188
+ setIsDragging(true);
10189
+ }, [disabled]);
10190
+ // Handle touch/mouse move
10191
+ const handleMove = useCallback((clientX) => {
10192
+ if (!isDragging || disabled)
10193
+ return;
10194
+ const deltaX = clientX - startX.current;
10195
+ currentX.current = clientX;
10196
+ // Determine direction and apply resistance at boundaries
10197
+ let newTranslateX = deltaX;
10198
+ // Swiping left (reveals left actions on right side)
10199
+ if (deltaX < 0) {
10200
+ if (leftActions.length === 0) {
10201
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10202
+ }
10203
+ else {
10204
+ const maxSwipe = fullSwipe
10205
+ ? -(containerRef.current?.offsetWidth || 300)
10206
+ : -leftActionsWidth;
10207
+ newTranslateX = Math.max(maxSwipe, deltaX);
10208
+ // Apply resistance past the action buttons
10209
+ if (newTranslateX < -leftActionsWidth) {
10210
+ const overSwipe = newTranslateX + leftActionsWidth;
10211
+ newTranslateX = -leftActionsWidth + overSwipe * 0.3;
10212
+ }
10213
+ }
10214
+ setActiveDirection('left');
10215
+ onSwipeChange?.('left');
10216
+ }
10217
+ // Swiping right (reveals right actions on left side)
10218
+ else if (deltaX > 0) {
10219
+ if (rightActions.length === 0) {
10220
+ newTranslateX = deltaX * 0.2; // Heavy resistance if no actions
10221
+ }
10222
+ else {
10223
+ const maxSwipe = fullSwipe
10224
+ ? (containerRef.current?.offsetWidth || 300)
10225
+ : rightActionsWidth;
10226
+ newTranslateX = Math.min(maxSwipe, deltaX);
10227
+ // Apply resistance past the action buttons
10228
+ if (newTranslateX > rightActionsWidth) {
10229
+ const overSwipe = newTranslateX - rightActionsWidth;
10230
+ newTranslateX = rightActionsWidth + overSwipe * 0.3;
10231
+ }
10232
+ }
10233
+ setActiveDirection('right');
10234
+ onSwipeChange?.('right');
10235
+ }
10236
+ setTranslateX(newTranslateX);
10237
+ }, [isDragging, disabled, leftActions.length, rightActions.length, leftActionsWidth, rightActionsWidth, fullSwipe, onSwipeChange]);
10238
+ // Handle touch/mouse end
10239
+ const handleEnd = useCallback(() => {
10240
+ if (!isDragging)
10241
+ return;
10242
+ setIsDragging(false);
10243
+ const deltaX = currentX.current - startX.current;
10244
+ const velocity = Math.abs(deltaX) / (Date.now() - startTime.current);
10245
+ const containerWidth = containerRef.current?.offsetWidth || 300;
10246
+ // Check for full swipe trigger
10247
+ if (fullSwipe) {
10248
+ const swipePercentage = Math.abs(translateX) / containerWidth;
10249
+ if (swipePercentage >= fullSwipeThreshold || velocity > 0.5) {
10250
+ // Find primary action and trigger it
10251
+ if (translateX < 0 && leftActions.length > 0) {
10252
+ const primaryAction = leftActions.find(a => a.primary) || leftActions[0];
10253
+ primaryAction.onClick();
10254
+ resetPosition();
10255
+ return;
10256
+ }
10257
+ else if (translateX > 0 && rightActions.length > 0) {
10258
+ const primaryAction = rightActions.find(a => a.primary) || rightActions[0];
10259
+ primaryAction.onClick();
10260
+ resetPosition();
10261
+ return;
10262
+ }
10263
+ }
10264
+ }
10265
+ // Snap to open or closed position
10266
+ if (Math.abs(translateX) >= threshold || velocity > 0.3) {
10267
+ // Snap open
10268
+ if (translateX < 0 && leftActions.length > 0) {
10269
+ setTranslateX(-leftActionsWidth);
10270
+ setActiveDirection('left');
10271
+ onSwipeChange?.('left');
10272
+ }
10273
+ else if (translateX > 0 && rightActions.length > 0) {
10274
+ setTranslateX(rightActionsWidth);
10275
+ setActiveDirection('right');
10276
+ onSwipeChange?.('right');
10277
+ }
10278
+ else {
10279
+ resetPosition();
10280
+ }
10281
+ }
10282
+ else {
10283
+ // Snap closed
10284
+ resetPosition();
10285
+ }
10286
+ }, [isDragging, translateX, threshold, fullSwipe, fullSwipeThreshold, leftActions, rightActions, leftActionsWidth, rightActionsWidth, resetPosition, onSwipeChange]);
10287
+ // Touch event handlers
10288
+ const handleTouchStart = (e) => {
10289
+ handleStart(e.touches[0].clientX);
10290
+ };
10291
+ const handleTouchMove = (e) => {
10292
+ handleMove(e.touches[0].clientX);
10293
+ };
10294
+ const handleTouchEnd = () => {
10295
+ handleEnd();
10296
+ };
10297
+ // Mouse event handlers (for testing/desktop)
10298
+ const handleMouseDown = (e) => {
10299
+ handleStart(e.clientX);
10300
+ };
10301
+ const handleMouseMove = (e) => {
10302
+ handleMove(e.clientX);
10303
+ };
10304
+ const handleMouseUp = () => {
10305
+ handleEnd();
10306
+ };
10307
+ // Close on outside click
10308
+ useEffect(() => {
10309
+ if (activeDirection === null)
10310
+ return;
10311
+ const handleClickOutside = (e) => {
10312
+ if (containerRef.current && !containerRef.current.contains(e.target)) {
10313
+ resetPosition();
10314
+ }
10315
+ };
10316
+ document.addEventListener('mousedown', handleClickOutside);
10317
+ return () => document.removeEventListener('mousedown', handleClickOutside);
10318
+ }, [activeDirection, resetPosition]);
10319
+ // Handle mouse leave during drag
10320
+ useEffect(() => {
10321
+ if (!isDragging)
10322
+ return;
10323
+ const handleMouseLeave = () => {
10324
+ handleEnd();
10325
+ };
10326
+ document.addEventListener('mouseup', handleMouseLeave);
10327
+ return () => document.removeEventListener('mouseup', handleMouseLeave);
10328
+ }, [isDragging, handleEnd]);
10329
+ // Render action button
10330
+ const renderActionButton = (action) => {
10331
+ const colorClass = colorClasses[action.color || 'default'];
10332
+ return (jsxs("button", { onClick: (e) => {
10333
+ e.stopPropagation();
10334
+ action.onClick();
10335
+ resetPosition();
10336
+ }, className: `
10337
+ flex flex-col items-center justify-center
10338
+ w-18 h-full min-w-[72px]
10339
+ ${colorClass}
10340
+ transition-transform duration-150
10341
+ `, style: {
10342
+ transform: isDragging ? 'scale(1)' : 'scale(1)',
10343
+ }, children: [action.icon && (jsx("div", { className: "mb-1", children: action.icon })), jsx("span", { className: "text-xs font-medium", children: action.label })] }, action.id));
10344
+ };
10345
+ 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: `
10346
+ relative bg-white
10347
+ ${isDragging ? '' : 'transition-transform duration-200 ease-out'}
10348
+ `, style: {
10349
+ transform: `translateX(${translateX}px)`,
10350
+ touchAction: 'pan-y', // Allow vertical scrolling
10351
+ }, children: children })] }));
7887
10352
  }
7888
10353
 
7889
10354
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -51371,6 +53836,171 @@ function PageLayout({ title, description, children, className = '', headerConten
51371
53836
  return (jsxs(Page, { padding: "none", maxWidth: maxWidth, fixed: fixed, children: [headerContent, jsxs("div", { className: `${paddingClasses} ${maxWidthClasses[maxWidth]} mx-auto ${className}`, children: [jsxs("div", { className: "mb-8", children: [jsx("h1", { className: "text-3xl font-bold text-ink-900 mb-2", children: title }), description && (jsx("p", { className: "text-ink-600", children: description }))] }), children] })] }));
51372
53837
  }
51373
53838
 
53839
+ /**
53840
+ * PageHeader - Standard page header with title, breadcrumbs, and actions
53841
+ *
53842
+ * A consistent header component for pages that provides:
53843
+ * - Page title and optional subtitle
53844
+ * - Breadcrumb navigation
53845
+ * - Action buttons (Create, Export, etc.)
53846
+ * - Optional back button
53847
+ * - Sticky positioning option
53848
+ *
53849
+ * @example Basic usage
53850
+ * ```tsx
53851
+ * <PageHeader
53852
+ * title="Products"
53853
+ * subtitle="Manage your product catalog"
53854
+ * breadcrumbs={[{ label: 'Inventory' }, { label: 'Products' }]}
53855
+ * actions={[
53856
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport, variant: 'ghost' },
53857
+ * { id: 'add', label: 'Add Product', icon: <Plus />, onClick: handleAdd, variant: 'primary' },
53858
+ * ]}
53859
+ * />
53860
+ * ```
53861
+ *
53862
+ * @example With back button
53863
+ * ```tsx
53864
+ * <PageHeader
53865
+ * title="Edit Product"
53866
+ * backButton={{ label: 'Back to Products', onClick: () => navigate('/products') }}
53867
+ * />
53868
+ * ```
53869
+ *
53870
+ * @example With custom right content
53871
+ * ```tsx
53872
+ * <PageHeader
53873
+ * title="Dashboard"
53874
+ * rightContent={<DateRangePicker value={range} onChange={setRange} />}
53875
+ * />
53876
+ * ```
53877
+ */
53878
+ function PageHeader({ title, subtitle, breadcrumbs, showHomeBreadcrumb = true, actions, rightContent, belowTitle, className = '', sticky = false, backButton, }) {
53879
+ const variantStyles = {
53880
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
53881
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
53882
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
53883
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
53884
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
53885
+ };
53886
+ return (jsx("div", { className: `
53887
+ bg-white border-b border-paper-200
53888
+ ${sticky ? 'sticky top-0 z-40' : ''}
53889
+ ${className}
53890
+ `, children: jsxs("div", { className: "px-6 py-4", children: [breadcrumbs && breadcrumbs.length > 0 && (jsx("div", { className: "mb-3", children: jsx(Breadcrumbs, { items: breadcrumbs, showHome: showHomeBreadcrumb }) })), backButton && (jsx("div", { className: "mb-3", children: jsxs("button", { onClick: backButton.onClick, className: "inline-flex items-center gap-1.5 text-sm text-ink-500 hover:text-ink-700 transition-colors", children: [jsx("svg", { className: "h-4 w-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), jsx("span", { children: backButton.label || 'Back' })] }) })), jsxs("div", { className: "flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4", children: [jsxs("div", { className: "min-w-0 flex-1", children: [jsx("h1", { className: "text-2xl font-bold text-ink-900 truncate", children: title }), subtitle && (jsx("p", { className: "mt-1 text-sm text-ink-500", children: subtitle }))] }), (actions || rightContent) && (jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
53891
+ inline-flex items-center justify-center gap-2
53892
+ px-4 py-2 text-sm font-medium rounded-lg border
53893
+ transition-all duration-200
53894
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
53895
+ disabled:opacity-40 disabled:cursor-not-allowed
53896
+ ${variantStyles[action.variant || 'secondary']}
53897
+ ${action.hideOnMobile ? 'hidden sm:inline-flex' : ''}
53898
+ `, children: [action.loading ? (jsxs("svg", { className: "h-4 w-4 animate-spin", 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" })] })) : action.icon ? (jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsx("span", { children: action.label })] }, action.id)))] }))] }), belowTitle && (jsx("div", { className: "mt-4", children: belowTitle }))] }) }));
53899
+ }
53900
+
53901
+ /**
53902
+ * ActionBar - Flexible toolbar for page-level and contextual actions
53903
+ *
53904
+ * A versatile action container that can be used for:
53905
+ * - Bulk actions when rows are selected
53906
+ * - Page-level actions and controls
53907
+ * - Form action buttons (Save/Cancel)
53908
+ * - Contextual toolbars
53909
+ *
53910
+ * @example Basic bulk actions bar
53911
+ * ```tsx
53912
+ * <ActionBar
53913
+ * visible={selectedIds.length > 0}
53914
+ * leftContent={<Text weight="medium">{selectedIds.length} selected</Text>}
53915
+ * actions={[
53916
+ * { id: 'export', label: 'Export', icon: <Download />, onClick: handleExport },
53917
+ * { id: 'delete', label: 'Delete', icon: <Trash />, onClick: handleDelete, variant: 'danger' },
53918
+ * ]}
53919
+ * onDismiss={() => setSelectedIds([])}
53920
+ * showDismiss
53921
+ * />
53922
+ * ```
53923
+ *
53924
+ * @example Sticky bottom form actions
53925
+ * ```tsx
53926
+ * <ActionBar
53927
+ * position="bottom"
53928
+ * sticky
53929
+ * rightContent={
53930
+ * <>
53931
+ * <Button variant="ghost" onClick={handleCancel}>Cancel</Button>
53932
+ * <Button variant="primary" onClick={handleSave} loading={isSaving}>Save Changes</Button>
53933
+ * </>
53934
+ * }
53935
+ * />
53936
+ * ```
53937
+ *
53938
+ * @example Info bar with center content
53939
+ * ```tsx
53940
+ * <ActionBar
53941
+ * variant="info"
53942
+ * centerContent={
53943
+ * <Text size="sm">Showing results for "search term" - 42 items found</Text>
53944
+ * }
53945
+ * onDismiss={clearSearch}
53946
+ * showDismiss
53947
+ * />
53948
+ * ```
53949
+ */
53950
+ function ActionBar({ leftContent, centerContent, rightContent, actions, position = 'top', sticky = false, visible = true, onDismiss, showDismiss = false, className = '', variant = 'default', compact = false, }) {
53951
+ if (!visible) {
53952
+ return null;
53953
+ }
53954
+ const variantStyles = {
53955
+ default: 'bg-white border-paper-200',
53956
+ primary: 'bg-accent-50 border-accent-200',
53957
+ warning: 'bg-warning-50 border-warning-200',
53958
+ info: 'bg-blue-50 border-blue-200',
53959
+ };
53960
+ const buttonVariantStyles = {
53961
+ primary: 'bg-accent-500 text-white border-accent-500 hover:bg-accent-600 hover:shadow-sm',
53962
+ secondary: 'bg-white text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-paper-400 shadow-xs hover:shadow-sm',
53963
+ ghost: 'bg-transparent text-ink-600 border-transparent hover:text-ink-800 hover:bg-paper-100',
53964
+ danger: 'bg-error-500 text-white border-error-500 hover:bg-error-600 hover:shadow-sm',
53965
+ outline: 'bg-transparent text-ink-700 border-paper-300 hover:bg-paper-50 hover:border-ink-400',
53966
+ };
53967
+ const positionStyles = {
53968
+ top: sticky ? 'sticky top-0 z-40 border-b' : 'border-b',
53969
+ bottom: sticky ? 'sticky bottom-0 z-40 border-t' : 'border-t',
53970
+ };
53971
+ return (jsx("div", { className: `
53972
+ ${variantStyles[variant]}
53973
+ ${positionStyles[position]}
53974
+ ${compact ? 'px-4 py-2' : 'px-6 py-3'}
53975
+ ${className}
53976
+ `, role: "toolbar", "aria-label": "Action bar", children: jsxs("div", { className: "flex items-center justify-between gap-4", children: [jsxs("div", { className: "flex items-center gap-3 min-w-0 flex-shrink-0", children: [showDismiss && onDismiss && (jsx("button", { onClick: onDismiss, className: "\n flex items-center justify-center\n w-8 h-8 rounded-full\n text-ink-400 hover:text-ink-600\n hover:bg-paper-100\n transition-colors\n ", "aria-label": "Dismiss", children: jsx(X, { className: "h-4 w-4" }) })), leftContent] }), centerContent && (jsx("div", { className: "flex-1 flex items-center justify-center min-w-0", children: centerContent })), jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [rightContent, actions && actions.map((action) => (jsxs("button", { onClick: action.onClick, disabled: action.disabled || action.loading, className: `
53977
+ inline-flex items-center justify-center gap-2
53978
+ px-3 py-1.5 text-sm font-medium rounded-lg border
53979
+ transition-all duration-200
53980
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-accent-400
53981
+ disabled:opacity-40 disabled:cursor-not-allowed
53982
+ ${buttonVariantStyles[action.variant || 'secondary']}
53983
+ `, children: [action.loading ? (jsxs("svg", { className: "h-4 w-4 animate-spin", 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" })] })) : action.icon ? (jsx("span", { className: "h-4 w-4", children: action.icon })) : null, jsx("span", { children: action.label })] }, action.id)))] })] }) }));
53984
+ }
53985
+ /**
53986
+ * ActionBar.Left - Semantic wrapper for left content
53987
+ */
53988
+ function ActionBarLeft({ children }) {
53989
+ return jsx(Fragment, { children: children });
53990
+ }
53991
+ /**
53992
+ * ActionBar.Center - Semantic wrapper for center content
53993
+ */
53994
+ function ActionBarCenter({ children }) {
53995
+ return jsx(Fragment, { children: children });
53996
+ }
53997
+ /**
53998
+ * ActionBar.Right - Semantic wrapper for right content
53999
+ */
54000
+ function ActionBarRight({ children }) {
54001
+ return jsx(Fragment, { children: children });
54002
+ }
54003
+
51374
54004
  const sizeClasses = {
51375
54005
  sm: 'max-w-sm',
51376
54006
  md: 'max-w-md',
@@ -51963,5 +54593,204 @@ function useColumnReorder(initialColumns, options = {}) {
51963
54593
  };
51964
54594
  }
51965
54595
 
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 };
54596
+ /**
54597
+ * Default context value (SSR-safe defaults)
54598
+ */
54599
+ const defaultContextValue = {
54600
+ isMobile: false,
54601
+ isTablet: false,
54602
+ isDesktop: true,
54603
+ isTouchDevice: false,
54604
+ breakpoint: 'lg',
54605
+ orientation: 'landscape',
54606
+ viewport: { width: 1024, height: 768 },
54607
+ safeAreaInsets: { top: 0, right: 0, bottom: 0, left: 0 },
54608
+ useMobileUI: false,
54609
+ };
54610
+ /**
54611
+ * Mobile context
54612
+ */
54613
+ const MobileContext = createContext$1(defaultContextValue);
54614
+ /**
54615
+ * MobileProvider - Provides responsive state to the entire application
54616
+ *
54617
+ * Wrap your application with MobileProvider to enable auto-responsive
54618
+ * behavior in notebook-ui components.
54619
+ *
54620
+ * @example Basic usage
54621
+ * ```tsx
54622
+ * import { MobileProvider } from 'notebook-ui';
54623
+ *
54624
+ * function App() {
54625
+ * return (
54626
+ * <MobileProvider>
54627
+ * <YourApplication />
54628
+ * </MobileProvider>
54629
+ * );
54630
+ * }
54631
+ * ```
54632
+ *
54633
+ * @example Force mobile UI for testing
54634
+ * ```tsx
54635
+ * <MobileProvider forceMobileUI>
54636
+ * <YourApplication />
54637
+ * </MobileProvider>
54638
+ * ```
54639
+ */
54640
+ function MobileProvider({ children, forceMobileUI = false, forceDesktopUI = false, }) {
54641
+ const isMobile = useIsMobile();
54642
+ const isTablet = useIsTablet();
54643
+ const isDesktop = useIsDesktop();
54644
+ const isTouchDevice = useIsTouchDevice();
54645
+ const breakpoint = useBreakpoint();
54646
+ const orientation = useOrientation();
54647
+ const viewport = useViewportSize();
54648
+ const safeAreaInsets = useSafeAreaInsets();
54649
+ const value = useMemo(() => {
54650
+ // Calculate effective mobile UI state
54651
+ let useMobileUI = isMobile || isTouchDevice;
54652
+ // Apply force overrides
54653
+ if (forceMobileUI) {
54654
+ useMobileUI = true;
54655
+ }
54656
+ else if (forceDesktopUI) {
54657
+ useMobileUI = false;
54658
+ }
54659
+ return {
54660
+ isMobile: forceMobileUI ? true : forceDesktopUI ? false : isMobile,
54661
+ isTablet: forceMobileUI || forceDesktopUI ? false : isTablet,
54662
+ isDesktop: forceDesktopUI ? true : forceMobileUI ? false : isDesktop,
54663
+ isTouchDevice,
54664
+ breakpoint: forceMobileUI ? 'xs' : forceDesktopUI ? 'lg' : breakpoint,
54665
+ orientation,
54666
+ viewport,
54667
+ safeAreaInsets,
54668
+ useMobileUI,
54669
+ };
54670
+ }, [
54671
+ isMobile,
54672
+ isTablet,
54673
+ isDesktop,
54674
+ isTouchDevice,
54675
+ breakpoint,
54676
+ orientation,
54677
+ viewport,
54678
+ safeAreaInsets,
54679
+ forceMobileUI,
54680
+ forceDesktopUI,
54681
+ ]);
54682
+ return (jsx(MobileContext.Provider, { value: value, children: children }));
54683
+ }
54684
+ /**
54685
+ * useMobileContext - Hook to access mobile responsive state
54686
+ *
54687
+ * Must be used within a MobileProvider. Returns comprehensive
54688
+ * responsive state for making UI decisions.
54689
+ *
54690
+ * @example
54691
+ * ```tsx
54692
+ * function MyComponent() {
54693
+ * const { isMobile, useMobileUI, breakpoint } = useMobileContext();
54694
+ *
54695
+ * return useMobileUI ? <MobileView /> : <DesktopView />;
54696
+ * }
54697
+ * ```
54698
+ */
54699
+ function useMobileContext() {
54700
+ const context = useContext(MobileContext);
54701
+ if (context === undefined) {
54702
+ // Return default value if used outside provider (graceful degradation)
54703
+ console.warn('useMobileContext was used outside of MobileProvider. ' +
54704
+ 'Wrap your app with <MobileProvider> for full mobile support.');
54705
+ return defaultContextValue;
54706
+ }
54707
+ return context;
54708
+ }
54709
+ /**
54710
+ * withMobileContext - HOC to inject mobile context as props
54711
+ *
54712
+ * For class components or when you prefer props over hooks.
54713
+ *
54714
+ * @example
54715
+ * ```tsx
54716
+ * interface Props {
54717
+ * mobile: MobileContextValue;
54718
+ * }
54719
+ *
54720
+ * class MyComponent extends React.Component<Props> {
54721
+ * render() {
54722
+ * const { isMobile } = this.props.mobile;
54723
+ * return isMobile ? <Mobile /> : <Desktop />;
54724
+ * }
54725
+ * }
54726
+ *
54727
+ * export default withMobileContext(MyComponent);
54728
+ * ```
54729
+ */
54730
+ function withMobileContext(Component) {
54731
+ const displayName = Component.displayName || Component.name || 'Component';
54732
+ const WrappedComponent = (props) => {
54733
+ const mobile = useMobileContext();
54734
+ return jsx(Component, { ...props, mobile: mobile });
54735
+ };
54736
+ WrappedComponent.displayName = `withMobileContext(${displayName})`;
54737
+ return WrappedComponent;
54738
+ }
54739
+ /**
54740
+ * MobileOnly - Renders children only on mobile devices
54741
+ *
54742
+ * @example
54743
+ * ```tsx
54744
+ * <MobileOnly>
54745
+ * <BottomNavigation items={navItems} />
54746
+ * </MobileOnly>
54747
+ * ```
54748
+ */
54749
+ function MobileOnly({ children }) {
54750
+ const { useMobileUI } = useMobileContext();
54751
+ return useMobileUI ? jsx(Fragment, { children: children }) : null;
54752
+ }
54753
+ /**
54754
+ * DesktopOnly - Renders children only on desktop devices
54755
+ *
54756
+ * @example
54757
+ * ```tsx
54758
+ * <DesktopOnly>
54759
+ * <Sidebar items={navItems} />
54760
+ * </DesktopOnly>
54761
+ * ```
54762
+ */
54763
+ function DesktopOnly({ children }) {
54764
+ const { useMobileUI } = useMobileContext();
54765
+ return useMobileUI ? null : jsx(Fragment, { children: children });
54766
+ }
54767
+ /**
54768
+ * Responsive - Renders different content based on device type
54769
+ *
54770
+ * @example
54771
+ * ```tsx
54772
+ * <Responsive
54773
+ * mobile={<MobileNavigation />}
54774
+ * tablet={<TabletNavigation />}
54775
+ * desktop={<DesktopNavigation />}
54776
+ * />
54777
+ * ```
54778
+ */
54779
+ function Responsive({ mobile, tablet, desktop, }) {
54780
+ const { isMobile, isTablet, isDesktop } = useMobileContext();
54781
+ if (isMobile && mobile)
54782
+ return jsx(Fragment, { children: mobile });
54783
+ if (isTablet && tablet)
54784
+ return jsx(Fragment, { children: tablet });
54785
+ if (isDesktop && desktop)
54786
+ return jsx(Fragment, { children: desktop });
54787
+ // Fallback: desktop -> tablet -> mobile
54788
+ if (isDesktop)
54789
+ return jsx(Fragment, { children: desktop || tablet || mobile });
54790
+ if (isTablet)
54791
+ return jsx(Fragment, { children: tablet || mobile || desktop });
54792
+ return jsx(Fragment, { children: mobile || tablet || desktop });
54793
+ }
54794
+
54795
+ export { Accordion, ActionBar, ActionBarCenter, ActionBarLeft, ActionBarRight, ActionButton, AdminModal, Alert, AlertDialog, AppLayout, Autocomplete, Avatar, BREAKPOINTS, Badge, BottomNavigation, BottomNavigationSpacer, BottomSheet, Box, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardView, Carousel, Checkbox, CheckboxList, Chip, ChipGroup, Collapsible, ColorPicker, Combobox, ComingSoon, CommandPalette, ConfirmDialog, ContextMenu, ControlBar, CurrencyDisplay, CurrencyInput, Dashboard, DashboardContent, DashboardHeader, 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, PageHeader, PageLayout, PageNavigation, Pagination, PasswordInput, Popover, Progress, PullToRefresh, QueryTransparency, RadioGroup, Rating, Responsive, RichTextEditor, SearchBar, SearchableList, Select, Separator, Show, Sidebar, SidebarGroup, Skeleton, SkeletonCard$1 as SkeletonCard, SkeletonTable, Slider, Spreadsheet, SpreadsheetReport, Stack, StatCard, StatItem, StatsCardGrid, StatsGrid, StatusBadge, StatusBar, StepIndicator, Stepper, SwipeActions, Switch, Tabs, Text, Textarea, ThemeToggle, TimePicker, Timeline, Toast, ToastContainer, Tooltip, Transfer, TreeView, TwoColumnContent, UserProfileButton, addErrorMessage, addInfoMessage, addSuccessMessage, addWarningMessage, calculateColumnWidth, createActionsSection, createFiltersSection, createMultiSheetExcel, createPageControlsSection, createQueryDetailsSection, exportDataTableToExcel, exportToExcel, formatStatisticValue, formatStatistics, 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
54796
  //# sourceMappingURL=index.esm.js.map