Rediseñar UserCard con estilo minimalista inspirado en diseño 3D
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 15s

- Cambiar a diseño limpio tipo ficha con información organizada en filas label-value
- Reducir avatar a inicial pequeña (36x36px) con fondo sutil
- Implementar sistema de sombras suaves de múltiples capas para profundidad
- Simplificar paleta de colores con acentos sutiles
- Mejorar tipografía con mejor jerarquía (18px título, 13px info)
- Cambiar botones a diseño minimalista con borde sutil
- Agregar transiciones suaves con cubic-bezier para movimiento natural
- Implementar hover con elevación de 4px y sombras progresivas
- Organizar información en formato tabla limpia (vlan, estado, conexión, etiquetas)
- Reducir efectos visuales excesivos por diseño más elegante y profesional
This commit is contained in:
2025-10-28 10:12:30 -06:00
parent 37fdcaec9c
commit 5c39dc1fd2

View File

@@ -1,65 +1,75 @@
<template>
<div class="user-card">
<div class="user-card-header">
<div class="user-avatar" :class="{ connected: hasConnected, disabled: item.disabled }">
{{ userInitial }}
<div class="user-card" :class="{ 'is-connected': hasConnected, 'is-disabled': item.disabled }">
<div class="card-inner">
<div class="card-header">
<div class="user-initial">{{ userInitial }}</div>
<h3 class="user-name">{{ item.username }}</h3>
</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' }}
<div class="card-body">
<div class="info-row">
<span class="info-label">vlan</span>
<span class="info-value">{{ item.vlan }}</span>
</div>
<div class="info-row">
<span class="info-label">estado</span>
<span class="info-value">{{ item.disabled ? 'deshabilitado' : 'activo' }}</span>
</div>
<div v-if="hasConnected" class="info-row">
<span class="info-label">conexión</span>
<span class="info-value status-connected">conectado</span>
</div>
<div v-if="item.etiquetas && item.etiquetas.length" class="info-row">
<span class="info-label">etiquetas</span>
<span class="info-value tags">
<span v-for="tag in item.etiquetas" :key="tag" class="tag">{{ tag }}</span>
</span>
<span v-if="hasConnected" class="chip chip-connected">Conectado</span>
</div>
</div>
<span class="spacer"></span>
<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">
<div class="card-actions">
<button class="action-btn" @click="$emit('edit', item)" title="Editar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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">
<button class="action-btn" @click="$emit('disconnect', item)" title="Desconectar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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">
<button class="action-btn" @click="$emit('toggleDisable', item)" :title="item.disabled ? 'Habilitar' : 'Deshabilitar'">
<svg v-if="item.disabled" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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">
<svg v-else width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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">
<button class="action-btn action-btn--danger" @click="$emit('remove', item)" title="Eliminar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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' }">
<button v-if="expandable" class="action-btn" @click="$emit('toggleExpand')" :title="expanded ? 'Contraer' : 'Expandir'">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" :style="{ transform: expanded ? 'rotate(180deg)' : 'rotate(0deg)' }">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
<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">
<div v-if="expanded && deviceList.length" class="card-devices">
<div class="devices-grid">
<DispositivoCard v-for="d in deviceList" :key="d.id" :device="d" :connected="isConnected(d.id)" simple @edit="$emit('editDevice', d)" />
</div>
</div>
</div>
</div>
</template>
<script setup>
@@ -92,188 +102,190 @@ const userInitial = computed(() => {
<style scoped>
.user-card {
border: 1px solid rgba(var(--border));
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-inner {
position: relative;
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));
border: 1px solid rgba(var(--border));
border-radius: 16px;
padding: 20px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.05),
0 2px 8px rgba(0, 0, 0, 0.03);
}
.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:hover .card-inner {
transform: translateY(-4px);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.08),
0 8px 32px rgba(0, 0, 0, 0.06),
0 16px 48px rgba(0, 0, 0, 0.04);
}
.user-card-header {
display: flex;
align-items: center;
gap: 12px;
/* Estados */
.user-card.is-connected .card-inner {
border-color: rgba(255, 127, 187, 0.3);
}
.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);
.user-card.is-disabled .card-inner {
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;
/* Header */
.card-header {
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;
gap: 12px;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(var(--border));
}
.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;
.user-initial {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid rgba(var(--border));
background: rgba(var(--card));
background: rgba(var(--accent), 0.08);
color: rgb(var(--accent));
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
}
.user-card.is-connected .user-initial {
background: rgba(255, 127, 187, 0.12);
color: rgb(255, 127, 187);
}
.user-name {
margin: 0;
font-size: 18px;
font-weight: 600;
color: rgb(var(--fg));
cursor: pointer;
transition: all 0.2s ease;
backdrop-filter: blur(var(--glass-blur));
letter-spacing: -0.01em;
}
.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);
/* Body */
.card-body {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.user-action-btn:active {
transform: translateY(0) scale(0.98);
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
line-height: 1.5;
}
.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);
.info-label {
color: rgb(var(--muted));
font-weight: 500;
text-transform: lowercase;
letter-spacing: 0.02em;
}
.user-tags {
.info-value {
color: rgb(var(--fg));
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.status-connected {
color: rgb(255, 127, 187);
}
.tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 10px;
padding-top: 10px;
}
.tag {
padding: 2px 8px;
background: rgba(var(--accent), 0.08);
border-radius: 6px;
font-size: 11px;
font-weight: 500;
color: rgb(var(--accent));
}
/* Actions */
.card-actions {
display: flex;
gap: 6px;
padding-top: 16px;
border-top: 1px solid rgba(var(--border));
}
.user-devices {
margin-top: 12px;
padding-top: 12px;
.action-btn {
flex: 1;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(var(--border));
border-radius: 8px;
background: transparent;
color: rgb(var(--muted));
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-btn:hover {
background: rgba(var(--accent), 0.06);
border-color: rgba(var(--accent), 0.2);
color: rgb(var(--accent));
transform: translateY(-1px);
}
.action-btn:active {
transform: translateY(0);
}
.action-btn--danger:hover {
background: rgba(255, 107, 107, 0.06);
border-color: rgba(255, 107, 107, 0.2);
color: rgb(255, 107, 107);
}
.action-btn svg {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Devices */
.card-devices {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(var(--border));
}
.devices-grid {
display: grid;
gap: 12px;
}
/* Responsive */
@media (max-width: 640px) {
.user-actions {
width: 100%;
justify-content: flex-end;
.card-inner {
padding: 16px;
}
.user-action-btn {
width: 36px;
.user-name {
font-size: 16px;
}
.action-btn {
height: 36px;
}
}