O Impeccable é um agent skill para o GitHub Copilot voltado inteiramente para design de interface: crítica de UX, ajustes visuais, acessibilidade, edição ao vivo no navegador e mais de vinte comandos especializados. Neste artigo eu mostro como apliquei esse skill em um projeto real, o CarStore, um back-office de concessionária construído em .NET 10 MVC com uma SPA em Vue 3, PrimeVue e Tailwind CSS, para sair de um diagnóstico de UX objetivo até um conjunto de correções de código concretas. A ideia aqui não é falar sobre o Impeccable em teoria, e sim mostrar o antes e depois de um app que realmente rodou por esse processo.
Tela Dashboard:

Tela de Cars:

Tela Brands:

Tela Customers:

Um agent skill no GitHub Copilot é, na prática, uma pasta com instruções, scripts e recursos que o GitHub Copilot carrega quando a tarefa pede um conhecimento especializado que não caberia inteiro no prompt. Skills de projeto ficam em `.github/skills` , cada uma descrita por um arquivo `SKILL.md` com metadados de quando ela deve entrar em ação. No CarStore, o Impeccable chegou exatamente assim:
name: impeccable
description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface.
version: 4.3.1
user-invocable: true
argument-hint: "[shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]"Esse cabeçalho é o que o GitHub Copilot usa para decidir, a partir do pedido do usuário, se deve carregar o skill inteiro. Cada comando citado no `argument-hint` (`critique`, `polish`, `live`, `harden`, entre outros) tem seu próprio arquivo de referência dentro de `.github/skills/impeccable/reference/`, carregado sob demanda para não estourar o contexto com instruções que não vão ser usadas naquela tarefa.
github/skills/impeccable/SKILL.MD

.github/skills/impeccable/reference/

O primeiro comando que rodei foi o `critique`, que faz uma auditoria de UX completa em cima de heurísticas conhecidas. Ele se apoia nas 10 heurísticas de usabilidade de Jakob Nielsen, publicadas em 1994 e usadas até hoje como régua padrão para avaliar qualidade de interface, coisas como visibilidade do status do sistema, prevenção de erros, consistência e reconhecimento em vez de memorização. O comando roda duas avaliações isoladas em paralelo, uma leitura de design feita por um agente e uma varredura determinística por outro, e depois cruza os dois resultados num relatório único, já que cada abordagem pega problemas diferentes.

No CarStore, essa varredura determinística voltou limpa, sem nenhum anti-padrão conhecido, mas a leitura manual do código encontrou cinco problemas reais que a ferramenta automatizada simplesmente não tinha como enxergar. A pontuação final ficou em 24 de 40 pontos nas heurísticas de Nielsen, faixa classificada como aceitável, com espaço claro para melhorar. Dois desses problemas eram bugs de execução contra a própria especificação de design do app, e três eram falhas de prevenção de erro e de eficiência de uso.
Imagem do relatório de critique do Impeccable no terminal, mostrando a tabela de heurísticas com a pontuação 24/40

O primeiro bug de prioridade máxima estava no diálogo de edição de carro. O comentário no código já dizia o que devia acontecer, mas o campo correspondente no template não respeitava essa regra:
function formFromCar(car: CarDto | null) {
return {
model: car?.model ?? "",
vin: car?.vin ?? "",
brandId: car?.brandId ?? (null as number | null),
bodyType: car?.bodyType ?? "",
modelYear: car?.modelYear ?? (null as number | null),
price: car?.price ?? (null as number | null),
stock: car?.stock ?? (null as number | null),
mileage: car?.mileage ?? (null as number | null),
description: car?.description ?? "",
};
}O template renderizava o campo de quilometragem como um `InputNumber` totalmente editável, quando a intenção original do design era que ele ficasse travado no modo de edição. A correção foi direta:
<FormField label="Mileage">
<InputNumber v-model="form.mileage" :min="0" disabled class="w-full opacity-50" />
</FormField>`disabled` bloqueia a edição do campo, e a classe `opacity-50` sinaliza visualmente que ele está desabilitado, seguindo o mesmo padrão de opacidade que o resto do design system do projeto já usava para estados inativos.
O segundo problema de prioridade máxima era mais simples, mas igualmente visível para quem usa o sistema: um bug de texto literal na página de carros, onde aspas duplas tinham ficado presas dentro do valor da string em vez de delimitá-la.
description="Manage the car inventory: browse model, brand, body type, price, stock and availability, and create, edit or remove entries."
Antes:

