feat(ui): implementa sidebar colapsavel com icones e tooltips
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 9s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m42s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
Quality Checks / Lint, Test and Build (push) Successful in 4m20s

- Muda collapsible de offcanvas para icon na sidebar
- Adiciona tooltips aos itens de menu quando colapsado
- Itens com submenu mostram mini-menu no tooltip
- Logo mostra apenas icone quando colapsado
- NavUser mostra apenas avatar quando colapsado
- Adiciona separadores entre secoes quando colapsado
- Centraliza icones horizontalmente no modo colapsado
- Persiste estado da sidebar via cookie entre navegacoes
- Corrige hydration mismatch com sincronizacao pos-hidratacao
- Desabilita transicoes durante sincronizacao inicial
- Remove bolinha do tooltip e ajusta espacamento
- Corrige redirecionamento ao resetar dispositivo no Tauri

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-18 14:38:35 -03:00
parent 826b376dd3
commit 84117e6821
6 changed files with 305 additions and 158 deletions

View file

@ -44,7 +44,14 @@ import {
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
SidebarRail, SidebarRail,
SidebarSeparator,
useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { NavUser } from "@/components/nav-user" import { NavUser } from "@/components/nav-user"
import { useAuth } from "@/lib/auth-client" import { useAuth } from "@/lib/auth-client"
@ -151,9 +158,11 @@ const navigation: NavigationGroup[] = [
}, },
] ]
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) { export function AppSidebar(props: React.ComponentProps<typeof Sidebar>) {
const pathname = usePathname() const pathname = usePathname()
const { session, isLoading, isAdmin, isStaff, role } = useAuth() const { session, isLoading, isAdmin, isStaff, role } = useAuth()
const { state: sidebarState } = useSidebar()
const isCollapsed = sidebarState === "collapsed"
const [isHydrated, setIsHydrated] = React.useState(false) const [isHydrated, setIsHydrated] = React.useState(false)
const canAccess = React.useCallback( const canAccess = React.useCallback(
(requiredRole?: NavRoleRequirement) => { (requiredRole?: NavRoleRequirement) => {
@ -221,7 +230,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
if (!isHydrated) { if (!isHydrated) {
return ( return (
<Sidebar {...props}> <Sidebar collapsible="icon" {...props}>
<SidebarHeader className="pt-4"> <SidebarHeader className="pt-4">
<Skeleton className="h-14 w-full rounded-lg" /> <Skeleton className="h-14 w-full rounded-lg" />
</SidebarHeader> </SidebarHeader>
@ -247,7 +256,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
} }
return ( return (
<Sidebar {...props}> <Sidebar collapsible="icon" {...props}>
<SidebarHeader className="pt-4"> <SidebarHeader className="pt-4">
<SidebarBrand <SidebarBrand
logoSrc="/logo-raven.png" logoSrc="/logo-raven.png"
@ -257,27 +266,78 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
/> />
</SidebarHeader> </SidebarHeader>
<SidebarContent> <SidebarContent>
{navigation.map((group) => { {navigation.map((group, groupIndex) => {
if (!canAccess(group.requiredRole)) return null if (!canAccess(group.requiredRole)) return null
const visibleItems = group.items.filter((item) => !item.hidden && canAccess(item.requiredRole)) const visibleItems = group.items.filter((item) => !item.hidden && canAccess(item.requiredRole))
if (visibleItems.length === 0) return null if (visibleItems.length === 0) return null
return (
<SidebarGroup key={group.title}>
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{visibleItems.map((item) => {
if (item.children && item.children.length > 0) {
const childItems = item.children.filter((child) => !child.hidden && canAccess(child.requiredRole))
const isExpanded = expanded.has(item.title)
const isChildActive = childItems.some((child) => isActive(child))
const parentActive = isChildActive
const isToggleOnly = true // Todos os menus com filhos expandem ao clicar, nao navegam
return ( // Verifica se deve mostrar separador (quando colapsado e nao e o primeiro grupo)
<React.Fragment key={item.title}> const showSeparator = isCollapsed && groupIndex > 0
<SidebarMenuItem>
{isToggleOnly ? ( return (
<React.Fragment key={group.title}>
{showSeparator && <SidebarSeparator className="my-2" />}
<SidebarGroup key={group.title}>
{!isCollapsed && <SidebarGroupLabel>{group.title}</SidebarGroupLabel>}
<SidebarGroupContent>
<SidebarMenu>
{visibleItems.map((item) => {
if (item.children && item.children.length > 0) {
const childItems = item.children.filter((child) => !child.hidden && canAccess(child.requiredRole))
const isExpanded = expanded.has(item.title)
const isChildActive = childItems.some((child) => isActive(child))
// Quando colapsado, mostra tooltip com submenu
if (isCollapsed) {
return (
<SidebarMenuItem key={item.title}>
<Tooltip>
<TooltipTrigger asChild>
<SidebarMenuButton
asChild
isActive={isChildActive}
className="font-medium"
>
<Link href={childItems[0]?.url ?? item.url} className="gap-2">
{item.icon ? <item.icon className="size-5" /> : null}
</Link>
</SidebarMenuButton>
</TooltipTrigger>
<TooltipContent
side="right"
align="start"
className="bg-sidebar border-sidebar-border p-0 shadow-lg"
>
<div className="flex flex-col min-w-[180px]">
<div className="border-b border-sidebar-border px-3 py-2">
<p className="font-semibold text-sm text-sidebar-foreground">{item.title}</p>
</div>
<div className="py-1">
{childItems.map((child) => (
<Link
key={child.title}
href={child.url}
className={cn(
"flex items-center gap-2 px-3 py-1.5 text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-colors",
isActive(child) && "bg-sidebar-accent text-sidebar-accent-foreground font-medium"
)}
>
{child.icon ? <child.icon className="size-3.5 text-sidebar-foreground/70" /> : null}
<span>{child.title}</span>
</Link>
))}
</div>
</div>
</TooltipContent>
</Tooltip>
</SidebarMenuItem>
)
}
// Modo expandido - comportamento normal
return (
<React.Fragment key={item.title}>
<SidebarMenuItem>
<button <button
type="button" type="button"
className={cn( className={cn(
@ -298,60 +358,62 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<ChevronDown className="size-3" /> <ChevronDown className="size-3" />
</span> </span>
</button> </button>
) : ( </SidebarMenuItem>
<SidebarMenuButton asChild isActive={parentActive}> {isExpanded
<Link href={item.url} className={cn("gap-2", "relative pr-7") }> ? childItems.map((child) => (
{item.icon ? <item.icon className="size-4" /> : null} <SidebarMenuItem key={`${item.title}-${child.title}`}>
<span className="flex-1">{item.title}</span> <SidebarMenuButton asChild isActive={isActive(child)}>
<span <Link href={child.url} className="gap-2 pl-7 text-sm">
role="button" {child.icon ? <child.icon className="size-3.5 text-neutral-500" /> : null}
aria-label={isExpanded ? "Recolher submenu" : "Expandir submenu"} <span>{child.title}</span>
onClick={(event) => { </Link>
event.preventDefault() </SidebarMenuButton>
event.stopPropagation() </SidebarMenuItem>
toggleExpanded(item.title) ))
}} : null}
className={cn( </React.Fragment>
"absolute right-1.5 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-neutral-500 transition hover:bg-slate-200 hover:text-neutral-700", )
isExpanded && "rotate-180" }
)}
>
<ChevronDown className="size-3" />
</span>
</Link>
</SidebarMenuButton>
)}
</SidebarMenuItem>
{isExpanded
? childItems.map((child) => (
<SidebarMenuItem key={`${item.title}-${child.title}`}>
<SidebarMenuButton asChild isActive={isActive(child)}>
<Link href={child.url} className="gap-2 pl-7 text-sm">
{child.icon ? <child.icon className="size-3.5 text-neutral-500" /> : null}
<span>{child.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))
: null}
</React.Fragment>
)
}
return ( // Item simples (sem filhos)
<SidebarMenuItem key={item.title}> if (isCollapsed) {
<SidebarMenuButton asChild isActive={isActive(item)} className="font-medium"> return (
<Link href={item.url} className="gap-2"> <SidebarMenuItem key={item.title}>
{item.icon ? <item.icon className="size-4" /> : null} <Tooltip>
<span>{item.title}</span> <TooltipTrigger asChild>
</Link> <SidebarMenuButton asChild isActive={isActive(item)} className="font-medium">
</SidebarMenuButton> <Link href={item.url} className="gap-2">
</SidebarMenuItem> {item.icon ? <item.icon className="size-5" /> : null}
) </Link>
})} </SidebarMenuButton>
</SidebarMenu> </TooltipTrigger>
</SidebarGroupContent> <TooltipContent
</SidebarGroup> side="right"
align="center"
className="bg-sidebar border-sidebar-border text-sidebar-foreground shadow-lg"
>
{item.title}
</TooltipContent>
</Tooltip>
</SidebarMenuItem>
)
}
return (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton asChild isActive={isActive(item)} className="font-medium">
<Link href={item.url} className="gap-2">
{item.icon ? <item.icon className="size-4" /> : null}
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
)
})}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</React.Fragment>
) )
})} })}
</SidebarContent> </SidebarContent>

View file

@ -31,6 +31,7 @@ import {
useSidebar, useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import { signOut, useAuth } from "@/lib/auth-client" import { signOut, useAuth } from "@/lib/auth-client"
import { cn } from "@/lib/utils"
type NavUserProps = { type NavUserProps = {
user?: { user?: {
@ -42,7 +43,8 @@ type NavUserProps = {
export function NavUser({ user }: NavUserProps) { export function NavUser({ user }: NavUserProps) {
const normalizedUser = user ?? { name: null, email: null, avatarUrl: null } const normalizedUser = user ?? { name: null, email: null, avatarUrl: null }
const { isMobile } = useSidebar() const { isMobile, state } = useSidebar()
const isCollapsed = state === "collapsed"
const router = useRouter() const router = useRouter()
const [isSigningOut, setIsSigningOut] = useState(false) const [isSigningOut, setIsSigningOut] = useState(false)
const [isDesktopShell, setIsDesktopShell] = useState(false) const [isDesktopShell, setIsDesktopShell] = useState(false)
@ -96,19 +98,25 @@ export function NavUser({ user }: NavUserProps) {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<SidebarMenuButton <SidebarMenuButton
size="lg" 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 transition-all duration-200 ease-linear"
> >
<Avatar className="h-8 w-8 rounded-lg"> <Avatar className="h-8 w-8 shrink-0 rounded-lg transition-all duration-200 ease-linear">
<AvatarImage src={normalizedUser.avatarUrl ?? undefined} alt={displayName} /> <AvatarImage src={normalizedUser.avatarUrl ?? undefined} alt={displayName} />
<AvatarFallback className="rounded-lg bg-neutral-200 text-neutral-700 font-medium">{initials}</AvatarFallback> <AvatarFallback className="rounded-lg bg-neutral-200 text-neutral-700 font-medium">{initials}</AvatarFallback>
</Avatar> </Avatar>
<div className="grid flex-1 text-left text-sm leading-tight"> <div className={cn(
<span className="truncate font-medium">{displayName}</span> "grid flex-1 text-left text-sm leading-tight transition-all duration-200 ease-linear overflow-hidden",
<span className="text-muted-foreground truncate text-xs"> isCollapsed ? "w-0 opacity-0" : "w-auto opacity-100"
)}>
<span className="truncate font-medium whitespace-nowrap">{displayName}</span>
<span className="text-muted-foreground truncate text-xs whitespace-nowrap">
{displayEmail} {displayEmail}
</span> </span>
</div> </div>
<IconDotsVertical className="ml-auto size-4" /> <IconDotsVertical className={cn(
"ml-auto size-4 transition-all duration-200 ease-linear",
isCollapsed ? "w-0 opacity-0" : "w-4 opacity-100"
)} />
</SidebarMenuButton> </SidebarMenuButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent

View file

@ -41,8 +41,18 @@ export function PortalShell({ children }: PortalShellProps) {
}, []) }, [])
const handleTokenRevoked = useCallback(() => { const handleTokenRevoked = useCallback(() => {
console.log("[PortalShell] Token foi revogado - redirecionando para login") console.log("[PortalShell] Token foi revogado - redirecionando para tela de registro")
toast.error("Este dispositivo foi resetado. Faça login novamente.") toast.error("Este dispositivo foi resetado. Faça login novamente.")
// Se estiver rodando dentro do Tauri, navega para a URL do app para voltar à tela de registro
const isTauri = typeof window !== "undefined" && Boolean((window as typeof window & { __TAURI__?: unknown }).__TAURI__)
if (isTauri) {
// URL do app Tauri - em producao usa tauri.localhost, em dev usa localhost:1420
const isDev = process.env.NODE_ENV === "development"
const tauriUrl = isDev ? "http://localhost:1420/" : "http://tauri.localhost/"
console.log("[PortalShell] Detectado ambiente Tauri, navegando para:", tauriUrl)
window.location.href = tauriUrl
return
}
router.replace("/login") router.replace("/login")
}, [router]) }, [router])

View file

@ -6,7 +6,14 @@ import {
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar" } from "@/components/ui/sidebar"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
interface SidebarBrandProps { interface SidebarBrandProps {
logoSrc: string logoSrc: string
@ -16,33 +23,69 @@ interface SidebarBrandProps {
} }
export function SidebarBrand({ logoSrc, logoAlt, title, subtitle }: SidebarBrandProps) { export function SidebarBrand({ logoSrc, logoAlt, title, subtitle }: SidebarBrandProps) {
const { state } = useSidebar()
const isCollapsed = state === "collapsed"
const brandContent = (
<div className="flex items-center gap-3 transition-all duration-200 ease-linear">
<div className={cn(
"flex shrink-0 items-center justify-center transition-all duration-200 ease-linear",
isCollapsed ? "size-9" : "size-8"
)}>
<Image
src={logoSrc}
alt={logoAlt}
width={36}
height={36}
className={cn(
"object-contain transition-all duration-200 ease-linear",
isCollapsed ? "size-9" : "size-8"
)}
priority
/>
</div>
<div className={cn(
"flex flex-col items-start gap-1 leading-none transition-all duration-200 ease-linear overflow-hidden",
isCollapsed ? "w-0 opacity-0" : "w-auto opacity-100"
)}>
<span className="text-lg font-semibold whitespace-nowrap">{title}</span>
<span className="inline-flex whitespace-nowrap rounded-full bg-neutral-900 px-2.5 py-1 text-[11px] font-medium text-white">
{subtitle}
</span>
</div>
</div>
)
const button = (
<SidebarMenuButton
asChild
size="lg"
className="h-auto cursor-default select-none hover:bg-transparent hover:text-inherit active:bg-transparent active:text-inherit focus-visible:ring-0"
>
{brandContent}
</SidebarMenuButton>
)
return ( return (
<SidebarMenu> <SidebarMenu>
<SidebarMenuItem> <SidebarMenuItem>
<SidebarMenuButton {isCollapsed ? (
asChild <Tooltip>
size="lg" <TooltipTrigger asChild>
className="h-auto cursor-default select-none hover:bg-transparent hover:text-inherit active:bg-transparent active:text-inherit focus-visible:ring-0" {button}
> </TooltipTrigger>
<div className="flex items-center gap-3"> <TooltipContent
<div className="flex h-12 w-12 items-center justify-center rounded-lg"> side="right"
<Image align="center"
src={logoSrc} className="bg-sidebar border-sidebar-border text-sidebar-foreground shadow-lg"
alt={logoAlt} >
width={48} <p className="font-semibold">{title}</p>
height={48} <p className="text-xs text-sidebar-foreground/70">{subtitle}</p>
className="h-12 w-12 object-contain" </TooltipContent>
priority </Tooltip>
/> ) : (
</div> button
<div className="flex flex-col items-start gap-1 leading-none"> )}
<span className="text-lg font-semibold">{title}</span>
<span className="inline-flex whitespace-nowrap rounded-full bg-neutral-900 px-2.5 py-1 text-[11px] font-medium text-white">
{subtitle}
</span>
</div>
</div>
</SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu> </SidebarMenu>
) )

View file

@ -27,9 +27,9 @@ import {
const SIDEBAR_COOKIE_NAME = "sidebar_state" const SIDEBAR_COOKIE_NAME = "sidebar_state"
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
const SIDEBAR_WIDTH = "clamp(12rem, 15vw + 1.5rem, 16.5rem)" const SIDEBAR_WIDTH = "clamp(12rem, 15vw + 1.5rem, 16.5rem)"
const SIDEBAR_WIDTH_MOBILE = "18rem" const SIDEBAR_WIDTH_MOBILE = "18rem"
const SIDEBAR_WIDTH_ICON = "3rem" const SIDEBAR_WIDTH_ICON = "4rem"
const SIDEBAR_KEYBOARD_SHORTCUT = "b" const SIDEBAR_KEYBOARD_SHORTCUT = "b"
type SidebarContextProps = { type SidebarContextProps = {
@ -40,6 +40,7 @@ type SidebarContextProps = {
setOpenMobile: (open: boolean) => void setOpenMobile: (open: boolean) => void
isMobile: boolean isMobile: boolean
toggleSidebar: () => void toggleSidebar: () => void
isHydrated: boolean
} }
const SidebarContext = React.createContext<SidebarContextProps | null>(null) const SidebarContext = React.createContext<SidebarContextProps | null>(null)
@ -68,11 +69,31 @@ function SidebarProvider({
}) { }) {
const isMobile = useIsMobile() const isMobile = useIsMobile()
const [openMobile, setOpenMobile] = React.useState(false) const [openMobile, setOpenMobile] = React.useState(false)
const [isHydrated, setIsHydrated] = React.useState(false)
// This is the internal state of the sidebar. // This is the internal state of the sidebar.
// We use openProp and setOpenProp for control from outside the component. // We use openProp and setOpenProp for control from outside the component.
const [_open, _setOpen] = React.useState(defaultOpen) const [_open, _setOpen] = React.useState(defaultOpen)
const open = openProp ?? _open const open = openProp ?? _open
// Sync state from cookie after hydration to avoid hydration mismatch
React.useEffect(() => {
const cookie = document.cookie
.split("; ")
.find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`))
if (cookie) {
const cookieValue = cookie.split("=")[1] === "true"
if (cookieValue !== _open) {
_setOpen(cookieValue)
}
}
// Mark as hydrated after a small delay to allow state to settle without animation
requestAnimationFrame(() => {
setIsHydrated(true)
})
// Only run once on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const setOpen = React.useCallback( const setOpen = React.useCallback(
(value: boolean | ((value: boolean) => boolean)) => { (value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === "function" ? value(open) : value const openState = typeof value === "function" ? value(open) : value
@ -122,16 +143,18 @@ function SidebarProvider({
openMobile, openMobile,
setOpenMobile, setOpenMobile,
toggleSidebar, toggleSidebar,
isHydrated,
}), }),
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, isHydrated]
) )
return ( return (
<SidebarContext.Provider value={contextValue}> <SidebarContext.Provider value={contextValue}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<div <div
suppressHydrationWarning suppressHydrationWarning
data-slot="sidebar-wrapper" data-slot="sidebar-wrapper"
data-hydrated={isHydrated ? "true" : "false"}
style={ style={
{ {
"--sidebar-width": SIDEBAR_WIDTH, "--sidebar-width": SIDEBAR_WIDTH,
@ -141,6 +164,7 @@ function SidebarProvider({
} }
className={cn( className={cn(
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full", "group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
!isHydrated && "[&_*]:!transition-none",
className className
)} )}
{...props} {...props}
@ -254,34 +278,34 @@ function Sidebar({
) )
} }
function SidebarTrigger({ function SidebarTrigger({
className, className,
onClick, onClick,
tabIndex, tabIndex,
...props ...props
}: React.ComponentProps<typeof Button>) { }: React.ComponentProps<typeof Button>) {
const { toggleSidebar } = useSidebar() const { toggleSidebar } = useSidebar()
const [hydrated, setHydrated] = React.useState(false) const [hydrated, setHydrated] = React.useState(false)
React.useEffect(() => { React.useEffect(() => {
setHydrated(true) setHydrated(true)
}, []) }, [])
return ( return (
<Button <Button
data-sidebar="trigger" data-sidebar="trigger"
data-slot="sidebar-trigger" data-slot="sidebar-trigger"
variant="ghost" variant="ghost"
size="icon" size="icon"
className={cn("size-7", className)} className={cn("size-7", className)}
disabled={!hydrated} disabled={!hydrated}
aria-hidden={hydrated ? undefined : true} aria-hidden={hydrated ? undefined : true}
tabIndex={hydrated ? tabIndex : -1} tabIndex={hydrated ? tabIndex : -1}
onClick={(event) => { onClick={(event) => {
onClick?.(event) onClick?.(event)
toggleSidebar() toggleSidebar()
}} }}
{...props} {...props}
> >
<PanelLeftIcon /> <PanelLeftIcon />
<span className="sr-only">Toggle Sidebar</span> <span className="sr-only">Toggle Sidebar</span>
@ -314,17 +338,17 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
) )
} }
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) { function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
return ( return (
<main <main
data-slot="sidebar-inset" data-slot="sidebar-inset"
className={cn( className={cn(
"bg-background relative flex w-full min-w-0 flex-1 flex-col", "bg-background relative flex w-full min-w-0 flex-1 flex-col",
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2", "md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
className className
)} )}
{...props} {...props}
/> />
) )
} }
@ -466,7 +490,11 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
<ul <ul
data-slot="sidebar-menu" data-slot="sidebar-menu"
data-sidebar="menu" data-sidebar="menu"
className={cn("flex w-full min-w-0 flex-col gap-1", className)} className={cn(
"flex w-full min-w-0 flex-col gap-1",
"group-data-[collapsible=icon]:items-center",
className
)}
{...props} {...props}
/> />
) )

View file

@ -36,7 +36,7 @@ function TooltipTrigger({
function TooltipContent({ function TooltipContent({
className, className,
sideOffset = 20, sideOffset = 8,
children, children,
...props ...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) { }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
@ -46,16 +46,12 @@ function TooltipContent({
data-slot="tooltip-content" data-slot="tooltip-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 w-fit origin-(--radix-tooltip-content-transform-origin) overflow-visible rounded-md px-3 py-1.5 text-xs text-balance group", "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) overflow-hidden rounded-md px-3 py-1.5 text-xs text-balance",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<span
aria-hidden
className="pointer-events-none absolute z-50 size-2 rounded-full bg-neutral-900 group-data-[side=top]:left-1/2 group-data-[side=top]:top-full group-data-[side=top]:-translate-x-1/2 group-data-[side=top]:mt-1 group-data-[side=bottom]:left-1/2 group-data-[side=bottom]:bottom-full group-data-[side=bottom]:-translate-x-1/2 group-data-[side=bottom]:mb-1 group-data-[side=left]:left-full group-data-[side=left]:top-1/2 group-data-[side=left]:-translate-y-1/2 group-data-[side=left]:ml-1 group-data-[side=right]:right-full group-data-[side=right]:top-1/2 group-data-[side=right]:-translate-y-1/2 group-data-[side=right]:mr-1"
/>
</TooltipPrimitive.Content> </TooltipPrimitive.Content>
</TooltipPrimitive.Portal> </TooltipPrimitive.Portal>
) )