-
Notifications
You must be signed in to change notification settings - Fork 0
Landing: a11y, design-system conformance & performance #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| .next | ||
| node_modules | ||
| coverage | ||
| .remember/** | ||
| docs/uiux-assets/** | ||
| pnpm-lock.yaml | ||
| .agents/skills/**/SKILL.md |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
Comment on lines
+96
to
+98
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 3 \
'Analytics|SpeedInsights|cookies?|consentimento|consent|opt.?out|privacidade|Vercel' \
src/app/layout.tsx src/app/privacidade/page.tsx src/componentsRepository: prof-ramos/webingressos-page Length of output: 3042 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '--- policy outline ---\n'
wc -l src/app/privacidade/page.tsx
printf '\n--- policy content ---\n'
cat -n src/app/privacidade/page.tsx
printf '\n--- layout content ---\n'
cat -n src/app/layout.tsx | sed -n '1,90p'
printf '\n--- broader privacy/analytics related terms ---\n'
rg -n -i 'analytics|speedinsights|cookie|cookies?|consentimento|consent|recusa|opt.?out|retenção|retencao|tratament|LGPD|Google|Vercel' src docs package.json | sed -n '1,240p'Repository: prof-ramos/webingressos-page Length of output: 257 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- policy size ---'
wc -l src/app/privacidade/page.tsx
printf '%s\n' ''
printf '%s\n' '--- policy content ---'
cat -n src/app/privacidade/page.tsx
printf '%s\n' ''
printf '%s\n' '--- layout content ---'
cat -n src/app/layout.tsx | sed -n '1,90p'
printf '%s\n' ''
printf '%s\n' '--- broader privacy/analytics related terms ---'
rg -n -i 'analytics|speedinsights|cookie|cookies?|consentimento|consent|recusa|opt.?out|retenção|retencao|tratament|LGPD|Google|Vercel' src docs package.json | sed -n '1,240p'Repository: prof-ramos/webingressos-page Length of output: 8167 Complete a cobertura do Analytics global na política. O layout global carrega 🧰 Tools🪛 LanguageTool[grammar] ~98-~98: Possível erro de concordância de número. (GENERAL_NUMBER_AGREEMENT_ERRORS) 🤖 Prompt for AI Agents |
||
| - 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. | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Normalize o valor de
Correção- text-rendering: optimizeLegibility;
+ text-rendering: optimizelegibility;📝 Committable suggestion
Suggested change
🧰 Tools🪛 Stylelint (17.14.1)[error] 158-158: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case) (value-keyword-case) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||
| } | ||||||
|
|
||||||
| :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. */ | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <html lang="pt-BR" className={`${jakarta.variable} scroll-smooth`}> | ||
| <body className="bg-background text-foreground antialiased"> | ||
| <a | ||
| href="#main-content" | ||
| 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" | ||
|
Comment on lines
+33
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## candidate files"
git ls-files | rg '(^|/)(globals\.css|tailwind\.config\.(js|ts|cjs|mjs)|package\.json|src/app/layout\.tsx|styles\.css|app.*global.*\.css)$' || true
echo
echo "## layout snippet"
if [ -f src/app/layout.tsx ]; then
nl -ba src/app/layout.tsx | sed -n '1,80p'
fi
echo
echo "## color/ring tailwind and CSS variables"
for f in $(git ls-files | rg '(\.css$|tailwind\.config\.(js|ts|cjs|mjs)$|globals\.css$|layout\.tsx$)'); do
echo "--- $f"
rg -n -- '--(ring|brand|background|primary|secondary|surface|focus|outline)|ring-focus-ring|ring-ring|focus-visible:ring|focus-visible:outline|outline-none|--brand-[0-9]+' "$f" || true
done
echo
echo "## exact class usage"
rg -n "ring-ring/50|ring-focus-ring|outline-none|focus-visible:outline" .Repository: prof-ramos/webingressos-page Length of output: 297 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## layout snippet"
cat -n src/app/layout.tsx | sed -n '1,80p'
echo
echo "## globals CSS and Tailwind config"
for f in src/app/globals.css package.json; do
echo "--- $f"
cat -n "$f"
done
echo
echo "## exact class usage"
rg -n "ring-ring/50|ring-focus-ring|outline-none|focus-visible:outline|--ring|--brand-[0-9]+" . || trueRepository: prof-ramos/webingressos-page Length of output: 20130 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## contrast probe for focus styles"
python3 - <<'PY'
import math
def wcag_ratio(c1, c2):
lum = []
for c in [c1, c2]:
srgb = [x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055) ** 2.4 for x in c]
lum.append(0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2])
return (max(lum) + 0.05) / (min(lum) + 0.05)
brand600 = (27/255, 127/255, 82/255)
ink50 = (249/255, 250/255, 252/255)
def alpha_over(fg, bg, alpha):
r = fg[0]*alpha + bg[0]*(1-alpha)
g = fg[1]*alpha + bg[1]*(1-alpha)
b = fg[2]*alpha + bg[2]*(1-alpha)
return (r, g, b)
print(f"--ring/--focus-ring = var(--brand-600) = {brand600}")
print(f"light background = var(--ink-50) = {ink50}")
print(f"0d-ring vs light: {wcag_ratio(brand600, ink50):.2f}")
print(f"50%-alpha ring vs light: {wcag_ratio(alpha_over(brand600, ink50, 0.5), ink50):.2f}")
print(f"50%-alpha ring vs white: {wcag_ratio(alpha_over(brand600, (1,1,1), 0.5), (1,1,1)):.2f}")
print(f"ring-focus-ring vs light: {wcag_ratio(brand600, ink50):.2f}")
PY
echo
echo "## nearby generated CSS declarations if files exist"
rg -n "focus-visible:outline-none|focus-visible:ring-3|focus-visible:ring-ring|outline: 3px|focus-ring|focus-visible" node_modules/.vite 2>/dev/null || trueRepository: prof-ramos/webingressos-page Length of output: 567 Passe o link de pulo para fora antes de esconder o contorno de foco.
🤖 Prompt for AI Agents |
||
| > | ||
| Pular para o conteúdo | ||
| </a> | ||
| {children} | ||
| <Toaster richColors position="top-center" /> | ||
| <SpeedInsights /> | ||
| <Analytics /> | ||
|
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When deployed, these components collect page-view and performance telemetry on every route, including from visitors who never submit the pilot form, while Useful? React with 👍 / 👎.
Comment on lines
+41
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Tracked files matching layout/privacy/analytics/speedinsights/package:"
git ls-files | rg '(^|/)(layout|page\.tsx|package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$|SpeedInsights|Analytics|speedinsights|analsytics' || true
echo
echo "src/app/layout.tsx around SpeedInsights/Analytics:"
if [ -f src/app/layout.tsx ]; then
nl -ba src/app/layout.tsx | sed -n '1,90p'
fi
echo
echo "Privacy page candidate:"
if [ -f src/app/privacidade/page.tsx ]; then
nl -ba src/app/privacidade/page.tsx | sed -n '1,220p'
fi
if [ -f src/app/privacidade/page.ts ]; then
echo "src/app/privacidade/page.ts exists"
fi
echo
echo "Search for consent / opt / Vercel policy / speed insights mentions:"
rg -n "SpeedInsights|Analytics|Vercel|tracking|privacy|privacidade|consent|consentimento|recusa|opt|analytics|telemetria|cookies" src package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
echo
echo "Dependency versions from package/lock:"
for f in package.json pnpm-lock.yaml yarn.lock package-lock.json; do
[ -f "$f" ] && echo "--- $f ---" && rg -n "`@vercel/analytics`|next" "$f" -m 20 || true
done
echo
echo "Root files listing package and lock availability:"
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$'Repository: prof-ramos/webingressos-page Length of output: 430 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Checking whether Vercel/next package metadata is present in sandbox:"
if [ -f node_modules/@vercel/analytics/package.json ]; then
cat node_modules/@vercel/analytics/package.json | jq '{version, main, repository}'
else
echo "`@vercel/analytics` not installed in node_modules"
fi
if [ -f node_modules/next/package.json ]; then
cat node_modules/next/package.json | jq '{version}'
else
echo "next not installed in node_modules"
fiRepository: prof-ramos/webingressos-page Length of output: 410 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "src/app/layout.tsx:"
awk '{printf "%5d %s\n", NR, $0}' src/app/layout.tsx
echo
echo "src/app/privacidade/page.tsx:"
awk '{printf "%5d %s\n", NR, $0}' src/app/privacidade/page.tsx
echo
echo "package versions/snippets:"
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
for (const k of ['dependencies','devDependencies']) {
console.log(k, {next: pkg[k]?.next, '`@vercel/analytics`': pkg[k]?.['`@vercel/analytics`']});
}
JS
echo
echo "lock relevant entries:"
awk '/^$/ {next} /(`@vercel/analytics`|next@|next:|specifier): {print; for(i=0;i<8;i++){getline; if(/^$/) break; print}}' pnpm-lock.yamlRepository: prof-ramos/webingressos-page Length of output: 7182 Atualize a política antes de publicar a telemetria.
🤖 Prompt for AI Agents |
||
| </body> | ||
| </html> | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,10 @@ import { Logo } from "@/components/brand/logo" | |
|
|
||
| export default function ThankYouPage() { | ||
| return ( | ||
| <main className="flex min-h-screen flex-col items-center justify-center px-5 py-16"> | ||
| <main | ||
| id="main-content" | ||
| className="flex min-h-screen flex-col items-center justify-center px-5 py-16" | ||
| > | ||
|
Comment on lines
+8
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Files:"
git ls-files | rg '(^|/)src/app/(obrigado|privacidade)/page\.tsx$' || true
echo
echo "Relevant snippets:"
for f in src/app/obrigado/page.tsx src/app/privacidade/page.tsx; do
echo "--- $f"
wc -l "$f"
sed -n '1,80p' "$f" | cat -n
done
echo
echo "Search for skip/main-content implementations:"
rg -n 'main-content|tabIndex=\{-1\}|skip|Skip|pular|Pular' src/app src/components src 2>/dev/null || trueRepository: prof-ramos/webingressos-page Length of output: 7703 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "src/app/page.tsx:"
sed -n '1,80p' src/app/page.tsx | cat -n
echo
echo "src/app/layout skip-link region:"
sed -n '20,45p' src/app/layout.tsx | cat -n
echo
echo "All main-content usages:"
python3 - <<'PY'
from pathlib import Path
for path in [Path("src/app/page.tsx"), Path("src/app/obrigado/page.tsx"), Path("src/app/privacidade/page.tsx")]:
lines = path.read_text().splitlines()
for i, line in enumerate(lines, 1):
if "main-content" in line or "tabIndex" in line:
print(f"{path}:{i}:{line}")
PYRepository: prof-ramos/webingressos-page Length of output: 2948 🌐 Web query:
💡 Result: To satisfy WCAG 2.4.1 (Bypass Blocks), implementing a "skip to main content" link is a highly effective and recommended technique [1][2]. A common technical challenge with this approach is ensuring that the keyboard focus actually moves to the target content when the link is activated, rather than just scrolling the page [1][3]. Setting tabindex="-1" on the target element is the standard and recommended practice to make this jump reliable [4][5][6]. Why tabindex="-1" is used: Standard HTML elements like ,, or are not natively focusable [5]. Browsers often fail to move focus to these elements during fragment navigation (when clicking an tag pointing to an ID) [7][6]. Adding tabindex="-1" allows an element to receive programmatic focus via JavaScript or the browser's internal focus management, without including it in the natural tab sequence of the page [1][4][6]. Implementation Best Practices: 1. Skip Link Placement: The skip link must be the very first focusable element in the DOM so that it is the first thing a user encounters when tabbing after page load [1][4][7]. 2. Target Configuration: Assign a unique ID to your main content container and add tabindex="-1" to it [1][5][6]. 3. Programmatic Focus: While many modern browsers handle focus movement to hash links automatically, relying solely on the href can be inconsistent [7][6]. It is best practice to attach a JavaScript click handler to the skip link that explicitly calls the.focus method on the target element [1][3][6]. 4. Visibility: Never hide the skip link using display: none or visibility: hidden, as these methods remove the element from the accessibility tree, making it invisible to screen readers and keyboard users [1][7][5]. Instead, use CSS to hide it visually (e.g., positioning it off-screen) and reveal it when it receives focus [1][4][3]. By following these steps, you ensure that the mechanism is robust, works across different browsers, and satisfies the requirements of WCAG 2.4.1 [5][6].Citations:
Bora fazer o foco ir para o conteúdo pular o link. O link “Pular para o conteúdo” só aponta para
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| <div className="w-full max-w-lg rounded-card border border-border bg-white p-8 text-center shadow-card sm:p-10"> | ||
| <div className="flex justify-center"> | ||
| <Logo markClassName="size-7" wordmarkClassName="text-lg" /> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,24 @@ import { NAV_LINKS, SITE_CONFIG } from "@/lib/constants" | |
|
|
||
| export function Header() { | ||
| const [isMenuOpen, setIsMenuOpen] = React.useState(false) | ||
| const menuButtonRef = React.useRef<HTMLButtonElement>(null) | ||
| const firstMenuLinkRef = React.useRef<HTMLAnchorElement>(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 ( | ||
| <header className="sticky top-0 z-50 border-b border-border bg-white/85 backdrop-blur-md"> | ||
|
|
@@ -19,7 +37,7 @@ export function Header() { | |
| <Logo markClassName="size-7 sm:size-8" wordmarkClassName="text-lg sm:text-xl" /> | ||
| </Link> | ||
|
|
||
| <nav className="hidden items-center gap-7 lg:flex"> | ||
| <nav aria-label="Navegação principal" className="hidden items-center gap-7 lg:flex"> | ||
| {NAV_LINKS.map((link) => ( | ||
| <a | ||
| key={link.href} | ||
|
|
@@ -41,6 +59,7 @@ export function Header() { | |
| </a> | ||
|
|
||
| <button | ||
| ref={menuButtonRef} | ||
| type="button" | ||
| aria-label={isMenuOpen ? "Fechar menu" : "Abrir menu"} | ||
| aria-expanded={isMenuOpen} | ||
|
|
@@ -54,35 +73,35 @@ export function Header() { | |
| </div> | ||
| </Shell> | ||
|
|
||
| {isMenuOpen ? ( | ||
| <div | ||
| id="menu-principal" | ||
| className="border-t border-border bg-white transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden" | ||
| > | ||
| <Shell> | ||
| <nav className="flex flex-col gap-1 py-4"> | ||
| {NAV_LINKS.map((link) => ( | ||
| <a | ||
| key={link.href} | ||
| href={link.href} | ||
| onClick={() => setIsMenuOpen(false)} | ||
| className="rounded-xl px-3 py-3 text-base font-medium text-ink-700 transition-colors hover:bg-brand-50 hover:text-brand-700" | ||
| > | ||
| {link.label} | ||
| </a> | ||
| ))} | ||
| <div | ||
| id="menu-principal" | ||
| hidden={!isMenuOpen} | ||
| className="border-t border-border bg-white transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden" | ||
| > | ||
| <Shell> | ||
| <nav aria-label="Navegação móvel" className="flex flex-col gap-1 py-4"> | ||
| {NAV_LINKS.map((link, index) => ( | ||
| <a | ||
| href={SITE_CONFIG.pilotFormUrl} | ||
| key={link.href} | ||
| ref={index === 0 ? firstMenuLinkRef : undefined} | ||
| href={link.href} | ||
| onClick={() => setIsMenuOpen(false)} | ||
| 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" | ||
| className="rounded-xl px-3 py-3 text-base font-medium text-ink-700 transition-colors hover:bg-brand-50 hover:text-brand-700" | ||
| > | ||
| {SITE_CONFIG.ctaLabel} | ||
| <ArrowRight className="size-4" /> | ||
| {link.label} | ||
| </a> | ||
| </nav> | ||
| </Shell> | ||
| </div> | ||
| ) : null} | ||
| ))} | ||
| <a | ||
| href={SITE_CONFIG.pilotFormUrl} | ||
| onClick={() => setIsMenuOpen(false)} | ||
| 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" | ||
| > | ||
| {SITE_CONFIG.ctaLabel} | ||
| <ArrowRight className="size-4" /> | ||
| </a> | ||
| </nav> | ||
| </Shell> | ||
| </div> | ||
|
Comment on lines
+76
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: In modern CSS, the transition-behavior property with the value allow-discrete enables transitions on properties that are typically animatable only in a discrete (all-or-nothing) manner, such as display and content-visibility [1][2][3]. By default, transition-behavior is set to normal, which prevents transitions for these discrete properties [4][2]. Key technical details: 1. Mechanism: When allow-discrete is used, the browser coordinates the discrete property change with other interpolable transitions (like opacity) [4][5]. For example, when animating from display: block to display: none, the browser maintains the block state throughout the animation duration and flips to none only at the end, allowing the other property transitions to complete [1][4][3]. 2. Implementation: You can apply this via the transition shorthand or the transition-behavior property [6][2]: transition: opacity 0.5s, display 0.5s allow-discrete; or transition-behavior: allow-discrete; 3. Starting State: When transitioning an element into the DOM or from display: none to a visible state, the Citations:
🌐 Web query:
💡 Result: The CSS feature you are referring to is transition-behavior: allow-discrete [1][2]. It was introduced to solve the long-standing challenge of animating elements to and from display: none, which previously required JavaScript to manage the timing of DOM removal or style changes [1][3][4]. While some early documentation or discussions may have informally referred to a desired functionality as "allow-display," the official property name is transition-behavior with the value allow-discrete [1][5]. The term discrete refers to properties that cannot be interpolated (like display, visibility, or mix-blend-mode), meaning they typically flip instantly between values rather than transitioning smoothly [1][6][5]. Setting transition-behavior: allow-discrete allows the browser to perform a transition before switching the discrete property state, typically at the 50% mark of the transition duration [1][5][4]. Browser support for transition-behavior: allow-discrete is now broadly available in modern browsers [7]: - Chrome: Supported since version 117 [8][7]. - Firefox: Supported since version 129 [8][6]. - Safari: Supported since version 17.4 [8][6]. To create smooth entry and exit animations, this property is typically used in conjunction with the Citations:
🌐 Web query:
💡 Result: The Content In this example, transition-discrete ensures the exit animation (e.g., opacity fading out) completes before the element is set to display: none [2][3]. The starting: variant ensures a smooth fade-in when the element is first displayed [6][7].
Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== candidate files =="
fd -a 'header\.tsx$|tailwind.*config|package.json|pnpm-lock.yaml|package-lock.json|yarn.lock' . | sed 's#^\./##' | head -80
echo
echo "== package-related tailwind versions =="
for f in $(fd 'package.json$' .); do
echo "-- $f"
rg -n '"tailwindcss"|`@tailwind`|scripts|postcss' "$f" || true
done
echo
echo "== header excerpt =="
f="$(fd 'header\.tsx$' . | head -1)"
if [ -n "$f" ]; then
echo "FILE=$f"
sed -n '1,180p' "$f" | nl -ba | sed -n '60,115p'
fi
echo
echo "== search for transition-discrete and display in Tailwind support =="
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'transition-discrete|transition-behavior|display'|head -80 || trueRepository: prof-ramos/webingressos-page Length of output: 561 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== header excerpt =="
sed -n '60,120p' src/components/landing/header.tsx
echo
echo "== transition/display CSS hints / Tailwind version =="
sed -n '1,70p' package.json
printf '\n'
rg -n 'transition-discrete|transition-behavior|display|starting:opacity|starting:-translate|starting:' src/pnpm-lock.yaml package.json || trueRepository: prof-ramos/webingressos-page Length of output: 3932 Dê cobertura pra transição de fechamento. O menu abre via 🎖️ Ajuste sugerido <div
id="menu-principal"
hidden={!isMenuOpen}
- className="border-t border-border bg-white transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden"
+ className="border-t border-border bg-white transition-discrete transition-[opacity,translate] duration-200 starting:-translate-y-2 starting:opacity-0 lg:hidden"
>🤖 Prompt for AI Agents |
||
| </header> | ||
| ) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Corrija as afirmações sobre o estado final.
Line 8-10afirma que esta rodada não adicionou dependências.Line 53-65afirma que cinco WebP foram adicionados e servidos. Porém,Line 77-102registra a remoção desses arquivos e a adição de duas dependências. Marque os primeiros trechos como histórico da primeira rodada ou substitua-os pelo estado final. Corrija isso antes do merge.Also applies to: 51-65
🤖 Prompt for AI Agents