Mejorar diseño de UserCard con avatares, iconos y mejor jerarquía visual
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 24s

- Agregar avatar circular con inicial del usuario y estados visuales (normal, conectado, deshabilitado)
- Implementar iconos SVG en todos los botones de acción (editar, desconectar, habilitar/deshabilitar, eliminar, expandir)
- Mejorar jerarquía de información con chips de colores específicos (VLAN azul, activo verde, deshabilitado rojo, conectado rosa)
- Agregar animación pulse en avatar cuando el usuario está conectado
- Implementar efectos hover con elevación y sombras rosas
- Organizar etiquetas y dispositivos en secciones separadas con bordes
- Resolver conflictos de merge en styles.css (mantener estilos de scrollbar y gradientes de topbar/panel)
- Agregar responsive design para móviles
- Mejorar transiciones y efectos glassmorphism en toda la card
This commit is contained in:
2025-10-28 10:04:40 -06:00
parent b7503bb118
commit 37fdcaec9c
2 changed files with 248 additions and 33 deletions

View File

@@ -1,21 +1,60 @@
<template>
<div class="card">
<div class="row">
<b>{{ item.username }}</b>
<span class="chip">VLAN {{ item.vlan }}</span>
<span class="chip" :style="item.disabled ? 'color:#b33' : ''">{{ item.disabled ? 'deshabilitado' : 'activo' }}</span>
<span v-if="hasConnected" class="chip" style="background: rgba(255,127,187,.2); border-color: rgba(255,127,187,.5);">Conectado</span>
<div class="user-card">
<div class="user-card-header">
<div class="user-avatar" :class="{ connected: hasConnected, disabled: item.disabled }">
{{ userInitial }}
</div>
<div class="user-info">
<div class="user-name">{{ item.username }}</div>
<div class="user-meta">
<span class="chip chip-vlan">VLAN {{ item.vlan }}</span>
<span class="chip" :class="item.disabled ? 'chip-disabled' : 'chip-active'">
{{ item.disabled ? 'deshabilitado' : 'activo' }}
</span>
<span v-if="hasConnected" class="chip chip-connected">Conectado</span>
</div>
</div>
<span class="spacer"></span>
<button class="icon-btn" @click="$emit('edit', item)">Editar</button>
<button class="icon-btn" @click="$emit('disconnect', item)">Desconectar</button>
<button class="icon-btn" @click="$emit('toggleDisable', item)">{{ item.disabled ? 'Habilitar' : 'Deshabilitar' }}</button>
<button class="icon-btn" @click="$emit('remove', item)">Eliminar</button>
<button v-if="expandable" class="icon-btn" @click="$emit('toggleExpand')">{{ expanded ? 'Contraer' : 'Expandir' }}</button>
<div class="user-actions">
<button class="user-action-btn" @click="$emit('edit', item)" title="Editar usuario">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="user-action-btn" @click="$emit('disconnect', item)" title="Desconectar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path>
<line x1="12" y1="2" x2="12" y2="12"></line>
</svg>
</button>
<button class="user-action-btn" @click="$emit('toggleDisable', item)" :title="item.disabled ? 'Habilitar' : 'Deshabilitar'">
<svg v-if="item.disabled" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg v-else width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
<button class="user-action-btn user-action-danger" @click="$emit('remove', item)" title="Eliminar">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
<button v-if="expandable" class="user-action-btn" @click="$emit('toggleExpand')" :title="expanded ? 'Contraer' : 'Expandir'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" :style="{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
<div v-if="item.etiquetas && item.etiquetas.length" class="row" style="gap:6px; margin-top:6px;">
<span v-for="tag in item.etiquetas" :key="tag" class="chip">#{{ tag }}</span>
</div>
<div v-if="expanded && deviceList.length" style="margin-top:8px;">
<div v-if="item.etiquetas && item.etiquetas.length" class="user-tags">
<span v-for="tag in item.etiquetas" :key="tag" class="chip chip-tag">#{{ tag }}</span>
</div>
<div v-if="expanded && deviceList.length" class="user-devices">
<div class="grid">
<DispositivoCard v-for="d in deviceList" :key="d.id" :device="d" :connected="isConnected(d.id)" simple @edit="$emit('editDevice', d)" />
</div>
@@ -44,4 +83,198 @@ function isConnected(id) {
}
const hasConnected = computed(() => Array.isArray(props.item.dispositivos_conectados) && props.item.dispositivos_conectados.length > 0);
const userInitial = computed(() => {
const username = props.item.username || '';
return username.charAt(0).toUpperCase() || '?';
});
</script>
<style scoped>
.user-card {
border: 1px solid rgba(var(--border));
background: rgba(var(--card));
border-radius: 12px;
padding: 12px;
transition: all 0.2s ease;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
backdrop-filter: blur(var(--glass-blur));
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(255, 127, 187, 0.15);
border-color: rgba(255, 159, 203, 0.3);
}
.user-card-header {
display: flex;
align-items: center;
gap: 12px;
}
.user-avatar {
width: 48px;
height: 48px;
min-width: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 700;
background: linear-gradient(135deg, rgba(255, 127, 187, 0.4), rgba(255, 159, 203, 0.3));
border: 2px solid rgba(255, 159, 203, 0.5);
color: rgb(var(--fg));
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(255, 127, 187, 0.2);
}
.user-avatar.connected {
background: linear-gradient(135deg, rgba(255, 127, 187, 0.8), rgba(255, 159, 203, 0.6));
border-color: rgba(255, 127, 187, 0.9);
box-shadow: 0 4px 16px rgba(255, 127, 187, 0.4), 0 0 20px rgba(255, 127, 187, 0.3);
animation: pulse 2s ease-in-out infinite;
}
.user-avatar.disabled {
background: linear-gradient(135deg, rgba(180, 180, 190, 0.3), rgba(160, 160, 170, 0.2));
border-color: rgba(var(--muted), 0.3);
opacity: 0.6;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 4px 16px rgba(255, 127, 187, 0.4), 0 0 20px rgba(255, 127, 187, 0.3);
}
50% {
box-shadow: 0 4px 20px rgba(255, 127, 187, 0.6), 0 0 30px rgba(255, 127, 187, 0.5);
}
}
.user-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: rgb(var(--fg));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-meta {
display: flex;
gap: 6px;
flex-wrap: wrap;
align-items: center;
}
.chip-vlan {
background: rgba(80, 160, 255, 0.15);
border-color: rgba(80, 160, 255, 0.3);
color: rgb(80, 160, 255);
font-weight: 500;
}
.chip-active {
background: rgba(76, 217, 100, 0.15);
border-color: rgba(76, 217, 100, 0.3);
color: rgb(76, 217, 100);
font-weight: 500;
}
.chip-disabled {
background: rgba(255, 107, 107, 0.15);
border-color: rgba(255, 107, 107, 0.3);
color: rgb(255, 107, 107);
font-weight: 500;
}
.chip-connected {
background: rgba(255, 127, 187, 0.2);
border-color: rgba(255, 127, 187, 0.5);
color: rgb(255, 127, 187);
font-weight: 500;
}
.chip-tag {
background: rgba(var(--accent), 0.12);
border-color: rgba(var(--accent), 0.25);
font-size: 11px;
}
.user-actions {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.user-action-btn {
width: 32px;
height: 32px;
min-width: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(var(--border));
background: rgba(var(--card));
color: rgb(var(--fg));
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(var(--glass-blur));
}
.user-action-btn:hover {
transform: translateY(-1px) scale(1.05);
background: rgba(var(--accent), 0.15);
border-color: rgba(var(--accent), 0.4);
box-shadow: 0 4px 12px rgba(80, 160, 255, 0.2);
}
.user-action-btn:active {
transform: translateY(0) scale(0.98);
}
.user-action-danger:hover {
background: rgba(255, 107, 107, 0.15);
border-color: rgba(255, 107, 107, 0.4);
color: rgb(255, 107, 107);
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.2);
}
.user-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(var(--border));
}
.user-devices {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(var(--border));
}
@media (max-width: 640px) {
.user-actions {
width: 100%;
justify-content: flex-end;
}
.user-action-btn {
width: 36px;
height: 36px;
}
}
</style>

