aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
538 lines (535 loc) • 23.1 kB
JavaScript
'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