Como o Impeccable e o GitHub Copilot deram movimento e tema escuro ao meu dashboard Vue

Como o Impeccable e o GitHub Copilot deram movimento e tema escuro ao meu dashboard Vue

22 de outubro de 2026

O Impeccable é uma skill de design para agentes de código, e roda dentro do GitHub Copilot no VS Code em agent mode. No artigo: "Como usei o Impeccable com GitHub Copilot para melhorar o UI/UX", mostrei como a skill é organizada e usei o comando critique para avaliar o CarStore. Agora o foco é outro: como o Impeccable usa o contexto do projeto para tomar decisões de design. Para isso, aplico dois comandos de refinamento, /impeccable animate e /impeccable colorize, no dashboard do CarStore, uma SPA em Vue 3 com PrimeVue e Tailwind CSS servida por um back-end .NET 10 que segue os guias de arquitetura .NET.

O que torna o resultado diferente de um pedido genérico ao GitHub Copilot são dois arquivos na raiz do repositório. O PRODUCT.md descreve o produto, os usuários e as restrições. O DESIGN.md registra o sistema visual: a escala de superfícies slate, a regra de um único acento de ação e o princípio de separar superfícies com bordas, não com sombras. Antes de qualquer comando, o Impeccable executa o script de contexto, que carrega esses dois arquivos e entrega ao agente as diretrizes da tela em questão.

.github/skills/impeccable/scripts/impeccable context

Com esse contexto, o agente classifica a tela pelo que o usuário precisa fazer nela. O dashboard do CarStore cai no modo que o Impeccable chama de Operate, o de ferramentas de trabalho, em que leitura rápida e consistência vêm antes de efeitos chamativos. Antes de editar a interface, o agente também lê o craft floor da skill. É uma lista de checagens de contraste, estados, foco e movimento, mais padrões que ele deve evitar, como texto com gradiente e sombras duras sem desfoque. Foi esse conjunto que guiou cada decisão a seguir.

O primeiro pedido no GitHub Copilot foi para a camada de movimento, descrevendo o que se esperava em linguagem natural.

/impeccable animate Contagem dos KPIs, barras, transição de rota e Loader com o carrinho, revelação dos textos, efeitos no CRUD

Antes de editar qualquer arquivo, o Impeccable escreve uma tese de movimento. Ela define o momento principal da tela, o que é continuidade entre estados e o que é feedback de ação. Aqui, o momento principal é a abertura do dashboard: os dados chegam da API e a tela se acomoda em vez de saltar. Para os números dos KPIs, o agente criou um composable que interpola o valor exibido até o valor real.

import { onBeforeUnmount, ref, watch, type Ref } from "vue";

const easeOutExpo = (t: number) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t));

export function useTweenedNumber(source: Ref<number>, duration = 600, delay = 0) {
  const display = ref(source.value);
  let frame = 0;
  let timer = 0;

  watch(source, (target) => {
    cancelAnimationFrame(frame);
    clearTimeout(timer);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      display.value = target;
      return;
    }
    timer = window.setTimeout(() => {
      const from = display.value;
      const start = performance.now();
      const step = (now: number) => {
        const t = Math.min((now - start) / duration, 1);
        display.value = from + (target - from) * easeOutExpo(t);
        if (t < 1) frame = requestAnimationFrame(step);
      };
      frame = requestAnimationFrame(step);
    }, delay);
  });

  onBeforeUnmount(() => {
    cancelAnimationFrame(frame);
    clearTimeout(timer);
  });
  return display;
}

useTweenedNumber()
Observa uma referência numérica e, a cada mudança, anima o valor exibido até o novo alvo com uma curva exponencial, respeitando um atraso opcional.

easeOutExpo()
Desacelera no fim do movimento, o que faz o número parecer se acomodar no valor final.

A checagem de prefers-reduced-motion aplica o valor na hora para quem pediu menos movimento no sistema. Se o valor muda no meio de uma animação, a nova parte do número que está na tela, sem pular de volta para zero.

A contagem só faz sentido se começar quando o usuário pode vê-la. Por isso o agente criou uma tela de carregamento que acompanha as quatro requisições do Home. O carrinho animado do logo da sidebar anda sobre uma barra de porcentagem, e os KPIs, as barras e o gráfico esperam ela sair para começar.

