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