Files
seguidorDeLotes/nuxt-ui-tips.md
josedario87 af8dc49209
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 16s
Fix: Corregir permisos de archivos de componentes (644)
- Cambiar permisos de 600 (rw-------) a 644 (rw-r--r--)
- Esto permite que el build de Docker lea los archivos correctamente
- Agregar documentación completa de UTabs y auto-import en nuxt-ui-tips.md
2025-11-21 21:10:35 -06:00

8.8 KiB

Nuxt UI v4 - Tips y Cambios de API

Este documento contiene los cambios más comunes al migrar o trabajar con Nuxt UI v4, basado en la experiencia práctica del proyecto.

UModal - Componente Modal

Cambios de API en v4

1. v-model actualizado a v-model:open

Incorrecto (API antigua):

<UModal v-model="showModal">
  <!-- contenido -->
</UModal>

Correcto (Nuxt UI v4):

<UModal v-model:open="showModal">
  <!-- contenido -->
</UModal>

2. Slot #content requerido

En v4, el contenido debe ir dentro del slot #content.

Incorrecto:

<UModal v-model:open="showModal">
  <MiComponente />
</UModal>

Correcto:

<UModal v-model:open="showModal">
  <template #content>
    <MiComponente />
  </template>
</UModal>

3. Prop :ui para personalización

La prop :ui no tiene un slot width. Para personalizar el ancho, debes usar el slot content.

Incorrecto:

<UModal
  v-model:open="showModal"
  :ui="{ width: 'max-w-4xl' }"
>
  <!-- contenido -->
</UModal>

Correcto:

<UModal
  v-model:open="showModal"
  :ui="{ content: 'w-[calc(100vw-2rem)] max-w-4xl rounded-lg shadow-lg ring ring-default' }"
>
  <template #content>
    <!-- contenido -->
  </template>
</UModal>

Nota: Las clases del tema por defecto son:

  • w-[calc(100vw-2rem)] - ancho responsive
  • rounded-lg - bordes redondeados
  • shadow-lg - sombra
  • ring ring-default - borde/anillo

Luego agregas tu clase de ancho máximo personalizado: max-w-3xl, max-w-4xl, etc.

Slots disponibles en UModal

Según la documentación oficial de Nuxt UI v4:

interface ModalSlots {
  default(): any;      // Elemento trigger (opcional)
  content(): any;      // Contenido completo del modal
  header(): any;       // Solo el header
  title(): any;        // Solo el título
  description(): any;  // Solo la descripción
  actions(): any;      // Acciones del header
  close(): any;        // Botón de cerrar
  body(): any;         // Solo el cuerpo
  footer(): any;       // Solo el footer
}

Props importantes

interface ModalProps {
  open?: boolean;              // Estado del modal (usar con v-model:open)
  defaultOpen?: boolean;       // Estado inicial
  modal?: boolean;             // Bloquea interacción fuera (default: true)
  overlay?: boolean;           // Mostrar overlay (default: true)
  dismissible?: boolean;       // Cerrar al hacer clic fuera (default: true)
  transition?: boolean;        // Animación (default: true)
  fullscreen?: boolean;        // Pantalla completa
  scrollable?: boolean;        // Contenido scrollable (v4.2+)
  title?: string;              // Título del modal
  description?: string;        // Descripción del modal
  close?: boolean | Partial<ButtonProps>;  // Customizar botón cerrar
}

Ejemplo completo

<template>
  <!-- Modal básico con título y descripción -->
  <UModal
    v-model:open="showBasicModal"
    title="Crear Nuevo Lote"
    description="Complete el formulario para crear un lote"
  >
    <template #body>
      <LoteForm @submit="handleSubmit" />
    </template>

    <template #footer>
      <UButton @click="showBasicModal = false" variant="outline">
        Cancelar
      </UButton>
      <UButton @click="handleSubmit">
        Guardar
      </UButton>
    </template>
  </UModal>

  <!-- Modal con contenido personalizado y ancho custom -->
  <UModal
    v-model:open="showCustomModal"
    :ui="{ content: 'w-[calc(100vw-2rem)] max-w-4xl rounded-lg shadow-lg ring ring-default' }"
  >
    <template #content>
      <MiComponenteCustom />
    </template>
  </UModal>
</template>

<script setup lang="ts">
const showBasicModal = ref(false)
const showCustomModal = ref(false)

const handleSubmit = () => {
  // lógica
  showBasicModal.value = false
}
</script>

Resumen de Cambios Comunes

Componente Cambio Desde Hacia
UModal v-model v-model="show" v-model:open="show"
UModal Contenido Directo <template #content>
UModal Ancho custom :ui="{ width: 'max-w-4xl' }" :ui="{ content: 'w-[calc(100vw-2rem)] max-w-4xl ...' }"

UTabs - Componente de Pestañas

Cambios de API en v4

1. v-model usa valores string

En Nuxt UI v4, los valores de los tabs deben ser strings, no números.

