aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
363 lines (360 loc) • 13.6 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import React, { forwardRef, useCallback, useState, createContext, useContext } 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 { cn } from '../../lib/utilsComprehensive.js';
import { useA11yId, announceToScreenReader, createFormFieldA11y } from '../../utils/a11y.js';
import { useMotionPreference } from '../../hooks/useMotionPreference.js';
import { useGlassSound } from '../../utils/soundDesign.js';
const CheckboxGroupContext = /*#__PURE__*/createContext(null);
const useCheckboxGroup = () => {
const context = useContext(CheckboxGroupContext);
if (!context) {
throw new Error("CheckboxItem must be used within CheckboxGroup");
}
return context;
};
const GlassCheckboxItem = /*#__PURE__*/forwardRef(({
value,
checked,
disabled: itemDisabled,
indeterminate = false,
children,
className,
onChange,
onClick,
onKeyDown,
...props
}, ref) => {
const context = useCheckboxGroup();
const {
shouldAnimate
} = useMotionPreference();
const {
play
} = useGlassSound();
// Use context if available, otherwise standalone
const isSelected = context ? context.selectedValues.includes(value) : checked || false;
const isDisabled = context ? context.disabled || itemDisabled : itemDisabled;
const size = context?.size || "md";
const variant = context?.variant || "default";
const respectMotionPreference = context?.respectMotionPreference ?? true;
const itemId = useA11yId("glass-checkbox-item");
const sizeConfig = {
sm: {
checkbox: "w-4 h-4",
check: "w-2 h-2",
text: "glass-text-xs",
gap: "glass-gap-2"
},
md: {
checkbox: "w-5 h-5",
check: "w-2.5 h-2.5",
text: "glass-text-sm",
gap: "glass-gap-3"
},
lg: {
checkbox: "w-6 h-6",
check: "w-3 h-3",
text: "glass-text-base",
gap: "glass-gap-4"
}
};
const variantConfig = {
default: {
checkbox: isSelected || indeterminate ? "bg-primary/20 border-primary/40" : "bg-background/50 border-border/20",
check: "bg-primary",
focus: "focus:ring-primary/50"
},
success: {
checkbox: isSelected || indeterminate ? "glass-surface-success/20 glass-border-success/40" : "bg-background/50 border-border/20",
check: "glass-surface-success",
focus: "focus:ring-success/50"
},
warning: {
checkbox: isSelected || indeterminate ? "glass-surface-warning/20 glass-border-warning/40" : "bg-background/50 border-border/20",
check: "glass-surface-warning",
focus: "focus:ring-warning/50"
},
error: {
checkbox: isSelected || indeterminate ? "glass-surface-danger/20 glass-border-danger/40" : "bg-background/50 border-border/20",
check: "glass-surface-danger",
focus: "focus:ring-danger/50"
},
info: {
checkbox: isSelected || indeterminate ? "bg-blue-500/20 border-blue-400/40" : "bg-background/50 border-border/20",
check: "bg-blue-500",
focus: "focus:ring-blue-500/50"
}
};
const config = sizeConfig[size];
const colors = variantConfig[variant];
const handleClick = useCallback(e => {
if (isDisabled) return;
const newChecked = !isSelected;
if (context) {
context.onChange?.(value, newChecked);
} else {
onChange?.(newChecked);
}
onClick?.(e);
play(newChecked ? "check" : "uncheck");
// Announce selection for screen readers
const childText = typeof children === "string" ? children : value;
announceToScreenReader(`${childText} ${newChecked ? "checked" : "unchecked"}`, "polite");
}, [isDisabled, isSelected, context, value, onChange, onClick, play, children]);
const handleKeyDown = useCallback(e => {
if (e.key === " " || e.key === "Enter") {
e.preventDefault();
handleClick(e);
}
onKeyDown?.(e);
}, [handleClick, onKeyDown]);
return jsxs(OptimizedGlassCore, {
"data-glass-component": true,
ref: ref,
elevation: "level1",
intensity: "subtle",
depth: 0.5,
tint: "neutral",
border: "subtle",
animation: shouldAnimate && respectMotionPreference ? "float" : "none",
performanceMode: "high",
liftOnHover: !isDisabled,
className: cn("glass-checkbox-item flex items-center cursor-pointer", "glass-p-3 glass-radius-lg border glass-backdrop-blur-md transition-all duration-200", "focus:outline-none glass-focus", config.gap, isDisabled && "opacity-50 cursor-not-allowed", (isSelected || indeterminate) && "ring-2 ring-current/20", className),
onClick: handleClick,
onKeyDown: handleKeyDown,
tabIndex: isDisabled ? -1 : 0,
role: "checkbox",
"aria-checked": indeterminate ? "mixed" : isSelected,
"aria-disabled": isDisabled,
id: itemId,
...props,
children: [jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "scaleIn" : "none",
children: jsxs(OptimizedGlassCore, {
elevation: "level2",
intensity: "medium",
depth: 1,
tint: "neutral",
border: "subtle",
className: cn("glass-checkbox-square relative glass-radius-md border glass-backdrop-blur-md", "flex items-center justify-center transition-all duration-200", config.checkbox, colors.checkbox, colors.focus),
children: [(isSelected || indeterminate) && jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "scaleIn" : "none",
className: cn("glass-checkbox-indicator flex items-center justify-center glass-text-primary", config.check),
children: indeterminate ?
// Indeterminate indicator (horizontal line)
jsx("div", {
className: 'w-2 h-0-5 bg-transparent glass-radius-full'
}) :
// Check mark
jsx("svg", {
className: "glass-w-full glass-h-full",
fill: "none",
stroke: "currentColor",
viewBox: "0 0 24 24",
strokeWidth: 3,
children: jsx("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
d: "M5 13l4 4L19 7"
})
})
}), jsx("div", {
className: 'absolute inset-0 glass-radius-md glass-gradient-primary glass-gradient-primary via-transparent glass-gradient-primary'
})]
})
}), jsx("div", {
className: "glass-flex-1 glass-min-w-0",
children: children
}), jsx("input", {
type: "checkbox",
name: context?.name,
value: value,
checked: isSelected,
onChange: () => {},
disabled: isDisabled,
className: 'sr-only',
tabIndex: -1
})]
});
});
GlassCheckboxItem.displayName = "GlassCheckboxItem";
const GlassCheckboxGroup = /*#__PURE__*/forwardRef(({
value,
defaultValue = [],
onChange,
options,
disabled = false,
size = "md",
variant = "default",
orientation = "vertical",
label,
description,
error,
required = false,
minSelections,
maxSelections,
renderItem,
respectMotionPreference = true,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledBy,
"aria-describedby": ariaDescribedBy,
className,
id,
...props
}, ref) => {
useMotionPreference();
// Generate unique IDs for accessibility
const checkboxGroupId = useA11yId("glass-checkbox-group");
const finalId = id || checkboxGroupId;
const labelId = label ? useA11yId("glass-checkbox-group-label") : undefined;
const descriptionId = description ? useA11yId("glass-checkbox-group-description") : undefined;
const errorId = error ? useA11yId("glass-checkbox-group-error") : undefined;
const name = useA11yId("glass-checkbox-group-name");
const [internalValue, setInternalValue] = useState(value ?? defaultValue);
const currentValue = value !== undefined ? value : internalValue;
const isInvalid = !!error || minSelections !== undefined && currentValue.length < minSelections || maxSelections !== undefined && currentValue.length > maxSelections;
// Create accessibility attributes
const a11yProps = createFormFieldA11y({
id: finalId,
label: !ariaLabelledBy && !labelId ? ariaLabel || label : undefined,
description: description,
error: error,
required: required,
invalid: isInvalid,
disabled: disabled,
labelId: ariaLabelledBy || labelId,
descriptionId: ariaDescribedBy || descriptionId,
errorId: errorId
});
// Announce error state changes
React.useEffect(() => {
if (error) {
announceToScreenReader(`Checkbox group error: ${error}`, "assertive");
}
}, [error]);
const handleChange = useCallback((itemValue, checked) => {
let newValue;
if (checked) {
if (maxSelections && currentValue.length >= maxSelections) {
// Don't allow more selections than maximum
return;
}
newValue = [...currentValue, itemValue];
} else {
if (minSelections && currentValue.length <= minSelections) {
// Don't allow fewer selections than minimum
return;
}
newValue = currentValue.filter(v => v !== itemValue);
}
if (value === undefined) {
setInternalValue(newValue);
}
onChange?.(newValue);
}, [value, currentValue, onChange, minSelections, maxSelections]);
const contextValue = {
selectedValues: currentValue,
onChange: handleChange,
disabled,
size,
variant,
name,
respectMotionPreference
};
const sizeConfig = {
sm: {
label: "glass-text-xs",
gap: "glass-gap-2"
},
md: {
label: "glass-text-sm",
gap: "glass-gap-3"
},
lg: {
label: "glass-text-base",
gap: "glass-gap-4"
}
};
const config = sizeConfig[size];
// Get validation error message
const getValidationError = () => {
if (minSelections && currentValue.length < minSelections) {
return `Please select at least ${minSelections} option${minSelections > 1 ? "s" : ""}`;
}
if (maxSelections && currentValue.length > maxSelections) {
return `Please select no more than ${maxSelections} option${maxSelections > 1 ? "s" : ""}`;
}
return error;
};
const validationError = getValidationError();
return jsx(CheckboxGroupContext.Provider, {
value: contextValue,
children: jsxs("div", {
className: cn("glass-checkbox-group-container", className),
children: [label && jsx("label", {
id: labelId,
className: cn("glass-checkbox-group-label block font-medium text-foreground mb-3", config.label, required && 'after:content-["*"] after:glass-ml-1 after:text-destructive'),
children: label
}), jsx("div", {
ref: ref,
id: finalId,
role: "group",
className: cn("glass-checkbox-group flex", orientation === "horizontal" ? "flex-row flex-wrap" : "flex-col", config.gap, disabled && "opacity-50", validationError && "ring-2 ring-destructive/50 glass-radius-lg glass-p-2"),
...a11yProps,
"aria-invalid": isInvalid || undefined,
"aria-required": required || undefined,
...props,
children: options.map((option, index) => {
if (renderItem) {
return jsx("div", {
children: renderItem(option, index, currentValue.includes(option.value))
}, option.value);
}
return jsx(GlassCheckboxItem, {
value: option.value,
disabled: option.disabled,
children: jsxs("div", {
className: "glass-flex glass-items-start glass-gap-3",
children: [option.icon && jsx("div", {
className: "glass-flex-shrink-0 glass-mt-0-5",
children: option.icon
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsx("div", {
className: cn("font-medium text-foreground", config.label),
children: option.label
}), option.description && jsx("div", {
className: cn("glass-text-secondary glass-mt-1", size === "sm" ? "glass-text-xs" : "glass-text-sm"),
children: option.description
})]
})]
})
}, option.value);
})
}), (minSelections || maxSelections) && jsxs("div", {
className: cn("glass-mt-2 glass-text-secondary", size === "sm" ? "glass-text-xs" : "glass-text-sm"),
children: ["Selected: ", currentValue.length, maxSelections && ` / ${maxSelections}`, minSelections && !maxSelections && ` (min: ${minSelections})`]
}), description && jsx("p", {
id: descriptionId,
className: cn("glass-checkbox-group-description glass-text-secondary glass-mt-2", size === "sm" ? "glass-text-xs" : "glass-text-sm"),
children: description
}), validationError && jsx("p", {
id: errorId,
className: cn("glass-checkbox-group-error text-destructive glass-mt-2", size === "sm" ? "glass-text-xs" : "glass-text-sm"),
role: "alert",
"aria-live": "polite",
children: validationError
})]
})
});
});
GlassCheckboxGroup.displayName = "GlassCheckboxGroup";
export { GlassCheckboxGroup, GlassCheckboxItem, GlassCheckboxGroup as default };
//# sourceMappingURL=GlassCheckboxGroup.js.map