diff --git a/.prettierignore b/.prettierignore index 7c35acb..05e2689 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,7 @@ .next node_modules coverage +.remember/** +docs/uiux-assets/** pnpm-lock.yaml .agents/skills/**/SKILL.md diff --git a/docs/uiux-analysis.md b/docs/uiux-analysis.md new file mode 100644 index 0000000..edd89e1 --- /dev/null +++ b/docs/uiux-analysis.md @@ -0,0 +1,117 @@ +# Análise UI/UX — WebIngressos + +Data da análise: 2026-07-31 +Escopo: landing page pública e candidatura ao Programa Piloto. + +## Resumo executivo + +A landing já tinha uma base consistente: Next.js 16, React 19, Tailwind 4, HTML semântico básico, skip link, foco visível, formulário com React Hook Form + Zod, feedback via Sonner e ilustrações SVG leves. + +O foco desta rodada foi conversão da candidatura, sem trocar a identidade visual nem adicionar dependências de runtime. A proposta continua usando evergreen/mint e o motivo de ingresso/perfuração como assinatura própria; cinco assets locais foram adicionados de forma pontual como apoio visual. + +### Pontos principais corrigidos + +1. **Menu mobile sem ciclo de foco previsível** — o primeiro item agora recebe foco ao abrir e `Escape` fecha o menu e devolve foco ao botão. +2. **Erros do formulário pouco anunciados** — mensagens individuais agora usam `role="alert"`; o envio e falhas de rede são anunciados por região viva. +3. **Formulário sem agrupamento semântico** — contato e dados do evento agora são `fieldset`s com `legend` acessível. +4. **Proposta de valor pouco contextualizada acima da dobra** — o hero informa imediatamente que se trata do programa piloto e explicita o prazo de retorno já descrito no FAQ. +5. **Texto de eyebrow em brand-500** — pequenos textos de contexto foram movidos para brand-700, que tem contraste adequado em superfícies claras. + +## Evidências técnicas + +### Acessibilidade + +- O layout mantém `lang="pt-BR"`, skip link, landmarks principais, labels associados e controles com targets de toque adequados. +- O foco global usa anel de 3px com offset de 3px; componentes que já possuem foco customizado continuam preservando seus estilos. +- O `FormControl` mantém `aria-describedby` e `aria-invalid`; `FormMessage` anuncia erros com `role="alert"`. +- A navegação mobile mantém `aria-expanded`, `aria-controls` e labels distintos para navegação principal e móvel. +- O movimento reduzido continua coberto por `prefers-reduced-motion`. + +### Contraste calculado + +Os valores abaixo foram calculados diretamente a partir dos tokens CSS atuais; não são resultados estimados de uma ferramenta visual. + +| Combinação | Contraste | Referência WCAG AA | +| ------------------------ | --------: | -----------------------------: | +| brand-500 sobre branco | 3,38:1 | insuficiente para texto normal | +| brand-700 sobre branco | 7,30:1 | aprovado | +| ink-500 sobre ink-50 | 5,52:1 | aprovado | +| destructive sobre branco | 5,06:1 | aprovado | + +Por isso, brand-500 permanece reservado para detalhes e elementos decorativos, enquanto textos pequenos de contexto usam brand-700. + +### Conversão e UX + +- O CTA primário continua levando ao formulário do piloto. +- O hero agora explicita o público e o caráter de candidatura sem compromisso. +- O formulário informa que a candidatura não garante vaga e que o retorno ocorre em até 48 horas úteis, conteúdo já presente no FAQ. +- Em validação inválida, o foco é movido para o primeiro campo com erro. +- Em falha de rede/API, a pessoa recebe feedback visual persistente e anúncio para tecnologia assistiva, além do toast existente. + +### Performance + +- Os cinco PNGs selecionados foram convertidos para WebP, redimensionados e adicionados em `public/assets/landing`; o conjunto servido soma 95.928 bytes. +- O ingresso principal usa carregamento prioritário no hero; as imagens abaixo da dobra usam lazy loading e dimensões explícitas. +- Os PNGs originais chegaram a ser arquivados para curadoria, mas o acervo `docs/uiux-assets` foi removido do repositório (ver Atualização). +- O dashboard decorativo continua sendo SVG/CSS e permanece marcado como ilustrativo. +- Não há métrica Lighthouse registrada nesta rodada; qualquer pontuação futura deve ser medida em ambiente de produção ou preview equivalente. + +### Assets adicionados + +- `ticket-event.webp`: assinatura visual do hero. +- `mobile-ticket.webp`, `qr-code-ticket.webp` e `checkmark-ticket.webp`: sequência operacional de venda, check-in e confirmação. +- `concert-ticket.webp`: apoio visual da seção de público universitário. + +Os arquivos têm licença de produção ainda não confirmada; a publicação comercial depende da validação da licença oficial do Envato. + +## Atualização (2026-07-31) — Auditoria de design + +Uma auditoria contra o `DESIGN.md` reprovou os cinco `.webp` adicionados nesta rodada: +eram renders 3D glossy multicolor (azul/laranja/verde), violando a regra de ilustração +"traço fino, uma cor, nunca preenchido/multicolor/fotográfico", introduzindo azul e +laranja fora da paleta verde-única (o pior caso no hero, acima da dobra) e mantendo a +licença Envato pendente. + +Ações aplicadas: + +- Os cinco `.webp` foram removidos de `public/assets/landing/` (diretório eliminado), + encerrando o risco de licença na aplicação servida. +- **Hero**: o ingresso 3D flutuante foi removido; o `DashboardPreview` passa a ser o + foco visual único. +- **Fluxo operacional**: deixou de ser uma terceira faixa `brand-900` (que competia com + a band de fechamento) e virou seção clara com cards brancos e ícones de traço verde + (`lucide`), preservando a numeração de sequência 01/02/03. +- **Público**: o render 3D foi trocado pela ilustração de traço `TicketLine` em verde; o + painel de apoio deixou de ser verde (`bg-brand-50`) e passou a neutro (`bg-ink-100`), + para não criar uma terceira faixa verde entre a band do piloto e o CTA final. + +Em seguida, todo o acervo de referência `docs/uiux-assets` (~413 MB de pacotes Envato não referenciados pelo app) foi removido do repositório, encerrando de vez a exposição à licença pendente. + +### Performance e dependências + +Diferente da primeira rodada (que foi feita sem dependências de runtime), esta etapa de +performance **adicionou duas dependências de runtime deliberadamente**, com necessidade +demonstrada: + +- `@vercel/speed-insights` e `@vercel/analytics`, montados em `src/app/layout.tsx`. Como + não há Google Chrome nesta máquina para rodar Lighthouse, o Speed Insights é o caminho + para obter Core Web Vitals de campo reais após o deploy. +- O formulário do piloto (abaixo da dobra) passou a ser carregado por `next/dynamic` com + `ssr: false` + `IntersectionObserver`, tirando a stack de validação (`zod` + + `react-hook-form` + `radix`) do first-load: **~338 KB → 222 KB gzip** no first-load da + home (medido no build). + +## Validação executada + +- `pnpm format:check`: passou. +- `pnpm lint`: passou sem erros; existe um warning preexistente em `.remember/tmp/last-ndc.ts`. +- `pnpm typecheck`: passou. +- `pnpm build`: passou; rotas estáticas e `/api/subscribe` foram compiladas. +- `curl http://localhost:3000/`: respondeu `200` com headers de segurança e HTML contendo o novo contexto do piloto, formulário e menu. +- Contraste dos tokens principais: calculado e registrado acima. + +## Limitações e próximos passos + +- Ainda é necessário executar Lighthouse em uma URL servida e testar manualmente com teclado/leitor de tela em navegador real; a tentativa automatizada de abrir o navegador local expirou antes de permitir essa inspeção. +- O acervo `docs/uiux-assets` foi removido do repositório; não há mais arquivos de licença pendente versionados. +- Não foram alterados contratos da API, banco de dados ou implantação. diff --git a/eslint.config.mjs b/eslint.config.mjs index d4eba46..f3b2f5c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,5 +5,5 @@ import nextTypeScript from "eslint-config-next/typescript" export default defineConfig([ ...nextVitals, ...nextTypeScript, - globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]), + globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "docs/uiux-assets/**"]), ]) diff --git a/src/app/globals.css b/src/app/globals.css index 469c4e6..33a4c9c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -53,7 +53,9 @@ --destructive-foreground: #ffffff; --border: var(--ink-200); --input: var(--ink-200); - --ring: var(--brand-500); + --ring: var(--brand-600); + /* Single source for focus color: the 3px outline and component rings share it. */ + --focus-ring: var(--ring); --radius: 0.625rem; } @@ -74,6 +76,7 @@ --color-accent-foreground: var(--accent-foreground); --color-destructive: var(--destructive); --color-destructive-foreground: var(--destructive-foreground); + --color-focus-ring: var(--focus-ring); --color-border: var(--border); --color-input: var(--input); --color-ring: var(--ring); @@ -144,16 +147,45 @@ border-color: var(--color-border); } + html { + scroll-padding-top: 5rem; + } + body { background-color: var(--color-background); color: var(--color-foreground); -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + } + + :where(a, button, input, select, textarea, [tabindex]):focus-visible { + outline: 3px solid var(--focus-ring); + outline-offset: 3px; } ::selection { background-color: var(--brand-100); color: var(--brand-800); } + + a, + button { + touch-action: manipulation; + } +} + +/* Respect prefers-reduced-motion for every transition/animation (accordion + height, header mobile menu slide, card hover-lift) instead of hand-rolling + a reduced variant per component. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } /* Decorative dot grid, tinted with `text-*`. Replaces the /grid.svg that never existed. */ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e873c85..ee791bc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,7 @@ import type { Metadata, Viewport } from "next" import { Plus_Jakarta_Sans } from "next/font/google" +import { Analytics } from "@vercel/analytics/next" +import { SpeedInsights } from "@vercel/speed-insights/next" import "./globals.css" import { Toaster } from "@/components/ui/sonner" @@ -28,8 +30,16 @@ export default function RootLayout({ return ( + + Pular para o conteúdo + {children} + + ) diff --git a/src/app/obrigado/page.tsx b/src/app/obrigado/page.tsx index 6e3f457..b5d4a5f 100644 --- a/src/app/obrigado/page.tsx +++ b/src/app/obrigado/page.tsx @@ -5,7 +5,10 @@ import { Logo } from "@/components/brand/logo" export default function ThankYouPage() { return ( -
+
diff --git a/src/app/page.tsx b/src/app/page.tsx index e19d93f..695892d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,25 +4,27 @@ import { Problems } from "@/components/landing/problems" import { Pillars } from "@/components/landing/pillars" import { TargetAudience } from "@/components/landing/target-audience" import { PilotBand } from "@/components/landing/pilot-band" -import { PilotForm } from "@/components/landing/pilot-form" import { Faq } from "@/components/landing/faq" import { FinalCta } from "@/components/landing/final-cta" import { Footer } from "@/components/landing/footer" +import { OperationalFlow } from "@/components/landing/operational-flow" +import { PilotFormLazy } from "@/components/landing/pilot-form-lazy" import { Section } from "@/components/landing/section" export default function Home() { return ( <>
-
+
+
- +
diff --git a/src/app/privacidade/page.tsx b/src/app/privacidade/page.tsx index d77d43b..672ea84 100644 --- a/src/app/privacidade/page.tsx +++ b/src/app/privacidade/page.tsx @@ -3,7 +3,7 @@ import { ArrowLeft } from "lucide-react" export default function PrivacyPolicyPage() { return ( -
+
(null) + const firstMenuLinkRef = React.useRef(null) + + React.useEffect(() => { + if (!isMenuOpen) return + + firstMenuLinkRef.current?.focus() + + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") { + setIsMenuOpen(false) + menuButtonRef.current?.focus() + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isMenuOpen]) return (
@@ -19,7 +37,7 @@ export function Header() { -
) } diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx index 80451a6..76ea99f 100644 --- a/src/components/landing/hero.tsx +++ b/src/components/landing/hero.tsx @@ -10,6 +10,9 @@ export function Hero() {
+

+ Programa piloto para eventos universitários +

{/* Explicit breaks from lg up so the three lines land exactly as in the reference design; below lg the copy wraps naturally. */}

@@ -38,9 +41,16 @@ export function Hero() { Conhecer a proposta

+ +

+ Candidatura sem compromisso. Retornamos em até 48 horas úteis para entender o seu + evento. +

- +
+ +
diff --git a/src/components/landing/operational-flow.tsx b/src/components/landing/operational-flow.tsx new file mode 100644 index 0000000..5c29974 --- /dev/null +++ b/src/components/landing/operational-flow.tsx @@ -0,0 +1,52 @@ +import { QrCode, Smartphone, TicketCheck } from "lucide-react" + +import { Section, SectionHeading } from "@/components/landing/section" + +const FLOW_STEPS = [ + { + title: "Ingresso digital", + description: "A pessoa compra e acessa o ingresso pelo celular, sem fila e sem papel.", + icon: Smartphone, + }, + { + title: "Check-in por QR Code", + description: "A equipe valida a entrada com rapidez e clareza na porta do evento.", + icon: QrCode, + }, + { + title: "Confirmação da entrada", + description: "O evento acompanha em tempo real o público que já entrou.", + icon: TicketCheck, + }, +] as const + +export function OperationalFlow() { + return ( +
+ + +
    + {FLOW_STEPS.map((step, index) => ( +
  1. +
    + + + + {String(index + 1).padStart(2, "0")} + +
    +

    {step.title}

    +

    {step.description}

    +
  2. + ))} +
+
+ ) +} diff --git a/src/components/landing/pilot-band.tsx b/src/components/landing/pilot-band.tsx index 7c5ce1d..bc63e11 100644 --- a/src/components/landing/pilot-band.tsx +++ b/src/components/landing/pilot-band.tsx @@ -6,11 +6,11 @@ import { PILOT_BENEFITS } from "@/lib/constants" export function PilotBand() { return ( -
+
-

+

Programa piloto

diff --git a/src/components/landing/pilot-form-lazy.tsx b/src/components/landing/pilot-form-lazy.tsx new file mode 100644 index 0000000..160e16c --- /dev/null +++ b/src/components/landing/pilot-form-lazy.tsx @@ -0,0 +1,60 @@ +"use client" + +import * as React from "react" +import dynamic from "next/dynamic" + +/** + * Placeholder that mirrors the form's card container and reserves height, so the + * swap to the real form doesn't shift the page. Kept intentionally quiet. + */ +function FormSkeleton() { + return ( +