Feature: Mejorar UX del tema y persistencia
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 55s
- Mover botón editar al lado del nombre (siempre visible, sutil) - Quitar efecto hover del header - Detectar tema del sistema operativo automáticamente - Actualizar theme-color dinámicamente (azul cielo día / oscuro noche) - Usar cookies para persistir tema y filtros (1 año) - Sincronizar filtros de apps con cookies
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-header">
|
<div class="user-header">
|
||||||
<!-- Header principal clickable -->
|
<!-- Header principal -->
|
||||||
<div class="header-content" @click="openEditProfile">
|
<div class="header-content">
|
||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div class="avatar-section">
|
<div class="avatar-section">
|
||||||
<UAvatar
|
<UAvatar
|
||||||
@@ -15,7 +15,12 @@
|
|||||||
|
|
||||||
<!-- Info del usuario -->
|
<!-- Info del usuario -->
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<h1 class="user-name">{{ user?.name || user?.username }}</h1>
|
<div class="user-name-row">
|
||||||
|
<h1 class="user-name">{{ user?.name || user?.username }}</h1>
|
||||||
|
<button class="edit-button" @click.stop="openEditProfile" title="Editar perfil">
|
||||||
|
<UIcon name="i-heroicons-pencil-square" class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p class="user-email">{{ user?.email }}</p>
|
<p class="user-email">{{ user?.email }}</p>
|
||||||
<div class="user-badges">
|
<div class="user-badges">
|
||||||
<UBadge
|
<UBadge
|
||||||
@@ -39,12 +44,6 @@
|
|||||||
</UBadge>
|
</UBadge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ícono de edición -->
|
|
||||||
<div class="edit-hint">
|
|
||||||
<UIcon name="i-heroicons-pencil-square" class="w-5 h-5" />
|
|
||||||
<span class="edit-text">Editar perfil</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botón de tema -->
|
<!-- Botón de tema -->
|
||||||
@@ -202,20 +201,10 @@ const handleSubmit = async () => {
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
0 8px 32px 0 rgba(31, 38, 135, 0.15),
|
||||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content:hover {
|
|
||||||
transform: translateY(-4px) scale(1.01);
|
|
||||||
box-shadow:
|
|
||||||
0 12px 40px 0 rgba(31, 38, 135, 0.25),
|
|
||||||
0 0 0 1px rgba(var(--color-primary-500), 0.4),
|
|
||||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
|
|
||||||
border-color: rgba(var(--color-primary-500), 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-section {
|
.avatar-section {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -225,15 +214,17 @@ const handleSubmit = async () => {
|
|||||||
transition: box-shadow 0.3s ease;
|
transition: box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content:hover .avatar-glow {
|
|
||||||
box-shadow: 0 0 30px rgba(var(--color-primary-500), 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-info {
|
.user-info {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -271,28 +262,22 @@ const handleSubmit = async () => {
|
|||||||
inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
|
inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-hint {
|
.edit-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
justify-content: center;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.25rem;
|
||||||
border-radius: 0.5rem;
|
background: transparent;
|
||||||
background: rgba(var(--color-primary-500), 0.1);
|
border: none;
|
||||||
|
color: var(--color-gray-400);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-button:hover {
|
||||||
color: rgb(var(--color-primary-500));
|
color: rgb(var(--color-primary-500));
|
||||||
opacity: 0;
|
background: rgba(var(--color-primary-500), 0.1);
|
||||||
transform: translateX(-10px);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-content:hover .edit-hint {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-text {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
font-weight: 500;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
@@ -348,15 +333,6 @@ const handleSubmit = async () => {
|
|||||||
padding: 1.5rem 1rem;
|
padding: 1.5rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-hint {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-text {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
.theme-toggle {
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
right: 0.5rem;
|
right: 0.5rem;
|
||||||
|
|||||||
@@ -122,8 +122,15 @@ const { data: applications, pending, error, refresh } = await useFetch<Applicati
|
|||||||
default: () => []
|
default: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
// Estado de filtros
|
// Cookie para persistir filtros seleccionados
|
||||||
const selectedGroups = ref<string[]>([])
|
const filtersCookie = useCookie<string[]>('app-filters', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365, // 1 año
|
||||||
|
sameSite: 'lax',
|
||||||
|
default: () => []
|
||||||
|
})
|
||||||
|
|
||||||
|
// Estado de filtros (inicializado desde cookie)
|
||||||
|
const selectedGroups = ref<string[]>(filtersCookie.value || [])
|
||||||
|
|
||||||
// Estado para iconos fallidos (para no intentar cargarlos de nuevo)
|
// Estado para iconos fallidos (para no intentar cargarlos de nuevo)
|
||||||
const failedIcons = ref<Set<string>>(new Set())
|
const failedIcons = ref<Set<string>>(new Set())
|
||||||
@@ -222,8 +229,15 @@ const toggleGroup = (group: string) => {
|
|||||||
} else {
|
} else {
|
||||||
selectedGroups.value.splice(index, 1)
|
selectedGroups.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
// Guardar en cookie
|
||||||
|
filtersCookie.value = selectedGroups.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sincronizar con cookie cuando cambian los filtros
|
||||||
|
watch(selectedGroups, (newFilters) => {
|
||||||
|
filtersCookie.value = newFilters
|
||||||
|
})
|
||||||
|
|
||||||
// Refrescar automáticamente cada 5 minutos
|
// Refrescar automáticamente cada 5 minutos
|
||||||
const refreshInterval = setInterval(() => {
|
const refreshInterval = setInterval(() => {
|
||||||
refresh()
|
refresh()
|
||||||
|
|||||||
@@ -6,23 +6,43 @@
|
|||||||
export type Theme = 'day' | 'night'
|
export type Theme = 'day' | 'night'
|
||||||
|
|
||||||
export const useTheme = () => {
|
export const useTheme = () => {
|
||||||
// Estado del tema con persistencia
|
const themeCookie = useCookie<Theme>('theme', {
|
||||||
|
maxAge: 60 * 60 * 24 * 365, // 1 año
|
||||||
|
sameSite: 'lax'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Estado del tema con persistencia en cookies
|
||||||
const currentTheme = useState<Theme>('theme', () => {
|
const currentTheme = useState<Theme>('theme', () => {
|
||||||
if (import.meta.client && typeof window !== 'undefined') {
|
// Primero intentar obtener de la cookie
|
||||||
const saved = localStorage.getItem('theme')
|
if (themeCookie.value) {
|
||||||
if (saved === 'day' || saved === 'night') {
|
return themeCookie.value
|
||||||
return saved
|
|
||||||
}
|
|
||||||
// Detectar preferencia del sistema
|
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
return prefersDark ? 'night' : 'day'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Si no hay cookie, detectar preferencia del sistema
|
||||||
|
if (import.meta.client && typeof window !== 'undefined') {
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
const systemTheme = prefersDark ? 'night' : 'day'
|
||||||
|
themeCookie.value = systemTheme
|
||||||
|
return systemTheme
|
||||||
|
}
|
||||||
|
|
||||||
return 'day'
|
return 'day'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Computed para saber si es modo noche
|
// Computed para saber si es modo noche
|
||||||
const isNight = computed(() => currentTheme.value === 'night')
|
const isNight = computed(() => currentTheme.value === 'night')
|
||||||
|
|
||||||
|
// Función para actualizar theme-color meta tag
|
||||||
|
const updateThemeColor = (theme: Theme) => {
|
||||||
|
if (import.meta.client && typeof window !== 'undefined') {
|
||||||
|
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
|
||||||
|
const color = theme === 'night' ? '#0B1026' : '#87CEEB' // Cielo nocturno / Cielo diurno
|
||||||
|
if (metaThemeColor) {
|
||||||
|
metaThemeColor.setAttribute('content', color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Función para aplicar la clase dark al HTML
|
// Función para aplicar la clase dark al HTML
|
||||||
const applyThemeClass = (theme: Theme) => {
|
const applyThemeClass = (theme: Theme) => {
|
||||||
if (import.meta.client && typeof window !== 'undefined') {
|
if (import.meta.client && typeof window !== 'undefined') {
|
||||||
@@ -34,6 +54,7 @@ export const useTheme = () => {
|
|||||||
html.classList.add('light')
|
html.classList.add('light')
|
||||||
html.classList.remove('dark')
|
html.classList.remove('dark')
|
||||||
}
|
}
|
||||||
|
updateThemeColor(theme)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,23 +67,18 @@ export const useTheme = () => {
|
|||||||
// Sincronizar clase cuando cambie el tema
|
// Sincronizar clase cuando cambie el tema
|
||||||
watch(currentTheme, (newTheme) => {
|
watch(currentTheme, (newTheme) => {
|
||||||
applyThemeClass(newTheme)
|
applyThemeClass(newTheme)
|
||||||
|
themeCookie.value = newTheme
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alternar tema
|
// Alternar tema
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
currentTheme.value = currentTheme.value === 'day' ? 'night' : 'day'
|
currentTheme.value = currentTheme.value === 'day' ? 'night' : 'day'
|
||||||
if (import.meta.client && typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem('theme', currentTheme.value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Establecer tema específico
|
// Establecer tema específico
|
||||||
const setTheme = (theme: Theme) => {
|
const setTheme = (theme: Theme) => {
|
||||||
currentTheme.value = theme
|
currentTheme.value = theme
|
||||||
if (import.meta.client && typeof window !== 'undefined') {
|
|
||||||
localStorage.setItem('theme', theme)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user