All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s
Se añade campo de entrada para pegar URLs de imágenes directamente, permitiendo cargar avatares desde enlaces externos. Incluye validación de URLs, descarga de imágenes con manejo de errores CORS, validación de tipo y tamaño (máx 5MB), con feedback visual de errores específicos.
1478 lines
38 KiB
Vue
1478 lines
38 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>
|
|
|
|
<!-- Zona de drag & drop -->
|
|
<div
|
|
class="avatar-dropzone"
|
|
:class="{ 'dropzone-active': isDragging }"
|
|
@drop.prevent="handleDrop"
|
|
@dragover.prevent="handleDragOver"
|
|
@dragleave="handleDragLeave"
|
|
>
|
|
<UIcon name="i-heroicons-photo" class="w-12 h-12 dropzone-icon" />
|
|
<p class="dropzone-text">
|
|
Arrastra una imagen aquí
|
|
</p>
|
|
<span class="dropzone-hint">o usa los botones de abajo</span>
|
|
</div>
|
|
|
|
<!-- Campo para URL de imagen -->
|
|
<div class="avatar-url-section">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<UIcon name="i-heroicons-link" class="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">O pega una URL de imagen</span>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<UInput
|
|
v-model="avatarUrl"
|
|
placeholder="https://ejemplo.com/imagen.jpg"
|
|
:disabled="isUploading"
|
|
class="flex-1"
|
|
/>
|
|
<UButton
|
|
@click="loadAvatarFromUrl"
|
|
color="primary"
|
|
variant="outline"
|
|
:disabled="isUploading || !avatarUrl"
|
|
:loading="isLoadingUrl"
|
|
>
|
|
<UIcon name="i-heroicons-arrow-down-tray" class="w-4 h-4" />
|
|
Cargar
|
|
</UButton>
|
|
</div>
|
|
<span v-if="urlError" class="text-xs text-red-500 mt-1">{{ urlError }}</span>
|
|
</div>
|
|
|
|
<!-- Botones de acción -->
|
|
<div class="avatar-actions">
|
|
<input
|
|
ref="fileInput"
|
|
type="file"
|
|
accept="image/*"
|
|
@change="handleFileSelect"
|
|
style="display: none"
|
|
/>
|
|
|
|
<UButton
|
|
@click="showCamera = true"
|
|
color="primary"
|
|
size="sm"
|
|
:disabled="isUploading"
|
|
>
|
|
<UIcon name="i-heroicons-camera" class="w-4 h-4" />
|
|
Tomar foto
|
|
</UButton>
|
|
|
|
<UButton
|
|
@click="triggerFileInput"
|
|
color="primary"
|
|
variant="soft"
|
|
size="sm"
|
|
:disabled="isUploading"
|
|
>
|
|
<UIcon name="i-heroicons-arrow-up-tray" class="w-4 h-4" />
|
|
Subir desde dispositivo
|
|
</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()
|
|
|
|
// Props
|
|
const props = defineProps<{
|
|
sharedImageUrl?: string | null
|
|
}>()
|
|
|
|
// Emits
|
|
const emit = defineEmits(['close'])
|
|
|
|
// Estado del formulario
|
|
const isSubmitting = ref(false)
|
|
const isUploading = ref(false)
|
|
const showCamera = ref(false)
|
|
const showExitConfirm = ref(false)
|
|
const isDragging = ref(false)
|
|
const isLoadingUrl = ref(false)
|
|
|
|
// Ref para el input de archivo
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
|
|
// URL del avatar
|
|
const avatarUrl = ref('')
|
|
const urlError = ref('')
|
|
|
|
// 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)
|
|
})
|
|
|
|
// Detectar y procesar imagen compartida automáticamente
|
|
watch(() => props.sharedImageUrl, async (newUrl) => {
|
|
if (newUrl && import.meta.client) {
|
|
await processSharedImage(newUrl)
|
|
}
|
|
}, { immediate: true })
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// Abrir selector de archivos
|
|
const triggerFileInput = () => {
|
|
fileInput.value?.click()
|
|
}
|
|
|
|
// Manejar selección de archivo desde input
|
|
const handleFileSelect = (event: Event) => {
|
|
const input = event.target as HTMLInputElement
|
|
const file = input.files?.[0]
|
|
if (file) {
|
|
processImageFile(file)
|
|
}
|
|
// Limpiar input para permitir seleccionar el mismo archivo de nuevo
|
|
input.value = ''
|
|
}
|
|
|
|
// Manejar drag over
|
|
const handleDragOver = (event: DragEvent) => {
|
|
event.preventDefault()
|
|
isDragging.value = true
|
|
}
|
|
|
|
// Manejar drag leave
|
|
const handleDragLeave = () => {
|
|
isDragging.value = false
|
|
}
|
|
|
|
// Manejar drop
|
|
const handleDrop = (event: DragEvent) => {
|
|
event.preventDefault()
|
|
isDragging.value = false
|
|
|
|
const file = event.dataTransfer?.files[0]
|
|
if (file) {
|
|
processImageFile(file)
|
|
}
|
|
}
|
|
|
|
// Procesar archivo de imagen
|
|
const processImageFile = async (file: File) => {
|
|
// Validar que sea una imagen
|
|
if (!file.type.startsWith('image/')) {
|
|
toast.add({
|
|
title: 'Archivo inválido',
|
|
description: 'Por favor selecciona un archivo de imagen',
|
|
color: 'error',
|
|
icon: 'i-heroicons-exclamation-triangle'
|
|
})
|
|
return
|
|
}
|
|
|
|
// Validar tamaño (máximo 5MB)
|
|
const maxSize = 5 * 1024 * 1024 // 5MB
|
|
if (file.size > maxSize) {
|
|
toast.add({
|
|
title: 'Archivo muy grande',
|
|
description: 'La imagen debe ser menor a 5MB',
|
|
color: 'error',
|
|
icon: 'i-heroicons-exclamation-triangle'
|
|
})
|
|
return
|
|
}
|
|
|
|
// Convertir a Blob y subir
|
|
const blob = new Blob([file], { type: file.type })
|
|
await handleAvatarCapture(blob)
|
|
}
|
|
|
|
// Cargar avatar desde URL
|
|
const loadAvatarFromUrl = async () => {
|
|
urlError.value = ''
|
|
|
|
// Validar que la URL no esté vacía
|
|
if (!avatarUrl.value.trim()) {
|
|
urlError.value = 'Por favor ingresa una URL'
|
|
return
|
|
}
|
|
|
|
// Validar formato de URL
|
|
try {
|
|
new URL(avatarUrl.value)
|
|
} catch (error) {
|
|
urlError.value = 'URL inválida. Por favor ingresa una URL válida (ej: https://ejemplo.com/imagen.jpg)'
|
|
return
|
|
}
|
|
|
|
// Validar que la URL sea https
|
|
if (!avatarUrl.value.startsWith('https://') && !avatarUrl.value.startsWith('http://')) {
|
|
urlError.value = 'La URL debe comenzar con http:// o https://'
|
|
return
|
|
}
|
|
|
|
isLoadingUrl.value = true
|
|
isUploading.value = true
|
|
|
|
try {
|
|
// Descargar la imagen
|
|
const response = await fetch(avatarUrl.value, {
|
|
mode: 'cors'
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Error al descargar la imagen: ${response.statusText}`)
|
|
}
|
|
|
|
const blob = await response.blob()
|
|
|
|
// Validar que sea una imagen
|
|
if (!blob.type.startsWith('image/')) {
|
|
throw new Error('El archivo en la URL no es una imagen válida')
|
|
}
|
|
|
|
// Validar tamaño (máximo 5MB)
|
|
const maxSize = 5 * 1024 * 1024 // 5MB
|
|
if (blob.size > maxSize) {
|
|
throw new Error('La imagen debe ser menor a 5MB')
|
|
}
|
|
|
|
// Procesar la imagen
|
|
await handleAvatarCapture(blob)
|
|
|
|
// Limpiar el campo de URL y errores
|
|
avatarUrl.value = ''
|
|
urlError.value = ''
|
|
|
|
toast.add({
|
|
title: 'Imagen cargada',
|
|
description: 'La imagen se descargó correctamente desde la URL',
|
|
color: 'success',
|
|
icon: 'i-heroicons-check-circle'
|
|
})
|
|
} catch (error: any) {
|
|
console.error('Error loading image from URL:', error)
|
|
|
|
// Manejar errores de CORS
|
|
if (error.message.includes('CORS') || error.message.includes('NetworkError')) {
|
|
urlError.value = 'No se pudo acceder a la imagen. Puede que el servidor no permita descargas externas (CORS). Intenta descargar la imagen manualmente y subirla desde tu dispositivo.'
|
|
} else {
|
|
urlError.value = error.message || 'No se pudo cargar la imagen desde la URL'
|
|
}
|
|
|
|
toast.add({
|
|
title: 'Error',
|
|
description: error.message || 'No se pudo cargar la imagen desde la URL',
|
|
color: 'error',
|
|
icon: 'i-heroicons-exclamation-triangle'
|
|
})
|
|
} finally {
|
|
isLoadingUrl.value = false
|
|
isUploading.value = false
|
|
}
|
|
}
|
|
|
|
// Procesar imagen compartida desde Web Share Target
|
|
const processSharedImage = async (imageUrl: string) => {
|
|
try {
|
|
// Mostrar notificación
|
|
toast.add({
|
|
title: 'Imagen compartida recibida',
|
|
description: 'Procesando imagen para tu avatar...',
|
|
color: 'info',
|
|
icon: 'i-heroicons-photo'
|
|
})
|
|
|
|
// Descargar la imagen del servidor
|
|
const response = await fetch(imageUrl)
|
|
|
|
if (!response.ok) {
|
|
throw new Error('No se pudo cargar la imagen compartida')
|
|
}
|
|
|
|
const blob = await response.blob()
|
|
|
|
// Validar que sea una imagen
|
|
if (!blob.type.startsWith('image/')) {
|
|
throw new Error('El archivo compartido no es una imagen válida')
|
|
}
|
|
|
|
// Procesar y subir la imagen
|
|
await handleAvatarCapture(blob)
|
|
|
|
// Limpiar archivo temporal del servidor
|
|
try {
|
|
await $fetch('/api/share-target/cleanup', {
|
|
method: 'POST',
|
|
body: { imageUrl }
|
|
})
|
|
} catch (cleanupError) {
|
|
console.error('Error limpiando archivo temporal:', cleanupError)
|
|
// No es crítico si falla la limpieza
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error procesando imagen compartida:', error)
|
|
toast.add({
|
|
title: 'Error',
|
|
description: error.message || 'No se pudo procesar la imagen compartida',
|
|
color: 'error',
|
|
icon: 'i-heroicons-exclamation-triangle'
|
|
})
|
|
}
|
|
}
|
|
</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;
|
|
}
|
|
|
|
/* Zona de drag & drop para avatar */
|
|
.avatar-dropzone {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.75rem;
|
|
padding: 2rem;
|
|
border: 2px dashed rgba(var(--color-primary-500), 0.3);
|
|
border-radius: 1rem;
|
|
background: rgba(var(--color-primary-500), 0.05);
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.avatar-dropzone:hover {
|
|
border-color: rgba(var(--color-primary-500), 0.5);
|
|
background: rgba(var(--color-primary-500), 0.1);
|
|
}
|
|
|
|
.avatar-dropzone.dropzone-active {
|
|
border-color: rgb(var(--color-primary-500));
|
|
background: rgba(var(--color-primary-500), 0.15);
|
|
border-style: solid;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.dropzone-icon {
|
|
color: rgb(var(--color-primary-500));
|
|
opacity: 0.6;
|
|
transition: opacity 0.3s ease;
|
|
}
|
|
|
|
.avatar-dropzone:hover .dropzone-icon,
|
|
.avatar-dropzone.dropzone-active .dropzone-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
.dropzone-text {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: var(--color-gray-700);
|
|
margin: 0;
|
|
}
|
|
|
|
.dropzone-hint {
|
|
font-size: 0.875rem;
|
|
color: var(--color-gray-500);
|
|
font-style: italic;
|
|
}
|
|
|
|
.avatar-url-section {
|
|
margin-top: 1rem;
|
|
padding: 1rem;
|
|
background: rgba(var(--color-gray-100), 0.5);
|
|
border-radius: 0.75rem;
|
|
border: 1px solid rgba(var(--color-gray-300), 0.5);
|
|
}
|
|
|
|
.avatar-actions {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
flex-wrap: wrap;
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.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 .avatar-dropzone {
|
|
border-color: rgba(var(--color-primary-400), 0.3) !important;
|
|
background: rgba(var(--color-primary-500), 0.08) !important;
|
|
}
|
|
|
|
.dark .avatar-dropzone:hover {
|
|
border-color: rgba(var(--color-primary-400), 0.5) !important;
|
|
background: rgba(var(--color-primary-500), 0.12) !important;
|
|
}
|
|
|
|
.dark .avatar-dropzone.dropzone-active {
|
|
border-color: rgb(var(--color-primary-400)) !important;
|
|
background: rgba(var(--color-primary-500), 0.18) !important;
|
|
}
|
|
|
|
.dark .dropzone-icon {
|
|
color: rgb(var(--color-primary-400)) !important;
|
|
}
|
|
|
|
.dark .dropzone-text {
|
|
color: var(--color-gray-200) !important;
|
|
}
|
|
|
|
.dark .dropzone-hint {
|
|
color: var(--color-gray-400) !important;
|
|
}
|
|
|
|
.dark .avatar-url-section {
|
|
background: rgba(255, 255, 255, 0.03) !important;
|
|
border-color: rgba(255, 255, 255, 0.1) !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>
|