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
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:
@@ -1,62 +1,72 @@
|
||||
<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">
|
||||
<DispositivoCard v-for="d in deviceList" :key="d.id" :device="d" :connected="isConnected(d.id)" simple @edit="$emit('editDevice', d)" />
|
||||
|
||||
<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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user