UNPKG

pulse-dashboard

Version:

A Next.js Dashboard application for real-time monitoring and historical analysis of Playwright test executions, based on playwright-pulse-report. This component provides the UI for visualizing Playwright test results and can be run as a standalone CLI too

178 lines 10.3 kB
"use client"; import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; import { PanelLeft } from "lucide-react"; import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Separator } from "@/components/ui/separator"; import { Sheet, SheetContent, SheetOverlay } from "@/components/ui/sheet"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; const SIDEBAR_COOKIE_NAME = "sidebar_state"; const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; const SIDEBAR_WIDTH = "320px"; const SIDEBAR_WIDTH_MOBILE = "320px"; const SIDEBAR_WIDTH_ICON = "3.5rem"; // 56px const SIDEBAR_KEYBOARD_SHORTCUT = "b"; const SidebarContext = React.createContext(null); function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider."); } return context; } const SidebarProvider = React.forwardRef(({ defaultOpen = true, open: openProp, onOpenChange: setOpenProp, className, style, children, ...props }, ref) => { const isMobile = useIsMobile(); const [openMobile, setOpenMobile] = React.useState(false); const [_open, _setOpen] = React.useState(defaultOpen); const open = openProp ?? _open; const setOpen = React.useCallback((value) => { const openState = typeof value === "function" ? value(open) : value; if (setOpenProp) { setOpenProp(openState); } else { _setOpen(openState); } document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; }, [setOpenProp, open]); const toggleSidebar = React.useCallback(() => { return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); }, [isMobile, setOpen, setOpenMobile]); React.useEffect(() => { const handleKeyDown = (event) => { if (event.key === SIDEBAR_KEYBOARD_SHORTCUT && (event.metaKey || event.ctrlKey)) { event.preventDefault(); toggleSidebar(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [toggleSidebar]); const state = open ? "expanded" : "collapsed"; const contextValue = React.useMemo(() => ({ state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, }), [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]); return (<SidebarContext.Provider value={contextValue}> <TooltipProvider delayDuration={0}> <div style={{ "--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width-icon": SIDEBAR_WIDTH_ICON, ...style, }} className={cn("group/sidebar-wrapper flex min-h-svh w-full", className)} ref={ref} {...props}> {children} </div> </TooltipProvider> </SidebarContext.Provider>); }); SidebarProvider.displayName = "SidebarProvider"; const Sidebar = React.forwardRef(({ side = "left", collapsible = "icon", className, children, ...props }, ref) => { const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); if (isMobile) { return (<Sheet open={openMobile} onOpenChange={setOpenMobile}> <SheetOverlay className="bg-black/60 backdrop-blur-sm"/> <SheetContent data-sidebar="sidebar" data-mobile="true" className={cn("w-[var(--sidebar-width)] p-0 text-[--color-text-primary] border-r-0", "bg-[--glass-bg] backdrop-blur-[20px] saturate-180 border-r border-[--glass-border]")} style={{ "--sidebar-width": SIDEBAR_WIDTH_MOBILE }} side={side}> <div className="flex h-full w-full flex-col">{children}</div> </SheetContent> </Sheet>); } return (<aside ref={ref} data-sidebar="sidebar" data-state={state} data-collapsible={collapsible} data-side={side} className={cn("group hidden h-svh h-[100dvh] flex-shrink-0 flex-col md:flex sticky top-0", "bg-[--glass-bg] backdrop-blur-[20px] saturate-180", "border-[--glass-border]", side === "left" ? "border-r" : "border-l", "transition-all duration-300 ease-in-out", state === "collapsed" ? "w-[var(--sidebar-width-icon)]" : "w-[var(--sidebar-width)]", className)} {...props}> <div className="flex h-full w-full flex-col">{children}</div> </aside>); }); Sidebar.displayName = "Sidebar"; const SidebarTrigger = React.forwardRef(({ className, onClick, ...props }, ref) => { const { toggleSidebar } = useSidebar(); return (<Button ref={ref} data-sidebar="trigger" variant="ghost" size="icon" className={cn("h-7 w-7 md:hidden", className)} // Show only on mobile onClick={(event) => { onClick?.(event); toggleSidebar(); }} {...props}> <PanelLeft /> <span className="sr-only">Toggle Sidebar</span> </Button>); }); SidebarTrigger.displayName = "SidebarTrigger"; const SidebarInset = React.forwardRef(({ className, ...props }, ref) => { return (<main ref={ref} className={cn("relative flex min-h-svh flex-1 flex-col bg-background", className)} {...props}/>); }); SidebarInset.displayName = "SidebarInset"; const SidebarHeader = React.forwardRef(({ className, ...props }, ref) => { return (<div ref={ref} data-sidebar="header" className={cn("flex flex-col gap-4 p-6", className)} {...props}/>); }); SidebarHeader.displayName = "SidebarHeader"; const SidebarContent = React.forwardRef(({ className, ...props }, ref) => { return (<div ref={ref} data-sidebar="content" className={cn("flex min-h-0 flex-1 flex-col gap-2 overflow-auto p-6", "group-data-[collapsible=icon][data-state=collapsed]:p-2 group-data-[collapsible=icon][data-state=collapsed]:overflow-hidden", className)} {...props}/>); }); SidebarContent.displayName = "SidebarContent"; const SidebarMenu = React.forwardRef(({ className, ...props }, ref) => (<ul ref={ref} data-sidebar="menu" className={cn("flex w-full min-w-0 flex-col gap-1", className)} {...props}/>)); SidebarMenu.displayName = "SidebarMenu"; const SidebarMenuItem = React.forwardRef(({ className, ...props }, ref) => (<li ref={ref} data-sidebar="menu-item" className={cn("group/menu-item relative", className)} {...props}/>)); SidebarMenuItem.displayName = "SidebarMenuItem"; const sidebarMenuButtonVariants = cva("peer/menu-button relative flex w-full items-center gap-4 overflow-hidden rounded-xl p-4 text-left text-[0.95rem] font-medium outline-none transition-all duration-300 ease-in-out disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { default: "text-[--color-text-secondary] hover:text-[--color-primary] hover:bg-[--color-hover] hover:translate-x-1 hover:shadow-md data-[active=true]:bg-primary/10 data-[active=true]:text-primary data-[active=true]:font-semibold data-[active=true]:shadow-lg", }, }, defaultVariants: { variant: "default", }, }); const SidebarMenuButton = React.forwardRef(({ asChild = false, isActive = false, variant = "default", tooltip, className, children, ...props }, ref) => { const Comp = asChild ? Slot : "button"; const { isMobile, state } = useSidebar(); const button = (<Comp ref={ref} data-sidebar="menu-button" data-active={isActive} className={cn(sidebarMenuButtonVariants({ variant }), // Styles for collapsed state - No more !important "group-data-[collapsible=icon][data-state=collapsed]:size-12 group-data-[collapsible=icon][data-state=collapsed]:justify-center group-data-[collapsible=icon][data-state=collapsed]:p-0", "group-data-[collapsible=icon][data-state=collapsed]:[&>span:last-child]:hidden", className)} {...props}> <div className={cn("absolute top-0 left-[-100%] w-full h-full bg-gradient-to-r from-primary/10 to-transparent opacity-50 transition-all duration-400 ease-in-out", "peer-hover/menu-button:left-0 peer-data-[active=true]/menu-button:left-0")}/> {children} </Comp>); if (!tooltip) { return button; } if (typeof tooltip === "string") { tooltip = { children: tooltip, }; } return (<Tooltip> <TooltipTrigger asChild>{button}</TooltipTrigger> <TooltipContent side="right" align="center" className={cn(state !== "collapsed" || isMobile ? "hidden" : "block")} {...tooltip}> {tooltip.children} </TooltipContent> </Tooltip>); }); SidebarMenuButton.displayName = "SidebarMenuButton"; const SidebarFooter = React.forwardRef(({ className, ...props }, ref) => { return (<div ref={ref} data-sidebar="footer" className={cn("flex flex-col gap-2 p-6 mt-auto", className)} {...props}/>); }); SidebarFooter.displayName = "SidebarFooter"; const SidebarSeparator = React.forwardRef(({ className, ...props }, ref) => { return (<Separator ref={ref} data-sidebar="separator" className={cn("mx-2 w-auto bg-[--color-border]", "group-data-[state=collapsed]:mx-auto group-data-[state=collapsed]:my-2 group-data-[state=collapsed]:h-auto group-data-[state=collapsed]:w-8/12", className)} {...props}/>); }); SidebarSeparator.displayName = "SidebarSeparator"; const SidebarGroup = React.forwardRef(({ className, ...props }, ref) => { return (<div ref={ref} data-sidebar="group" className={cn("relative flex w-full min-w-0 flex-col", className)} {...props}/>); }); SidebarGroup.displayName = "SidebarGroup"; const SidebarInput = React.forwardRef(({ className, ...props }, ref) => { return (<Input ref={ref} data-sidebar="input" className={cn("h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-[--color-primary]", className)} {...props}/>); }); SidebarInput.displayName = "SidebarInput"; export { Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider, SidebarSeparator, SidebarTrigger, useSidebar, }; //# sourceMappingURL=sidebar.jsx.map