const showLoader = ref(!introPlayed);
const ready = ref(introPlayed);
const loadsDone = ref(0);
const startupLoads = 4;
const loaderProgress = computed(() => (loadsDone.value / startupLoads) * 100);

function track(request: Promise<void>) {
  return request.finally(() => loadsDone.value++);
}

onMounted(async () => {
  const minimumShow = new Promise((resolve) => setTimeout(resolve, showLoader.value ? 1100 : 0));
  await Promise.all([
    track(loadCars(1, 100)),
    track(loadBrands(1, 100)),
    track(loadCustomersForActivity(1, 100)),
    track(loadRecentCustomers(1, recentRows.value)),
    minimumShow,
  ]);
  if (!showLoader.value) return;
  // Let the bar visibly land on 100% before the car drives off.
  await new Promise((resolve) => setTimeout(resolve, 500));
  introPlayed = true;
  showLoader.value = false;
});

track()
Envolve cada requisição e incrementa o contador quando ela termina, com sucesso ou erro, o que alimenta a porcentagem da barra.

onMounted()
Dispara as quatro cargas em paralelo, garante um tempo mínimo de exibição e deixa a barra parada em 100% por meio segundo antes de liberar a revelação.

A variável introPlayed fica no escopo do módulo, fora do componente. Por isso o loader aparece só na primeira visita da sessão, e as próximas navegações para o Home vão direto para a revelação.

Imagem: Tela de carregamento com o carrinho do logo sobre a barra de progresso em 100%

0:00
/0:18

Quando o loader sai, a classe home-ready entra no container da página e dispara a revelação. O título "Fleet command" sobe palavra por palavra de dentro de uma máscara, e o subtítulo, os cards e os títulos das seções entram em seguida, em ordem de leitura. Os quatro KPIs contam de zero até o valor em 1,8 segundo, um card após o outro.

const countDuration = 1800;
const gated = (value: () => number) => computed(() => (ready.value ? value() : 0));
const fleetValueShown = useTweenedNumber(gated(() => fleetValue.value), countDuration, 200);
const carsInStockShown = useTweenedNumber(gated(() => carsInStock.value), countDuration, 300);
const recentTotalShown = useTweenedNumber(gated(() => recentTotal.value), countDuration, 400);
const availabilityRateShown = useTweenedNumber(gated(() => availabilityRate.value), countDuration, 500);

gated()
Devolve zero enquanto a página não foi revelada e o valor real depois, de modo que a contagem começa exatamente quando o loader termina.

Imagem: Dashboard no meio da revelação, com o valor da frota ainda subindo até o total

0:00
/0:11

Os efeitos nas operações de CRUD resolveram um problema que os toasts sozinhos não resolvem: mostrar qual linha mudou. O agente criou um pequeno registro reativo de efeitos por linha, que os composables de criação, edição e exclusão alimentam e que a tabela converte em classes CSS.

const effects = reactive(new Map<string, RowEffect>());

const key = (scope: string, id: unknown) => `${scope}:${id}`;

export function flagRows(scope: string, ids: unknown[], effect: RowEffect, duration: number) {
  for (const id of ids) effects.set(key(scope, id), effect);
  setTimeout(() => {
    for (const id of ids) if (effects.get(key(scope, id)) === effect) effects.delete(key(scope, id));
  }, duration);
}

export function rowEffectClass(scope: string | undefined, id: unknown): string | undefined {
  if (!scope) return undefined;
  const effect = effects.get(key(scope, id));
  return effect ? `row-fx-${effect}` : undefined;
}

export function playRowExit(scope: string, ids: unknown[]): Promise<void> {
  flagRows(scope, ids, "leaving", 2000);
  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  return new Promise((resolve) => setTimeout(resolve, reduceMotion ? 250 : 380));
}

flagRows()
Marca as linhas de uma entidade com um efeito temporário, usando a URL da API como escopo para que cada tabela veja só as próprias linhas.

rowEffectClass()
É chamada pelo row-class do DataTable do PrimeVue e devolve a classe row-fx-created, row-fx-updated ou row-fx-leaving.

playRowExit()
Toca a animação de saída antes que a tabela recarregue os dados, para que a linha excluída suma diante do usuário em vez de desaparecer de repente.

