Especialização em Front-End com Vue • Demonstrações Práticas Compiláveis


💻 Código de Exemplo 11 — Estilização Avançada, Seletor :deep() e Animações com <TransitionGroup>

Este exemplo apresenta a integração do Tailwind CSS com Scoped CSS, uso do seletor :deep() para estilizar elementos internos ou bibliotecas de terceiros, e animações fluidas de inserção, remoção e reordenação de itens com <TransitionGroup>.

<!-- src/components/NotificationManager.vue -->
<script setup lang="ts">
import { ref } from 'vue';

interface SystemNotification {
  id: number;
  title: string;
  type: 'info' | 'warning' | 'error';
  timestamp: string;
}

const notifications = ref<SystemNotification[]>([
  { id: 1, title: 'Backup do PostgreSQL concluído com sucesso.', type: 'info', timestamp: '14:20:00' },
  { id: 2, title: 'Uso de CPU do nó 03 ultrapassou 85%.', type: 'warning', timestamp: '14:22:15' },
  { id: 3, title: 'Falha no heartbeat do pod redis-cache-01.', type: 'error', timestamp: '14:25:30' }
]);

let nextId = 4;

function addAlert(): void {
  const types: ('info' | 'warning' | 'error')[] = ['info', 'warning', 'error'];
  const chosenType = types[Math.floor(Math.random() * types.length)];
  const labels = {
    info: 'Sincronização de réplicas finalizada.',
    warning: 'Alta latência detectada no gateway de pagamentos.',
    error: 'Exceção de timeout na conexão com banco secundário.'
  };

  notifications.value.unshift({
    id: nextId++,
    title: labels[chosenType],
    type: chosenType,
    timestamp: new Date().toLocaleTimeString()
  });
}

function dismiss(id: number): void {
  notifications.value = notifications.value.filter((n) => n.id !== id);
}

function shuffleAlerts(): void {
  notifications.value = [...notifications.value].sort(() => Math.random() - 0.5);
}
</script>

<template>
  <div class="max-w-xl mx-auto p-6 bg-white border rounded-2xl shadow-sm space-y-5">
    <div class="flex items-center justify-between border-b pb-4">
      <div>
        <h2 class="text-xl font-bold text-slate-800">🔔 Gerenciador de Alertas do Cluster</h2>
        <p class="text-xs text-slate-500">Transições animadas do Vue com TransitionGroup</p>
      </div>
      <div class="flex gap-2">
        <button
          @click="shuffleAlerts"
          class="px-3 py-1.5 text-xs font-semibold bg-slate-100 hover:bg-slate-200 text-slate-700 rounded-lg transition-colors"
        >
          Embaralhar
        </button>
        <button
          @click="addAlert"
          class="px-3 py-1.5 text-xs font-semibold bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors"
        >
          + Novo Alerta
        </button>
      </div>
    </div>

    <!-- Container com Transição de Lista e Movimentação Suave (FLIP) -->
    <TransitionGroup
      name="alert-list"
      tag="ul"
      class="space-y-3 relative min-h-[160px]"
    >
      <li
        v-for="item in notifications"
        :key="item.id"
        class="notification-item flex items-center justify-between p-4 rounded-xl border transition-all duration-300"
        :class="{
          'bg-blue-50/70 border-blue-200 text-blue-900': item.type === 'info',
          'bg-amber-50/70 border-amber-200 text-amber-900': item.type === 'warning',
          'bg-red-50/70 border-red-200 text-red-900': item.type === 'error'
        }"
      >
        <div class="space-y-0.5">
          <div class="flex items-center gap-2">
            <span class="text-xs font-mono font-bold uppercase tracking-wider">[{{ item.type }}]</span>
            <span class="text-sm font-medium">{{ item.title }}</span>
          </div>
          <p class="text-[11px] opacity-75 font-mono">Timestamp: {{ item.timestamp }}</p>
        </div>

        <button
          @click="dismiss(item.id)"
          class="dismiss-btn text-xs font-bold px-2 py-1 rounded hover:bg-black/10 transition-colors"
          title="Dispensar notificação"
        >
          ✕
        </button>
      </li>
    </TransitionGroup>

    <div v-if="notifications.length === 0" class="text-center py-8 text-slate-400 text-xs">
      Nenhuma notificação ativa no momento.
    </div>
  </div>
</template>

<style scoped>
/* 1. Classes de Animação do TransitionGroup (Entrada e Saída) */
.alert-list-enter-from {
  opacity: 0;
  transform: translateY(-20px) scale(0.95);
}
.alert-list-enter-to {
  opacity: 1;
  transform: translateY(0) scale(1);
}
.alert-list-leave-from {
  opacity: 1;
  transform: scale(1);
}
.alert-list-leave-to {
  opacity: 0;
  transform: translateX(30px) scale(0.9);
}

/* Garante remoção do fluxo para permitir que a animação FLIP mova os outros nós */
.alert-list-leave-active {
  position: absolute;
  width: 100%;
}

/* 2. Animação FLIP para reordenação suave dos itens que ficaram */
.alert-list-move {
  transition: all 0.4s ease;
}

/* 3. Demonstração de Seletor Profundo (:deep) para estilização de nós filhos */
:deep(.dismiss-btn:hover) {
  cursor: pointer;
  transform: scale(1.1);
}
</style>

🖥️ Saída / Comportamento Esperado

Renderização Inicial:
Lista com 3 alertas ordenados cronologicamente.

Ação 1 — Clicar em "+ Novo Alerta":
O novo alerta entra pelo topo com animação de fade-in e scale de 0.95 para 1.0.
Os alertas inferiores deslizam para baixo suavemente graças à classe .alert-list-move.

Ação 2 — Clicar no "✕" do alerta intermediário:
O alerta selecionado desliza para a direita com fade-out.
Os alertas abaixo dele sobem ocupando a lacuna instantaneamente sem salto visual brusco.

Ação 3 — Clicar em "Embaralhar":
Todos os alertas trocam de posição através da técnica FLIP calculada pelo Vue nativamente.

🔍 Destaques da Implementação

  1. Transição FLIP com .alert-list-move: O Vue calcula as coordenadas antigas e novas via getBoundingClientRect e interpola o movimento via transform CSS puro.
  2. position: absolute na Saída: Permite que os itens restantes se reposicionem imediatamente enquanto o item removido termina sua animação de desaparecimento.
  3. Isolamento de Estilos com :deep(): Permite customizar propriedades de botões ou componentes de bibliotecas externas sem remover o atributo scoped.