Compare commits
9 Commits
perf/man-1
...
merge54
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d1ca40c11 | |||
| 213d6843e7 | |||
| f8765add85 | |||
| 168a21e459 | |||
| 7902b35393 | |||
| 28e25b4464 | |||
| 8afb925e17 | |||
| 338e3c9adb | |||
| 0ecae4c7d6 |
6
.github/workflows/ci.yml
vendored
@@ -205,15 +205,12 @@ jobs:
|
|||||||
- run: npm run build
|
- run: npm run build
|
||||||
- run: npx playwright install chromium --with-deps
|
- run: npx playwright install chromium --with-deps
|
||||||
- run: php artisan migrate --force
|
- run: php artisan migrate --force
|
||||||
- run: php artisan db:seed --class=VisualContentSeeder --force
|
|
||||||
- run: php artisan storage:link
|
- run: php artisan storage:link
|
||||||
|
|
||||||
- name: Build application image
|
- name: Build application image
|
||||||
run: docker build -t amare-app:ci .
|
run: docker build -t amare-app:ci .
|
||||||
|
|
||||||
- name: Run browser tests against FrankenPHP container
|
- name: Run browser tests against FrankenPHP container
|
||||||
env:
|
|
||||||
APP_FROZEN_NOW: "2026-03-15T12:00:00-03:00"
|
|
||||||
run: |
|
run: |
|
||||||
docker run -d --name amare-web \
|
docker run -d --name amare-web \
|
||||||
-e APP_ENV=testing \
|
-e APP_ENV=testing \
|
||||||
@@ -222,7 +219,6 @@ jobs:
|
|||||||
-e APP_LOCALE=pt_BR \
|
-e APP_LOCALE=pt_BR \
|
||||||
-e APP_FALLBACK_LOCALE=pt_BR \
|
-e APP_FALLBACK_LOCALE=pt_BR \
|
||||||
-e APP_TIMEZONE=America/Sao_Paulo \
|
-e APP_TIMEZONE=America/Sao_Paulo \
|
||||||
-e APP_FROZEN_NOW="${APP_FROZEN_NOW}" \
|
|
||||||
-e DB_CONNECTION=pgsql \
|
-e DB_CONNECTION=pgsql \
|
||||||
-e DB_HOST=host.docker.internal \
|
-e DB_HOST=host.docker.internal \
|
||||||
-e DB_PORT=5432 \
|
-e DB_PORT=5432 \
|
||||||
@@ -253,8 +249,6 @@ jobs:
|
|||||||
mkdir -p artifacts/browser
|
mkdir -p artifacts/browser
|
||||||
docker logs amare-web > artifacts/browser/container.log 2>&1 || true
|
docker logs amare-web > artifacts/browser/container.log 2>&1 || true
|
||||||
cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true
|
cp -R storage/logs artifacts/browser/app-logs 2>/dev/null || true
|
||||||
cp -R tests/Browser/Screenshots artifacts/browser/screenshots 2>/dev/null || true
|
|
||||||
cp -R tests/.pest artifacts/browser/pest 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Upload browser failure artifacts
|
- name: Upload browser failure artifacts
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|||||||
1
.gitignore
vendored
@@ -18,7 +18,6 @@
|
|||||||
/public/fonts-manifest.dev.json
|
/public/fonts-manifest.dev.json
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
/tests/Browser/Screenshots
|
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/storage/pail
|
/storage/pail
|
||||||
/vendor
|
/vendor
|
||||||
|
|||||||
13
CLAUDE.md
@@ -53,18 +53,9 @@ Site-wide content is a singleton row reached via `SiteSetting::instance()`. Publ
|
|||||||
|
|
||||||
`App\Application` is **not** covered by that rule. `app/Domain/` currently holds a single placeholder (`DomainModule.php`); business reads live in `app/Application/Queries`. Extend the arch test when you add a boundary.
|
`App\Application` is **not** covered by that rule. `app/Domain/` currently holds a single placeholder (`DomainModule.php`); business reads live in `app/Application/Queries`. Extend the arch test when you add a boundary.
|
||||||
|
|
||||||
## Visual regression — read before touching baselines
|
## Deterministic test support
|
||||||
|
|
||||||
- Baselines are committed `.snap` files under `tests/.pest/snapshots/Browser/VisualRegressionTest/`.
|
`APP_FROZEN_NOW` configures `CarbonImmutable::setTestNow()` through `AppServiceProvider::freezeClockWhenConfigured()` outside production. `VisualContentSeeder` provides deterministic image/content fixtures for tests that explicitly need them. Neither setting is a browser-CI global default.
|
||||||
- `tests/Browser/Screenshots/` is gitignored — it only holds diff output.
|
|
||||||
- Regenerate with `composer visual:update`.
|
|
||||||
- **Baselines are CI-parity artifacts.** CI runs the browser suite against a `docker build`-produced FrankenPHP container (see the `browser` job in `.github/workflows/ci.yml`), so baselines regenerated on macOS against a local server will be rejected by CI. Commit `4578457` exists because of this.
|
|
||||||
|
|
||||||
Determinism relies on three cooperating pieces:
|
|
||||||
|
|
||||||
- `APP_FROZEN_NOW` → `CarbonImmutable::setTestNow()` in `AppServiceProvider::freezeClockWhenConfigured()` (no-op in production).
|
|
||||||
- `Database\Seeders\VisualContentSeeder::FROZEN_NOW` — the value the browser tests and the CI job both pin to.
|
|
||||||
- `Tests\Support\StableScreenshot` — forces Arial, disables transitions/animations, scrolls the page to settle lazy images, and avoids the flaky `networkidle` wait.
|
|
||||||
|
|
||||||
## Other things that bite
|
## Other things that bite
|
||||||
|
|
||||||
|
|||||||
28
PRODUCT.md
@@ -14,36 +14,42 @@ web
|
|||||||
|
|
||||||
## Product Purpose
|
## Product Purpose
|
||||||
|
|
||||||
A Amare Assessoria usa uma única plataforma para apresentar sua marca, converter visitantes em oportunidades e conduzir a operação dos eventos. O produto deve reduzir planilhas e informações dispersas, preservar o fluxo de trabalho da equipe e deixar claro o que exige atenção.
|
No recorte de lançamento, a Amare Assessoria usa o site institucional e seu CMS para apresentar a marca, publicar conteúdo e converter visitantes em pedidos de proposta por e-mail. O produto futuro continua planejado como uma única plataforma para conduzir leads e a operação dos eventos, mas essa evolução não faz parte do lançamento de 31/08/2026.
|
||||||
|
|
||||||
Sucesso significa:
|
Sucesso no lançamento significa:
|
||||||
|
|
||||||
- visitantes qualificados concluírem o briefing;
|
- visitantes qualificados concluírem o briefing;
|
||||||
|
- a assessora atualizar os conteúdos essenciais sem depender de desenvolvimento;
|
||||||
|
- o site equilibrar eventos sociais e corporativos com conteúdo real e autorizado;
|
||||||
|
- acessibilidade, desempenho, jornadas críticas e deploy serem verificados antes da publicação.
|
||||||
|
|
||||||
|
Nas fases futuras, sucesso também significará:
|
||||||
|
|
||||||
- leads avançarem pelo pipeline sem depender de planilhas externas;
|
- leads avançarem pelo pipeline sem depender de planilhas externas;
|
||||||
- leads conquistados virarem eventos sem redigitação;
|
- leads conquistados virarem eventos sem redigitação;
|
||||||
- pendências reais do dia aparecerem com clareza;
|
- pendências reais do dia aparecerem com clareza;
|
||||||
- tarefas, fornecedores, orçamento, pagamentos manuais e documentos permanecerem organizados por evento;
|
- tarefas, fornecedores, orçamento, pagamentos manuais e documentos permanecerem organizados por evento.
|
||||||
- jornadas críticas, acessibilidade e qualidade visual serem verificadas antes de cada deploy.
|
|
||||||
|
|
||||||
## Positioning
|
## Positioning
|
||||||
|
|
||||||
Site público premium e operação interna formam um fluxo único. A mesma plataforma que transmite profissionalismo e capta a demanda transforma o lead em evento e orienta a equipe pelas pendências e exceções que precisam de ação, sem exigir troca radical de processo.
|
No lançamento, o site público premium e o CMS interno formam a superfície entregue: a mesma aplicação transmite profissionalismo, capta a demanda por e-mail e permite manter o conteúdo essencial. A operação interna completa permanece como direção futura para transformar leads em eventos e orientar a equipe pelas pendências e exceções que precisam de ação, sem exigir troca radical de processo.
|
||||||
|
|
||||||
## Operating Context
|
## Operating Context
|
||||||
|
|
||||||
- Atuação em São Paulo, capital.
|
- Atuação em São Paulo, capital.
|
||||||
- Serviços: casamentos, eventos corporativos e celebrações.
|
- Serviços: casamentos, eventos corporativos e celebrações.
|
||||||
- Operação realizada por assessora principal e equipe pequena.
|
- Operação realizada por assessora principal e equipe pequena.
|
||||||
- Jornada prevista: descoberta no site, briefing, qualificação do lead, pipeline comercial, conversão em evento, checklist, fornecedores, orçamento, pagamentos manuais e documentos.
|
- Jornada de lançamento: descoberta no site, briefing por e-mail e contato comercial fora da plataforma.
|
||||||
|
- Jornada futura prevista: qualificação do lead, pipeline comercial, conversão em evento, checklist, fornecedores, orçamento, pagamentos manuais e documentos.
|
||||||
- Interface pública em português do Brasil; dados financeiros em reais.
|
- Interface pública em português do Brasil; dados financeiros em reais.
|
||||||
- Área interna acessível por administradora e assistentes com papéis simples.
|
- Área interna acessível por administradora e assistentes com papéis simples.
|
||||||
|
|
||||||
## Capabilities and Constraints
|
## Capabilities and Constraints
|
||||||
|
|
||||||
- Produto single-tenant para a operação da Amare; não é SaaS para várias assessorias.
|
- Produto single-tenant para a operação da Amare; não é SaaS para várias assessorias.
|
||||||
- Duas superfícies integradas: site público responsivo com CMS e área interna operacional em Filament.
|
- O lançamento inclui site público responsivo, CMS em Filament, briefing enviado por e-mail sem persistência de Lead, usuários internos simples, SEO básico, acessibilidade automatizada e deploy em contêiner.
|
||||||
- MVP inclui briefing, CRM de leads, conversão em evento, tarefas, fornecedores, orçamento, pagamentos manuais, documentos, dashboard orientado a exceções, notificações, auditoria, SEO básico e deploy automatizado.
|
- CRM de leads, conversão em evento, tarefas, fornecedores, orçamento, pagamentos manuais, documentos, dashboard orientado a exceções, notificações e auditoria permanecem especificados para as Fases 2 a 5, adiadas sem data pela ADR-016.
|
||||||
- Implementação atual cobre fundação, site público e CMS; módulos operacionais posteriores ainda fazem parte do escopo planejado.
|
- O produto futuro mantém duas superfícies integradas: site público responsivo com CMS e área interna operacional em Filament; no lançamento, a área interna cobre apenas CMS, configurações e usuários.
|
||||||
- Não inclui portal ou login para cliente final, aplicativo nativo, pagamentos online, RSVP, gestão de convidados, assinatura eletrônica, integração oficial com WhatsApp, multi-tenancy ou PWA offline avançada.
|
- Não inclui portal ou login para cliente final, aplicativo nativo, pagamentos online, RSVP, gestão de convidados, assinatura eletrônica, integração oficial com WhatsApp, multi-tenancy ou PWA offline avançada.
|
||||||
- Contas internas usam os papéis `admin` e `assistant`; permissões granulares ficam fora do MVP.
|
- Contas internas usam os papéis `admin` e `assistant`; permissões granulares ficam fora do MVP.
|
||||||
- Fatos ainda em aberto: processo real de qualificação comercial, política de retenção LGPD para leads perdidos, canais oficiais, domínio e infraestrutura de produção.
|
- Fatos ainda em aberto: processo real de qualificação comercial, política de retenção LGPD para leads perdidos, canais oficiais, domínio e infraestrutura de produção.
|
||||||
@@ -58,7 +64,7 @@ Site público premium e operação interna formam um fluxo único. A mesma plata
|
|||||||
## Evidence on Hand
|
## Evidence on Hand
|
||||||
|
|
||||||
- `SPEC.md` define escopo, personas, jornadas, métricas, requisitos e decisões do MVP.
|
- `SPEC.md` define escopo, personas, jornadas, métricas, requisitos e decisões do MVP.
|
||||||
- Código atual contém site público, CMS, estrutura de acessibilidade, testes visuais e conteúdo demonstrativo.
|
- Código atual contém site público, CMS, estrutura de acessibilidade, testes browser e conteúdo demonstrativo.
|
||||||
- Textos, fotografias, portfólio, depoimentos, contatos e perfis sociais presentes nos seeders são dados fictícios de desenvolvimento e não constituem prova comercial.
|
- Textos, fotografias, portfólio, depoimentos, contatos e perfis sociais presentes nos seeders são dados fictícios de desenvolvimento e não constituem prova comercial.
|
||||||
- Ainda não há logo, fotografia proprietária, depoimentos autorizados, cases reais, imprensa, credenciais, CNPJ ou texto jurídico definitivo confirmados no repositório. Trabalho futuro não deve inventá-los.
|
- Ainda não há logo, fotografia proprietária, depoimentos autorizados, cases reais, imprensa, credenciais, CNPJ ou texto jurídico definitivo confirmados no repositório. Trabalho futuro não deve inventá-los.
|
||||||
|
|
||||||
@@ -68,7 +74,7 @@ Site público premium e operação interna formam um fluxo único. A mesma plata
|
|||||||
2. **Uma informação, um fluxo:** conduzir o dado do briefing ao evento sem redigitação ou planilhas paralelas.
|
2. **Uma informação, um fluxo:** conduzir o dado do briefing ao evento sem redigitação ou planilhas paralelas.
|
||||||
3. **Adoção sem ruptura:** organizar o trabalho existente da equipe sem impor processo desnecessariamente complexo.
|
3. **Adoção sem ruptura:** organizar o trabalho existente da equipe sem impor processo desnecessariamente complexo.
|
||||||
4. **Confiança sustentada por fatos:** usar somente conteúdo, resultados e provas reais e autorizados.
|
4. **Confiança sustentada por fatos:** usar somente conteúdo, resultados e provas reais e autorizados.
|
||||||
5. **Qualidade verificável:** tratar acessibilidade, desempenho, testes visuais e jornadas críticas como critérios de entrega.
|
5. **Qualidade verificável:** tratar acessibilidade, desempenho, motion e jornadas críticas como critérios de entrega.
|
||||||
|
|
||||||
## Accessibility & Inclusion
|
## Accessibility & Inclusion
|
||||||
|
|
||||||
|
|||||||
65
SPEC.md
@@ -23,7 +23,7 @@
|
|||||||
| Área pública | Blade + JS vanilla progressivo (Livewire é dependência do Filament, não usada no site público — ver ADR-015) |
|
| Área pública | Blade + JS vanilla progressivo (Livewire é dependência do Filament, não usada no site público — ver ADR-015) |
|
||||||
| Servidor de aplicação | FrankenPHP em modo regular |
|
| Servidor de aplicação | FrankenPHP em modo regular |
|
||||||
| Banco de dados | PostgreSQL |
|
| Banco de dados | PostgreSQL |
|
||||||
| Testes | Pest, Pest Browser/Playwright e testes visuais |
|
| Testes | Pest e Pest Browser/Playwright |
|
||||||
|
|
||||||
### 0.1 Vocabulário normativo
|
### 0.1 Vocabulário normativo
|
||||||
|
|
||||||
@@ -80,7 +80,6 @@ O agente:
|
|||||||
- NÃO DEVE colocar regras financeiras diretamente em views, Resources, Models observers ou callbacks de formulário.
|
- NÃO DEVE colocar regras financeiras diretamente em views, Resources, Models observers ou callbacks de formulário.
|
||||||
- NÃO DEVE usar `float` para dinheiro.
|
- NÃO DEVE usar `float` para dinheiro.
|
||||||
- NÃO DEVE persistir status derivados que possam ser calculados corretamente a partir dos dados fonte.
|
- NÃO DEVE persistir status derivados que possam ser calculados corretamente a partir dos dados fonte.
|
||||||
- NÃO DEVE atualizar snapshots visuais apenas para fazer o CI passar sem revisar o diff.
|
|
||||||
|
|
||||||
### 1.2 Entrega incremental
|
### 1.2 Entrega incremental
|
||||||
|
|
||||||
@@ -135,7 +134,6 @@ Uma assessora adotará o sistema quando ele:
|
|||||||
| Operação | Tarefas vencidas sem responsável | `0` |
|
| Operação | Tarefas vencidas sem responsável | `0` |
|
||||||
| Financeiro | Itens de orçamento sem valor ou status | Menos de 5% por evento ativo |
|
| Financeiro | Itens de orçamento sem valor ou status | Menos de 5% por evento ativo |
|
||||||
| Qualidade | Jornadas E2E críticas passando | 100% antes de deploy |
|
| Qualidade | Jornadas E2E críticas passando | 100% antes de deploy |
|
||||||
| Visual | Snapshots aprovados | 100% |
|
|
||||||
| Confiabilidade | Erros não tratados | Alerta imediato e tendência decrescente |
|
| Confiabilidade | Erros não tratados | Alerta imediato e tendência decrescente |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -207,7 +205,7 @@ No recorte do lançamento (Fases 0–1):
|
|||||||
- formulário de briefing;
|
- formulário de briefing;
|
||||||
- usuários internos e papéis simples;
|
- usuários internos e papéis simples;
|
||||||
- SEO básico;
|
- SEO básico;
|
||||||
- acessibilidade e testes visuais;
|
- acessibilidade automatizada;
|
||||||
- CI/CD e deploy em contêiner com FrankenPHP.
|
- CI/CD e deploy em contêiner com FrankenPHP.
|
||||||
|
|
||||||
Adiados para depois do lançamento (Fases 2–5, ver ADR-016):
|
Adiados para depois do lançamento (Fases 2–5, ver ADR-016):
|
||||||
@@ -1901,7 +1899,7 @@ A pirâmide deve possuir:
|
|||||||
1. muitos testes unitários rápidos;
|
1. muitos testes unitários rápidos;
|
||||||
2. testes feature/integration suficientes para Laravel, PostgreSQL, Livewire e Filament;
|
2. testes feature/integration suficientes para Laravel, PostgreSQL, Livewire e Filament;
|
||||||
3. poucos testes E2E cobrindo jornadas críticas;
|
3. poucos testes E2E cobrindo jornadas críticas;
|
||||||
4. testes visuais determinísticos nas telas mais importantes.
|
4. testes E2E, de acessibilidade, motion e smoke nas jornadas públicas mais importantes.
|
||||||
|
|
||||||
A distribuição é por intenção, não por percentual rígido.
|
A distribuição é por intenção, não por percentual rígido.
|
||||||
|
|
||||||
@@ -1983,40 +1981,7 @@ Em falha, CI deve publicar:
|
|||||||
- logs do browser;
|
- logs do browser;
|
||||||
- HTML report quando disponível.
|
- HTML report quando disponível.
|
||||||
|
|
||||||
### 13.5 Regressão visual
|
### 13.5 Testes de arquitetura
|
||||||
|
|
||||||
Usar `assertScreenshotMatches()` ou API equivalente do Pest Browser.
|
|
||||||
|
|
||||||
Snapshots obrigatórios:
|
|
||||||
|
|
||||||
| Tela | Desktop | Mobile |
|
|
||||||
|---|---:|---:|
|
|
||||||
| Home | 1440×1000 | 390×844 |
|
|
||||||
| Serviços | 1440×1000 | 390×844 |
|
|
||||||
| Portfólio | 1440×1000 | 390×844 |
|
|
||||||
| Detalhe do portfólio | 1440×1000 | 390×844 |
|
|
||||||
| Briefing vazio | 1280×900 | 390×844 |
|
|
||||||
| Briefing com erros | 1280×900 | 390×844 |
|
|
||||||
| Briefing sucesso | 1280×900 | 390×844 |
|
|
||||||
| Login | 1280×900 | Opcional |
|
|
||||||
| Dashboard | 1440×1000 | Não obrigatório |
|
|
||||||
| Detalhe do evento | 1440×1000 | Não obrigatório |
|
|
||||||
|
|
||||||
Determinismo obrigatório:
|
|
||||||
|
|
||||||
- Chromium e imagem Linux fixos;
|
|
||||||
- viewport fixo;
|
|
||||||
- timezone `America/Sao_Paulo`;
|
|
||||||
- locale `pt-BR`;
|
|
||||||
- fontes instaladas na imagem;
|
|
||||||
- relógio congelado;
|
|
||||||
- seed determinístico;
|
|
||||||
- animações e transições desabilitadas;
|
|
||||||
- dados dinâmicos mascarados quando necessário.
|
|
||||||
|
|
||||||
Atualização de baseline deve usar comando explícito e revisão humana do diff.
|
|
||||||
|
|
||||||
### 13.6 Testes de arquitetura
|
|
||||||
|
|
||||||
Criar regras Pest Architecture:
|
Criar regras Pest Architecture:
|
||||||
|
|
||||||
@@ -2033,14 +1998,14 @@ arch()
|
|||||||
|
|
||||||
Adicionar regras conforme a estrutura real, sem tornar a suíte excessivamente frágil.
|
Adicionar regras conforme a estrutura real, sem tornar a suíte excessivamente frágil.
|
||||||
|
|
||||||
### 13.7 Cobertura
|
### 13.6 Cobertura
|
||||||
|
|
||||||
- meta de 80% para `Domain` e `Application`;
|
- meta de 80% para `Domain` e `Application`;
|
||||||
- não medir views, migrations, código gerado e framework;
|
- não medir views, migrations, código gerado e framework;
|
||||||
- cobertura não substitui critérios de aceite;
|
- cobertura não substitui critérios de aceite;
|
||||||
- queda de cobertura em módulo crítico deve bloquear merge.
|
- queda de cobertura em módulo crítico deve bloquear merge.
|
||||||
|
|
||||||
### 13.8 Acessibilidade automatizada
|
### 13.7 Acessibilidade automatizada
|
||||||
|
|
||||||
Smoke/browser tests devem verificar:
|
Smoke/browser tests devem verificar:
|
||||||
|
|
||||||
@@ -2058,7 +2023,7 @@ Revisão manual mínima antes do lançamento:
|
|||||||
- zoom a 200%;
|
- zoom a 200%;
|
||||||
- leitor de tela nos fluxos de briefing e login.
|
- leitor de tela nos fluxos de briefing e login.
|
||||||
|
|
||||||
### 13.9 Comandos padronizados
|
### 13.8 Comandos padronizados
|
||||||
|
|
||||||
O projeto deve expor scripts equivalentes:
|
O projeto deve expor scripts equivalentes:
|
||||||
|
|
||||||
@@ -2068,7 +2033,6 @@ composer test:feature
|
|||||||
composer test:browser
|
composer test:browser
|
||||||
composer test
|
composer test
|
||||||
composer quality
|
composer quality
|
||||||
composer visual:update
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Composição esperada:
|
Composição esperada:
|
||||||
@@ -2076,7 +2040,7 @@ Composição esperada:
|
|||||||
```text
|
```text
|
||||||
test:unit → Unit + Architecture
|
test:unit → Unit + Architecture
|
||||||
test:feature → Feature + Livewire + Filament
|
test:feature → Feature + Livewire + Filament
|
||||||
test:browser → E2E + visual + acessibilidade + smoke
|
test:browser → E2E + acessibilidade + motion + smoke
|
||||||
quality → Pint check + PHPStan/Larastan + audits + testes
|
quality → Pint check + PHPStan/Larastan + audits + testes
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2091,7 +2055,7 @@ quality → Pint check + PHPStan/Larastan + audits + testes
|
|||||||
| `static` | Pint, PHPStan/Larastan, Composer validate, Composer e npm audit | Sim |
|
| `static` | Pint, PHPStan/Larastan, Composer validate, Composer e npm audit | Sim |
|
||||||
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
| `unit` | Unitários, arquitetura e cobertura | Sim |
|
||||||
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
| `feature` | PostgreSQL, migrations, Livewire, Filament e integração | Sim |
|
||||||
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e visual | Sim |
|
| `browser` | Vite, FrankenPHP, E2E, smoke, acessibilidade e motion | Sim |
|
||||||
| `container` | Build da imagem final e healthcheck | Sim |
|
| `container` | Build da imagem final e healthcheck | Sim |
|
||||||
|
|
||||||
### 14.2 Regras do pipeline
|
### 14.2 Regras do pipeline
|
||||||
@@ -2125,7 +2089,6 @@ Uma história só está concluída quando:
|
|||||||
- PHPStan/Larastan está verde;
|
- PHPStan/Larastan está verde;
|
||||||
- unit e feature tests cobrem regras relevantes;
|
- unit e feature tests cobrem regras relevantes;
|
||||||
- E2E foi criado ou atualizado para fluxo crítico;
|
- E2E foi criado ou atualizado para fluxo crítico;
|
||||||
- snapshot foi criado ou revisado para UI coberta;
|
|
||||||
- acessibilidade automatizada não possui issues críticas/sérias;
|
- acessibilidade automatizada não possui issues críticas/sérias;
|
||||||
- migration foi testada;
|
- migration foi testada;
|
||||||
- autorização foi verificada;
|
- autorização foi verificada;
|
||||||
@@ -2383,10 +2346,9 @@ O agente deve implementar na sequência, salvo instrução explícita.
|
|||||||
- [x] privacidade;
|
- [x] privacidade;
|
||||||
- [x] SEO;
|
- [x] SEO;
|
||||||
- [x] mídia otimizada;
|
- [x] mídia otimizada;
|
||||||
- [x] snapshots desktop/mobile;
|
|
||||||
- [x] testes de acessibilidade.
|
- [x] testes de acessibilidade.
|
||||||
|
|
||||||
**Critério de saída:** conteúdo gerenciável no Filament e site público aprovado visualmente (baselines em `tests/.pest/snapshots/`; aprovação humana do diff visual no PR).
|
**Critério de saída:** conteúdo gerenciável no Filament, testes browser funcionais, de acessibilidade e motion aprovados, e revisão humana do site público no PR.
|
||||||
|
|
||||||
### Fase 2 — Leads — ADIADA (ADR-016)
|
### Fase 2 — Leads — ADIADA (ADR-016)
|
||||||
|
|
||||||
@@ -2497,8 +2459,7 @@ Toda operação financeira deve:
|
|||||||
| Risco | Mitigação obrigatória |
|
| Risco | Mitigação obrigatória |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Filament concentrar domínio | Resources finos, Actions e Queries testáveis |
|
| Filament concentrar domínio | Resources finos, Actions e Queries testáveis |
|
||||||
| Site parecer genérico | design tokens, conteúdo real, fotografia e visual tests |
|
| Site parecer genérico | design tokens, conteúdo real, fotografia e revisão humana |
|
||||||
| Snapshots instáveis | contêiner fixo, relógio/dados/fontes determinísticos |
|
|
||||||
| Escopo crescer | lista de não objetivos e mudança somente com hipótese real |
|
| Escopo crescer | lista de não objetivos e mudança somente com hipótese real |
|
||||||
| Financeiro virar contabilidade | limitar a orçamento e pagamentos manuais |
|
| Financeiro virar contabilidade | limitar a orçamento e pagamentos manuais |
|
||||||
| Worker mode vazar estado | manter modo regular até benchmark e ADR |
|
| Worker mode vazar estado | manter modo regular até benchmark e ADR |
|
||||||
@@ -2517,7 +2478,7 @@ Toda operação financeira deve:
|
|||||||
| ADR-002 | Filament para área interna (Livewire é dependência interna do Filament); site público em Blade — ver ADR-015 | Aceita |
|
| ADR-002 | Filament para área interna (Livewire é dependência interna do Filament); site público em Blade — ver ADR-015 | Aceita |
|
||||||
| ADR-003 | PostgreSQL como único banco transacional | Aceita |
|
| ADR-003 | PostgreSQL como único banco transacional | Aceita |
|
||||||
| ADR-004 | Pagamentos somente manuais | Aceita |
|
| ADR-004 | Pagamentos somente manuais | Aceita |
|
||||||
| ADR-005 | Pest unifica unit, feature, browser e visual | Aceita |
|
| ADR-005 | Pest unifica unit, feature e browser | Aceita |
|
||||||
| ADR-006 | FrankenPHP regular mode; worker mode adiado | Aceita |
|
| ADR-006 | FrankenPHP regular mode; worker mode adiado | Aceita |
|
||||||
| ADR-007 | Single-tenant; SaaS e portal do cliente adiados | Aceita |
|
| ADR-007 | Single-tenant; SaaS e portal do cliente adiados | Aceita |
|
||||||
| ADR-008 | Database queue; Redis adiado | Aceita |
|
| ADR-008 | Database queue; Redis adiado | Aceita |
|
||||||
@@ -2563,7 +2524,7 @@ O lançamento está concluído somente quando:
|
|||||||
- assessora edita os conteúdos essenciais sem desenvolvedor;
|
- assessora edita os conteúdos essenciais sem desenvolvedor;
|
||||||
- briefing envia o pedido de proposta de forma segura, com proteção contra abuso e aceite de privacidade registrado;
|
- briefing envia o pedido de proposta de forma segura, com proteção contra abuso e aceite de privacidade registrado;
|
||||||
- usuários e Policies estão corretos;
|
- usuários e Policies estão corretos;
|
||||||
- testes unit, feature, visuais e arquitetura estão verdes;
|
- testes unit, feature, browser funcionais e arquitetura estão verdes;
|
||||||
- CI bloqueia regressões;
|
- CI bloqueia regressões;
|
||||||
- imagem FrankenPHP é reproduzível;
|
- imagem FrankenPHP é reproduzível;
|
||||||
- staging e produção usam a mesma imagem promovida;
|
- staging e produção usam a mesma imagem promovida;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
|
|
||||||
foreach ($paths as $path) {
|
foreach ($paths as $path) {
|
||||||
if (! Storage::disk($disk)->exists($path)) {
|
if (! Storage::disk($disk)->exists($path)) {
|
||||||
|
ResponsiveImage::forgetMetadata($path, $disk);
|
||||||
$this->warn("Missing file: {$path}");
|
$this->warn("Missing file: {$path}");
|
||||||
$skipped++;
|
$skipped++;
|
||||||
|
|
||||||
@@ -56,9 +57,21 @@ final class MediaGenerateVariantsCommand extends Command
|
|||||||
if ($settings && filled($settings->default_og_image_path)) {
|
if ($settings && filled($settings->default_og_image_path)) {
|
||||||
$paths[] = (string) $settings->default_og_image_path;
|
$paths[] = (string) $settings->default_og_image_path;
|
||||||
}
|
}
|
||||||
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
|
}
|
||||||
if ($settings && filled($settings->about_image_path)) {
|
if ($settings && filled($settings->about_image_path)) {
|
||||||
$paths[] = (string) $settings->about_image_path;
|
$paths[] = (string) $settings->about_image_path;
|
||||||
}
|
}
|
||||||
|
if ($settings && filled($settings->hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->hero_image_path;
|
||||||
|
}
|
||||||
|
if ($settings && filled($settings->services_hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->services_hero_image_path;
|
||||||
|
}
|
||||||
|
if ($settings && filled($settings->portfolio_hero_image_path)) {
|
||||||
|
$paths[] = (string) $settings->portfolio_hero_image_path;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
foreach (Service::query()->whereNotNull('cover_image_path')->pluck('cover_image_path') as $path) {
|
||||||
$paths[] = (string) $path;
|
$paths[] = (string) $path;
|
||||||
|
|||||||
@@ -160,11 +160,24 @@ class ManageSiteSettings extends Page
|
|||||||
Textarea::make('hero_note')
|
Textarea::make('hero_note')
|
||||||
->label('Nota do hero')
|
->label('Nota do hero')
|
||||||
->rows(2),
|
->rows(2),
|
||||||
|
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem do hero', 'content/home'),
|
||||||
|
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
|
||||||
Textarea::make('about_summary')
|
Textarea::make('about_summary')
|
||||||
->label('Resumo institucional')
|
->label('Resumo institucional')
|
||||||
->rows(3),
|
->rows(3),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
|
Section::make('Imagens das aberturas')
|
||||||
|
->description('Fotos reais e autorizadas para as primeiras dobras. Sem imagem, cada rota mantém a abertura tonal.')
|
||||||
|
->schema([
|
||||||
|
PublicImageUploadRules::fileUpload('hero_image_path', 'Imagem da home', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('hero_image_alt', 'hero_image_path'),
|
||||||
|
PublicImageUploadRules::fileUpload('services_hero_image_path', 'Imagem de Serviços', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('services_hero_image_alt', 'services_hero_image_path'),
|
||||||
|
PublicImageUploadRules::fileUpload('portfolio_hero_image_path', 'Imagem de Portfólio', 'content/heroes'),
|
||||||
|
PublicImageUploadRules::altTextField('portfolio_hero_image_alt', 'portfolio_hero_image_path'),
|
||||||
|
])
|
||||||
|
->columns(2),
|
||||||
Section::make('Manifesto editorial')
|
Section::make('Manifesto editorial')
|
||||||
->schema([
|
->schema([
|
||||||
TextInput::make('manifesto_title')
|
TextInput::make('manifesto_title')
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
* @property string|null $default_og_image_alt
|
* @property string|null $default_og_image_alt
|
||||||
* @property string|null $about_image_path
|
* @property string|null $about_image_path
|
||||||
* @property string|null $about_image_alt
|
* @property string|null $about_image_alt
|
||||||
|
* @property string|null $hero_image_path
|
||||||
|
* @property string|null $hero_image_alt
|
||||||
|
* @property string|null $services_hero_image_path
|
||||||
|
* @property string|null $services_hero_image_alt
|
||||||
|
* @property string|null $portfolio_hero_image_path
|
||||||
|
* @property string|null $portfolio_hero_image_alt
|
||||||
* @property string|null $logo_path
|
* @property string|null $logo_path
|
||||||
* @property string|null $logo_alt
|
* @property string|null $logo_alt
|
||||||
*/
|
*/
|
||||||
@@ -31,6 +37,12 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
'hero_cta_label',
|
'hero_cta_label',
|
||||||
'hero_secondary_cta_label',
|
'hero_secondary_cta_label',
|
||||||
'hero_note',
|
'hero_note',
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'services_hero_image_path',
|
||||||
|
'services_hero_image_alt',
|
||||||
|
'portfolio_hero_image_path',
|
||||||
|
'portfolio_hero_image_alt',
|
||||||
'about_summary',
|
'about_summary',
|
||||||
'about_image_path',
|
'about_image_path',
|
||||||
'about_image_alt',
|
'about_image_alt',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
use Illuminate\Contracts\Filesystem\Filesystem;
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Intervention\Image\Drivers\Gd\Driver;
|
use Intervention\Image\Drivers\Gd\Driver;
|
||||||
use Intervention\Image\ImageManager;
|
use Intervention\Image\ImageManager;
|
||||||
@@ -12,13 +13,23 @@ use Throwable;
|
|||||||
|
|
||||||
final class ResponsiveImage
|
final class ResponsiveImage
|
||||||
{
|
{
|
||||||
/** @var list<int> */
|
/**
|
||||||
public const WIDTHS = [480, 960, 1440];
|
* 720 sits between the original 480 and 960 because that is where the common
|
||||||
|
* mobile viewport lands: 412 CSS px at a 1.75 device pixel ratio asks for
|
||||||
|
* ~721 px, so a full-width image used to jump straight to the 960 variant and
|
||||||
|
* pay for a third more pixels than it drew. Measured on the hero (MAN-109):
|
||||||
|
* 143 KiB at 960 in jpeg against 75 KiB at 720 in webp.
|
||||||
|
*
|
||||||
|
* @var list<int>
|
||||||
|
*/
|
||||||
|
public const WIDTHS = [480, 720, 960, 1440];
|
||||||
|
|
||||||
public static function generate(string $path, ?string $disk = null): void
|
public static function generate(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
if (! $filesystem->exists($path)) {
|
if (! $filesystem->exists($path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -47,40 +58,85 @@ final class ResponsiveImage
|
|||||||
};
|
};
|
||||||
|
|
||||||
$filesystem->put($variantPath, (string) $encoded);
|
$filesystem->put($variantPath, (string) $encoded);
|
||||||
|
|
||||||
|
// A webp sibling for every variant. The MAN-109 audit measured the
|
||||||
|
// hero as the LCP element on every mobile page, at 143 KiB for a
|
||||||
|
// 960 px jpeg — webp carries the same picture for roughly a third of
|
||||||
|
// that. `x-media.image` offers these through a <source> so a browser
|
||||||
|
// that cannot decode webp still gets the original format.
|
||||||
|
if ($extension === 'webp') {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$webp = $manager->read($contents);
|
||||||
|
|
||||||
|
if ($webp->width() > $width) {
|
||||||
|
$webp->scale(width: $width);
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesystem->put(
|
||||||
|
self::webpVariantPath($path, $width),
|
||||||
|
(string) $webp->toWebp(quality: 80)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::metadata($path, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function deleteVariants(string $path, ?string $disk = null): void
|
public static function deleteVariants(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
foreach (self::WIDTHS as $width) {
|
foreach (self::WIDTHS as $width) {
|
||||||
$variantPath = self::variantPath($path, $width);
|
foreach ([self::variantPath($path, $width), self::webpVariantPath($path, $width)] as $variantPath) {
|
||||||
|
|
||||||
if ($filesystem->exists($variantPath)) {
|
if ($filesystem->exists($variantPath)) {
|
||||||
$filesystem->delete($variantPath);
|
$filesystem->delete($variantPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function delete(string $path, ?string $disk = null): void
|
public static function delete(string $path, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
|
try {
|
||||||
self::deleteVariants($path, $disk);
|
self::deleteVariants($path, $disk);
|
||||||
|
|
||||||
if ($filesystem->exists($path)) {
|
if ($filesystem->exists($path)) {
|
||||||
$filesystem->delete($path);
|
$filesystem->delete($path);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($path, $disk);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function replace(string $previousPath, string $newPath, ?string $disk = null): void
|
public static function replace(string $previousPath, string $newPath, ?string $disk = null): void
|
||||||
{
|
{
|
||||||
|
self::forgetMetadata($previousPath, $disk);
|
||||||
|
self::forgetMetadata($newPath, $disk);
|
||||||
|
|
||||||
|
try {
|
||||||
if ($previousPath !== '' && $previousPath !== $newPath) {
|
if ($previousPath !== '' && $previousPath !== $newPath) {
|
||||||
self::delete($previousPath, $disk);
|
self::delete($previousPath, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
self::generate($newPath, $disk);
|
self::generate($newPath, $disk);
|
||||||
|
} finally {
|
||||||
|
self::forgetMetadata($previousPath, $disk);
|
||||||
|
self::forgetMetadata($newPath, $disk);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::metadata($newPath, $disk);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function variantPath(string $path, int $width): string
|
public static function variantPath(string $path, int $width): string
|
||||||
@@ -93,26 +149,35 @@ final class ResponsiveImage
|
|||||||
return $directory === '' ? $variantName : $directory.'/'.$variantName;
|
return $directory === '' ? $variantName : $directory.'/'.$variantName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The webp sibling of a variant, named by appending rather than replacing the
|
||||||
|
* extension. Uploads are stored under a UUID so a collision is already
|
||||||
|
* unlikely, but `photo-480.jpg.webp` cannot collide with the webp variant of
|
||||||
|
* a `photo.png` the way `photo-480.webp` would.
|
||||||
|
*/
|
||||||
|
public static function webpVariantPath(string $path, int $width): string
|
||||||
|
{
|
||||||
|
return self::variantPath($path, $width).'.webp';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array{path: string, width: int}>
|
* @return list<array{path: string, width: int}>
|
||||||
*/
|
*/
|
||||||
public static function availableVariants(string $path, ?string $disk = null): array
|
public static function availableVariants(string $path, ?string $disk = null): array
|
||||||
{
|
{
|
||||||
$filesystem = self::filesystem($disk);
|
return self::metadata($path, $disk)['variants'] ?? [];
|
||||||
$variants = [];
|
|
||||||
|
|
||||||
foreach (self::WIDTHS as $width) {
|
|
||||||
$variantPath = self::variantPath($path, $width);
|
|
||||||
|
|
||||||
if ($filesystem->exists($variantPath)) {
|
|
||||||
$variants[] = [
|
|
||||||
'path' => $variantPath,
|
|
||||||
'width' => $width,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $variants;
|
/**
|
||||||
|
* The webp variants that exist for a path. Empty when the media predates
|
||||||
|
* `media:generate-variants` running with webp support, which is why
|
||||||
|
* `x-media.image` treats the <source> as optional rather than assuming it.
|
||||||
|
*
|
||||||
|
* @return list<array{path: string, width: int}>
|
||||||
|
*/
|
||||||
|
public static function availableWebpVariants(string $path, ?string $disk = null): array
|
||||||
|
{
|
||||||
|
return self::metadata($path, $disk)['webp_variants'] ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -120,13 +185,38 @@ final class ResponsiveImage
|
|||||||
*/
|
*/
|
||||||
public static function dimensions(string $path, ?string $disk = null): ?array
|
public static function dimensions(string $path, ?string $disk = null): ?array
|
||||||
{
|
{
|
||||||
|
$metadata = self::metadata($path, $disk);
|
||||||
|
|
||||||
|
return $metadata === null ? null : [
|
||||||
|
'width' => $metadata['width'],
|
||||||
|
'height' => $metadata['height'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* width: int,
|
||||||
|
* height: int,
|
||||||
|
* variants: list<array{path: string, width: int}>,
|
||||||
|
* webp_variants: list<array{path: string, width: int}>
|
||||||
|
* }|null
|
||||||
|
*/
|
||||||
|
public static function metadata(string $path, ?string $disk = null): ?array
|
||||||
|
{
|
||||||
|
$key = self::metadataCacheKey($path, $disk);
|
||||||
|
$cached = self::cachedMetadata($key);
|
||||||
|
|
||||||
|
if (is_array($cached)) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
$filesystem = self::filesystem($disk);
|
$filesystem = self::filesystem($disk);
|
||||||
|
|
||||||
if (! $filesystem->exists($path)) {
|
if (! $filesystem->exists($path)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
$contents = $filesystem->get($path);
|
$contents = $filesystem->get($path);
|
||||||
|
|
||||||
if ($contents === null) {
|
if ($contents === null) {
|
||||||
@@ -134,18 +224,88 @@ final class ResponsiveImage
|
|||||||
}
|
}
|
||||||
|
|
||||||
$image = (new ImageManager(new Driver))->read($contents);
|
$image = (new ImageManager(new Driver))->read($contents);
|
||||||
|
$metadata = [
|
||||||
return [
|
|
||||||
'width' => $image->width(),
|
'width' => $image->width(),
|
||||||
'height' => $image->height(),
|
'height' => $image->height(),
|
||||||
|
'variants' => self::existingVariants($filesystem, $path, false),
|
||||||
|
'webp_variants' => self::existingVariants($filesystem, $path, true),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
self::storeMetadata($key, $metadata);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function forgetMetadata(string $path, ?string $disk = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Cache::forget(self::metadataCacheKey($path, $disk));
|
||||||
|
} catch (Throwable) {
|
||||||
|
// Cache availability must not block public media rendering or cleanup.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static function filesystem(?string $disk): Filesystem
|
private static function filesystem(?string $disk): Filesystem
|
||||||
{
|
{
|
||||||
return Storage::disk($disk ?? PublicImageUploadRules::disk());
|
return Storage::disk(self::diskName($disk));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{path: string, width: int}>
|
||||||
|
*/
|
||||||
|
private static function existingVariants(Filesystem $filesystem, string $path, bool $webp): array
|
||||||
|
{
|
||||||
|
$variants = [];
|
||||||
|
|
||||||
|
foreach (self::WIDTHS as $width) {
|
||||||
|
$variantPath = $webp
|
||||||
|
? self::webpVariantPath($path, $width)
|
||||||
|
: self::variantPath($path, $width);
|
||||||
|
|
||||||
|
if ($filesystem->exists($variantPath)) {
|
||||||
|
$variants[] = ['path' => $variantPath, 'width' => $width];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $variants;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function metadataCacheKey(string $path, ?string $disk): string
|
||||||
|
{
|
||||||
|
return 'responsive-image:metadata:'.sha1(self::diskName($disk).'|'.$path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private static function cachedMetadata(string $key): ?array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$cached = Cache::get($key);
|
||||||
|
|
||||||
|
return is_array($cached) ? $cached : null;
|
||||||
|
} catch (Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $metadata
|
||||||
|
*/
|
||||||
|
private static function storeMetadata(string $key, array $metadata): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Cache::forever($key, $metadata);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// The uncached result remains safe to use for this request.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function diskName(?string $disk): string
|
||||||
|
{
|
||||||
|
return $disk ?? PublicImageUploadRules::disk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,9 +87,6 @@
|
|||||||
"npm audit --omit=dev --audit-level=high",
|
"npm audit --omit=dev --audit-level=high",
|
||||||
"@test"
|
"@test"
|
||||||
],
|
],
|
||||||
"visual:update": [
|
|
||||||
"@php artisan test --testsuite=Browser --update-snapshots"
|
|
||||||
],
|
|
||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover --ansi",
|
"@php artisan package:discover --ansi",
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ return [
|
|||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
| When set outside production, the application clock is frozen for
|
| When set outside production, the application clock is frozen for
|
||||||
| deterministic rendering (visual regression / seeded content).
|
| deterministic time-dependent tests or seeded content.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->string('hero_image_path')->nullable()->after('hero_note');
|
||||||
|
$table->string('hero_image_alt')->nullable()->after('hero_image_path');
|
||||||
|
$table->string('services_hero_image_path')->nullable()->after('hero_image_alt');
|
||||||
|
$table->string('services_hero_image_alt')->nullable()->after('services_hero_image_path');
|
||||||
|
$table->string('portfolio_hero_image_path')->nullable()->after('services_hero_image_alt');
|
||||||
|
$table->string('portfolio_hero_image_alt')->nullable()->after('portfolio_hero_image_path');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('site_settings', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn([
|
||||||
|
'hero_image_path',
|
||||||
|
'hero_image_alt',
|
||||||
|
'services_hero_image_path',
|
||||||
|
'services_hero_image_alt',
|
||||||
|
'portfolio_hero_image_path',
|
||||||
|
'portfolio_hero_image_alt',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -61,6 +61,12 @@ class ContentSeeder extends Seeder
|
|||||||
'hero_cta_label' => 'Solicitar proposta',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
|
'hero_image_path' => $this->copyFixture('case-casamento-ana-lucas.jpg', 'content/heroes/home.jpg'),
|
||||||
|
'hero_image_alt' => 'Celebração ao ar livre em São Paulo',
|
||||||
|
'services_hero_image_path' => $this->copyFixture('service-eventos-corporativos.jpg', 'content/heroes/services.jpg'),
|
||||||
|
'services_hero_image_alt' => 'Mesa preparada para um evento corporativo',
|
||||||
|
'portfolio_hero_image_path' => $this->copyFixture('case-lancamento-verano.jpg', 'content/heroes/portfolio.jpg'),
|
||||||
|
'portfolio_hero_image_alt' => 'Ambientação de um evento de lançamento',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
'about_image_path' => $this->copyFixture('about-image.jpg', 'content/about/about-image.jpg'),
|
||||||
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
'about_image_alt' => 'Mesa de planejamento com caderno, café e guardanapos de pano',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use Illuminate\Support\Carbon;
|
|||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deterministic content for visual regression. Keep separate from ContentSeeder demo data.
|
* Deterministic content and image fixtures. Keep separate from ContentSeeder demo data.
|
||||||
*/
|
*/
|
||||||
class VisualContentSeeder extends Seeder
|
class VisualContentSeeder extends Seeder
|
||||||
{
|
{
|
||||||
@@ -42,6 +42,12 @@ class VisualContentSeeder extends Seeder
|
|||||||
'hero_cta_label' => 'Solicitar proposta',
|
'hero_cta_label' => 'Solicitar proposta',
|
||||||
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
'hero_secondary_cta_label' => 'Conheça nosso olhar',
|
||||||
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
'hero_note' => 'Planejamento cuidadoso, comunicação clara e execução segura — do primeiro encontro ao último detalhe.',
|
||||||
|
'hero_image_path' => $this->writeSolidJpeg('visual/heroes/home.jpg', 1600, 1100, [218, 224, 205]),
|
||||||
|
'hero_image_alt' => 'Imagem editorial da home',
|
||||||
|
'services_hero_image_path' => $this->writeSolidJpeg('visual/heroes/services.jpg', 1600, 1100, [196, 200, 184]),
|
||||||
|
'services_hero_image_alt' => 'Imagem editorial de Serviços',
|
||||||
|
'portfolio_hero_image_path' => $this->writeSolidJpeg('visual/heroes/portfolio.jpg', 1600, 1100, [228, 226, 221]),
|
||||||
|
'portfolio_hero_image_alt' => 'Imagem editorial de Portfólio',
|
||||||
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
'about_summary' => 'Assessoria boutique especializada em experiências memoráveis em São Paulo.',
|
||||||
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
'about_image_path' => $this->writeSolidJpeg('visual/about/about-image.jpg', 1200, 900, [232, 228, 218]),
|
||||||
'about_image_alt' => 'Imagem editorial da página Sobre',
|
'about_image_alt' => 'Imagem editorial da página Sobre',
|
||||||
|
|||||||
409
docs/evidence/lighthouse/2026-08-10-local-antes.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# Lighthouse — local
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T15:55:03.101Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `2e43fde`
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.53 s | 0.37 s | 0.000 | 0 ms | 55 ms | 0.53 s – 0.53 s |
|
||||||
|
| contato | mobile | 97 | 100 | 100 | 100 | 2.55 s | 1.50 s | 0.000 | 0 ms | 97 ms | 2.55 s – 2.56 s |
|
||||||
|
| home | desktop | 99 | 100 | 100 | 100 | 0.87 s | 0.37 s | 0.000 | 0 ms | 75 ms | 0.87 s – 0.87 s |
|
||||||
|
| home | mobile | 83 | 100 | 100 | 100 | 4.58 s | 1.51 s | 0.000 | 0 ms | 217 ms | 4.36 s – 4.58 s |
|
||||||
|
| portfolio-detalhe | desktop | 99 | 100 | 100 | 100 | 0.97 s | 0.37 s | 0.000 | 0 ms | 89 ms | 0.97 s – 0.97 s |
|
||||||
|
| portfolio-detalhe | mobile | 87 | 100 | 100 | 100 | 3.98 s | 1.51 s | 0.000 | 0 ms | 126 ms | 3.98 s – 4.05 s |
|
||||||
|
| portfolio | desktop | 99 | 100 | 100 | 100 | 0.83 s | 0.37 s | 0.000 | 0 ms | 187 ms | 0.63 s – 0.83 s |
|
||||||
|
| portfolio | mobile | 87 | 100 | 100 | 100 | 3.98 s | 1.51 s | 0.000 | 0 ms | 108 ms | 3.91 s – 4.20 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.77 s | 0.37 s | 0.000 | 0 ms | 124 ms | 0.77 s – 0.77 s |
|
||||||
|
| servicos | mobile | 89 | 100 | 100 | 100 | 3.68 s | 1.51 s | 0.000 | 0 ms | 78 ms | 3.61 s – 3.68 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.61 s | 0.37 s | 0.000 | 0 ms | 48 ms | 0.57 s – 0.61 s |
|
||||||
|
| sobre | mobile | 94 | 100 | 100 | 100 | 3.01 s | 1.51 s | 0.000 | 0 ms | 133 ms | 2.87 s – 3.01 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.41 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 2.10 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.11 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.11 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Render blocking requests | 0.60 s | — |
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.45 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.27 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.08 s |
|
||||||
|
| `/brand/mark-on-light.webp` | Image | 42 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.25 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 3.30 s |
|
||||||
|
| Load Time | 0.11 s |
|
||||||
|
| Render Delay | 0.72 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.25 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.25 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.25 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.23 s |
|
||||||
|
| `/brand/mark-on-light.webp` | Image | 42 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.24 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.24 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.35 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.30 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.54 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.26 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg` | Image | 287 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.11 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.62 s |
|
||||||
|
| Load Time | 0.06 s |
|
||||||
|
| Render Delay | 0.85 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.16 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.16 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.14 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.19 s |
|
||||||
|
| Load Delay | 0.51 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.23 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.23 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.20 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.23 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.22 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.21 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.25 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.68 s |
|
||||||
|
| Load Time | 0.09 s |
|
||||||
|
| Render Delay | 0.76 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.13 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.35 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.13 s |
|
||||||
|
| Load Delay | 0.49 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.15 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.15 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.15 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 2.22 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.94 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.12 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.12 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.12 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 1.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.25 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.22 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.06 s |
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.05 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.05 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.05 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.94 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.54 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.16 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-xCe6GkOx.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DZU8xF9o.woff` | Font | 30 KiB | 0.15 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-maQnHeKB.woff` | Font | 28 KiB | 0.16 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.14 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.30 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
407
docs/evidence/lighthouse/2026-08-10-local-depois.md
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# Lighthouse — local-sizes
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T17:38:33.741Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `9ab2beb` (o script gravou `65919d4`, o HEAD no momento da coleta; a árvore medida é a que virou `9ab2beb`)
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 91 ms | 0.36 s – 0.37 s |
|
||||||
|
| contato | mobile | 100 | 100 | 100 | 100 | 1.50 s | 0.92 s | 0.000 | 0 ms | 66 ms | 1.50 s – 1.51 s |
|
||||||
|
| home | desktop | 100 | 100 | 100 | 100 | 0.53 s | 0.25 s | 0.000 | 0 ms | 110 ms | 0.53 s – 0.54 s |
|
||||||
|
| home | mobile | 98 | 100 | 100 | 100 | 2.49 s | 0.92 s | 0.000 | 0 ms | 77 ms | 2.48 s – 2.57 s |
|
||||||
|
| portfolio-detalhe | desktop | 100 | 100 | 100 | 100 | 0.65 s | 0.25 s | 0.000 | 0 ms | 81 ms | 0.65 s – 0.66 s |
|
||||||
|
| portfolio-detalhe | mobile | 99 | 100 | 100 | 100 | 2.18 s | 0.90 s | 0.000 | 0 ms | 77 ms | 2.18 s – 2.18 s |
|
||||||
|
| portfolio | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 72 ms | 0.36 s – 0.49 s |
|
||||||
|
| portfolio | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 71 ms | 1.58 s – 1.58 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.24 s | 0.000 | 0 ms | 59 ms | 0.36 s – 0.38 s |
|
||||||
|
| servicos | mobile | 99 | 100 | 100 | 100 | 2.03 s | 0.90 s | 0.000 | 0 ms | 62 ms | 1.58 s – 2.03 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 51 ms | 0.36 s – 0.37 s |
|
||||||
|
| sobre | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 93 ms | 1.58 s – 1.58 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.24 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 1.05 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-720.jpg.webp" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…"`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.25 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-720.jpg.webp` | Image | 74 KiB | 0.13 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.14 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.14 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.12 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.12 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-720.jpg.webp" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…"`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.24 s |
|
||||||
|
| Load Time | 0.10 s |
|
||||||
|
| Render Delay | 0.69 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-720.jpg.webp` | Image | 74 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.09 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.28 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.22 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg.webp` | Image | 210 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-720.jpg.webp` | Image | 105 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-720.jpg.webp` | Image | 88 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.09 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.11 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.57 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-720.jpg.webp` | Image | 105 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-720.jpg.webp` | Image | 88 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.09 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.14 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.09 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.66 s |
|
||||||
|
| Load Time | 0.04 s |
|
||||||
|
| Render Delay | 0.42 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-720.jpg.webp` | Image | 67 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-720.jpg.webp` | Image | 45 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-720.jpg.webp` | Image | 43 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-720.jpg.webp" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/se`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.14 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.10 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-720.jpg.webp` | Image | 49 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-720.jpg.webp` | Image | 39 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-720.jpg.webp` | Image | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.07 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-720.jpg.webp" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/se`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.08 s |
|
||||||
|
| Load Time | 0.07 s |
|
||||||
|
| Render Delay | 0.43 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-720.jpg.webp` | Image | 49 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-720.jpg.webp` | Image | 39 KiB | 0.08 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-720.jpg.webp` | Image | 28 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.07 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.07 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.45 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-720.jpg.webp" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.11 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.12 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/storage/content/about/about-image-720.jpg.webp` | Image | 22 KiB | 0.07 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/sobre` | Document | 5 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-720.jpg.webp" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.77 s |
|
||||||
|
| Load Time | 0.02 s |
|
||||||
|
| Render Delay | 0.33 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/storage/content/about/about-image-720.jpg.webp` | Image | 22 KiB | 0.11 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-8wBXH8kj.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
| `/sobre` | Document | 5 KiB | 0.09 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
# Lighthouse — local-pos
|
||||||
|
|
||||||
|
- Origem: `http://127.0.0.1:8000`
|
||||||
|
- Lighthouse: 12.8.2
|
||||||
|
- Navegador: `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.0.0 Safari/537.36`
|
||||||
|
- Coletado em: 2026-08-10T16:20:48.372Z
|
||||||
|
- Execuções por página/preset: 3 (reportada a de LCP mediano)
|
||||||
|
- Commit: `65919d4` (o script gravou `2e43fde`, o HEAD no momento da coleta; a árvore medida é a que virou `65919d4`)
|
||||||
|
- Seeder: `ContentSeeder`
|
||||||
|
|
||||||
|
Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.
|
||||||
|
|
||||||
|
| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 20 ms | 0.36 s – 0.37 s |
|
||||||
|
| contato | mobile | 100 | 100 | 100 | 100 | 1.51 s | 0.92 s | 0.000 | 0 ms | 46 ms | 1.50 s – 1.51 s |
|
||||||
|
| home | desktop | 100 | 100 | 100 | 100 | 0.67 s | 0.25 s | 0.000 | 0 ms | 87 ms | 0.67 s – 0.67 s |
|
||||||
|
| home | mobile | 92 | 100 | 100 | 100 | 3.39 s | 0.92 s | 0.000 | 0 ms | 88 ms | 3.38 s – 3.46 s |
|
||||||
|
| portfolio-detalhe | desktop | 100 | 100 | 100 | 100 | 0.79 s | 0.25 s | 0.000 | 0 ms | 74 ms | 0.79 s – 0.79 s |
|
||||||
|
| portfolio-detalhe | mobile | 95 | 100 | 100 | 100 | 3.01 s | 0.91 s | 0.000 | 0 ms | 89 ms | 2.93 s – 3.01 s |
|
||||||
|
| portfolio | desktop | 100 | 100 | 100 | 100 | 0.65 s | 0.25 s | 0.000 | 0 ms | 73 ms | 0.37 s – 0.65 s |
|
||||||
|
| portfolio | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 69 ms | 1.58 s – 3.24 s |
|
||||||
|
| servicos | desktop | 100 | 100 | 100 | 100 | 0.36 s | 0.25 s | 0.000 | 0 ms | 121 ms | 0.36 s – 0.59 s |
|
||||||
|
| servicos | mobile | 97 | 100 | 100 | 100 | 2.63 s | 0.90 s | 0.000 | 0 ms | 73 ms | 1.58 s – 2.63 s |
|
||||||
|
| sobre | desktop | 100 | 100 | 100 | 100 | 0.37 s | 0.25 s | 0.000 | 0 ms | 97 ms | 0.36 s – 0.45 s |
|
||||||
|
| sobre | mobile | 100 | 100 | 100 | 100 | 1.58 s | 0.91 s | 0.000 | 0 ms | 48 ms | 1.58 s – 1.96 s |
|
||||||
|
|
||||||
|
## Decomposição do LCP
|
||||||
|
|
||||||
|
### contato — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.24 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.03 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.03 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.03 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.03 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.03 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.02 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.03 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### contato — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<h1 class="text-headline font-medium tracking-tight text-amare-text">`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.00 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 1.05 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.05 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.05 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/contato` | Document | 5 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.33 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.19 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.15 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### home — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/og/og-default-960.jpg" srcset="/storage/content/og/og-default-480.jpg 480w, /storage/content/og/og-defaul…" size`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.88 s |
|
||||||
|
| Load Time | 0.17 s |
|
||||||
|
| Render Delay | 0.88 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/og/og-default-960.jpg` | Image | 143 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.12 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.12 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.43 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.21 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-1440.jpg` | Image | 287 KiB | 0.08 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.10 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio-detalhe — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.63 s |
|
||||||
|
| Load Time | 0.06 s |
|
||||||
|
| Render Delay | 0.87 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-1-960.jpg` | Image | 209 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-gallery-2-960.jpg` | Image | 172 KiB | 0.11 s |
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.10 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.90 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.15 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.28 s |
|
||||||
|
| Load Time | 0.05 s |
|
||||||
|
| Render Delay | 0.19 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.10 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.20 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### portfolio — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/portfolio/casamento-ana-lucas-cover-…" srcset="/storage/content/portfolio/casamento-ana-lucas-cover-480.jpg 480w`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.64 s |
|
||||||
|
| Load Time | 0.03 s |
|
||||||
|
| Render Delay | 0.46 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/portfolio/casamento-ana-lucas-cover-960.jpg` | Image | 142 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/lancamento-verano-cover-960.jpg` | Image | 106 KiB | 0.09 s |
|
||||||
|
| `/storage/content/portfolio/mini-wedding-marina-cover-960.jpg` | Image | 100 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.12 s |
|
||||||
|
| Load Delay | 0.18 s |
|
||||||
|
| Load Time | 0.01 s |
|
||||||
|
| Render Delay | 0.06 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.14 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.14 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.14 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.13 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.13 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.13 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.13 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### servicos — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/services/casamentos-960.jpg" srcset="/storage/content/services/casamentos-480.jpg 480w, /storage/content/servic…`
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 1.50 s |
|
||||||
|
| Load Time | 0.11 s |
|
||||||
|
| Render Delay | 0.56 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/services/casamentos-960.jpg` | Image | 107 KiB | 0.09 s |
|
||||||
|
| `/storage/content/services/eventos-corporativos-960.jpg` | Image | 100 KiB | 0.09 s |
|
||||||
|
| `/storage/content/services/celebracoes-intimistas-960.jpg` | Image | 83 KiB | 0.09 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.08 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.08 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.08 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.08 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Improve image delivery | 0.60 s | — |
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — desktop
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.13 s |
|
||||||
|
| Load Delay | 0.15 s |
|
||||||
|
| Load Time | 0.00 s |
|
||||||
|
| Render Delay | 0.09 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.12 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.11 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.10 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.10 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.11 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.11 s |
|
||||||
|
| `/sobre` | Document | 4 KiB | 0.10 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.11 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
|
### sobre — mobile
|
||||||
|
|
||||||
|
Elemento de LCP: `<img src="http://127.0.0.1:8000/storage/content/about/about-image-960.jpg" srcset="/storage/content/about/about-image-480.jpg 480w, /storage/content/about/ab…" `
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0.45 s |
|
||||||
|
| Load Delay | 0.57 s |
|
||||||
|
| Load Time | 0.10 s |
|
||||||
|
| Render Delay | 0.46 s |
|
||||||
|
|
||||||
|
Requests concluídas até o LCP, mais pesadas primeiro:
|
||||||
|
|
||||||
|
| recurso | tipo | transferido | fim |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/storage/content/about/about-image-960.jpg` | Image | 60 KiB | 0.07 s |
|
||||||
|
| `/build/assets/eb-garamond-600-normal-DHwxsLHv.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-500-normal-DehAIUv0.woff2` | Font | 25 KiB | 0.06 s |
|
||||||
|
| `/build/assets/eb-garamond-400-normal-BCNrxLz_.woff2` | Font | 24 KiB | 0.06 s |
|
||||||
|
| `/brand/lockup-on-light.webp` | Image | 14 KiB | 0.06 s |
|
||||||
|
| `/build/assets/app-Cswasu1n.css` | Stylesheet | 10 KiB | 0.06 s |
|
||||||
|
| `/sobre` | Document | 4 KiB | 0.05 s |
|
||||||
|
| `/build/assets/app-Cr9w0NCu.js` | Script | 2 KiB | 0.06 s |
|
||||||
|
|
||||||
|
| insight com falha | ganho estimado de LCP | bytes |
|
||||||
|
|---|---|---|
|
||||||
|
| Use efficient cache lifetimes | 0.00 s | — |
|
||||||
|
| Improve image delivery | 0.00 s | — |
|
||||||
|
| LCP request discovery | 0.00 s | — |
|
||||||
|
| Network dependency tree | 0.00 s | — |
|
||||||
|
| Render blocking requests | 0.00 s | — |
|
||||||
|
|
||||||
205
docs/evidence/lighthouse/README.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Lighthouse — MAN-109
|
||||||
|
|
||||||
|
Medições versionadas porque a rodada anterior (PR #34) sobreviveu apenas como
|
||||||
|
uma tabela digitada à mão num comentário do Linear: `storage/app/lighthouse` é
|
||||||
|
gitignored, então não havia contra o quê comparar. Os relatórios brutos pesam
|
||||||
|
~48 MB por passada e continuam fora do repositório; o que fica versionado são os
|
||||||
|
resumos, que já carregam a decomposição do LCP e a lista de requests até o LCP.
|
||||||
|
|
||||||
|
## Como reproduzir
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Sobe a imagem de produção. O seed roda no host, não no container:
|
||||||
|
# ContentSeeder é no-op fora de local/staging/testing (database/seeders/ContentSeeder.php:44),
|
||||||
|
# então sob APP_ENV=production ele não semeia nada e o Lighthouse mede páginas vazias.
|
||||||
|
docker build -t amare-app:man109 .
|
||||||
|
php artisan migrate --force
|
||||||
|
php artisan db:seed --class=ContentSeeder --force
|
||||||
|
php artisan media:generate-variants # sem isso o LCP da home infla ~2,5 s
|
||||||
|
docker run -d --name amare-web -p 8000:8000 -e APP_ENV=production ... amare-app:man109
|
||||||
|
|
||||||
|
TARGET=local bash scripts/perf/lighthouse.sh storage/app/lighthouse
|
||||||
|
```
|
||||||
|
|
||||||
|
Cada página é auditada 3 vezes por preset e o relatório de LCP mediano é o
|
||||||
|
reportado — nunca a média. O LCP se move alguns décimos entre execuções na mesma
|
||||||
|
build, e uma única execução não sustenta comparação.
|
||||||
|
|
||||||
|
## Resultado
|
||||||
|
|
||||||
|
Preset mobile padrão do Lighthouse 12.8.2: throttling simulado de 150 ms de RTT,
|
||||||
|
~1,6 Mbps, CPU 4× mais lenta. Chrome estável do sistema (não o Chromium do
|
||||||
|
Playwright), imagem de produção local, seeder `ContentSeeder`.
|
||||||
|
|
||||||
|
Antes: commit `2e43fde`. As colunas intermediárias mostram cada correção
|
||||||
|
isoladamente, medida com uma passada completa antes da seguinte entrar.
|
||||||
|
|
||||||
|
| página | preset | antes | + fontes e marca | + webp e 720w | + sizes correto | perf antes → depois | bytes antes → depois |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| contato | desktop | 0.53 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 262 → 106 KiB |
|
||||||
|
| contato | mobile | 2.55 s | 1.51 s | 1.51 s | **1.50 s** | 97 → 100 | 262 → 106 KiB |
|
||||||
|
| home | desktop | 0.87 s | 0.67 s | 0.53 s | **0.53 s** | 99 → 100 | 798 → 347 KiB |
|
||||||
|
| home | mobile | 4.58 s | 3.39 s | 2.87 s | **2.49 s** | 83 → 98 | 798 → 347 KiB |
|
||||||
|
| portfolio | desktop | 0.83 s | 0.65 s | 0.37 s | **0.36 s** | 99 → 100 | 609 → 259 KiB |
|
||||||
|
| portfolio | mobile | 3.98 s | 1.58 s | 2.64 s | **1.58 s** | 87 → 100 | 609 → 259 KiB |
|
||||||
|
| portfolio-detalhe | desktop | 0.97 s | 0.79 s | 0.66 s | **0.65 s** | 99 → 100 | 930 → 508 KiB |
|
||||||
|
| portfolio-detalhe | mobile | 3.98 s | 3.01 s | 2.71 s | **2.18 s** | 87 → 99 | 785 → 365 KiB |
|
||||||
|
| servicos | desktop | 0.77 s | 0.36 s | 0.37 s | **0.36 s** | 100 → 100 | 552 → 222 KiB |
|
||||||
|
| servicos | mobile | 3.68 s | 2.63 s | 1.58 s | **2.03 s** | 89 → 99 | 552 → 222 KiB |
|
||||||
|
| sobre | desktop | 0.61 s | 0.37 s | 0.37 s | **0.36 s** | 100 → 100 | 322 → 127 KiB |
|
||||||
|
| sobre | mobile | 3.01 s | 1.58 s | 1.60 s | **1.58 s** | 94 → 100 | 322 → 127 KiB |
|
||||||
|
|
||||||
|
FCP cai de 1,51 s para 0,91 s em todas as páginas no mobile. Acessibilidade,
|
||||||
|
boas práticas e SEO marcam 100 em todas as páginas nos dois presets, antes e
|
||||||
|
depois. CLS é 0,000 e TBT é 0 ms em todas — as metas de §6.6 para essas três
|
||||||
|
métricas já passavam e continuam passando.
|
||||||
|
|
||||||
|
**Contra a meta de LCP ≤ 2,5 s da §6.6: todas as páginas passam nos dois
|
||||||
|
presets.** Desktop com folga (máximo 0,65 s). No mobile o pior caso é a home a
|
||||||
|
2,49 s, ou seja **em cima da linha** — a pior das três execuções dela deu 2,57 s.
|
||||||
|
Tratar a home como aprovada por margem, não com folga.
|
||||||
|
|
||||||
|
Duas colunas intermediárias merecem leitura cuidadosa em vez de conclusão:
|
||||||
|
|
||||||
|
- `portfolio` mobile aparece pior na coluna do webp (2,64 s) do que na anterior
|
||||||
|
(1,58 s). É variância, não regressão: as três execuções daquela passada foram
|
||||||
|
1,59 / 2,64 / 2,78 s. A página é a mais instável do conjunto e a mediana pulou
|
||||||
|
de ponta. Na passada final as três deram 1,58 s.
|
||||||
|
- `servicos` mobile sobe de 1,58 s para 2,03 s da terceira para a quarta coluna,
|
||||||
|
pelo mesmo motivo (1,58 / 1,58 / 2,03).
|
||||||
|
|
||||||
|
É exatamente por isso que o script roda três vezes e reporta a mediana; ainda
|
||||||
|
assim, diferenças abaixo de meio segundo entre passadas não devem ser lidas como
|
||||||
|
efeito de uma correção.
|
||||||
|
|
||||||
|
Ressalva: a medição é local, então latência de origem e TLS não entram, e o
|
||||||
|
throttling de rede é simulado. Trate o LCP como piso, não como valor de campo.
|
||||||
|
|
||||||
|
## O que cada correção comprou
|
||||||
|
|
||||||
|
**Fontes servidas em dobro — 88 KiB fora do caminho crítico.** Bunny entrega
|
||||||
|
cada peso de EB Garamond em woff2 e woff, e o plugin de fontes emitia uma regra
|
||||||
|
`@font-face` para cada, woff2 primeiro e woff depois. Duas regras com a mesma
|
||||||
|
família, peso, estilo e unicode-range fazem a **última** vencer: o navegador
|
||||||
|
renderizava a partir dos woff e descartava os woff2 pré-carregados.
|
||||||
|
|
||||||
|
A prova está no log de rede da home antes da correção: 3 woff em prioridade
|
||||||
|
`VeryHigh` (88 KiB) — a prioridade mais alta da página, à frente do elemento de
|
||||||
|
LCP — somados a 3 woff2 em `High` (74 KiB) que só foram baixados porque estavam
|
||||||
|
em `<link rel="preload">`. 162 KiB de tráfego de fonte para 74 KiB de fonte útil.
|
||||||
|
|
||||||
|
woff2 é suportado por todo navegador que este site atende desde 2016, então as
|
||||||
|
regras woff não eram fallback e sim peso morto. O plugin `amare:fonts-woff2-only`
|
||||||
|
em `vite.config.js` remove as regras do CSS e do manifest e tira os arquivos do
|
||||||
|
bundle. É o que derruba o FCP de 1,51 s para 0,91 s em todas as páginas.
|
||||||
|
|
||||||
|
**Ativos de marca reencodados — 102 KiB fora do caminho crítico.** O logotipo
|
||||||
|
era servido a 512 px de largura para renderizar em 48 px (lockup, cabeçalho e
|
||||||
|
rodapé) e 32 px (mark, home): 84 KiB + 42 KiB com `loading="eager"` em todas as
|
||||||
|
páginas. Reencodados a 3× do maior render — lockup 149×144 (14 KiB) e mark
|
||||||
|
191×96 (10 KiB) —, mantendo os mesmos nomes de arquivo para não invalidar cache.
|
||||||
|
As variantes `on-dark` foram reencodadas junto por consistência; nenhuma view as
|
||||||
|
usa hoje.
|
||||||
|
|
||||||
|
Isto absorve MAN-122: os ativos de marca foram reencodados sem alterar a forma
|
||||||
|
ou a cor percebida, reduzindo apenas o peso transferido.
|
||||||
|
|
||||||
|
Os arquivos versionados aqui são: `2026-08-10-local-antes.md` (commit `2e43fde`),
|
||||||
|
`2026-08-10-local-etapa-fontes-e-marca.md` (passada intermediária) e
|
||||||
|
`2026-08-10-local-depois.md` (estado final).
|
||||||
|
|
||||||
|
**Variantes WebP nas imagens de conteúdo.** Com fontes e marca resolvidas, o
|
||||||
|
elemento de LCP de toda página no mobile era a imagem do hero, e o Load Delay de
|
||||||
|
1,88 s era contenção de banda pura: 143 KiB de JPEG q82 a 960 px, com as três
|
||||||
|
capas do portfólio somando outros 348 KiB. `ResponsiveImage::generate()` passa a
|
||||||
|
escrever uma variante `.webp` ao lado de cada variante no formato original, e
|
||||||
|
`x-media.image` a oferece num `<source type="image/webp">`. O `<img>` continua
|
||||||
|
apontando para o formato original, então nada quebra em quem não decodifica webp,
|
||||||
|
e mídia antiga sem irmãos webp renderiza `<img>` puro como antes.
|
||||||
|
|
||||||
|
**`sizes` que descreve a realidade.** Nenhuma imagem do site ocupa a viewport
|
||||||
|
inteira: todas ficam dentro de `container-amare`, que reserva 1,5rem de padding
|
||||||
|
de cada lado. Declarar `100vw` fazia uma viewport de 412 px em DPR 1,75 pedir
|
||||||
|
721 px e pular para a variante de 960 para desenhar uma caixa de 637 px — errar
|
||||||
|
por um pixel custava um terço a mais de bytes. Com `calc(100vw - 3rem)` a home
|
||||||
|
passa a usar a variante de 720 (**74 KiB**, contra 143 KiB no início).
|
||||||
|
|
||||||
|
Foi também por isso que 720 entrou em `ResponsiveImage::WIDTHS`: sem ela o salto
|
||||||
|
de 480 para 960 é grande demais para a viewport mobile mais comum.
|
||||||
|
|
||||||
|
Decomposição final do LCP da home no mobile:
|
||||||
|
|
||||||
|
| fase | tempo |
|
||||||
|
|---|---|
|
||||||
|
| TTFB | 0,45 s |
|
||||||
|
| Load Delay | 1,24 s |
|
||||||
|
| Load Time | 0,10 s |
|
||||||
|
| Render Delay | 0,69 s |
|
||||||
|
|
||||||
|
## O ganho de imagem só aparece depois do deploy regenerar as variantes
|
||||||
|
|
||||||
|
Toda a medição acima usou `FILESYSTEM_DISK=local`. Em staging e produção o disco
|
||||||
|
é `r2`, e lá as variantes `.webp` e a largura 720 **ainda não existem** para a
|
||||||
|
mídia já publicada. Até `media:generate-variants` rodar, `availableWebpVariants()`
|
||||||
|
volta vazio, o `<source>` é omitido e o `srcset` do formato original perde a
|
||||||
|
entrada de 720w — ou seja, nenhum dos dois ganhos de imagem aparece.
|
||||||
|
|
||||||
|
O serviço `migrate` do `docker-compose.deploy.yml` roda
|
||||||
|
`php artisan media:generate-variants` sem condição a cada deploy, depois de
|
||||||
|
`migrate` e do seed, e o comando só pula um caminho quando o arquivo original não
|
||||||
|
existe (`MediaGenerateVariantsCommand`) — regenera mesmo quando já há variantes.
|
||||||
|
Então o primeiro deploy desta branch produz as variantes novas por conta própria.
|
||||||
|
|
||||||
|
Enquanto isso não acontece, há um custo sem contrapartida: `x-media.image` faz
|
||||||
|
agora **8 chamadas `exists()` por imagem** (4 larguras × 2 formatos) contra o
|
||||||
|
object store, no lugar de 3. Na home são ~32 round trips remotos por request em
|
||||||
|
vez de ~12. Isso agrava a hipótese não validada abaixo em vez de melhorá-la, e é
|
||||||
|
mais um motivo para cachear esses metadados.
|
||||||
|
|
||||||
|
## Se a home precisar de mais folga
|
||||||
|
|
||||||
|
O Load Delay de 1,24 s ainda é contenção: as três capas do portfólio somam
|
||||||
|
155 KiB e, embora sejam `loading="lazy"` e prioridade `Low`, o navegador as busca
|
||||||
|
porque entram no limiar de lazy loading da viewport emulada. Os levers restantes,
|
||||||
|
em ordem de custo:
|
||||||
|
|
||||||
|
1. Baixar a qualidade webp de 80 para 75 (medido: 109 → 92 KiB na imagem do
|
||||||
|
hero a 960 px). Barato, mas mexe na qualidade de imagem de uma marca cujo
|
||||||
|
posicionamento é acabamento editorial — decisão de produto, não de engenharia.
|
||||||
|
2. Reduzir o Render Delay de 0,69 s, que agora é a segunda maior fatia e é
|
||||||
|
trabalho de main thread, não de rede.
|
||||||
|
3. FrankenPHP worker mode para o TTFB de 0,45 s. Tem gatilho objetivo em
|
||||||
|
SPEC §22 e não deve ser puxado antes dele.
|
||||||
|
|
||||||
|
## Cobertura que este trabalho não tem
|
||||||
|
|
||||||
|
Os testes browser não exercitam `srcset` nem `<picture>` com variantes geradas:
|
||||||
|
nem `ContentSeeder` nem `VisualContentSeeder` geram variantes. Nesses cenários,
|
||||||
|
`availableVariants()` volta vazio e o componente renderiza `<img>` puro. A
|
||||||
|
cobertura do caminho com variantes fica nos testes de feature
|
||||||
|
(`MediaImageComponentTest`).
|
||||||
|
|
||||||
|
## Staging
|
||||||
|
|
||||||
|
Não medido. A origem de staging responde `303` para
|
||||||
|
`blocked.teams.cloudflare.com` a partir da rede corporativa da Creditas
|
||||||
|
("O conteúdo deste site viola a Política de Segurança da Informação"), inclusive
|
||||||
|
em `/up`. O preflight de HTTP 200 do script barra a execução antes de gastar
|
||||||
|
minutos auditando páginas de bloqueio.
|
||||||
|
|
||||||
|
Duas hipóteses seguem **não validadas** porque só existem com
|
||||||
|
`FILESYSTEM_DISK=r2`, que é configuração de staging e produção:
|
||||||
|
|
||||||
|
- `x-media.image` chama `ResponsiveImage::availableVariants()`,
|
||||||
|
`availableWebpVariants()` (8 `exists()` somados) e `ResponsiveImage::dimensions()`
|
||||||
|
(que baixa o arquivo inteiro) a cada render, sem cache. No disco `local` são
|
||||||
|
leituras de filesystem; no `r2` são ~9 round trips remotos por imagem no lado
|
||||||
|
servidor, ~36 na home. Teste discriminante: comparar o TTFB de `/contato`
|
||||||
|
(zero imagens) com o de `/portfolio` (N imagens). Se o TTFB escalar com a
|
||||||
|
contagem de imagens, está confirmado. **Este é o item mais urgente da lista**,
|
||||||
|
porque as variantes webp multiplicaram o número de chamadas.
|
||||||
|
- A mídia vem de `R2_URL`, uma origem cross-origin, e não há `preconnect` no
|
||||||
|
`<head>` — DNS e TLS entram antes do LCP.
|
||||||
|
|
||||||
|
Para medir: rodar `TARGET=staging BASE_URL=<origem> bash scripts/perf/lighthouse.sh`
|
||||||
|
de uma rede sem o filtro corporativo.
|
||||||
81
docs/superpowers/plans/2026-08-11-full-bleed-home-hero.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Hero full-bleed da home Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Fazer a fotografia configurada do hero da home preencher toda a coluna direita abaixo do header, preservando a leitura editorial e o fallback sem foto.
|
||||||
|
|
||||||
|
**Architecture:** `x-home.hero` continuará a renderizar as duas variantes. A variante com imagem usará uma grade full-width com a coluna textual ancorada à régua global e a mídia como segundo trilho sem container; a variante tonal permanece contida. Os testes de feature e browser documentam os contratos de markup, tamanho e responsividade.
|
||||||
|
|
||||||
|
**Tech Stack:** Laravel Blade, Tailwind CSS 4 utilities, Pest feature/browser tests, Vite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: Layout e contratos do hero
|
||||||
|
|
||||||
|
### Task 1: Cobrir o spread full-bleed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/Feature/PublicSite/ImmersivePhotoHeroTest.php`
|
||||||
|
- Modify: `tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Escrever as expectativas de feature para a variante com foto**
|
||||||
|
|
||||||
|
Exigir `data-split-hero`, `data-hero-content`, `data-motion="page-open"`, `data-reveal-group`, `aria-labelledby="hero-heading"`, `loading="eager"`, `fetchpriority="high"` e `sizes="(max-width: 767px) 100vw, 55vw"`. Com `hero_image_path` nulo, exigir `data-tonal-hero`, os mesmos atributos de motion/semântica, ausência de `data-split-hero` e ausência de `<img` dentro de `[data-tonal-hero]`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Estender a verificação browser de geometria**
|
||||||
|
|
||||||
|
No viewport 1440×1000, obter os retângulos de `.site-header`, `[data-chapter="hero"]`, `[data-hero-content]` e `[data-split-hero]`; esperar, com tolerância de 1px, que `hero.top === header.bottom`, `media.top === hero.top`, `media.bottom === hero.bottom`, `media.right === innerWidth` e `hero.height === innerHeight - header.height`. No viewport 390×844, comparar `data-split-hero` com `[data-hero-content]`; esperar que a mídia inicie depois do conteúdo, que `width / height` fique entre 0,79 e 0,81, e que não exista overflow horizontal. Antes das duas visitas de fallback, zerar `hero_image_path`; numa visita com reduced motion, esperar conteúdo visível. Em outra com `reducedMotion: no-preference` e `IntersectionObserver` desabilitado antes da navegação, esperar ausência de `data-motion` em `document.documentElement`, cada `[data-motion="page-open"]` com `.is-active`, cada `[data-reveal]` com `.is-revealed` e `aria-labelledby="hero-heading"`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Executar os testes focados e confirmar a falha inicial**
|
||||||
|
|
||||||
|
Run: `php artisan test tests/Feature/PublicSite/ImmersivePhotoHeroTest.php && php artisan test tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
Expected: falha nas novas expectativas de geometria/markup até a alteração do componente.
|
||||||
|
|
||||||
|
### Task 2: Implementar a grade full-bleed
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/components/home/hero.blade.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Separar a variante com imagem do container compartilhado**
|
||||||
|
|
||||||
|
Substituir `min-h-[100dvh]` do `<section>` por `md:h-[calc(100dvh-5rem)]` quando `hero_image_path` estiver preenchido e renderizar um wrapper `grid min-h-[calc(100dvh-5rem)] md:h-full md:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)]`. A coluna de texto deve manter `data-reveal-group`, receber `data-hero-content`, ter `padding-left: max(var(--amare-container-padding), calc((100vw - var(--amare-container-max)) / 2 + var(--amare-container-padding)))`, padding direito fluido e centralização vertical.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Tornar a mídia contínua**
|
||||||
|
|
||||||
|
Aplicar ao bloco `data-split-hero` `overflow-hidden`, `bg-amare-bg-deep`, `md:min-h-full`; abaixo de `md`, aplicar `aspect-[4/5]`. Manter `data-motion-beat="media"`, `data-reveal-media`, `object-cover`, path/alt CMS e mudar `sizes` para `(max-width: 767px) 100vw, 55vw`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Preservar o fallback**
|
||||||
|
|
||||||
|
Manter o wrapper tonal existente para hero sem imagem, com `data-tonal-hero`, `data-motion="page-open"` e `data-reveal-group`; não renderizar o bloco de mídia nessa variante.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Executar os testes focados e confirmar a passagem**
|
||||||
|
|
||||||
|
Run: `php artisan test tests/Feature/PublicSite/ImmersivePhotoHeroTest.php && php artisan test tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
### Task 3: Verificar e registrar
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `resources/views/components/home/hero.blade.php`
|
||||||
|
- Modify: `tests/Feature/PublicSite/ImmersivePhotoHeroTest.php`
|
||||||
|
- Modify: `tests/Browser/HomeEditorialCadenceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Formatar e executar as verificações proporcionais**
|
||||||
|
|
||||||
|
Run: `composer pint && composer phpstan && composer test:feature && composer test:browser && npm run build`
|
||||||
|
|
||||||
|
Expected: todos os comandos passam.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Inspecionar o diff e registrar**
|
||||||
|
|
||||||
|
Run: `git diff --check && git status --short`
|
||||||
|
|
||||||
|
Expected: apenas os três arquivos de implementação/testes e a documentação deste plano/especificação aparecem como escopo.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
Run: `git add resources/views/components/home/hero.blade.php tests/Feature/PublicSite/ImmersivePhotoHeroTest.php tests/Browser/HomeEditorialCadenceTest.php docs/superpowers && git commit -m "feat(home): tornar hero fotográfico full-bleed"`
|
||||||
|
|
||||||
|
Expected: commit focado, sem artefatos de build ou screenshots.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Hero da home com fotografia full-bleed
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Dar à fotografia do hero da home a mesma presença contínua da referência aprovada: no desktop, ela preenche integralmente a coluna direita abaixo do header; o conteúdo editorial permanece concentrado na coluna esquerda.
|
||||||
|
|
||||||
|
## Escopo
|
||||||
|
|
||||||
|
- Alterar somente o componente `resources/views/components/home/hero.blade.php` e seus testes diretos.
|
||||||
|
- Quando `hero_image_path` existir, usar uma grade externa `0.9fr / 1.1fr` a partir de `md` (768px), com altura exata `calc(100dvh - 5rem)`: o espaço restante depois do header desktop. A mídia deve chegar ao topo e à base da seção, sem padding vertical ou lateral de container.
|
||||||
|
- Manter o texto, CTAs, imagem CMS, `alt`, carregamento eager, `fetchpriority`, responsividade de imagem e atributos de motion existentes.
|
||||||
|
- Abaixo de `md`, manter a ordem texto seguido de imagem, sem overflow horizontal, e fixar a mídia em `aspect-ratio: 4 / 5` com `object-cover`.
|
||||||
|
- Quando não houver imagem configurada, manter a abertura tonal, tipográfica e contida já existente, sem área vazia para mídia.
|
||||||
|
|
||||||
|
## Fora de escopo
|
||||||
|
|
||||||
|
- Não alterar conteúdo do CMS, imagens, Open Graph, outras rotas públicas, header, animações, tokens globais ou dependências.
|
||||||
|
- Não criar ou publicar mídia nova.
|
||||||
|
|
||||||
|
## Estrutura
|
||||||
|
|
||||||
|
O componente continuará sendo a única unidade de layout do hero. A variante com foto terá um wrapper full-width em desktop. A coluna textual terá `data-hero-content`, margem esquerda `max(var(--amare-container-padding), calc((100vw - var(--amare-container-max)) / 2 + var(--amare-container-padding)))`, a mesma régua esquerda de `container-amare`, e padding direito fluido para não comprimir a leitura. O bloco de mídia ocupará os 55% da grade, receberá `data-split-hero` e `data-reveal-media`, e deixará de ficar limitado pelo container. O atributo `sizes` será `(max-width: 767px) 100vw, 55vw`. A variante sem foto permanece no wrapper atual.
|
||||||
|
|
||||||
|
## Critérios de aceitação
|
||||||
|
|
||||||
|
- Em desktop, `data-split-hero` inicia sob o header e mede `calc(100dvh - 5rem)`, sem margens externas de container.
|
||||||
|
- A foto usa `object-cover`, `loading="eager"`, `fetchpriority="high"`, path/alt próprios do hero e `sizes="(max-width: 767px) 100vw, 55vw"`.
|
||||||
|
- Em mobile, o conteúdo essencial continua acessível antes da foto, sem overflow nem texto recortado.
|
||||||
|
- O fallback sem foto continua com `data-tonal-hero`, sem `data-split-hero` e sem mídia renderizada.
|
||||||
|
- `data-motion="page-open"`, `data-reveal-group`, beats existentes, motion reduzido, CTAs e semântica atual continuam funcionais.
|
||||||
|
- Testes verificam os contratos full-bleed da variante com foto, a ausência de mídia no fallback, e a continuidade de motion/semântica.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
A suíte browser contém uma camada exclusiva de comparação de pixels: um teste com baselines versionados, um trait para estabilizar capturas, exportação de diffs e configuração de CI para seeds e artefatos determinísticos. O restante da cobertura browser — smoke, acessibilidade, motion e fixtures de conteúdo — não depende dessa camada.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Remover integralmente a comparação de pixels, os baselines e a infraestrutura exclusiva de suporte.
|
||||||
|
- Manter o job browser em FrankenPHP e suas verificações funcionais, de acessibilidade e motion.
|
||||||
|
- Atualizar as fontes normativas e as specs ativas para que não exijam snapshots visuais.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Não substituir a comparação de pixels por outro serviço ou ferramenta de regressão visual.
|
||||||
|
- Não remover `VisualContentSeeder`, fixtures JPEG, `APP_FROZEN_NOW` que tenham usos fora das capturas, screenshots documentais ou testes browser não relacionados.
|
||||||
|
- Não alterar APIs públicas, rotas, conteúdo nem comportamentos de interface.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
1. **Eliminar a capability em vez de reduzir a matriz.** Todos os requisitos de `visual-regression` serão removidos, pois o produto não terá mais comparação de pixels. Alternativa rejeitada: conservar apenas algumas telas, pois ainda manteria baselines e o custo operacional indesejado.
|
||||||
|
2. **Preservar diagnósticos gerais de browser.** O CI manterá screenshots, logs e relatórios úteis a falhas funcionais; serão removidos apenas seed global, relógio congelado e artefatos específicos de baseline/diff. Alternativa rejeitada: retirar todos os diagnósticos, pois reduziria a capacidade de investigar falhas não visuais.
|
||||||
|
3. **Separar determinismo reutilizável da captura visual.** `VisualContentSeeder` e `APP_FROZEN_NOW` permanecem quando usados por outros testes ou pela aplicação; somente as referências exclusivas a screenshot são excluídas. Alternativa rejeitada: remover os nomes por associação, pois quebraria fixtures e cenários existentes.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Uma mudança puramente visual deixa de bloquear CI] → acessibilidade, smoke, motion e revisão humana continuam no fluxo de qualidade.
|
||||||
|
- [Remover preparação comum do job browser afeta testes restantes] → inspecionar usos de seed/relógio e executar a suíte browser contra FrankenPHP após a alteração.
|
||||||
|
- [Uma spec ativa ainda reintroduz snapshots] → remover as menções no delta de `enhance-public-motion` antes da validação OpenSpec.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Publicar a remoção junto com a atualização da documentação e do CI; não há migração de dados. O rollback é um revert do commit, restaurando os arquivos versionados e a configuração anterior.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
As comparações pixel-a-pixel e seus baselines adicionam manutenção e infraestrutura exclusiva sem serem um gate de qualidade desejado. A qualidade do site continuará protegida por jornadas browser, acessibilidade, motion, smoke e screenshots documentais, sem dependência de snapshots versionados.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Remove a capability `visual-regression` e todos os requisitos de baselines, snapshots e diffs visuais.
|
||||||
|
- Atualiza os quality gates para manter a cobertura browser funcional, de acessibilidade e motion sem comparações de pixels.
|
||||||
|
- Remove a infraestrutura de testes, CI e documentação usada exclusivamente para regressão visual.
|
||||||
|
- **BREAKING** para contribuidores: o comando `composer visual:update` deixa de existir.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
Nenhuma.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `visual-regression`: Remover os requisitos da capability descontinuada.
|
||||||
|
- `quality-gates`: Remover a exigência de executar e publicar diagnósticos de snapshots visuais.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta os testes browser e seus helpers exclusivos, baselines rastreados, configuração de Composer e CI, `.gitignore`, `SPEC.md` e a documentação ativa de motion. Não altera rotas, APIs, dados de conteúdo, fixtures reutilizadas, nem a cobertura funcional, de acessibilidade ou motion.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Browser tests run against FrankenPHP-served application
|
||||||
|
|
||||||
|
The system SHALL execute browser tests using Pest Browser/Playwright against an application served by FrankenPHP in CI. The `browser` job MUST cover the E2E journeys available in the current phase, the automated accessibility checks, motion behavior and smoke checks for public routes (SPEC §13.4, §13.7 and §14.1), without pixel comparison.
|
||||||
|
|
||||||
|
#### Scenario: Browser job validates served application
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** tests execute against the built application artifact or equivalent production-like image
|
||||||
|
|
||||||
|
#### Scenario: Browser job covers functional, accessibility and motion assertions
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job runs
|
||||||
|
- **THEN** it MUST execute the functional browser, accessibility, motion and smoke suites
|
||||||
|
- **AND** a failing functional assertion or a critical/serious accessibility issue MUST block merge
|
||||||
|
|
||||||
|
#### Scenario: Browser failures publish diagnostics
|
||||||
|
|
||||||
|
- **WHEN** a browser test fails in CI
|
||||||
|
- **THEN** the job MUST publish screenshots, application logs and browser logs as diagnostics
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
## REMOVED Requirements
|
||||||
|
|
||||||
|
### Requirement: Public screens have desktop and mobile visual baselines
|
||||||
|
**Reason**: O projeto não manterá mais comparação de pixels nem baselines versionados para telas públicas.
|
||||||
|
**Migration**: Remover as asserções de screenshot e os arquivos baseline; manter testes browser funcionais, de acessibilidade, motion e smoke.
|
||||||
|
|
||||||
|
### Requirement: Visual runs are deterministic
|
||||||
|
**Reason**: Não haverá execução de captura para comparação visual.
|
||||||
|
**Migration**: Preservar seeds e relógio congelado apenas onde forem necessários por outros testes.
|
||||||
|
|
||||||
|
### Requirement: Baseline updates are explicit and reviewed
|
||||||
|
**Reason**: O comando e o fluxo de atualização de baselines foram descontinuados junto com a capability.
|
||||||
|
**Migration**: Remover `composer visual:update` e qualquer configuração de CI dedicada a regenerar, transportar ou publicar baselines e diffs.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
## 1. Contratos normativos
|
||||||
|
|
||||||
|
- [x] 1.1 Remover os requisitos e critérios de regressão visual de `SPEC.md`, ADR-005 e do delta ativo `enhance-public-motion`.
|
||||||
|
- [x] 1.2 Sincronizar os deltas da mudança para remover a capability `visual-regression` e atualizar `quality-gates`.
|
||||||
|
|
||||||
|
## 2. Infraestrutura de regressão visual
|
||||||
|
|
||||||
|
- [x] 2.1 Remover o teste browser, os 16 baselines, o trait de screenshot, o exportador de diffs e seu teste unitário.
|
||||||
|
- [x] 2.2 Simplificar o `TestCase` e remover o script Composer e a regra de ignore exclusivos de baselines.
|
||||||
|
|
||||||
|
## 3. Browser CI e documentação operacional
|
||||||
|
|
||||||
|
- [x] 3.1 Remover seed global, relógio congelado e cópias de snapshots/diffs exclusivos de baseline do job browser, preservando diagnósticos funcionais.
|
||||||
|
- [x] 3.2 Atualizar a documentação operacional que referencia o fluxo descontinuado sem afetar fixtures ou congelamento usados fora dele.
|
||||||
|
|
||||||
|
## 4. Verificação e arquivamento
|
||||||
|
|
||||||
|
- [x] 4.1 Confirmar ausência dos símbolos e comandos removidos no código ativo e executar os gates de qualidade aplicáveis.
|
||||||
|
- [x] 4.2 Validar OpenSpec estritamente, marcar as tarefas e arquivar a mudança sincronizada.
|
||||||
@@ -10,7 +10,7 @@ O site público usa Blade/Tailwind e já contém um runtime pequeno em `resource
|
|||||||
- Manter texto legível durante toda entrada, animando somente `transform` e recorte de mídia.
|
- Manter texto legível durante toda entrada, animando somente `transform` e recorte de mídia.
|
||||||
- Garantir enhancement progressivo: estado final imediato sem JavaScript, observer ou com movimento reduzido.
|
- Garantir enhancement progressivo: estado final imediato sem JavaScript, observer ou com movimento reduzido.
|
||||||
- Preservar interação, foco, validação e ausência de overflow em desktop/mobile.
|
- Preservar interação, foco, validação e ausência de overflow em desktop/mobile.
|
||||||
- Cobrir marcação, comportamento real, acessibilidade, console e capturas determinísticas.
|
- Cobrir marcação, comportamento real, acessibilidade e console.
|
||||||
|
|
||||||
**Non-Goals:**
|
**Non-Goals:**
|
||||||
|
|
||||||
@@ -32,15 +32,14 @@ O site público usa Blade/Tailwind e já contém um runtime pequeno em `resource
|
|||||||
|
|
||||||
6. **Progresso com coalescência por frame.** Eventos de scroll apenas agendam uma atualização por `requestAnimationFrame`; o cálculo existente é preservado. Alternativa rejeitada: calcular em todo evento, que multiplica leituras/escritas durante scroll.
|
6. **Progresso com coalescência por frame.** Eventos de scroll apenas agendam uma atualização por `requestAnimationFrame`; o cálculo existente é preservado. Alternativa rejeitada: calcular em todo evento, que multiplica leituras/escritas durante scroll.
|
||||||
|
|
||||||
7. **Testes em camadas.** Feature tests comprovam contratos Blade/CSS/JS e renderização de erros; browser tests comprovam entrada, stagger, direções, reduced motion, fallback, interação, overflow e console. Axe/snapshots passam a incluir sobre, contato, privacidade e 404; demais erros ficam em renderização feature.
|
7. **Testes em camadas.** Feature tests comprovam contratos Blade/CSS/JS e renderização de erros; browser tests comprovam entrada, stagger, direções, reduced motion, fallback, interação, overflow e console. Axe e console incluem sobre, contato, privacidade e 404; demais erros ficam em renderização feature.
|
||||||
|
|
||||||
## Risks / Trade-offs
|
## Risks / Trade-offs
|
||||||
|
|
||||||
- [Conteúdo pisca entre estado final e início do enhancement] → inicializar no primeiro módulo Vite, limitar transformações a distâncias pequenas e nunca ocultar texto.
|
- [Conteúdo pisca entre estado final e início do enhancement] → inicializar no primeiro módulo Vite, limitar transformações a distâncias pequenas e nunca ocultar texto.
|
||||||
- [Transforms laterais causam overflow horizontal] → limitar distância por breakpoint, manter recorte no contêiner público e testar `scrollWidth` em ambos viewports.
|
- [Transforms laterais causam overflow horizontal] → limitar distância por breakpoint, manter recorte no contêiner público e testar `scrollWidth` em ambos viewports.
|
||||||
- [Snapshots ficam instáveis] → continuar capturando com `reducedMotion: reduce` e transições desabilitadas.
|
|
||||||
- [Muitos observers/estilos inline] → usar um único observer, custom property de índice limitada e `unobserve` imediato.
|
- [Muitos observers/estilos inline] → usar um único observer, custom property de índice limitada e `unobserve` imediato.
|
||||||
- [Páginas de erro não carregam o runtime em todos os contextos] → o estado final é o baseline seguro; feature tests cobrem os cinco templates.
|
- [Páginas de erro não carregam o runtime em todos os contextos] → o estado final é seguro; feature tests cobrem os cinco templates.
|
||||||
|
|
||||||
## Migration Plan
|
## Migration Plan
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ O site público já possui uma abertura focal na home, mas o restante da experi
|
|||||||
- Alternar depoimentos entre esquerda e direita, filtrando citações vazias antes de renderizar a sequência.
|
- Alternar depoimentos entre esquerda e direita, filtrando citações vazias antes de renderizar a sequência.
|
||||||
- Adicionar feedback curto e não bloqueante a links, CTAs, navegação e controles de formulário, preservando foco e mensagens de validação.
|
- Adicionar feedback curto e não bloqueante a links, CTAs, navegação e controles de formulário, preservando foco e mensagens de validação.
|
||||||
- Garantir estado final imediato sem JavaScript, sem `IntersectionObserver` e com `prefers-reduced-motion: reduce`.
|
- Garantir estado final imediato sem JavaScript, sem `IntersectionObserver` e com `prefers-reduced-motion: reduce`.
|
||||||
- Limitar o progresso/índice da home a uma atualização por frame e ampliar testes de marcação, browser, acessibilidade, console e regressão visual.
|
- Limitar o progresso/índice da home a uma atualização por frame e ampliar testes de marcação, browser, acessibilidade e console.
|
||||||
- Não objetivos: não criar transições entre rotas, alterar conteúdo ou layout estrutural, adicionar biblioteca de animação, modificar CMS/API/banco/Filament, nem introduzir itens fora do MVP listados em SPEC.md §4.2.
|
- Não objetivos: não criar transições entre rotas, alterar conteúdo ou layout estrutural, adicionar biblioteca de animação, modificar CMS/API/banco/Filament, nem introduzir itens fora do MVP listados em SPEC.md §4.2.
|
||||||
|
|
||||||
## Capabilities
|
## Capabilities
|
||||||
@@ -23,7 +23,6 @@ O site público já possui uma abertura focal na home, mas o restante da experi
|
|||||||
- `design-tokens`: Especificar tokens compartilhados de duração, stagger, distância e easing para motion editorial.
|
- `design-tokens`: Especificar tokens compartilhados de duração, stagger, distância e easing para motion editorial.
|
||||||
- `public-site-pages`: Cobrir todas as páginas públicas visuais e templates de erro com o contrato compartilhado de abertura e reveal.
|
- `public-site-pages`: Cobrir todas as páginas públicas visuais e templates de erro com o contrato compartilhado de abertura e reveal.
|
||||||
- `testimonials`: Filtrar citações vazias e alternar explicitamente a direção de entrada dos depoimentos renderizados.
|
- `testimonials`: Filtrar citações vazias e alternar explicitamente a direção de entrada dos depoimentos renderizados.
|
||||||
- `visual-regression`: Ampliar a matriz visual para sobre, contato, privacidade e 404, mantendo capturas determinísticas sem animação.
|
|
||||||
- `web-accessibility`: Ampliar axe/console e validar conteúdo imediatamente utilizável com movimento reduzido ou enhancement indisponível.
|
- `web-accessibility`: Ampliar axe/console e validar conteúdo imediatamente utilizável com movimento reduzido ou enhancement indisponível.
|
||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
- [x] 3.1 Add browser tests for real opening/reveal state, 90 ms capped stagger, one-shot observation and interaction during motion
|
- [x] 3.1 Add browser tests for real opening/reveal state, 90 ms capped stagger, one-shot observation and interaction during motion
|
||||||
- [x] 3.2 Add desktop/mobile tests for directional distance, reduced motion final state, no-JS/observer fallback, horizontal overflow and clean console
|
- [x] 3.2 Add desktop/mobile tests for directional distance, reduced motion final state, no-JS/observer fallback, horizontal overflow and clean console
|
||||||
- [x] 3.3 Extend axe and console route matrices to about, contact, privacy and branded 404
|
- [x] 3.3 Extend axe and console route matrices to about, contact, privacy and branded 404
|
||||||
- [x] 3.4 Extend deterministic desktop/mobile visual snapshots to about, contact, privacy and branded 404 and review the generated diffs
|
- [x] 3.4 Extend axe and console route matrices to about, contact, privacy and branded 404
|
||||||
|
|
||||||
## 4. Verification and delivery
|
## 4. Verification and delivery
|
||||||
|
|
||||||
- [x] 4.1 Run focused feature/browser tests, build, Pint, isolated-cache PHPStan and strict OpenSpec validation
|
- [x] 4.1 Run focused feature/browser tests, build, Pint, isolated-cache PHPStan and strict OpenSpec validation
|
||||||
- [x] 4.2 Run `composer quality` and visually review desktop/mobile with normal and reduced motion
|
- [x] 4.2 Run `composer quality` and visually review desktop/mobile with normal and reduced motion
|
||||||
- [ ] 4.3 Stage only scoped files, commit conventionally, open a PR with screenshots and verification evidence, watch CI to green, merge and remove the worktree
|
- [x] 4.3 Stage only scoped files, commit conventionally, open a PR with screenshots and verification evidence, watch CI to green, merge and remove the worktree — delivered in PR #22 with the requested screenshots and local verification; the PR merged as `0aee15e`, the worktree/remote branch no longer exists, and the current implementation is covered by all five green CI jobs on `main` (run 31501097667). Pixel snapshots were later removed by PR #44 and are not part of the current gate.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
O commit de base ja separa as imagens hero de Open Graph, oferece variantes responsivas, fallbacks tonais e preserva o formulario de briefing. As views publicas ainda usam algumas grades simetricas, especialmente nas colecoes de portfolio. A referencia aprovada define composicao e materialidade, nunca novo acervo ou copy.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Aplicar uma cadencia de campos tonais, regras e proporcoes alternadas nas paginas narrativas.
|
||||||
|
- Manter uma ordem DOM linear e completa em telas pequenas.
|
||||||
|
- Reforcar os contratos existentes de acessibilidade, motion progressivo, performance de imagem e SEO.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Alterar dados CMS, contratos de rota, controller, metadata, uploads, variantes ou o formulario.
|
||||||
|
- Usar imagens externas, criar conteudo comercial, adicionar dependencias, atualizar snapshots ou reintroduzir regressao visual.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Usar variacoes de `col-span` e `aspect-ratio` nas galerias existentes
|
||||||
|
|
||||||
|
As imagens continuam no mesmo componente responsivo, mas a primeira imagem e os itens pares recebem encaixes editoriais no desktop. A alternativa seria uma galeria JavaScript ou masonry; foi descartada por prejudicar a ordem de leitura e adicionar comportamento sem necessidade.
|
||||||
|
|
||||||
|
### Aplicar assimetria pela composicao, nao por posicionamento absoluto
|
||||||
|
|
||||||
|
Grid responsivo, margens e campos tonais criam o deslocamento sem tirar conteudo do fluxo. Isso preserva foco, leitura mobile e fallback sem JavaScript. Posicionamento absoluto foi descartado por aumentar risco de sobreposicao e overflow.
|
||||||
|
|
||||||
|
### Reutilizar motion de transformacao e clip ja existente
|
||||||
|
|
||||||
|
As novas regioes conservam `data-reveal` e grupos existentes. Nenhum texto passa a depender de opacidade; `prefers-reduced-motion` continua exibindo o estado final imediatamente.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Ritmo visual pode parecer irregular com poucos cases] -> regras e metadados mantem uma leitura coerente mesmo com um unico item.
|
||||||
|
- [Classes responsivas podem afetar o fluxo em mobile] -> DOM linear e classes de uma coluna continuam sendo a base abaixo de `md`.
|
||||||
|
- [Mudar CSS pode gerar diferencas de ambiente] -> validar markup, acessibilidade funcional e build; snapshots permanecem fora do escopo.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
Nao ha migracao de dados. A mudanca e retrocompativel: sem imagens, os fallbacks tonais existentes continuam ativos; com imagens, o mesmo `srcset` responsivo e usado. Reverter o commit restaura somente apresentacao.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
Nenhuma. Fotografia autorizada de producao permanece uma dependencia de conteudo futura.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
O site público já possui os conteúdos, fluxos e a base Heritage Editorial, mas algumas páginas ainda repetem grades regulares que enfraquecem a cadência da referência aprovada. A recomposição consolida uma leitura editorial contínua sem alterar dados CMS, SEO ou a jornada de briefing.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Recompõe as superfícies públicas narrativas com campos tonais, regras finas, assimetria desktop e sequência vertical legível em mobile.
|
||||||
|
- Organiza a home nos capítulos editoriais existentes, com portfólio em campo oliva e recortes de imagens em proporções alternadas.
|
||||||
|
- Reestiliza listagens e detalhe de portfólio para que imagens e blocos de leitura não dependam de uma grade de cartões repetida.
|
||||||
|
- Mantém contato, privacidade e erros em tratamento sóbrio; preserva menu móvel, foco, formulário, SEO, motion progressivo e conteúdo do CMS.
|
||||||
|
- Documenta que heros fotográficos e sua administração já estão presentes na base atual, sem acoplar mídia editorial à imagem Open Graph.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- Nenhuma.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `public-site-pages`: composição e cadência visual das rotas públicas passam a exigir ritmos editoriais assimétricos.
|
||||||
|
- `content-media`: a apresentação de mídia de portfólio passa a suportar proporções editoriais alternadas sem mudar armazenamento ou variantes.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Afeta Blade e estilos do site público, testes de estrutura/renderização e a documentação OpenSpec. Não altera rotas, controladores, dados de conteúdo, dependências ou contratos de SEO.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
Não introduz itens excluídos por SPEC.md §4.2, novos canais, prova comercial, conteúdo de produção, imagens externas, dependências de animação, snapshots visuais ou mudanças no Filament.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Editorial layouts MUST use CSS-only tonal treatment and alternating aspect ratios for portfolio media while retaining document order and the image component's `srcset`, `sizes`, dimensions and loading behavior. The system MUST NOT introduce external image CDN dependencies that compromise browser-test reliability.
|
||||||
|
|
||||||
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
|
|
||||||
|
#### Scenario: Editorial portfolio media retains responsive delivery
|
||||||
|
|
||||||
|
- **WHEN** a portfolio listing or gallery applies an editorial image proportion
|
||||||
|
- **THEN** the rendered image MUST still expose the responsive media component markup
|
||||||
|
- **AND** image order and lazy-loading behavior MUST remain intact
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition: asymmetric spreads and tonal fields on desktop, with portfolio images in alternating editorial proportions, and a linear complete sequence on mobile rather than rounded card grids.
|
||||||
|
|
||||||
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
|
- **GIVEN** published services, cases and testimonials exist
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
- **AND** the portfolio section MUST use an olive tonal field with editorial image proportions
|
||||||
|
|
||||||
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
|
- **WHEN** a visitor activates the primary or final CTA on the home
|
||||||
|
- **THEN** the visitor MUST be taken to the `contact` route
|
||||||
|
- **AND** no lead record MUST be created
|
||||||
|
|
||||||
|
#### Scenario: Empty catalog sections are omitted
|
||||||
|
|
||||||
|
- **GIVEN** no published services, cases or testimonials
|
||||||
|
- **WHEN** a visitor loads the home
|
||||||
|
- **THEN** the response MUST be 200
|
||||||
|
- **AND** the services, portfolio and testimonials sections MUST be omitted instead of rendering empty containers
|
||||||
|
- **AND** hero, manifesto, method, positioning and final CTA MUST still render
|
||||||
|
|
||||||
|
#### Scenario: Home has no console errors
|
||||||
|
|
||||||
|
- **WHEN** the home is loaded in a real browser at desktop and mobile viewports
|
||||||
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
|
|
||||||
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
|
||||||
|
|
||||||
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads `/servicos`
|
||||||
|
- **THEN** every published service MUST be listed with title and summary in `sort_order`
|
||||||
|
- **AND** the listing MUST use the public editorial layout (not an unrelated visual system)
|
||||||
|
|
||||||
|
#### Scenario: Gallery respects stored order
|
||||||
|
|
||||||
|
- **GIVEN** a published case with multiple gallery images
|
||||||
|
- **WHEN** a visitor loads the case detail
|
||||||
|
- **THEN** the images MUST be rendered ordered by `sort_order`
|
||||||
|
- **AND** desktop presentation MUST alternate editorial image proportions without changing that order
|
||||||
|
|
||||||
|
#### Scenario: Listings paginate open-ended growth
|
||||||
|
|
||||||
|
- **WHEN** the number of published cases exceeds the page size
|
||||||
|
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
||||||
|
|
||||||
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. Contact, privacy and error surfaces MUST remain sober tonal layouts without decorative photography. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
|
|
||||||
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
|
- **WHEN** a visitor requests a non-existent public URL
|
||||||
|
- **THEN** the response status MUST be 404
|
||||||
|
- **AND** the page MUST use the public layout and offer navigation back to the home
|
||||||
|
|
||||||
|
#### Scenario: Server error hides internals in production
|
||||||
|
|
||||||
|
- **GIVEN** `APP_DEBUG` is false
|
||||||
|
- **WHEN** an unhandled exception occurs on a public route
|
||||||
|
- **THEN** the response MUST be a generic branded error page
|
||||||
|
- **AND** MUST NOT contain a stack trace, file path, or environment variable
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
## 1. Editorial composition
|
||||||
|
|
||||||
|
- [x] 1.1 Add regression coverage for alternating public portfolio composition and sober functional surfaces.
|
||||||
|
- [x] 1.2 Recompose home portfolio, portfolio listing and case gallery with responsive editorial proportions while preserving order and responsive media delivery.
|
||||||
|
- [x] 1.3 Refine public content-page rhythm with tonal fields and desktop offsets without changing routes, CMS content or briefing behavior.
|
||||||
|
|
||||||
|
## 2. Verification
|
||||||
|
|
||||||
|
- [x] 2.1 Run focused feature and browser checks for public routes, motion, accessibility and navigation.
|
||||||
|
- [x] 2.2 Run formatting, static analysis, frontend build and strict OpenSpec validation without updating snapshots.
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
|
|
||||||
## 6. Phase 0 exit evidence
|
## 6. Phase 0 exit evidence
|
||||||
|
|
||||||
- [ ] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output)
|
- [x] 6.1 Perform first successful staging deploy of a `main` SHA and capture evidence (workflow URL, smoke output) — run 31395107465 deployed `7e68c0e` successfully; smoke recorded `/up`, `/` and `/admin/login` returning HTTP 200.
|
||||||
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
- [ ] 6.2 Verify rollback to previous SHA works once on staging
|
||||||
- [x] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any — flipped L2338 (FrankenPHP/Compose) and the auth/npm-audit/coverage bullets to `[x]`; left the staging hello-world bullet and the phase exit-criterion line unchecked (Dokploy deploy still failing, out of scope here)
|
- [x] 6.3 Update `SPEC.md` §18 Fase 0 checkboxes only for items with evidence; note remaining deferred items if any — local runtime/auth/audit/coverage evidence was recorded by PR #39, and the staging exit criterion was subsequently marked complete from successful run 31395107465.
|
||||||
- [ ] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green
|
- [x] 6.4 Run full `composer quality` and confirm all five CI jobs + staging deploy path green — local quality evidence is recorded in task 2.4; PR #39 passed `static`, `unit`, `feature`, `browser` and `container`, then run 31395107465 deployed that exact merge SHA and passed all three smoke endpoints.
|
||||||
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks (1–3) also complete
|
- [ ] 6.5 Report in SPEC §24 format; archive this change only after remaining parity tasks are complete — blocked by tasks 3.4 and 6.2, tracked separately in MAN-125.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: Public screens have desktop and mobile visual baselines
|
|
||||||
|
|
||||||
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio, Detalhe do portfólio, Sobre, Contato, Privacidade and branded 404, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
|
||||||
|
|
||||||
#### Scenario: Unintended visual change fails the suite
|
|
||||||
|
|
||||||
- **GIVEN** approved baselines exist
|
|
||||||
- **WHEN** a code change alters the rendering of a covered screen
|
|
||||||
- **THEN** the visual assertion MUST fail and report the diff
|
|
||||||
|
|
||||||
#### Scenario: Both viewports are covered
|
|
||||||
|
|
||||||
- **WHEN** the visual suite runs
|
|
||||||
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
|
||||||
|
|
||||||
#### Scenario: Heritage Editorial identity is captured
|
|
||||||
|
|
||||||
- **WHEN** approved baselines for the home are reviewed after this change
|
|
||||||
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
|
||||||
|
|
||||||
#### Scenario: Motion does not destabilize new baselines
|
|
||||||
|
|
||||||
- **WHEN** Sobre, Contato, Privacidade, or 404 is captured
|
|
||||||
- **THEN** the browser MUST use the final reduced-motion state before asserting the screenshot
|
|
||||||
@@ -57,3 +57,31 @@ The system SHALL define a container healthcheck that verifies application availa
|
|||||||
- **WHEN** the application inside the container fails to respond healthy on `/up`
|
- **WHEN** the application inside the container fails to respond healthy on `/up`
|
||||||
- **THEN** the container healthcheck MUST report unhealthy status
|
- **THEN** the container healthcheck MUST report unhealthy status
|
||||||
|
|
||||||
|
### Requirement: Local Compose runs FrankenPHP application alongside PostgreSQL
|
||||||
|
|
||||||
|
The local Docker Compose stack SHALL include a FrankenPHP application service built from the project Dockerfile, dependent on a healthy PostgreSQL service, exposing the application on port 8000 (or documented equivalent). FrankenPHP MUST run in regular mode (ADR-006).
|
||||||
|
|
||||||
|
#### Scenario: Developer starts full local stack
|
||||||
|
|
||||||
|
- **WHEN** a developer runs `docker compose up -d` with the application service enabled
|
||||||
|
- **THEN** PostgreSQL and the FrankenPHP app containers MUST become healthy
|
||||||
|
- **AND** `GET /up` on the app port MUST return HTTP 200
|
||||||
|
|
||||||
|
#### Scenario: App waits for database readiness
|
||||||
|
|
||||||
|
- **WHEN** the application service starts
|
||||||
|
- **THEN** it MUST depend on the PostgreSQL healthcheck succeeding before becoming ready
|
||||||
|
|
||||||
|
### Requirement: Runtime PHP version is canonically 8.4
|
||||||
|
|
||||||
|
Docker, CI, and project documentation MUST treat PHP 8.4 as the canonical runtime for this MVP. Statements that require PHP 8.5+ MUST NOT remain in project docs while the runtime image uses 8.4.
|
||||||
|
|
||||||
|
#### Scenario: Dockerfile pins PHP 8.4
|
||||||
|
|
||||||
|
- **WHEN** the production/runtime image is built
|
||||||
|
- **THEN** the FrankenPHP base image MUST use PHP 8.4
|
||||||
|
|
||||||
|
#### Scenario: Documentation matches the runtime
|
||||||
|
|
||||||
|
- **WHEN** a contributor reads the README requirements
|
||||||
|
- **THEN** the documented PHP version MUST be 8.4 (compatible with Composer `^8.3`)
|
||||||
|
|||||||
@@ -126,11 +126,16 @@ The system SHALL provide optimized Amare brand logo assets derived from the offi
|
|||||||
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
- **THEN** the uploaded logo MUST be used instead of the static fallback
|
||||||
|
|
||||||
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
### Requirement: Editorial image treatment remains self-hosted and deterministic
|
||||||
|
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Editorial layouts MUST use CSS-only tonal treatment and alternating aspect ratios for portfolio media while retaining document order and the image component's `srcset`, `sizes`, dimensions and loading behavior. The system MUST NOT introduce external image CDN dependencies that compromise browser-test reliability.
|
||||||
Public photography SHALL continue to use validated self-hosted uploads and responsive variants. Decorative saturation/contrast treatment for editorial mood MUST be applied via CSS on self-hosted images and MUST NOT introduce external image CDN dependencies that break deterministic visual tests.
|
|
||||||
|
|
||||||
#### Scenario: Public pages do not depend on external stock hosts
|
#### Scenario: Public pages do not depend on external stock hosts
|
||||||
|
|
||||||
- **WHEN** the visual or browser suite loads covered public routes
|
- **WHEN** the visual or browser suite loads covered public routes
|
||||||
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
- **THEN** content images MUST resolve from the application media disk or static fixtures
|
||||||
- **AND** MUST NOT require network access to third-party stock hosts
|
- **AND** MUST NOT require network access to third-party stock hosts
|
||||||
|
|
||||||
|
#### Scenario: Editorial portfolio media retains responsive delivery
|
||||||
|
|
||||||
|
- **WHEN** a portfolio listing or gallery applies an editorial image proportion
|
||||||
|
- **THEN** the rendered image MUST still expose the responsive media component markup
|
||||||
|
- **AND** image order and lazy-loading behavior MUST remain intact
|
||||||
|
|||||||
@@ -53,3 +53,11 @@ The public site SHALL express hierarchy through tonal paper fields, 1px botanica
|
|||||||
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
- **THEN** they MUST NOT depend on `--amare-shadow-*` card elevation for hierarchy
|
||||||
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
- **AND** separation MUST come from borders, tonal backgrounds, or whitespace
|
||||||
|
|
||||||
|
### Requirement: Editorial motion tokens are centralized
|
||||||
|
|
||||||
|
The system SHALL define shared public motion tokens for an approximately 500 ms entrance, short interaction feedback, exponential ease-out, 90 ms stagger capped at 270 ms, 12 px vertical distance, and 24 px desktop / 16 px mobile lateral distance. Public motion CSS MUST consume these tokens instead of duplicating arbitrary values.
|
||||||
|
|
||||||
|
#### Scenario: Motion runtime uses shared values
|
||||||
|
- **WHEN** page openings, reveals, or interaction feedback are styled
|
||||||
|
- **THEN** duration, easing, stagger, and distance MUST be derived from centralized tokens
|
||||||
|
|
||||||
|
|||||||
@@ -26,3 +26,17 @@ The system SHALL return a failure status when the application cannot initialize
|
|||||||
- **WHEN** the application fails to boot due to misconfiguration or missing dependencies
|
- **WHEN** the application fails to boot due to misconfiguration or missing dependencies
|
||||||
- **THEN** the health endpoint MUST NOT return HTTP 200
|
- **THEN** the health endpoint MUST NOT return HTTP 200
|
||||||
|
|
||||||
|
### Requirement: Staging health and smoke use the public /up endpoint
|
||||||
|
|
||||||
|
Staging healthchecks and post-deploy smoke MUST call `GET /up` without authentication and MUST treat a non-200 response as deployment failure (SPEC §15.5, §16.2). The health response MUST NOT expose secrets.
|
||||||
|
|
||||||
|
#### Scenario: Staging healthcheck probes /up
|
||||||
|
|
||||||
|
- **WHEN** Dokploy or the container runtime evaluates application health after deploy
|
||||||
|
- **THEN** it MUST request `/up`
|
||||||
|
- **AND** MUST require HTTP 200 before marking the service healthy
|
||||||
|
|
||||||
|
#### Scenario: Smoke failure on /up fails the deploy gate
|
||||||
|
|
||||||
|
- **WHEN** post-deploy smoke requests `/up` and receives a non-200 response
|
||||||
|
- **THEN** the staging promotion MUST be considered failed
|
||||||
|
|||||||
@@ -58,3 +58,35 @@ The system SHALL restrict user management (create, update, deactivate) to users
|
|||||||
- **WHEN** an authenticated assistant attempts to access user management in the panel
|
- **WHEN** an authenticated assistant attempts to access user management in the panel
|
||||||
- **THEN** the system MUST deny access via authorization policy
|
- **THEN** the system MUST deny access via authorization policy
|
||||||
|
|
||||||
|
### Requirement: Verified email is required for panel access
|
||||||
|
|
||||||
|
Internal users MUST have a verified email address to access the Filament panel (SPEC §12.1, ADM-01). Active users with unverified email MUST be denied panel access. Development seeds MUST mark local demo users as verified.
|
||||||
|
|
||||||
|
#### Scenario: Unverified active user is denied panel access
|
||||||
|
|
||||||
|
- **WHEN** an active user with null `email_verified_at` authenticates
|
||||||
|
- **THEN** the system MUST NOT grant access to the Filament panel
|
||||||
|
|
||||||
|
#### Scenario: Verified active user can access the panel
|
||||||
|
|
||||||
|
- **WHEN** an active user with a non-null `email_verified_at` submits valid credentials
|
||||||
|
- **THEN** the system authenticates the user and allows Filament panel access subject to role rules
|
||||||
|
|
||||||
|
#### Scenario: Local seed users are verified
|
||||||
|
|
||||||
|
- **WHEN** `DatabaseSeeder` creates the local admin and assistant
|
||||||
|
- **THEN** both users MUST have `email_verified_at` set
|
||||||
|
|
||||||
|
### Requirement: Password reset flow is covered by automated tests
|
||||||
|
|
||||||
|
The secure password reset flow for internal users MUST be covered by feature tests that assert a reset request for a registered email queues/sends a reset notification without revealing whether the email exists to the client (SPEC §12.1; existing password-reset requirement).
|
||||||
|
|
||||||
|
#### Scenario: Reset request does not reveal account existence
|
||||||
|
|
||||||
|
- **WHEN** a visitor submits a password reset for an unknown email
|
||||||
|
- **THEN** the response MUST not disclose that the email is unregistered
|
||||||
|
|
||||||
|
#### Scenario: Registered email receives reset notification
|
||||||
|
|
||||||
|
- **WHEN** a visitor submits a password reset for a registered email
|
||||||
|
- **THEN** the system MUST dispatch the password reset notification (faked in tests)
|
||||||
|
|||||||
79
openspec/specs/public-site-motion/spec.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# public-site-motion Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define progressive, accessible and non-blocking motion choreography for the public site, including shared timings, one-shot reveals, interaction feedback and safe fallbacks.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
### Requirement: Public pages use shared progressive motion choreography
|
||||||
|
|
||||||
|
The public site SHALL expose a declarative motion contract using `data-motion="page-open"`, `data-motion-beat`, `data-reveal-group`, `data-reveal`, and `data-reveal-from="up|left|right"`. Home, services, portfolio index/detail, about, contact, privacy, and branded 404/419/429/500/503 pages MUST use the contract without introducing route transitions.
|
||||||
|
|
||||||
|
#### Scenario: Visual public route exposes motion hooks
|
||||||
|
- **WHEN** a visitor loads any visual public route or branded error page
|
||||||
|
- **THEN** the rendered page MUST expose a page opening hook and appropriate reveal hooks
|
||||||
|
|
||||||
|
#### Scenario: Technical endpoints remain outside motion
|
||||||
|
- **WHEN** a visitor requests `sitemap.xml` or `robots.txt`
|
||||||
|
- **THEN** the response MUST NOT depend on the public motion runtime
|
||||||
|
|
||||||
|
### Requirement: Editorial entrance timing and direction are consistent
|
||||||
|
|
||||||
|
Entrances SHALL use shared tokens for an approximately 500 ms duration, 90 ms stagger capped at 270 ms, 12 px vertical distance, and 24 px desktop / 16 px mobile lateral distance. Text MUST remain fully visible; motion MUST use transforms and MAY use media clipping without opacity fades for text.
|
||||||
|
|
||||||
|
#### Scenario: Reveal group computes capped stagger
|
||||||
|
- **WHEN** more than four reveal items are rendered in one group
|
||||||
|
- **THEN** the applied delay MUST NOT exceed 270 ms
|
||||||
|
|
||||||
|
#### Scenario: Text enters without opacity fade
|
||||||
|
- **WHEN** page opening or scroll reveal motion runs
|
||||||
|
- **THEN** text MUST remain visible throughout the transition
|
||||||
|
|
||||||
|
### Requirement: Reveals execute once without blocking interaction
|
||||||
|
|
||||||
|
Each scroll reveal SHALL activate once, reach its final state, and be removed from observation. Visitors MUST be able to activate links and form controls while entrance motion is running.
|
||||||
|
|
||||||
|
#### Scenario: Revealed content leaves observer
|
||||||
|
- **WHEN** a reveal target intersects the configured viewport threshold
|
||||||
|
- **THEN** it MUST receive the final state and be unobserved
|
||||||
|
- **AND** scrolling away and back MUST NOT replay the reveal
|
||||||
|
|
||||||
|
#### Scenario: CTA remains interactive during entrance
|
||||||
|
- **WHEN** a visitor activates a CTA while its entrance is in progress
|
||||||
|
- **THEN** navigation MUST proceed without waiting for animation completion
|
||||||
|
|
||||||
|
### Requirement: Motion enhancement has immediate safe fallbacks
|
||||||
|
|
||||||
|
Without JavaScript, without `IntersectionObserver`, or when `prefers-reduced-motion: reduce` is active, the site SHALL render every motion target immediately in its final usable state. Non-essential animation and transition MUST NOT run under reduced motion.
|
||||||
|
|
||||||
|
#### Scenario: JavaScript is unavailable
|
||||||
|
- **WHEN** a public page renders without executing JavaScript
|
||||||
|
- **THEN** all content MUST be visible and usable in its final state
|
||||||
|
|
||||||
|
#### Scenario: IntersectionObserver is unavailable
|
||||||
|
- **WHEN** JavaScript runs but `IntersectionObserver` is not supported
|
||||||
|
- **THEN** all reveal targets MUST remain in their final state
|
||||||
|
|
||||||
|
#### Scenario: Reduced motion is preferred
|
||||||
|
- **WHEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
|
- **THEN** page openings, reveals, media clips, and non-essential feedback transitions MUST NOT run
|
||||||
|
|
||||||
|
### Requirement: Public interaction feedback is short and accessible
|
||||||
|
|
||||||
|
CTAs, links, navigation, and form controls SHALL provide short feedback using shared tokens while preserving visible `:focus-visible` indication. Alerts and validation messages MUST NOT be animated by this contract.
|
||||||
|
|
||||||
|
#### Scenario: Keyboard focus remains visible
|
||||||
|
- **WHEN** a visitor focuses an interactive element with the keyboard
|
||||||
|
- **THEN** its focus indicator MUST remain visible and MUST NOT be displaced by motion feedback
|
||||||
|
|
||||||
|
#### Scenario: Validation message appears without motion
|
||||||
|
- **WHEN** server validation renders an error message
|
||||||
|
- **THEN** the message MUST appear in its final position without entrance animation
|
||||||
|
|
||||||
|
### Requirement: Home progress updates at most once per animation frame
|
||||||
|
|
||||||
|
Scroll-driven chapter index and progress updates on the home SHALL be coalesced through `requestAnimationFrame`, with at most one pending update per frame.
|
||||||
|
|
||||||
|
#### Scenario: Multiple scroll events occur in one frame
|
||||||
|
- **WHEN** multiple scroll events fire before the next animation frame
|
||||||
|
- **THEN** the home progress calculation MUST execute only once for that frame
|
||||||
@@ -34,8 +34,7 @@ The system SHALL expose the public routes of SPEC §5.1: `home` (`/`), `services
|
|||||||
- **AND** the case MUST appear in the `/portfolio` listing
|
- **AND** the case MUST appear in the `/portfolio` listing
|
||||||
|
|
||||||
### Requirement: Home renders the editorial structure from CMS content
|
### Requirement: Home renders the editorial structure from CMS content
|
||||||
|
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition: asymmetric spreads and tonal fields on desktop, with portfolio images in alternating editorial proportions, and a linear complete sequence on mobile rather than rounded card grids.
|
||||||
The home page SHALL render, in order: header/navigation, hero, manifesto, featured services summary, featured portfolio selection, working method (four steps), testimonials, Amare positioning/profile, final contact CTA, and footer with contact, social links and legal links (WEB-01). Hero copy, brand name, manifesto, method and principles MUST come from `site_settings` (with editorial defaults when optional fields are empty); services, cases and testimonials MUST come from published records. The home MUST follow the Heritage Editorial composition (asymmetric spreads on desktop, linear sequence on mobile) rather than rounded card grids.
|
|
||||||
|
|
||||||
#### Scenario: Published content is displayed in configured order
|
#### Scenario: Published content is displayed in configured order
|
||||||
|
|
||||||
@@ -44,6 +43,7 @@ The home page SHALL render, in order: header/navigation, hero, manifesto, featur
|
|||||||
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
- **THEN** the published content MUST be displayed following the `sort_order` and featured flags
|
||||||
- **AND** the hero MUST show the values stored in `site_settings`
|
- **AND** the hero MUST show the values stored in `site_settings`
|
||||||
- **AND** the manifesto, method and positioning sections MUST be present
|
- **AND** the manifesto, method and positioning sections MUST be present
|
||||||
|
- **AND** the portfolio section MUST use an olive tonal field with editorial image proportions
|
||||||
|
|
||||||
#### Scenario: CTA leads to the contact placeholder page
|
#### Scenario: CTA leads to the contact placeholder page
|
||||||
|
|
||||||
@@ -65,8 +65,7 @@ The home page SHALL render, in order: header/navigation, hero, manifesto, featur
|
|||||||
- **THEN** the browser console MUST contain no JavaScript errors
|
- **THEN** the browser console MUST contain no JavaScript errors
|
||||||
|
|
||||||
### Requirement: Listing and detail pages exist for catalog content
|
### Requirement: Listing and detail pages exist for catalog content
|
||||||
|
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery. Portfolio listings and galleries MUST use alternating editorial image proportions on desktop while retaining DOM order and a single-column readable sequence on mobile.
|
||||||
The system SHALL render a services listing (WEB-02) and a portfolio listing plus case detail (WEB-03) using the Heritage Editorial visual language. The case detail MUST present summary, event type, optional city/venue/date, challenge, solution, optional result, cover image and the ordered gallery.
|
|
||||||
|
|
||||||
#### Scenario: Services listing shows published services
|
#### Scenario: Services listing shows published services
|
||||||
|
|
||||||
@@ -79,6 +78,7 @@ The system SHALL render a services listing (WEB-02) and a portfolio listing plus
|
|||||||
- **GIVEN** a published case with multiple gallery images
|
- **GIVEN** a published case with multiple gallery images
|
||||||
- **WHEN** a visitor loads the case detail
|
- **WHEN** a visitor loads the case detail
|
||||||
- **THEN** the images MUST be rendered ordered by `sort_order`
|
- **THEN** the images MUST be rendered ordered by `sort_order`
|
||||||
|
- **AND** desktop presentation MUST alternate editorial image proportions without changing that order
|
||||||
|
|
||||||
#### Scenario: Listings paginate open-ended growth
|
#### Scenario: Listings paginate open-ended growth
|
||||||
|
|
||||||
@@ -86,8 +86,7 @@ The system SHALL render a services listing (WEB-02) and a portfolio listing plus
|
|||||||
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
- **THEN** `/portfolio` MUST paginate instead of rendering all records
|
||||||
|
|
||||||
### Requirement: Institutional and error pages have brand identity
|
### Requirement: Institutional and error pages have brand identity
|
||||||
|
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. Contact, privacy and error surfaces MUST remain sober tonal layouts without decorative photography. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
||||||
The system SHALL provide the Sobre and Política de privacidade pages and branded error pages (WEB-07) using the Heritage Editorial public layout, including the brand mark when available. The 404 page MUST use the public layout, and the 500 page MUST NOT expose stack traces or internal details when `APP_DEBUG` is false.
|
|
||||||
|
|
||||||
#### Scenario: Unknown URL renders branded 404
|
#### Scenario: Unknown URL renders branded 404
|
||||||
|
|
||||||
@@ -146,3 +145,16 @@ When public pages render illustrative/demo photography that is not an authorized
|
|||||||
|
|
||||||
- **WHEN** the home or portfolio renders placeholder photography
|
- **WHEN** the home or portfolio renders placeholder photography
|
||||||
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
- **THEN** a visible note or equivalent disclosure MUST indicate the imagery is illustrative pending authorized assets
|
||||||
|
|
||||||
|
### Requirement: Visual public and error pages expose the shared motion contract
|
||||||
|
|
||||||
|
The system SHALL apply the shared progressive opening and reveal contract to home, services, portfolio index/detail, about, contact, privacy, and branded 404/419/429/500/503 pages. The contract MUST NOT change page copy, content ordering, route behavior, or structural layout.
|
||||||
|
|
||||||
|
#### Scenario: Institutional page uses shared opening
|
||||||
|
- **WHEN** a visitor loads `/sobre`, `/contato`, or `/privacidade`
|
||||||
|
- **THEN** the page MUST expose the shared page opening hook and section reveal hooks
|
||||||
|
|
||||||
|
#### Scenario: Error page remains branded and progressively enhanced
|
||||||
|
- **WHEN** Laravel renders a branded 404, 419, 429, 500, or 503 response
|
||||||
|
- **THEN** the page MUST expose the shared opening hook
|
||||||
|
- **AND** all error guidance and navigation MUST remain immediately usable without enhancement
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# quality-gates Specification
|
# quality-gates Specification
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Define Composer quality scripts, architecture boundaries, PostgreSQL-backed feature tests, and the five blocking CI jobs including browser visual and accessibility coverage.
|
Define Composer quality scripts, architecture boundaries, PostgreSQL-backed feature tests, and the five blocking CI jobs including browser functional, accessibility and motion coverage.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Standardized Composer test scripts exist
|
### Requirement: Standardized Composer test scripts exist
|
||||||
@@ -53,20 +53,57 @@ The system SHALL configure `phpunit.xml` so that the Feature test suite uses Pos
|
|||||||
|
|
||||||
### Requirement: Browser tests run against FrankenPHP-served application
|
### Requirement: Browser tests run against FrankenPHP-served application
|
||||||
|
|
||||||
The system SHALL execute browser tests using Pest Browser/Playwright against an application served by FrankenPHP in CI. The `browser` job MUST cover the E2E journeys available in the current phase, the visual regression assertions and the automated accessibility checks for public routes, and MUST run in assertion mode without regenerating baselines (SPEC §13.4, §13.5, §13.8, §14.1).
|
The system SHALL execute browser tests using Pest Browser/Playwright against an application served by FrankenPHP in CI. The `browser` job MUST cover the E2E journeys available in the current phase, automated accessibility checks, motion behavior and smoke checks for public routes (SPEC §13.4, §13.7 and §14.1), without pixel comparison.
|
||||||
|
|
||||||
#### Scenario: Browser job validates served application
|
#### Scenario: Browser job validates served application
|
||||||
|
|
||||||
- **WHEN** the `browser` CI job runs
|
- **WHEN** the `browser` CI job runs
|
||||||
- **THEN** tests execute against the built application artifact or equivalent production-like image
|
- **THEN** tests execute against the built application artifact or equivalent production-like image
|
||||||
|
|
||||||
#### Scenario: Browser job covers visual and accessibility assertions
|
#### Scenario: Browser job covers functional, accessibility and motion assertions
|
||||||
|
|
||||||
- **WHEN** the `browser` CI job runs
|
- **WHEN** the `browser` CI job runs
|
||||||
- **THEN** it MUST execute the visual regression suite and the accessibility suite
|
- **THEN** it MUST execute the functional browser, accessibility, motion and smoke suites
|
||||||
- **AND** a failing snapshot or a critical/serious accessibility issue MUST block merge
|
- **AND** a failing functional assertion or a critical/serious accessibility issue MUST block merge
|
||||||
|
|
||||||
#### Scenario: Browser failures publish diagnostics
|
#### Scenario: Browser failures publish diagnostics
|
||||||
|
|
||||||
- **WHEN** a browser test fails in CI
|
- **WHEN** a browser test fails in CI
|
||||||
- **THEN** the job MUST publish screenshots, snapshot diffs, application logs and browser logs as artifacts
|
- **THEN** the job MUST publish application logs and browser logs as diagnostics
|
||||||
|
|
||||||
|
### Requirement: Quality gate includes npm dependency audit
|
||||||
|
|
||||||
|
The system SHALL run an npm audit as part of the quality/static gate according to the project policy (SPEC §12.6, §13.9). Audit failure MUST block merge unless an explicit, documented exception exists.
|
||||||
|
|
||||||
|
#### Scenario: Vulnerable dependency fails static gate
|
||||||
|
|
||||||
|
- **WHEN** `npm audit` reports a policy-violating vulnerability in production dependencies
|
||||||
|
- **THEN** the static/quality gate MUST fail and block merge
|
||||||
|
|
||||||
|
#### Scenario: Developer runs quality locally with audit
|
||||||
|
|
||||||
|
- **WHEN** a developer runs `composer quality` (or the documented npm audit step it requires)
|
||||||
|
- **THEN** the npm audit MUST execute as part of the gate sequence
|
||||||
|
|
||||||
|
### Requirement: Domain and Application maintain minimum coverage
|
||||||
|
|
||||||
|
The CI unit gate SHALL measure code coverage for `App\Domain` and `App\Application` and MUST fail when either namespace falls below 80% (SPEC §13.7). Views, migrations, generated code, and framework code MUST NOT be required to meet this threshold.
|
||||||
|
|
||||||
|
#### Scenario: Coverage drop below threshold blocks merge
|
||||||
|
|
||||||
|
- **WHEN** Domain or Application coverage is below 80% on the unit CI job
|
||||||
|
- **THEN** the job MUST fail
|
||||||
|
|
||||||
|
#### Scenario: Coverage ignores non-domain layers
|
||||||
|
|
||||||
|
- **WHEN** coverage is computed for the gate
|
||||||
|
- **THEN** Blade views and migrations MUST NOT be counted toward the Domain/Application threshold
|
||||||
|
|
||||||
|
### Requirement: Staging deploy job runs only after blocking CI jobs
|
||||||
|
|
||||||
|
When deploying from `main`, the staging deploy workflow MUST require the five blocking CI jobs (`static`, `unit`, `feature`, `browser`, `container`) to succeed before building/pushing the image and triggering Dokploy (SPEC §14.1, §14.3).
|
||||||
|
|
||||||
|
#### Scenario: Failed browser job prevents staging deploy
|
||||||
|
|
||||||
|
- **WHEN** the `browser` CI job fails on `main`
|
||||||
|
- **THEN** the staging deploy workflow MUST NOT promote a new image
|
||||||
|
|||||||
83
openspec/specs/staging-deployment/spec.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# staging-deployment Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Define immutable staging and production promotion through GHCR and Dokploy Compose, including migration, health, smoke, rollback, and backup evidence.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Staging deploys an immutable application image by commit SHA
|
||||||
|
|
||||||
|
The system SHALL deploy staging from a single FrankenPHP application image tagged with the Git commit SHA and published to GHCR (SPEC §14.3, §15.2). Web, queue worker, and scheduler processes MUST use that same image digest. Rebuilds per process on the staging host MUST NOT be the promotion path.
|
||||||
|
|
||||||
|
#### Scenario: Same image serves all application processes
|
||||||
|
|
||||||
|
- **WHEN** a staging deployment is promoted for commit SHA `abc123`
|
||||||
|
- **THEN** web, queue, and scheduler MUST run from `ghcr.io/<owner>/<repo>:abc123` (or equivalent digest)
|
||||||
|
- **AND** MUST NOT rebuild distinct images per process
|
||||||
|
|
||||||
|
#### Scenario: CI publishes the image before Dokploy deploy
|
||||||
|
|
||||||
|
- **WHEN** `main` passes the blocking CI jobs
|
||||||
|
- **THEN** the deploy workflow MUST build and push the SHA-tagged image before triggering Dokploy
|
||||||
|
|
||||||
|
### Requirement: Staging runs on Dokploy Compose on the operator VPS
|
||||||
|
|
||||||
|
Staging SHALL be operated as a Dokploy Docker Compose deployment on the operator-managed VPS. PostgreSQL MUST be a managed private database service; application media MUST use the configured production object storage disk (`r2`) and mail MUST use the configured Resend transport when enabled.
|
||||||
|
|
||||||
|
#### Scenario: Compose defines required processes
|
||||||
|
|
||||||
|
- **WHEN** staging Compose is deployed
|
||||||
|
- **THEN** it MUST include web, queue worker, scheduler, and a one-shot migrate job using the application image
|
||||||
|
|
||||||
|
#### Scenario: Database is not embedded in the application image
|
||||||
|
|
||||||
|
- **WHEN** staging boots
|
||||||
|
- **THEN** PostgreSQL MUST be provided by the Dokploy/database service
|
||||||
|
- **AND** MUST NOT live inside the application image layers
|
||||||
|
|
||||||
|
### Requirement: Staging deploy migrates then proves health with smoke checks
|
||||||
|
|
||||||
|
A staging promotion MUST run database migrations against the staging database, wait for `/up` to report healthy, and execute smoke HTTP checks for `/up`, the public home, and `/admin/login` (SPEC §14.3, §15.5, §16.2).
|
||||||
|
|
||||||
|
#### Scenario: Unhealthy deploy is not considered successful
|
||||||
|
|
||||||
|
- **WHEN** `/up` fails after a staging deploy attempt
|
||||||
|
- **THEN** the deployment MUST be treated as failed
|
||||||
|
- **AND** MUST NOT be recorded as a successful Phase 0 exit criterion
|
||||||
|
|
||||||
|
#### Scenario: Smoke checks cover public and login surfaces
|
||||||
|
|
||||||
|
- **WHEN** staging deploy health succeeds
|
||||||
|
- **THEN** smoke checks MUST request `/`, `/up`, and `/admin/login`
|
||||||
|
- **AND** each MUST return HTTP 200
|
||||||
|
|
||||||
|
### Requirement: Staging rollback uses the previous image SHA
|
||||||
|
|
||||||
|
Rollback SHALL redeploy a previously published SHA-tagged image without rebuilding from source (SPEC §14.3).
|
||||||
|
|
||||||
|
#### Scenario: Operator rolls back to previous SHA
|
||||||
|
|
||||||
|
- **WHEN** the operator points staging Compose at a previous SHA tag and redeploys
|
||||||
|
- **THEN** web, queue, and scheduler MUST run that previous image
|
||||||
|
- **AND** no source rebuild MUST be required
|
||||||
|
|
||||||
|
### Requirement: Production promotion reuses the same immutable digest
|
||||||
|
|
||||||
|
Production SHALL be promoted from an already-published SHA-tagged image without rebuilding from source. Promotion MUST require explicit human action (SPEC §14.3).
|
||||||
|
|
||||||
|
#### Scenario: Operator promotes a staging-approved SHA to production
|
||||||
|
|
||||||
|
- **WHEN** the operator confirms promotion of commit SHA `abc123`
|
||||||
|
- **THEN** production web, queue, and scheduler MUST run the same digest previously published as `ghcr.io/<owner>/<repo>:abc123`
|
||||||
|
- **AND** MUST NOT rebuild from source for that promotion
|
||||||
|
|
||||||
|
### Requirement: Database backups exist before production cutover
|
||||||
|
|
||||||
|
Staging and production PostgreSQL services SHALL have automated daily backups with retention of at least 14 days, and a documented restore procedure MUST be verified on staging before the first production promotion (SPEC §16.3).
|
||||||
|
|
||||||
|
#### Scenario: Staging restore is proven before production promotion
|
||||||
|
|
||||||
|
- **WHEN** the operator prepares the first production promotion
|
||||||
|
- **THEN** a restore from a staging backup MUST have been documented and successfully tested
|
||||||
|
- **AND** production MUST have daily backup configured with retention of at least 14 days
|
||||||
@@ -6,7 +6,7 @@ Define testimonial management, publication, authorization, and featured filterin
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Testimonials are managed with publication control
|
### Requirement: Testimonials are managed with publication control
|
||||||
|
|
||||||
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote text (including multi-paragraph content), author name, optional context (event type and/or date), optional photo with alt text, sort order, featured flag, and `published_at`. Public rendering MUST preserve paragraph breaks from the stored quote, MUST omit blank/whitespace-only quotes before creating the rendered sequence, and MUST assign alternating `left`/`right` reveal directions according to the remaining rendered column order at desktop and mobile viewports. Testimonials sourced from real clients MUST NOT be published to production without authorization; development seeds MAY include the authorized-pending real quotes marked for review.
|
||||||
|
|
||||||
#### Scenario: Unpublished testimonial is excluded
|
#### Scenario: Unpublished testimonial is excluded
|
||||||
|
|
||||||
@@ -34,6 +34,18 @@ The system SHALL allow admins to manage testimonials (SPEC WEB-04) with quote te
|
|||||||
- **WHEN** the home testimonials section is rendered
|
- **WHEN** the home testimonials section is rendered
|
||||||
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
- **THEN** each paragraph MUST appear as distinct block text rather than a single collapsed line
|
||||||
|
|
||||||
|
#### Scenario: Blank quotes do not affect alternation
|
||||||
|
|
||||||
|
- **GIVEN** the published testimonial collection contains a blank quote between two non-blank quotes
|
||||||
|
- **WHEN** the home testimonials section is rendered
|
||||||
|
- **THEN** the blank testimonial MUST be omitted
|
||||||
|
- **AND** the two rendered testimonials MUST receive alternating `left` and `right` directions based on their rendered order
|
||||||
|
|
||||||
|
#### Scenario: Direction alternation remains on mobile
|
||||||
|
|
||||||
|
- **WHEN** testimonials are viewed at a mobile viewport
|
||||||
|
- **THEN** their explicit `left` and `right` directions MUST be preserved with the reduced mobile distance
|
||||||
|
|
||||||
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
### Requirement: Real wedding testimonials are seeded from authorized source copy
|
||||||
|
|
||||||
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
Content and visual seeders SHALL replace fictional testimonials with the five real wedding testimonials from `depoimentos.md`, preserving author couple names, quote wording, and date/context. Until final publication authorization is confirmed, production deployments MUST keep those records unpublished or gated by explicit admin publish action.
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
# visual-regression Specification
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
Define deterministic visual baselines for public screens and the explicit baseline update workflow.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
### Requirement: Public screens have desktop and mobile visual baselines
|
|
||||||
|
|
||||||
The system SHALL keep versioned screenshot baselines for the public screens available in this phase (SPEC §13.5): Home, Serviços, Portfólio and Detalhe do portfólio, at 1440×1000 desktop and 390×844 mobile, under the Heritage Editorial identity. A rendering change that alters those screens MUST fail the browser suite until the diff is reviewed and baselines are explicitly updated.
|
|
||||||
|
|
||||||
#### Scenario: Unintended visual change fails the suite
|
|
||||||
|
|
||||||
- **GIVEN** approved baselines exist
|
|
||||||
- **WHEN** a code change alters the rendering of a covered screen
|
|
||||||
- **THEN** the visual assertion MUST fail and report the diff
|
|
||||||
|
|
||||||
#### Scenario: Both viewports are covered
|
|
||||||
|
|
||||||
- **WHEN** the visual suite runs
|
|
||||||
- **THEN** each covered screen MUST be asserted at 1440×1000 and 390×844
|
|
||||||
|
|
||||||
#### Scenario: Heritage Editorial identity is captured
|
|
||||||
|
|
||||||
- **WHEN** approved baselines for the home are reviewed after this change
|
|
||||||
- **THEN** they MUST reflect EB Garamond typography, olive/paper palette and sharp-edged editorial layout rather than the previous gold/rounded placeholder look
|
|
||||||
|
|
||||||
### Requirement: Visual runs are deterministic
|
|
||||||
|
|
||||||
Visual runs SHALL be deterministic per SPEC §13.5: fixed Chromium and Linux image, fixed viewport, timezone `America/Sao_Paulo`, locale `pt-BR`, self-hosted fonts installed/bundled for the suite, frozen clock, deterministic seed (including real testimonial subset and São Paulo settings), animations and transitions disabled, and no dependency on external network.
|
|
||||||
|
|
||||||
#### Scenario: Repeated run without code change produces no diff
|
|
||||||
|
|
||||||
- **WHEN** the visual suite runs twice against the same commit and seed
|
|
||||||
- **THEN** both runs MUST pass with no pixel diff
|
|
||||||
|
|
||||||
#### Scenario: Time-dependent content does not cause drift
|
|
||||||
|
|
||||||
- **GIVEN** the clock is frozen and the seed is deterministic
|
|
||||||
- **WHEN** the suite runs on a different calendar day
|
|
||||||
- **THEN** rendered dates MUST remain identical to the baseline
|
|
||||||
|
|
||||||
#### Scenario: Motion is disabled during capture
|
|
||||||
|
|
||||||
- **WHEN** a screenshot is captured
|
|
||||||
- **THEN** CSS animations and transitions MUST be disabled
|
|
||||||
|
|
||||||
### Requirement: Baseline updates are explicit and reviewed
|
|
||||||
|
|
||||||
Baselines SHALL only be updated through the explicit `composer visual:update` command, and the resulting diff MUST be reviewed by a human before merge. Baselines MUST NOT be regenerated automatically to make CI pass.
|
|
||||||
|
|
||||||
#### Scenario: CI does not regenerate baselines
|
|
||||||
|
|
||||||
- **WHEN** the `browser` CI job runs
|
|
||||||
- **THEN** it MUST run in assertion mode
|
|
||||||
- **AND** MUST NOT write new baselines
|
|
||||||
|
|
||||||
#### Scenario: Developer updates baselines intentionally
|
|
||||||
|
|
||||||
- **WHEN** a developer runs `composer visual:update`
|
|
||||||
- **THEN** the updated baseline files MUST be written to the versioned baseline directory for review
|
|
||||||
@@ -6,7 +6,7 @@ Define automated accessibility checks, semantic structure, keyboard operability,
|
|||||||
## Requirements
|
## Requirements
|
||||||
### Requirement: Public routes have no critical or serious accessibility issues
|
### Requirement: Public routes have no critical or serious accessibility issues
|
||||||
|
|
||||||
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial redesign. A critical or serious issue MUST fail the suite.
|
The system SHALL run automated accessibility checks on the public routes covered by the browser suite (SPEC §6.5, §13.8) after the Heritage Editorial motion enhancement. A critical or serious issue MUST fail the suite.
|
||||||
|
|
||||||
#### Scenario: Critical issue blocks the suite
|
#### Scenario: Critical issue blocks the suite
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ The system SHALL run automated accessibility checks on the public routes covered
|
|||||||
#### Scenario: Covered routes are checked
|
#### Scenario: Covered routes are checked
|
||||||
|
|
||||||
- **WHEN** the accessibility suite runs
|
- **WHEN** the accessibility suite runs
|
||||||
- **THEN** the home, services listing, portfolio listing and case detail MUST each be checked
|
- **THEN** home, services listing, portfolio listing, case detail, about, contact, privacy and branded 404 MUST each be checked
|
||||||
|
|
||||||
### Requirement: Public pages use accessible semantic structure
|
### Requirement: Public pages use accessible semantic structure
|
||||||
|
|
||||||
@@ -65,19 +65,34 @@ Visitors SHALL be able to reach and activate every interactive element with the
|
|||||||
|
|
||||||
### Requirement: Reduced motion preference is honored
|
### Requirement: Reduced motion preference is honored
|
||||||
|
|
||||||
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including editorial hover scales and menu transitions introduced by the redesign.
|
The system SHALL suppress non-essential animation and transition when the user agent reports `prefers-reduced-motion: reduce`, including page openings, editorial reveals, image hover scales, interaction feedback and menu transitions. All affected content MUST render in its final visible and interactive state.
|
||||||
|
|
||||||
#### Scenario: Reduced motion disables transitions
|
#### Scenario: Reduced motion disables transitions
|
||||||
|
|
||||||
- **GIVEN** the browser reports `prefers-reduced-motion: reduce`
|
- **GIVEN** the browser reports `prefers-reduced-motion: reduce`
|
||||||
- **WHEN** a public page is loaded
|
- **WHEN** a public page is loaded
|
||||||
- **THEN** decorative transitions and animations MUST NOT run
|
- **THEN** decorative transitions and animations MUST NOT run
|
||||||
|
- **AND** every motion target MUST be in its final visible and interactive state
|
||||||
|
|
||||||
### Requirement: Public pages emit no console errors
|
### Requirement: Public pages emit no console errors
|
||||||
|
|
||||||
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including pages that load the mobile navigation script.
|
Covered public routes SHALL load without JavaScript console errors in a real browser (SPEC §13.8, §19), including home, services listing, portfolio listing, case detail, about, contact, privacy, branded 404, and pages that load the mobile navigation or motion runtime.
|
||||||
|
|
||||||
#### Scenario: Console stays clean on covered routes
|
#### Scenario: Console stays clean on covered routes
|
||||||
|
|
||||||
- **WHEN** a covered public route is loaded in the browser suite
|
- **WHEN** a covered public route is loaded in the browser suite
|
||||||
- **THEN** the console MUST contain no error-level messages
|
- **THEN** the console MUST contain no error-level messages
|
||||||
|
|
||||||
|
### Requirement: Motion enhancement failure does not hide or disable content
|
||||||
|
|
||||||
|
The public site SHALL remain readable, keyboard-operable, and interactive when JavaScript is disabled or `IntersectionObserver` is unavailable. Motion targets MUST default to their final state without relying on timeout recovery.
|
||||||
|
|
||||||
|
#### Scenario: JavaScript disabled keeps public page usable
|
||||||
|
|
||||||
|
- **WHEN** a visitor loads a public page with JavaScript disabled
|
||||||
|
- **THEN** headings, copy, navigation, CTAs, and form controls MUST be visible and operable
|
||||||
|
|
||||||
|
#### Scenario: Observer unavailable keeps reveals final
|
||||||
|
|
||||||
|
- **WHEN** the motion runtime executes without `IntersectionObserver`
|
||||||
|
- **THEN** reveal targets MUST remain in the final state
|
||||||
|
|||||||
13
package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "site-amare",
|
"name": "daisyui",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"daisyui": "^5.7.16",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
@@ -790,6 +791,16 @@
|
|||||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/daisyui": {
|
||||||
|
"version": "5.7.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.7.16.tgz",
|
||||||
|
"integrity": "sha512-V9CJxvrWIXTDuP/0tpijjWaKaKJNYo2IrULJfTWs3/DjW9rPqswk1n8NCLQRLrTutmlG2Ech7nkJDpCnF+6Dzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
|
"daisyui": "^5.7.16",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"laravel-vite-plugin": "^3.1",
|
"laravel-vite-plugin": "^3.1",
|
||||||
"playwright": "^1.62.0",
|
"playwright": "^1.62.0",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -1,6 +1,46 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import './tokens.css';
|
@import './tokens.css';
|
||||||
|
|
||||||
|
@plugin "daisyui" {
|
||||||
|
themes: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "amare";
|
||||||
|
default: true;
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--color-base-100: var(--amare-color-bg);
|
||||||
|
--color-base-200: var(--amare-color-bg-deep);
|
||||||
|
--color-base-300: var(--amare-color-bg-archive);
|
||||||
|
--color-base-content: var(--amare-color-text);
|
||||||
|
--color-primary: var(--amare-color-accent);
|
||||||
|
--color-primary-content: var(--amare-color-accent-text);
|
||||||
|
--color-secondary: var(--amare-color-sage);
|
||||||
|
--color-secondary-content: var(--amare-color-text);
|
||||||
|
--color-accent: var(--amare-color-accent-deep);
|
||||||
|
--color-accent-content: var(--amare-color-accent-text);
|
||||||
|
--color-neutral: var(--amare-color-muted);
|
||||||
|
--color-neutral-content: var(--amare-color-accent-text);
|
||||||
|
--color-info: #2563eb;
|
||||||
|
--color-info-content: #ffffff;
|
||||||
|
--color-success: var(--amare-color-success);
|
||||||
|
--color-success-content: var(--amare-color-accent-text);
|
||||||
|
--color-warning: var(--amare-color-warning);
|
||||||
|
--color-warning-content: var(--amare-color-accent-text);
|
||||||
|
--color-error: var(--amare-color-error);
|
||||||
|
--color-error-content: var(--amare-color-accent-text);
|
||||||
|
|
||||||
|
--radius-selector: 0;
|
||||||
|
--radius-field: 0;
|
||||||
|
--radius-box: 0;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||||
@source '../../storage/framework/views/*.php';
|
@source '../../storage/framework/views/*.php';
|
||||||
@source '../views/**/*.blade.php';
|
@source '../views/**/*.blade.php';
|
||||||
@@ -130,43 +170,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-chapters {
|
|
||||||
counter-reset: home-chapter -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-chapter {
|
|
||||||
counter-increment: home-chapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-folio {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--amare-color-accent);
|
|
||||||
font-size: var(--amare-text-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.14em;
|
|
||||||
line-height: 1;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-folio--inverse {
|
|
||||||
color: color-mix(in srgb, var(--amare-color-accent-text) 82%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-folio__separator {
|
|
||||||
color: var(--amare-color-sage);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-folio--inverse .home-folio__separator {
|
|
||||||
color: color-mix(in srgb, var(--amare-color-accent-text) 55%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-folio__number::before {
|
|
||||||
content: counter(home-chapter, decimal-leading-zero);
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
.main-nav {
|
.main-nav {
|
||||||
transition: opacity var(--amare-duration-normal) var(--amare-ease-standard);
|
transition: opacity var(--amare-duration-normal) var(--amare-ease-standard);
|
||||||
@@ -181,85 +184,6 @@
|
|||||||
overflow-x: clip;
|
overflow-x: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dossiê vivo — content visible by default; enhance only when opted in */
|
|
||||||
[data-chapter-index] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-progress] {
|
|
||||||
display: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-index] a[aria-current="true"] {
|
|
||||||
color: var(--amare-color-accent-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-index] a[aria-current="true"]::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0.35em;
|
|
||||||
bottom: 0.35em;
|
|
||||||
width: 1px;
|
|
||||||
background: var(--amare-color-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-progress] > span {
|
|
||||||
display: block;
|
|
||||||
height: 100%;
|
|
||||||
width: var(--chapter-progress, 0%);
|
|
||||||
background: var(--amare-color-accent);
|
|
||||||
transform-origin: left center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
[data-chapter-index] {
|
|
||||||
display: flex;
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
right: max(1rem, calc((100vw - var(--amare-container-max)) / 2 - 7.5rem));
|
|
||||||
z-index: 30;
|
|
||||||
max-width: 6.5rem;
|
|
||||||
translate: 0 -50%;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-index] a {
|
|
||||||
position: relative;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 2.75rem;
|
|
||||||
padding-left: 0.75rem;
|
|
||||||
font-size: var(--amare-text-xs);
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: var(--amare-color-muted);
|
|
||||||
text-decoration: none;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
transition: color var(--amare-duration-fast) var(--amare-ease-standard);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-chapter-index] a:hover {
|
|
||||||
color: var(--amare-color-accent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1279px) {
|
|
||||||
[data-chapter-progress] {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 45;
|
|
||||||
width: 100%;
|
|
||||||
height: 2px;
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
:where(a, button, input, textarea, select) {
|
:where(a, button, input, textarea, select) {
|
||||||
transition-duration: var(--amare-motion-feedback);
|
transition-duration: var(--amare-motion-feedback);
|
||||||
@@ -339,3 +263,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* daisyUI emits unlayered; match focus ring to the amare accent. */
|
||||||
|
.input:focus,
|
||||||
|
.select:focus,
|
||||||
|
.textarea:focus,
|
||||||
|
.checkbox:focus-visible {
|
||||||
|
--input-color: var(--amare-color-accent);
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,86 +84,11 @@ function observeReveals(enhance) {
|
|||||||
nodes.forEach((node) => observer.observe(node));
|
nodes.forEach((node) => observer.observe(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupChapterIndex() {
|
|
||||||
const index = document.querySelector('[data-chapter-index]');
|
|
||||||
const progress = document.querySelector('[data-chapter-progress] span');
|
|
||||||
const chapters = Array.from(document.querySelectorAll('[data-chapter]'));
|
|
||||||
|
|
||||||
if (!index || chapters.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = Array.from(index.querySelectorAll('a[href^="#"]'));
|
|
||||||
let framePending = false;
|
|
||||||
|
|
||||||
const setActive = (id) => {
|
|
||||||
links.forEach((link) => {
|
|
||||||
const isCurrent = link.getAttribute('href') === `#${id}`;
|
|
||||||
if (isCurrent) {
|
|
||||||
link.setAttribute('aria-current', 'true');
|
|
||||||
} else {
|
|
||||||
link.removeAttribute('aria-current');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateProgress = (ratio) => {
|
|
||||||
if (!progress) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clamped = Math.min(1, Math.max(0, ratio));
|
|
||||||
progress.style.setProperty('--chapter-progress', `${(clamped * 100).toFixed(2)}%`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sync = () => {
|
|
||||||
const marker = window.scrollY + Math.min(window.innerHeight * 0.35, 280);
|
|
||||||
let current = chapters[0];
|
|
||||||
|
|
||||||
chapters.forEach((chapter) => {
|
|
||||||
if (chapter.offsetTop <= marker) {
|
|
||||||
current = chapter;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const heading = current.querySelector('[id$="-heading"]') || document.getElementById(`${current.dataset.chapter}-heading`);
|
|
||||||
const headingId = heading?.id
|
|
||||||
|| current.getAttribute('aria-labelledby')
|
|
||||||
|| `${current.dataset.chapter}-heading`;
|
|
||||||
|
|
||||||
setActive(headingId);
|
|
||||||
|
|
||||||
const doc = document.documentElement;
|
|
||||||
const max = Math.max(1, doc.scrollHeight - window.innerHeight);
|
|
||||||
updateProgress(window.scrollY / max);
|
|
||||||
};
|
|
||||||
|
|
||||||
const scheduleSync = () => {
|
|
||||||
if (framePending) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
framePending = true;
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
framePending = false;
|
|
||||||
sync();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
setActive(chapters[0].getAttribute('aria-labelledby') || 'hero-heading');
|
|
||||||
sync();
|
|
||||||
|
|
||||||
window.addEventListener('scroll', scheduleSync, { passive: true });
|
|
||||||
window.addEventListener('resize', scheduleSync);
|
|
||||||
}
|
|
||||||
|
|
||||||
function bootMotion() {
|
function bootMotion() {
|
||||||
initializeMotionDelays();
|
initializeMotionDelays();
|
||||||
const enhance = enableEnhancement();
|
const enhance = enableEnhancement();
|
||||||
activatePageOpen(enhance);
|
activatePageOpen(enhance);
|
||||||
observeReveals(enhance);
|
observeReveals(enhance);
|
||||||
setupChapterIndex();
|
|
||||||
|
|
||||||
window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => {
|
window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => {
|
||||||
if (event.matches) {
|
if (event.matches) {
|
||||||
document.documentElement.removeAttribute('data-motion');
|
document.documentElement.removeAttribute('data-motion');
|
||||||
|
|||||||
@@ -31,9 +31,14 @@
|
|||||||
// dimensions, so it gets no attributes rather than wrong ones. The CSS
|
// dimensions, so it gets no attributes rather than wrong ones. The CSS
|
||||||
// classes still govern the rendered size in both cases — width/height only
|
// classes still govern the rendered size in both cases — width/height only
|
||||||
// give the browser the aspect ratio to reserve.
|
// give the browser the aspect ratio to reserve.
|
||||||
|
//
|
||||||
|
// The webp assets are encoded at 3x the largest rendered size (lockup at
|
||||||
|
// h-12 = 48 px, mark at h-8 = 32 px), which is why these are 149x144 and
|
||||||
|
// 191x96 rather than the 512-wide originals kept as png fallbacks. See
|
||||||
|
// tests/Feature/PublicSite/BrandAssetBudgetTest.php.
|
||||||
$intrinsic = $usesUploadedLogo
|
$intrinsic = $usesUploadedLogo
|
||||||
? []
|
? []
|
||||||
: ($mark ? ['width' => 512, 'height' => 257] : ['width' => 512, 'height' => 495]);
|
: ($mark ? ['width' => 191, 'height' => 96] : ['width' => 149, 'height' => 144]);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
@props([
|
|
||||||
'chapters' => [],
|
|
||||||
])
|
|
||||||
|
|
||||||
@if (count($chapters) > 0)
|
|
||||||
<nav data-chapter-index aria-label="Índice de capítulos">
|
|
||||||
@foreach ($chapters as $chapter)
|
|
||||||
<a href="#{{ $chapter['id'] }}">{{ $chapter['label'] }}</a>
|
|
||||||
@endforeach
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div data-chapter-progress aria-hidden="true">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@@ -7,20 +7,18 @@
|
|||||||
'border-t border-amare-border bg-amare-bg-deep py-20',
|
'border-t border-amare-border bg-amare-bg-deep py-20',
|
||||||
'home-chapter' => $editorial,
|
'home-chapter' => $editorial,
|
||||||
]) data-chapter="final-cta" data-reveal-group>
|
]) data-chapter="final-cta" data-reveal-group>
|
||||||
<div class="container-amare space-y-6 text-center" data-reveal data-reveal-from="up">
|
<div class="container-amare max-w-4xl space-y-6 py-4" data-reveal data-reveal-from="up">
|
||||||
@if ($editorial)
|
@unless ($editorial)
|
||||||
<x-home.folio label="Próximo passo" class="justify-center" />
|
|
||||||
@else
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||||
@endif
|
@endunless
|
||||||
<h2 id="final-cta-heading" class="text-headline font-medium text-amare-text">Do casamento ao evento corporativo, tudo começa com uma boa conversa.</h2>
|
<h2 id="final-cta-heading" class="text-headline font-medium text-amare-text">Do casamento ao evento corporativo, tudo começa com uma boa conversa.</h2>
|
||||||
<p class="mx-auto max-w-2xl text-amare-muted">
|
<p class="max-w-2xl text-amare-muted">
|
||||||
Compartilhe as primeiras informações do seu evento. A Amare retorna para entender o contexto e orientar os próximos passos.
|
Compartilhe as primeiras informações do seu evento. A Amare retorna para entender o contexto e orientar os próximos passos.
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<a
|
<a
|
||||||
href="{{ route('contact') }}"
|
href="{{ route('contact') }}"
|
||||||
class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-deep"
|
class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"
|
||||||
>
|
>
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
@props([
|
|
||||||
'label',
|
|
||||||
'tone' => 'default',
|
|
||||||
])
|
|
||||||
|
|
||||||
@php
|
|
||||||
$tone = $tone === 'inverse' ? 'inverse' : 'default';
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<p
|
|
||||||
{{ $attributes->class([
|
|
||||||
'home-folio',
|
|
||||||
'home-folio--inverse' => $tone === 'inverse',
|
|
||||||
]) }}
|
|
||||||
data-home-folio
|
|
||||||
>
|
|
||||||
<span data-home-folio-label>{{ $label }}</span>
|
|
||||||
<span class="home-folio__separator" aria-hidden="true">·</span>
|
|
||||||
<span class="home-folio__number" data-home-folio-number aria-hidden="true"></span>
|
|
||||||
</p>
|
|
||||||
@@ -4,16 +4,21 @@
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
aria-labelledby="hero-heading"
|
aria-labelledby="hero-heading"
|
||||||
class="home-chapter border-b border-amare-border bg-amare-bg"
|
@class([
|
||||||
|
'home-chapter border-b border-amare-border bg-amare-bg',
|
||||||
|
'min-h-[100dvh]' => blank($settings->hero_image_path),
|
||||||
|
'lg:h-[calc(100dvh-5rem)]' => filled($settings->hero_image_path),
|
||||||
|
])
|
||||||
data-chapter="hero"
|
data-chapter="hero"
|
||||||
data-motion="page-open"
|
data-motion="page-open"
|
||||||
>
|
>
|
||||||
<div class="container-amare pt-8 md:pt-10">
|
@if (filled($settings->hero_image_path))
|
||||||
<x-home.folio label="Capa" data-motion-beat="folio" />
|
<div class="grid lg:h-full lg:grid-cols-2">
|
||||||
</div>
|
<div
|
||||||
|
data-hero-content
|
||||||
<div class="container-amare grid gap-12 pb-20 pt-10 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:pb-28 md:pt-14" data-reveal-group>
|
data-reveal-group
|
||||||
<div class="space-y-8">
|
class="flex flex-col justify-center space-y-8 px-6 py-20 lg:py-12 lg:pr-[clamp(3rem,6vw,7rem)] lg:pl-[max(var(--amare-container-padding),calc((100vw-var(--amare-container-max))/2+var(--amare-container-padding)))]"
|
||||||
|
>
|
||||||
<div data-motion-beat="seal" class="flex items-center gap-4">
|
<div data-motion-beat="seal" class="flex items-center gap-4">
|
||||||
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
||||||
@if (filled($settings->hero_eyebrow))
|
@if (filled($settings->hero_eyebrow))
|
||||||
@@ -30,19 +35,12 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
|
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
|
||||||
<a
|
<a href="{{ route('contact') }}" data-testid="home-primary-cta" class="btn btn-primary text-sm font-semibold">
|
||||||
href="{{ route('contact') }}"
|
|
||||||
data-testid="home-primary-cta"
|
|
||||||
class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold text-amare-accent-text transition-colors duration-(--amare-duration-normal) ease-(--amare-ease-standard) hover:bg-amare-accent-hover"
|
|
||||||
>
|
|
||||||
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@if (filled($settings->hero_secondary_cta_label))
|
@if (filled($settings->hero_secondary_cta_label))
|
||||||
<a
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
href="{{ route('portfolio.index') }}"
|
|
||||||
class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep"
|
|
||||||
>
|
|
||||||
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
|
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
|
||||||
</a>
|
</a>
|
||||||
@endif
|
@endif
|
||||||
@@ -53,17 +51,44 @@
|
|||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filled($settings->default_og_image_path))
|
<div data-split-hero data-motion-beat="media" class="max-lg:aspect-[4/5] overflow-hidden bg-amare-bg-deep lg:h-full" data-reveal-media>
|
||||||
<div data-motion-beat="media" class="min-h-72 overflow-hidden bg-amare-bg-deep">
|
<x-media.image :path="$settings->hero_image_path" :alt="$settings->hero_image_alt ?: $settings->brand_name" loading="eager" fetchpriority="high" sizes="(max-width: 1023px) 100vw, 50vw" class="img-editorial h-full w-full object-cover" />
|
||||||
<x-media.image
|
|
||||||
:path="$settings->default_og_image_path"
|
|
||||||
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
|
|
||||||
loading="eager"
|
|
||||||
fetchpriority="high"
|
|
||||||
sizes="(max-width: 768px) 100vw, 40vw"
|
|
||||||
class="img-editorial h-full w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="container-amare grid min-h-[100dvh] gap-12 py-20 md:max-w-4xl md:items-center md:py-10" data-reveal-group data-tonal-hero>
|
||||||
|
<div class="flex flex-col justify-center space-y-8 md:py-12">
|
||||||
|
<div data-motion-beat="seal" class="flex items-center gap-4">
|
||||||
|
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
||||||
|
@if (filled($settings->hero_eyebrow))
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h1 id="hero-heading" data-motion-beat="title" class="max-w-3xl text-display font-medium text-amare-text">
|
||||||
|
{{ $settings->hero_title ?: 'Celebrações com propósito' }}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
@if (filled($settings->hero_subtitle))
|
||||||
|
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
|
||||||
|
<a href="{{ route('contact') }}" data-testid="home-primary-cta" class="btn btn-primary text-sm font-semibold">
|
||||||
|
{{ $settings->hero_cta_label ?: 'Solicitar proposta' }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
@if (filled($settings->hero_secondary_cta_label))
|
||||||
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
|
<span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (filled($settings->hero_note))
|
||||||
|
<p class="max-w-xl text-sm text-amare-text-muted">{{ $settings->hero_note }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -8,14 +8,12 @@
|
|||||||
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="manifesto-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-20" data-chapter="manifesto">
|
<section aria-labelledby="manifesto-heading" class="home-chapter border-b border-amare-border bg-amare-bg-deep py-24 md:py-32" data-chapter="manifesto">
|
||||||
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal-group>
|
<div class="container-amare grid gap-10 md:grid-cols-12" data-reveal-group>
|
||||||
<div class="space-y-5 md:pr-8" data-reveal data-reveal-from="up">
|
<div class="space-y-5 md:col-span-4 md:pt-20" data-reveal data-reveal-from="left">
|
||||||
<x-home.folio label="Manifesto" />
|
|
||||||
<span class="block h-px w-16 bg-amare-border" aria-hidden="true"></span>
|
|
||||||
<p class="max-w-52 text-lg leading-snug text-amare-text">Humana no cuidado. Precisa na entrega.</p>
|
<p class="max-w-52 text-lg leading-snug text-amare-text">Humana no cuidado. Precisa na entrega.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-6" data-reveal data-reveal-from="up">
|
<div class="space-y-6 md:col-span-7 md:col-start-6" data-reveal data-reveal-from="up">
|
||||||
<h2 id="manifesto-heading" class="max-w-3xl text-headline font-medium text-amare-text">{{ $title }}</h2>
|
<h2 id="manifesto-heading" class="max-w-3xl text-headline font-medium text-amare-text">{{ $title }}</h2>
|
||||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $lead }}</p>
|
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $lead }}</p>
|
||||||
<p class="max-w-2xl text-amare-text-muted">{{ $body }}</p>
|
<p class="max-w-2xl text-amare-text-muted">{{ $body }}</p>
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
$intro = $settings->method_intro ?: 'Clareza em cada etapa. Tranquilidade durante todo o processo.';
|
$intro = $settings->method_intro ?: 'Clareza em cada etapa. Tranquilidade durante todo o processo.';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="method-heading" class="home-chapter border-b border-amare-border bg-amare-bg-archive py-20" data-chapter="method">
|
<section aria-labelledby="method-heading" class="home-chapter border-b border-amare-border bg-amare-bg-archive py-24" data-chapter="method">
|
||||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start" data-reveal-group>
|
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start" data-reveal-group>
|
||||||
<div class="space-y-4" data-reveal data-reveal-from="up">
|
<div class="space-y-4" data-reveal data-reveal-from="up">
|
||||||
<x-home.folio label="Método" />
|
|
||||||
<h2 id="method-heading" class="text-headline font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
<h2 id="method-heading" class="text-headline font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
||||||
<p class="text-amare-text-muted">{{ $intro }}</p>
|
<p class="text-amare-text-muted">{{ $intro }}</p>
|
||||||
<x-brand.logo
|
<x-brand.logo
|
||||||
|
|||||||
@@ -3,25 +3,43 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($cases->isNotEmpty())
|
@if ($cases->isNotEmpty())
|
||||||
<section aria-labelledby="portfolio-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-20 text-amare-accent-text" data-chapter="portfolio">
|
<section aria-labelledby="portfolio-heading" class="home-chapter border-b border-amare-accent-deep bg-amare-accent-deep py-24 text-amare-accent-text" data-chapter="portfolio">
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal data-reveal-from="up">
|
<div class="grid gap-4 md:grid-cols-12" data-reveal data-reveal-from="up">
|
||||||
<x-home.folio label="Portfólio" tone="inverse" />
|
<div class="md:col-span-7 md:col-start-5 space-y-3">
|
||||||
<div class="space-y-3">
|
|
||||||
<h2 id="portfolio-heading" class="text-headline font-medium">Celebrações que ganham forma com intenção.</h2>
|
<h2 id="portfolio-heading" class="text-headline font-medium">Celebrações que ganham forma com intenção.</h2>
|
||||||
<p class="max-w-2xl text-amare-accent-text/80">Recortes de eventos conduzidos com escuta, direção e presença em cada etapa.</p>
|
<p class="max-w-2xl text-amare-accent-text/80">Recortes de eventos conduzidos com escuta, direção e presença em cada etapa.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-10 md:grid-cols-2">
|
<div class="grid gap-8 md:grid-cols-12 md:gap-x-8 md:gap-y-14" data-editorial-portfolio>
|
||||||
@foreach ($cases as $case)
|
@foreach ($cases as $case)
|
||||||
<article class="space-y-4 border-t border-amare-accent-text/30 pt-4" data-reveal data-reveal-from="up">
|
@php
|
||||||
|
$isFeature = $loop->first;
|
||||||
|
$isOffset = ! $isFeature && $loop->even;
|
||||||
|
@endphp
|
||||||
|
<article
|
||||||
|
@class([
|
||||||
|
'space-y-4 border-t border-amare-accent-text/30 pt-4',
|
||||||
|
'md:col-span-7' => $isFeature,
|
||||||
|
'md:col-span-5 md:col-start-8 md:pt-12' => $isOffset,
|
||||||
|
'md:col-span-5' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
|
data-editorial-portfolio-item="{{ $isFeature ? 'feature' : ($isOffset ? 'offset' : 'standard') }}"
|
||||||
|
data-reveal
|
||||||
|
data-reveal-from="up"
|
||||||
|
>
|
||||||
@if (filled($case->cover_image_path))
|
@if (filled($case->cover_image_path))
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$case->cover_image_path"
|
:path="$case->cover_image_path"
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
|
||||||
class="img-editorial aspect-[4/3] w-full object-cover"
|
@class([
|
||||||
|
'img-editorial w-full object-cover',
|
||||||
|
'aspect-[5/4]' => $isFeature,
|
||||||
|
'aspect-[4/5]' => $isOffset,
|
||||||
|
'aspect-[4/3]' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
/>
|
/>
|
||||||
@endif
|
@endif
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|||||||
@@ -7,25 +7,34 @@
|
|||||||
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="positioning-heading" class="home-chapter border-b border-amare-border py-20" data-chapter="positioning">
|
<section aria-labelledby="positioning-heading" class="home-chapter border-b border-amare-border py-24" data-chapter="positioning">
|
||||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal-group>
|
<div class="container-amare grid gap-10 md:grid-cols-12" data-reveal-group>
|
||||||
<div class="space-y-4" data-reveal data-reveal-from="up">
|
<div class="space-y-4 md:col-span-5" data-reveal data-reveal-from="left">
|
||||||
<x-home.folio label="A Amare" />
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<span class="h-px w-10 bg-amare-border" aria-hidden="true"></span>
|
<span class="h-px w-10 bg-amare-border" aria-hidden="true"></span>
|
||||||
<p class="text-sm uppercase tracking-[0.12em] text-amare-text-muted">Assessoria boutique · São Paulo</p>
|
<p class="text-sm uppercase tracking-[0.12em] text-amare-text-muted">Assessoria boutique · São Paulo</p>
|
||||||
</div>
|
</div>
|
||||||
<h2 id="positioning-heading" class="text-headline font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
<h2 id="positioning-heading" class="text-headline font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-8" data-reveal data-reveal-from="up">
|
<div class="space-y-8 md:col-span-6 md:col-start-7" data-reveal data-reveal-from="up">
|
||||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
||||||
|
@if (filled($settings->about_image_path))
|
||||||
|
<div class="overflow-hidden bg-amare-bg-deep" data-reveal-media>
|
||||||
|
<x-media.image
|
||||||
|
:path="$settings->about_image_path"
|
||||||
|
:alt="$settings->about_image_alt ?: $settings->brand_name"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
class="img-editorial aspect-[4/3] w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
<ul class="grid gap-3 border-t border-amare-border">
|
<ul class="grid gap-3 border-t border-amare-border">
|
||||||
@foreach ($principles as $principle)
|
@foreach ($principles as $principle)
|
||||||
<li class="border-b border-amare-border py-3 text-amare-text-muted">{{ $principle }}</li>
|
<li class="border-b border-amare-border py-3 text-amare-text-muted">{{ $principle }}</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('about') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
<span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
|
<span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($services->isNotEmpty())
|
@if ($services->isNotEmpty())
|
||||||
<section aria-labelledby="services-heading" class="home-chapter border-b border-amare-border py-20" data-chapter="services">
|
<section aria-labelledby="services-heading" class="home-chapter border-b border-amare-border py-24" data-chapter="services">
|
||||||
<div class="container-amare space-y-10" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="grid gap-6 md:grid-cols-12 md:items-end" data-reveal data-reveal-from="up">
|
<div class="grid gap-6 md:grid-cols-12 md:items-end" data-reveal data-reveal-from="up">
|
||||||
<div class="space-y-3 md:col-span-4">
|
<div class="space-y-3 md:col-span-4">
|
||||||
<x-home.folio label="Serviços" />
|
|
||||||
<h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2>
|
<h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4 md:col-span-6 md:col-start-7">
|
<div class="space-y-4 md:col-span-6 md:col-start-7">
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ route('services.index') }}" class="inline-flex min-h-11 items-center text-sm font-semibold text-amare-accent transition-colors hover:text-amare-accent-deep">
|
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 text-sm font-semibold text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
|
||||||
<span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
|
<span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -9,21 +9,23 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($testimonials->isNotEmpty())
|
@if ($testimonials->isNotEmpty())
|
||||||
<section aria-labelledby="testimonials-heading" class="home-chapter border-b border-amare-border bg-amare-bg py-16" data-chapter="testimonials">
|
<section aria-labelledby="testimonials-heading" class="home-chapter border-b border-amare-border bg-amare-bg py-24" data-chapter="testimonials">
|
||||||
<div class="container-amare space-y-8" data-reveal-group>
|
<div class="container-amare space-y-10" data-reveal-group>
|
||||||
<div class="max-w-2xl space-y-3" data-reveal data-reveal-from="up">
|
<div class="max-w-2xl space-y-3 md:ml-[16.666667%]" data-reveal data-reveal-from="up">
|
||||||
<x-home.folio label="Depoimentos" />
|
|
||||||
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
<h2 id="testimonials-heading" class="text-3xl font-medium text-amare-text">Depoimentos</h2>
|
||||||
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-12">
|
||||||
@foreach ($testimonials as $testimonial)
|
@foreach ($testimonials as $testimonial)
|
||||||
@php
|
@php
|
||||||
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
||||||
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
||||||
@endphp
|
@endphp
|
||||||
<blockquote class="space-y-4 border-t border-amare-border pt-4" data-reveal data-reveal-from="{{ $loop->odd ? 'left' : 'right' }}">
|
<blockquote @class([
|
||||||
|
'space-y-4 border-t border-amare-border pt-4 md:col-span-5' => $loop->odd,
|
||||||
|
'space-y-4 border-t border-amare-border pt-4 md:col-span-5 md:col-start-7 md:mt-16' => $loop->even,
|
||||||
|
]) data-reveal data-reveal-from="{{ $loop->odd ? 'left' : 'right' }}">
|
||||||
<div class="grid grid-cols-[1.5rem_minmax(0,1fr)] gap-2">
|
<div class="grid grid-cols-[1.5rem_minmax(0,1fr)] gap-2">
|
||||||
<span class="text-3xl leading-none text-amare-sage" aria-hidden="true">“</span>
|
<span class="text-3xl leading-none text-amare-sage" aria-hidden="true">“</span>
|
||||||
<div class="space-y-3 text-lg text-amare-text">
|
<div class="space-y-3 text-lg text-amare-text">
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
@props([
|
@props([
|
||||||
'path',
|
'path',
|
||||||
'alt',
|
'alt',
|
||||||
'sizes' => '(max-width: 768px) 100vw, 960px',
|
// No image on the site spans the full viewport: every one of them sits inside
|
||||||
|
// `container-amare`, which reserves 1.5rem of padding on each side. Claiming
|
||||||
|
// 100vw made a 412 px viewport at DPR 1.75 ask for 721 px and jump to the
|
||||||
|
// 960 variant for a box it draws at 637 px.
|
||||||
|
'sizes' => '(max-width: 768px) calc(100vw - 3rem), 960px',
|
||||||
'loading' => 'lazy',
|
'loading' => 'lazy',
|
||||||
'fetchpriority' => null,
|
'fetchpriority' => null,
|
||||||
'width' => null,
|
'width' => null,
|
||||||
@@ -18,17 +22,26 @@
|
|||||||
$diskName = $disk ?? PublicImageUploadRules::disk();
|
$diskName = $disk ?? PublicImageUploadRules::disk();
|
||||||
$filesystem = Storage::disk($diskName);
|
$filesystem = Storage::disk($diskName);
|
||||||
$src = $filesystem->url($path);
|
$src = $filesystem->url($path);
|
||||||
$variants = ResponsiveImage::availableVariants($path, $diskName);
|
|
||||||
$srcset = collect($variants)
|
$toSrcset = fn (array $variants): string => collect($variants)
|
||||||
->map(fn (array $variant): string => $filesystem->url($variant['path']).' '.$variant['width'].'w')
|
->map(fn (array $variant): string => $filesystem->url($variant['path']).' '.$variant['width'].'w')
|
||||||
->implode(', ');
|
->implode(', ');
|
||||||
|
|
||||||
if ($srcset === '' && $filesystem->exists($path)) {
|
// All public rendering reads one durable metadata record. On a cache hit this
|
||||||
$srcset = null;
|
// avoids both existence probes and downloading the original from R2.
|
||||||
}
|
$metadata = ResponsiveImage::metadata($path, $diskName);
|
||||||
|
$variants = $metadata['variants'] ?? [];
|
||||||
|
$srcset = $toSrcset($variants);
|
||||||
|
|
||||||
|
// Offered ahead of the original format because webp carries the same picture
|
||||||
|
// for roughly a third of the bytes, and the MAN-109 audit found the hero
|
||||||
|
// image to be the LCP element on every page at mobile widths. Media uploaded
|
||||||
|
// before `media:generate-variants` learned to emit webp has no siblings, so
|
||||||
|
// the <source> is skipped rather than pointed at nothing.
|
||||||
|
$webpSrcset = $toSrcset($metadata['webp_variants'] ?? []);
|
||||||
|
|
||||||
$dimensions = ($width === null || $height === null)
|
$dimensions = ($width === null || $height === null)
|
||||||
? ResponsiveImage::dimensions($path, $diskName)
|
? $metadata
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
$resolvedWidth = $width ?? $dimensions['width'] ?? null;
|
$resolvedWidth = $width ?? $dimensions['width'] ?? null;
|
||||||
@@ -36,9 +49,17 @@
|
|||||||
$loadingValue = $loading;
|
$loadingValue = $loading;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
|
{{-- `display: contents` keeps <picture> out of the layout: the callers style the
|
||||||
|
<img> with classes like `h-full w-full object-cover` that resolve against the
|
||||||
|
grid or flex parent, and an inline wrapper would break that. --}}
|
||||||
|
@if ($webpSrcset !== '')
|
||||||
|
<picture class="contents">
|
||||||
|
<source type="image/webp" srcset="{{ $webpSrcset }}" sizes="{{ $sizes }}">
|
||||||
|
@endif
|
||||||
<img
|
<img
|
||||||
src="{{ $src }}"
|
src="{{ $src }}"
|
||||||
@if ($srcset) srcset="{{ $srcset }}" sizes="{{ $sizes }}" @endif
|
@if ($srcset) srcset="{{ $srcset }}" @endif
|
||||||
|
sizes="{{ $sizes }}"
|
||||||
alt="{{ $alt }}"
|
alt="{{ $alt }}"
|
||||||
@if ($resolvedWidth) width="{{ $resolvedWidth }}" @endif
|
@if ($resolvedWidth) width="{{ $resolvedWidth }}" @endif
|
||||||
@if ($resolvedHeight) height="{{ $resolvedHeight }}" @endif
|
@if ($resolvedHeight) height="{{ $resolvedHeight }}" @endif
|
||||||
@@ -47,3 +68,6 @@
|
|||||||
@if ($class) class="{{ $class }}" @endif
|
@if ($class) class="{{ $class }}" @endif
|
||||||
{{ $attributes->except(['path', 'alt', 'sizes', 'loading', 'width', 'height', 'disk', 'class']) }}
|
{{ $attributes->except(['path', 'alt', 'sizes', 'loading', 'width', 'height', 'disk', 'class']) }}
|
||||||
>
|
>
|
||||||
|
@if ($webpSrcset !== '')
|
||||||
|
</picture>
|
||||||
|
@endif
|
||||||
|
|||||||
76
resources/views/components/public/photo-hero.blade.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
@props([
|
||||||
|
'imagePath' => null,
|
||||||
|
'imageAlt' => null,
|
||||||
|
'eyebrow' => null,
|
||||||
|
'title',
|
||||||
|
'summary' => null,
|
||||||
|
'metadata' => null,
|
||||||
|
'headingId' => 'hero-heading',
|
||||||
|
'brandMark' => false,
|
||||||
|
])
|
||||||
|
|
||||||
|
<section aria-labelledby="{{ $headingId }}" {{ $attributes->class(['border-b border-amare-border bg-amare-bg']) }} data-motion="page-open">
|
||||||
|
@if (filled($imagePath))
|
||||||
|
<div class="relative flex min-h-[calc(100dvh-5.5rem)] items-end overflow-hidden" data-photo-hero>
|
||||||
|
<x-media.image
|
||||||
|
:path="$imagePath"
|
||||||
|
:alt="$imageAlt ?: $title"
|
||||||
|
loading="eager"
|
||||||
|
fetchpriority="high"
|
||||||
|
sizes="100vw"
|
||||||
|
class="img-editorial absolute inset-0 h-full w-full object-cover"
|
||||||
|
data-motion-beat="media"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="container-amare relative z-10 w-full py-8 md:py-12">
|
||||||
|
<div class="max-w-3xl border border-amare-border bg-amare-bg p-6 text-amare-text md:p-10" data-motion-beat="heading">
|
||||||
|
@if ($brandMark || filled($eyebrow))
|
||||||
|
<div class="mb-5 flex items-center gap-4" data-motion-beat="seal">
|
||||||
|
@if ($brandMark)
|
||||||
|
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
||||||
|
@endif
|
||||||
|
@if (filled($eyebrow))
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $eyebrow }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<h1 id="{{ $headingId }}" data-motion-beat="title" class="max-w-3xl text-display font-medium text-amare-text">{{ $title }}</h1>
|
||||||
|
@if (filled($summary))
|
||||||
|
<p class="mt-5 max-w-2xl text-lg text-amare-text-muted">{{ $summary }}</p>
|
||||||
|
@endif
|
||||||
|
@if (filled($metadata))
|
||||||
|
<p class="mt-4 text-sm break-words text-amare-text-muted">{{ $metadata }}</p>
|
||||||
|
@endif
|
||||||
|
@if (trim((string) $slot) !== '')
|
||||||
|
<div class="mt-7" data-motion-beat="cta">{{ $slot }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="container-amare py-16 md:py-24" data-tonal-hero>
|
||||||
|
<div class="max-w-3xl space-y-4" data-motion-beat="heading">
|
||||||
|
@if ($brandMark || filled($eyebrow))
|
||||||
|
<div class="flex items-center gap-4" data-motion-beat="seal">
|
||||||
|
@if ($brandMark)
|
||||||
|
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
||||||
|
@endif
|
||||||
|
@if (filled($eyebrow))
|
||||||
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $eyebrow }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<h1 id="{{ $headingId }}" data-motion-beat="title" class="text-headline font-medium tracking-tight text-amare-text">{{ $title }}</h1>
|
||||||
|
@if (filled($summary))
|
||||||
|
<p class="text-lg text-amare-muted">{{ $summary }}</p>
|
||||||
|
@endif
|
||||||
|
@if (filled($metadata))
|
||||||
|
<p class="text-sm break-words text-amare-muted">{{ $metadata }}</p>
|
||||||
|
@endif
|
||||||
|
@if (trim((string) $slot) !== '')
|
||||||
|
<div data-motion-beat="cta">{{ $slot }}</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Página não encontrada</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Página não encontrada</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Sessão expirada</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Sessão expirada</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Muitas solicitações</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Muitas solicitações</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Algo deu errado</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Algo deu errado</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Não foi possível concluir o pedido agora. Tente novamente em instantes.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Em manutenção</h1>
|
<h1 class="text-4xl font-medium tracking-tight text-amare-text" data-motion-beat="title">Em manutenção</h1>
|
||||||
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
|
<p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
|
||||||
<p data-reveal data-reveal-from="up">
|
<p data-reveal data-reveal-from="up">
|
||||||
<a href="{{ route('home') }}" class="inline-flex min-h-11 items-center bg-amare-accent px-5 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep">
|
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
|
||||||
Voltar para a home
|
Voltar para a home
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" data-theme="amare">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
@@ -10,6 +10,24 @@
|
|||||||
<title>{{ $pageMeta->title }}</title>
|
<title>{{ $pageMeta->title }}</title>
|
||||||
<x-seo.meta :page-meta="$pageMeta" />
|
<x-seo.meta :page-meta="$pageMeta" />
|
||||||
|
|
||||||
|
@php
|
||||||
|
$origin = static function (string $url): ?string {
|
||||||
|
$parts = parse_url($url);
|
||||||
|
|
||||||
|
if (! is_array($parts) || ! isset($parts['scheme'], $parts['host'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parts['scheme'].'://'.$parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
$r2Origin = $origin((string) config('filesystems.disks.r2.url'));
|
||||||
|
$siteOrigin = $origin((string) config('app.url'));
|
||||||
|
@endphp
|
||||||
|
@if (\App\Support\PublicImageUploadRules::disk() === 'r2' && $r2Origin !== null && $r2Origin !== $siteOrigin)
|
||||||
|
<link rel="preconnect" href="{{ $r2Origin }}" crossorigin>
|
||||||
|
@endif
|
||||||
|
|
||||||
<x-fonts />
|
<x-fonts />
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
|
|
||||||
@@ -40,11 +58,11 @@
|
|||||||
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 backdrop-blur-sm">
|
<header class="site-header sticky top-0 z-40 border-b border-amare-border/80 bg-amare-bg/90 backdrop-blur-sm">
|
||||||
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr]">
|
<div class="container-amare grid grid-cols-[auto_1fr_auto] items-center gap-4 py-4 md:grid-cols-[1fr_auto_1fr]">
|
||||||
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
<nav id="main-nav" class="main-nav order-3 col-span-3 hidden flex-col gap-4 border-t border-amare-border pt-4 md:order-1 md:col-span-1 md:flex md:flex-row md:items-center md:gap-1 md:border-0 md:pt-0" aria-label="Principal" data-main-nav>
|
||||||
<a href="{{ route('home') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Início</a>
|
<a href="{{ route('home') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted hover:text-amare-accent md:px-2">Início</a>
|
||||||
<a href="{{ route('services.index') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Serviços</a>
|
<a href="{{ route('services.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted hover:text-amare-accent md:px-2">Serviços</a>
|
||||||
<a href="{{ route('portfolio.index') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Portfólio</a>
|
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted hover:text-amare-accent md:px-2">Portfólio</a>
|
||||||
<a href="{{ route('about') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:px-2">Amare</a>
|
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted hover:text-amare-accent md:px-2">Amare</a>
|
||||||
<a href="{{ route('contact') }}" class="inline-flex items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted transition-colors hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
<a href="{{ route('contact') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<a href="{{ route('home') }}" class="order-1 justify-self-start md:order-2 md:justify-self-center" aria-label="{{ $siteSettings->brand_name }} — página inicial">
|
<a href="{{ route('home') }}" class="order-1 justify-self-start md:order-2 md:justify-self-center" aria-label="{{ $siteSettings->brand_name }} — página inicial">
|
||||||
@@ -52,13 +70,13 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
<div class="order-2 flex items-center justify-end gap-3 md:order-3">
|
||||||
<a href="{{ route('contact') }}" class="hidden items-center py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent transition-colors hover:text-amare-accent-deep md:inline-flex">
|
<a href="{{ route('contact') }}" class="btn btn-ghost hidden min-h-0 px-0 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent hover:text-amare-accent-deep md:inline-flex">
|
||||||
Solicitar proposta
|
Solicitar proposta
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="menu-button inline-flex h-11 w-11 items-center justify-center border border-amare-border text-amare-text md:hidden"
|
class="menu-button btn btn-square btn-ghost border border-amare-border text-amare-text md:hidden"
|
||||||
aria-label="Abrir menu"
|
aria-label="Abrir menu"
|
||||||
aria-controls="main-nav"
|
aria-controls="main-nav"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
|
|||||||
@@ -8,30 +8,12 @@
|
|||||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
$city = $siteSettings->city ?: 'São Paulo - SP';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg" data-motion="page-open">
|
<x-public.photo-hero :image-path="$siteSettings->about_image_path" :image-alt="$siteSettings->about_image_alt" eyebrow="A Amare" title="Humana no cuidado. Precisa na entrega." :summary="$siteSettings->about_summary">
|
||||||
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:py-24">
|
<p class="max-w-xl text-amare-text-muted">A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo, presença no dia do evento e uma condução serena do início ao fim.</p>
|
||||||
<div class="space-y-6" data-motion-beat="heading">
|
</x-public.photo-hero>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
|
||||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Humana no cuidado. Precisa na entrega.</h1>
|
|
||||||
<p class="text-lg text-amare-muted">{{ $siteSettings->about_summary }}</p>
|
|
||||||
<p class="text-amare-muted">
|
|
||||||
A {{ $siteSettings->brand_name }} atua em {{ $city }} com foco em planejamento completo,
|
|
||||||
presença no dia do evento e uma condução serena do início ao fim.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
@if (filled($siteSettings->about_image_path))
|
|
||||||
<div class="overflow-hidden bg-amare-bg-deep">
|
|
||||||
<x-media.image
|
|
||||||
:path="$siteSettings->about_image_path"
|
|
||||||
:alt="$siteSettings->about_image_alt ?: $siteSettings->brand_name"
|
|
||||||
loading="lazy"
|
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
|
||||||
class="img-editorial aspect-[4/3] w-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
|
<div class="container-amare py-16 md:py-24">
|
||||||
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare" data-reveal-group>
|
<ul class="space-y-4 border-t border-amare-border pt-6" aria-label="Princípios da Amare" data-reveal-group>
|
||||||
@foreach ($principles as $index => $principle)
|
@foreach ($principles as $index => $principle)
|
||||||
<li class="grid grid-cols-[3rem_minmax(0,1fr)] gap-4 border-b border-amare-border pb-4 text-amare-text" data-reveal data-reveal-from="up">
|
<li class="grid grid-cols-[3rem_minmax(0,1fr)] gap-4 border-b border-amare-border pb-4 text-amare-text" data-reveal data-reveal-from="up">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
@if (session('status') === 'briefing-sent')
|
@if (session('status') === 'briefing-sent')
|
||||||
<div role="status" class="mt-8 border border-amare-border bg-amare-bg-deep px-6 py-5">
|
<div role="status" class="alert mt-8 border border-amare-border bg-amare-bg-deep">
|
||||||
<p class="font-semibold text-amare-text">Mensagem enviada.</p>
|
<p class="font-semibold text-amare-text">Mensagem enviada.</p>
|
||||||
<p class="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
|
<p class="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($errors->any())
|
@if ($errors->any())
|
||||||
<div role="alert" class="border border-amare-error/40 bg-amare-error/5 px-6 py-5">
|
<div role="alert" class="alert alert-error border border-amare-error/40">
|
||||||
<p class="font-semibold text-amare-error">Não foi possível enviar.</p>
|
<p class="font-semibold text-amare-error">Não foi possível enviar.</p>
|
||||||
<p class="mt-1 text-sm text-amare-muted">Revise os campos destacados abaixo e tente novamente.</p>
|
<p class="mt-1 text-sm text-amare-muted">Revise os campos destacados abaixo e tente novamente.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
autocomplete="name"
|
autocomplete="name"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Seu nome"
|
placeholder="Seu nome"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('nome') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
|
||||||
@error('nome') aria-invalid="true" aria-describedby="nome-error" @enderror
|
@error('nome') aria-invalid="true" aria-describedby="nome-error" @enderror
|
||||||
>
|
>
|
||||||
@error('nome')
|
@error('nome')
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
autocomplete="email"
|
autocomplete="email"
|
||||||
maxlength="254"
|
maxlength="254"
|
||||||
placeholder="voce@email.com"
|
placeholder="voce@email.com"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('email') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
|
||||||
@error('email') aria-invalid="true" aria-describedby="email-error" @enderror
|
@error('email') aria-invalid="true" aria-describedby="email-error" @enderror
|
||||||
>
|
>
|
||||||
@error('email')
|
@error('email')
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
autocomplete="tel"
|
autocomplete="tel"
|
||||||
maxlength="40"
|
maxlength="40"
|
||||||
placeholder="(11) 90000-0000"
|
placeholder="(11) 90000-0000"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('telefone') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
|
||||||
@error('telefone') aria-invalid="true" aria-describedby="telefone-error" @enderror
|
@error('telefone') aria-invalid="true" aria-describedby="telefone-error" @enderror
|
||||||
>
|
>
|
||||||
@error('telefone')
|
@error('telefone')
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
id="tipo_evento"
|
id="tipo_evento"
|
||||||
name="tipo_evento"
|
name="tipo_evento"
|
||||||
required
|
required
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors focus:border-amare-accent focus:outline-none @error('tipo_evento') border-amare-error @enderror"
|
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
|
||||||
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
|
||||||
>
|
>
|
||||||
<option value="" selected disabled>Selecione...</option>
|
<option value="" selected disabled>Selecione...</option>
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
value="{{ old('data_periodo') }}"
|
value="{{ old('data_periodo') }}"
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
placeholder="Ex.: novembro de 2027"
|
placeholder="Ex.: novembro de 2027"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('data_periodo') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
|
||||||
@error('data_periodo') aria-invalid="true" aria-describedby="data_periodo-error" @enderror
|
@error('data_periodo') aria-invalid="true" aria-describedby="data_periodo-error" @enderror
|
||||||
>
|
>
|
||||||
@error('data_periodo')
|
@error('data_periodo')
|
||||||
@@ -172,7 +172,7 @@
|
|||||||
required
|
required
|
||||||
maxlength="80"
|
maxlength="80"
|
||||||
placeholder="Ex.: São Paulo"
|
placeholder="Ex.: São Paulo"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('cidade') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
|
||||||
@error('cidade') aria-invalid="true" aria-describedby="cidade-error" @enderror
|
@error('cidade') aria-invalid="true" aria-describedby="cidade-error" @enderror
|
||||||
>
|
>
|
||||||
@error('cidade')
|
@error('cidade')
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="Ex.: 120"
|
placeholder="Ex.: 120"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('convidados') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
|
||||||
@error('convidados') aria-invalid="true" aria-describedby="convidados-error" @enderror
|
@error('convidados') aria-invalid="true" aria-describedby="convidados-error" @enderror
|
||||||
>
|
>
|
||||||
@error('convidados')
|
@error('convidados')
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
value="{{ old('servico_interesse') }}"
|
value="{{ old('servico_interesse') }}"
|
||||||
maxlength="120"
|
maxlength="120"
|
||||||
placeholder="Ex.: planejamento completo"
|
placeholder="Ex.: planejamento completo"
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('servico_interesse') border-amare-error @enderror"
|
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
|
||||||
@error('servico_interesse') aria-invalid="true" aria-describedby="servico_interesse-error" @enderror
|
@error('servico_interesse') aria-invalid="true" aria-describedby="servico_interesse-error" @enderror
|
||||||
>
|
>
|
||||||
@error('servico_interesse')
|
@error('servico_interesse')
|
||||||
@@ -227,7 +227,7 @@
|
|||||||
rows="6"
|
rows="6"
|
||||||
maxlength="3000"
|
maxlength="3000"
|
||||||
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
|
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
|
||||||
class="w-full border-0 border-b border-amare-border bg-transparent py-3 text-amare-text transition-colors placeholder:text-amare-muted/60 focus:border-amare-accent focus:outline-none @error('mensagem') border-amare-error @enderror"
|
class="textarea w-full text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
|
||||||
@error('mensagem') aria-invalid="true" aria-describedby="mensagem-error" @enderror
|
@error('mensagem') aria-invalid="true" aria-describedby="mensagem-error" @enderror
|
||||||
>{{ old('mensagem') }}</textarea>
|
>{{ old('mensagem') }}</textarea>
|
||||||
@error('mensagem')
|
@error('mensagem')
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
value="1"
|
value="1"
|
||||||
required
|
required
|
||||||
@checked(old('privacidade'))
|
@checked(old('privacidade'))
|
||||||
class="mt-1 h-5 w-5 shrink-0 accent-amare-accent"
|
class="checkbox checkbox-primary mt-1 shrink-0"
|
||||||
@error('privacidade') aria-invalid="true" aria-describedby="privacidade-error" @enderror
|
@error('privacidade') aria-invalid="true" aria-describedby="privacidade-error" @enderror
|
||||||
>
|
>
|
||||||
<span class="text-sm text-amare-muted">
|
<span class="text-sm text-amare-muted">
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
data-submit-button
|
data-submit-button
|
||||||
class="inline-flex min-h-[44px] items-center justify-center bg-amare-accent px-8 py-3 text-sm font-semibold uppercase tracking-[0.12em] text-amare-accent-text transition-colors hover:bg-amare-accent-deep"
|
class="btn btn-primary min-h-11 px-8 text-sm font-semibold uppercase tracking-[0.12em]"
|
||||||
>
|
>
|
||||||
Enviar briefing
|
Enviar briefing
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,36 +1,6 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
@php
|
|
||||||
$testimonials = $content->testimonials
|
|
||||||
->filter(fn ($testimonial): bool => filled(trim((string) $testimonial->quote)))
|
|
||||||
->values();
|
|
||||||
|
|
||||||
$chapters = [
|
|
||||||
['id' => 'hero-heading', 'label' => 'Capa'],
|
|
||||||
['id' => 'manifesto-heading', 'label' => 'Manifesto'],
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($content->featuredServices->isNotEmpty()) {
|
|
||||||
$chapters[] = ['id' => 'services-heading', 'label' => 'Serviços'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($content->featuredCases->isNotEmpty()) {
|
|
||||||
$chapters[] = ['id' => 'portfolio-heading', 'label' => 'Portfólio'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$chapters[] = ['id' => 'method-heading', 'label' => 'Método'];
|
|
||||||
|
|
||||||
if ($testimonials->isNotEmpty()) {
|
|
||||||
$chapters[] = ['id' => 'testimonials-heading', 'label' => 'Depoimentos'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$chapters[] = ['id' => 'positioning-heading', 'label' => 'A Amare'];
|
|
||||||
$chapters[] = ['id' => 'final-cta-heading', 'label' => 'Próximo passo'];
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
<x-home.chapter-index :chapters="$chapters" />
|
|
||||||
|
|
||||||
<div class="home-chapters">
|
<div class="home-chapters">
|
||||||
<x-home.hero :settings="$content->settings" />
|
<x-home.hero :settings="$content->settings" />
|
||||||
<x-home.manifesto :settings="$content->settings" />
|
<x-home.manifesto :settings="$content->settings" />
|
||||||
|
|||||||
@@ -1,29 +1,56 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="border-b border-amare-border bg-amare-bg-deep" data-motion="page-open">
|
<x-public.photo-hero :image-path="$siteSettings->portfolio_hero_image_path" :image-alt="$siteSettings->portfolio_hero_image_alt" eyebrow="Portfólio" title="Atmosferas que contam histórias." summary="Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência." />
|
||||||
|
|
||||||
|
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||||
<div class="max-w-2xl space-y-4" data-motion-beat="heading">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
|
||||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Atmosferas que contam histórias.</h1>
|
|
||||||
<p class="text-lg text-amare-muted">Casos conduzidos com atenção a ritmo, composição e cada detalhe da experiência.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($cases->isEmpty())
|
@if ($cases->isEmpty())
|
||||||
<p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p>
|
<p class="text-amare-muted">Novos casos serão publicados assim que o acervo estiver organizado. Enquanto isso, fale conosco para conhecer o nosso trabalho.</p>
|
||||||
@else
|
@else
|
||||||
<div class="grid gap-10 md:grid-cols-2" data-reveal-group>
|
@php
|
||||||
|
$hasPrioritizedImage = false;
|
||||||
|
@endphp
|
||||||
|
<div class="grid gap-8 md:grid-cols-12 md:gap-x-8 md:gap-y-14" data-editorial-portfolio-grid data-reveal-group>
|
||||||
@foreach ($cases as $case)
|
@foreach ($cases as $case)
|
||||||
<article class="space-y-4" data-reveal data-reveal-from="up">
|
@php
|
||||||
|
$isFeature = $loop->first;
|
||||||
|
$isOffset = ! $isFeature && $loop->even;
|
||||||
|
@endphp
|
||||||
|
<article
|
||||||
|
@class([
|
||||||
|
'space-y-4',
|
||||||
|
'md:col-span-7' => $isFeature,
|
||||||
|
'md:col-span-5 md:col-start-8 md:pt-12' => $isOffset,
|
||||||
|
'md:col-span-5' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
|
data-editorial-portfolio-item="{{ $isFeature ? 'feature' : ($isOffset ? 'offset' : 'standard') }}"
|
||||||
|
data-reveal
|
||||||
|
data-reveal-from="up"
|
||||||
|
>
|
||||||
@if (filled($case->cover_image_path))
|
@if (filled($case->cover_image_path))
|
||||||
|
@php
|
||||||
|
$prioritizeImage = ! $hasPrioritizedImage;
|
||||||
|
@endphp
|
||||||
<a href="{{ route('portfolio.show', $case->slug) }}" class="block overflow-hidden">
|
<a href="{{ route('portfolio.show', $case->slug) }}" class="block overflow-hidden">
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$case->cover_image_path"
|
:path="$case->cover_image_path"
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
:alt="$case->cover_image_alt ?: $case->title"
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
:loading="$prioritizeImage ? 'eager' : 'lazy'"
|
||||||
class="img-editorial aspect-[4/3] w-full object-cover transition-transform duration-(--amare-duration-slow) ease-(--amare-ease-standard) motion-safe:hover:scale-[1.02]"
|
:fetchpriority="$prioritizeImage ? 'high' : null"
|
||||||
|
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
|
||||||
|
@class([
|
||||||
|
'img-editorial w-full object-cover transition-transform duration-(--amare-duration-slow) ease-(--amare-ease-standard) motion-safe:hover:scale-[1.02]',
|
||||||
|
'aspect-[5/4]' => $isFeature,
|
||||||
|
'aspect-[4/5]' => $isOffset,
|
||||||
|
'aspect-[4/3]' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
@php
|
||||||
|
$hasPrioritizedImage = true;
|
||||||
|
@endphp
|
||||||
@endif
|
@endif
|
||||||
<div class="space-y-2 border-t border-amare-border pt-4">
|
<div class="space-y-2 border-t border-amare-border pt-4">
|
||||||
<h2 class="text-2xl font-medium text-amare-text">
|
<h2 class="text-2xl font-medium text-amare-text">
|
||||||
|
|||||||
@@ -1,34 +1,8 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<article data-motion="page-open">
|
<article>
|
||||||
<div class="border-b border-amare-border bg-amare-bg">
|
<x-public.photo-hero :image-path="$case->cover_image_path" :image-alt="$case->cover_image_alt" :eyebrow="$case->event_type" :title="$case->title" :summary="$case->summary" :metadata="collect([$case->city, $case->venue, $case->event_date?->format('d/m/Y')])->filter()->implode(' · ')" />
|
||||||
<div class="container-amare space-y-8 py-16 md:py-24">
|
|
||||||
<header class="max-w-3xl space-y-4" data-motion-beat="heading">
|
|
||||||
@if (filled($case->event_type))
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ $case->event_type }}</p>
|
|
||||||
@endif
|
|
||||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">{{ $case->title }}</h1>
|
|
||||||
<p class="text-lg text-amare-muted">{{ $case->summary }}</p>
|
|
||||||
<p class="text-sm break-words text-amare-muted">
|
|
||||||
@if ($case->city){{ $case->city }}@endif
|
|
||||||
@if ($case->venue) · {{ $case->venue }}@endif
|
|
||||||
@if ($case->event_date) · {{ $case->event_date->format('d/m/Y') }}@endif
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
@if (filled($case->cover_image_path))
|
|
||||||
<x-media.image
|
|
||||||
:path="$case->cover_image_path"
|
|
||||||
:alt="$case->cover_image_alt ?: $case->title"
|
|
||||||
loading="eager"
|
|
||||||
sizes="(max-width: 1024px) 100vw, 1120px"
|
|
||||||
class="img-editorial aspect-[16/9] w-full object-cover"
|
|
||||||
data-motion-beat="media"
|
|
||||||
/>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-b border-amare-border bg-amare-bg-deep">
|
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||||
<div class="container-amare grid gap-10 py-16 md:grid-cols-3" data-reveal-group>
|
<div class="container-amare grid gap-10 py-16 md:grid-cols-3" data-reveal-group>
|
||||||
@@ -53,14 +27,34 @@
|
|||||||
<section aria-labelledby="gallery-heading" class="bg-amare-bg">
|
<section aria-labelledby="gallery-heading" class="bg-amare-bg">
|
||||||
<div class="container-amare space-y-8 py-16">
|
<div class="container-amare space-y-8 py-16">
|
||||||
<h2 id="gallery-heading" class="text-3xl font-medium text-amare-text">Galeria</h2>
|
<h2 id="gallery-heading" class="text-3xl font-medium text-amare-text">Galeria</h2>
|
||||||
<div class="grid gap-6 md:grid-cols-2" data-reveal-group>
|
<div class="grid gap-6 md:grid-cols-12 md:gap-x-8 md:gap-y-12" data-editorial-gallery data-reveal-group>
|
||||||
@foreach ($case->images as $image)
|
@foreach ($case->images as $image)
|
||||||
<figure class="space-y-2" data-reveal data-reveal-from="up" data-reveal-media>
|
@php
|
||||||
|
$isFeature = $loop->first;
|
||||||
|
$isOffset = ! $isFeature && $loop->even;
|
||||||
|
@endphp
|
||||||
|
<figure
|
||||||
|
@class([
|
||||||
|
'space-y-2',
|
||||||
|
'md:col-span-7' => $isFeature,
|
||||||
|
'md:col-span-5 md:col-start-8 md:pt-12' => $isOffset,
|
||||||
|
'md:col-span-5' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
|
data-editorial-gallery-item="{{ $isFeature ? 'feature' : ($isOffset ? 'offset' : 'standard') }}"
|
||||||
|
data-reveal
|
||||||
|
data-reveal-from="up"
|
||||||
|
data-reveal-media
|
||||||
|
>
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$image->path"
|
:path="$image->path"
|
||||||
:alt="$image->alt_text"
|
:alt="$image->alt_text"
|
||||||
sizes="(max-width: 768px) 100vw, 50vw"
|
sizes="(max-width: 768px) calc(100vw - 3rem), 50vw"
|
||||||
class="img-editorial aspect-[4/3] w-full object-cover"
|
@class([
|
||||||
|
'img-editorial w-full object-cover',
|
||||||
|
'aspect-[5/4]' => $isFeature,
|
||||||
|
'aspect-[4/5]' => $isOffset,
|
||||||
|
'aspect-[4/3]' => ! $isFeature && ! $isOffset,
|
||||||
|
])
|
||||||
/>
|
/>
|
||||||
@if (filled($image->caption))
|
@if (filled($image->caption))
|
||||||
<figcaption class="text-sm text-amare-muted">{{ $image->caption }}</figcaption>
|
<figcaption class="text-sm text-amare-muted">{{ $image->caption }}</figcaption>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="border-b border-amare-border bg-amare-bg" data-motion="page-open">
|
<x-public.photo-hero :image-path="$siteSettings->services_hero_image_path" :image-alt="$siteSettings->services_hero_image_alt" eyebrow="Serviços" title="Uma mesma excelência, diferentes ocasiões." summary="O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento." />
|
||||||
|
|
||||||
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||||
<div class="max-w-2xl space-y-4" data-motion-beat="heading">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
|
|
||||||
<h1 class="text-headline font-medium tracking-tight text-amare-text">Uma mesma excelência, diferentes ocasiões.</h1>
|
|
||||||
<p class="text-lg text-amare-muted">O escopo é construído de acordo com o momento do projeto, o nível de apoio necessário e a complexidade de cada evento.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if ($services->isEmpty())
|
@if ($services->isEmpty())
|
||||||
<p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p>
|
<p class="text-amare-muted">O catálogo de serviços está em organização. Enquanto isso, fale conosco para uma primeira conversa.</p>
|
||||||
@else
|
@else
|
||||||
<div class="divide-y divide-amare-border border-y border-amare-border" data-reveal-group>
|
@php($hasPrioritizedImage = false)
|
||||||
|
<div class="divide-y divide-amare-border border-y border-amare-border" data-editorial-service-list data-reveal-group>
|
||||||
@foreach ($services as $index => $service)
|
@foreach ($services as $index => $service)
|
||||||
<article class="grid gap-4 py-8 md:grid-cols-[5rem_minmax(0,1fr)_minmax(0,1.2fr)] md:items-start" data-reveal data-reveal-from="up">
|
<article @class([
|
||||||
|
'grid gap-4 py-8 md:grid-cols-[5rem_minmax(0,1fr)_minmax(0,1.2fr)] md:items-start',
|
||||||
|
'md:translate-x-8' => $loop->even,
|
||||||
|
]) data-reveal data-reveal-from="up">
|
||||||
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
<span class="text-sm font-semibold uppercase tracking-[0.14em] text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h2 class="text-2xl font-medium text-amare-text md:text-3xl">{{ $service->title }}</h2>
|
<h2 class="text-2xl font-medium text-amare-text md:text-3xl">{{ $service->title }}</h2>
|
||||||
@@ -22,12 +23,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
@if (filled($service->cover_image_path))
|
@if (filled($service->cover_image_path))
|
||||||
|
@php($prioritizeImage = ! $hasPrioritizedImage)
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$service->cover_image_path"
|
:path="$service->cover_image_path"
|
||||||
:alt="$service->cover_image_alt ?: $service->title"
|
:alt="$service->cover_image_alt ?: $service->title"
|
||||||
sizes="(max-width: 768px) 100vw, 40vw"
|
:loading="$prioritizeImage ? 'eager' : 'lazy'"
|
||||||
|
:fetchpriority="$prioritizeImage ? 'high' : null"
|
||||||
|
sizes="(max-width: 768px) calc(100vw - 3rem), 40vw"
|
||||||
class="img-editorial aspect-[16/10] w-full object-cover"
|
class="img-editorial aspect-[16/10] w-full object-cover"
|
||||||
/>
|
/>
|
||||||
|
@php($hasPrioritizedImage = true)
|
||||||
@endif
|
@endif
|
||||||
@if (filled($service->description))
|
@if (filled($service->description))
|
||||||
<div class="text-amare-muted">
|
<div class="text-amare-muted">
|
||||||
|
|||||||
@@ -8,6 +8,18 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# scripts/perf/lighthouse.sh [output-dir]
|
# scripts/perf/lighthouse.sh [output-dir]
|
||||||
#
|
#
|
||||||
|
# Environment:
|
||||||
|
# BASE_URL origin to audit (default http://127.0.0.1:8000)
|
||||||
|
# TARGET label for the output subtree (default local)
|
||||||
|
# RUNS runs per page and preset (default 3)
|
||||||
|
# PRESETS space-separated preset list (default "mobile desktop")
|
||||||
|
#
|
||||||
|
# Lighthouse LCP moves by a few tenths of a second between runs on the same
|
||||||
|
# build, so a single run cannot support a before/after comparison. Every page
|
||||||
|
# is audited RUNS times per preset and the run holding the median LCP is the
|
||||||
|
# one reported — averaging across runs would describe a page that never
|
||||||
|
# existed.
|
||||||
|
#
|
||||||
# Expects a site already answering on $BASE_URL. To raise one from scratch:
|
# Expects a site already answering on $BASE_URL. To raise one from scratch:
|
||||||
#
|
#
|
||||||
# docker build -t amare-app:ci .
|
# docker build -t amare-app:ci .
|
||||||
@@ -20,7 +32,11 @@
|
|||||||
# -v "$(pwd)/storage/app/public:/app/storage/app/public" \
|
# -v "$(pwd)/storage/app/public:/app/storage/app/public" \
|
||||||
# amare-app:ci
|
# amare-app:ci
|
||||||
#
|
#
|
||||||
# Seed content and generate the responsive variants first. Skipping
|
# Seed content and generate the responsive variants first, and run both from
|
||||||
|
# the host rather than inside the container. ContentSeeder guards itself with
|
||||||
|
# an allow-list of local/staging/testing (database/seeders/ContentSeeder.php),
|
||||||
|
# so under the container's APP_ENV=production it is a deliberate no-op and
|
||||||
|
# Lighthouse would end up measuring empty pages. Skipping
|
||||||
# `media:generate-variants` inflates LCP by roughly 2.5 s on the home page,
|
# `media:generate-variants` inflates LCP by roughly 2.5 s on the home page,
|
||||||
# because the originals are served at full size:
|
# because the originals are served at full size:
|
||||||
#
|
#
|
||||||
@@ -30,7 +46,11 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
BASE_URL="${BASE_URL:-http://127.0.0.1:8000}"
|
BASE_URL="${BASE_URL:-http://127.0.0.1:8000}"
|
||||||
OUT_DIR="${1:-storage/app/lighthouse}"
|
BASE_URL="${BASE_URL%/}"
|
||||||
|
TARGET="${TARGET:-local}"
|
||||||
|
RUNS="${RUNS:-3}"
|
||||||
|
PRESETS="${PRESETS:-mobile desktop}"
|
||||||
|
OUT_DIR="${1:-storage/app/lighthouse}/${TARGET}"
|
||||||
|
|
||||||
# Lighthouse needs a Chrome binary. Playwright's is already on disk after
|
# Lighthouse needs a Chrome binary. Playwright's is already on disk after
|
||||||
# `npx playwright install chromium`; fall back to a system Chrome.
|
# `npx playwright install chromium`; fall back to a system Chrome.
|
||||||
@@ -45,23 +65,73 @@ fi
|
|||||||
|
|
||||||
mkdir -p "${OUT_DIR}"
|
mkdir -p "${OUT_DIR}"
|
||||||
|
|
||||||
PAGES=("/:home" "/servicos:servicos" "/portfolio:portfolio" "/sobre:sobre" "/contato:contato")
|
PAGES=(
|
||||||
|
"/:home"
|
||||||
|
"/servicos:servicos"
|
||||||
|
"/portfolio:portfolio"
|
||||||
|
"/portfolio/casamento-ana-lucas:portfolio-detalhe"
|
||||||
|
"/sobre:sobre"
|
||||||
|
"/contato:contato"
|
||||||
|
)
|
||||||
|
|
||||||
|
# A route that answers 404 still produces a Lighthouse report, and the error
|
||||||
|
# page is light enough to score well — the heaviest route on the site would be
|
||||||
|
# reported as excellent and nobody would notice. Refuse to measure anything
|
||||||
|
# that is not a 200 before spending minutes on the audit.
|
||||||
|
echo "preflight against ${BASE_URL}"
|
||||||
|
|
||||||
for entry in "${PAGES[@]}"; do
|
for entry in "${PAGES[@]}"; do
|
||||||
|
path="${entry%%:*}"
|
||||||
|
code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 "${BASE_URL}${path}" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "${code}" != "200" ]]; then
|
||||||
|
echo "preflight failed: ${BASE_URL}${path} answered HTTP ${code:-<none>}, expected 200" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " ok ${path}"
|
||||||
|
done
|
||||||
|
|
||||||
|
for preset in ${PRESETS}; do
|
||||||
|
# The mobile preset is Lighthouse's default and rejects an explicit
|
||||||
|
# --preset flag, so only desktop is passed through.
|
||||||
|
preset_flags=()
|
||||||
|
if [[ "${preset}" != "mobile" ]]; then
|
||||||
|
preset_flags+=("--preset=${preset}")
|
||||||
|
fi
|
||||||
|
|
||||||
|
for entry in "${PAGES[@]}"; do
|
||||||
path="${entry%%:*}"
|
path="${entry%%:*}"
|
||||||
name="${entry##*:}"
|
name="${entry##*:}"
|
||||||
|
|
||||||
echo "auditing ${name} (${BASE_URL}${path})"
|
for run in $(seq 1 "${RUNS}"); do
|
||||||
|
echo "auditing ${name} ${preset} run ${run}/${RUNS} (${BASE_URL}${path})"
|
||||||
|
|
||||||
# Default preset: simulated mobile throttling, 150 ms RTT, ~1.6 Mbps,
|
# Default preset: simulated mobile throttling, 150 ms RTT,
|
||||||
# 4x CPU slowdown. Add --preset=desktop for the desktop numbers.
|
# ~1.6 Mbps, 4x CPU slowdown.
|
||||||
|
# ${array[@]+...} keeps `set -u` from treating an empty array as
|
||||||
|
# unbound, which bash 3.2 (the macOS system bash) still does.
|
||||||
npx --yes lighthouse@12 "${BASE_URL}${path}" \
|
npx --yes lighthouse@12 "${BASE_URL}${path}" \
|
||||||
--quiet \
|
--quiet \
|
||||||
|
${preset_flags[@]+"${preset_flags[@]}"} \
|
||||||
--output=json --output=html \
|
--output=json --output=html \
|
||||||
--output-path="${OUT_DIR}/${name}-mobile" \
|
--output-path="${OUT_DIR}/${name}-${preset}-run${run}" \
|
||||||
--chrome-flags="--headless=new --no-sandbox"
|
--chrome-flags="--headless=new --no-sandbox"
|
||||||
|
done
|
||||||
|
done
|
||||||
done
|
done
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "reports written to ${OUT_DIR}"
|
echo "reports written to ${OUT_DIR}"
|
||||||
|
|
||||||
|
# The seeder is recorded because it decides the byte weight of every hero
|
||||||
|
# image: a before/after comparison across different fixtures measures nothing.
|
||||||
|
node scripts/perf/summarize-lighthouse.mjs "${OUT_DIR}" \
|
||||||
|
--target="${TARGET}" \
|
||||||
|
--base-url="${BASE_URL}" \
|
||||||
|
--commit="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)$(git diff --quiet HEAD 2>/dev/null || echo '+alterações não commitadas')" \
|
||||||
|
--seeder="${SEEDER:-ContentSeeder}" \
|
||||||
|
> "${OUT_DIR}/summary.md"
|
||||||
|
|
||||||
|
echo "summary written to ${OUT_DIR}/summary.md"
|
||||||
echo "SPEC.md §6.6 targets: LCP <= 2.5s, CLS <= 0.1, INP <= 200ms, 0 console errors"
|
echo "SPEC.md §6.6 targets: LCP <= 2.5s, CLS <= 0.1, INP <= 200ms, 0 console errors"
|
||||||
|
|||||||
249
scripts/perf/summarize-lighthouse.mjs
Executable file
@@ -0,0 +1,249 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
//
|
||||||
|
// Turns a directory of Lighthouse JSON reports into one reviewable markdown
|
||||||
|
// summary. Written because the numbers behind MAN-109 previously survived only
|
||||||
|
// as a hand-typed table in a Linear comment: `storage/app/lighthouse` is
|
||||||
|
// gitignored, so there was nothing to diff a later run against.
|
||||||
|
//
|
||||||
|
// For each page and preset the run holding the median LCP is the one reported.
|
||||||
|
// Metrics are never averaged across runs — every figure in the table comes
|
||||||
|
// from the same single navigation, so the LCP phase breakdown adds up.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// node scripts/perf/summarize-lighthouse.mjs <report-dir> [--target=local]
|
||||||
|
// [--base-url=...] [--commit=...] [--seeder=...]
|
||||||
|
|
||||||
|
import { readdirSync, readFileSync } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const [dir, ...rest] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!dir) {
|
||||||
|
console.error('usage: summarize-lighthouse.mjs <report-dir> [--target=...]');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const flags = new Map(
|
||||||
|
rest
|
||||||
|
.filter((argument) => argument.startsWith('--'))
|
||||||
|
.map((argument) => {
|
||||||
|
const [key, ...value] = argument.replace(/^--/, '').split('=');
|
||||||
|
|
||||||
|
return [key, value.join('=')];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const ms = (value) => (typeof value === 'number' ? `${(value / 1000).toFixed(2)} s` : '—');
|
||||||
|
const kib = (value) => (typeof value === 'number' ? `${Math.round(value / 1024)} KiB` : '—');
|
||||||
|
const score = (value) => (typeof value === 'number' ? Math.round(value * 100) : '—');
|
||||||
|
const numeric = (audit) => (typeof audit?.numericValue === 'number' ? audit.numericValue : null);
|
||||||
|
|
||||||
|
/** Every nested details table in an audit, flattened. */
|
||||||
|
function nestedItems(audit) {
|
||||||
|
const items = audit?.details?.items ?? [];
|
||||||
|
|
||||||
|
return items.flatMap((item) => (Array.isArray(item?.items) ? item.items : [item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function lcpElement(lhr) {
|
||||||
|
const item = nestedItems(lhr.audits?.['largest-contentful-paint-element']).find((entry) => entry?.node);
|
||||||
|
const snippet = item?.node?.snippet ?? item?.node?.selector ?? null;
|
||||||
|
|
||||||
|
return snippet ? snippet.replace(/\s+/g, ' ').slice(0, 160) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lcpPhases(lhr) {
|
||||||
|
return nestedItems(lhr.audits?.['largest-contentful-paint-element'])
|
||||||
|
.filter((entry) => typeof entry?.phase === 'string')
|
||||||
|
.map((entry) => ({ phase: entry.phase, timing: entry.timing ?? null }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Requests that finished before LCP, heaviest first — the contention evidence. */
|
||||||
|
function requestsBeforeLcp(lhr, lcp) {
|
||||||
|
const requests = lhr.audits?.['network-requests']?.details?.items ?? [];
|
||||||
|
|
||||||
|
return requests
|
||||||
|
.filter((request) => typeof request.networkEndTime === 'number' && (lcp === null || request.networkEndTime <= lcp + 50))
|
||||||
|
.filter((request) => (request.transferSize ?? 0) > 1024)
|
||||||
|
.sort((a, b) => (b.transferSize ?? 0) - (a.transferSize ?? 0))
|
||||||
|
.slice(0, 8)
|
||||||
|
.map((request) => ({
|
||||||
|
url: String(request.url ?? '').replace(/^https?:\/\/[^/]+/, ''),
|
||||||
|
type: request.resourceType ?? '—',
|
||||||
|
transferSize: request.transferSize ?? null,
|
||||||
|
endTime: request.networkEndTime ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Failing LH12 insights, with whatever savings estimate they carry. */
|
||||||
|
function insights(lhr) {
|
||||||
|
return Object.entries(lhr.audits ?? {})
|
||||||
|
.filter(([id, audit]) => id.endsWith('-insight') && typeof audit.score === 'number' && audit.score < 1)
|
||||||
|
.map(([id, audit]) => ({
|
||||||
|
id,
|
||||||
|
title: audit.title ?? id,
|
||||||
|
lcpSavings: audit.metricSavings?.LCP ?? null,
|
||||||
|
byteSavings: audit.details?.overallSavingsBytes ?? null,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (b.lcpSavings ?? 0) - (a.lcpSavings ?? 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
const reports = new Map();
|
||||||
|
|
||||||
|
for (const file of readdirSync(dir).filter((name) => name.endsWith('.report.json')).sort()) {
|
||||||
|
const match = /^(.+)-(mobile|desktop)-run(\d+)\.report\.json$/.exec(file);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, page, preset] = match;
|
||||||
|
const key = `${page}::${preset}`;
|
||||||
|
const lhr = JSON.parse(readFileSync(join(dir, file), 'utf8'));
|
||||||
|
|
||||||
|
if (!reports.has(key)) {
|
||||||
|
reports.set(key, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
reports.get(key).push({ file, lhr });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reports.size === 0) {
|
||||||
|
console.error(`no Lighthouse JSON reports found in ${dir}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The run whose LCP is the median — reported whole, never blended. */
|
||||||
|
function medianRun(runs) {
|
||||||
|
const sorted = [...runs].sort(
|
||||||
|
(a, b) => (numeric(a.lhr.audits?.['largest-contentful-paint']) ?? Infinity)
|
||||||
|
- (numeric(b.lhr.audits?.['largest-contentful-paint']) ?? Infinity),
|
||||||
|
);
|
||||||
|
|
||||||
|
return sorted[Math.floor((sorted.length - 1) / 2)];
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
const details = [];
|
||||||
|
|
||||||
|
for (const [key, runs] of [...reports.entries()].sort()) {
|
||||||
|
const [page, preset] = key.split('::');
|
||||||
|
const chosen = medianRun(runs);
|
||||||
|
const { lhr } = chosen;
|
||||||
|
const lcp = numeric(lhr.audits?.['largest-contentful-paint']);
|
||||||
|
const spread = runs
|
||||||
|
.map((run) => numeric(run.lhr.audits?.['largest-contentful-paint']))
|
||||||
|
.filter((value) => value !== null)
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
rows.push({
|
||||||
|
page,
|
||||||
|
preset,
|
||||||
|
performance: score(lhr.categories?.performance?.score),
|
||||||
|
accessibility: score(lhr.categories?.accessibility?.score),
|
||||||
|
bestPractices: score(lhr.categories?.['best-practices']?.score),
|
||||||
|
seo: score(lhr.categories?.seo?.score),
|
||||||
|
lcp,
|
||||||
|
cls: numeric(lhr.audits?.['cumulative-layout-shift']),
|
||||||
|
tbt: numeric(lhr.audits?.['total-blocking-time']),
|
||||||
|
fcp: numeric(lhr.audits?.['first-contentful-paint']),
|
||||||
|
ttfb: numeric(lhr.audits?.['server-response-time']),
|
||||||
|
runs: runs.length,
|
||||||
|
spread: spread.length > 1 ? [spread[0], spread[spread.length - 1]] : null,
|
||||||
|
file: chosen.file,
|
||||||
|
});
|
||||||
|
|
||||||
|
details.push({
|
||||||
|
page,
|
||||||
|
preset,
|
||||||
|
element: lcpElement(lhr),
|
||||||
|
phases: lcpPhases(lhr),
|
||||||
|
requests: requestsBeforeLcp(lhr, lcp),
|
||||||
|
insights: insights(lhr),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const first = reports.values().next().value[0].lhr;
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
out.push(`# Lighthouse — ${flags.get('target') ?? 'local'}`);
|
||||||
|
out.push('');
|
||||||
|
out.push(`- Origem: \`${flags.get('base-url') ?? '—'}\``);
|
||||||
|
out.push(`- Lighthouse: ${first.lighthouseVersion ?? '—'}`);
|
||||||
|
// Which Chrome ran the audit is part of the measurement: the stable system
|
||||||
|
// Chrome and Playwright's Chromium do not produce interchangeable numbers.
|
||||||
|
out.push(`- Navegador: \`${first.environment?.hostUserAgent ?? '—'}\``);
|
||||||
|
out.push(`- Coletado em: ${first.fetchTime ?? '—'}`);
|
||||||
|
out.push(`- Execuções por página/preset: ${rows[0]?.runs ?? '—'} (reportada a de LCP mediano)`);
|
||||||
|
|
||||||
|
if (flags.get('commit')) {
|
||||||
|
out.push(`- Commit: \`${flags.get('commit')}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags.get('seeder')) {
|
||||||
|
out.push(`- Seeder: \`${flags.get('seeder')}\``);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('');
|
||||||
|
out.push('Metas SPEC §6.6: LCP ≤ 2,5 s · CLS ≤ 0,1 · INP ≤ 200 ms · zero erro de console.');
|
||||||
|
out.push('');
|
||||||
|
out.push('| página | preset | perf | a11y | BP | SEO | LCP | FCP | CLS | TBT | TTFB servidor | LCP min–max |');
|
||||||
|
out.push('|---|---|---|---|---|---|---|---|---|---|---|---|');
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
const spread = row.spread ? `${ms(row.spread[0])} – ${ms(row.spread[1])}` : '—';
|
||||||
|
|
||||||
|
out.push(
|
||||||
|
`| ${row.page} | ${row.preset} | ${row.performance} | ${row.accessibility} | ${row.bestPractices} `
|
||||||
|
+ `| ${row.seo} | ${ms(row.lcp)} | ${ms(row.fcp)} | ${typeof row.cls === 'number' ? row.cls.toFixed(3) : '—'} `
|
||||||
|
+ `| ${typeof row.tbt === 'number' ? `${Math.round(row.tbt)} ms` : '—'} `
|
||||||
|
+ `| ${typeof row.ttfb === 'number' ? `${Math.round(row.ttfb)} ms` : '—'} | ${spread} |`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('');
|
||||||
|
out.push('## Decomposição do LCP');
|
||||||
|
|
||||||
|
for (const detail of details) {
|
||||||
|
out.push('');
|
||||||
|
out.push(`### ${detail.page} — ${detail.preset}`);
|
||||||
|
out.push('');
|
||||||
|
out.push(`Elemento de LCP: ${detail.element ? `\`${detail.element}\`` : '—'}`);
|
||||||
|
|
||||||
|
if (detail.phases.length > 0) {
|
||||||
|
out.push('');
|
||||||
|
out.push('| fase | tempo |');
|
||||||
|
out.push('|---|---|');
|
||||||
|
|
||||||
|
for (const phase of detail.phases) {
|
||||||
|
out.push(`| ${phase.phase} | ${ms(phase.timing)} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.requests.length > 0) {
|
||||||
|
out.push('');
|
||||||
|
out.push('Requests concluídas até o LCP, mais pesadas primeiro:');
|
||||||
|
out.push('');
|
||||||
|
out.push('| recurso | tipo | transferido | fim |');
|
||||||
|
out.push('|---|---|---|---|');
|
||||||
|
|
||||||
|
for (const request of detail.requests) {
|
||||||
|
out.push(`| \`${request.url}\` | ${request.type} | ${kib(request.transferSize)} | ${ms(request.endTime)} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detail.insights.length > 0) {
|
||||||
|
out.push('');
|
||||||
|
out.push('| insight com falha | ganho estimado de LCP | bytes |');
|
||||||
|
out.push('|---|---|---|');
|
||||||
|
|
||||||
|
for (const insight of detail.insights) {
|
||||||
|
out.push(`| ${insight.title} | ${ms(insight.lcpSavings)} | ${kib(insight.byteSavings)} |`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('');
|
||||||
|
|
||||||
|
console.log(out.join('\n'));
|
||||||
19
tasks.md
@@ -1,19 +0,0 @@
|
|||||||
# Fix visual regression baselines (CI-parity)
|
|
||||||
|
|
||||||
- [x] 1. Worktree `fix/visual-baselines-ci` a partir de `origin/main` (.worktrees/visual-baselines-ci @ 0aee15e)
|
|
||||||
- [x] 2. Postgres `amare_test` up (amare-postgres reutilizado, DB criado)
|
|
||||||
- [x] 3. Build `amare-app:ci` + container amare-web com env idêntica ao job browser (APP_FROZEN_NOW etc.) — /up ok
|
|
||||||
- [x] 4. Build imagem amare-ci-runner (ubuntu:24.04 + PHP 8.4 + node 22 + playwright deps)
|
|
||||||
- [x] 5. Regenerar 16 snapshots via `php artisan test --testsuite=Browser --update-snapshots` dentro do ci-runner (network host)
|
|
||||||
- [x] 6. Verificar: re-run Browser suite verde (35/35)
|
|
||||||
- [x] 7. Commit .snap, push, PR #24, CI verde; bump commonmark 2.9.0 p/ auditoria; merged
|
|
||||||
|
|
||||||
# PR #23 — cadência editorial à home (sync + merge)
|
|
||||||
|
|
||||||
- [x] 1. Reset worktree home-editorial-cadence p/ origin/feat/home-editorial-cadence (8ae5824, incl. reconcile)
|
|
||||||
- [x] 2. Merge origin/main (PR #24): resolver conflitos home snaps (theirs); 14 snaps não-home via auto-merge
|
|
||||||
- [x] 3. Rebuild amare-app:ci com código PR #23; container amare-web recriado
|
|
||||||
- [x] 4. Regenerar home desktop/mobile snaps no ci-runner (--update-snapshots); commit d9751b1
|
|
||||||
- [x] 5. Verificação: Browser suite 37 passed (156 assertions) — inclui HomeEditorialCadenceTest/MotionTest
|
|
||||||
- [x] 6. Push; CI 5/5 verde (run 31230461189)
|
|
||||||
- [x] 7. Merge squash PR #23 (commit 42b282c1); worktree/branch/remota limpos
|
|
||||||