Compare commits

..

4 Commits

Author SHA1 Message Date
444153bf3d fix: assert font provider via import, not bunny( substring (MAN-139)
All checks were successful
CI / static (pull_request) Successful in 2m37s
CI / unit (pull_request) Successful in 3m55s
CI / feature (pull_request) Successful in 2m59s
CI / container (pull_request) Successful in 1m12s
CI / browser (pull_request) Successful in 5m40s
Comment in vite.config.js still said bunny(), so assertStringNotContainsString failed in unit coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:47:52 -03:00
290585d713 fix: use single quotes in fontsource Vite assertion (MAN-139)
Some checks failed
CI / static (pull_request) Successful in 2m37s
CI / unit (pull_request) Failing after 3m50s
CI / container (pull_request) Has been cancelled
CI / browser (pull_request) Has been cancelled
CI / feature (pull_request) Has been cancelled
Pint failed the static CI job on bunny( with double quotes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:34:22 -03:00
8965a7c2fd fix: build EB Garamond from fontsource instead of Bunny (MAN-139)
Some checks failed
CI / static (pull_request) Failing after 1m59s
CI / browser (pull_request) Has been cancelled
CI / container (pull_request) Has been cancelled
CI / feature (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
Vite was fetching fonts.bunny.net at build time; the feature CI job timed out on that host.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:27:38 -03:00
8decba693a feat: polir delight editorial no site público (MAN-139)
Some checks failed
CI / static (pull_request) Successful in 3m52s
CI / unit (pull_request) Successful in 4m54s
CI / feature (pull_request) Failing after 2m11s
CI / container (pull_request) Successful in 5m17s
CI / browser (pull_request) Successful in 7m25s
Confirmação de briefing/parceria, micro-feedback de campos e nav, e copy mais humana nos erros — sem quebrar reduced-motion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:13:09 -03:00
35 changed files with 351 additions and 322 deletions

View File

@@ -79,4 +79,4 @@ Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-
### Domain docs
Single-context repo. There is no `CONTEXT.md` on purpose — **`SPEC.md` §8.0 is the ubiquitous-language SSOT** (domain model/schema continue in §8.1+). `PRODUCT.md` covers positioning; capabilities live under `openspec/specs/`. `docs/adr/README.md` is an index only: ADR-001 through ADR-010 are decided in `SPEC.md` §21. `docs/agents/domain.md` tells skills to use `SPEC.md` §8 instead of inventing a parallel `CONTEXT.md`.
Single-context repo. There is no `CONTEXT.md` — the domain is documented in `SPEC.md` (§8 is the domain model and database schema) and `PRODUCT.md`, with current capabilities described per-capability under `openspec/specs/`. `docs/adr/README.md` is an index only: ADR-001 through ADR-010 are decided in `SPEC.md` §21, and there are no standalone ADR files. `docs/agents/domain.md` describes the generic `CONTEXT.md`/`CONTEXT-MAP.md` layout that the engineering skills look for and instructs them to proceed silently when it's absent, which is the case here.

View File

@@ -67,7 +67,7 @@ Site-wide content is a singleton row reached via `SiteSetting::instance()`. Publ
- **Livewire/Filament temp uploads are pinned to the `local` disk** when `FILESYSTEM_DISK=r2`, because the S3 driver would make the browser PUT straight to R2 and hit CORS. Final media still lands on `r2` via `App\Support\PublicImageUploadRules`. Set `LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK` explicitly to override.
- **Contact form is rate limited**: named limiter `contact-briefing`, 5/min per IP, registered in `AppServiceProvider` and applied in `routes/web.php`.
- **Filament 5 nested resource layout**: resources are split into `app/Filament/Resources/<Resource>/{Pages,Schemas,Tables,RelationManagers}` rather than a flat resource class. Follow the existing shape in `Resources/PortfolioCases/`.
- **Everything user-facing is pt-BR**: routes are `/servicos`, `/pacotes/{slug}`, `/portfolio`, `/portfolio/{slug}`, `/sobre`, `/briefing`, `/contato`, `/privacidade`. `APP_LOCALE=pt_BR`, `APP_TIMEZONE=America/Sao_Paulo` (`config/app.php:68`). Glossário canônico: `SPEC.md` §8.0.
- **Everything user-facing is pt-BR**: routes are `/servicos`, `/portfolio`, `/portfolio/{slug}`, `/sobre`, `/privacidade`, `/contato`. `APP_LOCALE=pt_BR`, `APP_TIMEZONE=America/Sao_Paulo` (`config/app.php:68`).
- **Design tokens** live in `resources/css/tokens.css` (Heritage Editorial; see `DESIGN.md`). `tests/Feature/PublicSite/HeritageEditorialTokensTest.php` reads that file and asserts the exact hex values, `EB Garamond`, zero border radii, `--amare-container-max: 1120px`, and the *absence* of shadow tokens — so any token edit is a deliberate test change too. Motion lives in `resources/js/motion.js` and is asserted by `tests/Feature/PublicSite/MotionMarkupTest.php` + `tests/Browser/MotionTest.php`.
## Navigating the normative docs

View File

@@ -37,7 +37,7 @@ No lançamento, o site público premium e o CMS interno formam a superfície ent
## Operating Context
- Atuação em São Paulo, capital.
- Serviços: casamentos, eventos corporativos e celebrações. No site, casamento aparece como três **modalidades** (Essenza, Conduzione, Grand Jour); preço e limites comerciais ficam na proposta, não na página.
- Serviços: casamentos, eventos corporativos e celebrações.
- Operação realizada por assessora principal e equipe pequena.
- 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.

69
SPEC.md
View File

@@ -283,15 +283,11 @@ Uma funcionalidade fora do MVP só poderá entrar quando:
| Método | Rota | Nome sugerido | Finalidade |
|---|---|---|---|
| GET | `/` | `home` | Home editorial |
| GET | `/servicos` | `services.index` | Lista de serviços / modalidades publicadas |
| GET | `/pacotes/{slug}` | `packages.show` | Detalhe de uma modalidade de casamento |
| GET | `/servicos` | `services.index` | Lista de serviços publicados |
| GET | `/portfolio` | `portfolio.index` | Lista de casos publicados |
| GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso |
| GET | `/sobre` | `about` | História, método e credenciais |
| GET | `/briefing` | `briefing` | Briefing comercial (pedido de proposta) |
| POST | `/briefing` | `briefing.store` | Envio do briefing comercial |
| GET | `/contato` | `contact` | Contato de parceiro / fornecedor (não é briefing) |
| POST | `/contato` | `contact.store` | Envio da consulta de parceiro |
| GET | `/contato` | `contact` | Briefing de contato |
| GET | `/privacidade` | `privacy` | Política de privacidade |
| GET | `/sitemap.xml` | `sitemap` | Sitemap público |
| GET | `/robots.txt` | `robots` | Política de crawling |
@@ -328,8 +324,7 @@ Administração
- Dashboard: `Filament Page` customizada com widgets orientados a exceção.
- Detalhe do evento: página customizada do Resource com resumo operacional.
- Briefing comercial: Blade + Controller (`POST /briefing`), ver §11.2.
- Contato de parceiro: Blade + Controller (`POST /contato`) — canal separado do briefing.
- Briefing público: Blade + Controller (`POST /contato`), ver §11.2.
- Home: Blade com componentes de design reutilizáveis.
### 5.4 Decisões de UX YAGNI
@@ -1248,35 +1243,6 @@ Auditoria não precisa ser um event sourcing. Registrar apenas operações crít
## 8. Modelo de domínio e banco de dados
Esta seção é a **fonte única de verdade** do vocabulário de domínio do produto. Issues Linear, OpenSpec, código e copy devem usar estes termos. Não criar glossário paralelo em `CONTEXT.md`.
### 8.0 Linguagem ubíqua (glossário)
**Vertente**:
Frente de marca da Amare — **Casamentos** ou **Corporate**.
_Avoid_: linha de negócio, vertical de produto (em copy pública), “área”
**Modalidade**:
Pacote/formato de acompanhamento de casamento gerenciado no CMS (`WeddingPackage`: Essenza, Conduzione, Grand Jour).
_Avoid_: pacote (em copy pública preferir “modalidade”), serviço genérico, plano
**Briefing**:
Pedido comercial de um potencial cliente descrevendo o evento e solicitando atendimento/proposta. Rota canônica: `/briefing`.
_Avoid_: contato (quando o sentido é pedido comercial), formulário de parceiro
**Contato parceiro**:
Consulta de fornecedor ou proposta de parceria. Rota canônica: `/contato`. Não mistura com Briefing.
_Avoid_: briefing, lead comercial
**CTA contextual**:
Link de conversão de uma Modalidade escolhida — WhatsApp com mensagem que nomeia a modalidade, ou fallback para `/briefing?servico_interesse=` quando não há número oficial válido.
**CTA orientação**:
Link de conversão para visitante que ainda não escolheu Modalidade — WhatsApp com mensagem de orientação humana (sem quiz), ou fallback para `/briefing` quando não há número oficial válido.
**Boutique**:
Conceito de posicionamento (cuidado, especialização, múltiplos serviços sob a mesma marca) — não é entidade de domínio nem rota.
### 8.1 Módulos
| Módulo | Responsabilidade |
@@ -1288,12 +1254,6 @@ Conceito de posicionamento (cuidado, especialização, múltiplos serviços sob
| Finance | Orçamento e pagamentos manuais |
| Identity | Usuários, acesso e auditoria |
Vocabulário público vs persistência (Marketing):
- **Modalidade** é o nome público de cada oferta de acompanhamento de casamento. O visitante vê o nome próprio (Essenza, Conduzione, Grand Jour) e a tag de momento (Completa, Parcial, Final). Persistida em `wedding_packages`.
- **Vertente** é o contexto da marca (Casamentos vs Corporate). A tabela `services` descreve vertentes do catálogo, não as três modalidades.
- **Pacote**, no sentido de SKU com preço e limites operacionais, não é superfície pública. Preço, equipe no dia, teto de fornecedores, quilometragem e ferramentas de terceiros (incluindo gestão de convidados) ficam na proposta, não no registro público da modalidade.
### 8.2 Tabelas obrigatórias
#### `users`
@@ -1354,27 +1314,6 @@ Singleton:
- timestamps;
- soft delete opcional somente se recuperação for necessária.
#### `wedding_packages`
Modalidades públicas de casamento (não confundir com `services`):
- `id`;
- `name`;
- `slug` unique;
- `level` (rótulo de momento, ex. `01 / COMPLETA`);
- `tag` nullable (ex. Assessoria completa);
- `subtitle` nullable;
- `summary`;
- `scope_items` jsonb (bullets de conceito nos cards);
- `cta_label`;
- `compare_heading` nullable;
- `compare_summary` nullable;
- campos editoriais da página `/pacotes/{slug}` (eyebrow, título, hero, benefícios, incluso em alto nível, audiência, CTA final, meta) — todos opcionais salvo o que a publicação exigir;
- `sort_order` integer indexed;
- `published_at` timestamp nullable indexed;
- timestamps;
- **sem** colunas de preço, centavos, equipe, limites de fornecedores/convidados/km ou ferramentas de terceiros.
#### `portfolio_cases`
- `id`;
@@ -1856,7 +1795,7 @@ Não transformar seções estáticas em componentes Livewire. Usar Blade quando
### 11.2 Formulário de briefing
> **Estado atual:** o formulário de **Briefing** é Blade + Controller (`GET/POST /briefing`, `ContactBriefingRequest`), conforme WEB-05. A rota `/contato` é o canal de **Contato parceiro** (consulta não persistente), separado do briefing. A criação de Lead comercial persistente segue para a Fase 2.
> **Estado atual:** o formulário é implementado em Blade + Controller (`POST /contato`, `ContactBriefingRequest`), conforme WEB-05, e essa é a abordagem aceita — não um estágio provisório. Os requisitos abaixo valem independentemente da tecnologia; a criação de Lead segue para a Fase 2.
O formulário deve:

View File

@@ -8,45 +8,20 @@ use App\Models\SiteSetting;
final class PackageContactLink
{
public const ORIENTATION_MESSAGE = 'Olá, ainda não sei qual modalidade de assessoria faz sentido para o meu casamento e gostaria de orientação.';
/**
* Resolve the contextual contact link for a wedding package modality.
*
* @return array{href: string, isWhatsapp: bool}
*/
public static function for(SiteSetting $settings, string $packageName): array
{
return self::resolve(
$settings,
'Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.',
['servico_interesse' => $packageName],
);
}
/**
* WhatsApp (or briefing fallback) when the visitor does not know which modality to choose.
*
* @return array{href: string, isWhatsapp: bool}
*/
public static function forOrientation(SiteSetting $settings): array
{
return self::resolve($settings, self::ORIENTATION_MESSAGE, []);
}
/**
* @param array<string, string> $briefingQuery
* @return array{href: string, isWhatsapp: bool}
*/
private static function resolve(SiteSetting $settings, string $whatsappMessage, array $briefingQuery): array
{
$digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
$isWhatsapp = strlen($digits) >= 10;
return [
'href' => $isWhatsapp
? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappMessage)
: route('briefing', $briefingQuery),
? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.')
: route('briefing', ['servico_interesse' => $packageName]),
'isWhatsapp' => $isWhatsapp,
];
}

View File

@@ -9,10 +9,11 @@ use Illuminate\Database\Seeder;
use Illuminate\Support\Carbon;
/**
* Wedding package modalities shown on the home page, services page and
* `/pacotes/{slug}`. Public copy follows "Apresentação dos serviços Amare"
* (conceito e momento). Commercial limits, price and third-party tools stay
* off this seeder and off the public site.
* Wedding package modalities shown on the home page (03 Amare Casamentos)
* and the services page. Copy is approved from the structural model preview
* (preview(2).html) and remains editable via the Filament resource. Detail
* copy follows the complete-assessoria preview
* (amare-assessoria-completa.html) adapted to each modality.
*/
class WeddingPackagesSeeder extends Seeder
{
@@ -26,147 +27,141 @@ class WeddingPackagesSeeder extends Seeder
'slug' => 'essenza',
'level' => '01 / COMPLETA',
'tag' => 'Assessoria completa',
'subtitle' => 'Do primeiro planejamento ao grande dia.',
'summary' => 'Ideal para casais que desejam contar com a Amare desde o início da organização ou que ainda estão estruturando etapas importantes do casamento.',
'subtitle' => 'Do planejamento ao grande dia.',
'summary' => 'Para casais que desejam contar com a Amare desde o início ou ainda estão estruturando etapas importantes.',
'scope_items' => [
'Estruturação e acompanhamento do planejamento',
'Organização de cronogramas, prazos e prioridades',
'Orientação na contratação e gestão de fornecedores',
'Acompanhamento das etapas do projeto',
'Organização e conferência de contratos e informações',
'Alinhamento entre casal, fornecedores e demais envolvidos',
'Construção do cronograma do grande dia',
'Coordenação e acompanhamento da execução do evento',
'Estruturação do planejamento',
'Cronogramas, prazos e prioridades',
'Orientação e gestão de fornecedores',
'Alinhamento entre os envolvidos',
'Coordenação da execução',
],
'cta_label' => 'Quero conhecer a Essenza',
'compare_heading' => 'Começar com a Amare',
'compare_summary' => 'Acompanhamento do primeiro planejamento ao grande dia.',
'compare_summary' => 'Acompanhamento mais amplo ao longo do planejamento.',
'sort_order' => 1,
'eyebrow' => 'Assessoria',
'title_line' => 'Assessoria',
'title_emphasis' => 'Completa',
'hero_lead' => 'A assessoria completa acompanha o projeto de forma estratégica e próxima, desde a construção do planejamento até a execução do evento.',
'hero_lead' => 'Do planejamento ao grande dia, com calma, precisão e cuidado.',
'benefits' => [
['icon_key' => 'calendar', 'label' => 'Planejamento organizado'],
['icon_key' => 'checklist', 'label' => 'Decisões mais seguras'],
['icon_key' => 'users', 'label' => 'Fornecedores alinhados'],
['icon_key' => 'heart', 'label' => 'Grande dia com leveza'],
['icon_key' => 'calendar', 'label' => 'Organização integral'],
['icon_key' => 'checklist', 'label' => 'Planejamento personalizado'],
['icon_key' => 'users', 'label' => 'Fornecedores selecionados'],
['icon_key' => 'heart', 'label' => 'Experiência leve e inesquecível'],
],
'included_items' => [
['icon_key' => 'checklist', 'title' => 'Estruturação do planejamento', 'description' => 'Organização do projeto desde o início, no ritmo do casal.'],
['icon_key' => 'calendar', 'title' => 'Cronogramas e prioridades', 'description' => 'Prazos e etapas visíveis para decidir com calma.'],
['icon_key' => 'map', 'title' => 'Contratação e gestão de fornecedores', 'description' => 'Orientação e acompanhamento de quem entra no projeto.'],
['icon_key' => 'users', 'title' => 'Alinhamento entre envolvidos', 'description' => 'Casal, fornecedores e demais partes na mesma conversa.'],
['icon_key' => 'spark', 'title' => 'Contratos e informações', 'description' => 'Conferência do que foi combinado antes do grande dia.'],
['icon_key' => 'heart', 'title' => 'Coordenação da execução', 'description' => 'Acompanhamento do evento para o casal viver o momento.'],
['icon_key' => 'heart', 'title' => 'Escuta e briefing', 'description' => 'Cada casal é único; começamos por ouvir.'],
['icon_key' => 'checklist', 'title' => 'Planejamento completo', 'description' => 'Cada etapa mapeada com clareza.'],
['icon_key' => 'map', 'title' => 'Gestão de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
['icon_key' => 'calendar', 'title' => 'Acompanhamento contínuo', 'description' => 'Perto do casal em cada decisão.'],
['icon_key' => 'spark', 'title' => 'Produção e logística', 'description' => 'Dia a dia do evento organizado nos detalhes.'],
['icon_key' => 'users', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
],
'audience_heading' => 'Para quem é esta modalidade',
'audience_intro' => 'Para quem deseja ter ao lado uma profissional conduzindo o processo como um todo, com organização, estratégia e tranquilidade.',
'audience_heading' => 'Para quem é este pacote',
'audience_intro' => 'Para quem quer uma assessoria completa, do início ao fim.',
'audience_points' => [
'Casais no início da organização',
'Quem ainda está estruturando etapas importantes',
'Quem quer acompanhamento do planejamento à execução',
'Casamentos',
'Aniversários',
'Eventos sociais e celebrações',
'Eventos corporativos',
],
'final_cta_heading' => 'Vamos nos conhecer?',
'final_cta_body' => 'Antes de definir qualquer caminho, queremos conhecer vocês. Basta entrar em contato para uma conversa.',
'final_cta_heading' => 'Pronta para começar?',
'final_cta_body' => 'Conte seu evento para a Amare e receba uma proposta.',
],
[
'name' => 'Conduzione',
'slug' => 'conduzione',
'level' => '02 / PARCIAL',
'tag' => 'Assessoria parcial',
'subtitle' => 'Para quem já começou, mas quer seguir acompanhado.',
'summary' => 'Indicada para casais que já iniciaram o planejamento e possuem parte dos fornecedores ou decisões definidas, mas desejam organizar o que falta e conduzir as próximas etapas.',
'subtitle' => 'Para quem já começou e quer seguir acompanhado.',
'summary' => 'Para casais que já tomaram decisões e contrataram parte dos fornecedores, mas precisam estruturar o que falta.',
'scope_items' => [
'Diagnóstico do planejamento já realizado',
'Organização das etapas pendentes',
'Gestão e acompanhamento dos fornecedores contratados',
'Orientação sobre as próximas decisões',
'Acompanhamento de prazos e informações',
'Comunicação e alinhamento com fornecedores',
'Organização do cronograma do evento',
'Coordenação da execução no grande dia',
'Diagnóstico do planejamento existente',
'Organização das pendências',
'Gestão dos fornecedores contratados',
'Orientação para próximas decisões',
'Coordenação da execução',
],
'cta_label' => 'Quero conhecer a Conduzione',
'compare_heading' => 'Trazer a Amare para o caminho',
'compare_summary' => 'O planejamento existe; a Amare organiza o que falta e conduz as próximas etapas.',
'compare_summary' => 'O planejamento existe, mas ainda há decisões e gestão pela frente.',
'sort_order' => 2,
'eyebrow' => 'Assessoria',
'title_line' => 'Assessoria',
'title_emphasis' => 'Parcial',
'hero_lead' => 'A Amare entra no momento em que vocês estão, compreende o que já foi construído e assume a gestão das etapas necessárias dentro do planejamento.',
'hero_lead' => 'Para retomar o planejamento, organizar o que falta e seguir com acompanhamento profissional.',
'benefits' => [
['icon_key' => 'checklist', 'label' => 'Diagnóstico do que já existe'],
['icon_key' => 'calendar', 'label' => 'Pendências organizadas'],
['icon_key' => 'users', 'label' => 'Fornecedores acompanhados'],
['icon_key' => 'heart', 'label' => 'Próximas decisões com clareza'],
['icon_key' => 'checklist', 'label' => 'Diagnóstico completo'],
['icon_key' => 'calendar', 'label' => 'Organização integral'],
['icon_key' => 'users', 'label' => 'Fornecedores selecionados'],
['icon_key' => 'heart', 'label' => 'Experiência leve e inesquecível'],
],
'included_items' => [
['icon_key' => 'checklist', 'title' => 'Diagnóstico do planejamento', 'description' => 'Um olhar profissional sobre o que o casal já construiu.'],
['icon_key' => 'calendar', 'title' => 'Etapas pendentes', 'description' => 'O que falta ganha ordem, prazo e prioridade.'],
['icon_key' => 'map', 'title' => 'Fornecedores já contratados', 'description' => 'Gestão e comunicação com quem já está no projeto.'],
['icon_key' => 'spark', 'title' => 'Próximas decisões', 'description' => 'Orientação para seguir sem retrabalho.'],
['icon_key' => 'users', 'title' => 'Cronograma do evento', 'description' => 'A operação do grande dia começa a ficar visível.'],
['icon_key' => 'heart', 'title' => 'Coordenação no grande dia', 'description' => 'Execução acompanhada para o casal viver o momento.'],
['icon_key' => 'checklist', 'title' => 'Diagnóstico do planejamento', 'description' => 'Um olhar profissional sobre o que já existe.'],
['icon_key' => 'calendar', 'title' => 'Organização das pendências', 'description' => 'Prioridades claras, etapa a etapa.'],
['icon_key' => 'map', 'title' => 'Gestão de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
['icon_key' => 'spark', 'title' => 'Gestão dos próximos passos', 'description' => 'Um plano para chegar tranquilo ao grande dia.'],
['icon_key' => 'users', 'title' => 'Acompanhamento contínuo', 'description' => 'Perto do casal em cada decisão.'],
['icon_key' => 'heart', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
],
'audience_heading' => 'Para quem é esta modalidade',
'audience_intro' => 'Para quem já deu os primeiros passos, mas deseja seguir com mais segurança, organização e tranquilidade.',
'audience_heading' => 'Para quem é este pacote',
'audience_intro' => 'Para quem já começou a planejar e quer retomar as rédeas com acompanhamento profissional.',
'audience_points' => [
'Casais que já iniciaram o planejamento',
'Quem já tem parte dos fornecedores ou decisões definidas',
'Quem quer condução profissional nas etapas que faltam',
'Casamentos em andamento',
'Casais com pouco tempo disponível',
'Quem precisa priorizar pendências',
'Quem busca fornecedores confiáveis',
],
'final_cta_heading' => 'Vamos organizar o que falta?',
'final_cta_body' => 'Conte onde o planejamento está. A Amare entra nesse momento e conduz as próximas etapas.',
'final_cta_heading' => 'Pronta para retomar?',
'final_cta_body' => 'Conte onde seu planejamento está e receba uma proposta.',
],
[
'name' => 'Grand Jour',
'slug' => 'grand-jour',
'level' => '03 / FINAL',
'tag' => 'Assessoria final',
'subtitle' => 'Para quem já planejou e precisa de alguém para assumir a operação.',
'summary' => 'Indicada para casais que estão na reta final dos preparativos e já possuem a maior parte do casamento definida.',
'subtitle' => 'O planejamento está pronto. Agora é hora de viver.',
'summary' => 'Para casais na reta final que precisam de uma equipe para assumir alinhamentos, cronograma e operação.',
'scope_items' => [
'Imersão no planejamento existente',
'Conferência das informações e contratos dos fornecedores',
'Alinhamento e comunicação com os profissionais envolvidos',
'Conferência das informações',
'Alinhamento dos fornecedores',
'Organização do cronograma final',
'Orientação sobre os últimos detalhes',
'Coordenação dos fornecedores no evento',
'Acompanhamento da montagem e dos momentos do casamento',
'Gestão da operação durante o grande dia',
'Gestão da operação do evento',
],
'cta_label' => 'Quero conhecer a Grand Jour',
'compare_heading' => 'Entregar a operação para a Amare',
'compare_summary' => 'O projeto está pronto; o foco passa a ser alinhamento e execução.',
'compare_summary' => 'O projeto está pronto e o foco passa a ser alinhamento e execução.',
'sort_order' => 3,
'eyebrow' => 'Assessoria',
'title_line' => 'Assessoria',
'title_emphasis' => 'Final',
'hero_lead' => 'A Amare assume a condução operacional, mergulha no planejamento construído pelo casal e transforma as informações em uma execução organizada e alinhada.',
'hero_lead' => 'Para assumir os alinhamentos e a operação quando o grande dia está próximo.',
'benefits' => [
['icon_key' => 'checklist', 'label' => 'Imersão no planejamento'],
['icon_key' => 'users', 'label' => 'Fornecedores alinhados'],
['icon_key' => 'checklist', 'label' => 'Imersão no que já existe'],
['icon_key' => 'users', 'label' => 'Alinhamento de fornecedores'],
['icon_key' => 'calendar', 'label' => 'Cronograma final'],
['icon_key' => 'heart', 'label' => 'Operação no grande dia'],
['icon_key' => 'heart', 'label' => 'Operação com leveza'],
],
'included_items' => [
['icon_key' => 'checklist', 'title' => 'Imersão no planejamento', 'description' => 'A Amare mergulha no que o casal já decidiu.'],
['icon_key' => 'map', 'title' => 'Contratos e informações', 'description' => 'Conferência do combinado com cada profissional.'],
['icon_key' => 'users', 'title' => 'Alinhamento dos envolvidos', 'description' => 'Comunicação centralizada na reta final.'],
['icon_key' => 'calendar', 'title' => 'Cronograma final', 'description' => 'A operação do grande dia ganha ordem.'],
['icon_key' => 'spark', 'title' => 'Últimos detalhes', 'description' => 'Orientação para fechar o que ainda falta.'],
['icon_key' => 'heart', 'title' => 'Gestão da operação', 'description' => 'Montagem, momentos e condução no dia do casamento.'],
['icon_key' => 'checklist', 'title' => 'Imersão no planejamento', 'description' => 'Entendemos cada decisão já tomada.'],
['icon_key' => 'map', 'title' => 'Alinhamento de fornecedores', 'description' => 'Contatos e prazos sob cuidado.'],
['icon_key' => 'calendar', 'title' => 'Cronograma final', 'description' => 'Cada etapa posicionada na reta final.'],
['icon_key' => 'spark', 'title' => 'Ensaios e detalhes', 'description' => 'Produção e logística nos detalhes.'],
['icon_key' => 'users', 'title' => 'Operação do evento', 'description' => 'Gestão da equipe e do dia a dia.'],
['icon_key' => 'heart', 'title' => 'Dia do evento', 'description' => 'Coordenação total para o casal viver o momento.'],
],
'audience_heading' => 'Para quem é esta modalidade',
'audience_intro' => 'Porque, na reta final, vocês não precisam de mais tarefas. Precisam de alguém que assuma a condução.',
'audience_heading' => 'Para quem é este pacote',
'audience_intro' => 'Para casais já planejados que querem chegar ao grande dia com equipe e operação no lugar.',
'audience_points' => [
'Casais na reta final dos preparativos',
'Quem já definiu a maior parte do casamento',
'Quem quer entregar a operação do grande dia',
'Casamentos já planejados',
'Casais na reta final',
'Quem precisa de operação no dia',
'Quem quer viver o momento com leveza',
],
'final_cta_heading' => 'Vamos assumir a operação?',
'final_cta_body' => 'Conte a data e o que já está definido. A Amare entra para conduzir o grande dia.',
'final_cta_heading' => 'Pronta para o grande dia?',
'final_cta_body' => 'Conte a data do seu evento e receba uma proposta.',
],
];

View File

@@ -1,28 +1,51 @@
# Domain Docs
How engineering skills should consume this repo's domain documentation.
How the engineering skills should consume this repo's domain documentation when exploring the codebase.
## Single source of truth
## Before exploring, read these
**`SPEC.md` §8** (especially **§8.0 Linguagem ubíqua**) is the glossary and domain model for Amare. There is no root `CONTEXT.md` on purpose — do not create one. Prefer `SPEC.md` over Linear copy or OpenSpec prose when terms conflict.
- **`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.
Also useful:
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.
- **`PRODUCT.md`** — positioning and audience (not the glossary)
- **`openspec/specs/`** — current capability contracts
- **`docs/adr/README.md`** — index only; ADR-001010 live in `SPEC.md` §21
## File structure
## Before exploring
Single-context repo (most repos):
1. Read `SPEC.md` §8.0 for vocabulary; skim §5.1 for public routes and §8.2+ for persistence shape when relevant.
2. If a skill expects `CONTEXT.md` / `CONTEXT-MAP.md` and finds neither, **proceed using `SPEC.md` §8** — do not invent a parallel glossary file.
```
/
├── 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 output names a domain concept (issue title, refactor, hypothesis, test name), use the term as defined in `SPEC.md` §8.0. Don't drift to synonyms the glossary explicitly avoids (`_Avoid_`).
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 isn't in §8.0 yet, either you're inventing language the project doesn't use (reconsider) or there's a real gap — resolve it by updating `SPEC.md` §8.0 via `/grill-with-docs` / domain-modeling, not by adding `CONTEXT.md`.
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 output contradicts an ADR in `SPEC.md` §21, surface it explicitly rather than silently overriding.
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding:
> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_

11
package-lock.json generated
View File

@@ -5,6 +5,7 @@
"packages": {
"": {
"devDependencies": {
"@fontsource/eb-garamond": "^5.2.7",
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1",
"daisyui": "^5.7.16",
@@ -49,6 +50,16 @@
"tslib": "^2.4.0"
}
},
"node_modules/@fontsource/eb-garamond": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@fontsource/eb-garamond/-/eb-garamond-5.3.0.tgz",
"integrity": "sha512-VplwHpB8FLuzl+4G5LDci1b3Z4SghiuXH8XEgaxjnOb5aue5mEt1njmDKeO6iyLeGa73LffU7PC7hzncLRx0ZA==",
"dev": true,
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",

View File

@@ -8,6 +8,7 @@
"prepare": "husky"
},
"devDependencies": {
"@fontsource/eb-garamond": "^5.2.7",
"@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1",
"daisyui": "^5.7.16",

View File

@@ -189,6 +189,18 @@
overflow-x: clip;
}
.link-editorial {
background-image: linear-gradient(currentColor, currentColor);
background-position: 0 100%;
background-repeat: no-repeat;
background-size: 100% 1px;
padding-bottom: 1px;
}
.nav-link-editorial {
position: relative;
}
@media (prefers-reduced-motion: no-preference) {
:where(a, button, input, textarea, select) {
transition-duration: var(--amare-motion-feedback);
@@ -200,6 +212,45 @@
transform: translateY(1px);
}
:where(button[aria-busy='true'], button:disabled):active {
transform: none;
}
.field-editorial {
transition:
border-color var(--amare-motion-feedback) var(--amare-ease-arrival),
background-color var(--amare-motion-feedback) var(--amare-ease-arrival);
}
.field-editorial:focus,
.field-editorial:focus-visible {
background-color: var(--amare-color-bg-deep);
}
.link-editorial {
transition: color var(--amare-motion-feedback) var(--amare-ease-arrival);
}
@media (min-width: 768px) {
.nav-link-editorial::after {
background-color: var(--amare-color-accent);
bottom: 0.55rem;
content: '';
height: 1px;
left: 0.75rem;
position: absolute;
right: 0.75rem;
transform: scaleX(0);
transform-origin: left center;
transition: transform var(--amare-motion-feedback) var(--amare-ease-arrival);
}
.nav-link-editorial:hover::after,
.nav-link-editorial:focus-visible::after {
transform: scaleX(1);
}
}
/* Transform/clip only — never fade text opacity (axe + WCAG mid-transition). */
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) [data-motion-beat] {
transform: translateY(var(--amare-motion-distance-y));

View File

@@ -24,7 +24,7 @@
<div class="lg:col-span-6 lg:col-start-7">
<p class="max-w-xl text-amare-text-muted">Uma composição de eventos sociais e corporativos para apresentar a versatilidade da Amare.</p>
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost mt-5 min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Ver portfólio completo</span>
<span class="link-editorial">Ver portfólio completo</span>
</a>
</div>
</div>

View File

@@ -50,7 +50,7 @@
autocomplete="name"
maxlength="120"
placeholder="Seu nome"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
@error('nome') aria-invalid="true" aria-describedby="briefing-nome-error" @enderror
>
@error('nome')
@@ -69,7 +69,7 @@
autocomplete="email"
maxlength="254"
placeholder="voce@email.com"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
@error('email') aria-invalid="true" aria-describedby="briefing-email-error" @enderror
>
@error('email')
@@ -88,7 +88,7 @@
autocomplete="tel"
maxlength="40"
placeholder="(11) 90000-0000"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
@error('telefone') aria-invalid="true" aria-describedby="briefing-telefone-error" @enderror
>
@error('telefone')
@@ -102,7 +102,7 @@
id="briefing-tipo_evento"
name="tipo_evento"
required
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
class="select field-editorial w-full text-amare-text @error('tipo_evento') select-error @enderror"
@error('tipo_evento') aria-invalid="true" aria-describedby="briefing-tipo_evento-error" @enderror
>
<option value="" selected disabled>Selecione...</option>
@@ -125,7 +125,7 @@
value="{{ old('data_periodo') }}"
maxlength="80"
placeholder="Ex.: novembro de 2027"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
@error('data_periodo') aria-invalid="true" aria-describedby="briefing-data_periodo-error" @enderror
>
@error('data_periodo')
@@ -143,7 +143,7 @@
required
maxlength="80"
placeholder="Ex.: São Paulo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
@error('cidade') aria-invalid="true" aria-describedby="briefing-cidade-error" @enderror
>
@error('cidade')
@@ -163,7 +163,7 @@
inputmode="numeric"
autocomplete="off"
placeholder="Ex.: 120"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
@error('convidados') aria-invalid="true" aria-describedby="briefing-convidados-error" @enderror
>
@error('convidados')
@@ -180,7 +180,7 @@
value="{{ old('servico_interesse') }}"
maxlength="120"
placeholder="Ex.: planejamento completo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
class="input field-editorial w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
@error('servico_interesse') aria-invalid="true" aria-describedby="briefing-servico_interesse-error" @enderror
>
@error('servico_interesse')
@@ -198,7 +198,7 @@
rows="5"
maxlength="3000"
placeholder="O que já está definido? Em que ponto você precisa de ajuda?"
class="textarea w-full min-h-[110px] text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
class="textarea field-editorial w-full min-h-[110px] text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
@error('mensagem') aria-invalid="true" aria-describedby="briefing-mensagem-error" @enderror
>{{ old('mensagem') }}</textarea>
@error('mensagem')

View File

@@ -19,7 +19,7 @@
@if (filled($settings->hero_secondary_cta_label))
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] 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>
<span class="link-editorial">{{ $settings->hero_secondary_cta_label }}</span>
</a>
@endif
</div>

View File

@@ -14,6 +14,9 @@
'note' => 'Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.',
])
@php
$bandHref = $bandCtaHref ?? route('briefing');
@endphp
<section
aria-labelledby="packages-heading"
class="home-chapter border-b border-amare-border bg-amare-bg"
@@ -61,7 +64,7 @@
href="{{ route('packages.show', $package->slug) }}"
class="btn btn-ghost min-h-0 px-0 text-xs font-semibold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"
>
<span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
<span class="link-editorial">Conhecer esta modalidade</span>
</a>
</div>
</article>
@@ -69,20 +72,12 @@
</div>
@endif
@php($orientationLink = \App\Support\PackageContactLink::forOrientation($settings))
@php($bandHref = filled($bandCtaHref) ? $bandCtaHref : $orientationLink['href'])
@php($bandIsWhatsapp = ! filled($bandCtaHref) && $orientationLink['isWhatsapp'])
<div class="mt-6 flex flex-col justify-between gap-8 bg-amare-accent-deep p-8 text-amare-accent-text md:flex-row md:items-center md:p-12" data-reveal data-reveal-from="up">
<div class="max-w-xl space-y-3">
<h3 class="text-[clamp(1.5625rem,2.7vw,2.125rem)] font-medium leading-tight">Ainda não sabe qual modalidade é ideal?</h3>
<p class="text-amare-accent-text/80">{{ $bandBody }}</p>
</div>
<a
data-testid="package-guidance-cta"
href="{{ $bandHref }}"
@if ($bandIsWhatsapp) target="_blank" rel="noopener noreferrer" @endif
class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto"
>
<a href="{{ $bandHref }}" class="inline-flex min-h-[48px] items-center justify-center self-start border border-amare-accent-text px-6 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent-text transition-colors hover:bg-amare-accent-text hover:text-amare-accent-deep md:self-auto">
Conversar com a Amare
</a>
</div>

View File

@@ -35,7 +35,7 @@
</ul>
<p>
<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="link-editorial">Conhecer a Amare</span>
</a>
</p>
</div>

View File

@@ -27,7 +27,7 @@
<p>
<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="link-editorial">Ver todos os serviços</span>
</a>
</p>
</div>

View File

@@ -9,7 +9,7 @@
<ol class="grid gap-8 border-t border-amare-border pt-6 md:grid-cols-3">
@foreach ($packages as $package)
@php($contactLink = \App\Support\PackageContactLink::for($settings, $package->name))
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><div class="space-y-2"><a href="{{ $contactLink['href'] }}" @if ($contactLink['isWhatsapp']) target="_blank" rel="noopener" @endif 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">{{ $package->cta_label }}</span></a><a href="{{ route('packages.show', $package->slug) }}" 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 esta modalidade</span></a></div></li>
<li class="space-y-5 border-t border-amare-border pt-4" data-reveal data-reveal-from="up"><p class="text-xs font-semibold uppercase tracking-[.14em] text-amare-accent">{{ $package->level }}</p><h3 class="text-3xl font-medium">{{ $package->name }}</h3><p class="text-amare-muted">{{ $package->summary }}</p><ul class="space-y-2 text-sm text-amare-text-muted">@foreach ($package->scope_items as $item)<li class="border-l border-amare-border pl-3">{{ $item }}</li>@endforeach</ul><div class="space-y-2"><a href="{{ $contactLink['href'] }}" @if ($contactLink['isWhatsapp']) target="_blank" rel="noopener" @endif 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="link-editorial">{{ $package->cta_label }}</span></a><a href="{{ route('packages.show', $package->slug) }}" 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="link-editorial">Conhecer esta modalidade</span></a></div></li>
@endforeach
</ol>
@endif

View File

@@ -0,0 +1,44 @@
@props([
'seal' => 'Recebido',
'title',
'body',
'homeLabel' => 'Voltar para a home',
'whatsappLabel' => 'Falar no WhatsApp',
'whatsappText' => 'Olá, enviei um briefing e gostaria de acompanhar.',
])
@php
$settings = $siteSettings ?? \App\Models\SiteSetting::instance();
$digits = preg_replace('/\D/', '', (string) ($settings->whatsapp_number ?? ''));
$whatsappHref = strlen($digits) >= 10
? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappText)
: null;
@endphp
<div
role="status"
data-form-success
class="mt-10 max-w-2xl space-y-6 border border-amare-border bg-amare-bg-deep px-6 py-10 md:px-10"
data-motion="page-open"
{{ $attributes }}
>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">{{ $seal }}</p>
<h3 class="text-3xl font-medium tracking-tight text-amare-text md:text-4xl" data-motion-beat="title">{{ $title }}</h3>
<p class="max-w-xl text-lg leading-relaxed text-amare-muted" data-motion-beat="heading">{{ $body }}</p>
<div class="flex flex-col items-start gap-4 pt-2 sm:flex-row sm:items-center sm:gap-8" data-motion-beat="cta">
<a href="{{ route('home') }}" class="btn btn-primary min-h-11 px-6 text-xs font-bold uppercase tracking-[0.09em]">
{{ $homeLabel }}
</a>
@if ($whatsappHref)
<a
href="{{ $whatsappHref }}"
class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep"
rel="noopener noreferrer"
target="_blank"
>
<span class="link-editorial">{{ $whatsappLabel }}</span>
</a>
@endif
</div>
</div>

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Página não encontrada - Amare Assessoria',
description: 'A página que você procura não existe ou foi movida.',
description: 'Não encontramos este endereço. Volte à home e continue por ali.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 404</p>
<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">Não encontramos este endereço talvez tenha mudado de lugar. A home continua sendo o melhor ponto de partida.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Sessão expirada - Amare Assessoria',
description: 'Sua sessão expirou. Volte e tente enviar novamente.',
description: 'Sua sessão ficou quieta demais. Volte e envie de novo com calma.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 419</p>
<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 ficou quieta demais e precisamos recomeçar. Volte à página anterior e envie de novo os dados ainda estão seguros.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Muitas solicitações - Amare Assessoria',
description: 'Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.',
description: 'Recebemos muitos pedidos em pouco tempo. Aguarde um instante e tente de novo.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 429</p>
<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">Recebemos vários pedidos em pouco tempo. Espere um instante assim que pudermos, tentamos de novo com calma.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Algo deu errado - Amare Assessoria',
description: 'Não foi possível concluir o pedido. Tente novamente em instantes.',
description: 'Algo saiu do roteiro do nosso lado. Tente novamente em instantes.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 500</p>
<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">Algo saiu do roteiro do nosso lado. estamos cuidando disso tente novamente em instantes.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta(
title: 'Em manutenção - Amare Assessoria',
description: 'Estamos realizando uma breve manutenção. Tente novamente em instantes.',
description: 'Estamos em uma pausa breve de manutenção. Voltamos em instantes.',
canonical: '',
robots: 'noindex, nofollow',
);
@@ -18,7 +18,7 @@
<div class="container-amare max-w-2xl space-y-6 py-20 text-center" data-motion="page-open" data-reveal-group>
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent" data-motion-beat="seal">Erro 503</p>
<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 em uma pausa breve para deixar tudo no lugar. Voltamos em instantes obrigado pela paciência.</p>
<p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home

View File

@@ -58,10 +58,10 @@
<header class="site-header sticky top-0 z-40 border-b border-amare-border bg-amare-bg/95 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] md:h-[74px] md:py-0">
<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="{{ url('/#casamentos') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Casamentos</a>
<a href="{{ url('/#corporate') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Corporate</a>
<a href="{{ route('portfolio.index') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Portfólio</a>
<a href="{{ route('about') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:px-3">Amare</a>
<a href="{{ url('/#casamentos') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Casamentos</a>
<a href="{{ url('/#corporate') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Corporate</a>
<a href="{{ route('portfolio.index') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Portfólio</a>
<a href="{{ route('about') }}" class="nav-link-editorial btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:bg-transparent hover:text-amare-accent md:px-3">Amare</a>
<a href="{{ route('briefing') }}" class="btn btn-ghost min-h-0 px-0 py-3 text-[13px] font-semibold uppercase tracking-[0.08em] text-amare-muted hover:text-amare-accent md:hidden">Solicitar proposta</a>
</nav>

View File

@@ -42,10 +42,11 @@
</p>
@if (session('status') === 'briefing-sent')
<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="mt-1 text-amare-muted">Recebemos seu briefing e retornaremos em breve pelo canal informado.</p>
</div>
<x-public.form-success
seal="Briefing recebido"
title="Recebemos sua ficha."
body="Vamos ler com atenção e retornar em breve pelo canal que você informou. Enquanto isso, pode seguir explorando o portfólio."
/>
@else
<form
method="POST"
@@ -79,7 +80,7 @@
autocomplete="name"
maxlength="120"
placeholder="Seu nome"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('nome') input-error @enderror"
class="input field-editorial 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')
@@ -98,7 +99,7 @@
autocomplete="email"
maxlength="254"
placeholder="voce@email.com"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('email') input-error @enderror"
class="input field-editorial 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')
@@ -117,7 +118,7 @@
autocomplete="tel"
maxlength="40"
placeholder="(11) 90000-0000"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('telefone') input-error @enderror"
class="input field-editorial 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')
@@ -131,7 +132,7 @@
id="tipo_evento"
name="tipo_evento"
required
class="select w-full text-amare-text @error('tipo_evento') select-error @enderror"
class="select field-editorial w-full text-amare-text @error('tipo_evento') select-error @enderror"
@error('tipo_evento') aria-invalid="true" aria-describedby="tipo_evento-error" @enderror
>
<option value="" selected disabled>Selecione...</option>
@@ -154,7 +155,7 @@
value="{{ old('data_periodo') }}"
maxlength="80"
placeholder="Ex.: novembro de 2027"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('data_periodo') input-error @enderror"
class="input field-editorial 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')
@@ -172,7 +173,7 @@
required
maxlength="80"
placeholder="Ex.: São Paulo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('cidade') input-error @enderror"
class="input field-editorial 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')
@@ -192,7 +193,7 @@
inputmode="numeric"
autocomplete="off"
placeholder="Ex.: 120"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('convidados') input-error @enderror"
class="input field-editorial 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')
@@ -209,7 +210,7 @@
value="{{ old('servico_interesse', request('servico_interesse')) }}"
maxlength="120"
placeholder="Ex.: planejamento completo"
class="input w-full text-amare-text placeholder:text-amare-muted/60 @error('servico_interesse') input-error @enderror"
class="input field-editorial 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')
@@ -227,7 +228,7 @@
rows="6"
maxlength="3000"
placeholder="Conte sobre o seu evento, expectativas e principais preocupações."
class="textarea w-full text-amare-text placeholder:text-amare-muted/60 @error('mensagem') textarea-error @enderror"
class="textarea field-editorial 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
>{{ old('mensagem') }}</textarea>
@error('mensagem')

View File

@@ -20,7 +20,12 @@
<div class="container-amare py-16 md:py-24" data-reveal-group>
<h2 id="partner-form-heading" class="text-3xl font-medium text-amare-text" data-reveal data-reveal-from="up">Apresente seu trabalho</h2>
@if (session('status') === 'partner-inquiry-sent')
<div role="status" class="mt-8 border border-amare-border bg-amare-bg px-6 py-5"><p class="font-semibold">Mensagem enviada.</p><p class="mt-1 text-amare-muted">Recebemos sua apresentação e entraremos em contato se houver aderência.</p></div>
<x-public.form-success
seal="Apresentação recebida"
title="Recebemos sua apresentação."
body="Vamos avaliar a aderência com o cuidado que cada parceria merece. Se fizer sentido, entraremos em contato."
whatsapp-text="Olá, enviei uma apresentação de parceria e gostaria de acompanhar."
/>
@else
<form method="POST" action="{{ route('contact.store') }}" class="mt-10 max-w-3xl space-y-8" data-contact-form data-reveal data-reveal-from="up">
@csrf
@@ -28,10 +33,10 @@
@if ($errors->any())<div role="alert" class="border border-amare-error/40 px-6 py-5">Revise os campos destacados abaixo.</div>@endif
<div class="grid gap-8 sm:grid-cols-2">
@foreach (['nome' => 'Nome *', 'empresa' => 'Nome profissional ou empresa', 'email' => 'E-mail *', 'atuacao' => 'Atuação ou serviço *', 'area_atendimento' => 'Área de atendimento *', 'portfolio_redes' => 'Portfólio ou redes', 'telefone' => 'Telefone'] as $field => $label)
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="{{ $field }}">{{ $label }}</label><input class="w-full border-0 border-b border-amare-border bg-transparent py-3 focus:border-amare-accent focus:outline-none @error($field) border-amare-error @enderror" id="{{ $field }}" name="{{ $field }}" value="{{ old($field) }}" maxlength="{{ $field === 'portfolio_redes' ? 500 : ($field === 'email' ? 254 : 160) }}" @required(in_array($field, ['nome','email','atuacao','area_atendimento'], true)) @error($field) aria-invalid="true" @enderror>@error($field)<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="{{ $field }}">{{ $label }}</label><input class="field-editorial w-full border-0 border-b border-amare-border bg-transparent py-3 focus:border-amare-accent focus:outline-none @error($field) border-amare-error @enderror" id="{{ $field }}" name="{{ $field }}" value="{{ old($field) }}" maxlength="{{ $field === 'portfolio_redes' ? 500 : ($field === 'email' ? 254 : 160) }}" @required(in_array($field, ['nome','email','atuacao','area_atendimento'], true)) @error($field) aria-invalid="true" @enderror>@error($field)<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
@endforeach
</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<div><label class="mb-2 block text-xs font-semibold uppercase tracking-[.14em]" for="mensagem">Mensagem *</label><textarea class="field-editorial w-full border border-amare-border bg-transparent p-3 focus:border-amare-accent focus:outline-none" id="mensagem" name="mensagem" rows="6" required maxlength="3000">{{ old('mensagem') }}</textarea>@error('mensagem')<p class="mt-2 text-sm text-amare-error">{{ $message }}</p>@enderror</div>
<p class="max-w-2xl text-sm text-amare-muted">Ao enviar, você reconhece o uso destes dados exclusivamente para analisar uma possível parceria, conforme a <a class="text-amare-accent underline" href="{{ route('privacy') }}">política de privacidade</a>.</p>
<button type="submit" data-submit-button class="btn btn-primary text-sm font-semibold uppercase tracking-[.12em]">Enviar apresentação</button>
</form>

View File

@@ -13,7 +13,7 @@
Conhecer os serviços
</a>
<a href="{{ route('contact') }}" class="btn btn-ghost min-h-0 px-0 text-xs font-bold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Tenho um evento em mente</span>
<span class="link-editorial">Tenho um evento em mente</span>
</a>
</div>
</x-public.photo-hero>
@@ -47,6 +47,7 @@
:show-intro="false"
kicker="tag"
:show-subtitle="true"
:band-cta-href="route('briefing')"
band-body="Conte um pouco sobre o casamento. A Amare entende o momento de vocês e orienta o melhor formato de acompanhamento sem depender de um quiz automático."
:show-note="false"
/>
@@ -64,7 +65,7 @@
<p class="text-amare-text-muted">{{ $package->compare_summary }}</p>
</div>
<a href="{{ route('packages.show', $package->slug) }}" class="btn btn-ghost min-h-0 self-start px-0 text-xs font-semibold uppercase tracking-[0.09em] text-amare-accent hover:bg-transparent hover:text-amare-accent-deep">
<span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
<span class="link-editorial">Conhecer esta modalidade</span>
</a>
</article>
@endif

View File

@@ -38,33 +38,6 @@ class WeddingPackageTest extends TestCase
['Essenza', 'Conduzione', 'Grand Jour'],
WeddingPackage::query()->published()->orderBy('sort_order')->pluck('name')->all(),
);
$essenza = WeddingPackage::query()->where('name', 'Essenza')->first();
$this->assertNotNull($essenza);
$this->assertSame('Assessoria completa', $essenza->tag);
$this->assertSame('Do primeiro planejamento ao grande dia.', $essenza->subtitle);
$this->assertStringContainsString('desde o início da organização', (string) $essenza->summary);
$this->assertContains('Estruturação e acompanhamento do planejamento', $essenza->scope_items);
$this->assertSame(
['Casais no início da organização', 'Quem ainda está estruturando etapas importantes', 'Quem quer acompanhamento do planejamento à execução'],
$essenza->audience_points,
);
$publicCopy = WeddingPackage::query()->published()->get()->map(fn (WeddingPackage $package): string => json_encode([
$package->summary,
$package->subtitle,
$package->hero_lead,
$package->scope_items,
$package->included_items,
$package->audience_intro,
$package->final_cta_body,
], JSON_THROW_ON_ERROR))->implode(' ');
$this->assertStringNotContainsString('R$', $publicCopy);
$this->assertStringNotContainsString('3.500', $publicCopy);
$this->assertStringNotContainsString('Assessoria VIP', $publicCopy);
$this->assertStringNotContainsString('90 km', $publicCopy);
$this->assertStringNotContainsString('3,49%', $publicCopy);
}
public function test_factory_generates_a_slug_and_autofills_blank_slug_from_name(): void

View File

@@ -89,6 +89,28 @@ class ContactBriefingTest extends TestCase
});
}
public function test_successful_submission_shows_editorial_confirmation(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
Mail::fake();
$response = $this
->from(route('briefing'))
->followingRedirects()
->post(route('briefing.store'), $this->validPayload());
$response
->assertOk()
->assertSee('data-form-success', false)
->assertSee('Briefing recebido')
->assertSee('Recebemos sua ficha')
->assertSee('role="status"', false)
->assertSee('data-motion-beat="seal"', false)
->assertSee(route('home'), false)
->assertSee('https://wa.me/5511988887777', false)
->assertDontSee('<form', false);
}
public function test_submission_normalizes_email_and_phone_before_reaching_mailables(): void
{
$settings = SiteSetting::instance();

View File

@@ -72,7 +72,19 @@ class HeritageEditorialTokensTest extends TestCase
{
$vite = (string) file_get_contents(base_path('vite.config.js'));
$this->assertStringContainsString("bunny('EB Garamond'", $vite);
$this->assertMatchesRegularExpression(
"/^import \\{ fontsource \\} from 'laravel-vite-plugin\\/fonts';/m",
$vite,
);
$this->assertDoesNotMatchRegularExpression(
'/^import \\{[^}]*\\bbunny\\b/m',
$vite,
);
$this->assertDoesNotMatchRegularExpression(
'/^import \\{[^}]*\\bgoogle\\b/m',
$vite,
);
$this->assertStringContainsString("fontsource('EB Garamond'", $vite);
$this->assertStringNotContainsString('Instrument Sans', $vite);
}

View File

@@ -8,7 +8,6 @@ use App\Models\PortfolioCase;
use App\Models\SiteSetting;
use App\Models\Testimonial;
use App\Models\WeddingPackage;
use App\Support\PackageContactLink;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@@ -214,24 +213,7 @@ class HomePageContentTest extends TestCase
false,
)
->assertSee('target="_blank"', false)
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false)
->assertSee('data-testid="package-guidance-cta"', false)
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode(PackageContactLink::ORIENTATION_MESSAGE),
false,
);
}
public function test_orientation_cta_falls_back_to_briefing_without_whatsapp_number(): void
{
SiteSetting::instance()->update(['whatsapp_number' => null]);
$response = $this->get(route('home'));
$response
->assertOk()
->assertSee('href="'.route('briefing').'"', false)
->assertDontSee(PackageContactLink::ORIENTATION_MESSAGE, false);
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
}
public function test_corporate_steps_and_placeholder_render_from_settings(): void

