aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
229 lines (226 loc) • 9.28 kB
JavaScript
'use client';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { forwardRef, useState, useRef, useEffect, 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 { createPortal } from 'react-dom';
const GlassSpotlightSearch = /*#__PURE__*/forwardRef(({
open,
onClose,
actions,
placeholder = "Search for actions...",
maxResults = 10,
elevation = "level5",
showRecent = true,
maxRecent = 5,
fuzzySearch = true,
groupByCategory = true,
className,
...props
}, ref) => {
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const [recentActions, setRecentActions] = useState([]);
const inputRef = useRef(null);
const resultsRef = useRef(null);
useEffect(() => {
if (open) {
setQuery("");
setSelectedIndex(0);
setTimeout(() => inputRef.current?.focus(), 100);
}
}, [open]);
useEffect(() => {
if (!open) return;
const handleKeyDown = e => {
switch (e.key) {
case "Escape":
e.preventDefault();
onClose();
break;
case "ArrowDown":
e.preventDefault();
setSelectedIndex(prev => Math.min(prev + 1, filteredActions.length - 1));
break;
case "ArrowUp":
e.preventDefault();
setSelectedIndex(prev => Math.max(prev - 1, 0));
break;
case "Enter":
e.preventDefault();
if (filteredActions[selectedIndex]) {
handleActionSelect(filteredActions[selectedIndex]);
}
break;
}
};
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [open, selectedIndex, onClose]);
useEffect(() => {
if (resultsRef.current && open) {
const selectedElement = resultsRef.current.children[selectedIndex];
if (selectedElement) {
selectedElement.scrollIntoView({
block: "nearest",
behavior: "smooth"
});
}
}
}, [selectedIndex, open]);
const fuzzyMatch = useCallback((text, query) => {
const textLower = text.toLowerCase();
const queryLower = query.toLowerCase();
if (textLower.includes(queryLower)) return true;
let queryIndex = 0;
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
if (textLower[i] === queryLower[queryIndex]) {
queryIndex++;
}
}
return queryIndex === queryLower.length;
}, []);
const filteredActions = useMemo(() => {
if (!query) {
if (showRecent && recentActions.length > 0) {
return actions.filter(action => recentActions.includes(action.id)).slice(0, maxRecent);
}
return actions.slice(0, maxResults);
}
const matches = actions.filter(action => {
const searchText = [action.title, action.description || "", ...(action.keywords || [])].join(" ");
return fuzzySearch ? fuzzyMatch(searchText, query) : searchText.toLowerCase().includes(query.toLowerCase());
});
return matches.slice(0, maxResults);
}, [query, actions, showRecent, recentActions, maxRecent, maxResults, fuzzySearch, fuzzyMatch]);
const groupedActions = useMemo(() => {
if (!groupByCategory) return {
"": filteredActions
};
return filteredActions.reduce((acc, action) => {
const category = action.category || "Other";
if (!acc[category]) {
acc[category] = [];
}
acc[category].push(action);
return acc;
}, {});
}, [filteredActions, groupByCategory]);
const handleActionSelect = useCallback(action => {
action.onAction();
setRecentActions(prev => {
const updated = [action.id, ...prev.filter(id => id !== action.id)].slice(0, maxRecent);
localStorage.setItem("spotlight-recent", JSON.stringify(updated));
return updated;
});
onClose();
}, [onClose, maxRecent]);
useEffect(() => {
if (typeof window !== "undefined") {
const stored = localStorage.getItem("spotlight-recent");
if (stored) {
try {
setRecentActions(JSON.parse(stored));
} catch (e) {
console.error("Failed to parse recent actions", e);
}
}
}
}, []);
if (!open) return null;
const content = jsx("div", {
className: 'fixed inset-0 z-50 glass-flex glass-items-start glass-justify-center glass-p-4 pt-24 glass-surface-dark/40 glass-glass-glass-backdrop-blur-sm glass-contrast-guard',
onClick: onClose,
role: "presentation",
children: jsxs(OptimizedGlassCore, {
ref: ref,
elevation: elevation,
className: cn("w-full max-w-2xl overflow-hidden glass-radius-2xl", "transform transition-all duration-200 ease-out", open ? "scale-100 opacity-100" : "scale-95 opacity-0", className),
onClick: e => e.stopPropagation(),
role: "dialog",
"aria-modal": "true",
"aria-label": "Command search",
...props,
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3 glass-p-4 glass-border-b glass-border-subtle",
children: [jsx("svg", {
viewBox: "0 0 24 24",
fill: "none",
className: 'w-5 h-5 glass-text-secondary glass-flex-shrink-0',
children: jsx("path", {
d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round"
})
}), jsx("input", {
ref: inputRef,
type: "text",
value: query,
onChange: e => {
setQuery(e.target.value);
setSelectedIndex(0);
},
placeholder: placeholder,
className: 'glass-flex-1 bg-transparent glass-text-base glass-text-primary outline-none placeholder:glass-text-secondary glass-focus glass-touch-target glass-contrast-guard'
}), jsx("kbd", {
className: "glass-px-2 glass-py-1 glass-text-xs glass-text-secondary glass-surface-subtle/10 glass-radius-sm",
children: "ESC"
})]
}), jsx("div", {
ref: resultsRef,
className: 'max-h-96 overflow-y-auto glass-p-2',
children: filteredActions.length === 0 ? jsx("div", {
className: 'glass-p-8 text-center glass-text-secondary',
children: query ? "No results found" : "No actions available"
}) : jsxs(Fragment, {
children: [!query && showRecent && recentActions.length > 0 && jsx("div", {
className: 'glass-px-3 glass-py-2 glass-text-xs font-semibold glass-text-secondary uppercase tracking-wider',
children: "Recent"
}), Object.entries(groupedActions).map(([category, categoryActions]) => jsxs("div", {
children: [groupByCategory && category && jsx("div", {
className: 'glass-px-3 glass-py-2 glass-text-xs font-semibold glass-text-secondary uppercase tracking-wider',
children: category
}), categoryActions.map((action, index) => {
const globalIndex = filteredActions.indexOf(action);
const isSelected = globalIndex === selectedIndex;
return jsxs("button", {
"data-glass-component": true,
type: "button",
onClick: () => handleActionSelect(action),
onMouseEnter: () => setSelectedIndex(globalIndex),
className: cn("w-full flex items-center gap-3 glass-p-3 glass-radius-lg", "transition-all duration-200", "text-left glass-focus glass-touch-target glass-contrast-guard", isSelected ? "bg-white/15 shadow-lg" : "hover:bg-white/5"),
children: [action.icon && jsx("div", {
className: "glass-flex-shrink-0 glass-text-primary",
children: action.icon
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsx("div", {
className: 'glass-text-sm font-medium glass-text-primary truncate',
children: action.title
}), action.description && jsx("div", {
className: 'glass-text-xs glass-text-secondary truncate',
children: action.description
})]
}), action.shortcut && jsx("kbd", {
className: "glass-px-2 glass-py-1 glass-text-xs glass-text-secondary glass-surface-subtle/10 glass-radius-sm glass-flex-shrink-0",
children: action.shortcut
})]
}, action.id);
})]
}, category))]
})
})]
})
});
return /*#__PURE__*/createPortal(content, document.body);
});
GlassSpotlightSearch.displayName = "GlassSpotlightSearch";
export { GlassSpotlightSearch, GlassSpotlightSearch as default };
//# sourceMappingURL=GlassSpotlightSearch.js.map