checkpoint/feature-a-medias
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
|
||||
import { useSidebar } from "$lib/components/ui/sidebar/context.svelte.js";
|
||||
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||
|
||||
let {
|
||||
items,
|
||||
@@ -9,8 +11,6 @@
|
||||
items: {
|
||||
title: string;
|
||||
url: string;
|
||||
// this should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon?: any;
|
||||
isActive?: boolean;
|
||||
items?: {
|
||||
@@ -19,6 +19,67 @@
|
||||
}[];
|
||||
}[];
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Estado del Sistema Híbrido
|
||||
let activeTitle = $state<string | null>(null);
|
||||
// Flags de estado físico
|
||||
let isHoveringTrigger = false;
|
||||
let isHoveringContent = false;
|
||||
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// PROTOCOLO DE TRASPASO (Handover Protocol)
|
||||
// 1. Cuando entramos en zona segura, cancelamos cualquier intento de cierre.
|
||||
function clearCloseTimer() {
|
||||
if (closeTimer) {
|
||||
clearTimeout(closeTimer);
|
||||
closeTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Cuando salimos, esperamos un "micro-tick" (75ms) para ver si el mouse aterriza en la otra zona.
|
||||
// Si no aterriza, cerramos.
|
||||
function scheduleClose() {
|
||||
clearCloseTimer();
|
||||
closeTimer = setTimeout(() => {
|
||||
if (!isHoveringTrigger && !isHoveringContent) {
|
||||
activeTitle = null;
|
||||
}
|
||||
}, 75);
|
||||
}
|
||||
|
||||
function handleTriggerEnter(title: string) {
|
||||
if (sidebar.state !== "collapsed") return;
|
||||
clearCloseTimer();
|
||||
isHoveringTrigger = true;
|
||||
activeTitle = title;
|
||||
}
|
||||
|
||||
function handleTriggerLeave() {
|
||||
isHoveringTrigger = false;
|
||||
scheduleClose();
|
||||
}
|
||||
|
||||
function handleContentEnter() {
|
||||
clearCloseTimer();
|
||||
isHoveringContent = true;
|
||||
}
|
||||
|
||||
function handleContentLeave() {
|
||||
isHoveringContent = false;
|
||||
scheduleClose();
|
||||
}
|
||||
|
||||
function onOpenChange(open: boolean, title: string) {
|
||||
if (open) {
|
||||
activeTitle = title;
|
||||
}
|
||||
// "AUTHORITY STRATEGY":
|
||||
// Ignoramos explícitamente cuando `open` es false.
|
||||
// La librería NO tiene permiso para cerrar el menú.
|
||||
// Solo nosotros (vía scheduleClose) podemos poner activeTitle = null.
|
||||
}
|
||||
</script>
|
||||
|
||||
<Sidebar.Group>
|
||||
@@ -26,43 +87,105 @@
|
||||
<Sidebar.Menu>
|
||||
{#each items as item (item.title)}
|
||||
{#if item.items && item.items.length > 0}
|
||||
<!-- Dropdown para items con subitems -->
|
||||
<Collapsible.Root open={item.isActive} class="group/collapsible">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
<Collapsible.Trigger>
|
||||
{#if sidebar.state === "collapsed"}
|
||||
<!-- Sidebar Colapsado: Protocolo Híbrido + Authority Strategy -->
|
||||
<Sidebar.MenuItem>
|
||||
<DropdownMenu.Root
|
||||
open={activeTitle === item.title}
|
||||
onOpenChange={(v) => onOpenChange(v, item.title)}
|
||||
modal={false}
|
||||
>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuButton {...props} tooltipContent={item.title}>
|
||||
{#if item.icon}
|
||||
<item.icon />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRightIcon
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
<div
|
||||
id={`trigger-${item.title}`}
|
||||
class="relative z-30 w-full"
|
||||
onmouseenter={() => handleTriggerEnter(item.title)}
|
||||
onmouseleave={handleTriggerLeave}
|
||||
>
|
||||
<!-- Puente de hover optimizado -->
|
||||
<div class="absolute inset-y-0 left-[80%] w-20 bg-transparent pointer-events-auto" />
|
||||
|
||||
<Sidebar.MenuButton {...props} tooltipContent={null}>
|
||||
{#if item.icon}
|
||||
<item.icon />
|
||||
{:else}
|
||||
<div class="size-4" />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
</Sidebar.MenuButton>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each item.items as subItem (subItem.title)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
{#snippet child({ props })}
|
||||
<a href={subItem.url} {...props}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={-4}
|
||||
closeOnOutsideClick={false}
|
||||
closeOnEscape={false}
|
||||
class="z-50 min-w-56 rounded-lg p-0 shadow-lg"
|
||||
>
|
||||
<div
|
||||
id={`content-${item.title}`}
|
||||
class="w-full h-full p-1 pointer-events-auto"
|
||||
role="none"
|
||||
onmouseenter={handleContentEnter}
|
||||
onmouseleave={handleContentLeave}
|
||||
>
|
||||
<DropdownMenu.DropdownMenuGroup>
|
||||
<DropdownMenu.DropdownMenuLabel>{item.title}</DropdownMenu.DropdownMenuLabel>
|
||||
<DropdownMenu.DropdownMenuSeparator />
|
||||
{#each item.items as subItem (subItem.title)}
|
||||
<DropdownMenu.DropdownMenuItem>
|
||||
<a href={subItem.url} class="w-full">
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
{/each}
|
||||
</DropdownMenu.DropdownMenuGroup>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{:else}
|
||||
<!-- Sidebar Expandido: Collapsible original -->
|
||||
<Collapsible.Root open={item.isActive} class="group/collapsible">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuButton {...props} tooltipContent={item.title}>
|
||||
{#if item.icon}
|
||||
<item.icon />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each item.items as subItem (subItem.title)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
{#snippet child({ props })}
|
||||
<a href={subItem.url} {...props}>
|
||||
<span>{subItem.title}</span>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Botón simple para items sin subitems -->
|
||||
<!-- Items sin submenú -->
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton tooltipContent={item.title}>
|
||||
{#snippet child({ props })}
|
||||
@@ -79,3 +202,4 @@
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
class={cn(
|
||||
"w-(--sidebar-width) fixed inset-y-0 z-10 hidden h-svh transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
"w-(--sidebar-width) fixed inset-y-0 z-30 hidden h-svh transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
|
||||
Reference in New Issue
Block a user