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