UNPKG

aura-glass

Version:

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

461 lines (458 loc) 17.4 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { AnimatePresence, motion } from 'framer-motion'; import { Search, X, SlidersHorizontal, Filter, Tag, ChevronDown, ChevronRight } from 'lucide-react'; import React, { useState, useCallback, useMemo } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { GlassButton } from '../button/GlassButton.js'; import { GlassCheckbox } from '../input/GlassCheckbox.js'; import { GlassDateRangePicker } from '../input/GlassDateRangePicker.js'; import { GlassInput } from '../input/GlassInput.js'; import { GlassSlider } from '../input/GlassSlider.js'; const GlassFacetSearch = /*#__PURE__*/React.forwardRef(({ query, onQueryChange, facets, facetValues, onFacetChange, results, onResultSelect, placeholder = "Search...", loading = false, showFilters = true, showResults = true, maxResults = 10, className, onSearch, suggestions = [], onSuggestionSelect, recentSearches = [], onRecentSearchSelect, variant = "default", size = "md", elevation = "medium", ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const [showSuggestions, setShowSuggestions] = useState(false); const [expandedFacets, setExpandedFacets] = useState(Object.fromEntries(facets.map(f => [f.id, !f.collapsed]))); const [showFacetPanel, setShowFacetPanel] = useState(showFilters); const handleQueryChange = useCallback(e => { const newQuery = e.target.value; onQueryChange(newQuery); setShowSuggestions(true); }, [onQueryChange]); const handleSearch = useCallback(() => { onSearch?.(query, facetValues); setShowSuggestions(false); }, [query, facetValues, onSearch]); const handleKeyDown = useCallback(e => { if (e.key === "Enter") { handleSearch(); } else if (e.key === "Escape") { setShowSuggestions(false); } }, [handleSearch]); const handleFacetToggle = useCallback(facetId => { setExpandedFacets(prev => ({ ...prev, [facetId]: !prev[facetId] })); }, []); const handleFacetChange = useCallback((facetId, value) => { onFacetChange(facetId, value); // Auto-search when facets change setTimeout(() => onSearch?.(query, { ...facetValues, [facetId]: value }), 300); }, [onFacetChange, onSearch, query, facetValues]); const clearFilters = useCallback(() => { const clearedValues = Object.fromEntries(facets.map(f => [f.id, f.type === "checkbox" ? [] : null])); Object.keys(clearedValues).forEach(facetId => { onFacetChange(facetId, clearedValues[facetId]); }); onSearch?.(query, clearedValues); }, [facets, onFacetChange, onSearch, query]); const hasActiveFilters = useMemo(() => { return Object.values(facetValues).some(value => Array.isArray(value) ? (value?.length || 0) > 0 : value != null); }, [facetValues]); const displayedResults = useMemo(() => { return results.slice(0, maxResults); }, [results, maxResults]); const sizeClasses = { sm: "glass-text-sm", md: "glass-text-base", lg: "glass-text-lg" }; const variantClasses = { default: "glass-p-6", compact: "glass-p-4", minimal: "glass-p-2" }; const elevationClasses = { low: "glass-backdrop-blur-md bg-white/10 border border-white/20", medium: "glass-backdrop-blur-md bg-white/20 border border-white/30 shadow-lg", high: "glass-backdrop-blur-md bg-white/30 border border-white/40 shadow-2xl" }; return jsxs("div", { ref: ref, className: cn("glass-radius-xl", elevationClasses[elevation], variantClasses[variant], sizeClasses[size], className), ...props, children: [jsxs("div", { className: 'relative mb-4', children: [jsx(GlassInput, { value: query, onChange: handleQueryChange, onKeyDown: handleKeyDown, placeholder: placeholder, className: 'glass-w-full pr-12', leftIcon: jsx(Search, { className: 'w-4 h-4' }) }), jsxs("div", { className: 'absolute right-3 glass-top-1/2 -translate-y-1/2 glass-flex glass-items-center glass-gap-2', children: [loading && jsx("div", { className: 'w-4 h-4 glass-border-2 glass-border-white/30 glass-border-t-white glass-radius-full animate-spin' }), query && jsx("button", { onClick: e => onQueryChange(""), className: 'text-primary/70 hover:text-primary transition-colors glass-focus glass-touch-target glass-contrast-guard', children: jsx(X, { className: 'w-4 h-4' }) }), showFilters && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setShowFacetPanel(!showFacetPanel), className: cn("transition-colors", hasActiveFilters && "text-blue-400"), children: jsx(SlidersHorizontal, { className: 'w-4 h-4' }) })] }), jsx(AnimatePresence, { children: showSuggestions && (suggestions.length > 0 || recentSearches.length > 0) && jsx(motion.div, { initial: { opacity: 0, y: -10 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, className: 'absolute top-full left-0 right-0 glass-mt-1 z-10 max-h-60 overflow-y-auto', children: jsxs(OptimizedGlassCore, { elevation: "level2", className: 'glass-radius-lg overflow-hidden glass-shadow-lg', children: [suggestions.length > 0 && jsxs("div", { className: "glass-p-2", children: [jsx("div", { className: 'glass-text-xs text-primary/70 glass-px-2 glass-py-1', children: "Suggestions" }), suggestions.map((suggestion, index) => jsx("button", { onClick: e => { onSuggestionSelect?.(suggestion); onQueryChange(suggestion); setShowSuggestions(false); }, className: 'glass-w-full text-left glass-px-2 glass-py-1 glass-text-sm text-primary hover:glass-surface-subtle/10 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard', children: suggestion }, index))] }), recentSearches.length > 0 && jsxs("div", { className: "glass-p-2 glass-border-t glass-border-white/10", children: [jsx("div", { className: 'glass-text-xs text-primary/70 glass-px-2 glass-py-1', children: "Recent Searches" }), recentSearches.map((search, index) => jsx("button", { onClick: e => { onRecentSearchSelect?.(search); onQueryChange(search); setShowSuggestions(false); }, className: 'glass-w-full text-left glass-px-2 glass-py-1 glass-text-sm text-primary hover:glass-surface-subtle/10 glass-radius-md transition-colors glass-focus glass-touch-target glass-contrast-guard', children: search }, index))] })] }) }) })] }), jsx(AnimatePresence, { children: showFacetPanel && showFilters && jsx(motion.div, { initial: { opacity: 0, height: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, className: 'mb-6 glass-border glass-border-white/20 glass-radius-lg overflow-hidden', children: jsxs("div", { className: "glass-p-4", children: [jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-4', children: [jsxs("h3", { className: 'font-semibold text-primary glass-flex glass-items-center glass-gap-2', children: [jsx(Filter, { className: 'w-4 h-4' }), "Filters"] }), hasActiveFilters && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: clearFilters, className: 'text-primary/70 hover:text-primary', children: "Clear All" })] }), jsx("div", { className: "glass-auto-gap glass-auto-gap-lg", children: facets.map(facet => jsx(FacetGroup, { facet: facet, value: facetValues[facet.id], onChange: value => handleFacetChange(facet.id, value), expanded: expandedFacets[facet.id], onToggle: () => handleFacetToggle(facet.id) }, facet.id)) })] }) }) }), showResults && jsxs("div", { className: "glass-auto-gap glass-auto-gap-sm", children: [jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("h3", { className: 'font-semibold text-primary', children: ["Results ", results.length > 0 && `(${results.length})`] }), results.length > maxResults && jsxs("span", { className: 'glass-text-sm text-primary/70', children: ["Showing ", maxResults, " of ", results.length] })] }), jsx(AnimatePresence, { children: loading ? jsx(motion.div, { initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, className: "glass-flex glass-items-center glass-justify-center glass-py-8", children: jsx("div", { className: 'w-6 h-6 glass-border-2 glass-border-white/30 glass-border-t-white glass-radius-full animate-spin' }) }) : displayedResults.length > 0 ? jsx(motion.div, { initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, className: "glass-auto-gap glass-auto-gap-sm", children: displayedResults.map((result, index) => jsx(motion.div, { initial: { opacity: 0, x: -20 }, animate: prefersReducedMotion ? {} : { opacity: 1, x: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, className: 'glass-p-3 glass-surface-dark/20 hover:glass-surface-dark/30 glass-radius-lg cursor-pointer transition-all duration-200 hover:-translate-y-0.5 ring-1 ring-white/10 hover:ring-white/20 glass-border glass-border-white/10 hover:border-white/20', onClick: e => onResultSelect?.(result), children: jsxs("div", { className: "glass-flex glass-items-start glass-justify-between", children: [jsxs("div", { className: "glass-flex-1", children: [jsx("h4", { className: 'font-medium text-primary', children: result.title }), result.description && jsx("p", { className: 'glass-text-sm text-primary/70 glass-mt-1', children: result.description }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2 glass-mt-2", children: [result.category && jsx("span", { className: 'glass-px-2 glass-py-1 glass-text-xs glass-surface-subtle/10 text-primary/80 glass-radius-md', children: result.category }), result.tags && result.tags.map((tag, tagIndex) => jsxs("span", { className: "glass-flex glass-items-center glass-gap-1 glass-px-2 glass-py-1 glass-text-xs glass-surface-blue/20 glass-text-secondary glass-radius-md", children: [jsx(Tag, { className: 'w-3 h-3' }), tag] }, tagIndex))] })] }), result.score && jsxs("div", { className: 'glass-text-xs text-primary/50', children: [Math.round(result.score * 100), "%"] })] }) }, result.id)) }) : query ? jsxs(motion.div, { initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, className: 'text-center glass-py-8 text-primary/70', children: ["No results found for \"", query, "\""] }) : jsx(motion.div, { initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, className: 'text-center glass-py-8 text-primary/70', children: "Start typing to search..." }) })] })] }); }); GlassFacetSearch.displayName = "GlassFacetSearch"; const FacetGroup = ({ facet, value, onChange, expanded, onToggle }) => { const prefersReducedMotion = useReducedMotion(); return jsxs("div", { className: 'glass-border glass-border-white/10 glass-radius-lg overflow-hidden', children: [jsxs("button", { onClick: onToggle, className: 'glass-w-full glass-flex glass-items-center glass-justify-between glass-p-3 text-left hover:glass-surface-subtle/5 transition-colors glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', children: [jsx("span", { className: 'font-medium text-primary', children: facet.label }), expanded ? jsx(ChevronDown, { className: 'w-4 h-4 text-primary/70' }) : jsx(ChevronRight, { className: 'w-4 h-4 text-primary/70' })] }), jsx(AnimatePresence, { children: expanded && jsx(motion.div, { initial: { opacity: 0, height: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1, height: "auto" }, exit: { opacity: 0, height: 0 }, className: 'glass-px-3 pb-3', children: jsx(FacetContent, { facet: facet, value: value, onChange: onChange }) }) })] }); }; const FacetContent = ({ facet, value, onChange }) => { switch (facet.type) { case "checkbox": return jsx("div", { className: "glass-auto-gap glass-auto-gap-sm", children: facet.options?.map(option => jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsx(GlassCheckbox, { id: option.id, checked: Array.isArray(value) && value.includes(option.value), onCheckedChange: checked => { const currentValues = Array.isArray(value) ? value : []; const newValues = checked ? [...currentValues, option.value] : currentValues.filter(v => v !== option.value); onChange(newValues); }, disabled: option.disabled, label: option.label }), facet.showCount && option.count != null && jsxs("span", { className: 'glass-text-xs text-primary/50', children: ["(", option.count, ")"] })] }, option.id)) }); case "range": return jsxs("div", { className: "glass-auto-gap glass-auto-gap-md", children: [jsx(GlassSlider, { min: facet.min || 0, max: facet.max || 100, step: facet.step || 1, value: Array.isArray(value) ? value : [facet.min || 0], onValueChange: onChange, className: "glass-w-full" }), jsxs("div", { className: 'glass-flex glass-justify-between glass-text-sm text-primary/70', children: [jsx("span", { children: facet.min || 0 }), jsx("span", { children: facet.max || 100 })] })] }); case "select": return jsx("div", { className: "glass-auto-gap glass-auto-gap-sm", children: facet.options?.map(option => jsx(GlassCheckbox, { id: option.id, checked: value === option.value, onCheckedChange: checked => { onChange(checked ? option.value : null); }, disabled: option.disabled, label: option.label }, option.id)) }); case "daterange": return jsx(GlassDateRangePicker, { value: value, onChange: onChange, placeholder: "Select date range" }); default: return null; } }; export { GlassFacetSearch }; //# sourceMappingURL=GlassFacetSearch.js.map