UNPKG

aura-glass

Version:

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

444 lines (441 loc) 17.5 kB
'use client'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { GlassButton } from '../button/GlassButton.js'; import { GlassInput } from '../input/GlassInput.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useCallback, useEffect } from 'react'; import { FocusTrap } from '../../primitives/focus/FocusTrap.js'; 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 { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassBadge } from '../data-display/GlassBadge.js'; const IS_TEST_ENV = typeof process !== "undefined" && process.env?.JEST_WORKER_ID !== undefined; /** * GlassSearchInterface component * Advanced search interface with filters, categories, and results */ const GlassSearchInterface = /*#__PURE__*/forwardRef(({ placeholder = "Search...", value = "", onChange, onSearch, results = [], suggestions = [], recentSearches = [], filters = {}, selectedFilters = {}, onFiltersChange, categories = [], activeCategory, onCategoryChange, loading = false, emptyMessage = "No results found", onResultClick, renderResult, variant = "default", showFilters = true, showCategories = true, maxResults = 10, debounceDelay = 300, className, ...props }, ref) => { const [internalValue, setInternalValue] = useState(value); const [isOpen, setIsOpen] = useState(false); const [focusedIndex, setFocusedIndex] = useState(-1); const searchRef = useRef(null); const resultsRef = useRef(null); const debounceRef = useRef(undefined); const displayResults = results.slice(0, maxResults); const hasActiveFilters = Object.values(selectedFilters).some(arr => arr.length > 0); // Debounced search const debouncedSearch = useCallback(query => { if (IS_TEST_ENV) { onSearch?.(query, selectedFilters); return; } if (debounceRef.current) { clearTimeout(debounceRef.current); } debounceRef.current = setTimeout(() => { onSearch?.(query, selectedFilters); }, debounceDelay); }, [onSearch, selectedFilters, debounceDelay]); useEffect(() => { return () => { if (debounceRef.current) { clearTimeout(debounceRef.current); } }; }, []); // Handle input change const handleInputChange = newValue => { setInternalValue(newValue); onChange?.(newValue); setIsOpen(true); setFocusedIndex(-1); if (newValue.trim()) { debouncedSearch(newValue); } }; // Handle filter change const handleFilterChange = (filterId, optionValue, checked) => { const currentFilters = { ...selectedFilters }; if (!currentFilters[filterId]) { currentFilters[filterId] = []; } if (checked) { currentFilters[filterId] = [...currentFilters[filterId], optionValue]; } else { currentFilters[filterId] = currentFilters[filterId].filter(v => v !== optionValue); } onFiltersChange?.(currentFilters); if (internalValue.trim()) { onSearch?.(internalValue, currentFilters); } }; // Handle keyboard navigation const handleKeyDown = event => { if (!isOpen) return; const totalItems = suggestions.length + displayResults.length + recentSearches.length; switch (event.key) { case "ArrowDown": event.preventDefault(); setFocusedIndex(prev => (prev + 1) % totalItems); break; case "ArrowUp": event.preventDefault(); setFocusedIndex(prev => (prev - 1 + totalItems) % totalItems); break; case "Enter": event.preventDefault(); if (focusedIndex >= 0) { handleItemSelect(focusedIndex); } else if (internalValue.trim()) { onSearch?.(internalValue, selectedFilters); setIsOpen(false); } break; case "Escape": event.preventDefault(); setIsOpen(false); setFocusedIndex(-1); break; } }; // Handle item selection const handleItemSelect = index => { let currentIndex = 0; // Check suggestions if (index < suggestions.length) { const suggestion = suggestions[index]; setInternalValue(suggestion); onChange?.(suggestion); onSearch?.(suggestion, selectedFilters); setIsOpen(false); return; } currentIndex += suggestions.length; // Check recent searches if (index < currentIndex + recentSearches.length) { const recent = recentSearches[index - currentIndex]; setInternalValue(recent); onChange?.(recent); onSearch?.(recent, selectedFilters); setIsOpen(false); return; } currentIndex += recentSearches.length; // Check results if (index < currentIndex + displayResults.length) { const result = displayResults[index - currentIndex]; onResultClick?.(result); setIsOpen(false); return; } }; // Clear filters const clearFilters = () => { onFiltersChange?.({}); if (internalValue.trim()) { onSearch?.(internalValue, {}); } }; // Clear search const clearSearch = () => { setInternalValue(""); onChange?.(""); setIsOpen(false); }; const variantClasses = { default: "max-w-2xl", modal: "w-full max-w-4xl", inline: "w-full", compact: "max-w-md" }; return jsxs("div", { "data-glass-component": true, ref: ref, className: cn("relative", variantClasses[variant], className), ...props, children: [showCategories && categories.length > 0 && jsxs("div", { className: 'glass-flex glass-items-center glass-gap-2 mb-4 overflow-x-auto', children: [jsx(GlassButton, { variant: !activeCategory ? "primary" : "ghost", size: "sm", onClick: e => onCategoryChange?.(""), children: "All" }), categories.map(category => jsxs(GlassButton, { variant: activeCategory === category.id ? "primary" : "ghost", size: "sm", leftIcon: category.icon, onClick: e => onCategoryChange?.(category.id), children: [category.label, category.count && jsx(GlassBadge, { variant: "outline", size: "xs", className: "glass-ml-2", children: category.count })] }, category.id))] }), jsxs("div", { className: 'relative', children: [jsx(GlassInput, { ref: searchRef, value: internalValue, onChange: e => handleInputChange(e.target.value), onFocus: () => { if (!IS_TEST_ENV) { setIsOpen(true); } }, onKeyDown: handleKeyDown, placeholder: placeholder, leftIcon: jsx("svg", { className: 'w-4 h-4', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }), rightIcon: loading ? jsx("div", { className: 'w-4 h-4 glass-border-2 glass-border-current glass-border-t-transparent glass-radius-full animate-spin' }) : undefined, clearable: true, onClear: clearSearch, fullWidth: true }), hasActiveFilters && jsx("div", { className: 'absolute right-12 glass-top-1/2 -translate-y-1/2', children: jsxs(GlassBadge, { variant: "default", size: "xs", onClick: clearFilters, className: 'cursor-pointer', children: [Object.values(selectedFilters).flat().length, " filters"] }) })] }), isOpen && jsx(MotionFramer, { preset: "slideDown", className: 'absolute top-full left-0 right-0 glass-mt-2 z-50', children: jsx(OptimizedGlassCore, { intent: "neutral", elevation: "level2", intensity: "medium", depth: 2, tint: "neutral", border: "subtle", animation: "none", performanceMode: "medium", className: 'glass-border glass-border-glass-border/20 max-h-96 overflow-hidden', children: jsx(FocusTrap, { active: isOpen, onEscape: () => setIsOpen(false), children: jsxs("div", { ref: resultsRef, className: 'overflow-y-auto max-h-96', children: [suggestions.length > 0 && jsxs("div", { className: "glass-p-2 glass-border-b glass-border-glass-border/20", children: [jsx("h4", { className: 'glass-px-3 glass-py-2 glass-text-xs font-medium glass-text-secondary uppercase tracking-wide', children: "Suggestions" }), suggestions.map((suggestion, index) => jsxs(GlassButton, { className: cn("w-full flex items-center glass-gap-3 glass-px-3 glass-py-2 glass-radius-md text-left", "hover:bg-muted/50 transition-colors", focusedIndex === index && "bg-muted/50"), onClick: e => handleItemSelect(index), children: [jsx("svg", { className: 'w-4 h-4 glass-text-secondary', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }), jsx("span", { children: suggestion })] }, suggestion))] }), recentSearches.length > 0 && jsxs("div", { className: "glass-p-2 glass-border-b glass-border-glass-border/20", children: [jsx("h4", { className: 'glass-px-3 glass-py-2 glass-text-xs font-medium glass-text-secondary uppercase tracking-wide', children: "Recent Searches" }), recentSearches.map((recent, index) => { const globalIndex = suggestions.length + index; return jsxs(GlassButton, { className: cn("w-full flex items-center glass-gap-3 glass-px-3 glass-py-2 glass-radius-md text-left", "hover:bg-muted/50 transition-colors", focusedIndex === globalIndex && "bg-muted/50"), onClick: e => handleItemSelect(globalIndex), children: [jsx("svg", { className: 'w-4 h-4 glass-text-secondary', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" }) }), jsx("span", { children: recent })] }, recent); })] }), displayResults.length > 0 ? jsxs("div", { className: "glass-p-2", children: [jsxs("h4", { className: 'glass-px-3 glass-py-2 glass-text-xs font-medium glass-text-secondary uppercase tracking-wide', children: ["Results (", results.length, ")"] }), displayResults.map((result, index) => { const globalIndex = suggestions.length + recentSearches.length + index; return jsx(GlassButton, { className: cn("w-full flex items-start glass-gap-3 glass-px-3 glass-py-3 glass-radius-md text-left", "hover:bg-muted/50 transition-colors", focusedIndex === globalIndex && "bg-muted/50"), onClick: e => handleItemSelect(globalIndex), children: renderResult ? renderResult(result) : jsxs(Fragment, { children: [jsx("div", { className: 'w-8 h-8 glass-radius-md glass-surface-primary/10 glass-flex glass-items-center glass-justify-center glass-flex-shrink-0 glass-mt-0-5', children: jsx("svg", { className: 'w-4 h-4 text-primary', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }) }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsx("h5", { className: 'font-medium text-primary', children: result.highlighted?.title || result.title }), result.description && jsx("p", { className: 'glass-text-sm glass-text-secondary glass-mt-1 line-clamp-2', children: result.highlighted?.description || result.description }), result.category && jsx(GlassBadge, { variant: "outline", size: "xs", className: "glass-mt-2", children: result.category })] })] }) }, result.id); }), results.length > maxResults && jsx("div", { className: 'glass-px-3 glass-py-2 text-center', children: jsxs(GlassButton, { variant: "ghost", size: "sm", onClick: e => onSearch?.(internalValue, selectedFilters), children: ["View all ", results.length, " results"] }) })] }) : internalValue.trim() && !loading ? jsxs("div", { className: 'glass-p-8 text-center', children: [jsx("div", { className: 'w-12 h-12 glass-radius-full glass-surface-subtle glass-flex glass-items-center glass-justify-center glass-mx-auto mb-3', children: jsx("svg", { className: 'w-6 h-6 glass-text-secondary', fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" }) }) }), jsx("p", { className: "glass-text-secondary", children: emptyMessage })] }) : null] }) }) }) }), showFilters && Object.keys(filters).length > 0 && variant !== "compact" && jsxs(OptimizedGlassCore, { intent: "neutral", elevation: "level1", intensity: "medium", depth: 2, tint: "neutral", border: "subtle", animation: "none", performanceMode: "medium", className: "glass-mt-4 glass-p-4 glass-border glass-border-glass-border/20", children: [jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between mb-4', children: [jsx("h3", { className: 'font-medium text-primary', children: "Filters" }), hasActiveFilters && jsx(GlassButton, { variant: "ghost", size: "sm", onClick: clearFilters, children: "Clear all" })] }), jsx("div", { className: 'glass-grid glass-grid-cols-1 md:grid-cols-2 lg:grid-cols-3 glass-gap-4', children: Object.entries(filters).map(([filterId, options]) => jsxs("div", { children: [jsx("h4", { className: 'font-medium glass-text-sm text-primary mb-2', children: filterId.charAt(0).toUpperCase() + filterId.slice(1) }), jsx("div", { className: "glass-gap-2", children: options.map(option => jsxs("label", { className: 'glass-flex glass-items-center glass-gap-2 cursor-pointer', children: [jsx(GlassInput, { type: "checkbox", checked: selectedFilters[filterId]?.includes(option.value) || false, onChange: e => handleFilterChange(filterId, option.value, e.target.checked), className: 'glass-radius-md glass-border-glass-border focus:ring-primary' }), jsx("span", { className: 'glass-text-sm text-primary glass-flex-1', children: option.label }), option.count && jsx("span", { className: "glass-text-xs glass-text-secondary", children: option.count })] }, option.id)) })] }, filterId)) })] })] }); }); GlassSearchInterface.displayName = "GlassSearchInterface"; export { GlassSearchInterface }; //# sourceMappingURL=GlassSearchInterface.js.map