feature/dropdown
This commit is contained in:
@@ -22,63 +22,57 @@
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Estado del Sistema Híbrido
|
||||
// Estado del Sistema Híbrido controlado por hover estricto (sin timers)
|
||||
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;
|
||||
}
|
||||
}
|
||||
function shouldKeepOpen(event: PointerEvent, title: string) {
|
||||
const next = event.relatedTarget as Node | null;
|
||||
if (!next) return false;
|
||||
|
||||
// 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);
|
||||
}
|
||||
const trigger = document.getElementById(`trigger-${title}`);
|
||||
const content = document.getElementById(`content-${title}`);
|
||||
|
||||
// Mantenemos abierto si el puntero se mueve hacia el trigger o el contenido del menú
|
||||
return (trigger && trigger.contains(next)) || (content && content.contains(next));
|
||||
}
|
||||
|
||||
function handleTriggerEnter(title: string) {
|
||||
if (sidebar.state !== "collapsed") return;
|
||||
clearCloseTimer();
|
||||
isHoveringTrigger = true;
|
||||
activeTitle = title;
|
||||
}
|
||||
|
||||
function handleTriggerLeave() {
|
||||
isHoveringTrigger = false;
|
||||
scheduleClose();
|
||||
}
|
||||
function handleTriggerLeave(event: PointerEvent, title: string) {
|
||||
if (sidebar.state !== "collapsed") return;
|
||||
|
||||
// Si nos movemos al contenido (o nos quedamos en el trigger), no cerramos
|
||||
if (shouldKeepOpen(event, title)) return;
|
||||
|
||||
activeTitle = null;
|
||||
}
|
||||
|
||||
function handleContentEnter() {
|
||||
clearCloseTimer();
|
||||
isHoveringContent = true;
|
||||
}
|
||||
function handleContentEnter(title: string) {
|
||||
if (sidebar.state !== "collapsed") return;
|
||||
activeTitle = title;
|
||||
}
|
||||
|
||||
function handleContentLeave() {
|
||||
isHoveringContent = false;
|
||||
scheduleClose();
|
||||
}
|
||||
function handleContentLeave(event: PointerEvent, title: string) {
|
||||
if (sidebar.state !== "collapsed") return;
|
||||
|
||||
// Si nos movemos de vuelta al trigger (o dentro del contenido), no cerramos
|
||||
if (shouldKeepOpen(event, title)) return;
|
||||
|
||||
activeTitle = null;
|
||||
}
|
||||
|
||||
function onOpenChange(open: boolean, title: string) {
|
||||
// Sincronización base
|
||||
if (open) {
|
||||
activeTitle = title;
|
||||
} else {
|
||||
if (activeTitle === title) {
|
||||
activeTitle = null;
|
||||
}
|
||||
}
|
||||
// "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>
|
||||
|
||||
@@ -88,31 +82,29 @@
|
||||
{#each items as item (item.title)}
|
||||
{#if item.items && item.items.length > 0}
|
||||
{#if sidebar.state === "collapsed"}
|
||||
<!-- Sidebar Colapsado: Protocolo Híbrido + Authority Strategy -->
|
||||
<!-- Sidebar Colapsado: Dropdown controlado por eventos estrictos -->
|
||||
<Sidebar.MenuItem>
|
||||
<DropdownMenu.Root
|
||||
open={activeTitle === item.title}
|
||||
onOpenChange={(v) => onOpenChange(v, item.title)}
|
||||
modal={false}
|
||||
modal={false}
|
||||
>
|
||||
<DropdownMenu.Trigger>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
{#snippet child({ props })}
|
||||
<div
|
||||
id={`trigger-${item.title}`}
|
||||
class="relative z-30 w-full"
|
||||
onmouseenter={() => handleTriggerEnter(item.title)}
|
||||
onmouseleave={handleTriggerLeave}
|
||||
id={`trigger-${item.title}`}
|
||||
class="relative z-30 w-full flex justify-center"
|
||||
onpointerenter={() => handleTriggerEnter(item.title)}
|
||||
onpointerleave={(e) => handleTriggerLeave(e, item.title)}
|
||||
>
|
||||
<!-- Puente de hover optimizado -->
|
||||
<div class="absolute inset-y-0 left-[80%] w-20 bg-transparent pointer-events-auto" />
|
||||
|
||||
<Sidebar.MenuButton {...props} tooltipContent={null}>
|
||||
<Sidebar.MenuButton {...props} tooltipContent={null} class="justify-center">
|
||||
{#if item.icon}
|
||||
<item.icon />
|
||||
{:else}
|
||||
<div class="size-4" />
|
||||
{/if}
|
||||
<span>{item.title}</span>
|
||||
<!-- Ocultamos el texto en modo colapsado para asegurar que solo sea el icono -->
|
||||
<span class="sr-only">{item.title}</span>
|
||||
</Sidebar.MenuButton>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -120,25 +112,40 @@
|
||||
<DropdownMenu.Content
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={-4}
|
||||
closeOnOutsideClick={false}
|
||||
closeOnEscape={false}
|
||||
class="z-50 min-w-56 rounded-lg p-0 shadow-lg"
|
||||
sideOffset={0}
|
||||
class="z-50 w-64 rounded-lg p-0 shadow-lg overflow-visible"
|
||||
id={`content-${item.title}`}
|
||||
onpointerenter={() => handleContentEnter(item.title)}
|
||||
onpointerleave={(e) => handleContentLeave(e, item.title)}
|
||||
>
|
||||
<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 />
|
||||
<!--
|
||||
Header "Tab" (Icono Flotante)
|
||||
Posicionado con right-full para estar exactamente donde el trigger termina (offset 0).
|
||||
Usamos w-8 h-8 para coincidir con un botón de tamaño estándar de sidebar.
|
||||
-->
|
||||
<div class="absolute right-full top-0 flex items-center justify-center bg-sidebar-accent text-sidebar-accent-foreground rounded-l-lg w-8 h-8 shadow-none border border-sidebar-border border-r-0 z-50">
|
||||
{#if item.icon}
|
||||
<item.icon class="size-4 shrink-0" />
|
||||
{:else}
|
||||
<div class="size-4 shrink-0" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!--
|
||||
Panel Principal
|
||||
-->
|
||||
<div class="w-full h-full p-1 pointer-events-auto bg-popover rounded-lg rounded-tl-none border border-sidebar-border ml-[0px]">
|
||||
|
||||
<!-- Título en el panel principal -->
|
||||
<div class="px-2 py-2 text-sm font-medium border-b text-sidebar-foreground truncate">
|
||||
{item.title}
|
||||
</div>
|
||||
|
||||
<DropdownMenu.DropdownMenuGroup class="mt-1 max-h-80 overflow-y-auto">
|
||||
{#each item.items as subItem (subItem.title)}
|
||||
<DropdownMenu.DropdownMenuItem>
|
||||
<a href={subItem.url} class="w-full">
|
||||
<span>{subItem.title}</span>
|
||||
<DropdownMenu.DropdownMenuItem asChild>
|
||||
<a href={subItem.url} class="flex w-full items-center gap-2 overflow-hidden">
|
||||
<span class="truncate">{subItem.title}</span>
|
||||
</a>
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
{/each}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Códigos',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Contenedores',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
'Lista Países',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
'Lista Tipos de Moneda',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
'Lista Almacenes Aduanales',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
'Lista Tipos de Factura',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Tipos de Material',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Sectores',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Estados',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Modos de Transporte',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
'Lista Tipos de Transporte',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
'Lista Métodos de Valoración',
|
||||
obtenerAtajosLista({
|
||||
manejarNuevo: () => (showCreateDialog = true),
|
||||
manejarActualizar: handleRefresh
|
||||
manejarActualizar: reloadData
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user