View File

@@ -96,6 +96,9 @@ class MotionMarkupTest extends TestCase
$this->assertStringContainsString('[data-reveal]', $appCss);
$this->assertStringNotContainsString('[data-chapter-index]', $appCss);
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
$this->assertStringContainsString('.field-editorial', $appCss);
$this->assertStringContainsString('.nav-link-editorial', $appCss);
$this->assertStringContainsString('.link-editorial', $appCss);
// Entrance motion must not fade text opacity — mid-fade fails WCAG contrast (axe).
$this->assertDoesNotMatchRegularExpression(

View File

@@ -66,6 +66,22 @@ class PartnerInquiryTest extends TestCase
Mail::assertQueued(PartnerInquiry::class, 1);
}
public function test_successful_partner_inquiry_shows_editorial_confirmation(): void
{
SiteSetting::instance();
Mail::fake();
$this
->from(route('contact'))
->followingRedirects()
->post(route('contact.store'), $this->validPayload())
->assertOk()
->assertSee('data-form-success', false)
->assertSee('Apresentação recebida')
->assertSee('Recebemos sua apresentação')
->assertDontSee('name="atuacao"', false);
}
public function test_partner_honeypot_reports_success_without_sending_mail(): void
{
SiteSetting::instance();

View File

@@ -6,7 +6,6 @@ namespace Tests\Feature\PublicSite;
use App\Models\SiteSetting;
use App\Models\WeddingPackage;
use App\Support\PackageContactLink;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
@@ -32,12 +31,7 @@ class ServicesPageCtaTest extends TestCase
false,
)
->assertSee('target="_blank"', false)
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false)
->assertSee('data-testid="package-guidance-cta"', false)
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode(PackageContactLink::ORIENTATION_MESSAGE),
false,
);
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), false);
}
public function test_package_cta_falls_back_to_briefing_without_whatsapp_number(): void
@@ -56,18 +50,4 @@ class ServicesPageCtaTest extends TestCase
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
->assertDontSee('wa.me/', false);
}
public function test_orientation_cta_uses_distinct_whatsapp_message_when_a_number_is_configured(): void
{
SiteSetting::instance()->update(['whatsapp_number' => '+55 11 98888-7777']);
$response = $this->get(route('services.index'));
$response
->assertOk()
->assertSee(
'https://wa.me/5511988887777?text='.rawurlencode(PackageContactLink::ORIENTATION_MESSAGE),
false,
);
}
}

View File

@@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import { bunny } from 'laravel-vite-plugin/fonts';
import { fontsource } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite';
/**
@@ -76,7 +76,7 @@ export default defineConfig({
],
refresh: true,
fonts: [
bunny('EB Garamond', {
fontsource('EB Garamond', {
weights: [400, 500, 600],
}),
],