Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .prettierignore
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
117 changes: 117 additions & 0 deletions docs/uiux-analysis.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.
Comment on lines +8 to +10

Copy link
Copy Markdown

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-10 afirma que esta rodada não adicionou dependências. Line 53-65 afirma que cinco WebP foram adicionados e servidos. Porém, Line 77-102 registra 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/uiux-analysis.md` around lines 8 - 10, Atualize os trechos introdutórios
e a seção que descreve os assets em docs/uiux-analysis.md para refletirem o
estado final registrado posteriormente: não afirme que nenhuma dependência foi
adicionada nem que cinco arquivos WebP permanecem servidos. Preserve essas
informações apenas se explicitamente marcadas como histórico da primeira rodada;
caso contrário, substitua-as pelo estado final com as duas dependências
adicionadas e os assets removidos, mantendo o restante da análise consistente.


### 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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/components

Repository: 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 <Analytics />, mas src/app/privacidade/page.tsx continua restrita ao formulário de candidatura e não aborda finalidade, dados tratados, retenção nem recusa do Analytics; o texto final não deve dizer que o Analytics é coberto até incluir essas regras.

🧰 Tools
🪛 LanguageTool

[grammar] ~98-~98: Possível erro de concordância de número.
Context: ...caminho para obter Core Web Vitals de campo reais após o deploy. - O formulário do piloto...

(GENERAL_NUMBER_AGREEMENT_ERRORS)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/uiux-analysis.md` around lines 96 - 98, Atualize a política descrita em
docs/uiux-analysis.md para cobrir o Analytics global carregado por Analytics em
src/app/layout.tsx, incluindo finalidade, dados tratados, retenção e formas de
recusa. Não afirme que o Analytics está coberto enquanto essas regras não
estiverem incluídas em src/app/privacidade/page.tsx.

- 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.
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/**"]),
])
34 changes: 33 additions & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown

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

Normalize o valor de text-rendering para passar no lint.

Stylelint sinaliza optimizeLegibility para a regra value-keyword-case. Use optimizelegibility nesta propriedade.

Correção
-    text-rendering: optimizeLegibility;
+    text-rendering: optimizelegibility;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
text-rendering: optimizeLegibility;
text-rendering: optimizelegibility;
🧰 Tools
🪛 Stylelint (17.14.1)

[error] 158-158: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/globals.css` at line 158, Update the text-rendering declaration in
the global stylesheet to use the lowercase optimizelegibility value required by
Stylelint’s value-keyword-case rule, leaving the surrounding styling unchanged.

Source: 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. */
Expand Down
10 changes: 10 additions & 0 deletions src/app/layout.tsx
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"

Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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]+" . || true

Repository: 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 || true

Repository: prof-ramos/webingressos-page

Length of output: 567


Passe o link de pulo para fora antes de esconder o contorno de foco.

focus-visible:outline-none mata a regra global de 3px solid var(--focus-ring). No fundo claro, ring-3 ring-ring/50 reduz o contraste do --brand-600 para cerca de 2:1, abaixo do 3:1 exigido pelo WCAG. Use ring-focus-ring sem opacidade no foco ou remova apenas outline-none para manter o contorno global.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/layout.tsx` around lines 33 - 35, Atualize o link de pulo no layout,
identificado pelo href="`#main-content`", para não remover o contorno global com
focus-visible:outline-none; alternativamente, substitua o anel sem opacidade por
ring-focus-ring. Preserve os demais estilos e garanta contraste de foco
compatível com WCAG.

>
Pular para o conteúdo
</a>
{children}
<Toaster richColors position="top-center" />
<SpeedInsights />
<Analytics />
Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disclose telemetry before enabling it globally

When deployed, these components collect page-view and performance telemetry on every route, including from visitors who never submit the pilot form, while /privacidade still describes only information supplied through that form and lists only form-related purposes. This introduces undisclosed collection; update the privacy disclosure and applicable legal-basis/consent handling, or leave telemetry disabled until that is done.

Useful? React with 👍 / 👎.

Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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"
fi

Repository: 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.yaml

Repository: prof-ramos/webingressos-page

Length of output: 7182


Atualize a política antes de publicar a telemetria.

<SpeedInsights /> e <Analytics /> estão ativos em todo o site. A página src/app/privacidade/page.tsx só aborda os dados do formulário e não documenta a telemetria da Vercel, os dados tratados, a finalidade, a retenção nem a opção de recusa. Alinhe a política. Se o fluxo exigir consentimento, carregue os componentes somente após a escolha do usuário. Confirme o comportamento das versões instaladas antes do merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/layout.tsx` around lines 41 - 42, Atualize a política de privacidade
em src/app/privacidade/page.tsx para documentar a telemetria de <SpeedInsights
/> e <Analytics />, incluindo dados tratados, finalidade, retenção e opção de
recusa; verifique também as versões instaladas para confirmar o comportamento
atual. Se essas versões exigirem consentimento, ajuste o fluxo em layout.tsx
para renderizar os componentes somente após o usuário consentir.

</body>
</html>
)
Expand Down
5 changes: 4 additions & 1 deletion src/app/obrigado/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 || true

Repository: 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}")
PY

Repository: prof-ramos/webingressos-page

Length of output: 2948


🌐 Web query:

WCAG 2.4.1 Bypass blocks programmatically setting focus jump target tabindex

💡 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 #main-content, e esses destinos não recebem foco programático. Adicione tabIndex={-1} em todos os <main id="main-content">:

  • src/app/obrigado/page.tsx#L8-L11
  • src/app/privacidade/page.tsx#L6
  • src/app/page.tsx#L18
📍 Affects 2 files
  • src/app/obrigado/page.tsx#L8-L11 (this comment)
  • src/app/privacidade/page.tsx#L6-L6
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/obrigado/page.tsx` around lines 8 - 11, Enable programmatic focus on
every main element with id="main-content" by adding tabIndex={-1} in
src/app/obrigado/page.tsx lines 8-11 and src/app/privacidade/page.tsx line 6;
also apply the same change to the corresponding main element in src/app/page.tsx
as requested.

