Compare commits

...

5 Commits

Author SHA1 Message Date
manoel.neto
343fd5e197 test: restaurar baselines visuais removidos por engano na reconciliação
O commit 58f24a6 ("chore: reconcile main deployment and agent docs")
apagou oito baselines de regressão visual — home, services, portfolio e
portfolio-detail, em desktop e mobile. O VisualRegressionTest continua
declarando os oito cenários, então as quatro telas mais importantes do
site ficaram sem proteção contra regressão visual.

Os arquivos voltam exatamente como estavam em 42b282c. Aquele commit e o
atual HEAD não têm nenhuma diferença que afete renderização: 58f24a6
tocou apenas AGENTS.md, docker-compose.deploy.yml, tasks.md e a própria
remoção dos snapshots. Nenhum Blade, CSS, token ou asset mudou, então os
baselines restaurados descrevem a renderização atual.

O job browser do CI, que roda em paridade com o ambiente de geração,
é a verificação desta mudança.

Co-Authored-By: Claude noreply@anthropic.com
AI-Assisted: yes
AI-Tool: claude-code
2026-08-10 02:18:57 -03:00
58f24a6d5a chore: reconcile main deployment and agent docs
Squash merge of the validated main reconciliation changes.
2026-08-07 22:22:24 -03:00
4061662c4b chore: setup agent skills config (#25) 2026-08-07 21:48:48 -03:00
42b282c1f2 feat: adicionar cadência editorial à home (#23)
* feat: adicionar cadência editorial à home

* fix: reconcile pr23 ci with current main

* test: atualizar baselines visuais da home para cadência editorial
2026-08-07 21:40:35 -03:00
457845758c test: regenerar baselines visuais em ambiente CI-parity (#24)
* test: regenerar baselines visuais em ambiente CI-parity

* chore: bump league/commonmark para 2.9.0
2026-08-07 21:21:41 -03:00
41 changed files with 455 additions and 134 deletions

View File

@@ -41,3 +41,21 @@ History follows Conventional Commit-style subjects, for example `feat: Fase 0
## Security & Configuration ## Security & Configuration
Copy `.env.example`; never commit secrets or production credentials. Development seed credentials are local-only. Validate uploads and authorization through Laravel policies, and run `composer security-audit` after dependency changes. Copy `.env.example`; never commit secrets or production credentials. Development seed credentials are local-only. Validate uploads and authorization through Laravel policies, and run `composer security-audit` after dependency changes.
## Design Context
Amare: refined, humane, precise — Heritage Editorial (see `.impeccable.md`). Trust-first, both private + corporate audiences. Never generic wedding decor (hearts/gold/script) or AI-slop. Real proof only. For design skills, read `.impeccable.md` at project root.
## Agent skills
### Issue tracker
Issues live in Linear, driven through the Linear MCP tools. See `docs/agents/issue-tracker.md`.
### Triage labels
Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`. See `docs/agents/triage-labels.md`.
### Domain docs
Single-context: `CONTEXT.md` at root + `docs/adr/`. See `docs/agents/domain.md`.

12
composer.lock generated
View File

@@ -2802,16 +2802,16 @@
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
"version": "2.8.3", "version": "2.9.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/thephpleague/commonmark.git", "url": "https://github.com/thephpleague/commonmark.git",
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7" "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/1902f60f984235023acbe03db6ad614a37b3c3e7", "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
"reference": "1902f60f984235023acbe03db6ad614a37b3c3e7", "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@@ -2848,7 +2848,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-main": "2.9-dev" "dev-main": "2.10-dev"
} }
}, },
"autoload": { "autoload": {
@@ -2905,7 +2905,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2026-07-12T15:29:16+00:00" "time": "2026-08-03T13:42:31+00:00"
}, },
{ {
"name": "league/config", "name": "league/config",

View File

@@ -14,7 +14,11 @@ services:
restart: "no" restart: "no"
env_file: env_file:
- .env - .env
command: ["sh", "-c", "php artisan migrate --force --no-interaction && php artisan db:seed --class=ContentSeeder --force; php artisan media:generate-variants --force; true"] command: >
sh -c
"php artisan migrate --force --no-interaction
&& php artisan db:seed --class=ContentSeeder --force --no-interaction
&& php artisan media:generate-variants --force"
networks: networks:
- dokploy-network - dokploy-network

51
docs/agents/domain.md Normal file
View File

@@ -0,0 +1,51 @@
# Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Before exploring, read these
- **`CONTEXT.md`** at the repo root, or
- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic.
- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src/<context>/docs/adr/` for context-scoped decisions.
If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The `/domain-modeling` skill (reached via `/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily when terms or decisions actually get resolved.
## File structure
Single-context repo (most repos):
```
/
├── CONTEXT.md
├── docs/adr/
│ ├── 0001-event-sourced-orders.md
│ └── 0002-postgres-for-write-model.md
└── src/
```
Multi-context repo (presence of `CONTEXT-MAP.md` at the root):
```
/
├── CONTEXT-MAP.md
├── docs/adr/ ← system-wide decisions
└── src/
├── ordering/
│ ├── CONTEXT.md
│ └── docs/adr/ ← context-specific decisions
└── billing/
├── CONTEXT.md
└── docs/adr/
```
## Use the glossary's vocabulary
When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids.
If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/domain-modeling`).
## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_

View File

@@ -0,0 +1,26 @@
# Issue tracker: Linear
Issues and specs for this repo live in Linear. Use the Linear MCP tools for all operations.
- **Workspace**: maneco-workspace (https://linear.app/maneco-workspace)
- **Team**: Maneco-workspace
- **Issue statuses**: Backlog, Todo, In Progress, Done, Canceled, Duplicate
## Conventions
- **Create an issue**: `save_issue` with `title`, `team` (`Maneco-workspace`), and markdown `description`. Assign via `assignee` ("me" for the current user).
- **Read an issue**: `get_issue` by identifier (e.g. `LIN-123`).
- **List issues**: `list_issues` filtered by `team`, `assignee`, `state`, or `project`.
- **Comment on an issue**: `save_comment` with `issueId` and markdown `body`.
- **Apply / remove labels**: `save_issue` with `labels` (replaces the full set).
- **Move workflow state**: `save_issue` with `state` (e.g. `Todo`, `In Progress`, `Done`, `Canceled`).
- **Link work**: `save_issue` with `project`, `cycle`, `parentId`, `blocks` / `blockedBy` (relations are append-only).
- **Cross-link to a PR**: attach the PR URL via `save_issue` `links`.
## When a skill says "publish to the issue tracker"
Create a Linear issue with `save_issue` on team `Maneco-workspace`.
## When a skill says "fetch the relevant ticket"
Run `get_issue` on the identifier and read the description, state, labels, and assignee.

View File

@@ -0,0 +1,15 @@
# Triage Labels
The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.
| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table.
Edit the right-hand column to match whatever vocabulary you actually use.

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

View File

@@ -109,6 +109,43 @@
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);

View File

@@ -1,10 +1,18 @@
@props([ @props([
'settings', 'settings',
'editorial' => false,
]) ])
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20" data-chapter="final-cta" data-reveal-group> <section aria-labelledby="final-cta-heading" @class([
'border-t border-amare-border bg-amare-bg-deep py-20',
'home-chapter' => $editorial,
]) 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 space-y-6 text-center" data-reveal data-reveal-from="up">
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p> @if ($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>
@endif
<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="mx-auto 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.

View File

@@ -0,0 +1,20 @@
@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>

View File

@@ -4,11 +4,15 @@
<section <section
aria-labelledby="hero-heading" aria-labelledby="hero-heading"
class="border-b border-amare-border bg-amare-bg" class="home-chapter border-b border-amare-border bg-amare-bg"
data-chapter="hero" data-chapter="hero"
data-motion="page-open" data-motion="page-open"
> >
<div class="container-amare grid gap-12 py-20 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:py-28" data-reveal-group> <div class="container-amare pt-8 md:pt-10">
<x-home.folio label="Capa" data-motion-beat="folio" />
</div>
<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>
<div class="space-y-8"> <div class="space-y-8">
<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="" />

View File

@@ -8,9 +8,13 @@
$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="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-20" 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-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal-group>
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent" data-reveal data-reveal-from="up">Manifesto</p> <div class="space-y-5 md:pr-8" data-reveal data-reveal-from="up">
<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>
</div>
<div class="space-y-6" data-reveal data-reveal-from="up"> <div class="space-y-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>

View File

@@ -7,12 +7,19 @@
$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="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-20" 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-3" data-reveal data-reveal-from="up"> <div class="space-y-4" data-reveal data-reveal-from="up">
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Método</p> <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
mark
variant="on-light"
class="mt-8 h-8 w-auto opacity-60"
alt=""
aria-hidden="true"
/>
</div> </div>
<ol class="grid gap-5 border-t border-amare-border"> <ol class="grid gap-5 border-t border-amare-border">

View File

@@ -3,10 +3,10 @@
]) ])
@if ($cases->isNotEmpty()) @if ($cases->isNotEmpty())
<section aria-labelledby="portfolio-heading" class="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-20 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-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal data-reveal-from="up">
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent-text/80">Portfólio</p> <x-home.folio label="Portfólio" tone="inverse" />
<div class="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>

View File

@@ -7,10 +7,14 @@
$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="border-b border-amare-border py-20" data-chapter="positioning"> <section aria-labelledby="positioning-heading" class="home-chapter border-b border-amare-border py-20" 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-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal-group>
<div class="space-y-3" data-reveal data-reveal-from="up"> <div class="space-y-4" data-reveal data-reveal-from="up">
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">A Amare</p> <x-home.folio label="A Amare" />
<div class="flex items-center gap-4">
<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>
</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" data-reveal data-reveal-from="up">

View File

@@ -3,12 +3,17 @@
]) ])
@if ($services->isNotEmpty()) @if ($services->isNotEmpty())
<section aria-labelledby="services-heading" class="border-b border-amare-border py-20" data-chapter="services"> <section aria-labelledby="services-heading" class="home-chapter border-b border-amare-border py-20" 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="max-w-2xl space-y-3" data-reveal data-reveal-from="up"> <div class="grid gap-6 md:grid-cols-12 md:items-end" data-reveal data-reveal-from="up">
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Atuação</p> <div class="space-y-3 md:col-span-4">
<h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2> <x-home.folio label="Serviços" />
<p class="text-amare-text-muted">Assessoria sob medida para decisões importantes e celebrações bem conduzidas.</p> <h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2>
</div>
<div class="space-y-4 md:col-span-6 md:col-start-7">
<p class="max-w-xl text-amare-text-muted">Assessoria sob medida para decisões importantes e celebrações bem conduzidas.</p>
<p class="border-l border-amare-border pl-4 text-sm uppercase tracking-[0.12em] text-amare-accent">Do íntimo ao corporativo</p>
</div>
</div> </div>
<ol class="border-t border-amare-border"> <ol class="border-t border-amare-border">

View File

@@ -9,9 +9,10 @@
@endphp @endphp
@if ($testimonials->isNotEmpty()) @if ($testimonials->isNotEmpty())
<section aria-labelledby="testimonials-heading" class="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-16" data-chapter="testimonials">
<div class="container-amare space-y-8" data-reveal-group> <div class="container-amare space-y-8" 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" 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>
@@ -23,12 +24,15 @@
$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" data-reveal data-reveal-from="{{ $loop->odd ? 'left' : 'right' }}">
<div class="space-y-3 text-lg text-amare-text"> <div class="grid grid-cols-[1.5rem_minmax(0,1fr)] gap-2">
@foreach ($paragraphs as $index => $paragraph) <span class="text-3xl leading-none text-amare-sage" aria-hidden="true"></span>
<p>@if ($index === 0)@endif{{ $paragraph }}@if ($index === count($paragraphs) - 1)@endif</p> <div class="space-y-3 text-lg text-amare-text">
@endforeach @foreach ($paragraphs as $paragraph)
<p>{{ $paragraph }}</p>
@endforeach
</div>
</div> </div>
<footer class="text-sm text-amare-text-muted"> <footer class="pl-8 text-sm text-amare-text-muted">
<cite class="not-italic font-semibold text-amare-text">{{ $testimonial->author_name }}</cite> <cite class="not-italic font-semibold text-amare-text">{{ $testimonial->author_name }}</cite>
@if (filled($testimonial->context)) @if (filled($testimonial->context))
<span> {{ $testimonial->context }}</span> <span> {{ $testimonial->context }}</span>

View File

@@ -31,12 +31,14 @@
<x-home.chapter-index :chapters="$chapters" /> <x-home.chapter-index :chapters="$chapters" />
<x-home.hero :settings="$content->settings" /> <div class="home-chapters">
<x-home.manifesto :settings="$content->settings" /> <x-home.hero :settings="$content->settings" />
<x-home.services :services="$content->featuredServices" /> <x-home.manifesto :settings="$content->settings" />
<x-home.portfolio :cases="$content->featuredCases" /> <x-home.services :services="$content->featuredServices" />
<x-home.method :settings="$content->settings" /> <x-home.portfolio :cases="$content->featuredCases" />
<x-home.testimonials :testimonials="$testimonials" /> <x-home.method :settings="$content->settings" />
<x-home.positioning :settings="$content->settings" /> <x-home.testimonials :testimonials="$content->testimonials" />
<x-home.final-cta :settings="$content->settings" /> <x-home.positioning :settings="$content->settings" />
<x-home.final-cta :settings="$content->settings" editorial />
</div>
@endsection @endsection

View File

@@ -1,79 +1,19 @@
# Frontend Audit — Amare site # Fix visual regression baselines (CI-parity)
## T0 — Baseline - [x] 1. Worktree `fix/visual-baselines-ci` a partir de `origin/main` (.worktrees/visual-baselines-ci @ 0aee15e)
- [x] Rodar `composer quality` p/ registrar estado verde atual (110 passed após composer install) - [x] 2. Postgres `amare_test` up (amare-postgres reutilizado, DB criado)
- [x] Registrado estado baseline; Lighthouse final consolidado no T6 - [x] 3. Build `amare-app:ci` + container amare-web com env idêntica ao job browser (APP_FROZEN_NOW etc.) — /up ok
- [x] Registrar achados baseline (audit completo em frontend-audit.md + relatório subagent) - [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
## T1 — Formulário de contato (e-mail, sem CRM) # PR #23 — cadência editorial à home (sync + merge)
- [x] Form em /contato (campos, LGPD obrigatório)
- [x] Rota POST /contato + validação + honeypot + throttle
- [x] Anti duplo-envio, estados sucesso/erro/rede, preservação de dados
- [x] E-mail via MAIL_MAILER (Resend prod / log local)
- [x] A11y do form (labels, erros ligados)
- [x] Testes feature + atualizar PublicPagesTest — Feature suite: 97 passed
## T2 — A11y/navegação - [x] 1. Reset worktree home-editorial-cadence p/ origin/feat/home-editorial-cadence (8ae5824, incl. reconcile)
- [x] Menu mobile: Escape, retorno de foco, fallback sem-JS (app.js + noscript) - [x] 2. Merge origin/main (PR #24): resolver conflitos home snaps (theirs); 14 snaps não-home via auto-merge
- [x] Touch targets ≥44px (header, footer, CTAs, capítulos) - [x] 3. Rebuild amare-app:ci com código PR #23; container amare-web recriado
- [x] Footer: ul/li + address - [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
## T3 — SEO/metadados - [x] 6. Push; CI 5/5 verde (run 31230461189)
- [x] Títulos "Página · Amare" (PageMeta::withBrandSuffix) - [x] 7. Merge squash PR #23 (commit 42b282c1); worktree/branch/remota limpos
- [x] og:locale, og:site_name, Twitter cards
- [x] Favicon real + link, theme-color
- [x] Metadados corretos em 404/500 (PageMeta::forErrorPage)
- [x] noindex fora de produção (robots meta condicional)
## T4 — Conteúdo/posicionamento
- [x] Remover welcome.blade.php, home/cases, home/proof
- [x] Dedup heading final-CTA vs h1 contato; dedup disclaimers
- [x] CTA consistente mobile/desktop
- [x] Corrigir copy "Em breve..." nas listas
- [x] Rebalancear copy corporativo/social
## T5 — Performance
- [x] fetchpriority + dimensões hero (LCP)
- [x] Guarda CLS no media/image
- [x] Cache manifest de fontes
- [x] Escape JSON-LD
## T6 — Validação final
- [x] pint, phpstan, testes unit/feature/architecture — `composer quality` verde (135 testes)
- [x] Browser tests + visual regression (18 passaram; 8 baselines `.snap` atualizadas com `--update-snapshots`)
- [x] Lighthouse final (home mobile: A11y 100, Best Practices 100, SEO 69 só por noindex intencional fora de prod; contato desktop: A11y 100, BP 100)
- [x] E2E form: submit → sucesso role=status; 2 e-mails (novo briefing + confirmação) logados via MAIL_MAILER=log; fila drenada
- [x] Mobile: sem overflow horizontal nas 7 rotas (390px); menu mobile abre/fecha + Escape + retorno de foco
- [x] Relatório de entrega
## T7 — Hardening Public Blades
- [x] Chunk 1: CSS overflow resilience (`overflow-wrap` body + `[data-chapter-index]` wrap safety, browser test)
- [x] Chunk 2: Hero + final-CTA fallbacks + `leading-tight` (TDD)
- [x] Chunk 3a: Skip blank-quote testimonials (TDD)
- [x] Chunk 3b: Guard empty `event_type` + `break-words` meta line (TDD)
- [x] Chunk 4: Extend `PageMeta::forErrorPage` + branded 419/429/503 error views (TDD)
- [x] Chunk 5: `app.js` defensive hardening — bfcache submit reset, "Enviando…" label swap, aria-busy, mobile-menu focus trap + Escape
- [x] Chunk 6: Quality gates — Pint passed, PHPStan clean, 104 feature tests passed (587 assertions), Vite build succeeded
### Deviations from plan
- `PageMeta::forErrorPage` landed on `main` during implementation; rebased and extended it for 419/429/503.
- Contact form landed on `main`; submit-state browser coverage was restored.
- `event_type: null` violates NOT NULL column — test uses `''` instead (guard covers both).
# Browser CI Gate — estabilidade e diagnosticabilidade
## BG — Causa raiz
- [x] Diagnosticar 3 runs: mismatch visual (8 testes), acessibilidade (1), timeout networkidle 5s + overflow real
- [x] Reproduzir local: `waitForLoadState('networkidle')` fragil (conexões longas), default 5s
- [x] Reproduzir warning `file_get_contents(.env)` em TODO teste (CI sem .env)
## BG — Fixes
- [x] Helper `StableScreenshot` (tests/Support/): drop networkidle → readyState + fonts + settle
- [x] Exportar PNGs standalone (diff/expected/actual) no mismatch — visíveis nos artifacts
- [x] CI: `cp .env.example .env` nos jobs de teste; checkout/cache v5
- [x] Gitignore tests/Browser/Screenshots
- [x] Unit test MismatchScreenshotExporter; pint/phpstan/unit/feature verdes
- [x] Push + PR + CI browser verde (PR #15 merge f09ea83, run 31098941038)
- [x] Refresh baselines de CI se necessário (não necessário — browser passou com baselines atuais)

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
use Database\Seeders\VisualContentSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
uses(RefreshDatabase::class);
beforeEach(function (): void {
Artisan::call('db:seed', ['--class' => VisualContentSeeder::class, '--force' => true]);
});
it('keeps the full home cadence accessible and contained at both viewports', function (): void {
foreach ([[1440, 1000], [390, 844]] as [$width, $height]) {
$page = $this->visit('/', [
'reducedMotion' => 'reduce',
])->resize($width, $height);
$page->assertNoAccessibilityIssues(1);
$layout = $page->script(<<<'JS'
() => {
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
const clippedValues = new Set(['clip', 'hidden']);
const textNodes = Array.from(document.querySelectorAll(
'.home-chapters h1, .home-chapters h2, .home-chapters h3, .home-chapters p, .home-chapters a, .home-chapters span'
));
return {
horizontalOverflow: document.documentElement.scrollWidth > window.innerWidth,
overlappingChapters: chapters.slice(1).some((chapter, index) => {
const previous = chapters[index].getBoundingClientRect();
const current = chapter.getBoundingClientRect();
return current.top < previous.bottom - 1;
}),
overflowingFolios: folios.filter((folio) => {
const rect = folio.getBoundingClientRect();
return rect.left < -1 || rect.right > window.innerWidth + 1;
}).length,
clippedText: textNodes.filter((node) => {
const style = getComputedStyle(node);
const clippedX = clippedValues.has(style.overflowX) && node.scrollWidth > node.clientWidth + 1;
const clippedY = clippedValues.has(style.overflowY) && node.scrollHeight > node.clientHeight + 1;
return clippedX || clippedY;
}).length,
};
}
JS);
expect($layout)->toBe([
'horizontalOverflow' => false,
'overlappingChapters' => false,
'overflowingFolios' => 0,
'clippedText' => 0,
]);
}
});

View File

@@ -302,3 +302,48 @@ it('updates the active chapter while scrolling the home dossier', function (): v
expect($before)->not->toBeNull(); expect($before)->not->toBeNull();
expect($after)->toBe('#method-heading'); expect($after)->toBe('#method-heading');
}); });
it('applies CSS numbering to only rendered home folios in chapter order', function (): void {
PortfolioCase::factory()->published()->create([
'is_featured' => true,
]);
Testimonial::factory()->published()->create([
'quote' => 'Relato publicado.',
]);
$page = $this->visit('/', [
'reducedMotion' => 'reduce',
]);
$numbering = $page->script(<<<'JS'
() => {
const flow = document.querySelector('.home-chapters');
const chapters = Array.from(document.querySelectorAll('.home-chapter'));
const folios = Array.from(document.querySelectorAll('[data-home-folio]'));
return {
reset: flow ? getComputedStyle(flow).counterReset : '',
increments: chapters.map((chapter) => getComputedStyle(chapter).counterIncrement),
labels: folios.map((folio) => folio.querySelector('[data-home-folio-label]')?.textContent?.trim() ?? ''),
contents: folios.map((folio) => {
const number = folio.querySelector('[data-home-folio-number]');
return number ? getComputedStyle(number, '::before').content.replace(/["']/g, '') : '';
}),
};
}
JS);
expect($numbering['reset'])->toContain('home-chapter -1');
expect($numbering['increments'])->each->toContain('home-chapter 1');
expect($numbering['labels'])->toBe([
'Capa',
'Manifesto',
'Serviços',
'Portfólio',
'Método',
'Depoimentos',
'A Amare',
'Próximo passo',
]);
expect($numbering['contents'])->each->toBe('counter(home-chapter, decimal-leading-zero)');
});

View File

@@ -51,7 +51,8 @@ foreach ($screens as $screen => $path) {
]) ])
->withLocale('pt-BR') ->withLocale('pt-BR')
->withTimezone('America/Fortaleza') ->withTimezone('America/Fortaleza')
->resize($width, $height) ->resize($width, $height),
resetScroll: $path === '/',
); );
}); });
} }

View File

@@ -197,4 +197,65 @@ class HomePageContentTest extends TestCase
->assertDontSee('href="#testimonials-heading"', false) ->assertDontSee('href="#testimonials-heading"', false)
->assertDontSee('Relato vazio'); ->assertDontSee('Relato vazio');
} }
public function test_home_folios_follow_rendered_chapter_order_and_hide_decorative_numbers(): void
{
SiteSetting::instance();
Service::factory()->published()->featured()->create();
PortfolioCase::factory()->published()->create(['is_featured' => true]);
Testimonial::factory()->published()->create(['quote' => 'Relato publicado.']);
$html = $this->get(route('home'))->assertOk()->getContent();
$this->assertSame([
'Capa',
'Manifesto',
'Serviços',
'Portfólio',
'Método',
'Depoimentos',
'A Amare',
'Próximo passo',
], $this->folioLabels((string) $html));
$this->assertSame(8, preg_match_all('/data-home-folio-number[^>]*aria-hidden="true"/i', (string) $html));
$this->assertSame(1, preg_match_all('/<h1\b/i', (string) $html));
$css = (string) file_get_contents(resource_path('css/app.css'));
$this->assertStringContainsString('counter-reset: home-chapter -1', $css);
$this->assertStringContainsString('counter-increment: home-chapter', $css);
$this->assertStringContainsString('counter(home-chapter, decimal-leading-zero)', $css);
}
public function test_home_folios_are_omitted_with_conditional_chapters_without_leaving_markup_gaps(): void
{
SiteSetting::instance();
$html = $this->get(route('home'))->assertOk()->getContent();
$this->assertSame([
'Capa',
'Manifesto',
'Método',
'A Amare',
'Próximo passo',
], $this->folioLabels((string) $html));
$this->assertSame(5, preg_match_all('/data-home-folio-number[^>]*aria-hidden="true"/i', (string) $html));
}
/** @return list<string> */
private function folioLabels(string $html): array
{
preg_match_all(
'/<span\b[^>]*data-home-folio-label[^>]*>\s*([^<]+?)\s*<\/span>/is',
$html,
$matches,
);
return array_values(array_map(
static fn (string $label): string => trim(html_entity_decode($label)),
$matches[1],
));
}
} }

View File

@@ -22,7 +22,7 @@ trait StableScreenshot
* images, so this helper additionally exports standalone diff/expected/actual * images, so this helper additionally exports standalone diff/expected/actual
* PNG files that are directly viewable in CI artifacts. * PNG files that are directly viewable in CI artifacts.
*/ */
public function assertStableScreenshotMatches(AwaitableWebpage $awaitable): void public function assertStableScreenshotMatches(AwaitableWebpage $awaitable, bool $resetScroll = false): void
{ {
$page = $awaitable->page(); $page = $awaitable->page();
@@ -66,6 +66,11 @@ trait StableScreenshot
} }
JS); JS);
if ($resetScroll) {
$page->evaluate('window.scrollTo(0, 0)');
$page->waitForFunction('window.scrollY === 0');
}
usleep(300_000); usleep(300_000);
try { try {