UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

538 lines (535 loc) 23.1 kB
'use client'; import { jsx, jsxs } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { FileText, Tag, MapPin, Users, Mic, Video, Image, Search, Save, Filter, X, History, SortAsc, SortDesc, List, Grid3X3, Clock, Star, MoreHorizontal } from 'lucide-react'; import { forwardRef, useState, useRef, useCallback } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { useA11yId } from '../../utils/a11y.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardHeader, CardTitle, CardContent } from '../card/index.js'; import '../data-display/GlassAccordion.js'; import '../data-display/GlassAlert.js'; import '../data-display/GlassAvatar.js'; import { GlassBadge } from '../data-display/GlassBadge.js'; import '../data-display/GlassBadgeLine.js'; import '../data-display/GlassDataGrid.js'; import '../data-display/GlassDataTable.js'; import '../data-display/GlassHeatmap.js'; import '../data-display/GlassLoadingSkeleton.js'; import '../data-display/GlassProgress.js'; import '../data-display/GlassTimeline.js'; import '../data-display/GlassSkeleton.js'; import '../data-display/GlassNotificationCenter.js'; import '../data-display/GlassAnimatedNumber.js'; import { GlassCard } from '../card/GlassCard.js'; /** * GlassAdvancedSearch component * A comprehensive search interface with filters, suggestions, and advanced features */ const GlassAdvancedSearch = /*#__PURE__*/forwardRef(({ placeholder = "Search...", filters = [], suggestions = [], results = [], loading = false, enableHistory = true, enableSavedSearches = true, enableAdvancedFilters = true, showStats = true, onSearch, onFilterChange, onResultClick, onSaveSearch, className, respectMotionPreference = true, id, ...props }, ref) => { const [query, setQuery] = useState(""); const [activeFilters, setActiveFilters] = useState({}); const [showSuggestions, setShowSuggestions] = useState(false); const [showFilters, setShowFilters] = useState(false); const [sortBy, setSortBy] = useState("relevance"); const [sortOrder, setSortOrder] = useState("desc"); const [viewMode, setViewMode] = useState("list"); const [searchHistory, setSearchHistory] = useState([]); const [savedSearches, setSavedSearches] = useState([]); useReducedMotion(); const componentId = id || useA11yId("advanced-search"); const inputRef = useRef(null); const suggestionsRef = useRef(null); // Handle search input const handleSearchInput = useCallback(e => { const value = e.target.value; setQuery(value); setShowSuggestions(value.length > 0); // Debounced search const timeoutId = setTimeout(() => { if (value.trim()) { onSearch?.(value, activeFilters); // Add to history if (enableHistory && !searchHistory.includes(value)) { setSearchHistory(prev => [value, ...prev.slice(0, 9)]); } } }, 300); return () => clearTimeout(timeoutId); }, [activeFilters, onSearch, enableHistory, searchHistory]); // Handle filter change const handleFilterChange = useCallback((filterId, value) => { const newFilters = { ...activeFilters, [filterId]: value }; setActiveFilters(newFilters); onFilterChange?.(newFilters); // Auto-search when filters change if (query.trim()) { onSearch?.(query, newFilters); } }, [activeFilters, query, onFilterChange, onSearch]); // Handle suggestion click const handleSuggestionClick = useCallback(suggestion => { setQuery(suggestion.text); setShowSuggestions(false); onSearch?.(suggestion.text, activeFilters); }, [activeFilters, onSearch]); // Handle result click const handleResultClick = useCallback(result => { onResultClick?.(result); }, [onResultClick]); // Handle save search const handleSaveSearch = useCallback(() => { const name = prompt("Enter a name for this search:"); if (name && query.trim()) { const search = { name, query, filters: activeFilters }; setSavedSearches(prev => [search, ...prev]); onSaveSearch?.(name, query, activeFilters); } }, [query, activeFilters, onSaveSearch]); // Clear all filters const handleClearFilters = useCallback(() => { setActiveFilters({}); onFilterChange?.({}); }, [onFilterChange]); // Get filter value display const getFilterValueDisplay = useCallback(filter => { const value = activeFilters[filter.id]; if (!value) return null; switch (filter.type) { case "select": case "multiselect": if (Array.isArray(value)) { return value.map(v => filter.options?.find(o => o.value === v)?.label || v).join(", "); } return filter.options?.find(o => o.value === value)?.label || value; case "date": return new Date(value).toLocaleDateString(); case "range": return `${value.min} - ${value.max}`; case "boolean": return value ? "Yes" : "No"; default: return String(value); } }, [activeFilters]); // Get result type icon const getResultTypeIcon = useCallback(type => { switch (type) { case "document": return jsx(FileText, { className: 'w-4 h-4' }); case "image": return jsx(Image, { className: 'w-4 h-4' }); case "video": return jsx(Video, { className: 'w-4 h-4' }); case "audio": return jsx(Mic, { className: 'w-4 h-4' }); case "user": return jsx(Users, { className: 'w-4 h-4' }); case "location": return jsx(MapPin, { className: 'w-4 h-4' }); case "tag": return jsx(Tag, { className: 'w-4 h-4' }); default: return jsx(FileText, { className: 'w-4 h-4' }); } }, []); // Format file size const formatFileSize = useCallback(bytes => { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; }, []); // Active filter count const activeFilterCount = Object.values(activeFilters).filter(v => v !== null && v !== undefined && v !== "" && (!Array.isArray(v) || v.length > 0)).length; return jsx(MotionFramer, { preset: "fadeIn", className: "glass-w-full", children: jsxs(GlassCard, { ref: ref, id: componentId, className: cn("overflow-hidden", className), role: "search", "aria-label": "Advanced search interface", "aria-describedby": `${componentId}-description`, ...props, children: [jsx("div", { id: `${componentId}-description`, className: 'sr-only', children: "Advanced search interface with filters, suggestions, and result management" }), jsx(CardHeader, { className: 'pb-3', children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs(CardTitle, { className: 'text-primary glass-text-lg font-semibold glass-flex glass-items-center glass-gap-2', children: [jsx(Search, { className: 'w-5 h-5' }), "Advanced Search"] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [enableSavedSearches && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleSaveSearch, disabled: !query.trim(), className: "glass-p-2", children: jsx(Save, { className: 'w-4 h-4' }) }), jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => setShowFilters(!showFilters), className: 'glass-p-2 relative', children: [jsx(Filter, { className: 'w-4 h-4' }), activeFilterCount > 0 && jsx("span", { className: 'absolute glass-top-1 -right-1 w-5 h-5 glass-surface-primary text-primary glass-text-xs glass-radius-full glass-flex glass-items-center glass-justify-center', children: activeFilterCount })] })] })] }) }), jsxs(CardContent, { className: 'pt-0 glass-auto-gap glass-auto-gap-lg', children: [jsxs("div", { className: 'relative', children: [jsxs("div", { className: 'relative', children: [jsx(Search, { className: 'absolute left-3 glass-top-1/2 transform -translate-y-1/2 text-primary/60 w-4 h-4' }), jsx("input", { ref: inputRef, type: "text", value: query, onChange: handleSearchInput, placeholder: placeholder, className: 'glass-w-full pl-10 pr-4 glass-py-3 glass-surface-subtle/10 ring-1 ring-white/10 glass-radius-lg text-primary placeholder-white/50 focus:outline-none focus:ring-white/30 glass-touch-target glass-contrast-guard' }), query && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { setQuery(""); setShowSuggestions(false); }, className: 'absolute right-2 glass-top-1/2 transform -translate-y-1/2 glass-p-1', children: jsx(X, { className: 'w-4 h-4' }) })] }), showSuggestions && suggestions.length > 0 && jsx(MotionFramer, { preset: "slideDown", className: 'absolute top-full left-0 right-0 glass-mt-2 z-10', children: jsx("div", { ref: suggestionsRef, className: 'glass-surface-dark/80 glass-glass-backdrop-blur-md glass-border glass-border-white/20 glass-radius-lg glass-shadow-xl glass-max-h-64 overflow-y-auto glass-contrast-guard', children: suggestions.map(suggestion => jsxs("button", { onClick: e => handleSuggestionClick(suggestion), className: 'glass-w-full text-left glass-px-4 glass-py-3 hover:glass-surface-subtle/10 transition-colors glass-flex glass-items-center glass-gap-3 first:glass-radius-t-lg last:glass-radius-b-lg', children: [suggestion.icon && jsx("span", { className: 'text-primary/60', children: suggestion.icon }), jsxs("div", { className: "glass-flex-1", children: [jsx("span", { className: 'text-primary', children: suggestion.text }), suggestion.category && jsxs("span", { className: 'text-primary/60 glass-text-sm glass-ml-2', children: ["in ", suggestion.category] })] }), suggestion.count && jsx("span", { className: 'text-primary/60 glass-text-sm', children: suggestion.count })] }, suggestion.id)) }) })] }), activeFilterCount > 0 && jsxs("div", { className: "glass-flex glass-flex-wrap glass-gap-2", children: [Object.entries(activeFilters).map(([filterId, value]) => { if (!value || value === "" || Array.isArray(value) && value.length === 0) return null; const filter = filters.find(f => f.id === filterId); const displayValue = getFilterValueDisplay(filter); return jsxs(GlassBadge, { variant: "secondary", className: "glass-flex glass-items-center glass-gap-2", children: [jsxs("span", { className: "glass-text-sm", children: [filter?.label, ": ", displayValue] }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => handleFilterChange(filterId, null), className: 'glass-p-0 h-auto', children: jsx(X, { className: 'w-3 h-3' }) })] }, filterId); }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: handleClearFilters, className: "glass-text-sm", children: "Clear all" })] }), showFilters && enableAdvancedFilters && jsx(MotionFramer, { preset: "slideDown", className: "glass-auto-gap glass-auto-gap-lg glass-p-4 glass-surface-subtle/5 glass-radius-lg", children: jsx("div", { className: 'glass-grid glass-grid-cols-1 md:grid-cols-2 lg:grid-cols-3 glass-gap-4', children: filters.map(filter => jsxs("div", { className: "glass-auto-gap glass-auto-gap-sm", children: [jsx("label", { className: 'text-primary/80 glass-text-sm font-medium', children: filter.label }), filter.type === "text" && jsx("input", { type: "text", value: activeFilters[filter.id] || "", onChange: e => handleFilterChange(filter.id, e.target.value), placeholder: filter.placeholder, className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 ring-1 ring-white/10 glass-radius-md text-primary placeholder-white/50 focus:outline-none focus:ring-white/30' }), filter.type === "select" && jsxs("select", { value: activeFilters[filter.id] || "", onChange: e => handleFilterChange(filter.id, e.target.value), className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 ring-1 ring-white/10 glass-radius-md text-primary focus:outline-none focus:ring-white/30', children: [jsx("option", { value: "", children: "All" }), filter.options?.map(option => jsx("option", { value: option.value, children: option.label }, option.value))] }), filter.type === "date" && jsx("input", { type: "date", value: activeFilters[filter.id] || "", onChange: e => handleFilterChange(filter.id, e.target.value), className: 'glass-w-full glass-px-3 glass-py-2 glass-surface-subtle/10 ring-1 ring-white/10 glass-radius-md text-primary focus:outline-none focus:ring-white/30' })] }, filter.id)) }) }), enableHistory && searchHistory.length > 0 && !query && jsxs("div", { className: "glass-auto-gap glass-auto-gap-sm", children: [jsxs("h4", { className: 'text-primary/80 glass-text-sm font-medium glass-flex glass-items-center glass-gap-2', children: [jsx(History, { className: 'w-4 h-4' }), "Recent Searches"] }), jsx("div", { className: "glass-flex glass-flex-wrap glass-gap-2", children: searchHistory.slice(0, 5).map((search, index) => jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { setQuery(search); onSearch?.(search, activeFilters); }, className: "glass-text-sm", children: search }, index)) })] }), query && jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-4", children: [showStats && jsx("span", { className: 'text-primary/80 glass-text-sm', children: loading ? "Searching..." : `${results.length} results` }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx("span", { className: 'text-primary/80 glass-text-sm', children: "Sort:" }), jsxs("select", { value: sortBy, onChange: e => setSortBy(e.target.value), className: 'glass-surface-subtle/10 ring-1 ring-white/10 glass-radius-md glass-px-2 glass-py-1 glass-text-sm text-primary focus:outline-none', children: [jsx("option", { value: "relevance", children: "Relevance" }), jsx("option", { value: "date", children: "Date" }), jsx("option", { value: "title", children: "Title" })] }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setSortOrder(sortOrder === "asc" ? "desc" : "asc"), className: "glass-p-1", children: sortOrder === "asc" ? jsx(SortAsc, { className: 'w-4 h-4' }) : jsx(SortDesc, { className: 'w-4 h-4' }) })] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [jsx(GlassButton, { variant: viewMode === "list" ? "primary" : "ghost", size: "sm", onClick: e => setViewMode("list"), className: "glass-p-2", children: jsx(List, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: viewMode === "grid" ? "primary" : "ghost", size: "sm", onClick: e => setViewMode("grid"), className: "glass-p-2", children: jsx(Grid3X3, { className: 'w-4 h-4' }) })] })] }), query && jsx("div", { className: "glass-auto-gap glass-auto-gap-md", children: loading ? jsx("div", { className: "glass-flex glass-items-center glass-justify-center glass-py-12", children: jsx("div", { className: 'animate-spin glass-radius-full h-8 w-8 glass-border-2 glass-border-white/20 glass-border-t-white/60' }) }) : results.length === 0 ? jsxs("div", { className: 'text-center glass-py-12', children: [jsx(Search, { className: 'w-12 h-12 text-primary/40 glass-mx-auto mb-4' }), jsxs("p", { className: 'text-primary/60', children: ["No results found for \"", query, "\""] })] }) : jsx("div", { className: cn(viewMode === "grid" ? "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 glass-gap-4" : "glass-auto-gap glass-auto-gap-md"), children: results.map(result => jsx(MotionFramer, { preset: "fadeIn", className: cn("cursor-pointer transition-all duration-200 hover:scale-[1.02]", viewMode === "list" && "glass-p-4 bg-white/5 glass-radius-lg"), onClick: e => handleResultClick(result), children: jsxs("div", { className: "glass-flex glass-gap-3", children: [result.thumbnail && jsx("div", { className: "glass-flex-shrink-0", children: jsx("img", { src: result.thumbnail, alt: result.title, className: 'w-12 h-12 glass-radius-md object-cover' }) }), jsx("div", { className: "glass-flex-1 glass-min-w-0", children: jsxs("div", { className: "glass-flex glass-items-start glass-justify-between glass-gap-2", children: [jsxs("div", { className: "glass-flex-1", children: [jsx("h3", { className: 'text-primary font-medium truncate', children: result.title }), result.description && jsx("p", { className: 'text-primary/70 glass-text-sm glass-mt-1 line-clamp-2', children: result.description }), jsxs("div", { className: 'glass-flex glass-items-center glass-gap-3 glass-mt-2 glass-text-xs text-primary/60', children: [jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [getResultTypeIcon(result.type), jsx("span", { className: 'capitalize', children: result.type })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(Clock, { className: 'w-3 h-3' }), result.updatedAt.toLocaleDateString()] }), result.metadata.size && jsx("span", { children: formatFileSize(result.metadata.size) })] })] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => { e.stopPropagation(); // Handle favorite }, className: "glass-p-1", children: jsx(Star, { className: 'w-3 h-3' }) }), jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => e.stopPropagation(), className: "glass-p-1", children: jsx(MoreHorizontal, { className: 'w-3 h-3' }) })] })] }) })] }) }, result.id)) }) })] })] }) }); }); GlassAdvancedSearch.displayName = "GlassAdvancedSearch"; export { GlassAdvancedSearch, GlassAdvancedSearch as default }; //# sourceMappingURL=GlassAdvancedSearch.js.map