// ~/src/siteplane/components/sheet/sheet.tsx "use client"; import { Dialog as SheetPrimitive } from "@base-ui/react/dialog"; import { mergeProps } from "@base-ui/react/merge-props"; import { useRender } from "@base-ui/react/use-render"; import { XIcon } from "lucide-react"; import type React from "react"; import { SiteplaneUIPortalScope } from "../../system/provider/siteplane-ui-provider"; import { surfaceClassNames } from "../../system/styles/surface"; import { cn, type WithStringClassName } from "../../system/utils/cn"; import { slotClass } from "../../system/utils/slot-class"; import { KitButton } from "../button/button-internal"; import { ScrollArea } from "../scroll-area"; import type { KitSheetPopupProps } from "./sheet-internal"; import "./sheet.css"; export const Sheet: typeof SheetPrimitive.Root = SheetPrimitive.Root; // Base UI's own Portal type, with className narrowed to a string. export const SheetPortal: React.ForwardRefExoticComponent< WithStringClassName> & React.RefAttributes > = SheetPrimitive.Portal; export function SheetTrigger( props: WithStringClassName, ): React.ReactElement { return ( ); } export function SheetClose( props: WithStringClassName, ): React.ReactElement { return ( ); } export function SheetBackdrop({ className, ...props }: WithStringClassName): React.ReactElement { return ( ); } export function SheetViewport({ className, side, variant = "default", ...props }: WithStringClassName & { side?: "right" | "left" | "top" | "bottom"; variant?: "default" | "inset" | "fullscreen"; }): React.ReactElement { // A fullscreen viewport takes its display and padding from sheet.css. const fullscreen = variant === "fullscreen"; return ( ); } type SheetPopupProps = WithStringClassName & { showCloseButton?: boolean; side?: "right" | "left" | "top" | "bottom"; variant?: "default" | "inset" | "fullscreen"; closeProps?: WithStringClassName; portalProps?: WithStringClassName; }; export function SheetPopup(popupProps: SheetPopupProps): React.ReactElement { // Sidebar passes the internal sidebar profile through KitSheetPopup // (sheet-internal.ts), the same function. const { className, children, showCloseButton = true, side = "right", variant = "default", profile = "default", closeProps, portalProps, ...props } = popupProps as KitSheetPopupProps; return ( {children} {showCloseButton && ( // The corner close of every overlay: the 24 px overlay-dismiss // Button 8 px inside the top-end corner. The header keeps its // text clear of it (design-rework.css). } {...closeProps} > )} ); } export function SheetHeader({ className, render, ...props }: useRender.ComponentProps<"div">): React.ReactElement { const defaultProps = { className: cn( slotClass("sheet-header", props, render), "flex flex-col", className, ), "data-slot": "sheet-header", }; return useRender({ defaultTagName: "div", props: mergeProps<"div">(defaultProps, props), render, }); } export function SheetFooter({ className, render, ...props }: useRender.ComponentProps<"div">): React.ReactElement { const defaultProps = { className: cn( // Actions that do not fit wrap onto another row with the same gap instead // of overflowing a narrow panel. slotClass("sheet-footer", props, render), "flex flex-wrap", className, ), "data-slot": "sheet-footer", }; return useRender({ defaultTagName: "div", props: mergeProps<"div">(defaultProps, props), render, }); } export function SheetTitle({ className, ...props }: WithStringClassName): React.ReactElement { return ( ); } export function SheetDescription({ className, ...props }: WithStringClassName): React.ReactElement { return ( ); } export function SheetPanel({ className, scrollFade = true, render, ...props }: useRender.ComponentProps<"div"> & { scrollFade?: boolean; }): React.ReactElement { const defaultProps = { className: cn(slotClass("sheet-panel", props, render), className), "data-slot": "sheet-panel", }; return ( {useRender({ defaultTagName: "div", props: mergeProps<"div">(defaultProps, props), render, })} ); } // SheetOverlay and SheetContent are the shadcn names of SheetBackdrop and // SheetPopup. export { SheetPrimitive, SheetBackdrop as SheetOverlay, SheetPopup as SheetContent, }; // ~/src/siteplane/components/sheet/sheet-internal.ts import type * as React from "react"; import { SheetPopup } from "./sheet"; /* * Kit-internal SheetPopup. Not public API: the sheet index does not export * this module, and its names and profiles may change in any release. Sidebar * renders its mobile sheet with the sidebar profile. */ export type SheetPopupProfile = "default" | "sidebar"; export type KitSheetPopupProps = React.ComponentProps & { profile?: SheetPopupProfile; }; /** SheetPopup with the kit's composition profile; the same component. */ export const KitSheetPopup = SheetPopup as ( props: KitSheetPopupProps, ) => React.ReactElement; // ~/src/siteplane/components/sheet/sheet.css @layer theme, base, components, siteplane, utilities; @layer siteplane { .sp-sheet-backdrop-motion { transition-property: opacity; transition-duration: var(--motion-duration-panel); transition-timing-function: var(--motion-ease-panel); will-change: opacity; } .sp-sheet-backdrop-motion[data-starting-style], .sp-sheet-backdrop-motion[data-ending-style] { opacity: 0; } .sp-sheet-panel-motion { transition-property: transform; transition-duration: var(--motion-duration-panel); transition-timing-function: var(--motion-ease-panel); will-change: transform; } .sp-sheet-panel-motion[data-side="right"][data-starting-style], .sp-sheet-panel-motion[data-side="right"][data-ending-style] { transform: translateX(calc(100% + 1rem)); } .sp-sheet-panel-motion[data-side="left"][data-starting-style], .sp-sheet-panel-motion[data-side="left"][data-ending-style] { transform: translateX(calc(-100% - 1rem)); } /* The viewport places a side sheet at the inline start or end * (justify-start, justify-end), so in a right-to-left page a left sheet * sits on the right, as the Sidebar's mobile sheet does. It slides in from * the edge it sits at. The dir attribute is named beside :dir(rtl), which a * build for older browsers turns into a list of right-to-left languages. */ .sp-sheet-panel-motion[data-side="right"][data-starting-style]:is( :dir(rtl), [dir="rtl"], [dir="rtl"] * ), .sp-sheet-panel-motion[data-side="right"][data-ending-style]:is( :dir(rtl), [dir="rtl"], [dir="rtl"] * ) { transform: translateX(calc(-100% - 1rem)); } .sp-sheet-panel-motion[data-side="left"][data-starting-style]:is( :dir(rtl), [dir="rtl"], [dir="rtl"] * ), .sp-sheet-panel-motion[data-side="left"][data-ending-style]:is( :dir(rtl), [dir="rtl"], [dir="rtl"] * ) { transform: translateX(calc(100% + 1rem)); } .sp-sheet-panel-motion[data-side="bottom"][data-starting-style], .sp-sheet-panel-motion[data-side="bottom"][data-ending-style] { transform: translateY(calc(100% + 1rem)); } .sp-sheet-panel-motion[data-side="top"][data-starting-style], .sp-sheet-panel-motion[data-side="top"][data-ending-style] { transform: translateY(calc(-100% - 1rem)); } /* Fullscreen navigation is a Sheet surface, including its nested subpages. */ :root .sp-sheet-viewport[data-variant="fullscreen"] { display: flex; padding: 0; } :root .sp-sheet-panel-motion[data-variant="fullscreen"] { width: 100%; max-width: none; height: 100%; border: 0; border-radius: 0; background: var(--sp-rw-bg-base); box-shadow: none; transition-duration: var(--motion-duration-slow); transition-timing-function: var(--motion-ease-standard); } :root .sp-sheet-panel-motion[data-variant="fullscreen"][data-side="top"] { transition-property: opacity, transform; } :root .sp-sheet-panel-motion[data-variant="fullscreen"][data-side="top"]:is( [data-starting-style], [data-ending-style] ) { opacity: 0; transform: translateY(calc(-1 * var(--sp-rw-space-4))); } html[data-motion-reduce="true"] .sp-sheet-panel-motion[data-starting-style], html[data-motion-reduce="true"] .sp-sheet-panel-motion[data-ending-style], :where([data-sp-motion="reduced"]) .sp-sheet-panel-motion[data-starting-style], :where([data-sp-motion="reduced"]) .sp-sheet-panel-motion[data-ending-style] { transform: none !important; } html[data-motion-reduce="true"] .sp-sheet-backdrop-motion, html[data-motion-reduce="true"] .sp-sheet-panel-motion, :where([data-sp-motion="reduced"]) .sp-sheet-backdrop-motion, :where([data-sp-motion="reduced"]) .sp-sheet-panel-motion { transition-duration: 1ms !important; } @media (prefers-reduced-motion: reduce) { .sp-sheet-panel-motion[data-starting-style], .sp-sheet-panel-motion[data-ending-style] { transform: none !important; } .sp-sheet-backdrop-motion, .sp-sheet-panel-motion { transition-duration: 1ms !important; } } } // ~/src/siteplane/components/sheet/index.ts export * from "./sheet";