From cf35fac1460d4671ca3cb1f25306aaea9878dcd4 Mon Sep 17 00:00:00 2001 From: Marco Pedone Date: Wed, 7 Jan 2026 16:45:23 +0100 Subject: [PATCH] feat: implement ExpandableScreen component with trigger and content for interactive UI --- .../src/components/expandable_screen.tsx | 228 ++++++++++++++++++ apps/infoalloggi/src/pages/test.tsx | 30 ++- 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 apps/infoalloggi/src/components/expandable_screen.tsx diff --git a/apps/infoalloggi/src/components/expandable_screen.tsx b/apps/infoalloggi/src/components/expandable_screen.tsx new file mode 100644 index 0000000..708d76e --- /dev/null +++ b/apps/infoalloggi/src/components/expandable_screen.tsx @@ -0,0 +1,228 @@ +"use client"; + +import { AnimatePresence, motion } from "framer-motion"; +import { X } from "lucide-react"; +import { + createContext, + type ReactNode, + useContext, + useEffect, + useState, +} from "react"; + +// Context +interface ExpandableScreenContextValue { + isExpanded: boolean; + expand: () => void; + collapse: () => void; + layoutId: string; + triggerRadius: string; + contentRadius: string; + animationDuration: number; +} + +const ExpandableScreenContext = + createContext(null); + +function useExpandableScreen() { + const context = useContext(ExpandableScreenContext); + if (!context) { + throw new Error( + "useExpandableScreen must be used within an ExpandableScreen", + ); + } + return context; +} + +// Root Component +interface ExpandableScreenProps { + children: ReactNode; + defaultExpanded?: boolean; + onExpandChange?: (expanded: boolean) => void; + layoutId?: string; + triggerRadius?: string; + contentRadius?: string; + animationDuration?: number; + lockScroll?: boolean; +} + +/** + * @link https://www.cult-ui.com/docs/components/expandable-screen + * */ +export function ExpandableScreen({ + children, + defaultExpanded = false, + onExpandChange, + layoutId = "expandable-card", + triggerRadius = "100px", + contentRadius = "24px", + animationDuration = 0.3, + lockScroll = true, +}: ExpandableScreenProps) { + const [isExpanded, setIsExpanded] = useState(defaultExpanded); + + const expand = () => { + setIsExpanded(true); + onExpandChange?.(true); + }; + + const collapse = () => { + setIsExpanded(false); + onExpandChange?.(false); + }; + + useEffect(() => { + if (lockScroll) { + if (isExpanded) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = "unset"; + } + } + }, [isExpanded, lockScroll]); + + return ( + + {children} + + ); +} + +// Trigger Component +interface ExpandableScreenTriggerProps { + children: ReactNode; + className?: string; +} + +export function ExpandableScreenTrigger({ + children, + className = "", +}: ExpandableScreenTriggerProps) { + const { isExpanded, expand, layoutId, triggerRadius } = useExpandableScreen(); + + return ( + + {!isExpanded && ( + + {/* Background layer with shared layoutId for morphing */} + + {/* Content layer that fades out on expand */} + + {children} + + + )} + + ); +} + +// Content Component +interface ExpandableScreenContentProps { + children: ReactNode; + className?: string; + showCloseButton?: boolean; + closeButtonClassName?: string; +} + +export function ExpandableScreenContent({ + children, + className = "", + showCloseButton = true, + closeButtonClassName = "", +}: ExpandableScreenContentProps) { + const { isExpanded, collapse, layoutId, contentRadius, animationDuration } = + useExpandableScreen(); + + return ( + + {isExpanded && ( +
+ {/* Morphing background with shared layoutId */} + + + {children} + + + {showCloseButton && ( + + + + )} + +
+ )} +
+ ); +} + +// Background Component (optional) +interface ExpandableScreenBackgroundProps { + trigger?: ReactNode; + content?: ReactNode; + className?: string; +} + +export function ExpandableScreenBackground({ + trigger, + content, + className = "", +}: ExpandableScreenBackgroundProps) { + const { isExpanded } = useExpandableScreen(); + + if (isExpanded && content) { + return
{content}
; + } + + if (!isExpanded && trigger) { + return
{trigger}
; + } + + return null; +} + +export { useExpandableScreen }; diff --git a/apps/infoalloggi/src/pages/test.tsx b/apps/infoalloggi/src/pages/test.tsx index 7e009b8..e670899 100644 --- a/apps/infoalloggi/src/pages/test.tsx +++ b/apps/infoalloggi/src/pages/test.tsx @@ -1,7 +1,35 @@ +import { + ExpandableScreen, + ExpandableScreenContent, + ExpandableScreenTrigger, +} from "~/components/expandable_screen"; +import { Button } from "~/components/ui/button"; import type { NextPageWithLayout } from "./_app"; const Test: NextPageWithLayout = () => { - return
asdsad
; + return ( +
+ +
+ + + +
+ + +
+

+ Full Screen Content +

+
+
+
+
+ ); }; export default Test;