aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
288 lines (285 loc) • 12 kB
JavaScript
'use client';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { forwardRef, useState, useRef, useEffect } 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 '../../primitives/motion/MotionFramer.js';
import { useA11yId } from '../../utils/a11y.js';
import { useMotionPreferenceContext } from '../../contexts/MotionPreferenceContext.js';
/**
* GlassScrollArea component
* A glassmorphism scrollable area with custom scrollbars
*/
const GlassScrollArea = /*#__PURE__*/forwardRef(({
// TODO: Integrate ContrastGuard for any section titles, labels, and helper text for WCAG AA compliance
children,
maxHeight = "200px",
maxWidth,
direction = "vertical",
showScrollbars = "auto",
scrollbarSize = "md",
scrollbarPosition = "inside",
scrollbarClassName,
className,
onScroll,
smoothScrolling = false,
respectMotionPreference = true,
"aria-label": ariaLabel = "Scrollable content area",
role = "region",
...props
}, ref) => {
const [scrollTop, setScrollTop] = useState(0);
const [scrollLeft, setScrollLeft] = useState(0);
const [isHovering, setIsHovering] = useState(false);
const [contentHeight, setContentHeight] = useState(0);
const [contentWidth, setContentWidth] = useState(0);
const [containerHeight, setContainerHeight] = useState(0);
const [containerWidth, setContainerWidth] = useState(0);
const containerRef = useRef(null);
const contentRef = useRef(null);
const scrollAreaId = useA11yId();
const {
prefersReducedMotion,
isMotionSafe
} = useMotionPreferenceContext();
const shouldRespectMotion = respectMotionPreference && !prefersReducedMotion && isMotionSafe;
// Update dimensions when content changes
useEffect(() => {
const updateDimensions = () => {
if (containerRef.current && contentRef.current) {
const containerRect = containerRef.current.getBoundingClientRect();
const contentRect = contentRef.current.getBoundingClientRect();
setContainerHeight(containerRect.height);
setContainerWidth(containerRect.width);
setContentHeight(contentRect.height);
setContentWidth(contentRect.width);
}
};
updateDimensions();
const resizeObserver = new ResizeObserver(updateDimensions);
if (containerRef.current) resizeObserver.observe(containerRef.current);
if (contentRef.current) resizeObserver.observe(contentRef.current);
return () => resizeObserver.disconnect();
}, [children]);
const [scrollActive, setScrollActive] = useState(false);
const scrollTimer = useRef(null);
const handleScroll = e => {
const target = e.target;
const newScrollTop = target.scrollTop;
const newScrollLeft = target.scrollLeft;
setScrollTop(newScrollTop);
setScrollLeft(newScrollLeft);
onScroll?.(newScrollTop, newScrollLeft);
setScrollActive(true);
if (scrollTimer.current) clearTimeout(scrollTimer.current);
scrollTimer.current = setTimeout(() => setScrollActive(false), 200);
};
const shouldShowScrollbars = () => {
switch (showScrollbars) {
case "always":
return true;
case "never":
return false;
case "hover":
return isHovering;
case "auto":
default:
return direction === "vertical" && contentHeight > containerHeight || direction === "horizontal" && contentWidth > containerWidth || direction === "both" && (contentHeight > containerHeight || contentWidth > containerWidth);
}
};
const getOverflowStyle = () => {
switch (direction) {
case "vertical":
return {
overflowX: "hidden",
overflowY: "auto"
};
case "horizontal":
return {
overflowX: "auto",
overflowY: "hidden"
};
case "both":
default:
return {
overflowX: "auto",
overflowY: "auto"
};
}
};
const isAtTop = scrollTop <= 0;
const isAtBottom = contentHeight - containerHeight - scrollTop <= 1;
return jsx(OptimizedGlassCore, {
ref: ref,
id: scrollAreaId,
intent: "neutral",
elevation: "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: shouldRespectMotion ? "shimmer" : "none",
performanceMode: "medium",
role: role,
"aria-label": ariaLabel,
className: cn("relative glass-backdrop-blur-md ring-1 ring-white/10 bg-white/5", scrollbarPosition === "outside" && "glass-p-2", smoothScrolling && "scroll-smooth",
// Motion preferences
shouldRespectMotion && "motion-safe:transition-all motion-reduce:transition-none", className),
style: {
maxHeight: typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight,
maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth
},
...props,
children: jsxs("div", {
ref: containerRef,
className: cn("relative overflow-hidden", scrollbarPosition === "inside" && "glass-radius-lg"),
onMouseEnter: () => setIsHovering(true),
onMouseLeave: () => setIsHovering(false),
children: [jsx("div", {
ref: contentRef,
className: 'relative',
style: getOverflowStyle(),
onScroll: handleScroll,
children: children
}), direction !== "horizontal" && jsxs(Fragment, {
children: [!isAtTop && jsx("div", {
className: cn("pointer-events-none absolute top-0 left-0 right-0 h-6 bg-gradient-to-b to-transparent", scrollActive ? "from-blue-400/20" : "from-black/20")
}), !isAtBottom && jsx("div", {
className: cn("pointer-events-none absolute bottom-0 left-0 right-0 h-6 bg-gradient-to-t to-transparent", scrollActive ? "from-blue-400/20" : "from-black/20")
})]
}), shouldShowScrollbars() && jsxs(Fragment, {
children: [direction !== "horizontal" && contentHeight > containerHeight && jsx(GlassScrollBar, {
orientation: "vertical",
size: scrollbarSize,
className: scrollbarClassName,
showThumb: true
}), direction !== "vertical" && contentWidth > containerWidth && jsx(GlassScrollBar, {
orientation: "horizontal",
size: scrollbarSize,
className: scrollbarClassName,
showThumb: true
})]
})]
})
});
});
GlassScrollArea.displayName = "GlassScrollArea";
/**
* GlassScrollBar component
* Custom scrollbar with glassmorphism styling
*/
const GlassScrollBar = /*#__PURE__*/forwardRef(({
orientation,
size = "md",
className,
showThumb = true,
respectMotionPreference = true,
...props
}, ref) => {
const [isDragging, setIsDragging] = useState(false);
const [thumbPosition, setThumbPosition] = useState(0);
const [thumbSize, setThumbSize] = useState(50);
const internalRef = useRef(null);
const scrollbarRef = ref || internalRef;
// Handle ref access safely for both function and object refs
const getScrollbarElement = () => {
if (typeof scrollbarRef === "function") {
return null; // Function refs don't have current property
}
return scrollbarRef.current;
};
const thumbRef = useRef(null);
const {
prefersReducedMotion,
isMotionSafe
} = useMotionPreferenceContext();
const shouldRespectMotion = respectMotionPreference && !prefersReducedMotion && isMotionSafe;
// Calculate thumb position and size based on scroll
useEffect(() => {
const updateThumb = () => {
const scrollbarElement = getScrollbarElement();
if (!scrollbarElement?.parentElement) return;
const container = scrollbarElement.parentElement;
const content = container.firstElementChild;
if (!content) return;
const containerSize = orientation === "vertical" ? container.clientHeight : container.clientWidth;
const contentSize = orientation === "vertical" ? content.scrollHeight : content.scrollWidth;
const scrollPosition = orientation === "vertical" ? content.scrollTop : content.scrollLeft;
// Calculate thumb size
const newThumbSize = Math.max(30, containerSize / contentSize * containerSize);
setThumbSize(newThumbSize);
// Calculate thumb position
const maxScroll = contentSize - containerSize;
const maxThumbTravel = containerSize - newThumbSize;
const newThumbPosition = maxScroll > 0 ? scrollPosition / maxScroll * maxThumbTravel : 0;
setThumbPosition(newThumbPosition);
};
const container = getScrollbarElement()?.parentElement;
if (container) {
updateThumb();
container.addEventListener("scroll", updateThumb);
return () => container.removeEventListener("scroll", updateThumb);
}
}, [orientation]);
const handleMouseDown = e => {
setIsDragging(true);
e.preventDefault();
};
useEffect(() => {
const handleMouseMove = e => {
const scrollbarElement = getScrollbarElement();
if (!isDragging || !scrollbarElement?.parentElement) return;
const container = scrollbarElement.parentElement;
const content = container.firstElementChild;
const rect = scrollbarElement.getBoundingClientRect();
const clickPosition = orientation === "vertical" ? e.clientY - rect.top : e.clientX - rect.left;
const containerSize = orientation === "vertical" ? container.clientHeight : container.clientWidth;
const contentSize = orientation === "vertical" ? content.scrollHeight : content.scrollWidth;
const scrollRatio = clickPosition / containerSize;
const newScrollPosition = scrollRatio * (contentSize - containerSize);
if (orientation === "vertical") {
content.scrollTop = Math.max(0, Math.min(newScrollPosition, contentSize - containerSize));
} else {
content.scrollLeft = Math.max(0, Math.min(newScrollPosition, contentSize - containerSize));
}
};
const handleMouseUp = () => {
setIsDragging(false);
};
if (isDragging) {
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
return () => {
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, orientation]);
const scrollbarClasses = orientation === "vertical" ? "absolute right-0 top-0 bottom-0 w-6" : "absolute bottom-0 left-0 right-0 h-6";
const thumbClasses = orientation === "vertical" ? "absolute right-1 glass-radius-full cursor-pointer" : "absolute bottom-1 glass-radius-full cursor-pointer";
return jsxs("div", {
ref: scrollbarRef,
className: cn(scrollbarClasses, "pointer-events-auto",
// Motion preferences
shouldRespectMotion && "motion-safe:transition-all motion-reduce:transition-none", className),
onMouseDown: handleMouseDown,
...props,
children: [jsx("div", {
className: cn("absolute bg-white/10 glass-radius-full transition-all duration-200", orientation === "vertical" ? "right-1 top-1 bottom-1 w-1 hover:w-2" : "bottom-1 left-1 right-1 h-1 hover:h-2")
}), showThumb && jsx("div", {
ref: thumbRef,
className: cn(thumbClasses, "glass-backdrop-blur-md bg-white/30 border border-white/20", shouldRespectMotion ? "hover:bg-white/40 transition-all duration-200" : "hover:bg-white/40", isDragging && "bg-white/50 scale-110", size === "sm" && "w-1 h-4", size === "md" && "w-2 h-6", size === "lg" && "w-3 h-8"),
style: {
[orientation === "vertical" ? "top" : "left"]: `${thumbPosition}px`,
[orientation === "vertical" ? "height" : "width"]: `${thumbSize}px`
}
})]
});
});
GlassScrollBar.displayName = "GlassScrollBar";
export { GlassScrollArea, GlassScrollBar };
//# sourceMappingURL=GlassScrollArea.js.map