feat(landing): штамп, лента недели, состояния кнопки, липкая CTA
Оттиск штампа при загрузке и при успехе формы, заполнение ленты недели по IntersectionObserver, мигающие точки при отправке. Только CSS и transform/opacity; анимации включает скрипт в head, поэтому без JS и при prefers-reduced-motion всё сразу в финале. Липкая CTA на мобиле: «Заказать кофе», прячется у формы, при фокусе в ней и при открытой клавиатуре. Ритм секций плотнее.
This commit is contained in:
@@ -140,11 +140,11 @@
|
||||
max-width: var(--container-page);
|
||||
}
|
||||
.section-y {
|
||||
padding-block: 72px;
|
||||
padding-block: 48px;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.section-y {
|
||||
padding-block: 128px;
|
||||
padding-block: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,3 +483,106 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Моушн (DESIGN.md → «Моушн»). Без JS и при reduced-motion — сразу финальное состояние. */
|
||||
@keyframes stamp-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: rotate(calc(var(--stamp-rotate, -6deg) - 3deg)) scale(1.18);
|
||||
}
|
||||
to {
|
||||
opacity: 0.92;
|
||||
transform: rotate(var(--stamp-rotate, -6deg)) scale(1);
|
||||
}
|
||||
}
|
||||
@keyframes busy-dot {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
33% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
[data-motion] .stamp[data-stamp='load'] {
|
||||
animation: stamp-in 380ms var(--ease-out-expo) 250ms both;
|
||||
}
|
||||
.stamp[data-stamp='event'] {
|
||||
animation: stamp-in 380ms var(--ease-out-expo) 80ms both;
|
||||
}
|
||||
.busy-dots i {
|
||||
animation: busy-dot 480ms steps(1) infinite;
|
||||
}
|
||||
.busy-dots i:nth-child(2) {
|
||||
animation-delay: 160ms;
|
||||
}
|
||||
.busy-dots i:nth-child(3) {
|
||||
animation-delay: 320ms;
|
||||
}
|
||||
[data-lead-notice] > * {
|
||||
animation: fade-in 160ms ease-out;
|
||||
}
|
||||
|
||||
/* лента недели */
|
||||
[data-motion] .week .week-rule {
|
||||
transition: transform 600ms var(--ease-out-quart);
|
||||
}
|
||||
[data-motion] .week .week-mark {
|
||||
transition: transform 320ms var(--ease-out-quart) 300ms;
|
||||
}
|
||||
[data-motion] .week .week-note {
|
||||
transition:
|
||||
opacity 320ms var(--ease-out-quart) 300ms,
|
||||
transform 320ms var(--ease-out-quart) 300ms;
|
||||
}
|
||||
[data-motion] .week .week-day:nth-child(5) .week-mark,
|
||||
[data-motion] .week .week-day:nth-child(5) .week-note {
|
||||
transition-delay: 480ms;
|
||||
}
|
||||
[data-motion] .week .week-bracket-line {
|
||||
transition: transform 500ms var(--ease-out-quart) 680ms;
|
||||
}
|
||||
[data-motion] .week .week-bracket-note {
|
||||
transition: opacity 240ms ease-out 1000ms;
|
||||
}
|
||||
[data-motion] .week:not([data-inview]) .week-rule,
|
||||
[data-motion] .week:not([data-inview]) .week-mark {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
[data-motion] .week:not([data-inview]) .week-note {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
[data-motion] .week:not([data-inview]) .week-bracket-line {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
[data-motion] .week:not([data-inview]) .week-bracket-note {
|
||||
opacity: 0;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
[data-motion] .week:not([data-inview]) .week-bracket-line {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* липкая CTA */
|
||||
.sticky-cta {
|
||||
transition: transform 180ms var(--ease-in-quart);
|
||||
}
|
||||
.sticky-cta[data-visible] {
|
||||
transition: transform 240ms var(--ease-out-quart);
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-cta:not([data-visible]) {
|
||||
transform: translateY(100%);
|
||||
visibility: hidden;
|
||||
transition-property: transform, visibility;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Golos_Text, Martian_Mono } from 'next/font/google';
|
||||
import { MOTION_BOOT, RevealObserver } from '@/components/RevealObserver';
|
||||
import { StampInkFilter } from '@/components/Stamp';
|
||||
import { StickyCta } from '@/components/StickyCta';
|
||||
import { TouchRecorder } from '@/components/TouchRecorder';
|
||||
import { site } from '@/content/site';
|
||||
import './globals.css';
|
||||
@@ -19,11 +21,17 @@ export const viewport: Viewport = {
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ru" className={`${golos.variable} ${martian.variable}`}>
|
||||
<html lang="ru" className={`${golos.variable} ${martian.variable}`} suppressHydrationWarning>
|
||||
<head>
|
||||
{/* до первой отрисовки: иначе блоки мигнут в начальном состоянии анимации */}
|
||||
<script dangerouslySetInnerHTML={{ __html: MOTION_BOOT }} />
|
||||
</head>
|
||||
<body>
|
||||
<StampInkFilter />
|
||||
{children}
|
||||
<StickyCta />
|
||||
<TouchRecorder />
|
||||
<RevealObserver />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user