app sidebar UI polishing

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-03-07 13:52:32 +04:00
parent dbc7711f98
commit a388c8cb96
11 changed files with 261 additions and 99 deletions

View File

@@ -93,7 +93,7 @@ export function NavMain({
if (item.items?.length) {
return item.items.some((subItem) =>
location.pathname.startsWith(subItem.url),
location.pathname.startsWith(subItem.url)
);
}
@@ -102,7 +102,9 @@ export function NavMain({
return (
<SidebarGroup>
<SidebarGroupLabel>Compliance</SidebarGroupLabel>
<SidebarGroupLabel className="text-gray-500 pl-3">
Compliance
</SidebarGroupLabel>
<SidebarMenu>
{items.map((item) => {
const active = isItemActive(item) || item.isActive;
@@ -114,11 +116,12 @@ export function NavMain({
asChild
tooltip={item.title}
data-active={active ? "true" : undefined}
className={active ? "text-primary font-medium" : ""}
>
<Link to={item.url ?? "#"}>
<item.icon />
<span>{item.title}</span>
<item.icon
className={active ? "text-lime-9" : "text-lime-6"}
/>
<span className="font-medium">{item.title}</span>
</Link>
</SidebarMenuButton>
{item.items?.length ? (
@@ -133,7 +136,7 @@ export function NavMain({
<SidebarMenuSub>
{item.items?.map((subItem) => {
const subItemActive = location.pathname.startsWith(
subItem.url,
subItem.url
);
return (
@@ -143,12 +146,14 @@ export function NavMain({
data-active={subItemActive ? "true" : undefined}
className={
subItemActive
? "text-primary font-medium"
: ""
? "text-primary"
: "text-gray-600"
}
>
<Link to={subItem.url}>
<span>{subItem.title}</span>
<span className="font-medium">
{subItem.title}
</span>
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>

View File

@@ -59,12 +59,12 @@ export function NavSecondary({
{item.url !== "#" ? (
<a href={item.url}>
<item.icon />
<span>{item.title}</span>
<span className="font-medium">{item.title}</span>
</a>
) : (
<>
<item.icon />
<span>{item.title}</span>
<span className="font-medium">{item.title}</span>
</>
)}
</SidebarMenuButton>

View File

@@ -74,14 +74,12 @@ export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
isActive
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarFallback className="rounded-lg">
<Avatar className="h-9 w-9">
<AvatarFallback className="bg-slate-400 text-gray-100">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
@@ -89,7 +87,9 @@ export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
<span className="truncate font-semibold">
{currentUser.fullName}
</span>
<span className="truncate text-xs">{currentUser.email}</span>
<span className="truncate text-xs text-sidebar-foreground/70">
{currentUser.email}
</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@@ -102,11 +102,8 @@ export function NavUser({ viewer }: { viewer: NavUser_viewer$key }) {
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarFallback className="rounded-lg">
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
<AvatarFallback className="rounded-lg">
<Avatar className="h-8 w-8">
<AvatarFallback>
{currentUser.fullName.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>

View File

@@ -24,6 +24,7 @@ import {
OrganizationSwitcher_organizations$key,
OrganizationSwitcher_organizations$data,
} from "./__generated__/OrganizationSwitcher_organizations.graphql";
import { cn } from "@/lib/utils";
export const organizationSwitcherFragment = graphql`
fragment OrganizationSwitcher_organizations on User {
@@ -52,9 +53,15 @@ const LogoComponent = ({
className?: string;
}) => {
if (org.logoUrl) {
return <img src={org.logoUrl} alt={org.name} className={className} />;
return (
<img
src={org.logoUrl}
alt={org.name}
className={cn("rounded-md", className)}
/>
);
}
return null;
return org.name.substring(0, 2).toUpperCase();
};
export function OrganizationSwitcher({
@@ -74,7 +81,7 @@ export function OrganizationSwitcher({
useEffect(() => {
if (hasOrganizations) {
const org = data.organizations.edges.find(
(edge) => edge.node.id === organizationId,
(edge) => edge.node.id === organizationId
);
if (org) {
setCurrentOrganization(org.node);
@@ -110,34 +117,30 @@ export function OrganizationSwitcher({
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className={`data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground ${
className={`data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground gap-2.5 ${
!currentOrganization
? "border border-dashed border-gray-400"
: ""
}`}
>
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
<div className="flex aspect-square size-8 items-center justify-center rounded-sm bg-slate-400 text-sidebar-primary-foreground overflow-hidden">
{currentOrganization ? (
<LogoComponent org={currentOrganization} className="size-4" />
<LogoComponent org={currentOrganization} className="size-8" />
) : (
<Building className="size-4 text-gray-400" />
<Building className="size-8 text-gray-400" />
)}
</div>
<div className="grid flex-1 text-left text-sm leading-tight">
<div className="grid text-left leading-tight">
<span
className={`truncate font-semibold ${
!currentOrganization ? "text-gray-500" : ""
className={`truncate font-medium text-lg leading-5 ${
!currentOrganization ? "text-gray-500" : "text-gray-900"
}`}
>
{currentOrganization
? currentOrganization.name
: "Select Organization"}
</span>
<span
className={`truncate text-xs ${
!currentOrganization ? "text-gray-400" : ""
}`}
>
<span className="truncate text-xs text-gray-500 font-medium">
{currentOrganization ? "Free" : "No organization selected"}
</span>
</div>
@@ -166,7 +169,7 @@ export function OrganizationSwitcher({
edge.node.id === organizationId ? "bg-muted" : ""
}`}
>
<div className="flex size-6 items-center justify-center rounded-sm border">
<div className="flex size-6 items-center justify-center rounded-sm">
<LogoComponent
org={edge.node}
className="size-4 shrink-0"

View File

@@ -11,7 +11,7 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
className
)}
{...props}
/>
@@ -38,7 +38,7 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
className
)}
{...props}
/>

View File

@@ -63,7 +63,7 @@ const SidebarProvider = React.forwardRef<
children,
...props
},
ref,
ref
) => {
const isMobile = useIsMobile();
const [openMobile, setOpenMobile] = React.useState(false);
@@ -84,7 +84,7 @@ const SidebarProvider = React.forwardRef<
// This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],
[setOpenProp, open]
);
// Helper to toggle the sidebar.
@@ -124,15 +124,7 @@ const SidebarProvider = React.forwardRef<
setOpenMobile,
toggleSidebar,
}),
[
state,
open,
setOpen,
isMobile,
openMobile,
setOpenMobile,
toggleSidebar,
],
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
);
return (
@@ -148,7 +140,7 @@ const SidebarProvider = React.forwardRef<
}
className={cn(
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
className,
className
)}
ref={ref}
{...props}
@@ -158,7 +150,7 @@ const SidebarProvider = React.forwardRef<
</TooltipProvider>
</SidebarContext.Provider>
);
},
}
);
SidebarProvider.displayName = "SidebarProvider";
@@ -179,7 +171,7 @@ const Sidebar = React.forwardRef<
children,
...props
},
ref,
ref
) => {
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
@@ -188,7 +180,7 @@ const Sidebar = React.forwardRef<
<div
className={cn(
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
className,
className
)}
ref={ref}
{...props}
@@ -235,7 +227,7 @@ const Sidebar = React.forwardRef<
"group-data-[side=right]:rotate-180",
variant === "floating" || variant === "inset"
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)",
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
)}
/>
<div
@@ -246,22 +238,22 @@ const Sidebar = React.forwardRef<
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
// Adjust the padding for floating and inset variants.
variant === "floating" || variant === "inset"
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
? "p-4 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
className,
className
)}
{...props}
>
<div
data-sidebar="sidebar"
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
className="flex gap-2 h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow-sm"
>
{children}
</div>
</div>
</div>
);
},
}
);
Sidebar.displayName = "Sidebar";
@@ -312,7 +304,7 @@ const SidebarRail = React.forwardRef<
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
className,
className
)}
{...props}
/>
@@ -330,7 +322,7 @@ const SidebarInset = React.forwardRef<
className={cn(
"relative flex min-h-svh flex-1 flex-col bg-background",
"peer-data-[variant=inset]:min-h-[calc(100svh-(--spacing(4)))] md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm",
className,
className
)}
{...props}
/>
@@ -348,7 +340,7 @@ const SidebarInput = React.forwardRef<
data-sidebar="input"
className={cn(
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
className,
className
)}
{...props}
/>
@@ -364,7 +356,7 @@ const SidebarHeader = React.forwardRef<
<div
ref={ref}
data-sidebar="header"
className={cn("flex flex-col gap-2 p-2", className)}
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
@@ -379,7 +371,7 @@ const SidebarFooter = React.forwardRef<
<div
ref={ref}
data-sidebar="footer"
className={cn("flex flex-col gap-2 p-2", className)}
className={cn("flex flex-col gap-2", className)}
{...props}
/>
);
@@ -411,7 +403,7 @@ const SidebarContent = React.forwardRef<
data-sidebar="content"
className={cn(
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
className,
className
)}
{...props}
/>
@@ -427,7 +419,7 @@ const SidebarGroup = React.forwardRef<
<div
ref={ref}
data-sidebar="group"
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
className={cn("relative flex w-full min-w-0 flex-col p-0", className)}
{...props}
/>
);
@@ -447,7 +439,7 @@ const SidebarGroupLabel = React.forwardRef<
className={cn(
"flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-hidden ring-sidebar-ring transition-[margin,opa] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
className,
className
)}
{...props}
/>
@@ -470,7 +462,7 @@ const SidebarGroupAction = React.forwardRef<
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@@ -518,25 +510,25 @@ const SidebarMenuItem = React.forwardRef<
SidebarMenuItem.displayName = "SidebarMenuItem";
const sidebarMenuButtonVariants = cva(
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
"text-gray-600 peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-primary focus-visible:ring-2 active:bg-sidebar-accent active:text-primary disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-primary data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-primary group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 hover:cursor-pointer",
{
variants: {
variant: {
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
default: "hover:bg-sidebar-accent hover:text-primary",
outline:
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-primary hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
},
size: {
default: "h-8 text-sm",
sm: "h-7 text-xs",
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
default: "h-10 py-2 px-3 gap-3",
sm: "h-8 text-sm",
lg: "h-12 py-2 px-3 gap-3 group-data-[collapsible=icon]:p-0!",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
}
);
const SidebarMenuButton = React.forwardRef<
@@ -557,7 +549,7 @@ const SidebarMenuButton = React.forwardRef<
className,
...props
},
ref,
ref
) => {
const Comp = asChild ? Slot : "button";
const { isMobile, state } = useSidebar();
@@ -594,7 +586,7 @@ const SidebarMenuButton = React.forwardRef<
/>
</Tooltip>
);
},
}
);
SidebarMenuButton.displayName = "SidebarMenuButton";
@@ -612,16 +604,16 @@ const SidebarMenuAction = React.forwardRef<
ref={ref}
data-sidebar="menu-action"
className={cn(
"absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
"absolute right-1 top-1.5 flex aspect-square w-6 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-hidden ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0 hover:cursor-pointer",
// Increases the hit area of the button on mobile.
"after:absolute after:-inset-2 md:after:hidden",
"peer-data-[size=sm]/menu-button:top-1",
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=default]/menu-button:top-2 peer-data-[size=default]/menu-button:right-2",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
showOnHover &&
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
className,
className
)}
{...props}
/>
@@ -643,7 +635,7 @@ const SidebarMenuBadge = React.forwardRef<
"peer-data-[size=default]/menu-button:top-1.5",
"peer-data-[size=lg]/menu-button:top-2.5",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@@ -696,9 +688,9 @@ const SidebarMenuSub = React.forwardRef<
ref={ref}
data-sidebar="menu-sub"
className={cn(
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5",
"ml-5 flex min-w-0 flex-col gap-1 border-l border-sidebar-border px-2.5 py-1",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>
@@ -728,12 +720,12 @@ const SidebarMenuSubButton = React.forwardRef<
data-size={size}
data-active={isActive}
className={cn(
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-4 text-sidebar-foreground outline-hidden ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
size === "sm" && "text-xs",
size === "md" && "text-sm",
"group-data-[collapsible=icon]:hidden",
className,
className
)}
{...props}
/>