aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
323 lines (320 loc) • 11.8 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import React, { forwardRef, useState, createContext } 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 { useA11yId, announceToScreenReader, createFormFieldA11y } from '../../utils/a11y.js';
import { useMotionPreference } from '../../hooks/useMotionPreference.js';
// Context for radio group state
const RadioGroupContext = /*#__PURE__*/createContext(null);
/**
* GlassRadioGroup component
* A glassmorphism radio button group
*/
const GlassRadioGroup = /*#__PURE__*/forwardRef(({
options,
value: controlledValue,
defaultValue,
onValueChange,
name,
disabled = false,
orientation = "vertical",
size = "md",
variant = "default",
className,
renderOption,
label,
description,
error,
required = false,
respectMotionPreference = true,
"aria-labelledby": ariaLabelledBy,
"aria-describedby": ariaDescribedBy,
"aria-label": ariaLabel
}, ref) => {
useMotionPreference();
// Generate unique IDs for accessibility
const groupId = useA11yId("glass-radio-group");
const labelId = label ? useA11yId("glass-radio-group-label") : undefined;
const descriptionId = description ? useA11yId("glass-radio-group-description") : undefined;
const errorId = error ? useA11yId("glass-radio-group-error") : undefined;
const [internalValue, setInternalValue] = useState(defaultValue || "");
const value = controlledValue !== undefined ? controlledValue : internalValue;
const isInvalid = !!error;
// Create accessibility attributes
const a11yProps = {
role: "radiogroup",
"aria-labelledby": ariaLabelledBy || labelId || undefined,
"aria-label": !ariaLabelledBy && !labelId ? ariaLabel || label : undefined,
"aria-describedby": ariaDescribedBy || descriptionId || description && `${groupId}-description` || undefined,
"aria-invalid": isInvalid || undefined,
"aria-required": required || undefined,
"aria-disabled": disabled || undefined
};
// Announce error state changes
React.useEffect(() => {
if (error) {
announceToScreenReader(`Radio group error: ${error}`, "assertive");
}
}, [error]);
const handleValueChange = newValue => {
if (controlledValue === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
// Announce selection change
const selectedOption = options.find(opt => opt.value === newValue);
if (selectedOption) {
announceToScreenReader(`Selected ${selectedOption.label}`, "polite");
}
};
const contextValue = {
value,
onValueChange: handleValueChange,
name,
disabled,
size,
variant,
respectMotionPreference
};
return jsxs("div", {
"data-glass-component": true,
className: cn("relative", className),
children: [label && jsx("label", {
id: labelId,
className: cn("block glass-text-sm font-medium text-foreground mb-3", required && 'after:content-["*"] after:glass-ml-1 after:text-destructive'),
children: label
}), description && jsx("p", {
id: descriptionId,
className: 'glass-text-sm glass-text-secondary mb-3',
children: description
}), jsx(RadioGroupContext.Provider, {
value: contextValue,
children: jsx("div", {
ref: ref,
...a11yProps,
className: cn("flex glass-gap-3", {
"flex-col": orientation === "vertical",
"flex-row flex-wrap": orientation === "horizontal"
}, error && "ring-2 ring-destructive/50 glass-radius-lg glass-p-2"),
role: "radiogroup",
"aria-invalid": isInvalid || undefined,
"aria-required": required || undefined,
children: options.map(option => jsx(GlassRadioGroupItem, {
option: option,
isSelected: value === option.value,
isDisabled: disabled || option.disabled || false,
onSelect: handleValueChange,
size: size,
variant: variant,
name: name,
renderOption: renderOption,
respectMotionPreference: respectMotionPreference
}, option.value))
})
}), error && jsx("p", {
id: errorId,
className: 'glass-mt-2 glass-text-sm text-destructive',
role: "alert",
"aria-live": "polite",
children: error
})]
});
});
GlassRadioGroup.displayName = "GlassRadioGroup";
const GlassRadioGroupItem = /*#__PURE__*/forwardRef(({
option,
isSelected,
isDisabled,
onSelect,
size = "md",
variant = "default",
name,
className,
renderOption,
respectMotionPreference = true
}, ref) => {
const {
shouldAnimate
} = useMotionPreference();
const itemId = useA11yId("glass-radio-item");
const sizeClasses = {
sm: "w-4 h-4",
md: "w-5 h-5",
lg: "w-6 h-6"
};
const textSizeClasses = {
sm: "glass-text-sm",
md: "glass-text-base",
lg: "glass-text-lg"
};
// Create accessibility attributes for the radio item
const labelElementId = useA11yId("glass-radio-label");
const descriptionElementId = option.description ? useA11yId("glass-radio-description") : undefined;
const usesCustomRenderer = typeof renderOption === "function";
const itemA11yProps = createFormFieldA11y({
id: itemId,
label: usesCustomRenderer ? option.label : undefined,
description: option.description,
disabled: isDisabled,
labelId: usesCustomRenderer ? undefined : labelElementId,
descriptionId: descriptionElementId
});
const handleClick = () => {
if (!isDisabled) {
onSelect(option.value);
}
};
const handleKeyDown = e => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleClick();
}
};
if (renderOption) {
return jsx("div", {
ref: ref,
onClick: handleClick,
onKeyDown: handleKeyDown,
tabIndex: isDisabled ? -1 : 0,
role: "radio",
"aria-checked": isSelected,
"aria-disabled": isDisabled,
...itemA11yProps,
children: renderOption(option, isSelected)
});
}
if (variant === "card") {
return jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? isSelected ? "scaleIn" : "fadeIn" : "none",
animateOnHover: !isDisabled && shouldAnimate && respectMotionPreference,
duration: 200,
children: jsxs(OptimizedGlassCore, {
elevation: isSelected ? "level3" : "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
liftOnHover: true,
press: true,
className: cn("relative cursor-pointer transition-all duration-200 glass-p-4 glass-radius-xl", "focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-primary/40 focus-within:ring-offset-background", {
"opacity-50 cursor-not-allowed": isDisabled
}, className),
ref: ref,
onClick: handleClick,
"data-selected": isSelected ? "true" : "false",
children: [jsx("input", {
id: itemId,
type: "radio",
name: name,
value: option.value,
checked: isSelected,
disabled: isDisabled,
onChange: () => onSelect(option.value),
className: 'sr-only',
...itemA11yProps
}), jsxs("div", {
className: "glass-flex glass-items-start glass-gap-3",
children: [jsx("div", {
className: cn("relative flex items-center justify-center glass-radius-full border-2", "transition-all duration-200 mt-0.5", sizeClasses[size], {
"border-white/40 bg-white/10": !isSelected,
"border-white bg-white/20": isSelected && !isDisabled,
"border-white/20 bg-white/5": isDisabled
}),
children: isSelected && jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "scaleIn" : "none",
duration: 200,
className: 'w-2 h-2 glass-radius-full glass-surface-subtle'
})
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [option.icon && jsx("div", {
className: 'glass-flex glass-items-center glass-justify-center text-primary/70',
children: option.icon
}), jsx("h3", {
id: labelElementId,
className: cn("font-medium glass-text-primary", textSizeClasses[size]),
children: option.label
})]
}), option.description && jsx("p", {
id: descriptionElementId,
className: 'text-primary/60 glass-text-sm glass-mt-1 leading-relaxed',
children: option.description
})]
})]
})]
})
});
}
// Default variant
return jsxs(OptimizedGlassCore, {
ref: ref,
elevation: isSelected ? "level2" : "level1",
intensity: "subtle",
depth: 1,
tint: isSelected ? "primary" : "neutral",
border: "subtle",
animation: shouldAnimate && respectMotionPreference ? "shimmer" : "none",
performanceMode: "medium",
liftOnHover: !isDisabled,
press: true,
className: cn("flex items-center glass-gap-3 cursor-pointer group", "transition-all duration-200 glass-p-3 glass-radius-lg", "focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-primary/40 focus-within:ring-offset-background", {
"opacity-50 cursor-not-allowed": isDisabled
}, className),
onClick: handleClick,
"data-selected": isSelected ? "true" : "false",
children: [jsx("input", {
id: itemId,
type: "radio",
name: name,
value: option.value,
checked: isSelected,
disabled: isDisabled,
onChange: () => onSelect(option.value),
className: 'sr-only',
...itemA11yProps
}), jsx("div", {
className: cn("relative flex items-center justify-center glass-radius-full border-2", "transition-all duration-200", sizeClasses[size], {
"border-white/40 bg-white/10": !isSelected,
"border-white bg-white/20": isSelected && !isDisabled,
"border-white/20 bg-white/5": isDisabled
}),
children: isSelected && jsx(MotionFramer, {
preset: shouldAnimate && respectMotionPreference ? "scaleIn" : "none",
duration: 200,
className: 'w-2 h-2 glass-radius-full glass-surface-subtle'
})
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [option.icon && jsx("div", {
className: 'glass-flex glass-items-center glass-justify-center text-primary/70',
children: option.icon
}), jsx("span", {
id: labelElementId,
className: cn("glass-text-primary/90", textSizeClasses[size], {
"font-medium": isSelected
}),
children: option.label
})]
}), option.description && jsx("p", {
id: descriptionElementId,
className: 'text-primary/60 glass-text-sm glass-mt-1',
children: option.description
})]
})]
});
});
GlassRadioGroupItem.displayName = "GlassRadioGroupItem";
export { GlassRadioGroup, GlassRadioGroupItem };
//# sourceMappingURL=GlassRadioGroup.js.map