Incorrecto:

<script setup>
const selectedTab = ref(0)  // ❌ Número
const tabs = [
  { label: 'Lotes', icon: 'i-heroicons-cube', slot: 'lotes' },
  { label: 'Operaciones', icon: 'i-heroicons-beaker', slot: 'operaciones' }
]
</script>

<template>
  <UTabs v-model="selectedTab" :items="tabs">
    <template #lotes>
      <!-- contenido -->
    </template>
  </UTabs>
</template>

Correcto:

<script setup>
const selectedTab = ref('lotes')  // ✅ String
const tabs = [
  { label: 'Lotes', icon: 'i-heroicons-cube', slot: 'lotes', value: 'lotes' },
  { label: 'Operaciones', icon: 'i-heroicons-beaker', slot: 'operaciones', value: 'operaciones' }
]
</script>

<template>
  <UTabs v-model="selectedTab" :items="tabs">
    <template #lotes>
      <!-- contenido -->
    </template>
  </UTabs>
</template>

2. Propiedad value en items

Cada item debe tener una propiedad value que coincida con el nombre del slot. Si no se especifica, Nuxt UI usa el índice como string ("0", "1", etc.).

Props de TabsItem:

interface TabsItem {
  label?: string
  icon?: string
  avatar?: AvatarProps
  badge?: string | number | BadgeProps
  content?: string
  value?: string | number  // ⚠️ Importante: debe coincidir con el nombre del slot
  disabled?: boolean
  slot?: string  // Nombre del slot personalizado
  class?: any
  ui?: { ... }
}

3. Uso de slots con nombres personalizados

Usa la propiedad slot en el item para definir un slot personalizado:

<script setup>
const tabs = [
  { label: 'Cuenta', slot: 'account', value: 'account' },
  { label: 'Contraseña', slot: 'password', value: 'password' }
]
</script>

<template>
  <UTabs v-model="selectedTab" :items="tabs">
    <template #account>
      <!-- Contenido del tab de cuenta -->
    </template>

    <template #password>
      <!-- Contenido del tab de contraseña -->
    </template>
  </UTabs>
</template>

Ejemplo completo

<script setup lang="ts">
import type { TabsItem } from '@nuxt/ui'

const selectedTab = ref('lotes')

const tabs: TabsItem[] = [
  {
    label: 'Lotes',
    icon: 'i-heroicons-cube',
    slot: 'lotes',
    value: 'lotes'
  },
  {
    label: 'Operaciones',
    icon: 'i-heroicons-beaker',
    slot: 'operaciones',
    value: 'operaciones'
  }
]
</script>

<template>
  <UTabs v-model="selectedTab" :items="tabs" class="mb-6">
    <template #lotes>
      <div class="py-4">
        <!-- Contenido del tab de lotes -->
      </div>
    </template>

    <template #operaciones>
      <div class="py-4">
        <!-- Contenido del tab de operaciones -->
      </div>
    </template>
  </UTabs>
</template>

Auto-import de Componentes en Nuxt

Convención de nombres para subdirectorios

Cuando organizas componentes en subdirectorios dentro de app/components/, Nuxt genera automáticamente nombres prefijados:

Estructura de archivos:

app/components/
├── lotes/
│   ├── LotesTable.vue
│   ├── LoteForm.vue
│   └── LoteCard.vue
└── operaciones/
    ├── OperacionesTable.vue
    └── OperacionForm.vue

Nombres de componentes generados:

<!--  INCORRECTO -->
<LotesTable />      <!-- No funciona -->
<LoteForm />        <!-- No funciona -->

<!--  CORRECTO -->
<LotesLotesTable />    <!-- app/components/lotes/LotesTable.vue -->
<LotesLoteForm />      <!-- app/components/lotes/LoteForm.vue -->
<LotesLoteCard />      <!-- app/components/lotes/LoteCard.vue -->
<OperacionesOperacionesTable />  <!-- app/components/operaciones/OperacionesTable.vue -->
<OperacionesOperacionForm />     <!-- app/components/operaciones/OperacionForm.vue -->

Patrón:

<DirectorioNombreArchivo />

NO se requieren imports explícitos

Nuxt hace auto-import automáticamente. NO necesitas importar componentes manualmente:

Incorrecto:

<script setup>
import LotesTable from '~/app/components/lotes/LotesTable.vue'  // ❌ Innecesario
</script>

<template>
  <LotesLotesTable />
</template>

Correcto:

<script setup>
// No se requiere import
</script>

<template>
  <LotesLotesTable />  <!-- Auto-import automático -->
</template>

Verificación con analiticaNucleo

Verificado en el proyecto analiticaNucleo que funciona correctamente:

  • app/components/comercios/TablaComerciosResumen.vue<ComerciosTablaComerciosResumen />
  • app/components/comercios/TotalesMonetariosComercio.vue<ComerciosTotalesMonetariosComercio />

Última actualización: 2025-11-21