💻 Exemplos: Capítulo 11: Scoped CSS, Tailwind CSS e Transições Nativas ()
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
- Transição FLIP com
.alert-list-move: O Vue calcula as coordenadas antigas e novas viagetBoundingClientRecte interpola o movimento via transform CSS puro. position: absolutena Saída: Permite que os itens restantes se reposicionem imediatamente enquanto o item removido termina sua animação de desaparecimento.- Isolamento de Estilos com
:deep(): Permite customizar propriedades de botões ou componentes de bibliotecas externas sem remover o atributoscoped.