Skip to content

Commit 18801b8

Browse files
authored
Merge pull request #3 from prof-ramos/polish/landing-a11y-design-perf
Landing: a11y, design-system conformance & performance
2 parents db383e1 + 4364162 commit 18801b8

17 files changed

Lines changed: 553 additions & 187 deletions

.prettierignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.next
22
node_modules
33
coverage
4+
.remember/**
5+
docs/uiux-assets/**
46
pnpm-lock.yaml
57
.agents/skills/**/SKILL.md

docs/uiux-analysis.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Análise UI/UX — WebIngressos
2+
3+
Data da análise: 2026-07-31
4+
Escopo: landing page pública e candidatura ao Programa Piloto.
5+
6+
## Resumo executivo
7+
8+
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.
9+
10+
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.
11+
12+
### Pontos principais corrigidos
13+
14+
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.
15+
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.
16+
3. **Formulário sem agrupamento semântico** — contato e dados do evento agora são `fieldset`s com `legend` acessível.
17+
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.
18+
5. **Texto de eyebrow em brand-500** — pequenos textos de contexto foram movidos para brand-700, que tem contraste adequado em superfícies claras.
19+
20+
## Evidências técnicas
21+
22+
### Acessibilidade
23+
24+
- O layout mantém `lang="pt-BR"`, skip link, landmarks principais, labels associados e controles com targets de toque adequados.
25+
- O foco global usa anel de 3px com offset de 3px; componentes que já possuem foco customizado continuam preservando seus estilos.
26+
- O `FormControl` mantém `aria-describedby` e `aria-invalid`; `FormMessage` anuncia erros com `role="alert"`.
27+
- A navegação mobile mantém `aria-expanded`, `aria-controls` e labels distintos para navegação principal e móvel.
28+
- O movimento reduzido continua coberto por `prefers-reduced-motion`.
29+
30+
### Contraste calculado
31+
32+
Os valores abaixo foram calculados diretamente a partir dos tokens CSS atuais; não são resultados estimados de uma ferramenta visual.
33+
34+
| Combinação | Contraste | Referência WCAG AA |
35+
| ------------------------ | --------: | -----------------------------: |
36+
| brand-500 sobre branco | 3,38:1 | insuficiente para texto normal |
37+
| brand-700 sobre branco | 7,30:1 | aprovado |
38+
| ink-500 sobre ink-50 | 5,52:1 | aprovado |
39+
| destructive sobre branco | 5,06:1 | aprovado |
40+
41+
Por isso, brand-500 permanece reservado para detalhes e elementos decorativos, enquanto textos pequenos de contexto usam brand-700.
42+
43+
### Conversão e UX
44+
45+
- O CTA primário continua levando ao formulário do piloto.
46+
- O hero agora explicita o público e o caráter de candidatura sem compromisso.
47+
- 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.
48+
- Em validação inválida, o foco é movido para o primeiro campo com erro.
49+
- Em falha de rede/API, a pessoa recebe feedback visual persistente e anúncio para tecnologia assistiva, além do toast existente.
50+
51+
### Performance
52+
53+
- Os cinco PNGs selecionados foram convertidos para WebP, redimensionados e adicionados em `public/assets/landing`; o conjunto servido soma 95.928 bytes.
54+
- O ingresso principal usa carregamento prioritário no hero; as imagens abaixo da dobra usam lazy loading e dimensões explícitas.
55+
- Os PNGs originais chegaram a ser arquivados para curadoria, mas o acervo `docs/uiux-assets` foi removido do repositório (ver Atualização).
56+
- O dashboard decorativo continua sendo SVG/CSS e permanece marcado como ilustrativo.
57+
- Não há métrica Lighthouse registrada nesta rodada; qualquer pontuação futura deve ser medida em ambiente de produção ou preview equivalente.
58+
59+
### Assets adicionados
60+
61+
- `ticket-event.webp`: assinatura visual do hero.
62+
- `mobile-ticket.webp`, `qr-code-ticket.webp` e `checkmark-ticket.webp`: sequência operacional de venda, check-in e confirmação.
63+
- `concert-ticket.webp`: apoio visual da seção de público universitário.
64+
65+
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.
66+
67+
## Atualização (2026-07-31) — Auditoria de design
68+
69+
Uma auditoria contra o `DESIGN.md` reprovou os cinco `.webp` adicionados nesta rodada:
70+
eram renders 3D glossy multicolor (azul/laranja/verde), violando a regra de ilustração
71+
"traço fino, uma cor, nunca preenchido/multicolor/fotográfico", introduzindo azul e
72+
laranja fora da paleta verde-única (o pior caso no hero, acima da dobra) e mantendo a
73+
licença Envato pendente.
74+
75+
Ações aplicadas:
76+
77+
- Os cinco `.webp` foram removidos de `public/assets/landing/` (diretório eliminado),
78+
encerrando o risco de licença na aplicação servida.
79+
- **Hero**: o ingresso 3D flutuante foi removido; o `DashboardPreview` passa a ser o
80+
foco visual único.
81+
- **Fluxo operacional**: deixou de ser uma terceira faixa `brand-900` (que competia com
82+
a band de fechamento) e virou seção clara com cards brancos e ícones de traço verde
83+
(`lucide`), preservando a numeração de sequência 01/02/03.
84+
- **Público**: o render 3D foi trocado pela ilustração de traço `TicketLine` em verde; o
85+
painel de apoio deixou de ser verde (`bg-brand-50`) e passou a neutro (`bg-ink-100`),
86+
para não criar uma terceira faixa verde entre a band do piloto e o CTA final.
87+
88+
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.
89+
90+
### Performance e dependências
91+
92+
Diferente da primeira rodada (que foi feita sem dependências de runtime), esta etapa de
93+
performance **adicionou duas dependências de runtime deliberadamente**, com necessidade
94+
demonstrada:
95+
96+
- `@vercel/speed-insights` e `@vercel/analytics`, montados em `src/app/layout.tsx`. Como
97+
não há Google Chrome nesta máquina para rodar Lighthouse, o Speed Insights é o caminho
98+
para obter Core Web Vitals de campo reais após o deploy.
99+
- O formulário do piloto (abaixo da dobra) passou a ser carregado por `next/dynamic` com
100+
`ssr: false` + `IntersectionObserver`, tirando a stack de validação (`zod` +
101+
`react-hook-form` + `radix`) do first-load: **~338 KB → 222 KB gzip** no first-load da
102+
home (medido no build).
103+
104+
## Validação executada
105+
106+
- `pnpm format:check`: passou.
107+
- `pnpm lint`: passou sem erros; existe um warning preexistente em `.remember/tmp/last-ndc.ts`.
108+
- `pnpm typecheck`: passou.
109+
- `pnpm build`: passou; rotas estáticas e `/api/subscribe` foram compiladas.
110+
- `curl http://localhost:3000/`: respondeu `200` com headers de segurança e HTML contendo o novo contexto do piloto, formulário e menu.
111+
- Contraste dos tokens principais: calculado e registrado acima.
112+
113+
## Limitações e próximos passos
114+
115+
- 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.
116+
- O acervo `docs/uiux-assets` foi removido do repositório; não há mais arquivos de licença pendente versionados.
117+
- Não foram alterados contratos da API, banco de dados ou implantação.

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import nextTypeScript from "eslint-config-next/typescript"
55
export default defineConfig([
66
...nextVitals,
77
...nextTypeScript,
8-
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
8+
globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts", "docs/uiux-assets/**"]),
99
])

src/app/globals.css

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
--destructive-foreground: #ffffff;
5454
--border: var(--ink-200);
5555
--input: var(--ink-200);
56-
--ring: var(--brand-500);
56+
--ring: var(--brand-600);
57+
/* Single source for focus color: the 3px outline and component rings share it. */
58+
--focus-ring: var(--ring);
5759
--radius: 0.625rem;
5860
}
5961

@@ -74,6 +76,7 @@
7476
--color-accent-foreground: var(--accent-foreground);
7577
--color-destructive: var(--destructive);
7678
--color-destructive-foreground: var(--destructive-foreground);
79+
--color-focus-ring: var(--focus-ring);
7780
--color-border: var(--border);
7881
--color-input: var(--input);
7982
--color-ring: var(--ring);
@@ -144,16 +147,45 @@
144147
border-color: var(--color-border);
145148
}
146149

