import type { Breakpoint } from "@seed-design/css/breakpoints"; import * as React from "react"; import { useBreakpointValue } from "../../hooks/useBreakpointValue"; import { type BottomSheet, BottomSheetBackdrop, BottomSheetBody, BottomSheetCloseButton, BottomSheetContent, BottomSheetDescription, BottomSheetFooter, BottomSheetHeader, BottomSheetPositioner, BottomSheetRoot, BottomSheetTitle, BottomSheetTrigger, } from "../BottomSheet"; import { BottomSheetHandle } from "../BottomSheetHandle"; import { type SidePanel, SidePanelBackdrop, SidePanelBody, SidePanelCloseButton, SidePanelContent, SidePanelDescription, SidePanelFooter, SidePanelHeader, SidePanelPositioner, SidePanelRoot, SidePanelTitle, SidePanelTrigger, } from "../SidePanel"; type SharedProps = Pick< SidePanelProps, Extract > & Pick>; interface ResponsiveSidePanelContextValue { shouldUseBottomSheet: boolean | undefined; } const ResponsiveSidePanelContext = React.createContext( null, ); export function useResponsiveSidePanelContext() { const ctx = React.useContext(ResponsiveSidePanelContext); if (!ctx) { throw new Error( "ResponsiveSidePanel sub-components must be used inside ", ); } return ctx; } export interface ResponsiveSidePanelRootProps { children?: React.ReactNode; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; /** * Breakpoint at and above which the panel renders as a SidePanel; below it, a * BottomSheet. Cannot be `"base"`, which would always be a SidePanel. * @default "md" */ sidePanelBreakpoint?: Exclude; /** Props forwarded to the underlying SidePanel root (at and above the breakpoint). */ sidePanelRootProps?: Omit< SidePanel.RootProps, "children" | "open" | "defaultOpen" | "onOpenChange" >; /** Props forwarded to the underlying BottomSheet root (below the breakpoint). */ bottomSheetRootProps?: Omit< BottomSheet.RootProps, "children" | "open" | "defaultOpen" | "onOpenChange" >; } export const ResponsiveSidePanelRoot = ({ children, open: openProp, defaultOpen = false, onOpenChange, sidePanelBreakpoint = "md", sidePanelRootProps, bottomSheetRootProps, }: ResponsiveSidePanelRootProps) => { const shouldUseBottomSheet = useBreakpointValue({ base: true, [sidePanelBreakpoint]: false }); const isControlled = openProp !== undefined; const [internalOpen, setInternalOpen] = React.useState(defaultOpen); const open = openProp ?? internalOpen; const setOpen = React.useCallback( (nextOpen: boolean) => { if (nextOpen === open) return; if (!isControlled) { setInternalOpen(nextOpen); } onOpenChange?.(nextOpen); }, [isControlled, onOpenChange, open], ); const value = React.useMemo(() => ({ shouldUseBottomSheet }), [shouldUseBottomSheet]); return ( {shouldUseBottomSheet ? ( {children} ) : ( {children} )} ); }; export interface ResponsiveSidePanelTriggerProps extends SharedProps {} export const ResponsiveSidePanelTrigger = React.forwardRef< HTMLButtonElement, ResponsiveSidePanelTriggerProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelTrigger.displayName = "ResponsiveSidePanelTrigger"; export interface ResponsiveSidePanelPositionerProps extends SharedProps {} export const ResponsiveSidePanelPositioner = React.forwardRef< HTMLDivElement, ResponsiveSidePanelPositionerProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelPositioner.displayName = "ResponsiveSidePanelPositioner"; export interface ResponsiveSidePanelBackdropProps extends SharedProps {} export const ResponsiveSidePanelBackdrop = React.forwardRef< HTMLDivElement, ResponsiveSidePanelBackdropProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelBackdrop.displayName = "ResponsiveSidePanelBackdrop"; /** * `width` / `maxWidth` only apply in SidePanel mode (md+); they are ignored when * rendered as a BottomSheet. */ export interface ResponsiveSidePanelContentProps extends SharedProps, Pick {} export const ResponsiveSidePanelContent = React.forwardRef< HTMLDivElement, ResponsiveSidePanelContentProps >(({ width, maxWidth, ...props }, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelContent.displayName = "ResponsiveSidePanelContent"; export interface ResponsiveSidePanelHeaderProps extends SharedProps {} export const ResponsiveSidePanelHeader = React.forwardRef< HTMLDivElement, ResponsiveSidePanelHeaderProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelHeader.displayName = "ResponsiveSidePanelHeader"; export interface ResponsiveSidePanelTitleProps extends SharedProps {} export const ResponsiveSidePanelTitle = React.forwardRef< HTMLHeadingElement, ResponsiveSidePanelTitleProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelTitle.displayName = "ResponsiveSidePanelTitle"; export interface ResponsiveSidePanelDescriptionProps extends SharedProps {} export const ResponsiveSidePanelDescription = React.forwardRef< HTMLParagraphElement, ResponsiveSidePanelDescriptionProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelDescription.displayName = "ResponsiveSidePanelDescription"; export interface ResponsiveSidePanelBodyProps extends SharedProps {} export const ResponsiveSidePanelBody = React.forwardRef< HTMLDivElement, ResponsiveSidePanelBodyProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelBody.displayName = "ResponsiveSidePanelBody"; export interface ResponsiveSidePanelFooterProps extends SharedProps {} export const ResponsiveSidePanelFooter = React.forwardRef< HTMLDivElement, ResponsiveSidePanelFooterProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelFooter.displayName = "ResponsiveSidePanelFooter"; export interface ResponsiveSidePanelCloseButtonProps extends SharedProps {} export const ResponsiveSidePanelCloseButton = React.forwardRef< HTMLButtonElement, ResponsiveSidePanelCloseButtonProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); return shouldUseBottomSheet ? ( ) : ( ); }); ResponsiveSidePanelCloseButton.displayName = "ResponsiveSidePanelCloseButton"; export interface ResponsiveSidePanelHandleProps extends BottomSheet.HandleProps {} export const ResponsiveSidePanelHandle = React.forwardRef< HTMLDivElement, ResponsiveSidePanelHandleProps >((props, ref) => { const { shouldUseBottomSheet } = useResponsiveSidePanelContext(); if (!shouldUseBottomSheet) return null; return ; }); ResponsiveSidePanelHandle.displayName = "ResponsiveSidePanelHandle";