Compare commits

..

5 Commits

Author SHA1 Message Date
900b76ad19 merge: integrar main (MAN-139) na branch de modalidades
Some checks failed
CI / static (pull_request) Successful in 2m1s
CI / unit (pull_request) Successful in 4m1s
CI / feature (pull_request) Successful in 3m1s
CI / container (pull_request) Successful in 55s
CI / browser (pull_request) Failing after 2m13s
Resolve conflito em PackageContactLink: mantém forOrientation do main e a mensagem de orientação da Ata.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:43:16 -03:00
9ee9b51b0c feat: CTA de orientação das modalidades via WhatsApp
Some checks failed
CI / static (pull_request) Successful in 2m55s
CI / unit (pull_request) Successful in 3m27s
CI / feature (pull_request) Successful in 2m57s
CI / container (pull_request) Successful in 59s
CI / browser (pull_request) Failing after 2m11s
A banda "ainda não sabe" abre wa.me com mensagem de orientação; sem número, cai no briefing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:39:29 -03:00
220143390e feat: alinhar copy das modalidades à Apresentação
Cards e páginas /pacotes passam a usar o texto conceitual oficial, sem preço, VIP ou limites comerciais.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 23:39:29 -03:00
fd3a4eb5f1 feat: CTA orientação WhatsApp para indecisos (MAN-127) (#7)
All checks were successful
CI / static (push) Successful in 2m33s
CI / unit (push) Successful in 3m39s
CI / feature (push) Successful in 2m49s
CI / container (push) Successful in 20s
CI / browser (push) Successful in 4m9s
## Summary

- CTA orientação (banda “Ainda não sabe?”) na home e em `/servicos` abre WhatsApp com mensagem distinta; sem número oficial, cai em `/briefing`.
- Glossário canônico em `SPEC.md` §8.0 (Vertente, Modalidade, Briefing, Contato parceiro, CTA contextual, CTA orientação). Sem `CONTEXT.md` paralelo.
- Linear: https://linear.app/maneco-workspace/issue/MAN-127/ajustar-ctas-consultivos-no-whatsapp-e-briefing-comercial

## Test plan

- [x] Feature: `HomePageContentTest` + `ServicesPageCtaTest` (17 passed)
- [ ] CI Gitea (static, unit, feature, browser, container)
- [ ] Conferir banda de modalidades com e sem `whatsapp_number` no Filament

Reviewed-on: #7
Co-authored-by: manoel freitas <manoel.josefneto@gmail.com>
Co-committed-by: manoel freitas <manoel.josefneto@gmail.com>
2026-08-14 02:35:06 +00:00
3207b7e568 docs: registrar Modalidade vs Service no modelo de domínio
O visitante vê modalidades de casamento; services permanece vertente. Preço e limites ficam fora do registro público.

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

View File

@@ -79,4 +79,4 @@ Default vocabulary: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-
### Domain docs ### Domain docs
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. 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`.

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. - **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`. - **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/`. - **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`, `/portfolio`, `/portfolio/{slug}`, `/sobre`, `/privacidade`, `/contato`. `APP_LOCALE=pt_BR`, `APP_TIMEZONE=America/Sao_Paulo` (`config/app.php:68`). - **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.
- **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`. - **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 ## 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 ## Operating Context
- Atuação em São Paulo, capital. - Atuação em São Paulo, capital.
- Serviços: casamentos, eventos corporativos e celebrações. - Serviços: casamentos, eventos corporativos e celebrações. 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.
- Operação realizada por assessora principal e equipe pequena. - 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 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. - 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,11 +283,15 @@ Uma funcionalidade fora do MVP só poderá entrar quando:
| Método | Rota | Nome sugerido | Finalidade | | Método | Rota | Nome sugerido | Finalidade |
|---|---|---|---| |---|---|---|---|
| GET | `/` | `home` | Home editorial | | GET | `/` | `home` | Home editorial |
| GET | `/servicos` | `services.index` | Lista de serviços publicados | | GET | `/servicos` | `services.index` | Lista de serviços / modalidades publicadas |
| GET | `/pacotes/{slug}` | `packages.show` | Detalhe de uma modalidade de casamento |
| GET | `/portfolio` | `portfolio.index` | Lista de casos publicados | | GET | `/portfolio` | `portfolio.index` | Lista de casos publicados |
| GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso | | GET | `/portfolio/{slug}` | `portfolio.show` | Detalhe de caso |
| GET | `/sobre` | `about` | História, método e credenciais | | GET | `/sobre` | `about` | História, método e credenciais |
| GET | `/contato` | `contact` | Briefing de contato | | 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 | `/privacidade` | `privacy` | Política de privacidade | | GET | `/privacidade` | `privacy` | Política de privacidade |
| GET | `/sitemap.xml` | `sitemap` | Sitemap público | | GET | `/sitemap.xml` | `sitemap` | Sitemap público |
| GET | `/robots.txt` | `robots` | Política de crawling | | GET | `/robots.txt` | `robots` | Política de crawling |
@@ -324,7 +328,8 @@ Administração
- Dashboard: `Filament Page` customizada com widgets orientados a exceção. - Dashboard: `Filament Page` customizada com widgets orientados a exceção.
- Detalhe do evento: página customizada do Resource com resumo operacional. - Detalhe do evento: página customizada do Resource com resumo operacional.
- Briefing público: Blade + Controller (`POST /contato`), ver §11.2. - Briefing comercial: Blade + Controller (`POST /briefing`), ver §11.2.
- Contato de parceiro: Blade + Controller (`POST /contato`) — canal separado do briefing.
- Home: Blade com componentes de design reutilizáveis. - Home: Blade com componentes de design reutilizáveis.
### 5.4 Decisões de UX YAGNI ### 5.4 Decisões de UX YAGNI
@@ -1243,6 +1248,35 @@ Auditoria não precisa ser um event sourcing. Registrar apenas operações crít
## 8. Modelo de domínio e banco de dados ## 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 ### 8.1 Módulos
| Módulo | Responsabilidade | | Módulo | Responsabilidade |
@@ -1254,6 +1288,12 @@ Auditoria não precisa ser um event sourcing. Registrar apenas operações crít
| Finance | Orçamento e pagamentos manuais | | Finance | Orçamento e pagamentos manuais |
| Identity | Usuários, acesso e auditoria | | 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 ### 8.2 Tabelas obrigatórias
#### `users` #### `users`
@@ -1314,6 +1354,27 @@ Singleton:
- timestamps; - timestamps;
- soft delete opcional somente se recuperação for necessária. - 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` #### `portfolio_cases`
- `id`; - `id`;
@@ -1795,7 +1856,7 @@ Não transformar seções estáticas em componentes Livewire. Usar Blade quando
### 11.2 Formulário de briefing ### 11.2 Formulário de briefing
> **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. > **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.
O formulário deve: O formulário deve:

View File

@@ -8,20 +8,45 @@ use App\Models\SiteSetting;
final class PackageContactLink 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. * Resolve the contextual contact link for a wedding package modality.
* *
* @return array{href: string, isWhatsapp: bool} * @return array{href: string, isWhatsapp: bool}
*/ */
public static function for(SiteSetting $settings, string $packageName): array 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); $digits = preg_replace('/\D/', '', (string) $settings->whatsapp_number);
$isWhatsapp = strlen($digits) >= 10; $isWhatsapp = strlen($digits) >= 10;
return [ return [
'href' => $isWhatsapp 'href' => $isWhatsapp
? 'https://wa.me/'.$digits.'?text='.rawurlencode('Olá, gostaria de conversar sobre a modalidade '.$packageName.' para meu casamento.') ? 'https://wa.me/'.$digits.'?text='.rawurlencode($whatsappMessage)
: route('briefing', ['servico_interesse' => $packageName]), : route('briefing', $briefingQuery),
'isWhatsapp' => $isWhatsapp, 'isWhatsapp' => $isWhatsapp,
]; ];
} }

