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
JSX
"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