Na prática, a linha criada desce para o lugar com um brilho que se apaga, a linha editada recebe uma faixa de luz que a atravessa e a linha excluída ganha um tom avermelhado, desliza e desfoca. A exclusão em lote usa o mesmo caminho para todas as linhas removidas.

Imagem: Tabela de Cars com a linha recém-criada destacada pelo brilho e o toast de confirmação;

0:00
/0:33

Imagem: Linha excluída com tom avermelhado saindo da tabela enquanto o toast confirma a exclusão;

0:00
/0:09

Com o movimento pronto, o segundo pedido foi sobre cor. É aqui que o DESIGN.md mais pesa. Ele exigia uma interface monocromática com um único acento, e o Impeccable trata esse tipo de regra como compromisso da marca. Em vez de quebrá-la, ele trouxe a cor como significado dos dados.

/impeccable colorize Paleta de pinturas na dashboard e Tema claro/escuro e o botão na sidebar

A paleta escolhida usa cores de pintura automotiva: Gulf, Racing Green, Bronze, Burgundy e Graphite. O agente calculou o contraste de cada cor contra o branco e contra o próprio fundo tingido antes de usá-la. Com a configuração CSS-first do Tailwind CSS v4, a paleta vira tokens declarados direto no CSS, e utilitários como bg-paint-gulf e text-paint-gulf/12 passam a existir automaticamente.

@theme {
  --color-paint-gulf: #2f6f9f;
  --color-paint-green: #2e6b4f;
  --color-paint-bronze: #9a6a1c;
  --color-paint-burgundy: #8e2f52;
  --color-paint-graphite: #475569;
}

Cada card de KPI ganhou uma cor de identidade, com um fundo levemente tingido e o ícone na mesma cor. O card de Fleet by brand ganhou uma barra segmentada com a composição da frota, e o gráfico de novos clientes deixou o verde padrão do Aura e passou a usar o azul Gulf. O slate continua sendo a única cor de ação, e o agente registrou essa decisão no DESIGN.md como uma nova regra: as cores de pintura representam só dados, nunca botões ou navegação. Assim, a próxima pessoa ou o próximo agente que mexer no projeto herda a decisão.

Imagem: Dashboard no tema claro com os cards de KPI tingidos e a barra de composição da frota por marca;

O tema escuro é a parte mais interessante da implementação. Em vez de espalhar variantes dark: por todos os componentes, o agente percebeu que os tokens semânticos do preset Aura do PrimeVue apontam para a escala de superfícies. O fundo dos componentes usa surface-0, o texto usa surface-700 e as bordas usam surface-200 e surface-300. Ao redefinir essa escala sob a classe app-dark, componentes, utilitários do Tailwind e a própria paleta mudam juntos.

html.app-dark {
  color-scheme: dark;
  --p-surface-0: #0d1117;
  --p-surface-50: #131922;
  --p-surface-100: #19202b;
  --p-surface-200: #232b37;
  --p-surface-300: #2f3846;
  --p-surface-400: #4d5868;
  --p-surface-500: #8b96a8;
  --p-surface-600: #aab3c2;
  --p-surface-700: #dbe1ea;
  --p-surface-800: #edf1f6;
  --p-surface-900: #f5f7fa;
  --p-surface-950: #fbfcfd;
  /* ... */
}

A escala não é uma inversão mecânica. O fundo fica num grafite quase preto, os cards e a sidebar sobem um degrau para criar elevação, o texto secundário fica com contraste de 5,9:1 e o principal com 14:1. O seletor html.app-dark vence o bloco :root que o PrimeVue injeta em tempo de execução, independente da ordem dos estilos. As cores de pintura ganharam versões mais claras para o fundo escuro, e o gráfico do ECharts reconstrói as cores e o tooltip sempre que o tema muda.

Imagem: Dashboard no tema escuro com os KPIs, o gráfico em azul claro e a barra de composição da frota

A troca de tema fica num botão na sidebar, desenhado como mais uma linha do menu, com lua no tema claro, sol no escuro e um mini interruptor quando a sidebar está aberta. A lógica fica num composable que salva a escolha no navegador e usa a View Transitions API para revelar o novo tema num círculo que cresce a partir do botão.

