aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
344 lines (341 loc) • 13.1 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
import { cn } from '../../lib/utilsComprehensive.js';
import { AnimatePresence, motion } from 'framer-motion';
import { Filter, RotateCcw, Search, Save, ChevronDown, ChevronRight } from 'lucide-react';
import React, { useState, useCallback, useMemo } from 'react';
import { GlassButton } from '../button/GlassButton.js';
import { GlassCheckbox } from '../input/GlassCheckbox.js';
import { GlassDateRangePicker } from '../input/GlassDateRangePicker.js';
import { GlassInput } from '../input/GlassInput.js';
import { GlassSelect, GlassSelectTrigger, GlassSelectValue, GlassSelectContent, GlassSelectItem } from '../input/GlassSelectCompound.js';
import { GlassSlider } from '../input/GlassSlider.js';
const GlassFilterPanel = /*#__PURE__*/React.forwardRef(({
filters,
values,
onChange,
onApply,
onClear,
presets = [],
onSavePreset,
className,
title = "Filters",
collapsible = true,
showSearch = true,
showPresets = true,
showApplyButton = true,
showClearButton = true,
variant = "default",
size = "md",
elevation = "medium",
...props
}, ref) => {
const prefersReducedMotion = useReducedMotion();
const [expandedGroups, setExpandedGroups] = useState(Object.fromEntries(filters.map(f => [f.id, !f.collapsed])));
const [searchQuery, setSearchQuery] = useState("");
const [showPresetDialog, setShowPresetDialog] = useState(false);
const [newPresetName, setNewPresetName] = useState("");
const toggleGroup = useCallback(groupId => {
setExpandedGroups(prev => ({
...prev,
[groupId]: !prev[groupId]
}));
}, []);
const handleFilterChange = useCallback((groupId, value) => {
onChange({
...values,
[groupId]: value
});
}, [values, onChange]);
const handleClearAll = useCallback(() => {
const clearedValues = Object.fromEntries(filters.map(f => [f.id, f.type === "checkbox" ? [] : null]));
onChange(clearedValues);
onClear?.();
}, [filters, onChange, onClear]);
const handleApplyFilters = useCallback(() => {
onApply?.();
}, [onApply]);
const handlePresetSelect = useCallback(preset => {
onChange(preset.filters);
}, [onChange]);
const handleSavePreset = useCallback(() => {
if (newPresetName.trim()) {
onSavePreset?.(newPresetName.trim());
setNewPresetName("");
setShowPresetDialog(false);
}
}, [newPresetName, onSavePreset]);
const filteredGroups = useMemo(() => {
if (!searchQuery) return filters;
return filters.filter(group => group.label.toLowerCase().includes(searchQuery.toLowerCase()) || group.options?.some(option => option.label.toLowerCase().includes(searchQuery.toLowerCase())));
}, [filters, searchQuery]);
const hasActiveFilters = useMemo(() => {
return Object.values(values).some(value => Array.isArray(value) ? (value?.length || 0) > 0 : value != null);
}, [values]);
const sizeClasses = {
sm: "glass-text-sm",
md: "glass-text-base",
lg: "glass-text-lg"
};
const variantClasses = {
default: "glass-p-6",
compact: "glass-p-4",
minimal: "glass-p-2"
};
const elevationClasses = {
low: "glass-glass-backdrop-blur-md bg-white/10 border border-white/20 glass-contrast-guard",
medium: "glass-glass-backdrop-blur-md bg-white/20 border border-white/30 shadow-lg glass-contrast-guard",
high: "glass-glass-backdrop-blur-md bg-white/30 border border-white/40 shadow-2xl glass-contrast-guard"
};
return jsxs("div", {
ref: ref,
className: cn("glass-radius-xl", elevationClasses[elevation], variantClasses[variant], sizeClasses[size], className),
...props,
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-6',
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx(Filter, {
className: 'w-5 h-5 text-primary/70'
}), jsx("h3", {
className: 'font-semibold text-primary',
children: title
}), hasActiveFilters && jsx("span", {
className: "glass-px-2 glass-py-1 glass-text-xs glass-surface-blue/20 glass-text-secondary glass-radius-full",
children: "Active"
})]
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [showClearButton && hasActiveFilters && jsxs(GlassButton, {
variant: "ghost",
size: "sm",
onClick: handleClearAll,
className: 'text-primary/70 hover:text-primary glass-focus glass-touch-target',
children: [jsx(RotateCcw, {
className: 'w-4 h-4 glass-mr-1'
}), "Clear"]
}), showApplyButton && jsx(GlassButton, {
variant: "primary",
size: "sm",
onClick: handleApplyFilters,
disabled: !hasActiveFilters,
className: "glass-focus glass-touch-target",
children: "Apply Filters"
})]
})]
}), showSearch && jsx("div", {
className: 'mb-4',
children: jsx(GlassInput, {
placeholder: "Search filters...",
value: searchQuery,
onChange: e => setSearchQuery(e.target.value),
className: "glass-w-full",
leftIcon: jsx(Search, {
className: 'w-4 h-4'
})
})
}), showPresets && presets.length > 0 && jsxs("div", {
className: 'mb-6',
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-2',
children: [jsx("span", {
className: 'glass-text-sm font-medium text-primary/70',
children: "Presets"
}), onSavePreset && jsxs(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => setShowPresetDialog(true),
className: 'text-primary/70 hover:text-primary glass-focus glass-touch-target',
children: [jsx(Save, {
className: 'w-4 h-4 glass-mr-1'
}), "Save"]
})]
}), jsx("div", {
className: "glass-flex glass-flex-wrap glass-gap-2",
children: presets.map(preset => jsx(GlassButton, {
variant: "outline",
size: "sm",
onClick: e => handlePresetSelect(preset),
className: "glass-text-xs glass-focus glass-touch-target",
children: preset.name
}, preset.id))
})]
}), jsx("div", {
className: "glass-gap-4",
children: jsx(AnimatePresence, {
children: filteredGroups.map(group => jsxs(motion.div, {
initial: {
opacity: 0,
height: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1,
height: "auto"
},
exit: {
opacity: 0,
height: 0
},
className: 'glass-radius-lg overflow-hidden ring-1 ring-white/10',
children: [collapsible && jsxs("button", {
onClick: e => toggleGroup(group.id),
className: 'glass-w-full glass-flex glass-items-center glass-justify-between glass-p-3 text-left hover:glass-surface-subtle/5 transition-colors glass-focus glass-touch-target glass-contrast-guard',
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: 'font-medium text-primary',
children: group.label
}), group.required && jsx("span", {
className: 'glass-text-xs text-primary',
children: "*"
})]
}), expandedGroups[group.id] ? jsx(ChevronDown, {
className: 'w-4 h-4 text-primary/70'
}) : jsx(ChevronRight, {
className: 'w-4 h-4 text-primary/70'
})]
}), jsx(AnimatePresence, {
children: (!collapsible || expandedGroups[group.id]) && jsx(motion.div, {
initial: {
opacity: 0
},
animate: prefersReducedMotion ? {} : {
opacity: 1
},
exit: {
opacity: 0
},
className: "glass-p-3 glass-border-t glass-border-white/10",
children: jsx(FilterGroupContent, {
group: group,
value: values[group.id],
onChange: value => handleFilterChange(group.id, value)
})
})
})]
}, group.id))
})
}), showPresetDialog && jsx("div", {
className: 'fixed inset-0 glass-surface-dark/50 glass-glass-glass-backdrop-blur-md z-50 glass-flex glass-items-center glass-justify-center glass-contrast-guard',
children: jsxs("div", {
className: 'glass-radius-lg glass-p-6 max-w-md glass-w-full glass-mx-4 glass-surface-subtle/5 ring-1 ring-white/10 glass-contrast-guard',
children: [jsx("h3", {
className: 'glass-text-lg font-semibold text-primary mb-4',
children: "Save Filter Preset"
}), jsx(GlassInput, {
placeholder: "Preset name...",
value: newPresetName,
onChange: e => setNewPresetName(e.target.value),
className: 'mb-4'
}), jsxs("div", {
className: "glass-flex glass-gap-2",
children: [jsx(GlassButton, {
variant: "ghost",
onClick: e => setShowPresetDialog(false),
className: "glass-flex-1 glass-focus glass-touch-target",
children: "Cancel"
}), jsx(GlassButton, {
variant: "primary",
onClick: handleSavePreset,
disabled: !newPresetName.trim(),
className: "glass-flex-1 glass-focus glass-touch-target",
children: "Save"
})]
})]
})
})]
});
});
GlassFilterPanel.displayName = "GlassFilterPanel";
const FilterGroupContent = ({
group,
value,
onChange
}) => {
switch (group.type) {
case "checkbox":
return jsx("div", {
className: "glass-gap-2",
children: group.options?.map(option => jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsx(GlassCheckbox, {
id: option.id,
checked: Array.isArray(value) && value.includes(option.value),
onCheckedChange: checked => {
const currentValues = Array.isArray(value) ? value : [];
const newValues = checked ? [...currentValues, option.value] : currentValues.filter(v => v !== option.value);
onChange(newValues);
},
disabled: option.disabled,
label: option.label
}), option.count != null && jsxs("span", {
className: 'glass-text-xs text-primary/50',
children: ["(", option.count, ")"]
})]
}, option.id))
});
case "select":
return jsxs(GlassSelect, {
value: value || "",
onValueChange: onChange,
children: [jsx(GlassSelectTrigger, {
children: jsx(GlassSelectValue, {
placeholder: group.placeholder || "Select..."
})
}), jsx(GlassSelectContent, {
children: group.options?.map(option => jsx(GlassSelectItem, {
value: option.value,
disabled: option.disabled,
children: jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between glass-w-full",
children: [jsx("span", {
children: option.label
}), option.count != null && jsxs("span", {
className: 'glass-text-xs text-primary/50 glass-ml-2',
children: ["(", option.count, ")"]
})]
})
}, option.id))
})]
});
case "slider":
return jsxs("div", {
className: "glass-gap-3",
children: [jsx(GlassSlider, {
min: group.min || 0,
max: group.max || 100,
step: group.step || 1,
value: Array.isArray(value) ? value : [group.min || 0],
onValueChange: newValue => onChange(newValue),
className: "glass-w-full"
}), jsxs("div", {
className: 'glass-flex glass-justify-between glass-text-sm text-primary/70',
children: [jsx("span", {
children: group.min || 0
}), jsx("span", {
children: group.max || 100
})]
})]
});
case "daterange":
return jsx(GlassDateRangePicker, {
value: value,
onChange: onChange,
placeholder: group.placeholder
});
case "search":
return jsx(GlassInput, {
placeholder: group.placeholder || "Search...",
value: value || "",
onChange: e => onChange(e.target.value),
leftIcon: jsx(Search, {
className: 'w-4 h-4'
})
});
default:
return null;
}
};
export { GlassFilterPanel };
//# sourceMappingURL=GlassFilterPanel.js.map