feat(landing): секции по DESIGN.md
Шрифты Golos Text и Martian Mono через next/font, токены @theme, все блоки по плану: первый экран со штампом даты ближайшей обжарки, лента недели, этикетки сортов, подписка, кто мы, доставка, форма, подвал. Три фото Unsplash локально, priority только у первого экрана. LeadForm перекрашена под токены, логика и data-атрибуты те же. В site.ts и form.ts добавлены служебные подписи для вёрстки.
This commit is contained in:
@@ -1 +1,485 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* Токены — DESIGN.md. Палитра Tailwind отключена: только цвета «Кварты». */
|
||||
@theme {
|
||||
--color-*: initial;
|
||||
--color-cream: #f3ebdd;
|
||||
--color-paper: #fbf7f0;
|
||||
--color-kraft: #dcc7a1;
|
||||
--color-roast: #1e120c;
|
||||
--color-ink: #2a1810;
|
||||
--color-muted: #5e4636;
|
||||
--color-placeholder: #7a6352;
|
||||
--color-rule-soft: #c9b595;
|
||||
--color-cherry: #b3221b;
|
||||
--color-cherry-deep: #8f1a14;
|
||||
--color-cherry-on-dark: #e8604f;
|
||||
--color-muted-on-dark: #bfa88a;
|
||||
--color-ok: #2e5e3a;
|
||||
--color-transparent: transparent;
|
||||
--color-current: currentColor;
|
||||
|
||||
--radius-*: initial;
|
||||
--radius-none: 0px;
|
||||
--radius-hair: 2px;
|
||||
--shadow-*: initial;
|
||||
|
||||
--text-display: clamp(2.75rem, 1.6rem + 5.2vw, 6rem);
|
||||
--text-display--line-height: 0.92;
|
||||
--text-display--letter-spacing: -0.035em;
|
||||
--text-headline: clamp(2rem, 1.4rem + 2.8vw, 3.5rem);
|
||||
--text-headline--line-height: 1;
|
||||
--text-headline--letter-spacing: -0.03em;
|
||||
--text-title: clamp(1.5rem, 1.3rem + 0.6vw, 1.875rem);
|
||||
--text-title--line-height: 1.05;
|
||||
--text-title--letter-spacing: -0.02em;
|
||||
--text-lead: clamp(1.125rem, 1rem + 0.5vw, 1.375rem);
|
||||
--text-lead--line-height: 1.45;
|
||||
--text-body: 1.0625rem;
|
||||
--text-body--line-height: 1.55;
|
||||
--text-small: 0.875rem;
|
||||
--text-small--line-height: 1.45;
|
||||
--text-ui: 0.9375rem;
|
||||
--text-ui--line-height: 1.3;
|
||||
--text-tag: 0.75rem;
|
||||
--text-tag--line-height: 1.3;
|
||||
--text-tag--letter-spacing: 0.08em;
|
||||
--text-price: clamp(1.375rem, 1.2rem + 0.8vw, 2rem);
|
||||
--text-price--line-height: 1;
|
||||
--text-price--letter-spacing: -0.02em;
|
||||
--text-date: clamp(1.75rem, 1.2rem + 2.2vw, 3.25rem);
|
||||
--text-date--line-height: 1;
|
||||
--text-date--letter-spacing: -0.04em;
|
||||
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
--ease-in-quart: cubic-bezier(0.5, 0, 0.75, 0);
|
||||
|
||||
--container-page: 1280px;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-golos), system-ui, sans-serif;
|
||||
--font-mono: var(--font-martian), ui-monospace, monospace;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
background: var(--color-cream);
|
||||
color: var(--color-ink);
|
||||
scroll-padding-top: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--text-body);
|
||||
line-height: 1.55;
|
||||
background: var(--color-cream);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-synthesis: none;
|
||||
}
|
||||
::selection {
|
||||
background: var(--color-cherry);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-ink);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.on-dark :focus-visible {
|
||||
outline-color: var(--color-cream);
|
||||
}
|
||||
input,
|
||||
textarea {
|
||||
caret-color: var(--color-cherry);
|
||||
}
|
||||
a {
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
p {
|
||||
text-wrap: pretty;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* моно-шрифт: ширина через wdth-ось Martian Mono */
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.mono-narrow {
|
||||
font-family: var(--font-mono);
|
||||
font-stretch: 87.5%;
|
||||
font-variation-settings: 'wdth' 87.5;
|
||||
}
|
||||
.tag {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--text-tag);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.page-x {
|
||||
padding-inline: 20px;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.page-x {
|
||||
padding-inline: 48px;
|
||||
}
|
||||
}
|
||||
.container-page {
|
||||
margin-inline: auto;
|
||||
max-width: var(--container-page);
|
||||
}
|
||||
.section-y {
|
||||
padding-block: 72px;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.section-y {
|
||||
padding-block: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Кнопки */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 0 24px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition:
|
||||
background-color 120ms ease-out,
|
||||
color 120ms ease-out,
|
||||
border-color 120ms ease-out,
|
||||
transform 60ms linear;
|
||||
}
|
||||
.btn:active:not(:disabled) {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--color-cherry);
|
||||
color: var(--color-paper);
|
||||
}
|
||||
.btn-primary:active:not(:disabled) {
|
||||
background: var(--color-cherry-deep);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: transparent;
|
||||
color: var(--color-ink);
|
||||
box-shadow: inset 0 0 0 1.5px var(--color-ink);
|
||||
padding: 0 20px;
|
||||
}
|
||||
.btn-on-dark {
|
||||
background: transparent;
|
||||
color: var(--color-cream);
|
||||
box-shadow: inset 0 0 0 1.5px var(--color-cream);
|
||||
padding: 0 20px;
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: var(--color-cherry-deep);
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: var(--color-ink);
|
||||
color: var(--color-cream);
|
||||
}
|
||||
.btn-on-dark:hover:not(:disabled) {
|
||||
background: var(--color-cream);
|
||||
color: var(--color-roast);
|
||||
}
|
||||
}
|
||||
.btn:disabled:not([aria-busy]) {
|
||||
background: var(--color-rule-soft);
|
||||
color: var(--color-ink);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn[aria-busy='true'] {
|
||||
cursor: progress;
|
||||
}
|
||||
.busy-dots {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.busy-dots i {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* Поля формы */
|
||||
.field-label {
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
padding: 12px 14px;
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
font-size: 1.0625rem;
|
||||
line-height: 1.45;
|
||||
border: 1.5px solid var(--color-muted);
|
||||
border-radius: 0;
|
||||
transition: border-color 120ms ease-out;
|
||||
}
|
||||
.field::placeholder {
|
||||
color: var(--color-placeholder);
|
||||
opacity: 1;
|
||||
}
|
||||
.field:focus {
|
||||
border-color: var(--color-ink);
|
||||
box-shadow: inset 0 0 0 0.5px var(--color-ink);
|
||||
}
|
||||
.field:focus-visible {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.field[data-invalid] {
|
||||
border-color: var(--color-cherry);
|
||||
box-shadow: inset 0 0 0 0.5px var(--color-cherry);
|
||||
}
|
||||
textarea.field {
|
||||
min-height: 112px;
|
||||
resize: vertical;
|
||||
}
|
||||
.field-error {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
font-size: var(--text-small);
|
||||
font-weight: 500;
|
||||
line-height: 1.45;
|
||||
color: var(--color-cherry-deep);
|
||||
}
|
||||
.talon {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
font-size: var(--text-ui);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 0 0 1.5px var(--color-muted);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 120ms ease-out,
|
||||
color 120ms ease-out;
|
||||
}
|
||||
.talon::before {
|
||||
content: '';
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
flex: none;
|
||||
box-shadow: inset 0 0 0 1.5px currentColor;
|
||||
}
|
||||
.talon[data-checked] {
|
||||
background: var(--color-ink);
|
||||
color: var(--color-cream);
|
||||
box-shadow: inset 0 0 0 1.5px var(--color-ink);
|
||||
}
|
||||
.talon[data-checked]::before {
|
||||
background: var(--color-cherry);
|
||||
box-shadow: none;
|
||||
}
|
||||
.talon:has(:focus-visible) {
|
||||
outline: 2px solid var(--color-ink);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[data-invalid] .talon:not([data-checked]) {
|
||||
box-shadow: inset 0 0 0 2px var(--color-cherry);
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.talon:not([data-checked]):hover {
|
||||
background: var(--color-cream);
|
||||
}
|
||||
}
|
||||
|
||||
/* Этикетка: лента-наклейка, срезанная краем */
|
||||
.ribbon {
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
right: -62px;
|
||||
width: 220px;
|
||||
padding: 6px 0;
|
||||
background: var(--color-cherry);
|
||||
color: var(--color-paper);
|
||||
text-align: center;
|
||||
transform: rotate(45deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
.perforation {
|
||||
background-image: linear-gradient(to right, var(--color-ink) 0 6px, transparent 6px 10px);
|
||||
background-size: 10px 1px;
|
||||
background-repeat: repeat-x;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Штамп */
|
||||
@layer components {
|
||||
.stamp {
|
||||
display: inline-block;
|
||||
transform: rotate(var(--stamp-rotate, -6deg));
|
||||
opacity: 0.92;
|
||||
}
|
||||
.stamp-text {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 700;
|
||||
font-stretch: 112.5%;
|
||||
font-variation-settings: 'wdth' 112.5;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Лента недели: 390 — столбиком, ≥1024 — строкой */
|
||||
@layer components {
|
||||
.week-days {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 128px;
|
||||
column-gap: 12px;
|
||||
}
|
||||
.week-day {
|
||||
grid-column: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
column-gap: 12px;
|
||||
align-items: center;
|
||||
min-height: 60px;
|
||||
padding-block: 10px;
|
||||
position: relative;
|
||||
}
|
||||
.week-head {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.week-letter {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
.week-date {
|
||||
font-size: var(--text-date);
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
.week-rule {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background: var(--color-ink);
|
||||
transform-origin: left;
|
||||
}
|
||||
.week-mark {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 64px;
|
||||
bottom: -1px;
|
||||
height: 4px;
|
||||
background: var(--color-cherry);
|
||||
transform-origin: left;
|
||||
}
|
||||
.week-note {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.week-bracket {
|
||||
grid-column: 2;
|
||||
grid-row: 5 / 8;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding-block: 12px;
|
||||
}
|
||||
.week-bracket-line {
|
||||
flex: none;
|
||||
width: 8px;
|
||||
border: 2px solid var(--color-cherry);
|
||||
border-left: 0;
|
||||
transform-origin: top;
|
||||
}
|
||||
.week-bracket-note {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.week-days {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
column-gap: 0;
|
||||
}
|
||||
.week-day {
|
||||
grid-column: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
.week-head {
|
||||
position: relative;
|
||||
gap: 10px;
|
||||
padding: 0 16px 20px;
|
||||
border-left: 1px solid var(--color-rule-soft);
|
||||
}
|
||||
.week-day:first-child .week-head {
|
||||
border-left: 0;
|
||||
padding-left: 0;
|
||||
}
|
||||
.week-letter {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.week-mark {
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
width: auto;
|
||||
}
|
||||
.week-note {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
.week-bracket {
|
||||
grid-column: 5 / 8;
|
||||
grid-row: 2;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 28px 16px 0;
|
||||
}
|
||||
.week-bracket-line {
|
||||
width: auto;
|
||||
height: 10px;
|
||||
border: 2px solid var(--color-cherry);
|
||||
border-top: 0;
|
||||
transform-origin: left;
|
||||
}
|
||||
.week-bracket-note {
|
||||
align-self: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { Metadata, Viewport } from 'next';
|
||||
import { Golos_Text, Martian_Mono } from 'next/font/google';
|
||||
import { StampInkFilter } from '@/components/Stamp';
|
||||
import { TouchRecorder } from '@/components/TouchRecorder';
|
||||
import { site } from '@/content/site';
|
||||
import './globals.css';
|
||||
|
||||
const golos = Golos_Text({ subsets: ['cyrillic', 'latin'], variable: '--font-golos', display: 'swap' });
|
||||
const martian = Martian_Mono({ subsets: ['cyrillic', 'latin'], variable: '--font-martian', axes: ['wdth'], display: 'swap' });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Кварта — свежая обжарка в Минске',
|
||||
description: 'Обжариваем по четвергам, привозим по пятницам.',
|
||||
title: site.meta.title,
|
||||
description: site.meta.description,
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: '#f3ebdd',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="ru">
|
||||
<html lang="ru" className={`${golos.variable} ${martian.variable}`}>
|
||||
<body>
|
||||
<StampInkFilter />
|
||||
{children}
|
||||
<TouchRecorder />
|
||||
</body>
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
// Заглушка страницы: проверяем форму и CTA. Секции и вёрстку сделает дизайнер.
|
||||
import { CtaButton } from '@/components/CtaButton';
|
||||
import { LeadForm } from '@/components/LeadForm';
|
||||
import { CTAS, FORM_TEXT } from '@/content/form';
|
||||
import { LEAD_SECTION_ID } from '@/lib/leadPrefill';
|
||||
import { About } from '@/components/sections/About';
|
||||
import { Catalog } from '@/components/sections/Catalog';
|
||||
import { Delivery } from '@/components/sections/Delivery';
|
||||
import { Footer } from '@/components/sections/Footer';
|
||||
import { Freshness } from '@/components/sections/Freshness';
|
||||
import { Header } from '@/components/sections/Header';
|
||||
import { Hero } from '@/components/sections/Hero';
|
||||
import { LeadSection } from '@/components/sections/LeadSection';
|
||||
import { Subscription } from '@/components/sections/Subscription';
|
||||
import { formatShortDate, getRoastWeek } from '@/lib/roastWeek';
|
||||
|
||||
const ctaClass = 'rounded border border-neutral-900 px-4 py-2';
|
||||
// дата ближайшей обжарки пересчитывается раз в час
|
||||
export const revalidate = 3600;
|
||||
|
||||
export default function Page() {
|
||||
const week = getRoastWeek();
|
||||
const roastDate = formatShortDate(week.roast);
|
||||
return (
|
||||
<main className="mx-auto flex max-w-2xl flex-col gap-12 px-4 py-10 font-sans">
|
||||
<section id="hero" className="flex flex-col gap-4">
|
||||
<h1 className="text-3xl font-bold">Кварта — свежая обжарка в Минске</h1>
|
||||
<p>Обжариваем по четвергам, привозим по пятницам.</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{Object.values(CTAS).map((cta) => (
|
||||
<CtaButton key={cta.label} interest={cta.interest} comment={'comment' in cta ? cta.comment : undefined} className={ctaClass}>
|
||||
{cta.label}
|
||||
</CtaButton>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id={LEAD_SECTION_ID} className="flex scroll-mt-4 flex-col gap-4">
|
||||
<h2 className="text-2xl font-bold">{FORM_TEXT.title}</h2>
|
||||
<p>{FORM_TEXT.lead}</p>
|
||||
<LeadForm />
|
||||
</section>
|
||||
</main>
|
||||
<>
|
||||
<Header />
|
||||
<main id="top">
|
||||
<Hero roastDate={roastDate} />
|
||||
<Freshness week={week} />
|
||||
<Catalog roastDate={roastDate} />
|
||||
<Subscription />
|
||||
<About />
|
||||
<Delivery />
|
||||
<LeadSection />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user