Depois:

Depois de corrigido, o texto passou a renderizar normalmente, sem as aspas soltas que apareciam antes na tela, consistente com a descrição das outras duas páginas de entidade do sistema (marcas e clientes).
Os dois problemas seguintes eram sobre prevenção de erro, uma das heurísticas de Nielsen que ficou com a pior nota no relatório. O domínio do CarStore já tinha uma regra de negócio clara, uma marca com carros cadastrados não pode ser excluída, mas a interface só descobria isso depois que a API rejeitava a requisição. Para resolver, estendi o componente genérico de confirmação de exclusão com duas propriedades novas:
const props = defineProps<{
visible: boolean;
title: string;
message: string;
details: ConfirmDeleteDetail[];
loading?: boolean;
error?: string | null;
confirmDisabled?: boolean;
warning?: string | null;
}>();`confirmDisabled`- Quando verdadeiro, desabilita o botão de exclusão no diálogo, impedindo o clique antes mesmo de qualquer chamada à API.
`warning`- Mensagem exibida em destaque no diálogo, usada para explicar ao usuário por que a ação está bloqueada, sem esperar por um erro de servidor.
Na tabela de marcas, a mesma lógica de bloqueio foi aplicada tanto para a exclusão individual quanto para a exclusão em massa que adicionei depois:
:confirm-disabled="!!deleteTarget && deleteTarget.carsCount > 0"
:warning="
deleteTarget && deleteTarget.carsCount > 0
? `This brand has ${deleteTarget.carsCount} car(s) registered. Remove or reassign them before deleting the brand.`
: null
"Agora o usuário vê o motivo do bloqueio antes de tentar excluir, em vez de descobrir a regra de negócio por tentativa e erro.
Antes:

Depois:

O quarto problema prioritário era a ausência total de feedback de sucesso. Criar, editar ou excluir qualquer registro simplesmente fechava o diálogo em silêncio, sem confirmação nenhuma para o usuário, o que achata a experiência logo no momento em que ela deveria terminar bem. A correção ficou centralizada nos composables compartilhados pelas três entidades do sistema, usando o serviço de Toast do PrimeVue:
async function onDeleteConfirm() {
if (!deleteTarget.value) return;
deleteLoading.value = true;
deleteError.value = null;
try {
const response = await fetch(`${apiUrl}/${deleteTarget.value.id}`, { method: "DELETE" });
if (!response.ok) throw new Error(await parseErrorMessage(response));
deleteDialogVisible.value = false;
toast.add({
severity: "success",
summary: `${capitalize(entityLabel)} deleted`,
life: 3000,
});
await reload();
} catch (err) {
deleteError.value = err instanceof Error ? err.message : `Failed to delete the ${entityLabel}.`;
} finally {
deleteLoading.value = false;
}
}`onDeleteConfirm()`- Executa a exclusão, e só depois de confirmar sucesso na resposta é que dispara o toast e recarrega a tabela, garantindo que o feedback positivo nunca apareça sobre uma operação que na verdade falhou.
Como esse composable é reutilizado por marcas, carros e clientes, bastou essa mudança em um único lugar para as três entidades ganharem confirmação visual de sucesso ao mesmo tempo.

