port scroller

This commit is contained in:
tresabhi committed 2026-10-13 20:40:47 -05:00
1 parent 1416d64e34
commit 0b740e3f60
2 files changed
+116

No files matched your search

@@ -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));
}
}
}
@@ -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<HTMLDivElement>(null!);
const curtainLeft = useRef<HTMLDivElement>(null!);
const curtainRight = useRef<HTMLDivElement>(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 (
<div className={classNames(styles.scroller, className)} {...props}>
<div className={styles.content} ref={content}>
{children}
</div>
<div ref={curtainLeft} data-side="left" className={styles.curtain} />
<div ref={curtainRight} data-side="right" className={styles.curtain} />
</div>
);
}