function toggle(event?: MouseEvent) {
  const next: Theme = theme.value === "dark" ? "light" : "dark";
  const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  if (!document.startViewTransition || reduceMotion) {
    apply(next);
    return;
  }

  const target = event?.currentTarget as HTMLElement | undefined;
  const rect = target?.getBoundingClientRect();
  const x = rect ? rect.left + rect.width / 2 : window.innerWidth / 2;
  const y = rect ? rect.top + rect.height / 2 : window.innerHeight / 2;
  const radius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));

  const transition = document.startViewTransition(() => apply(next));
  transition.ready.then(() => {
    document.documentElement.animate(
      { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },
      { duration: 550, easing: "cubic-bezier(0.16, 1, 0.3, 1)", pseudoElement: "::view-transition-new(root)" },
    );
  });
}

toggle()
Calcula o centro do botão clicado e o raio até o canto mais distante da tela, e anima o clip-path do novo snapshot de um círculo de zero até cobrir a página.

apply()
Alterna a classe app-dark no elemento raiz e salva a escolha no localStorage.

Quando o navegador não suporta View Transitions ou o usuário pediu menos movimento, a troca é instantânea. Para evitar que a página pisque no tema errado ao carregar, o agente também adicionou um script curto no _Layout.cshtml do ASP.NET Core que aplica a classe antes da primeira pintura.

Imagem: Página de Cars no tema escuro com a sidebar aberta e o interruptor de tema ligado

Um detalhe mostra bem como a skill trabalha com o agent mode do GitHub Copilot. O Impeccable manda verificar o resultado em uma única rodada, com desktop e mobile juntos, corrigi tudo de uma vez e para. Depois de implementar o tema escuro, o agente capturou as telas nos dois temas e encontrou dois problemas. O texto dos botões primários ficava branco sobre o novo fundo claro do botão, e os cards se confundiam com o fundo da página. Ele corrigiu os dois antes de entregar: o botão primário passou a usar text-surface-0, e os cards ganharam um degrau de elevação. Ao final, rodou o detector determinístico da skill, que confere o código contra padrões conhecidos sem usar um modelo de IA, e não houve apontamentos.

No fim, foram dois comandos do Impeccable dentro do GitHub Copilot, sem tocar na API do back-end. O dashboard do CarStore ganhou uma abertura com loader e contagem, efeitos que mostram o que mudou em cada operação, uma paleta de dados com regra própria e um tema escuro projetado. O principal aprendizado é que a qualidade dessas decisões vem do contexto. Com um PRODUCT.md e um DESIGN.md bem escritos, o agente deixa de decidir no gosto e passa a seguir o sistema do projeto, atualizando o próprio DESIGN.md quando cria uma regra nova.

O código completo deste projeto está disponível no meu repositório no GitHub.

Docs e Links:

The missing design vocabulary for agents.
Turn AI slop into interfaces you’re proud to ship. Impeccable gives your agent the design expertise to build from scratch, improve what you have, and stay true to your design system.
GitHub Copilot · Seu agente de codificação de IA
O GitHub Copilot atua em todas as etapas do processo, desde uma issue aberta até a revisão e o merge de um pull request. Use-o no seu editor, no terminal, na sua área de trabalho e no GitHub.
PrimeVue | Vue UI Component Library
The ultimate collection of design-agnostic, flexible and accessible Vue UI Components.
Tailwind CSS - Rapidly build modern websites without ever leaving your HTML.
Tailwind CSS is a utility-first CSS framework for rapidly building modern websites without ever leaving your HTML.
Guias de arquitetura do aplicativo .NET
E-books gratuitos e conselhos práticos para você desenvolver para a web, área de trabalho, dispositivos móveis e microsserviços com o Docker.
Visual Studio Code - The open source AI code editor | Your home for multi-agent development
Visual Studio Code is a free, open source AI code editor. Build with AI agents that plan, code, and debug for you. Manage multi-agent workflows across environments on Linux, macOS, and Windows.

Não esqueça de me seguir no LinkedIn para mais conteúdos.
Até a próxima!!!

Confira mais:

Fique por dentro das novidades

Assine nossa newsletter e receba as últimas atualizações e artigos diretamente em seu email.

Assinar gratuitamente