O quinto ponto prioritário ficou fora do lote de correções imediatas por ser uma questão de eficiência, não de prevenção de erro: as tabelas não tinham busca, ordenação nem ações em massa, forçando qualquer operação em lote a ser feita registro por registro. Como a paginação do CarStore é feita no servidor, cada página traz só uma fatia dos dados, decidi implementar busca e ordenação client-side, atuando apenas sobre os itens já carregados na página atual, evitando mexer nos três controllers e services da API. A lógica de filtro e ordenação ficou isolada num único `computed` no componente de tabela compartilhado:
const displayValue = computed(() => {
let rows = props.value;
const term = searchTerm.value.trim().toLowerCase();
if (term) {
rows = rows.filter((row) =>
props.columns.some((col) => String(row[col.field] ?? "").toLowerCase().includes(term)),
);
}
if (sortField.value) {
const field = sortField.value;
const order = sortOrder.value ?? 1;
rows = [...rows].sort((a, b) => {
const va = a[field];
const vb = b[field];
if (va == null && vb == null) return 0;
if (va == null) return -1 * order;
if (vb == null) return 1 * order;
if (va < vb) return -1 * order;
if (va > vb) return 1 * order;
return 0;
});
}
return rows;
});`displayValue`- Recalcula automaticamente a lista exibida sempre que o termo de busca ou o campo de ordenação muda, sem precisar de nenhuma chamada extra à API.
Junto com isso veio a seleção múltipla de linhas e um botão de exclusão em massa, reaproveitando o mesmo diálogo de confirmação genérico que já existia, incluindo a trava de segurança para marcas com carros cadastrados.
Antes:

Depois:

Vale registrar também um ajuste técnico que apareceu no meio do processo e que não tinha relação direta com UX, mas quebrava a experiência de qualquer pessoa usando o app: os ícones do PrimeIcons simplesmente não apareciam na tela. A causa era um detalhe sutil da configuração do Vite, a ferramenta de build usada pelo projeto para servir a SPA com Hot Module Replacement instantâneo durante o desenvolvimento. A página era servida pela aplicação ASP.NET Core em uma porta, enquanto os arquivos JavaScript e CSS vinham do dev server do Vite em outra porta, e como o CSS do PrimeIcons usa caminhos absolutos de raiz para as fontes, o navegador tentava buscar essas fontes na origem errada.
server: {
port: 5173,
strictPort: true,
cors: true,
origin: "http://localhost:5173",
},A propriedade `origin` resolve exatamente esse cenário, instruindo o Vite a gerar as URLs de asset já com a origem completa do próprio dev server, em vez de caminhos relativos que o navegador resolveria contra a página atual.
Fora do fluxo de crítica e correção pontual, o Impeccable guarda um modo ainda mais direto chamado `live`, que injeta um script na página em desenvolvimento e transforma o navegador numa mesa de edição visual: você seleciona um elemento com o mouse, descreve em linguagem natural a variação que quer, e recebe três alternativas geradas e aplicadas via HMR na hora, sem sair da tela e sem escrever uma linha de código à mão. Esse modo já estava configurado no CarStore antes de eu retomar o trabalho, com o helper rodando localmente e o script injetado no layout principal da aplicação por trás de uma tag de ambiente que só carrega em desenvolvimento. Não usei esse fluxo neste artigo de propósito, porque ele merece espaço próprio: na próxima parte desta série vou abrir o CarStore no navegador, ligar o `live` e mostrar a edição acontecendo em tempo real, do clique no elemento até o código final gerado.
Neste primeiro artigo utilizamos o /impeccable critique: um diagnóstico de UX objetivo, cinco problemas reais encontrados por uma leitura de código que a varredura automática não pegou, e um conjunto de correções validado com `vue-tsc --noEmit` limpo, sem nenhum erro de tipo, confirmando que nada quebrou entre os componentes mesmo sem uma suíte de testes de interface automatizada rodando em paralelo.
O próximo passo natural seria avançar para o item de eficiência que ficou de fora deste lote, evoluindo a busca e a ordenação para o lado do servidor conforme o volume de dados crescer.
Mas essa foi apenas uma parte do que a Impeccable pode oferecer. Nos próximos conteúdos, vou explorar outros itens e recursos disponíveis nas suas Skills, mostrando como eles podem contribuir para diferentes etapas do processo de desenvolvimento e evolução de uma interface.
O código completo deste projeto está disponível no meu repositório no GitHub.
Links e Docs:







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




