Files
perfil/nuxt4/app/components/UserProfileForm.vue
josedario87 a7d44f185d
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 56s
Agregar banner informativo sobre guardado local vs servidor
- Banner visible en header del formulario
- Explica que cambios son locales hasta guardar
- Diseño con borde izquierdo y fondo suave
- Soporte dark mode
- Mejora UX explicando comportamiento de persistencia
2025-10-17 17:51:32 -06:00

1100 lines
28 KiB
Vue

<template>
<div class="profile-form-container">
<div class="form-header">
<div class="flex items-center justify-between mb-4">
<h2 class="form-title">
<UIcon name="i-heroicons-user-circle" class="w-6 h-6" />
Editar Perfil
</h2>
<UButton
color="neutral"
variant="ghost"
icon="i-heroicons-x-mark"
@click="handleClose"
>
Cancelar
</UButton>
</div>
<!-- Banner informativo sobre guardado local -->
<div class="info-banner">
<UIcon name="i-heroicons-information-circle" class="w-5 h-5 flex-shrink-0" />
<p class="info-banner-text">
Los cambios que realices se guardan <strong>automáticamente en tu navegador</strong>.
Para que sean permanentes y se sincronicen con el servidor, debes presionar <strong>"Guardar cambios"</strong>.
</p>
</div>
</div>
<form @submit.prevent="handleSubmit" class="form-content">
<!-- Sección: Foto de Perfil -->
<div class="form-section">
<h3 class="section-title">
<UIcon name="i-heroicons-camera" class="w-5 h-5" />
Foto de Perfil
</h3>
<div class="avatar-actions-simple">
<UButton
@click="showCamera = true"
color="primary"
size="sm"
:disabled="isUploading"
>
<UIcon name="i-heroicons-camera" class="w-4 h-4" />
{{ currentAvatar && currentAvatar.includes('/avatars/') ? 'Cambiar foto' : 'Tomar foto' }}
</UButton>
<UButton
v-if="currentAvatar && currentAvatar.includes('/avatars/')"
@click="removeAvatar"
color="neutral"
variant="soft"
size="sm"
:disabled="isUploading"
>
<UIcon name="i-heroicons-trash" class="w-4 h-4" />
Eliminar
</UButton>
</div>
</div>
<!-- Sección: Información Básica -->
<div class="form-section">
<h3 class="section-title">
<UIcon name="i-heroicons-identification" class="w-5 h-5" />
Información Básica
</h3>
<div class="form-grid">
<!-- Nombre de usuario (readonly) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-at-symbol" class="w-4 h-4" />
Nombre de usuario
</label>
<UInput
:model-value="user?.username"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">No se puede cambiar</span>
</div>
<!-- UID de Authentik (readonly) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-finger-print" class="w-4 h-4" />
UID de Authentik
</label>
<UInput
:model-value="user?.uid || 'No disponible'"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Identificador único del sistema</span>
</div>
<!-- Nombre completo -->
<div class="form-field">
<label class="field-label" :class="{ 'field-modified': modifiedFields.name }">
<UIcon name="i-heroicons-user" class="w-4 h-4" />
Nombre completo
<span class="required">*</span>
<span v-if="modifiedFields.name" class="modified-indicator">
<UIcon name="i-heroicons-pencil-square" class="w-3 h-3" />
Modificado
</span>
</label>
<UInput
v-model="formData.name"
placeholder="Tu nombre completo"
:disabled="isSubmitting"
/>
</div>
<!-- Email -->
<div class="form-field">
<label class="field-label" :class="{ 'field-modified': modifiedFields.email }">
<UIcon name="i-heroicons-envelope" class="w-4 h-4" />
Correo electrónico
<span class="required">*</span>
<span v-if="modifiedFields.email" class="modified-indicator">
<UIcon name="i-heroicons-pencil-square" class="w-3 h-3" />
Modificado
</span>
</label>
<UInput
v-model="formData.email"
type="email"
placeholder="tu@email.com"
:disabled="isSubmitting"
/>
</div>
</div>
</div>
<!-- Sección: Conectividad -->
<div class="form-section">
<h3 class="section-title">
<UIcon name="i-heroicons-wifi" class="w-5 h-5" />
Conectividad
</h3>
<div class="form-grid">
<!-- Contraseña WiFi (readonly) -->
<div class="form-field full-width">
<label class="field-label">
<UIcon name="i-heroicons-key" class="w-4 h-4" />
Contraseña WiFi Nucleo
</label>
<UInput
value="Nucleo2024WiFi!"
disabled
type="text"
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Contraseña de la red WiFi de Nucleo</span>
</div>
</div>
</div>
<!-- Sección: Información Personal -->
<div class="form-section">
<h3 class="section-title">
<UIcon name="i-heroicons-user-circle" class="w-5 h-5" />
Información Personal
</h3>
<div class="form-grid">
<!-- Teléfono (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-phone" class="w-4 h-4" />
Teléfono
</label>
<UInput
v-model="formData.phone"
placeholder="+506 1234-5678"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
<!-- Cédula (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-identification" class="w-4 h-4" />
Cédula
</label>
<UInput
v-model="formData.cedula"
placeholder="1-2345-6789"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
<!-- Fecha de nacimiento (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-cake" class="w-4 h-4" />
Fecha de nacimiento
</label>
<UInput
v-model="formData.birthdate"
type="date"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
</div>
</div>
<!-- Sección: Seguridad y Acceso -->
<div class="form-section">
<h3 class="section-title">
<UIcon name="i-heroicons-shield-check" class="w-5 h-5" />
Seguridad y Acceso
</h3>
<div class="form-grid">
<!-- NFC vinculada (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-credit-card" class="w-4 h-4" />
NFC vinculada
</label>
<UInput
v-model="formData.nfc"
placeholder="ID de tarjeta NFC"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
<!-- PIN numérico (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-lock-closed" class="w-4 h-4" />
PIN numérico
</label>
<UInput
v-model="formData.pin"
type="password"
placeholder="••••"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
<!-- Código Nucleo V2 (deshabilitado) -->
<div class="form-field">
<label class="field-label">
<UIcon name="i-heroicons-qr-code" class="w-4 h-4" />
Código Nucleo V2
</label>
<UInput
v-model="formData.nucleoCode"
placeholder="NUCLEO-XXXX-XXXX"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<span class="field-help">Próximamente disponible</span>
</div>
</div>
</div>
<!-- Botones de acción -->
<div class="form-actions">
<UButton
color="neutral"
variant="ghost"
size="lg"
@click="handleClose"
:disabled="isSubmitting"
>
Cancelar
</UButton>
<UButton
v-if="hasChanges"
color="warning"
variant="soft"
size="lg"
@click="resetForm"
:disabled="isSubmitting"
>
<UIcon name="i-heroicons-arrow-path" class="w-5 h-5" />
Reiniciar
</UButton>
<UButton
color="primary"
size="lg"
type="submit"
:loading="isSubmitting"
>
Guardar cambios
</UButton>
</div>
</form>
<!-- Modal de cámara -->
<UModal v-model:open="showCamera">
<template #content>
<div class="camera-modal max-w-3xl mx-auto">
<div class="modal-header">
<h3 class="modal-title">
<UIcon name="i-heroicons-camera" class="w-6 h-6" />
Tomar foto de perfil
</h3>
</div>
<div class="modal-content">
<CameraCapture
@capture="handleAvatarCapture"
@cancel="showCamera = false"
/>
</div>
</div>
</template>
</UModal>
<!-- Modal de confirmación de salida -->
<UModal v-model:open="showExitConfirm">
<template #content>
<div class="exit-confirm-modal">
<div class="exit-modal-header">
<UIcon name="i-heroicons-exclamation-triangle" class="w-12 h-12 text-warning-500" />
<h3 class="exit-modal-title">¿Estás seguro de salir?</h3>
</div>
<div class="exit-modal-body">
<p class="exit-modal-message">
Tienes cambios pendientes en:
</p>
<ul class="exit-modal-fields">
<li v-for="field in modifiedFieldsList" :key="field" class="exit-field-item">
<UIcon name="i-heroicons-pencil-square" class="w-4 h-4 text-warning-500" />
{{ field }}
</li>
</ul>
<div class="exit-modal-notice">
<UIcon name="i-heroicons-information-circle" class="w-5 h-5" />
<p>
Estos cambios quedarán guardados <strong>localmente en tu navegador</strong>,
pero <strong class="text-error-600 dark:text-error-400">NO han sido enviados al servidor</strong>.
</p>
</div>
<div class="exit-modal-help">
<UIcon name="i-heroicons-light-bulb" class="w-5 h-5 text-primary-500" />
<p>
Para guardar definitivamente, haz clic en <strong>"Guardar cambios"</strong>.
</p>
</div>
</div>
<div class="exit-modal-actions">
<UButton
color="neutral"
variant="soft"
size="lg"
@click="showExitConfirm = false"
>
<UIcon name="i-heroicons-arrow-left" class="w-5 h-5" />
Continuar editando
</UButton>
<UButton
color="error"
size="lg"
@click="confirmExit"
>
<UIcon name="i-heroicons-arrow-right-on-rectangle" class="w-5 h-5" />
Salir sin guardar
</UButton>
</div>
</div>
</template>
</UModal>
</div>
</template>
<script setup lang="ts">
const { user } = useAuthentik()
const toast = useToast()
// Emits
const emit = defineEmits(['close'])
// Estado del formulario
const isSubmitting = ref(false)
const isUploading = ref(false)
const showCamera = ref(false)
const showExitConfirm = ref(false)
// Cookie para persistir cambios del formulario
const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
maxAge: 60 * 60 * 24 * 7, // 7 días
sameSite: 'lax',
default: () => ({})
})
// Datos originales del usuario (para comparar)
const originalData = ref({
name: user.value?.name || '',
email: user.value?.email || '',
avatar: '',
phone: '',
cedula: '',
birthdate: '',
nfc: '',
pin: '',
nucleoCode: ''
})
// Datos del formulario (restaurar desde cookie si existe)
const formData = ref({
name: formCookie.value.name || user.value?.name || '',
email: formCookie.value.email || user.value?.email || '',
avatar: formCookie.value.avatar || '',
phone: formCookie.value.phone || '',
cedula: formCookie.value.cedula || '',
birthdate: formCookie.value.birthdate || '',
nfc: formCookie.value.nfc || '',
pin: formCookie.value.pin || '',
nucleoCode: formCookie.value.nucleoCode || ''
})
// Avatar actual del usuario
const currentAvatar = ref(user.value?.avatar || '')
// Detectar qué campos han sido modificados
const modifiedFields = computed(() => {
const modified: Record<string, boolean> = {}
const keys = Object.keys(formData.value) as Array<keyof typeof formData.value>
keys.forEach(key => {
modified[key] = formData.value[key] !== originalData.value[key]
})
return modified
})
// Verificar si hay cambios pendientes
const hasChanges = computed(() => {
return Object.values(modifiedFields.value).some(modified => modified)
})
// Lista de campos modificados (para mostrar en alerta)
const modifiedFieldsList = computed(() => {
const fieldNames: Record<string, string> = {
name: 'Nombre completo',
email: 'Correo electrónico',
phone: 'Teléfono',
cedula: 'Cédula',
birthdate: 'Fecha de nacimiento',
nfc: 'NFC vinculada',
pin: 'PIN numérico',
nucleoCode: 'Código Nucleo V2'
}
return Object.entries(modifiedFields.value)
.filter(([_, modified]) => modified)
.map(([field, _]) => fieldNames[field] || field)
})
// Guardar en cookie cuando cambian los datos
watch(formData, (newData) => {
formCookie.value = { ...newData }
}, { deep: true })
// Actualizar avatar cuando cambie el usuario
watch(() => user.value?.avatar, (newAvatar) => {
if (newAvatar) {
currentAvatar.value = newAvatar
}
})
// Alertar al usuario antes de salir si hay cambios
if (import.meta.client) {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (hasChanges.value) {
const message = `¿Estás seguro de salir?\n\nTienes cambios pendientes en: ${modifiedFieldsList.value.join(', ')}\n\nEstos cambios quedarán guardados localmente en tu navegador, pero NO han sido enviados al servidor.\n\nPara guardar definitivamente, debes hacer clic en "Guardar cambios".`
e.preventDefault()
e.returnValue = message
return message
}
}
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
// Mostrar notificación si hay datos restaurados
if (Object.keys(formCookie.value).length > 0 && hasChanges.value) {
toast.add({
title: 'Borrador restaurado',
description: `Se restauraron cambios pendientes en: ${modifiedFieldsList.value.join(', ')}`,
color: 'info',
icon: 'i-heroicons-document-text'
})
}
})
onUnmounted(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
}
// Enviar formulario
const handleSubmit = async () => {
if (!formData.value.name || !formData.value.email) {
toast.add({
title: 'Error',
description: 'Por favor completa todos los campos requeridos',
color: 'error',
icon: 'i-heroicons-exclamation-triangle'
})
return
}
isSubmitting.value = true
try {
await $fetch('/api/authentik/user', {
method: 'PATCH',
body: {
name: formData.value.name,
email: formData.value.email
}
})
// Limpiar cookie de borrador al guardar exitosamente
formCookie.value = {}
// Actualizar datos originales
originalData.value.name = formData.value.name
originalData.value.email = formData.value.email
toast.add({
title: 'Perfil actualizado',
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
color: 'success',
icon: 'i-heroicons-check-circle',
actions: [{
label: 'Recargar',
onClick: () => window.location.reload()
}]
})
} catch (error) {
console.error('Error updating profile:', error)
toast.add({
title: 'Error',
description: 'No se pudo actualizar el perfil',
color: 'error',
icon: 'i-heroicons-x-circle'
})
} finally {
isSubmitting.value = false
}
}
// Manejar captura de avatar desde cámara
const handleAvatarCapture = async (imageBlob: Blob) => {
isUploading.value = true
showCamera.value = false
try {
// Crear FormData para subir el archivo
const formData = new FormData()
formData.append('avatar', imageBlob, 'avatar.jpg')
// Subir avatar
const response = await $fetch<{ success: boolean; avatarUrl: string; message: string }>('/api/avatar/upload', {
method: 'POST',
body: formData
})
if (response.success) {
// Actualizar preview del avatar
currentAvatar.value = response.avatarUrl
toast.add({
title: 'Avatar actualizado',
description: response.message,
color: 'success',
icon: 'i-heroicons-check-circle',
actions: [{
label: 'Recargar',
onClick: () => window.location.reload()
}]
})
}
} catch (error: any) {
console.error('Error uploading avatar:', error)
toast.add({
title: 'Error',
description: error.data?.message || 'No se pudo subir el avatar',
color: 'error',
icon: 'i-heroicons-x-circle'
})
} finally {
isUploading.value = false
}
}
// Manejar intento de cerrar formulario
const handleClose = () => {
if (hasChanges.value) {
// Si hay cambios, mostrar modal de confirmación
showExitConfirm.value = true
} else {
// Si no hay cambios, cerrar directamente
emit('close')
}
}
// Confirmar salida sin guardar
const confirmExit = () => {
showExitConfirm.value = false
emit('close')
}
// Reiniciar formulario a valores originales
const resetForm = () => {
if (!confirm('¿Estás seguro de descartar todos los cambios?\n\nSe restaurarán los valores originales y se eliminará el borrador guardado.')) {
return
}
// Restaurar valores originales
formData.value = {
name: originalData.value.name,
email: originalData.value.email,
avatar: originalData.value.avatar,
phone: originalData.value.phone,
cedula: originalData.value.cedula,
birthdate: originalData.value.birthdate,
nfc: originalData.value.nfc,
pin: originalData.value.pin,
nucleoCode: originalData.value.nucleoCode
}
// Limpiar cookie de borrador
formCookie.value = {}
toast.add({
title: 'Formulario reiniciado',
description: 'Se restauraron los valores originales y se eliminó el borrador',
color: 'info',
icon: 'i-heroicons-arrow-path'
})
}
// Eliminar avatar personalizado
const removeAvatar = async () => {
if (!confirm('¿Estás seguro de que quieres eliminar tu foto de perfil?')) {
return
}
isUploading.value = true
try {
// Actualizar Authentik para eliminar avatar_url
await $fetch('/api/authentik/user', {
method: 'PATCH',
body: {
name: formData.value.name,
email: formData.value.email,
removeAvatar: true
}
})
// Volver al avatar por defecto
currentAvatar.value = user.value?.avatar || ''
toast.add({
title: 'Avatar eliminado',
description: 'Se restauró el avatar por defecto. Recarga para ver los cambios.',
color: 'success',
icon: 'i-heroicons-check-circle',
actions: [{
label: 'Recargar',
onClick: () => window.location.reload()
}]
})
} catch (error) {
console.error('Error removing avatar:', error)
toast.add({
title: 'Error',
description: 'No se pudo eliminar el avatar',
color: 'error',
icon: 'i-heroicons-x-circle'
})
} finally {
isUploading.value = false
}
}
</script>
<style scoped>
.profile-form-container {
background: rgba(255, 255, 255, 0.35);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 1.5rem;
padding: 2rem;
box-shadow:
0 8px 32px 0 rgba(31, 38, 135, 0.15),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.form-header {
margin-bottom: 1.5rem;
}
.info-banner {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.875rem 1.125rem;
background: rgba(0, 150, 255, 0.08);
border-left: 3px solid rgb(var(--color-primary-500));
border-radius: 0.625rem;
margin-top: 1rem;
}
.info-banner-text {
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-700);
margin: 0;
}
.info-banner-text strong {
color: var(--color-gray-900);
font-weight: 600;
}
.form-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.75rem;
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.form-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.form-section {
display: flex;
flex-direction: column;
gap: 1rem;
}
.section-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0 0 0.5rem 0;
padding-bottom: 0.75rem;
border-bottom: 2px solid rgba(var(--color-primary-500), 0.2);
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.form-field {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-field.full-width {
grid-column: 1 / -1;
}
.field-label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--color-gray-700);
transition: color 0.3s ease;
}
.field-label.field-modified {
color: rgb(var(--color-warning-600));
}
.field-label .required {
color: rgb(var(--color-error-500));
margin-left: 0.125rem;
}
.modified-indicator {
display: inline-flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
padding: 0.125rem 0.5rem;
font-size: 0.6875rem;
font-weight: 600;
border-radius: 0.375rem;
background: rgba(var(--color-warning-500), 0.15);
color: rgb(var(--color-warning-600));
border: 1px solid rgba(var(--color-warning-500), 0.3);
}
.field-help {
font-size: 0.75rem;
color: var(--color-gray-500);
font-style: italic;
}
.avatar-actions-simple {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.camera-modal {
padding: 1.5rem;
}
.modal-header {
margin-bottom: 1.5rem;
}
.modal-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--color-gray-900);
}
.modal-content {
max-height: 70vh;
overflow-y: auto;
}
/* Modal de confirmación de salida */
.exit-confirm-modal {
padding: 2rem;
max-width: 600px;
margin: 0 auto;
}
.exit-modal-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
text-align: center;
}
.exit-modal-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.exit-modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.exit-modal-message {
font-size: 1rem;
font-weight: 600;
color: var(--color-gray-800);
margin: 0;
}
.exit-modal-fields {
list-style: none;
padding: 1rem;
margin: 0;
background: rgba(var(--color-warning-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-warning-500), 0.3);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.exit-field-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-gray-700);
}
.exit-modal-notice {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(var(--color-error-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-error-500), 0.3);
}
.exit-modal-notice p {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-700);
}
.exit-modal-help {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: rgba(var(--color-primary-500), 0.1);
border-radius: 0.75rem;
border: 1px solid rgba(var(--color-primary-500), 0.3);
}
.exit-modal-help p {
margin: 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-700);
}
.exit-modal-actions {
display: flex;
gap: 1rem;
justify-content: center;
}
@media (max-width: 640px) {
.exit-confirm-modal {
padding: 1.5rem;
}
.exit-modal-title {
font-size: 1.25rem;
}
.exit-modal-actions {
flex-direction: column-reverse;
}
.exit-modal-actions button {
width: 100%;
}
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
/* Responsive */
@media (max-width: 768px) {
.profile-form-container {
padding: 1.5rem;
}
.form-title {
font-size: 1.5rem;
}
.section-title {
font-size: 1rem;
}
.form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.form-field.full-width {
grid-column: 1;
}
.form-actions {
flex-direction: column;
}
}
</style>
<style>
/* Estilos de modo oscuro (sin scoped para que .dark funcione correctamente) */
.dark .profile-form-container {
background: rgba(0, 0, 0, 0.15) !important;
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.5),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
}
.dark .form-title {
color: var(--color-gray-100) !important;
}
.dark .info-banner {
background: rgba(0, 150, 255, 0.12) !important;
border-left-color: rgb(var(--color-primary-400)) !important;
}
.dark .info-banner-text {
color: var(--color-gray-300) !important;
}
.dark .info-banner-text strong {
color: var(--color-gray-100) !important;
}
.dark .section-title {
color: var(--color-gray-100) !important;
border-bottom-color: rgba(var(--color-primary-500), 0.3) !important;
}
.dark .field-label {
color: var(--color-gray-300) !important;
}
.dark .field-label.field-modified {
color: rgb(var(--color-warning-400)) !important;
}
.dark .modified-indicator {
background: rgba(var(--color-warning-500), 0.2) !important;
color: rgb(var(--color-warning-400)) !important;
border-color: rgba(var(--color-warning-500), 0.4) !important;
}
.dark .field-help {
color: var(--color-gray-400) !important;
}
.dark .form-actions {
border-top-color: rgba(255, 255, 255, 0.1);
}
.dark .modal-title {
color: var(--color-gray-100) !important;
}
.dark .exit-modal-title {
color: var(--color-gray-100) !important;
}
.dark .exit-modal-message {
color: var(--color-gray-200) !important;
}
.dark .exit-field-item {
color: var(--color-gray-300) !important;
}
.dark .exit-modal-fields {
background: rgba(var(--color-warning-500), 0.15) !important;
border-color: rgba(var(--color-warning-500), 0.4) !important;
}
.dark .exit-modal-notice {
background: rgba(var(--color-error-500), 0.15) !important;
border-color: rgba(var(--color-error-500), 0.4) !important;
}
.dark .exit-modal-notice p {
color: var(--color-gray-300) !important;
}
.dark .exit-modal-help {
background: rgba(var(--color-primary-500), 0.15) !important;
border-color: rgba(var(--color-primary-500), 0.4) !important;
}
.dark .exit-modal-help p {
color: var(--color-gray-300) !important;
}
</style>