diff --git a/packages/website/src/components/Scroller/index.module.css b/packages/website/src/components/Scroller/index.module.css new file mode 100644 index 000000000..5b5241677 --- /dev/null +++ b/packages/website/src/components/Scroller/index.module.css @@ -0,0 +1,48 @@ +.scroller { + display: flex; + flex-direction: column; + gap: var(--space-3); + + position: relative; + + .content { + overflow: auto; + + padding-bottom: var(--space-4); + + transition: scrollbar-color var(--duration-brief); + scrollbar-color: transparent transparent; + + &:hover { + scrollbar-color: var(--gray-7) var(--gray-3); + } + } + + &:hover, + &:focus-within { + .bar-wrapper { + opacity: 1; + } + } + + .curtain { + width: min(25%, var(--space-7)); + height: calc(100% - var(--space-3) - 2 * var(--space-1)); + + position: absolute; + top: 0; + + opacity: 0; + pointer-events: none; + + &[data-side="left"] { + left: 0; + background: linear-gradient(90deg, var(--gray-1), transparent); + } + + &[data-side="right"] { + right: 0; + background: linear-gradient(90deg, transparent, var(--gray-1)); + } + } +} diff --git a/packages/website/src/components/Scroller/index.tsx b/packages/website/src/components/Scroller/index.tsx new file mode 100644 index 000000000..fdab6bebc --- /dev/null +++ b/packages/website/src/components/Scroller/index.tsx @@ -0,0 +1,68 @@ +import { useEffect, useRef, type ComponentProps } from "react"; +import { clamp } from "three/src/math/MathUtils.js"; +import { classNames } from "../../ui/classNames"; +import styles from "./index.module.css"; + +interface ScrollerProps extends ComponentProps<"div"> {} + +export function Scroller({ children, className, ...props }: ScrollerProps) { + const content = useRef(null!); + const curtainLeft = useRef(null!); + const curtainRight = useRef(null!); + + useEffect(() => { + function syncCurtains() { + const maxScroll = + content.current.scrollWidth - content.current.clientWidth; + if (maxScroll <= 0) { + curtainLeft.current.style.opacity = "0"; + curtainRight.current.style.opacity = "0"; + return; + } + + const fadeDistance = Math.min(60, content.current.clientWidth * 0.15); + const leftOpacity = clamp( + content.current.scrollLeft / fadeDistance, + 0, + 1, + ); + const rightOpacity = clamp( + (maxScroll - content.current.scrollLeft) / fadeDistance, + 0, + 1, + ); + + curtainLeft.current.style.opacity = `${leftOpacity}`; + curtainRight.current.style.opacity = `${rightOpacity}`; + } + + function handleScroll() { + syncCurtains(); + } + + function handleResize() { + syncCurtains(); + } + + syncCurtains(); + + window.addEventListener("resize", handleResize); + content.current.addEventListener("scroll", handleScroll); + + return () => { + window.removeEventListener("resize", handleResize); + content.current.removeEventListener("scroll", handleScroll); + }; + }, []); + + return ( +
+
+ {children} +
+ +
+
+
+ ); +}