custom scroller for loadout

This commit is contained in:
tresabhi committed 2026-10-13 20:53:15 -05:00
1 parent 0b740e3f60
commit 3131217600
6 files changed
+128 -62

No files matched your search

@@ -1,4 +1,6 @@
.scroller {
--scroller-curtain-color: var(--gray-1);
display: flex;
flex-direction: column;
gap: var(--space-3);
@@ -25,24 +27,74 @@
}
}
.curtain {
width: min(25%, var(--space-7));
height: calc(100% - var(--space-3) - 2 * var(--space-1));
&[data-orientation="horizontal"] {
.curtain {
width: min(25%, var(--space-7));
height: calc(100% - var(--space-3) - 2 * var(--space-1));
position: absolute;
top: 0;
position: absolute;
top: 0;
opacity: 0;
pointer-events: none;
&[data-side="start"] {
left: 0;
background: linear-gradient(
90deg,
var(--scroller-curtain-color),
transparent
);
}
&[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));
&[data-side="end"] {
right: 0;
background: linear-gradient(
90deg,
transparent,
var(--scroller-curtain-color)
);
}
}
}
&[data-orientation="vertical"] {
min-height: 0;
.content {
flex: 1 1 auto;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
padding-bottom: 0;
}
.curtain {
width: calc(100% - var(--space-3) - 2 * var(--space-1));
height: min(25%, var(--space-7));
position: absolute;
left: 0;
&[data-side="start"] {
top: 0;
background: linear-gradient(
180deg,
var(--scroller-curtain-color),
transparent
);
}
&[data-side="end"] {
bottom: 0;
background: linear-gradient(
180deg,
transparent,
var(--scroller-curtain-color)
);
}
}
}
.curtain {
opacity: 0;
pointer-events: none;
}
}
@@ -3,66 +3,76 @@ import { clamp } from "three/src/math/MathUtils.js";
import { classNames } from "../../ui/classNames";
import styles from "./index.module.css";
interface ScrollerProps extends ComponentProps<"div"> {}
interface ScrollerProps extends ComponentProps<"div"> {
orientation?: "horizontal" | "vertical";
}
export function Scroller({ children, className, ...props }: ScrollerProps) {
export function Scroller({
children,
className,
orientation = "horizontal",
...props
}: ScrollerProps) {
const content = useRef<HTMLDivElement>(null!);
const curtainLeft = useRef<HTMLDivElement>(null!);
const curtainRight = useRef<HTMLDivElement>(null!);
const curtainStart = useRef<HTMLDivElement>(null!);
const curtainEnd = useRef<HTMLDivElement>(null!);
const isHorizontal = orientation === "horizontal";
useEffect(() => {
function syncCurtains() {
const maxScroll =
content.current.scrollWidth - content.current.clientWidth;
const maxScroll = isHorizontal
? content.current.scrollWidth - content.current.clientWidth
: content.current.scrollHeight - content.current.clientHeight;
const scrollPosition = isHorizontal
? content.current.scrollLeft
: content.current.scrollTop;
if (maxScroll <= 0) {
curtainLeft.current.style.opacity = "0";
curtainRight.current.style.opacity = "0";
curtainStart.current.style.opacity = "0";
curtainEnd.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,
const dimension = isHorizontal
? content.current.clientWidth
: content.current.clientHeight;
const fadeDistance = Math.min(60, dimension * 0.15);
const startOpacity = clamp(scrollPosition / fadeDistance, 0, 1);
const endOpacity = clamp(
(maxScroll - scrollPosition) / fadeDistance,
0,
1,
);
curtainLeft.current.style.opacity = `${leftOpacity}`;
curtainRight.current.style.opacity = `${rightOpacity}`;
}
function handleScroll() {
syncCurtains();
}
function handleResize() {
syncCurtains();
curtainStart.current.style.opacity = `${startOpacity}`;
curtainEnd.current.style.opacity = `${endOpacity}`;
}
syncCurtains();
window.addEventListener("resize", handleResize);
content.current.addEventListener("scroll", handleScroll);
window.addEventListener("resize", syncCurtains);
content.current.addEventListener("scroll", syncCurtains);
return () => {
window.removeEventListener("resize", handleResize);
content.current.removeEventListener("scroll", handleScroll);
window.removeEventListener("resize", syncCurtains);
content.current.removeEventListener("scroll", syncCurtains);
};
}, []);
}, [orientation]);
return (
<div className={classNames(styles.scroller, className)} {...props}>
<div
className={classNames(styles.scroller, className)}
{...props}
data-orientation={orientation}
>
<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 ref={curtainStart} data-side="start" className={styles.curtain} />
<div ref={curtainEnd} data-side="end" className={styles.curtain} />
</div>
);
}
@@ -1,6 +1,4 @@
.section {
max-width: 21rem;
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -80,8 +78,8 @@
}
.consumables {
display: flex;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(3, max-content);
gap: var(--space-3);
.consumable-wrapper {
@@ -40,7 +40,7 @@ interface TankopediaLoadoutProps {
}
export function TankopediaLoadout({ characteristics }: TankopediaLoadoutProps) {
const tank = useDuel("protagonist", DuelModule.Tank);
// const tank = useDuel("protagonist", DuelModule.Tank);
// const showModules = hasUpgrades(tank);
return (
@@ -13,11 +13,14 @@
display: flex;
flex-direction: column;
gap: var(--space-5);
padding: var(--space-5);
overflow-y: auto;
background-color: var(--gray-2);
.content {
padding: var(--space-5);
display: flex;
flex-direction: column;
gap: var(--space-5);
}
}
}
@@ -1,5 +1,6 @@
import { useMemo } from "react";
import { api } from "../../../../../api/dynamic";
import { Scroller } from "../../../../../components/Scroller";
import type { ThicknessRange } from "../../../../../components/StaticArmor";
import { TankopediaCharacteristics } from "../../../../../components/TankopediaCharacteristics";
import { TankopediaGameModeAbilities } from "../../../../../components/TankopediaGameModeAbilities";
@@ -52,9 +53,11 @@ export const Page = withErrorWrapper(
return (
<div className={styles.page}>
<div className={styles.loadout}>
<TankopediaLoadout characteristics={characteristics} />
</div>
<Scroller className={styles.loadout} orientation="vertical">
<div className={styles.content}>
<TankopediaLoadout characteristics={characteristics} />
</div>
</Scroller>
<div className={styles.sandbox}>
{/* {!skeleton && <TankopediaSandbox thicknessRange={thicknessRange} />} */}