UNPKG

aura-glass

Version:

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

381 lines (378 loc) 14.3 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { GlassButton } from '../button/GlassButton.js'; import { cn } from '../../lib/utilsComprehensive.js'; import { forwardRef, useState, useRef, useEffect, 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 { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { GlassBadge } from '../data-display/GlassBadge.js'; import { GlassInput } from '../input/GlassInput.js'; /** * GlassCommandPalette component * Modal command palette with search, keyboard navigation, and glassmorphism styling */ const GlassCommandPalette = /*#__PURE__*/forwardRef(({ items = [], groups = [], open = false, onOpenChange, onSelect, placeholder = "Search commands...", emptyMessage = "No commands found", maxResults = 50, enableRecents = true, recentsKey = "glass-command-palette-recents", maxRecents = 5, filter, sort, fuzzySearch = true, showCategories = true, showShortcuts = true, backdropBlur = true, closeOnEscape = true, closeOnSelect = true, loading = false, loadingMessage = "Loading commands...", className, ...props }, ref) => { // State const [search, setSearch] = useState(""); const [selectedIndex, setSelectedIndex] = useState(0); const [recentCommands, setRecentCommands] = useState([]); // Refs const inputRef = useRef(null); const listRef = useRef(null); // Load recent commands from localStorage useEffect(() => { if (!enableRecents || typeof window === "undefined") return; try { const stored = localStorage.getItem(recentsKey); if (stored) { setRecentCommands(JSON.parse(stored)); } } catch (error) { console.warn("Failed to load recent commands:", error); } }, [enableRecents, recentsKey]); // Save recent commands to localStorage const saveRecentCommand = item => { if (!enableRecents || typeof window === "undefined") return; try { const newRecents = [item, ...recentCommands.filter(cmd => cmd.id !== item?.id)].slice(0, maxRecents); setRecentCommands(newRecents); localStorage.setItem(recentsKey, JSON.stringify(newRecents)); } catch (error) { console.warn("Failed to save recent command:", error); } }; // Combine items and groups into flat list const allItems = useMemo(() => { const flatItems = [...items]; groups.forEach(group => { flatItems.push(...group.items.map(item => ({ ...item, category: item?.category || group.label }))); }); return flatItems; }, [items, groups]); // Default filter function const defaultFilter = (item, searchTerm) => { if (!searchTerm) return true; const normalizedSearch = searchTerm.toLowerCase(); const label = item?.label.toLowerCase(); const description = (item?.description || "").toLowerCase(); const keywords = (item?.keywords || []).join(" ").toLowerCase(); if (fuzzySearch) { // Simple fuzzy search implementation const searchRegex = new RegExp(normalizedSearch.split("").join(".*"), "i"); return searchRegex.test(label) || searchRegex.test(description) || searchRegex.test(keywords); } else { return label.includes(normalizedSearch) || description.includes(normalizedSearch) || keywords.includes(normalizedSearch); } }; // Filter and sort items const filteredItems = useMemo(() => { let result = allItems; // Apply search filter if (search) { result = result.filter(item => filter ? filter(item, search) : defaultFilter(item, search)); } else if (enableRecents && (recentCommands?.length || 0) > 0) { // Show recent commands when no search result = recentCommands.filter(recent => allItems.some(item => item?.id === recent.id)); } // Apply custom sort or default priority sort if (sort) { result.sort(sort); } else { // Default sort: exact matches first, then by relevance result.sort((a, b) => { if (search) { const aExact = a.label.toLowerCase().startsWith(search.toLowerCase()) ? 1 : 0; const bExact = b.label.toLowerCase().startsWith(search.toLowerCase()) ? 1 : 0; if (aExact !== bExact) return bExact - aExact; } return a.label.localeCompare(b.label); }); } // Limit results return result.slice(0, maxResults); }, [allItems, search, filter, sort, maxResults, fuzzySearch, enableRecents, recentCommands]); // Group filtered items by category const groupedItems = useMemo(() => { if (!showCategories) return { All: filteredItems }; const grouped = filteredItems.reduce((acc, item) => { const category = item?.category || "Other"; if (!acc[category]) acc[category] = []; acc[category].push(item); return acc; }, {}); return grouped; }, [filteredItems, showCategories]); // Handle item selection const handleSelect = item => { if (item?.disabled) return; // Save to recents saveRecentCommand(item); // Execute action item?.action?.(); onSelect?.(item); // Close palette if configured if (closeOnSelect) { onOpenChange?.(false); } // Reset search setSearch(""); setSelectedIndex(0); }; // Handle keyboard navigation const handleKeyDown = e => { switch (e.key) { case "Escape": if (closeOnEscape) { e.preventDefault(); onOpenChange?.(false); } break; case "ArrowDown": e.preventDefault(); setSelectedIndex(prev => prev < (filteredItems?.length || 0) - 1 ? prev + 1 : prev); break; case "ArrowUp": e.preventDefault(); setSelectedIndex(prev => prev > 0 ? prev - 1 : prev); break; case "Enter": e.preventDefault(); if (filteredItems[selectedIndex]) { handleSelect(filteredItems[selectedIndex]); } break; case "Home": e.preventDefault(); setSelectedIndex(0); break; case "End": e.preventDefault(); setSelectedIndex((filteredItems?.length || 0) - 1); break; } }; // Reset selection when search changes useEffect(() => { setSelectedIndex(0); }, [search]); // Focus input when opened useEffect(() => { if (open && inputRef.current) { inputRef.current.focus(); } }, [open]); // Scroll selected item into view useEffect(() => { if (selectedIndex >= 0 && listRef.current) { const selectedElement = listRef.current.children?.[selectedIndex]; if (selectedElement) { selectedElement.scrollIntoView({ block: "nearest", behavior: "smooth" }); } } }, [selectedIndex]); if (!open) return null; return jsxs("div", { "data-glass-component": true, className: 'fixed inset-0 z-50 glass-flex glass-items-start glass-justify-center pt-[10vh]', onClick: e => { if (e.target === e.currentTarget) { onOpenChange?.(false); } }, children: [jsx("div", { className: cn("absolute inset-0 bg-black/20", backdropBlur && "glass-backdrop-blur-md") }), jsx(MotionFramer, { preset: "scaleIn", duration: 200, className: 'relative glass-w-full max-w-2xl glass-mx-4', children: jsxs(OptimizedGlassCore, { ref: ref, intent: "neutral", elevation: "level4", intensity: "strong", depth: 3, tint: "neutral", border: "glow", animation: "float", performanceMode: "high", className: cn("w-full max-h-[80vh] overflow-hidden glass-radius-xl", className), onKeyDown: handleKeyDown, ...props, children: [jsx("div", { className: "glass-p-4 glass-border-b glass-border-glass-border/10", children: jsx(GlassInput, { ref: inputRef, value: search, onChange: e => setSearch(e.target.value), placeholder: placeholder, size: "lg", leftIcon: jsx("svg", { className: 'w-5 h-5', 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: search && jsx(GlassButton, { type: "button", className: 'glass-p-1 glass-radius-md hover:glass-surface-subtle transition-colors', onClick: e => setSearch(""), children: 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: "M6 18L18 6M6 6l12 12" }) }) }), className: 'glass-border-0 bg-transparent focus:ring-0' }) }), jsx("div", { ref: listRef, className: 'max-h-96 overflow-y-auto overscroll-contain', role: "listbox", children: loading ? jsx("div", { className: "glass-flex glass-items-center glass-justify-center glass-py-8", children: jsxs("div", { className: "glass-flex glass-items-center glass-gap-3", children: [jsx("div", { className: 'w-5 h-5 glass-border-2 glass-border-primary glass-border-t-transparent glass-radius-full animate-spin' }), jsx("span", { className: "glass-text-secondary", children: loadingMessage })] }) }) : (filteredItems?.length || 0) === 0 ? jsx("div", { className: 'glass-py-8 text-center glass-text-secondary', children: emptyMessage }) : Object.entries(groupedItems).map(([category, categoryItems]) => jsxs("div", { children: [showCategories && Object.keys(groupedItems).length > 1 && jsx("div", { className: 'glass-px-4 glass-py-2 glass-text-xs font-medium glass-text-secondary glass-surface-subtle glass-border-b glass-border-glass-border/5', children: search ? "Results" : category }), categoryItems.map((item, index) => { const globalIndex = filteredItems.indexOf(item); const isSelected = globalIndex === selectedIndex; if (item?.component) { const Component = item?.component; return jsx(Component, { item: item, isSelected: isSelected }, item?.id); } return jsxs(GlassButton, { type: "button", className: cn("w-full flex items-center glass-gap-3 glass-px-4 glass-py-3 text-left transition-colors", "hover:bg-muted/20 focus:bg-muted/20 focus:outline-none", { "bg-primary/10 border-l-2 border-primary": isSelected, "opacity-50 cursor-not-allowed": item?.disabled }), onClick: e => handleSelect(item), disabled: item?.disabled, role: "option", "aria-selected": isSelected, children: [item?.icon && jsx("span", { className: "glass-flex-shrink-0 glass-text-secondary", children: item?.icon }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [jsx("div", { className: 'font-medium text-primary', children: item?.label }), item?.description && jsx("div", { className: 'glass-text-sm glass-text-secondary truncate', children: item?.description })] }), showShortcuts && item?.shortcut && jsx(GlassBadge, { variant: "secondary", size: "sm", children: item?.shortcut })] }, item?.id); })] }, category)) }), (filteredItems?.length || 0) > 0 && jsx("div", { className: "glass-px-4 glass-py-2 glass-text-xs glass-text-secondary glass-surface-subtle glass-border-t glass-border-glass-border/5", children: jsxs("div", { className: "glass-flex glass-items-center glass-justify-between", children: [jsxs("span", { children: [filteredItems?.length || 0, " ", (filteredItems?.length || 0) === 1 ? "result" : "results"] }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-4", children: [jsxs("span", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(GlassBadge, { variant: "secondary", size: "sm", children: "\u2191\u2193" }), "Navigate"] }), jsxs("span", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(GlassBadge, { variant: "secondary", size: "sm", children: "\u21B5" }), "Select"] }), jsxs("span", { className: "glass-flex glass-items-center glass-gap-1", children: [jsx(GlassBadge, { variant: "secondary", size: "sm", children: "Esc" }), "Close"] })] })] }) })] }) })] }); }); GlassCommandPalette.displayName = "GlassCommandPalette"; export { GlassCommandPalette }; //# sourceMappingURL=GlassCommandPalette.js.map