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