150+
html {
151+
scroll-padding-top: 5rem;
152+
}
153+
147154
body {
148155
background-color: var(--color-background);
149156
color: var(--color-foreground);
150157
-webkit-font-smoothing: antialiased;
158+
text-rendering: optimizeLegibility;
159+
}
160+
161+
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
162+
outline: 3px solid var(--focus-ring);
163+
outline-offset: 3px;
151164
}
152165

153166
::selection {
154167
background-color: var(--brand-100);
155168
color: var(--brand-800);
156169
}
170+
171+
a,
172+
button {
173+
touch-action: manipulation;
174+
}
175+
}
176+
177+
/* Respect prefers-reduced-motion for every transition/animation (accordion
178+
height, header mobile menu slide, card hover-lift) instead of hand-rolling
179+
a reduced variant per component. */
180+
@media (prefers-reduced-motion: reduce) {
181+
*,
182+
*::before,
183+
*::after {
184+
animation-duration: 0.01ms !important;
185+
animation-iteration-count: 1 !important;
186+
transition-duration: 0.01ms !important;
187+
scroll-behavior: auto !important;
188+
}
157189
}
158190

159191
/* Decorative dot grid, tinted with `text-*`. Replaces the /grid.svg that never existed. */

src/app/layout.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type { Metadata, Viewport } from "next"
22
import { Plus_Jakarta_Sans } from "next/font/google"
3+
import { Analytics } from "@vercel/analytics/next"
4+
import { SpeedInsights } from "@vercel/speed-insights/next"
35
import "./globals.css"
46
import { Toaster } from "@/components/ui/sonner"
57

