OneLauncher-Cracked/apps/oneclient/frontend/src/components/SheetPage.tsx
2025-08-18 14:25:59 +02:00

173 lines
4.4 KiB
TypeScript

import type { HTMLMotionProps, MotionValue } from 'motion/react';
import { Button } from '@onelauncher/common/components';
import { useCanGoBack, useRouter } from '@tanstack/react-router';
import { ArrowLeftIcon } from '@untitled-theme/icons-react';
import { motion, useScroll, useTransform } from 'motion/react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import React, { useCallback, useMemo, useRef } from 'react';
import { twMerge } from 'tailwind-merge';
export interface SheetPageProps {
headerSmall: React.ReactNode;
headerLarge: React.ReactNode;
children?: React.ReactNode;
scrollContainerRef?: React.RefObject<HTMLElement | null>;
}
export interface SheetPageContextApi {
scrollContainerRef: React.RefObject<HTMLElement | null>;
}
const SheetPageContext = React.createContext<SheetPageContextApi | null>(null);
export function SheetPage({
headerSmall,
headerLarge,
children,
scrollContainerRef,
}: SheetPageProps) {
const scrollContainer = useRef<HTMLElement>(null);
const headerLargeRef = useRef<HTMLDivElement>(null);
const setScrollContainerRef = useCallback((el: HTMLElement | null) => {
scrollContainer.current = el;
if (scrollContainerRef)
scrollContainerRef.current = el;
}, [scrollContainerRef]);
const { scrollYProgress } = useScroll({
axis: 'y',
container: scrollContainer,
target: headerLargeRef,
layoutEffect: false,
offset: ['end start', 'start start'],
});
const smallTop = useTransform(scrollYProgress, [0, 1], ['0', '100%']);
const smallOpacity = useTransform(scrollYProgress, [0, 1], ['1', '0']);
const largeOpacity = useTransform(scrollYProgress, [0, 1], ['0', '1']);
const context = useMemo<SheetPageContextApi>(() => ({
scrollContainerRef: scrollContainer,
}), [scrollContainer]);
return (
<div className="h-full">
<HeaderBackgrounds scrollYProgress={scrollYProgress} />
<div className="flex flex-col w-full min-h-full h-0">
<div className="sticky top-0 left-0 mx-12 flex flex-row gap-4 h-8 overflow-hidden">
<GoBackButton />
<motion.div
className="relative w-full h-full left-0" style={{
top: smallTop,
opacity: smallOpacity,
}}
>
{headerSmall}
</motion.div>
</div>
<OverlayScrollbarsComponent className="flex flex-col flex-1 mask-t-from-97% pt-4 relative" ref={ref => setScrollContainerRef(ref?.osInstance()?.elements().content ?? null)}>
<div className="flex flex-col mx-12 gap-4 flex-1 h-full min-h-full">
<motion.div className="w-full" ref={headerLargeRef} style={{ opacity: largeOpacity }}>
{headerLarge}
</motion.div>
<SheetPageContext.Provider value={context}>
{children}
</SheetPageContext.Provider>
</div>
</OverlayScrollbarsComponent>
</div>
</div>
);
}
export function useSheetPageContext() {
const ctx = React.useContext(SheetPageContext);
if (!ctx)
throw new Error('useSheetPageContext must be used within a SheetPageContext.Provider');
return ctx;
}
SheetPage.Content = ({
children,
className,
...rest
}: Omit<HTMLMotionProps<'div'>, 'animate' | 'initial'>) => (
<motion.div
animate={{
bottom: 0,
opacity: 1,
}}
className={twMerge('relative flex flex-col px-10 py-6 last:mb-8 h-full bg-page-elevated rounded-2xl', className)}
exit={{
opacity: 0,
}}
initial={{
opacity: 0,
}}
transition={{ duration: 0.25 }}
{...rest}
>
{children}
</motion.div>
);
function GoBackButton() {
const router = useRouter();
const canGoBack = useCanGoBack();
if (!canGoBack)
return undefined;
const onClick = () => {
router.history.back();
};
return (
<Button color="ghost" onPress={onClick}>
<ArrowLeftIcon height={20} />
<span className="h-5">Back</span>
</Button>
);
}
function HeaderBackgrounds({
scrollYProgress,
}: {
scrollYProgress: MotionValue<number>;
}) {
const top = useTransform(scrollYProgress, [1, 0], ['0px', '-60px']);
return (
<motion.div
className="absolute left-0 w-full overflow-hidden"
style={{
top,
bottom: 0,
}}
>
<div
className="absolute -z-10 -top-20 h-50 left-0 w-screen" style={{
background: 'rgba(0, 0, 0, 0.20)',
}}
>
</div>
<div
className="relative h-50 top-0 left-0 w-screen"
style={{
background: 'linear-gradient(180deg, rgba(17, 23, 28, 0.00) 0%, #11171C 60.1%)',
}}
>
</div>
<div className="relative top-0 left-0 w-full h-full bg-page"></div>
</motion.div>
);
}