Compare commits
1 Commits
fix/browse
...
b54908541f
| Author | SHA1 | Date | |
|---|---|---|---|
| b54908541f |
@@ -12,5 +12,6 @@ related_targets: ["resources/views/pages/services/index.blade.php","resources/vi
|
||||
- **Action, proof, and constraints:** ação principal é enviar briefing inicial. Prova disponível: cinco depoimentos reais de casamentos, com nomes e datas; publicação depende de autorização final. Não inventar cases corporativos, credenciais ou resultados. Fotografias permanecem ilustrativas e marcadas até chegada de acervo autorizado.
|
||||
- **Chosen direction:** “Dossiê Editorial do Evento”. Home funciona como capa e índice; serviços viram capítulos, portfólio vira cadernos de caso, sobre vira perfil editorial e contato vira ficha de briefing. Sistema visual global segue Heritage Editorial em DESIGN.md.
|
||||
- **Memorable moment:** coração facetado atua como selo editorial enquanto índice discreto acompanha capítulos e deixa serviço, método e próximo passo visíveis sem transformar página em dashboard.
|
||||
- **Motion thesis (Dossiê vivo):** Home recebe abertura autoral (selo → título → recorte de imagem → CTAs, ≤800ms). Índice lateral no desktop e linha de progresso no mobile acompanham capítulos. Serviços, portfólio e sobre usam abertura curta e revelações discretas; contato, privacidade e erros permanecem quase estáticos. Sem parallax, loops, bounce ou scroll-jacking. `prefers-reduced-motion` entrega estado final imediato.
|
||||
- **Responsive and interaction:** spreads assimétricos no desktop; sequência linear no mobile. CTAs recorrentes, navegação clara, formulário com loading, erro e sucesso, foco visível e suporte a movimento reduzido.
|
||||
- **Unresolved:** logo transparente ou vetorial; fotografias autorizadas; WhatsApp, e-mail e Instagram oficiais; textos jurídicos; autorização dos depoimentos; provas reais de eventos corporativos.
|
||||
|
||||
@@ -61,6 +61,10 @@
|
||||
font-family: var(--amare-font-serif);
|
||||
}
|
||||
|
||||
[id$='-heading'] {
|
||||
scroll-margin-top: 5.5rem;
|
||||
}
|
||||
|
||||
a:focus-visible,
|
||||
button:focus-visible,
|
||||
summary:focus-visible,
|
||||
@@ -98,4 +102,147 @@
|
||||
body.menu-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dossiê vivo — content visible by default; enhance only when opted in */
|
||||
[data-chapter-index] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[data-chapter-progress] {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
[data-chapter-index] a[aria-current="true"] {
|
||||
color: var(--amare-color-accent-deep);
|
||||
}
|
||||
|
||||
[data-chapter-index] a[aria-current="true"]::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0.35em;
|
||||
bottom: 0.35em;
|
||||
width: 1px;
|
||||
background: var(--amare-color-accent);
|
||||
}
|
||||
|
||||
[data-chapter-progress] > span {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: var(--chapter-progress, 0%);
|
||||
background: var(--amare-color-accent);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
[data-chapter-index] {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
right: max(1rem, calc((100vw - var(--amare-container-max)) / 2 - 7.5rem));
|
||||
z-index: 30;
|
||||
max-width: 6.5rem;
|
||||
translate: 0 -50%;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
[data-chapter-index] a {
|
||||
position: relative;
|
||||
padding-left: 0.75rem;
|
||||
font-size: var(--amare-text-xs);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--amare-color-muted);
|
||||
text-decoration: none;
|
||||
transition: color var(--amare-duration-fast) var(--amare-ease-standard);
|
||||
}
|
||||
|
||||
[data-chapter-index] a:hover {
|
||||
color: var(--amare-color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1279px) {
|
||||
[data-chapter-progress] {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 45;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="seal"],
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="cta"] {
|
||||
opacity: 0;
|
||||
transform: translateY(0.5rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="title"] {
|
||||
transform: translateY(0.4rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"] [data-motion-beat="media"] {
|
||||
clip-path: inset(4% 0 0 0);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
clip-path: inset(0 0 0 0);
|
||||
transition:
|
||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
clip-path var(--amare-duration-focal) var(--amare-ease-arrival);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat="seal"] {
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat="title"] {
|
||||
transition-delay: 80ms;
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat="media"] {
|
||||
transition-delay: 120ms;
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="dossie-hero"].is-active [data-motion-beat="cta"] {
|
||||
transition-delay: 220ms;
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-reveal]:not(.is-revealed) {
|
||||
opacity: 0;
|
||||
transform: translateY(0.5rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-reveal].is-revealed {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition:
|
||||
opacity var(--amare-duration-slow) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-slow) var(--amare-ease-arrival);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="page-open"]:not(.is-active) {
|
||||
opacity: 0;
|
||||
transform: translateY(0.4rem);
|
||||
}
|
||||
|
||||
html[data-motion="enhance"] [data-motion="page-open"].is-active {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition:
|
||||
opacity var(--amare-duration-normal) var(--amare-ease-arrival),
|
||||
transform var(--amare-duration-normal) var(--amare-ease-arrival);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,9 @@
|
||||
--amare-duration-fast: 150ms;
|
||||
--amare-duration-normal: 250ms;
|
||||
--amare-duration-slow: 400ms;
|
||||
--amare-duration-focal: 720ms;
|
||||
--amare-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--amare-ease-arrival: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@@ -64,6 +66,7 @@
|
||||
--amare-duration-fast: 0.01ms;
|
||||
--amare-duration-normal: 0.01ms;
|
||||
--amare-duration-slow: 0.01ms;
|
||||
--amare-duration-focal: 0.01ms;
|
||||
}
|
||||
|
||||
*,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './motion.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const menuButton = document.querySelector('[data-menu-button]');
|
||||
const navigation = document.querySelector('[data-main-nav]');
|
||||
|
||||
166
resources/js/motion.js
Normal file
166
resources/js/motion.js
Normal file
@@ -0,0 +1,166 @@
|
||||
const MOTION_QUERY = '(prefers-reduced-motion: reduce)';
|
||||
|
||||
function prefersReducedMotion() {
|
||||
return window.matchMedia(MOTION_QUERY).matches;
|
||||
}
|
||||
|
||||
function enableEnhancement() {
|
||||
if (prefersReducedMotion()) {
|
||||
document.documentElement.removeAttribute('data-motion');
|
||||
return false;
|
||||
}
|
||||
|
||||
document.documentElement.dataset.motion = 'enhance';
|
||||
return true;
|
||||
}
|
||||
|
||||
function activateHero(enhance) {
|
||||
const hero = document.querySelector('[data-motion="dossie-hero"]');
|
||||
if (!hero) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activate = () => hero.classList.add('is-active');
|
||||
|
||||
if (!enhance) {
|
||||
activate();
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(activate);
|
||||
});
|
||||
}
|
||||
|
||||
function activatePageOpen(enhance) {
|
||||
document.querySelectorAll('[data-motion="page-open"]').forEach((node) => {
|
||||
if (!enhance) {
|
||||
node.classList.add('is-active');
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => node.classList.add('is-active'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function observeReveals(enhance) {
|
||||
const nodes = Array.from(document.querySelectorAll('[data-reveal]'));
|
||||
if (nodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enhance || typeof IntersectionObserver !== 'function') {
|
||||
nodes.forEach((node) => node.classList.add('is-revealed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.target.classList.add('is-revealed');
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
},
|
||||
{
|
||||
rootMargin: '0px 0px -12% 0px',
|
||||
threshold: 0.2,
|
||||
},
|
||||
);
|
||||
|
||||
nodes.forEach((node) => observer.observe(node));
|
||||
}
|
||||
|
||||
function setupChapterIndex() {
|
||||
const index = document.querySelector('[data-chapter-index]');
|
||||
const progress = document.querySelector('[data-chapter-progress] span');
|
||||
const chapters = Array.from(document.querySelectorAll('[data-chapter]'));
|
||||
|
||||
if (!index || chapters.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = Array.from(index.querySelectorAll('a[href^="#"]'));
|
||||
|
||||
const setActive = (id) => {
|
||||
links.forEach((link) => {
|
||||
const isCurrent = link.getAttribute('href') === `#${id}`;
|
||||
if (isCurrent) {
|
||||
link.setAttribute('aria-current', 'true');
|
||||
} else {
|
||||
link.removeAttribute('aria-current');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateProgress = (ratio) => {
|
||||
if (!progress) {
|
||||
return;
|
||||
}
|
||||
|
||||
const clamped = Math.min(1, Math.max(0, ratio));
|
||||
progress.style.setProperty('--chapter-progress', `${(clamped * 100).toFixed(2)}%`);
|
||||
};
|
||||
|
||||
const sync = () => {
|
||||
const marker = window.scrollY + Math.min(window.innerHeight * 0.35, 280);
|
||||
let current = chapters[0];
|
||||
|
||||
chapters.forEach((chapter) => {
|
||||
if (chapter.offsetTop <= marker) {
|
||||
current = chapter;
|
||||
}
|
||||
});
|
||||
|
||||
const heading = current.querySelector('[id$="-heading"]') || document.getElementById(`${current.dataset.chapter}-heading`);
|
||||
const headingId = heading?.id
|
||||
|| current.getAttribute('aria-labelledby')
|
||||
|| `${current.dataset.chapter}-heading`;
|
||||
|
||||
setActive(headingId);
|
||||
|
||||
const doc = document.documentElement;
|
||||
const max = Math.max(1, doc.scrollHeight - window.innerHeight);
|
||||
updateProgress(window.scrollY / max);
|
||||
};
|
||||
|
||||
setActive(chapters[0].getAttribute('aria-labelledby') || 'hero-heading');
|
||||
sync();
|
||||
|
||||
window.addEventListener('scroll', sync, { passive: true });
|
||||
window.addEventListener('resize', sync);
|
||||
}
|
||||
|
||||
function bootMotion() {
|
||||
const enhance = enableEnhancement();
|
||||
activateHero(enhance);
|
||||
activatePageOpen(enhance);
|
||||
observeReveals(enhance);
|
||||
setupChapterIndex();
|
||||
|
||||
window.matchMedia(MOTION_QUERY).addEventListener('change', (event) => {
|
||||
if (event.matches) {
|
||||
document.documentElement.removeAttribute('data-motion');
|
||||
document.querySelectorAll('[data-motion="dossie-hero"], [data-motion="page-open"]').forEach((node) => {
|
||||
node.classList.add('is-active');
|
||||
});
|
||||
document.querySelectorAll('[data-reveal]').forEach((node) => {
|
||||
node.classList.add('is-revealed');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.dataset.motion = 'enhance';
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootMotion, { once: true });
|
||||
} else {
|
||||
bootMotion();
|
||||
}
|
||||
15
resources/views/components/home/chapter-index.blade.php
Normal file
15
resources/views/components/home/chapter-index.blade.php
Normal file
@@ -0,0 +1,15 @@
|
||||
@props([
|
||||
'chapters' => [],
|
||||
])
|
||||
|
||||
@if (count($chapters) > 0)
|
||||
<nav data-chapter-index aria-label="Índice de capítulos">
|
||||
@foreach ($chapters as $chapter)
|
||||
<a href="#{{ $chapter['id'] }}">{{ $chapter['label'] }}</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
<div data-chapter-progress aria-hidden="true">
|
||||
<span></span>
|
||||
</div>
|
||||
@endif
|
||||
@@ -2,8 +2,8 @@
|
||||
'settings',
|
||||
])
|
||||
|
||||
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20">
|
||||
<div class="container-amare space-y-6 text-center">
|
||||
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20" data-chapter="final-cta">
|
||||
<div class="container-amare space-y-6 text-center" data-reveal>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Próximo passo</p>
|
||||
<h2 id="final-cta-heading" class="text-3xl font-medium text-amare-text md:text-4xl">Todo grande encontro começa com uma boa conversa.</h2>
|
||||
<p class="mx-auto max-w-2xl text-amare-muted">
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
'settings',
|
||||
])
|
||||
|
||||
<section aria-labelledby="hero-heading" class="border-b border-amare-border bg-amare-bg">
|
||||
<section
|
||||
aria-labelledby="hero-heading"
|
||||
class="border-b border-amare-border bg-amare-bg"
|
||||
data-chapter="hero"
|
||||
data-motion="dossie-hero"
|
||||
>
|
||||
<div class="container-amare grid gap-12 py-20 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:items-center md:py-28">
|
||||
<div class="space-y-8">
|
||||
@if (filled($settings->hero_eyebrow))
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
||||
@endif
|
||||
<div data-motion-beat="seal" class="flex items-center gap-4">
|
||||
<x-brand.logo mark variant="on-light" class="h-8 w-auto" alt="" />
|
||||
@if (filled($settings->hero_eyebrow))
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h1 id="hero-heading" class="max-w-3xl text-4xl font-medium leading-none text-amare-text md:text-5xl">
|
||||
<h1 id="hero-heading" data-motion-beat="title" class="max-w-3xl text-4xl font-medium leading-none text-amare-text md:text-5xl">
|
||||
{{ $settings->hero_title }}
|
||||
</h1>
|
||||
|
||||
@@ -17,7 +25,7 @@
|
||||
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
|
||||
<a
|
||||
href="{{ route('contact') }}"
|
||||
data-testid="home-primary-cta"
|
||||
@@ -42,7 +50,7 @@
|
||||
</div>
|
||||
|
||||
@if (filled($settings->default_og_image_path))
|
||||
<div class="min-h-72 bg-amare-bg-deep">
|
||||
<div data-motion-beat="media" class="min-h-72 overflow-hidden bg-amare-bg-deep">
|
||||
<x-media.image
|
||||
:path="$settings->default_og_image_path"
|
||||
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
||||
@endphp
|
||||
|
||||
<section aria-labelledby="manifesto-heading" class="border-b border-amare-border bg-amare-bg-deep py-20">
|
||||
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||
<section aria-labelledby="manifesto-heading" class="border-b border-amare-border bg-amare-bg-deep py-20" data-chapter="manifesto">
|
||||
<div class="container-amare grid gap-8 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Manifesto</p>
|
||||
<div class="space-y-6">
|
||||
<h2 id="manifesto-heading" class="max-w-3xl text-3xl font-medium leading-tight text-amare-text md:text-4xl">{{ $title }}</h2>
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
$intro = $settings->method_intro ?: 'Clareza em cada etapa. Tranquilidade durante todo o processo.';
|
||||
@endphp
|
||||
|
||||
<section aria-labelledby="method-heading" class="border-b border-amare-border bg-amare-bg-archive py-20">
|
||||
<section aria-labelledby="method-heading" class="border-b border-amare-border bg-amare-bg-archive py-20" data-chapter="method">
|
||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)] md:items-start">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Método</p>
|
||||
<h2 id="method-heading" class="text-3xl font-medium text-amare-text">Cuidado orientado por processo.</h2>
|
||||
<p class="text-amare-text-muted">{{ $intro }}</p>
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<ol class="grid gap-5 border-t border-amare-border">
|
||||
@foreach ($steps as $index => $step)
|
||||
<li class="grid gap-2 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,1fr)]">
|
||||
<li class="grid gap-2 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,1fr)]" data-reveal>
|
||||
<span class="text-sm text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-2xl font-medium text-amare-text">{{ $step['title'] ?? '' }}</h3>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
])
|
||||
|
||||
@if ($cases->isNotEmpty())
|
||||
<section aria-labelledby="portfolio-heading" class="border-b border-amare-accent-deep bg-amare-accent-deep py-20 text-amare-accent-text">
|
||||
<section aria-labelledby="portfolio-heading" class="border-b border-amare-accent-deep bg-amare-accent-deep py-20 text-amare-accent-text" data-chapter="portfolio">
|
||||
<div class="container-amare space-y-10">
|
||||
<div class="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||
<div class="grid gap-4 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent-text/80">Portfólio</p>
|
||||
<div class="space-y-3">
|
||||
<h2 id="portfolio-heading" class="text-3xl font-medium md:text-4xl">Celebrações que ganham forma com intenção.</h2>
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
<div class="grid gap-10 md:grid-cols-2">
|
||||
@foreach ($cases as $case)
|
||||
<article class="space-y-4 border-t border-amare-accent-text/30 pt-4">
|
||||
<article class="space-y-4 border-t border-amare-accent-text/30 pt-4" data-reveal>
|
||||
@if (filled($case->cover_image_path))
|
||||
<x-media.image
|
||||
:path="$case->cover_image_path"
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
||||
@endphp
|
||||
|
||||
<section aria-labelledby="positioning-heading" class="border-b border-amare-border py-20">
|
||||
<section aria-labelledby="positioning-heading" class="border-b border-amare-border py-20" data-chapter="positioning">
|
||||
<div class="container-amare grid gap-10 md:grid-cols-[minmax(0,0.8fr)_minmax(0,1.2fr)]">
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-3" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">A Amare</p>
|
||||
<h2 id="positioning-heading" class="text-3xl font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||
</div>
|
||||
<div class="space-y-8">
|
||||
<div class="space-y-8" data-reveal>
|
||||
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
||||
<ul class="grid gap-3 border-t border-amare-border">
|
||||
@foreach ($principles as $principle)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
])
|
||||
|
||||
@if ($services->isNotEmpty())
|
||||
<section aria-labelledby="services-heading" class="border-b border-amare-border py-20">
|
||||
<section aria-labelledby="services-heading" class="border-b border-amare-border py-20" data-chapter="services">
|
||||
<div class="container-amare space-y-10">
|
||||
<div class="max-w-2xl space-y-3">
|
||||
<div class="max-w-2xl space-y-3" data-reveal>
|
||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Atuação</p>
|
||||
<h2 id="services-heading" class="text-3xl font-medium text-amare-text">Serviços</h2>
|
||||
<p class="text-amare-text-muted">Assessoria sob medida para decisões importantes e celebrações bem conduzidas.</p>
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<ol class="border-t border-amare-border">
|
||||
@foreach ($services as $index => $service)
|
||||
<li class="grid gap-3 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,0.8fr)_minmax(0,1.2fr)] md:gap-6">
|
||||
<li class="grid gap-3 border-b border-amare-border py-5 md:grid-cols-[4rem_minmax(0,0.8fr)_minmax(0,1.2fr)] md:gap-6" data-reveal>
|
||||
<span class="text-sm text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||
<h3 class="text-2xl font-medium text-amare-text">{{ $service->title }}</h3>
|
||||
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
])
|
||||
|
||||
@if ($testimonials->isNotEmpty())
|
||||
<section aria-labelledby="testimonials-heading" class="border-b border-amare-border bg-amare-bg-muted py-16">
|
||||
<section aria-labelledby="testimonials-heading" class="border-b border-amare-border bg-amare-bg-muted py-16" data-chapter="testimonials">
|
||||
<div class="container-amare space-y-8">
|
||||
<div class="max-w-2xl space-y-3">
|
||||
<div class="max-w-2xl space-y-3" data-reveal>
|
||||
<h2 id="testimonials-heading" class="text-3xl font-semibold text-amare-text">Depoimentos</h2>
|
||||
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
||||
</div>
|
||||
@@ -16,7 +16,7 @@
|
||||
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
||||
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
||||
@endphp
|
||||
<blockquote class="space-y-4 border-t border-amare-border pt-4">
|
||||
<blockquote class="space-y-4 border-t border-amare-border pt-4" data-reveal>
|
||||
<div class="space-y-3 text-lg text-amare-text">
|
||||
@foreach ($paragraphs as $index => $paragraph)
|
||||
<p>@if ($index === 0)“@endif{{ $paragraph }}@if ($index === count($paragraphs) - 1)”@endif</p>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
||||
@endphp
|
||||
|
||||
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg">
|
||||
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg" data-motion="page-open">
|
||||
<div class="container-amare grid flex-1 content-start gap-12 py-16 md:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)] md:py-24">
|
||||
<div class="space-y-6">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
||||
|
||||
@@ -1,6 +1,32 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$chapters = [
|
||||
['id' => 'hero-heading', 'label' => 'Capa'],
|
||||
['id' => 'manifesto-heading', 'label' => 'Manifesto'],
|
||||
];
|
||||
|
||||
if ($content->featuredServices->isNotEmpty()) {
|
||||
$chapters[] = ['id' => 'services-heading', 'label' => 'Serviços'];
|
||||
}
|
||||
|
||||
if ($content->featuredCases->isNotEmpty()) {
|
||||
$chapters[] = ['id' => 'portfolio-heading', 'label' => 'Portfólio'];
|
||||
}
|
||||
|
||||
$chapters[] = ['id' => 'method-heading', 'label' => 'Método'];
|
||||
|
||||
if ($content->testimonials->isNotEmpty()) {
|
||||
$chapters[] = ['id' => 'testimonials-heading', 'label' => 'Depoimentos'];
|
||||
}
|
||||
|
||||
$chapters[] = ['id' => 'positioning-heading', 'label' => 'A Amare'];
|
||||
$chapters[] = ['id' => 'final-cta-heading', 'label' => 'Próximo passo'];
|
||||
@endphp
|
||||
|
||||
<x-home.chapter-index :chapters="$chapters" />
|
||||
|
||||
<x-home.hero :settings="$content->settings" />
|
||||
<x-home.manifesto :settings="$content->settings" />
|
||||
<x-home.services :services="$content->featuredServices" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('content')
|
||||
<div class="border-b border-amare-border bg-amare-bg-deep">
|
||||
<div class="border-b border-amare-border bg-amare-bg-deep" data-motion="page-open">
|
||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('content')
|
||||
<article>
|
||||
<article data-motion="page-open">
|
||||
<div class="border-b border-amare-border bg-amare-bg">
|
||||
<div class="container-amare space-y-8 py-16 md:py-24">
|
||||
<header class="max-w-3xl space-y-4">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('content')
|
||||
<div class="border-b border-amare-border bg-amare-bg">
|
||||
<div class="border-b border-amare-border bg-amare-bg" data-motion="page-open">
|
||||
<div class="container-amare space-y-10 py-16 md:py-24">
|
||||
<div class="max-w-2xl space-y-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -95,20 +95,36 @@ it('loads covered public routes without console errors', function (): void {
|
||||
});
|
||||
|
||||
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
||||
$page = $this->visit('/', [
|
||||
'reducedMotion' => 'reduce',
|
||||
$case = PortfolioCase::factory()->published()->create([
|
||||
'slug' => 'casamento-jardim',
|
||||
]);
|
||||
|
||||
$duration = $page->script(<<<'JS'
|
||||
() => {
|
||||
const probe = document.createElement('div');
|
||||
probe.style.transition = 'opacity var(--amare-duration-normal) ease';
|
||||
document.body.appendChild(probe);
|
||||
const value = getComputedStyle(probe).transitionDuration;
|
||||
probe.remove();
|
||||
return value;
|
||||
}
|
||||
JS);
|
||||
$routes = [
|
||||
'/',
|
||||
'/servicos',
|
||||
'/portfolio',
|
||||
'/portfolio/'.$case->slug,
|
||||
'/sobre',
|
||||
'/contato',
|
||||
'/privacidade',
|
||||
];
|
||||
|
||||
expect((float) $duration)->toBeLessThan(0.02);
|
||||
foreach ($routes as $route) {
|
||||
$page = $this->visit($route, [
|
||||
'reducedMotion' => 'reduce',
|
||||
]);
|
||||
|
||||
$duration = $page->script(<<<'JS'
|
||||
() => {
|
||||
const probe = document.createElement('div');
|
||||
probe.style.transition = 'opacity var(--amare-duration-normal) ease';
|
||||
document.body.appendChild(probe);
|
||||
const value = getComputedStyle(probe).transitionDuration;
|
||||
probe.remove();
|
||||
return value;
|
||||
}
|
||||
JS);
|
||||
|
||||
expect((float) $duration)->toBeLessThan(0.02);
|
||||
}
|
||||
});
|
||||
|
||||
121
tests/Browser/MotionTest.php
Normal file
121
tests/Browser/MotionTest.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Service;
|
||||
use App\Models\SiteSetting;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
SiteSetting::instance()->update([
|
||||
'hero_title' => 'Celebrações com propósito',
|
||||
'default_og_image_path' => 'media/hero.jpg',
|
||||
'default_og_image_alt' => 'Capa editorial',
|
||||
]);
|
||||
|
||||
Service::factory()->published()->featured()->create([
|
||||
'title' => 'Casamentos',
|
||||
'summary' => 'Assessoria completa',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps home motion in final visible state when reduced motion is preferred', function (): void {
|
||||
$page = $this->visit('/', [
|
||||
'reducedMotion' => 'reduce',
|
||||
]);
|
||||
|
||||
$state = $page->script(<<<'JS'
|
||||
() => {
|
||||
const hero = document.querySelector('[data-motion="dossie-hero"]');
|
||||
const title = document.querySelector('#hero-heading');
|
||||
const index = document.querySelector('[data-chapter-index]');
|
||||
const active = document.querySelector('[data-chapter-index] [aria-current="true"]');
|
||||
const titleStyle = title ? getComputedStyle(title) : null;
|
||||
|
||||
return {
|
||||
heroActive: hero?.classList.contains('is-active') ?? false,
|
||||
titleOpacity: titleStyle ? Number.parseFloat(titleStyle.opacity) : 0,
|
||||
hasIndex: Boolean(index),
|
||||
hasActiveChapter: Boolean(active),
|
||||
};
|
||||
}
|
||||
JS);
|
||||
|
||||
expect($state['heroActive'])->toBeTrue();
|
||||
expect($state['titleOpacity'])->toBeGreaterThan(0.9);
|
||||
expect($state['hasIndex'])->toBeTrue();
|
||||
expect($state['hasActiveChapter'])->toBeTrue();
|
||||
});
|
||||
|
||||
it('activates the home focal opening once when motion is allowed', function (): void {
|
||||
$page = $this->visit('/', [
|
||||
'reducedMotion' => 'no-preference',
|
||||
]);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
() => new Promise((resolve) => {
|
||||
const wait = () => {
|
||||
const hero = document.querySelector('[data-motion="dossie-hero"]');
|
||||
if (hero?.classList.contains('is-active')) {
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(wait);
|
||||
};
|
||||
wait();
|
||||
setTimeout(() => resolve(false), 2000);
|
||||
})
|
||||
JS);
|
||||
|
||||
$active = $page->script(<<<'JS'
|
||||
() => document.querySelector('[data-motion="dossie-hero"]')?.classList.contains('is-active') ?? false
|
||||
JS);
|
||||
|
||||
expect($active)->toBeTrue();
|
||||
});
|
||||
|
||||
it('updates the active chapter while scrolling the home dossier', function (): void {
|
||||
$page = $this->visit('/', [
|
||||
'reducedMotion' => 'no-preference',
|
||||
]);
|
||||
|
||||
$page->resize(1440, 1000);
|
||||
|
||||
$before = $page->script(<<<'JS'
|
||||
() => document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href') ?? null
|
||||
JS);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
() => {
|
||||
const target = document.querySelector('#method-heading');
|
||||
target?.scrollIntoView({ block: 'start' });
|
||||
window.dispatchEvent(new Event('scroll'));
|
||||
return Boolean(target);
|
||||
}
|
||||
JS);
|
||||
|
||||
$page->script(<<<'JS'
|
||||
() => new Promise((resolve) => {
|
||||
let frames = 0;
|
||||
const check = () => {
|
||||
const href = document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href');
|
||||
if (href === '#method-heading' || frames > 60) {
|
||||
resolve(href);
|
||||
return;
|
||||
}
|
||||
frames += 1;
|
||||
requestAnimationFrame(check);
|
||||
};
|
||||
check();
|
||||
})
|
||||
JS);
|
||||
|
||||
$after = $page->script(<<<'JS'
|
||||
() => document.querySelector('[data-chapter-index] [aria-current="true"]')?.getAttribute('href') ?? null
|
||||
JS);
|
||||
|
||||
expect($before)->not->toBeNull();
|
||||
expect($after)->toBe('#method-heading');
|
||||
});
|
||||
132
tests/Feature/PublicSite/MotionMarkupTest.php
Normal file
132
tests/Feature/PublicSite/MotionMarkupTest.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\PublicSite;
|
||||
|
||||
use App\Models\PortfolioCase;
|
||||
use App\Models\Service;
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\Testimonial;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MotionMarkupTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_home_exposes_focal_hero_hooks_and_chapter_index_links(): void
|
||||
{
|
||||
$settings = SiteSetting::instance();
|
||||
$settings->update([
|
||||
'hero_title' => 'Celebrações com propósito',
|
||||
'default_og_image_path' => 'media/hero.jpg',
|
||||
'default_og_image_alt' => 'Capa editorial',
|
||||
]);
|
||||
|
||||
Service::factory()->published()->featured()->create(['title' => 'Casamentos']);
|
||||
PortfolioCase::factory()->published()->create([
|
||||
'title' => 'Caso A',
|
||||
'is_featured' => true,
|
||||
]);
|
||||
Testimonial::factory()->published()->create(['author_name' => 'Ana']);
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('data-motion="dossie-hero"', false)
|
||||
->assertSee('data-motion-beat="seal"', false)
|
||||
->assertSee('data-motion-beat="title"', false)
|
||||
->assertSee('data-motion-beat="media"', false)
|
||||
->assertSee('data-motion-beat="cta"', false)
|
||||
->assertSee('data-chapter-index', false)
|
||||
->assertSee('data-chapter-progress', false)
|
||||
->assertSee('href="#hero-heading"', false)
|
||||
->assertSee('href="#manifesto-heading"', false)
|
||||
->assertSee('href="#services-heading"', false)
|
||||
->assertSee('href="#portfolio-heading"', false)
|
||||
->assertSee('href="#method-heading"', false)
|
||||
->assertSee('href="#testimonials-heading"', false)
|
||||
->assertSee('href="#positioning-heading"', false)
|
||||
->assertSee('href="#final-cta-heading"', false)
|
||||
->assertSee('data-chapter="hero"', false)
|
||||
->assertSee('data-chapter="manifesto"', false)
|
||||
->assertSee('data-reveal', false);
|
||||
}
|
||||
|
||||
public function test_chapter_index_omits_empty_cms_sections(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertSee('data-chapter-index', false)
|
||||
->assertSee('href="#hero-heading"', false)
|
||||
->assertSee('href="#manifesto-heading"', false)
|
||||
->assertSee('href="#method-heading"', false)
|
||||
->assertSee('href="#positioning-heading"', false)
|
||||
->assertSee('href="#final-cta-heading"', false)
|
||||
->assertDontSee('href="#services-heading"', false)
|
||||
->assertDontSee('href="#portfolio-heading"', false)
|
||||
->assertDontSee('href="#testimonials-heading"', false)
|
||||
->assertDontSee('id="services-heading"', false)
|
||||
->assertDontSee('id="portfolio-heading"', false)
|
||||
->assertDontSee('id="testimonials-heading"', false);
|
||||
}
|
||||
|
||||
public function test_motion_tokens_and_runtime_exist_with_reduced_motion_guards(): void
|
||||
{
|
||||
$tokens = (string) file_get_contents(resource_path('css/tokens.css'));
|
||||
$appCss = (string) file_get_contents(resource_path('css/app.css'));
|
||||
$appJs = (string) file_get_contents(resource_path('js/app.js'));
|
||||
|
||||
$this->assertStringContainsString('--amare-duration-focal:', $tokens);
|
||||
$this->assertStringContainsString('--amare-ease-arrival:', $tokens);
|
||||
$this->assertStringContainsString('--amare-duration-focal: 0.01ms', $tokens);
|
||||
|
||||
$this->assertStringContainsString('[data-motion="dossie-hero"]', $appCss);
|
||||
$this->assertStringContainsString('[data-reveal]', $appCss);
|
||||
$this->assertStringContainsString('[data-chapter-index]', $appCss);
|
||||
$this->assertStringContainsString('prefers-reduced-motion: no-preference', $appCss);
|
||||
|
||||
$this->assertFileExists(resource_path('js/motion.js'));
|
||||
$this->assertStringContainsString("import './motion.js'", $appJs);
|
||||
$this->assertStringContainsString('prefers-reduced-motion', (string) file_get_contents(resource_path('js/motion.js')));
|
||||
}
|
||||
|
||||
public function test_internal_pages_expose_shared_page_open_hook(): void
|
||||
{
|
||||
SiteSetting::instance();
|
||||
$case = PortfolioCase::factory()->published()->create([
|
||||
'slug' => 'casamento-jardim',
|
||||
'title' => 'Casamento Jardim',
|
||||
]);
|
||||
|
||||
$this->get(route('services.index'))
|
||||
->assertOk()
|
||||
->assertSee('data-motion="page-open"', false);
|
||||
|
||||
$this->get(route('portfolio.index'))
|
||||
->assertOk()
|
||||
->assertSee('data-motion="page-open"', false);
|
||||
|
||||
$this->get(route('portfolio.show', $case->slug))
|
||||
->assertOk()
|
||||
->assertSee('data-motion="page-open"', false);
|
||||
|
||||
$this->get(route('about'))
|
||||
->assertOk()
|
||||
->assertSee('data-motion="page-open"', false);
|
||||
|
||||
$this->get(route('contact'))
|
||||
->assertOk()
|
||||
->assertDontSee('data-motion="page-open"', false);
|
||||
|
||||
$this->get(route('privacy'))
|
||||
->assertOk()
|
||||
->assertDontSee('data-motion="page-open"', false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user