View File

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

View File

@@ -1,51 +1,28 @@
# Domain Docs # Domain Docs
How the engineering skills should consume this repo's domain documentation when exploring the codebase. How engineering skills should consume this repo's domain documentation.
## Before exploring, read these ## Single source of truth
- **`CONTEXT.md`** at the repo root, or **`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-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. Also useful:
## File structure - **`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
Single-context repo (most repos): ## Before exploring
``` 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 ## 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. 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_`).
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`). 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`.
## Flag ADR conflicts ## Flag ADR conflicts
If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: If output contradicts an ADR in `SPEC.md` §21, 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,7 +5,6 @@
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"@fontsource/eb-garamond": "^5.2.7",
"@tailwindcss/vite": "^4.0.0", "@tailwindcss/vite": "^4.0.0",
"concurrently": "^9.0.1", "concurrently": "^9.0.1",
"daisyui": "^5.7.16", "daisyui": "^5.7.16",
@@ -50,16 +49,6 @@
"tslib": "^2.4.0" "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": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",

View File

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

View File

@@ -189,18 +189,6 @@
overflow-x: clip; 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) { @media (prefers-reduced-motion: no-preference) {
:where(a, button, input, textarea, select) { :where(a, button, input, textarea, select) {
transition-duration: var(--amare-motion-feedback); transition-duration: var(--amare-motion-feedback);
@@ -212,45 +200,6 @@
transform: translateY(1px); 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). */ /* 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] { html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) [data-motion-beat] {
transform: translateY(var(--amare-motion-distance-y)); transform: translateY(var(--amare-motion-distance-y));

View File

@@ -24,7 +24,7 @@
<div class="lg:col-span-6 lg:col-start-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> <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"> <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="link-editorial">Ver portfólio completo</span> <span class="border-b border-amare-accent pb-1">Ver portfólio completo</span>
</a> </a>
</div> </div>
</div> </div>

View File

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

View File

@@ -19,7 +19,7 @@
@if (filled($settings->hero_secondary_cta_label)) @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"> <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="link-editorial">{{ $settings->hero_secondary_cta_label }}</span> <span class="border-b border-amare-accent pb-1">{{ $settings->hero_secondary_cta_label }}</span>
</a> </a>
@endif @endif
</div> </div>

View File

@@ -14,9 +14,6 @@
'note' => 'Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.', 'note' => 'Nomenclaturas exibidas conforme materiais/reunião; confirmar versão final antes da publicação.',
]) ])
@php
$bandHref = $bandCtaHref ?? route('briefing');
@endphp
<section <section
aria-labelledby="packages-heading" aria-labelledby="packages-heading"
class="home-chapter border-b border-amare-border bg-amare-bg" class="home-chapter border-b border-amare-border bg-amare-bg"
@@ -64,7 +61,7 @@
href="{{ route('packages.show', $package->slug) }}" 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" 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="link-editorial">Conhecer esta modalidade</span> <span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
</a> </a>
</div> </div>
</article> </article>
@@ -72,12 +69,20 @@
</div> </div>
@endif @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="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"> <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> <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> <p class="text-amare-accent-text/80">{{ $bandBody }}</p>
</div> </div>
<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"> <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"
>
Conversar com a Amare Conversar com a Amare
</a> </a>
</div> </div>

View File

@@ -35,7 +35,7 @@
</ul> </ul>
<p> <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"> <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="link-editorial">Conhecer a Amare</span> <span class="border-b border-amare-accent pb-1">Conhecer a Amare</span>
</a> </a>
</p> </p>
</div> </div>

View File

@@ -27,7 +27,7 @@
<p> <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"> <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="link-editorial">Ver todos os serviços</span> <span class="border-b border-amare-accent pb-1">Ver todos os serviços</span>
</a> </a>
</p> </p>
</div> </div>

View File

@@ -9,7 +9,7 @@
<ol class="grid gap-8 border-t border-amare-border pt-6 md:grid-cols-3"> <ol class="grid gap-8 border-t border-amare-border pt-6 md:grid-cols-3">
@foreach ($packages as $package) @foreach ($packages as $package)
@php($contactLink = \App\Support\PackageContactLink::for($settings, $package->name)) @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="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> <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>
@endforeach @endforeach
</ol> </ol>
@endif @endif

View File

@@ -1,44 +0,0 @@
@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) { } catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta( $pageMeta = new \App\Application\Data\PageMeta(
title: 'Página não encontrada - Amare Assessoria', title: 'Página não encontrada - Amare Assessoria',
description: 'Não encontramos este endereço. Volte à home e continue por ali.', description: 'A página que você procura não existe ou foi movida.',
canonical: '', canonical: '',
robots: 'noindex, nofollow', 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> <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> <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> <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">Não encontramos este endereço talvez tenha mudado de lugar. A home continua sendo o melhor ponto de partida.</p> <p class="text-amare-muted" data-reveal data-reveal-from="up">O endereço que você tentou abrir não existe ou foi movido.</p>
<p data-reveal data-reveal-from="up"> <p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"> <a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) { } catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta( $pageMeta = new \App\Application\Data\PageMeta(
title: 'Sessão expirada - Amare Assessoria', title: 'Sessão expirada - Amare Assessoria',
description: 'Sua sessão ficou quieta demais. Volte e envie de novo com calma.', description: 'Sua sessão expirou. Volte e tente enviar novamente.',
canonical: '', canonical: '',
robots: 'noindex, nofollow', 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> <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> <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> <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 ficou quieta demais e precisamos recomeçar. Volte à página anterior e envie de novo os dados ainda estão seguros.</p> <p class="text-amare-muted" data-reveal data-reveal-from="up">Sua sessão expirou. Volte e tente enviar novamente.</p>
<p data-reveal data-reveal-from="up"> <p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"> <a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) { } catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta( $pageMeta = new \App\Application\Data\PageMeta(
title: 'Muitas solicitações - Amare Assessoria', title: 'Muitas solicitações - Amare Assessoria',
description: 'Recebemos muitos pedidos em pouco tempo. Aguarde um instante e tente de novo.', description: 'Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.',
canonical: '', canonical: '',
robots: 'noindex, nofollow', 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> <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> <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> <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">Recebemos vários pedidos em pouco tempo. Espere um instante assim que pudermos, tentamos de novo com calma.</p> <p class="text-amare-muted" data-reveal data-reveal-from="up">Você enviou muitas solicitações em pouco tempo. Aguarde um instante e tente novamente.</p>
<p data-reveal data-reveal-from="up"> <p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"> <a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) { } catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta( $pageMeta = new \App\Application\Data\PageMeta(
title: 'Algo deu errado - Amare Assessoria', title: 'Algo deu errado - Amare Assessoria',
description: 'Algo saiu do roteiro do nosso lado. Tente novamente em instantes.', description: 'Não foi possível concluir o pedido. Tente novamente em instantes.',
canonical: '', canonical: '',
robots: 'noindex, nofollow', 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> <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> <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> <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">Algo saiu do roteiro do nosso lado. estamos cuidando disso tente novamente em instantes.</p> <p class="text-amare-muted" data-reveal data-reveal-from="up">o foi possível concluir o pedido agora. Tente novamente em instantes.</p>
<p data-reveal data-reveal-from="up"> <p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"> <a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home Voltar para a home

View File

@@ -5,7 +5,7 @@
} catch (\Throwable $e) { } catch (\Throwable $e) {
$pageMeta = new \App\Application\Data\PageMeta( $pageMeta = new \App\Application\Data\PageMeta(
title: 'Em manutenção - Amare Assessoria', title: 'Em manutenção - Amare Assessoria',
description: 'Estamos em uma pausa breve de manutenção. Voltamos em instantes.', description: 'Estamos realizando uma breve manutenção. Tente novamente em instantes.',
canonical: '', canonical: '',
robots: 'noindex, nofollow', 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> <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> <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> <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 em uma pausa breve para deixar tudo no lugar. Voltamos em instantes obrigado pela paciência.</p> <p class="text-amare-muted" data-reveal data-reveal-from="up">Estamos realizando uma breve manutenção. Tente novamente em instantes.</p>
<p data-reveal data-reveal-from="up"> <p data-reveal data-reveal-from="up">
<a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]"> <a href="{{ route('home') }}" class="btn btn-primary text-sm font-semibold uppercase tracking-[0.12em]">
Voltar para a home 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"> <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"> <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> <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="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('/#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="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="{{ 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="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('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="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('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="{{ 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> <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> </nav>

View File

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

View File

@@ -20,12 +20,7 @@
<div class="container-amare py-16 md:py-24" data-reveal-group> <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> <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') @if (session('status') === 'partner-inquiry-sent')
<x-public.form-success <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>
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 @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"> <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 @csrf
@@ -33,10 +28,10 @@
@if ($errors->any())<div role="alert" class="border border-amare-error/40 px-6 py-5">Revise os campos destacados abaixo.</div>@endif @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"> <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) @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="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> <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>
@endforeach @endforeach
</div> </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> <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>
<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> <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> <button type="submit" data-submit-button class="btn btn-primary text-sm font-semibold uppercase tracking-[.12em]">Enviar apresentação</button>
</form> </form>

View File

@@ -13,7 +13,7 @@
Conhecer os serviços Conhecer os serviços
</a> </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"> <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="link-editorial">Tenho um evento em mente</span> <span class="border-b border-amare-accent pb-1">Tenho um evento em mente</span>
</a> </a>
</div> </div>
</x-public.photo-hero> </x-public.photo-hero>
@@ -47,7 +47,6 @@
:show-intro="false" :show-intro="false"
kicker="tag" kicker="tag"
:show-subtitle="true" :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." 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" :show-note="false"
/> />
@@ -65,7 +64,7 @@
<p class="text-amare-text-muted">{{ $package->compare_summary }}</p> <p class="text-amare-text-muted">{{ $package->compare_summary }}</p>
</div> </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"> <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="link-editorial">Conhecer esta modalidade</span> <span class="border-b border-amare-accent pb-1">Conhecer esta modalidade</span>
</a> </a>
</article> </article>
@endif @endif

View File

@@ -38,6 +38,33 @@ class WeddingPackageTest extends TestCase
['Essenza', 'Conduzione', 'Grand Jour'], ['Essenza', 'Conduzione', 'Grand Jour'],
WeddingPackage::query()->published()->orderBy('sort_order')->pluck('name')->all(), 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 public function test_factory_generates_a_slug_and_autofills_blank_slug_from_name(): void

View File

@@ -89,28 +89,6 @@ 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 public function test_submission_normalizes_email_and_phone_before_reaching_mailables(): void
{ {
$settings = SiteSetting::instance(); $settings = SiteSetting::instance();

View File

@@ -72,19 +72,7 @@ class HeritageEditorialTokensTest extends TestCase
{ {
$vite = (string) file_get_contents(base_path('vite.config.js')); $vite = (string) file_get_contents(base_path('vite.config.js'));
$this->assertMatchesRegularExpression( $this->assertStringContainsString("bunny('EB Garamond'", $vite);
"/^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); $this->assertStringNotContainsString('Instrument Sans', $vite);
} }

View File

@@ -8,6 +8,7 @@ use App\Models\PortfolioCase;
use App\Models\SiteSetting; use App\Models\SiteSetting;
use App\Models\Testimonial; use App\Models\Testimonial;
use App\Models\WeddingPackage; use App\Models\WeddingPackage;
use App\Support\PackageContactLink;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@@ -213,7 +214,24 @@ class HomePageContentTest extends TestCase
false, false,
) )
->assertSee('target="_blank"', false) ->assertSee('target="_blank"', false)
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), 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);
} }
public function test_corporate_steps_and_placeholder_render_from_settings(): void public function test_corporate_steps_and_placeholder_render_from_settings(): void

View File

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

View File

@@ -66,22 +66,6 @@ class PartnerInquiryTest extends TestCase
Mail::assertQueued(PartnerInquiry::class, 1); 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 public function test_partner_honeypot_reports_success_without_sending_mail(): void
{ {
SiteSetting::instance(); SiteSetting::instance();

View File

@@ -6,6 +6,7 @@ namespace Tests\Feature\PublicSite;
use App\Models\SiteSetting; use App\Models\SiteSetting;
use App\Models\WeddingPackage; use App\Models\WeddingPackage;
use App\Support\PackageContactLink;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase; use Tests\TestCase;
@@ -31,7 +32,12 @@ class ServicesPageCtaTest extends TestCase
false, false,
) )
->assertSee('target="_blank"', false) ->assertSee('target="_blank"', false)
->assertDontSee(route('briefing', ['servico_interesse' => 'Grand Jour']), 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_package_cta_falls_back_to_briefing_without_whatsapp_number(): void public function test_package_cta_falls_back_to_briefing_without_whatsapp_number(): void
@@ -50,4 +56,18 @@ class ServicesPageCtaTest extends TestCase
->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false) ->assertSee('href="'.route('briefing', ['servico_interesse' => 'Essenza']).'"', false)
->assertDontSee('wa.me/', 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 { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin'; import laravel from 'laravel-vite-plugin';
import { fontsource } from 'laravel-vite-plugin/fonts'; import { bunny } from 'laravel-vite-plugin/fonts';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
/** /**
@@ -76,7 +76,7 @@ export default defineConfig({
], ],
refresh: true, refresh: true,
fonts: [ fonts: [
fontsource('EB Garamond', { bunny('EB Garamond', {
weights: [400, 500, 600], weights: [400, 500, 600],
}), }),
], ],