feat(ui): implementa sidebar colapsavel com icones e tooltips
All checks were successful
All checks were successful
- 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:
parent
826b376dd3
commit
84117e6821
6 changed files with 305 additions and 158 deletions
|
|
@ -44,7 +44,14 @@ import {
|
|||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { NavUser } from "@/components/nav-user"
|
||||
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 { session, isLoading, isAdmin, isStaff, role } = useAuth()
|
||||
const { state: sidebarState } = useSidebar()
|
||||
const isCollapsed = sidebarState === "collapsed"
|
||||
const [isHydrated, setIsHydrated] = React.useState(false)
|
||||
const canAccess = React.useCallback(
|
||||
(requiredRole?: NavRoleRequirement) => {
|
||||
|
|
@ -221,7 +230,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
|
||||
if (!isHydrated) {
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader className="pt-4">
|
||||
<Skeleton className="h-14 w-full rounded-lg" />
|
||||
</SidebarHeader>
|
||||
|
|
@ -247,7 +256,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Sidebar {...props}>
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader className="pt-4">
|
||||
<SidebarBrand
|
||||
logoSrc="/logo-raven.png"
|
||||
|
|
@ -257,27 +266,78 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
/>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
{navigation.map((group) => {
|
||||
{navigation.map((group, groupIndex) => {
|
||||
if (!canAccess(group.requiredRole)) return null
|
||||
const visibleItems = group.items.filter((item) => !item.hidden && canAccess(item.requiredRole))
|
||||
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 (
|
||||
<React.Fragment key={item.title}>
|
||||
<SidebarMenuItem>
|
||||
{isToggleOnly ? (
|
||||
// Verifica se deve mostrar separador (quando colapsado e nao e o primeiro grupo)
|
||||
const showSeparator = isCollapsed && groupIndex > 0
|
||||
|
||||
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
|
||||
type="button"
|
||||
className={cn(
|
||||
|
|
@ -298,60 +358,62 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|||
<ChevronDown className="size-3" />
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<SidebarMenuButton asChild isActive={parentActive}>
|
||||
<Link href={item.url} className={cn("gap-2", "relative pr-7") }>
|
||||
{item.icon ? <item.icon className="size-4" /> : null}
|
||||
<span className="flex-1">{item.title}</span>
|
||||
<span
|
||||
role="button"
|
||||
aria-label={isExpanded ? "Recolher submenu" : "Expandir submenu"}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
toggleExpanded(item.title)
|
||||
}}
|
||||
className={cn(
|
||||
"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>
|
||||
)
|
||||
}
|
||||
</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 (
|
||||
<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>
|
||||
// Item simples (sem filhos)
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SidebarMenuButton asChild isActive={isActive(item)} className="font-medium">
|
||||
<Link href={item.url} className="gap-2">
|
||||
{item.icon ? <item.icon className="size-5" /> : null}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import {
|
|||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { signOut, useAuth } from "@/lib/auth-client"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
type NavUserProps = {
|
||||
user?: {
|
||||
|
|
@ -42,7 +43,8 @@ type NavUserProps = {
|
|||
|
||||
export function NavUser({ user }: NavUserProps) {
|
||||
const normalizedUser = user ?? { name: null, email: null, avatarUrl: null }
|
||||
const { isMobile } = useSidebar()
|
||||
const { isMobile, state } = useSidebar()
|
||||
const isCollapsed = state === "collapsed"
|
||||
const router = useRouter()
|
||||
const [isSigningOut, setIsSigningOut] = useState(false)
|
||||
const [isDesktopShell, setIsDesktopShell] = useState(false)
|
||||
|
|
@ -96,19 +98,25 @@ export function NavUser({ user }: NavUserProps) {
|
|||
<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 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} />
|
||||
<AvatarFallback className="rounded-lg bg-neutral-200 text-neutral-700 font-medium">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{displayName}</span>
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
<div className={cn(
|
||||
"grid flex-1 text-left text-sm leading-tight transition-all duration-200 ease-linear overflow-hidden",
|
||||
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}
|
||||
</span>
|
||||
</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>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
|
|
|
|||
|
|
@ -41,8 +41,18 @@ export function PortalShell({ children }: PortalShellProps) {
|
|||
}, [])
|
||||
|
||||
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.")
|
||||
// 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])
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ import {
|
|||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface SidebarBrandProps {
|
||||
logoSrc: string
|
||||
|
|
@ -16,33 +23,69 @@ interface 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 (
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<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"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg">
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={logoAlt}
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-12 w-12 object-contain"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
{isCollapsed ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{button}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
className="bg-sidebar border-sidebar-border text-sidebar-foreground shadow-lg"
|
||||
>
|
||||
<p className="font-semibold">{title}</p>
|
||||
<p className="text-xs text-sidebar-foreground/70">{subtitle}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
button
|
||||
)}
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
|||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "clamp(12rem, 15vw + 1.5rem, 16.5rem)"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_WIDTH_ICON = "4rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
|
|
@ -40,6 +40,7 @@ type SidebarContextProps = {
|
|||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
isHydrated: boolean
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
|
@ -68,11 +69,31 @@ function SidebarProvider({
|
|||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
const [isHydrated, setIsHydrated] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
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(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
|
|
@ -122,8 +143,9 @@ function SidebarProvider({
|
|||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
isHydrated,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar, isHydrated]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
@ -132,6 +154,7 @@ function SidebarProvider({
|
|||
<div
|
||||
suppressHydrationWarning
|
||||
data-slot="sidebar-wrapper"
|
||||
data-hydrated={isHydrated ? "true" : "false"}
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
|
|
@ -141,6 +164,7 @@ function SidebarProvider({
|
|||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
!isHydrated && "[&_*]:!transition-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
@ -466,7 +490,11 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
|||
<ul
|
||||
data-slot="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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function TooltipTrigger({
|
|||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 20,
|
||||
sideOffset = 8,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
|
|
@ -46,16 +46,12 @@ function TooltipContent({
|
|||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{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.Portal>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue