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