@@ -28,8 +30,16 @@ export default function RootLayout({
2830
return (
2931
<html lang="pt-BR" className={`${jakarta.variable} scroll-smooth`}>
3032
<body className="bg-background text-foreground antialiased">
33+
<a
34+
href="#main-content"
35+
className="sr-only focus-visible:not-sr-only focus-visible:fixed focus-visible:top-4 focus-visible:left-4 focus-visible:z-100 focus-visible:rounded-xl focus-visible:bg-brand-700 focus-visible:px-4 focus-visible:py-2 focus-visible:text-sm focus-visible:font-bold focus-visible:text-white focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none"
36+
>
37+
Pular para o conteúdo
38+
</a>
3139
{children}
3240
<Toaster richColors position="top-center" />
41+
<SpeedInsights />
42+
<Analytics />
3343
</body>
3444
</html>
3545
)

src/app/obrigado/page.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { Logo } from "@/components/brand/logo"
55

66
export default function ThankYouPage() {
77
return (
8-
<main className="flex min-h-screen flex-col items-center justify-center px-5 py-16">
8+
<main
9+
id="main-content"
10+
className="flex min-h-screen flex-col items-center justify-center px-5 py-16"
11+
>
912
<div className="w-full max-w-lg rounded-card border border-border bg-white p-8 text-center shadow-card sm:p-10">
1013
<div className="flex justify-center">
1114
<Logo markClassName="size-7" wordmarkClassName="text-lg" />

src/app/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,27 @@ import { Problems } from "@/components/landing/problems"
44
import { Pillars } from "@/components/landing/pillars"
55
import { TargetAudience } from "@/components/landing/target-audience"
66
import { PilotBand } from "@/components/landing/pilot-band"
7-
import { PilotForm } from "@/components/landing/pilot-form"
87
import { Faq } from "@/components/landing/faq"
98
import { FinalCta } from "@/components/landing/final-cta"
109
import { Footer } from "@/components/landing/footer"
10+
import { OperationalFlow } from "@/components/landing/operational-flow"
11+
import { PilotFormLazy } from "@/components/landing/pilot-form-lazy"
1112
import { Section } from "@/components/landing/section"
1213

1314
export default function Home() {
1415
return (
1516
<>
1617
<Header />
17-
<main>
18+
<main id="main-content">
1819
<Hero />
1920
<Problems />
2021
<Pillars />
22+
<OperationalFlow />
2123
<TargetAudience />
2224
<PilotBand />
2325

2426
<Section id="piloto" className="py-8 sm:py-10">
25-
<PilotForm />
27+
<PilotFormLazy />
2628
</Section>
2729

2830
<Faq />

src/app/privacidade/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ArrowLeft } from "lucide-react"
33

44
export default function PrivacyPolicyPage() {
55
return (
6-
<main className="px-5 py-16 sm:py-20">
6+
<main id="main-content" className="px-5 py-16 sm:py-20">
77
<div className="mx-auto max-w-3xl">
88
<Link
99
href="/"

src/components/landing/header.tsx

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import { NAV_LINKS, SITE_CONFIG } from "@/lib/constants"
1010

1111
export function Header() {
1212
const [isMenuOpen, setIsMenuOpen] = React.useState(false)
13+
const menuButtonRef = React.useRef<HTMLButtonElement>(null)
14+
const firstMenuLinkRef = React.useRef<HTMLAnchorElement>(null)
15+
16+
React.useEffect(() => {
17+
if (!isMenuOpen) return
18+
19+
firstMenuLinkRef.current?.focus()
20+
21+
function handleKeyDown(event: KeyboardEvent) {
22+
if (event.key === "Escape") {
23+
setIsMenuOpen(false)
24+
menuButtonRef.current?.focus()
25+
}
26+
}
27+
28+
document.addEventListener("keydown", handleKeyDown)
29+
return () => document.removeEventListener("keydown", handleKeyDown)
30+
}, [isMenuOpen])
1331

1432
return (
1533
<header className="sticky top-0 z-50 border-b border-border bg-white/85 backdrop-blur-md">
@@ -19,7 +37,7 @@ export function Header() {
1937
<Logo markClassName="size-7 sm:size-8" wordmarkClassName="text-lg sm:text-xl" />
2038
</Link>
2139

22-
<nav className="hidden items-center gap-7 lg:flex">
40+
<nav aria-label="Navegação principal" className="hidden items-center gap-7 lg:flex">
2341
{NAV_LINKS.map((link) => (
2442
<a
2543
key={link.href}
@@ -41,6 +59,7 @@ export function Header() {
4159
</a>
4260

4361
<button
62+
ref={menuButtonRef}
4463
type="button"
4564
aria-label={isMenuOpen ? "Fechar menu" : "Abrir menu"}
4665
aria-expanded={isMenuOpen}
@@ -54,35 +73,35 @@ export function Header() {
5473
</div>
5574
</Shell>
5675

57-
{isMenuOpen ? (
58-
<div
59-
id="menu-principal"
60-
className="border-t border-border bg-white transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden"
61-
>
62-
<Shell>
63-
<nav className="flex flex-col gap-1 py-4">
64-
{NAV_LINKS.map((link) => (
65-
<a
66-
key={link.href}
67-
href={link.href}
68-
onClick={() => setIsMenuOpen(false)}
69-
className="rounded-xl px-3 py-3 text-base font-medium text-ink-700 transition-colors hover:bg-brand-50 hover:text-brand-700"
70-
>
71-
{link.label}
72-
</a>
73-
))}
76+
<div
77+
id="menu-principal"
78+
hidden={!isMenuOpen}
79+
className="border-t border-border bg-white transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden"
80+
>
81+
<Shell>
82+
<nav aria-label="Navegação móvel" className="flex flex-col gap-1 py-4">
83+
{NAV_LINKS.map((link, index) => (
7484
<a
75-
href={SITE_CONFIG.pilotFormUrl}
85+
key={link.href}
86+
ref={index === 0 ? firstMenuLinkRef : undefined}
87+
href={link.href}
7688
onClick={() => setIsMenuOpen(false)}
77-
className="mt-2 inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-brand-700 px-5 text-sm font-bold text-white transition-colors hover:bg-brand-800 sm:hidden"
89+
className="rounded-xl px-3 py-3 text-base font-medium text-ink-700 transition-colors hover:bg-brand-50 hover:text-brand-700"
7890
>
79-
{SITE_CONFIG.ctaLabel}
80-
<ArrowRight className="size-4" />
91+
{link.label}
8192
</a>
82-
</nav>
83-
</Shell>
84-
</div>
85-
) : null}
93+
))}
94+
<a
95+
href={SITE_CONFIG.pilotFormUrl}
96+
onClick={() => setIsMenuOpen(false)}
97+
className="mt-2 inline-flex h-12 items-center justify-center gap-2 rounded-xl bg-brand-700 px-5 text-sm font-bold text-white transition-colors hover:bg-brand-800 sm:hidden"
98+
>
99+
{SITE_CONFIG.ctaLabel}
100+
<ArrowRight className="size-4" />
101+
</a>
102+
</nav>
103+
</Shell>
104+
</div>
86105
</header>
87106
)
88107
}

0 commit comments

Comments
 (0)