Feature: Mejorar UX del tema y persistencia
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:
2025-10-16 23:03:43 -06:00
parent 8b94e81dc8
commit 9a3dc1f0e6
3 changed files with 74 additions and 68 deletions

View File

@@ -1,7 +1,7 @@
<template>
<div class="user-header">
<!-- Header principal clickable -->
<div class="header-content" @click="openEditProfile">
<!-- Header principal -->
<div class="header-content">
<!-- Avatar -->
<div class="avatar-section">
<UAvatar
@@ -15,7 +15,12 @@
<!-- Info del usuario -->
<div class="user-info">
<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>
<div class="user-badges">
<UBadge
@@ -39,12 +44,6 @@
</UBadge>
</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>
<!-- Botón de tema -->
@@ -202,20 +201,10 @@ const handleSubmit = async () => {
box-shadow:
0 8px 32px 0 rgba(31, 38, 135, 0.15),
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);
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 {
flex-shrink: 0;
}
@@ -225,15 +214,17 @@ const handleSubmit = async () => {
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 {
flex: 1;
min-width: 0;
}
.user-name-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.user-name {
font-size: 1.5rem;
font-weight: 700;
@@ -271,28 +262,22 @@ const handleSubmit = async () => {
inset 0 1px 1px 0 rgba(255, 255, 255, 0.4);
}
.edit-hint {
.edit-button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: rgba(var(--color-primary-500), 0.1);
justify-content: center;
padding: 0.25rem;
background: transparent;
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));
opacity: 0;
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;
background: rgba(var(--color-primary-500), 0.1);
}
.theme-toggle {
@@ -348,15 +333,6 @@ const handleSubmit = async () => {
padding: 1.5rem 1rem;
}
.edit-hint {
opacity: 1;
transform: translateX(0);
}
.edit-text {
display: none;
}
.theme-toggle {
top: 0.5rem;
right: 0.5rem;

View File

@@ -122,8 +122,15 @@ const { data: applications, pending, error, refresh } = await useFetch<Applicati
default: () => []
})
// Estado de filtros
const selectedGroups = ref<string[]>([])
// Cookie para persistir filtros seleccionados
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)
const failedIcons = ref<Set<string>>(new Set())
@@ -222,8 +229,15 @@ const toggleGroup = (group: string) => {
} else {
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
const refreshInterval = setInterval(() => {
refresh()

View File

@@ -6,23 +6,43 @@
export type Theme = 'day' | 'night'
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', () => {
// Primero intentar obtener de la cookie
if (themeCookie.value) {
return themeCookie.value
}
// Si no hay cookie, detectar preferencia del sistema
if (import.meta.client && typeof window !== 'undefined') {
const saved = localStorage.getItem('theme')
if (saved === 'day' || saved === 'night') {
return saved
}
// Detectar preferencia del sistema
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
return prefersDark ? 'night' : 'day'
const systemTheme = prefersDark ? 'night' : 'day'
themeCookie.value = systemTheme
return systemTheme
}
return 'day'
})
// Computed para saber si es modo noche
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
const applyThemeClass = (theme: Theme) => {
if (import.meta.client && typeof window !== 'undefined') {
@@ -34,6 +54,7 @@ export const useTheme = () => {
html.classList.add('light')
html.classList.remove('dark')
}
updateThemeColor(theme)
}
}
@@ -46,23 +67,18 @@ export const useTheme = () => {
// Sincronizar clase cuando cambie el tema
watch(currentTheme, (newTheme) => {
applyThemeClass(newTheme)
themeCookie.value = newTheme
})
}
// Alternar tema
const toggleTheme = () => {
currentTheme.value = currentTheme.value === 'day' ? 'night' : 'day'
if (import.meta.client && typeof window !== 'undefined') {
localStorage.setItem('theme', currentTheme.value)
}
}
// Establecer tema específico
const setTheme = (theme: Theme) => {
currentTheme.value = theme
if (import.meta.client && typeof window !== 'undefined') {
localStorage.setItem('theme', theme)
}
}
return {