@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.
- package/dist/components/ActionBar.d.ts +112 -0
- package/dist/components/ActionBar.d.ts.map +1 -0
- package/dist/components/BottomNavigation.d.ts +98 -0
- package/dist/components/BottomNavigation.d.ts.map +1 -0
- package/dist/components/Checkbox.d.ts +2 -0
- package/dist/components/Checkbox.d.ts.map +1 -1
- package/dist/components/CheckboxList.d.ts +81 -0
- package/dist/components/CheckboxList.d.ts.map +1 -0
- package/dist/components/Chip.d.ts +92 -1
- package/dist/components/Chip.d.ts.map +1 -1
- package/dist/components/ConfirmDialog.d.ts +43 -1
- package/dist/components/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +10 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/DataTableCardView.d.ts +99 -0
- package/dist/components/DataTableCardView.d.ts.map +1 -0
- package/dist/components/ExpandablePanel.d.ts +142 -0
- package/dist/components/ExpandablePanel.d.ts.map +1 -0
- package/dist/components/FloatingActionButton.d.ts +98 -0
- package/dist/components/FloatingActionButton.d.ts.map +1 -0
- package/dist/components/Input.d.ts +45 -1
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MobileHeader.d.ts +98 -0
- package/dist/components/MobileHeader.d.ts.map +1 -0
- package/dist/components/MobileLayout.d.ts +121 -0
- package/dist/components/MobileLayout.d.ts.map +1 -0
- package/dist/components/Modal.d.ts +78 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/PageHeader.d.ts +86 -0
- package/dist/components/PageHeader.d.ts.map +1 -0
- package/dist/components/PullToRefresh.d.ts +87 -0
- package/dist/components/PullToRefresh.d.ts.map +1 -0
- package/dist/components/QueryTransparency.d.ts +1 -1
- package/dist/components/QueryTransparency.d.ts.map +1 -1
- package/dist/components/SearchableList.d.ts +83 -0
- package/dist/components/SearchableList.d.ts.map +1 -0
- package/dist/components/Select.d.ts +16 -2
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +40 -1
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/SwipeActions.d.ts +93 -0
- package/dist/components/SwipeActions.d.ts.map +1 -0
- package/dist/components/Switch.d.ts +1 -0
- package/dist/components/Switch.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +13 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +31 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/context/MobileContext.d.ts +168 -0
- package/dist/context/MobileContext.d.ts.map +1 -0
- package/dist/hooks/useResponsive.d.ts +158 -0
- package/dist/hooks/useResponsive.d.ts.map +1 -0
- package/dist/index.d.ts +1871 -51
- package/dist/index.esm.js +3025 -196
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3063 -194
- package/dist/index.js.map +1 -1
- package/dist/styles.css +434 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/ActionBar.stories.tsx +246 -0
- package/src/components/ActionBar.tsx +242 -0
- package/src/components/BottomNavigation.stories.tsx +142 -0
- package/src/components/BottomNavigation.tsx +225 -0
- package/src/components/Checkbox.stories.tsx +162 -0
- package/src/components/Checkbox.tsx +22 -6
- package/src/components/CheckboxList.stories.tsx +311 -0
- package/src/components/CheckboxList.tsx +433 -0
- package/src/components/Chip.stories.tsx +389 -0
- package/src/components/Chip.tsx +182 -3
- package/src/components/ConfirmDialog.tsx +56 -4
- package/src/components/DataTable.tsx +60 -1
- package/src/components/DataTableCardView.stories.tsx +307 -0
- package/src/components/DataTableCardView.tsx +419 -0
- package/src/components/ExpandablePanel.stories.tsx +620 -0
- package/src/components/ExpandablePanel.tsx +383 -0
- package/src/components/FloatingActionButton.stories.tsx +197 -0
- package/src/components/FloatingActionButton.tsx +301 -0
- package/src/components/Grid.stories.tsx +16 -16
- package/src/components/Input.stories.tsx +214 -0
- package/src/components/Input.tsx +81 -4
- package/src/components/MobileHeader.stories.tsx +205 -0
- package/src/components/MobileHeader.tsx +233 -0
- package/src/components/MobileLayout.stories.tsx +338 -0
- package/src/components/MobileLayout.tsx +313 -0
- package/src/components/Modal.stories.tsx +388 -0
- package/src/components/Modal.tsx +122 -4
- package/src/components/PageHeader.stories.tsx +198 -0
- package/src/components/PageHeader.tsx +217 -0
- package/src/components/PullToRefresh.stories.tsx +321 -0
- package/src/components/PullToRefresh.tsx +294 -0
- package/src/components/QueryTransparency.tsx +1 -1
- package/src/components/SearchableList.stories.tsx +437 -0
- package/src/components/SearchableList.tsx +326 -0
- package/src/components/Select.stories.tsx +190 -0
- package/src/components/Select.tsx +353 -137
- package/src/components/Sidebar.tsx +193 -10
- package/src/components/SwipeActions.stories.tsx +327 -0
- package/src/components/SwipeActions.tsx +387 -0
- package/src/components/Switch.stories.tsx +158 -0
- package/src/components/Switch.tsx +12 -3
- package/src/components/Textarea.tsx +31 -1
- package/src/components/index.ts +69 -3
- package/src/context/MobileContext.tsx +296 -0
- package/src/hooks/useResponsive.ts +360 -0
- package/src/types/index.ts +4 -0
- package/tailwind.config.js +56 -1
package/dist/index.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,
|
|
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-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
487
|
-
|
|
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:
|
|
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[
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
755
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
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$
|
|
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$
|
|
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
|
|
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
|
-
|
|
6732
|
-
|
|
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:
|
|
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
|
-
|
|
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
|