Fix: Usar DropdownMenu de radix-vue para menú de configuración
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 26s

- Reemplaza dropdown manual por componente DropdownMenu con radix-vue
- Agrega avoid-collisions y collision-padding para posicionamiento automático
- El menú ahora se ajusta automáticamente cuando no hay espacio horizontal
- Actualiza DropdownMenuItem para soportar prop 'as' y 'href' para enlaces
This commit is contained in:
2025-11-25 01:17:39 -06:00
parent 2d1c2b1df3
commit 6639c93cfe
3 changed files with 41 additions and 25 deletions

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import { onMounted, reactive, ref, computed, watch } from 'vue'; import { onMounted, reactive, ref, computed, watch } from 'vue';
import { Button, Badge, Input, Card, CardHeader, CardTitle, CardActions, CardContent, Textarea } from '@/components/ui'; import { Button, Badge, Input, Card, CardHeader, CardTitle, CardActions, CardContent, Textarea, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui';
import EventCard from '@/components/EventCard.vue'; import EventCard from '@/components/EventCard.vue';
import UserCard from '@/components/UserCard.vue'; import UserCard from '@/components/UserCard.vue';
import DispositivoCard from '@/components/DispositivoCard.vue'; import DispositivoCard from '@/components/DispositivoCard.vue';
@@ -45,7 +45,6 @@ const mainCollapsed = ref(false);
const layoutMode = ref('user'); const layoutMode = ref('user');
const theme = ref(localStorage.getItem('theme') || 'dark'); const theme = ref(localStorage.getItem('theme') || 'dark');
const statusText = ref('OK'); const statusText = ref('OK');
const showSettingsMenu = ref(false);
const showInfoModal = ref(false); const showInfoModal = ref(false);
async function fetchUsers() { async function fetchUsers() {
@@ -305,13 +304,12 @@ function openAddGuest() {
userFormModel.value = { username:'', password:'', vlan:'5', disabled:false, etiquetas: ['invitado'] }; userFormModel.value = { username:'', password:'', vlan:'5', disabled:false, etiquetas: ['invitado'] };
showUserForm.value = true; showUserForm.value = true;
} }
function toggleSettingsMenu() { showSettingsMenu.value = !showSettingsMenu.value; }
const showRawDb = ref(false); const showRawDb = ref(false);
const rawDbFullscreen = ref(false); const rawDbFullscreen = ref(false);
function openRawDb() { showSettingsMenu.value = false; showRawDb.value = true; } function openRawDb() { showRawDb.value = true; }
function closeRawDb() { showRawDb.value = false; } function closeRawDb() { showRawDb.value = false; }
const showVlan = ref(false); const showVlan = ref(false);
function openVlanForm() { showSettingsMenu.value = false; showVlan.value = true; } function openVlanForm() { showVlan.value = true; }
function closeVlan() { showVlan.value = false; } function closeVlan() { showVlan.value = false; }
function onVlanCreated() { showVlan.value = false; } function onVlanCreated() { showVlan.value = false; }
const showDevice = ref(false); const showDevice = ref(false);
@@ -426,22 +424,23 @@ async function handleUserFormSubmit(data) {
<img class="size-4 opacity-90" src="/icons/guest.svg" alt="invitado"> Invitado <img class="size-4 opacity-90" src="/icons/guest.svg" alt="invitado"> Invitado
</Button> </Button>
<UserDropdown /> <UserDropdown />
<div class="relative"> <DropdownMenu align="end">
<Button @click="toggleSettingsMenu"> <template #trigger>
<img class="size-4 opacity-90" src="/icons/settings.svg" alt="config"> Configuración <Button>
</Button> <img class="size-4 opacity-90" src="/icons/settings.svg" alt="config"> Configuración
<div v-if="showSettingsMenu" class="absolute right-0 top-full mt-1.5 glass-card p-1.5 min-w-[200px] shadow-lg border border-pink-200 dark:border-pink-600/50 z-50 space-y-1"> </Button>
<Button variant="ghost" class="w-full justify-start" @click="openRawDb">ver rawDB</Button> </template>
<Button variant="ghost" class="w-full justify-start" @click="openVlanForm">crear VLAN</Button> <DropdownMenuItem @click="openRawDb">ver rawDB</DropdownMenuItem>
<hr class="border-border my-1" /> <DropdownMenuItem @click="openVlanForm">crear VLAN</DropdownMenuItem>
<Button as="a" variant="ghost" class="w-full justify-start" href="/api/users.csv">Exportar usuarios CSV</Button> <DropdownMenuSeparator />
<Button as="a" variant="ghost" class="w-full justify-start" href="/api/devices.csv">Exportar dispositivos CSV</Button> <DropdownMenuItem as="a" href="/api/users.csv">Exportar usuarios CSV</DropdownMenuItem>
<Button as="a" variant="ghost" class="w-full justify-start" href="/api/vlans.csv">Exportar VLANs CSV</Button> <DropdownMenuItem as="a" href="/api/devices.csv">Exportar dispositivos CSV</DropdownMenuItem>
<Button variant="ghost" class="w-full justify-start" @click="openImport('users')">Importar usuarios CSV</Button> <DropdownMenuItem as="a" href="/api/vlans.csv">Exportar VLANs CSV</DropdownMenuItem>
<Button variant="ghost" class="w-full justify-start" @click="openImport('devices')">Importar dispositivos CSV</Button> <DropdownMenuSeparator />
<Button variant="ghost" class="w-full justify-start" @click="openImport('vlans')">Importar VLANs CSV</Button> <DropdownMenuItem @click="openImport('users')">Importar usuarios CSV</DropdownMenuItem>
</div> <DropdownMenuItem @click="openImport('devices')">Importar dispositivos CSV</DropdownMenuItem>
</div> <DropdownMenuItem @click="openImport('vlans')">Importar VLANs CSV</DropdownMenuItem>
</DropdownMenu>
</div> </div>
</header> </header>

View File

@@ -6,7 +6,7 @@ import {
DropdownMenuContent DropdownMenuContent
} from 'radix-vue'; } from 'radix-vue';
const props = defineProps({ defineProps({
align: { align: {
type: String, type: String,
default: 'end' default: 'end'
@@ -14,6 +14,14 @@ const props = defineProps({
side: { side: {
type: String, type: String,
default: 'bottom' default: 'bottom'
},
sideOffset: {
type: Number,
default: 6
},
collisionPadding: {
type: [Number, Object],
default: 8
} }
}); });
</script> </script>
@@ -28,7 +36,9 @@ const props = defineProps({
<DropdownMenuContent <DropdownMenuContent
:align="align" :align="align"
:side="side" :side="side"
:side-offset="6" :side-offset="sideOffset"
:collision-padding="collisionPadding"
:avoid-collisions="true"
class="z-50 min-w-[180px] glass-card p-1.5 shadow-lg border border-pink-200 dark:border-pink-600/50 animate-slide-in" class="z-50 min-w-[180px] glass-card p-1.5 shadow-lg border border-pink-200 dark:border-pink-600/50 animate-slide-in"
> >
<slot /> <slot />

View File

@@ -6,7 +6,12 @@ import { cn } from '@/lib/utils';
const props = defineProps({ const props = defineProps({
disabled: Boolean, disabled: Boolean,
danger: Boolean, danger: Boolean,
class: String class: String,
as: {
type: [String, Object],
default: undefined
},
href: String
}); });
const classes = computed(() => cn( const classes = computed(() => cn(
@@ -17,10 +22,12 @@ const classes = computed(() => cn(
props.danger && 'hover:border-red-400/30 hover:text-red-400', props.danger && 'hover:border-red-400/30 hover:text-red-400',
props.class props.class
)); ));
const elementAs = computed(() => props.as || (props.href ? 'a' : undefined));
</script> </script>
<template> <template>
<DropdownMenuItem :disabled="disabled" :class="classes"> <DropdownMenuItem :as="elementAs" :href="href" :disabled="disabled" :class="classes">
<slot /> <slot />
</DropdownMenuItem> </DropdownMenuItem>
</template> </template>