Compare commits
3 Commits
feat/remod
...
feat/dossi
| Author | SHA1 | Date | |
|---|---|---|---|
| 03145c747c | |||
| 52afb27d6e | |||
| 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.
|
- **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.
|
- **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.
|
- **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.
|
- **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.
|
- **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);
|
font-family: var(--amare-font-serif);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[id$='-heading'] {
|
||||||
|
scroll-margin-top: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
a:focus-visible,
|
a:focus-visible,
|
||||||
button:focus-visible,
|
button:focus-visible,
|
||||||
summary:focus-visible,
|
summary:focus-visible,
|
||||||
@@ -98,4 +102,147 @@
|
|||||||
body.menu-open {
|
body.menu-open {
|
||||||
overflow: hidden;
|
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-fast: 150ms;
|
||||||
--amare-duration-normal: 250ms;
|
--amare-duration-normal: 250ms;
|
||||||
--amare-duration-slow: 400ms;
|
--amare-duration-slow: 400ms;
|
||||||
|
--amare-duration-focal: 720ms;
|
||||||
--amare-ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
|
--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) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
--amare-duration-fast: 0.01ms;
|
--amare-duration-fast: 0.01ms;
|
||||||
--amare-duration-normal: 0.01ms;
|
--amare-duration-normal: 0.01ms;
|
||||||
--amare-duration-slow: 0.01ms;
|
--amare-duration-slow: 0.01ms;
|
||||||
|
--amare-duration-focal: 0.01ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './motion.js';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const menuButton = document.querySelector('[data-menu-button]');
|
const menuButton = document.querySelector('[data-menu-button]');
|
||||||
const navigation = document.querySelector('[data-main-nav]');
|
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',
|
'settings',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section aria-labelledby="final-cta-heading" class="border-t border-amare-border bg-amare-bg-deep py-20">
|
<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">
|
<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>
|
<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>
|
<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">
|
<p class="mx-auto max-w-2xl text-amare-muted">
|
||||||
|
|||||||
@@ -2,14 +2,22 @@
|
|||||||
'settings',
|
'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="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">
|
<div class="space-y-8">
|
||||||
|
<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))
|
@if (filled($settings->hero_eyebrow))
|
||||||
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">{{ $settings->hero_eyebrow }}</p>
|
||||||
@endif
|
@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 }}
|
{{ $settings->hero_title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -17,7 +25,7 @@
|
|||||||
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
<p class="max-w-2xl text-lg text-amare-text-muted">{{ $settings->hero_subtitle }}</p>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-4">
|
<div data-motion-beat="cta" class="flex flex-wrap items-center gap-4">
|
||||||
<a
|
<a
|
||||||
href="{{ route('contact') }}"
|
href="{{ route('contact') }}"
|
||||||
data-testid="home-primary-cta"
|
data-testid="home-primary-cta"
|
||||||
@@ -42,7 +50,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (filled($settings->default_og_image_path))
|
@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
|
<x-media.image
|
||||||
:path="$settings->default_og_image_path"
|
:path="$settings->default_og_image_path"
|
||||||
:alt="$settings->default_og_image_alt ?: $settings->brand_name"
|
: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.';
|
$body = $settings->manifesto_body ?: 'A Amare combina sensibilidade e precisão para criar encontros coerentes com cada cliente, marca e ocasião — sem fórmulas prontas, excessos ou ruído.';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="manifesto-heading" class="border-b border-amare-border bg-amare-bg-deep py-20">
|
<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)]">
|
<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>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent">Manifesto</p>
|
||||||
<div class="space-y-6">
|
<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>
|
<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.';
|
$intro = $settings->method_intro ?: 'Clareza em cada etapa. Tranquilidade durante todo o processo.';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="method-heading" class="border-b border-amare-border bg-amare-bg-archive py-20">
|
<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="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>
|
<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>
|
<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>
|
<p class="text-amare-text-muted">{{ $intro }}</p>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<ol class="grid gap-5 border-t border-amare-border">
|
<ol class="grid gap-5 border-t border-amare-border">
|
||||||
@foreach ($steps as $index => $step)
|
@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>
|
<span class="text-sm text-amare-accent">{{ str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT) }}</span>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<h3 class="text-2xl font-medium text-amare-text">{{ $step['title'] ?? '' }}</h3>
|
<h3 class="text-2xl font-medium text-amare-text">{{ $step['title'] ?? '' }}</h3>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($cases->isNotEmpty())
|
@if ($cases->isNotEmpty())
|
||||||
<section aria-labelledby="portfolio-heading" class="border-b border-amare-accent-deep bg-amare-accent-deep py-20 text-amare-accent-text">
|
<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="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>
|
<p class="text-sm font-medium uppercase tracking-[0.18em] text-amare-accent-text/80">Portfólio</p>
|
||||||
<div class="space-y-3">
|
<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>
|
<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">
|
<div class="grid gap-10 md:grid-cols-2">
|
||||||
@foreach ($cases as $case)
|
@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))
|
@if (filled($case->cover_image_path))
|
||||||
<x-media.image
|
<x-media.image
|
||||||
:path="$case->cover_image_path"
|
:path="$case->cover_image_path"
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
$principles = filled($settings->principles) ? $settings->principles : \App\Models\SiteSetting::defaultPrinciples();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<section aria-labelledby="positioning-heading" class="border-b border-amare-border py-20">
|
<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="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>
|
<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>
|
<h2 id="positioning-heading" class="text-3xl font-medium text-amare-text">Presença que organiza o essencial.</h2>
|
||||||
</div>
|
</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>
|
<p class="max-w-2xl text-xl leading-relaxed text-amare-text">{{ $summary }}</p>
|
||||||
<ul class="grid gap-3 border-t border-amare-border">
|
<ul class="grid gap-3 border-t border-amare-border">
|
||||||
@foreach ($principles as $principle)
|
@foreach ($principles as $principle)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($services->isNotEmpty())
|
@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="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>
|
<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>
|
<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>
|
<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">
|
<ol class="border-t border-amare-border">
|
||||||
@foreach ($services as $index => $service)
|
@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>
|
<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>
|
<h3 class="text-2xl font-medium text-amare-text">{{ $service->title }}</h3>
|
||||||
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
<p class="text-amare-text-muted">{{ $service->summary }}</p>
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
])
|
])
|
||||||
|
|
||||||
@if ($testimonials->isNotEmpty())
|
@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="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>
|
<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>
|
<p class="text-amare-text-muted">Quem celebrou com a Amare conta como foi a experiência.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
$paragraphs = preg_split('/\n\s*\n/', trim((string) $testimonial->quote)) ?: [];
|
||||||
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
$paragraphs = array_values(array_filter(array_map('trim', $paragraphs), fn (string $p): bool => $p !== ''));
|
||||||
@endphp
|
@endphp
|
||||||
<blockquote class="space-y-4 border-t border-amare-border pt-4">
|
<blockquote class="space-y-4 border-t border-amare-border pt-4" data-reveal>
|
||||||
<div class="space-y-3 text-lg text-amare-text">
|
<div class="space-y-3 text-lg text-amare-text">
|
||||||
@foreach ($paragraphs as $index => $paragraph)
|
@foreach ($paragraphs as $index => $paragraph)
|
||||||
<p>@if ($index === 0)“@endif{{ $paragraph }}@if ($index === count($paragraphs) - 1)”@endif</p>
|
<p>@if ($index === 0)“@endif{{ $paragraph }}@if ($index === count($paragraphs) - 1)”@endif</p>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
$city = $siteSettings->city ?: 'São Paulo - SP';
|
$city = $siteSettings->city ?: 'São Paulo - SP';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex min-h-[calc(100dvh-14rem)] flex-col border-b border-amare-border bg-amare-bg">
|
<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="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">
|
<div class="space-y-6">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">A Amare</p>
|
||||||
|
|||||||
@@ -1,6 +1,32 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@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.hero :settings="$content->settings" />
|
||||||
<x-home.manifesto :settings="$content->settings" />
|
<x-home.manifesto :settings="$content->settings" />
|
||||||
<x-home.services :services="$content->featuredServices" />
|
<x-home.services :services="$content->featuredServices" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@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="container-amare space-y-10 py-16 md:py-24">
|
||||||
<div class="max-w-2xl space-y-4">
|
<div class="max-w-2xl space-y-4">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Portfólio</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<article>
|
<article data-motion="page-open">
|
||||||
<div class="border-b border-amare-border bg-amare-bg">
|
<div class="border-b border-amare-border bg-amare-bg">
|
||||||
<div class="container-amare space-y-8 py-16 md:py-24">
|
<div class="container-amare space-y-8 py-16 md:py-24">
|
||||||
<header class="max-w-3xl space-y-4">
|
<header class="max-w-3xl space-y-4">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@extends('layouts.public')
|
@extends('layouts.public')
|
||||||
|
|
||||||
@section('content')
|
@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="container-amare space-y-10 py-16 md:py-24">
|
||||||
<div class="max-w-2xl space-y-4">
|
<div class="max-w-2xl space-y-4">
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-amare-accent">Serviços</p>
|
<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
@@ -95,7 +95,22 @@ it('loads covered public routes without console errors', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
it('disables transitions when prefers-reduced-motion is reduce', function (): void {
|
||||||
$page = $this->visit('/', [
|
$case = PortfolioCase::factory()->published()->create([
|
||||||
|
'slug' => 'casamento-jardim',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$routes = [
|
||||||
|
'/',
|
||||||
|
'/servicos',
|
||||||
|
'/portfolio',
|
||||||
|
'/portfolio/'.$case->slug,
|
||||||
|
'/sobre',
|
||||||
|
'/contato',
|
||||||
|
'/privacidade',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
$page = $this->visit($route, [
|
||||||
'reducedMotion' => 'reduce',
|
'reducedMotion' => 'reduce',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -111,4 +126,5 @@ it('disables transitions when prefers-reduced-motion is reduce', function (): vo
|
|||||||
JS);
|
JS);
|
||||||
|
|
||||||
expect((float) $duration)->toBeLessThan(0.02);
|
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