View File

@@ -7,15 +7,12 @@
--border: 255 255 255 / 0.12;
--glass-blur: 14px;
--radius: 14px;
<<<<<<< HEAD
/* Scrollbar */
--sb-size: 10px;
--sb-thumb: rgba(255, 159, 203, 0.55);
--sb-thumb-hover: rgba(255, 159, 203, 0.75);
--sb-thumb-active: rgba(255, 127, 187, 0.9);
--sb-track: rgba(255,255,255,0.05);
=======
>>>>>>> c92df7bb9a1b22aee21d2c083ea163d21ab1a722
}
:root.light {
--bg: 245 245 248;
@@ -24,13 +21,10 @@
--accent: 18 108 242;
--card: 255 255 255 / 0.6;
--border: 0 0 0 / 0.08;
<<<<<<< HEAD
--sb-thumb: rgba(255, 127, 187, 0.65);
--sb-thumb-hover: rgba(255, 127, 187, 0.82);
--sb-thumb-active: rgba(255, 110, 178, 0.95);
--sb-track: rgba(0,0,0,0.06);
=======
>>>>>>> c92df7bb9a1b22aee21d2c083ea163d21ab1a722
}
* { box-sizing: border-box; }
html, body, #app { height: 100%; }
@@ -44,7 +38,6 @@ a { color: inherit; }
position: sticky; top: 0; z-index: 10;
display: flex; flex-wrap: wrap; align-items: center;
gap: 10px; padding: 10px 14px; backdrop-filter: blur(var(--glass-blur));
<<<<<<< HEAD
border: 1px solid transparent;
background:
linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box,
@@ -69,10 +62,6 @@ a { color: inherit; }
rgba(0, 0, 0, 0.00) 70%
) border-box;
border-width: 0.5px; /* borde más delgado en dark */
=======
background: linear-gradient(180deg, rgba(var(--card)), rgba(var(--card)) 60%, rgba(0,0,0,0));
border-bottom: 1px solid rgba(var(--border));
>>>>>>> c92df7bb9a1b22aee21d2c083ea163d21ab1a722
}
.title { font-size: 16px; font-weight: 700; letter-spacing: .2px; flex: 1 1 auto; }
.actions { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
@@ -89,7 +78,6 @@ a { color: inherit; }
.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; grid-template-areas: "sidebar main"; gap: 12px; padding: 12px; }
.shell > aside { grid-area: sidebar; }
.shell > main { grid-area: main; }
<<<<<<< HEAD
.panel {
border: 1px solid #ffcfe4; /* light más claro */
background: linear-gradient(rgba(var(--card)), rgba(var(--card))) padding-box;
@@ -98,9 +86,6 @@ a { color: inherit; }
overflow: hidden; display: flex; flex-direction: column; min-height: 0;
}
:root:not(.light) .panel { border-color: #ff2e86; border-width: 0.5px; /* borde más delgado en dark */ }
=======
.panel { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
>>>>>>> c92df7bb9a1b22aee21d2c083ea163d21ab1a722
.panel-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid rgba(var(--border)); }
.panel-title { font-weight: 600; }
.panel-actions { display: inline-flex; flex-wrap: wrap; gap: 6px; }
@@ -146,7 +131,6 @@ a { color: inherit; }
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.spacer { flex: 1; }
.toggle { padding: 6px 10px; border-radius: 8px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); }
<<<<<<< HEAD
/* Scrollbars */
/* Firefox */
@@ -177,5 +161,3 @@ html::-webkit-scrollbar-thumb:active, body::-webkit-scrollbar-thumb:active, .scr
background: var(--sb-thumb-active);
background-clip: content-box;
}
=======
>>>>>>> c92df7bb9a1b22aee21d2c083ea163d21ab1a722