{
 "grupo": "frontend",
 "descricao": "Front-end e interface",
 "itens": [
  {
   "id": "frontend-estado-global-sem-biblioteca-em-react",
   "nome": "Estado global sem biblioteca em React",
   "receita": "createContext + useReducer num provider; exporte hook useX que lança erro fora do provider; divida contextos por frequência de mudança",
   "prova": "trocar um valor re-renderiza só os consumidores (React DevTools Profiler)"
  },
  {
   "id": "frontend-formulario-com-validacao-por-schema",
   "nome": "Formulário com validação por schema",
   "receita": "defina o schema zod uma vez; use react-hook-form com zodResolver; erro por campo vinculado por aria-describedby",
   "prova": "enviar vazio mostra os erros e o foco vai ao primeiro campo inválido"
  },
  {
   "id": "frontend-busca-com-debounce",
   "nome": "Busca com debounce",
   "receita": "guarde o texto em estado, dispare a consulta 300 ms depois do último caractere e cancele a anterior com AbortController",
   "prova": "digitar rápido gera uma única requisição (aba Network)"
  },
  {
   "id": "frontend-lista-infinita-sem-travar",
   "nome": "Lista infinita sem travar",
   "receita": "IntersectionObserver no último item pede a próxima página; mantenha o cursor, não o número da página; virtualize acima de 500 itens",
   "prova": "rolar 5000 itens mantém o DOM abaixo de 100 nós"
  },
  {
   "id": "frontend-rotas-com-carregamento-e-erro",
   "nome": "Rotas com carregamento e erro",
   "receita": "cada rota declara loader, estado de carregando e errorElement; redirecione não autenticado no loader, não no render",
   "prova": "abrir a rota sem login redireciona sem piscar conteúdo"
  },
  {
   "id": "frontend-rota-protegida-por-papel",
   "nome": "Rota protegida por papel",
   "receita": "guard no roteador lê o papel do usuário; sem permissão renderiza 403, não esconde só o botão",
   "prova": "usuário comum abrindo a URL de admin recebe 403"
  },
  {
   "id": "frontend-ssr-sem-erro-de-hidratacao",
   "nome": "SSR sem erro de hidratação",
   "receita": "não leia window, Date.now ou Math.random no render; mova para useEffect ou passe o valor do servidor",
   "prova": "console sem aviso \"Hydration failed\""
  },
  {
   "id": "frontend-dado-inicial-no-servidor",
   "nome": "Dado inicial no servidor",
   "receita": "busque no servidor (loader/getServerSideProps/server component) e passe como props; reidrate o cache do cliente com o mesmo dado",
   "prova": "HTML da página já contém o texto (curl mostra o conteúdo)"
  },
  {
   "id": "frontend-cache-de-requisicoes-do-cliente",
   "nome": "Cache de requisições do cliente",
   "receita": "use TanStack Query com queryKey estável, staleTime definido e invalidateQueries após mutação",
   "prova": "voltar à tela mostra dado instantâneo e atualiza em segundo plano"
  },
  {
   "id": "frontend-atualizacao-otimista",
   "nome": "Atualização otimista",
   "receita": "na mutação, grave o novo valor no cache em onMutate, guarde o anterior e restaure em onError",
   "prova": "com a API forçada a falhar, a interface volta ao valor antigo"
  },
  {
   "id": "frontend-modal-acessivel",
   "nome": "Modal acessível",
   "receita": "role=\"dialog\", aria-modal, foco preso dentro, Esc fecha, foco volta ao botão que abriu, fundo inerte",
   "prova": "navegar só pelo teclado: Tab não sai do modal e Esc devolve o foco"
  },
  {
   "id": "frontend-menu-dropdown-com-teclado",
   "nome": "Menu dropdown com teclado",
   "receita": "botão com aria-expanded e aria-controls; setas movem entre itens, Home/End vão às pontas, Esc fecha",
   "prova": "operar o menu inteiro sem mouse"
  },
  {
   "id": "frontend-tabs-acessiveis",
   "nome": "Tabs acessíveis",
   "receita": "tablist/tab/tabpanel com aria-selected; setas trocam de aba; só a aba ativa entra na ordem do Tab",
   "prova": "leitor de tela anuncia \"aba 2 de 4, selecionada\""
  },
  {
   "id": "frontend-skip-link-e-landmarks",
   "nome": "Skip link e landmarks",
   "receita": "primeiro elemento do body é link \"Pular para o conteúdo\"; use header, nav, main, footer uma vez cada",
   "prova": "Tab no carregamento revela o link e ele pula a navegação"
  },
  {
   "id": "frontend-foco-visivel-consistente",
   "nome": "Foco visível consistente",
   "receita": "nunca outline:none sem substituto; use :focus-visible com contorno de 2px e contraste de 3:1",
   "prova": "percorrer a página com Tab mostra o foco em todo controle"
  },
  {
   "id": "frontend-texto-alternativo-certo-em-imagem",
   "nome": "Texto alternativo certo em imagem",
   "receita": "imagem informativa descreve a função; decorativa leva alt=\"\"; imagem dentro de link descreve o destino",
   "prova": "auditoria de acessibilidade sem \"imagem sem alt\""
  },
  {
   "id": "frontend-anuncio-de-mudanca-dinamica",
   "nome": "Anúncio de mudança dinâmica",
   "receita": "região aria-live=\"polite\" para status de salvar e erros assíncronos; assertive só para erro crítico",
   "prova": "leitor de tela fala \"salvo\" sem mover o foco"
  },
  {
   "id": "frontend-skeleton-em-vez-de-spinner",
   "nome": "Skeleton em vez de spinner",
   "receita": "reserve o tamanho final do conteúdo com blocos cinza para evitar salto de layout",
   "prova": "CLS abaixo de 0,1 no Lighthouse"
  },
  {
   "id": "frontend-imagens-responsivas",
   "nome": "Imagens responsivas",
   "receita": "use srcset e sizes com larguras reais, width e height no img, loading=\"lazy\" abaixo da dobra e fetchpriority=\"high\" no hero",
   "prova": "LCP baixa e a aba Network baixa a variante certa por viewport"
  },
  {
   "id": "frontend-fonte-sem-salto",
   "nome": "Fonte sem salto",
   "receita": "font-display: swap, preload da fonte principal WOFF2 e size-adjust na fonte de fallback",
   "prova": "trocar a fonte não move o texto (CLS zero)"
  },
  {
   "id": "frontend-code-splitting-por-rota",
   "nome": "Code splitting por rota",
   "receita": "import() dinâmico por rota e por componente pesado; Suspense com fallback",
   "prova": "o bundle inicial cai e o analisador mostra chunks por rota"
  },
  {
   "id": "frontend-analisar-o-tamanho-do-bundle",
   "nome": "Analisar o tamanho do bundle",
   "receita": "rode o visualizador (rollup-plugin-visualizer ou webpack-bundle-analyzer) e troque a maior dependência por uma menor ou nativa",
   "prova": "tabela do tamanho antes e depois em KB gzip"
  },
  {
   "id": "frontend-evitar-re-render-desnecessario",
   "nome": "Evitar re-render desnecessário",
   "receita": "meça com o Profiler antes; mova estado para baixo, use memo só no componente caro e estabilize props com useMemo",
   "prova": "o Profiler mostra menos renderizações no mesmo gesto"
  },
  {
   "id": "frontend-tema-claro-e-escuro-persistente",
   "nome": "Tema claro e escuro persistente",
   "receita": "variáveis CSS por [data-theme]; script inline no head lê localStorage e prefers-color-scheme antes da pintura",
   "prova": "recarregar não pisca o tema errado"
  },
  {
   "id": "frontend-internacionalizacao-no-front",
   "nome": "Internacionalização no front",
   "receita": "chaves por namespace, i18next ou next-intl, formatação com Intl, idioma na URL e carregamento por demanda",
   "prova": "trocar o idioma atualiza todos os textos e as datas"
  },
  {
   "id": "frontend-datas-e-fuso-horario",
   "nome": "Datas e fuso horário",
   "receita": "guarde ISO em UTC, formate com Intl.DateTimeFormat no fuso do usuário; nunca monte data por concatenação",
   "prova": "mesma data mostra a hora local certa em dois fusos"
  },
  {
   "id": "frontend-upload-com-progresso",
   "nome": "Upload com progresso",
   "receita": "XMLHttpRequest ou fetch com stream mostra progresso; valide tipo e tamanho no cliente e de novo no servidor; permita cancelar",
   "prova": "arquivo grande mostra barra e o cancelamento interrompe o envio"
  },
  {
   "id": "frontend-arrastar-e-soltar-acessivel",
   "nome": "Arrastar e soltar acessível",
   "receita": "use dnd-kit com sensor de teclado e anúncios; ofereça botões de mover como alternativa",
   "prova": "reordenar a lista só pelo teclado funciona"
  },
  {
   "id": "frontend-error-boundary-util",
   "nome": "Error boundary útil",
   "receita": "envolva seções independentes, mostre mensagem e botão de tentar de novo, e reporte o erro ao Sentry com contexto",
   "prova": "lançar erro num componente derruba só a seção"
  },
  {
   "id": "frontend-fila-de-toasts",
   "nome": "Fila de toasts",
   "receita": "uma só região aria-live, limite de 3 visíveis, tempo proporcional ao texto e pausa ao passar o mouse",
   "prova": "disparar 10 avisos não empilha além do limite"
  },
  {
   "id": "frontend-layout-de-grade-responsivo",
   "nome": "Layout de grade responsivo",
   "receita": "grid com repeat(auto-fit, minmax(240px, 1fr)) e gap; container queries para o componente reagir ao próprio espaço",
   "prova": "redimensionar de 360 a 1440 px sem rolagem horizontal"
  },
  {
   "id": "frontend-evitar-layout-shift-em-anuncio-e-iframe",
   "nome": "Evitar layout shift em anúncio e iframe",
   "receita": "reserve altura com aspect-ratio ou min-height antes de carregar",
   "prova": "o CLS das páginas com embed fica abaixo de 0,1"
  },
  {
   "id": "frontend-estado-na-url",
   "nome": "Estado na URL",
   "receita": "filtros, ordenação e página vão na query string; leia com useSearchParams e use replace para não poluir o histórico",
   "prova": "copiar a URL e abrir em outra aba reproduz a tela"
  },
  {
   "id": "frontend-web-component-reutilizavel",
   "nome": "Web component reutilizável",
   "receita": "custom element com shadow DOM, atributos observados, eventos CustomEvent e partes estilizáveis por ::part",
   "prova": "usar o elemento em uma página sem framework funciona"
  },
  {
   "id": "frontend-mutacao-com-estado-de-envio",
   "nome": "Mutação com estado de envio",
   "receita": "desabilite o botão e mostre \"Salvando...\" durante a requisição; ignore duplo clique; trate erro com mensagem útil",
   "prova": "clicar duas vezes envia uma única requisição"
  },
  {
   "id": "frontend-virtualizacao-de-tabela-grande",
   "nome": "Virtualização de tabela grande",
   "receita": "react-window ou TanStack Virtual renderiza só as linhas visíveis com altura estimada",
   "prova": "tabela de 100 mil linhas rola a 60 fps"
  },
  {
   "id": "frontend-copiar-para-a-area-de-transferencia",
   "nome": "Copiar para a área de transferência",
   "receita": "navigator.clipboard.writeText com tratamento de falha e feedback \"Copiado\"; fallback com seleção em contexto não seguro",
   "prova": "colar em outro lugar entrega o texto exato"
  },
  {
   "id": "frontend-preview-de-imagem-antes-de-enviar",
   "nome": "Preview de imagem antes de enviar",
   "receita": "URL.createObjectURL do arquivo no input, revogue com revokeObjectURL ao trocar",
   "prova": "escolher a imagem mostra a miniatura sem enviar nada"
  },
  {
   "id": "frontend-rolagem-restaurada-e-ancoras",
   "nome": "Rolagem restaurada e âncoras",
   "receita": "history.scrollRestoration, scroll-margin-top para cabeçalho fixo e foco no heading ao trocar de rota",
   "prova": "voltar à lista leva ao mesmo ponto de rolagem"
  },
  {
   "id": "frontend-mascara-de-entrada-sem-dor",
   "nome": "Mascara de entrada sem dor",
   "receita": "use inputmode e autocomplete certos e formate ao sair do campo; nunca bloqueie colar",
   "prova": "celular abre o teclado numérico e o autofill funciona"
  },
  {
   "id": "frontend-testar-componente-pela-perspectiva-do-usuario",
   "nome": "Testar componente pela perspectiva do usuário",
   "receita": "Testing Library: consulte por papel e texto, dispare eventos reais com userEvent e espere o resultado visível",
   "prova": "o teste continua verde após refatorar o markup sem mudar o comportamento"
  }
 ]
}