<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" />
Expand Down
8 changes: 5 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<Header />
<main>
<main id="main-content">
<Hero />
<Problems />
<Pillars />
<OperationalFlow />
<TargetAudience />
<PilotBand />

<Section id="piloto" className="py-8 sm:py-10">
<PilotForm />
<PilotFormLazy />
</Section>

<Faq />
Expand Down
2 changes: 1 addition & 1 deletion src/app/privacidade/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ArrowLeft } from "lucide-react"

export default function PrivacyPolicyPage() {
return (
<main className="px-5 py-16 sm:py-20">
<main id="main-content" className="px-5 py-16 sm:py-20">
<div className="mx-auto max-w-3xl">
<Link
href="/"
Expand Down
71 changes: 45 additions & 26 deletions src/components/landing/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand All @@ -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}
Expand All @@ -41,6 +59,7 @@ export function Header() {
</a>

<button
ref={menuButtonRef}
type="button"
aria-label={isMenuOpen ? "Fechar menu" : "Abrir menu"}
aria-expanded={isMenuOpen}
Expand All @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

MDN display property CSS transitions visibility hidden discrete discrete transition-discrete allow-discrete browser support

💡 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 @starting-style at-rule is often required to define the initial state, as transitions are not triggered on an element's first style update [1][7][4]. 4. Browser Support: Support for transition-behavior: allow-discrete is generally available in modern browsers, including Chrome (117+), Edge (117+), Firefox (129+), and Safari (17.4+) [8][9][10]. Unlike display, the visibility property is historically considered an interpolable property (though it animates discretely, switching to visible at 0% and hidden at the end) and does not strictly require allow-discrete to animate, though it is often used in conjunction with these modern techniques to manage accessibility and layout transitions [11]. [1][8][10]

Citations:


🌐 Web query:

developer.chrome blog exit-animations display transition-discrete allow-display Safari Firefox transition-discrete

💡 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 @starting-style at-rule [1][3]. The @starting-style rule defines the initial state for entry animations, preventing the browser from skipping the transition when an element is first added to the DOM or changes from display: none [1][2][4]. Without these combined features, animating an element out of display: none typically results in an immediate, jumpy appearance or disappearance [3][2]. For implementation, it is often recommended to use the transition shorthand and place the transition-behavior declaration after it, or to explicitly include allow-discrete within the shorthand transition definition [1][9][5]. Because older browsers will not support these properties, it is best practice to treat them as a progressive enhancement [3][4]. You can detect support using CSS feature queries like @supports (transition-behavior: allow-discrete) {... } [3].

Citations:


🌐 Web query:

"transition-discrete" display none Tailwind CSS

💡 Result:

The transition-discrete utility in Tailwind CSS is a wrapper for the CSS property transition-behavior: allow-discrete [1][2]. It is used to enable transitions for properties that are traditionally "discrete" and cannot be animated, most notably display [1][3]. Normally, when an element is toggled to display: none, it disappears instantly because the browser removes it from the layout, preventing any accompanying animations (like opacity) from playing [2][3]. By using transition-discrete, you instruct the browser to defer the display property change until the end of the transition timeline [2]. To effectively transition an element in and out of display: none, you must combine transition-discrete with other Tailwind utilities: 1. Transition Property: Ensure display is included in your transition properties. Tailwind's default transition utility now includes display by default [4][5]. 2. State Management: Toggle the element's visibility state (e.g., using hidden and block, or state-based variants) [1][2]. 3. Starting Style: To animate an element as it first appears (since transitions typically do not run on an element's first render or when switching from display: none to display: block), use the starting: variant [6][7]. This utilizes the CSS @starting-style rule [6][2]. Example pattern:

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 || true

Repository: 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 || true

Repository: prof-ramos/webingressos-page

Length of output: 3932


Dê cobertura pra transição de fechamento.

O menu abre via display do atributo hidden, mas esse estado não é tratado como propriedade discreta. Quando isMenuOpen vira false, display: none é aplicado na hora e corta opacity/translate. Adicione transition-discrete no div#menu-principal para fechar com animação em browsers modernos.

🎖️ 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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/header.tsx` around lines 76 - 104, Update the
div#menu-principal className to include the transition-discrete utility,
preserving the existing opacity and translate transitions so closing the menu
animates before hidden applies display:none in modern browsers.

</header>
)
}
Loading