feat: split big file and update agents.md

This commit is contained in:
2026-06-18 23:14:57 +03:00
parent a5c674404d
commit c7ee4a691a
19 changed files with 347 additions and 265 deletions

View File

@@ -5,7 +5,55 @@ Lineform Studio — архитектурный шаблон с жесткой Sw
## Project Specifics ## Project Specifics
- Доменный контент лежит в `src/entities/site-content.ts`; не размазывай списки проектов и услуг по компонентам. - Доменный контент лежит в `src/entities/site-content.ts`; не размазывай списки проектов и услуг по компонентам.
- `src/app` содержит только route wrappers; композиция страниц находится в `src/widgets/template-ui.tsx`. - `src/app` содержит только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/<page>-page.tsx`). См. File Map.
- Feature-формы и узкие mock-интеракции размещай в `src/features/*/ui`. - Feature-формы и узкие mock-интеракции размещай в `src/features/*/ui`.
- Не добавляй CMS, реальные заявки, карты, платежи или backend API без отдельного запроса. - Не добавляй CMS, реальные заявки, карты, платежи или backend API без отдельного запроса.
- Проверка после правок: `pnpm lint` и `pnpm build`. - Проверка после правок: `pnpm lint` и `pnpm build`.
## Design System
Источник токенов — `src/app/globals.css` (`@theme` + `:root`/`.dark`). Шрифт — Roboto Flex (`--font-lineform`), один гарнитур на sans и mono, с включёнными `ss01`/`cv01`. Работай через семантические классы Tailwind (`bg-background`, `text-foreground`, `border-foreground`), не хардкодь hex/oklch.
Личность: **Swiss/brutalist «бумага и тушь»** — почти белый бумажный фон, near-black «ink» как граница и текст, и единственный тёплый янтарный accent как редкий сигнал. Цвета намеренно нейтральные (нулевая хрома у фона/текста/secondary) — драматизм создаёт не палитра, а сетка, масштаб типографики и толстые границы.
| Роль | Light | Характер |
|---|---|---|
| `background` | почти белый (oklch 0.985) | бумажный фон страниц |
| `foreground` | near-black ink (oklch 0.13) | текст + **границы** (`border-2 border-foreground`) |
| `primary` | near-black ink | заливка кнопок/инверсия |
| `secondary` | светло-серый | нейтральные плашки |
| `muted` / `muted-foreground` | серый | вторичный текст, фон фото |
| `accent` | тёплый янтарь (oklch 0.78 0.16 75) | единственный цветной сигнал (бейдж города) |
| `card` | чистый белый | поверхности на бумажном фоне |
Узнаваемые приёмы (держи их, это и есть «лицо» проекта):
- **Жёсткие углы:** `--radius` = 0.125rem; всё `rounded-none` (кнопки, бейджи, карточки).
- **Толстые границы и divider-ы:** `border-2 border-foreground`, `divide-y-2 divide-foreground`, `border-y-2` — структура страницы строится границами, а не тенями.
- **Никаких теней:** объём даёт инверсия `hover:bg-foreground hover:text-background`, а не drop-shadow.
- **Чертёжная типографика:** `font-black uppercase`, очень плотный `leading` (`leading-none`/`leading-[0.78]`/`leading-[0.88]`), гигантские viewport-размеры (`text-[18vw]`/`md:text-[10vw]`, до `text-9xl`), индексы вида `Index / 01`.
- **Утилитарные классы:** `.lineform-grid` — миллиметровая сетка-подложка для hero/секций; `.arch-photo``grayscale(1) contrast(1.08)` на всех фото (ч/б архив).
- **Широкий каркас:** контейнер `max-w-[1500px]`, многоколоночные `md:grid-cols-[...]` раскладки с фиксированными колонками-индексами.
Do / Don't:
- **Do:** держи ч/б палитру, толстые границы, индексную нумерацию, grayscale-фото и крупную uppercase-типографику; accent — только точечно.
- **Don't:** мягкие тени, скруглённые углы, цветные градиенты, цветные фото, generic-SaaS hero — это ломает «чертёжную» личность шаблона.
## File Map
| Route | Widget |
|---|---|
| `/` | `src/widgets/home-page.tsx` (`HomePage`) |
| `/work` | `src/widgets/work-page.tsx` (`WorkPage`) |
| `/work/courtyard-house` | `src/widgets/project-detail-page.tsx` (`ProjectDetailPage`) |
| `/services` | `src/widgets/services-page.tsx` (`ServicesPage`) |
| `/process` | `src/widgets/process-page.tsx` (`ProcessPage`) |
| `/studio` | `src/widgets/studio-page.tsx` (`StudioPage`) |
| `/contact` | `src/widgets/contact-page.tsx` (`ContactPage`) |
Переиспользуемые блоки:
- `src/widgets/site-shell.tsx``SiteShell` (sticky header + nav, обёртка всех страниц).
- `src/widgets/project-image.tsx``ProjectImage` (grayscale-фото проекта с индексом; Home + Project Detail).
- `src/shared/ui/page-title.tsx``PageTitle` (заголовочная секция `code / title / text` внутренних страниц: Work, Services, Process, Studio, Contact).
- `src/features/project-brief/ui/project-brief-form.tsx` — mock-форма брифа.
Одноразовые блоки колоцированы со своей страницей (напр. featured-секция и сетка проектов в `home-page.tsx`, список фактов студии в `studio-page.tsx`).

View File

@@ -1,4 +1,4 @@
import { ContactPage } from "@/widgets/template-ui"; import { ContactPage } from "@/widgets/contact-page";
export default function Page() { export default function Page() {
return <ContactPage />; return <ContactPage />;

View File

@@ -1,4 +1,4 @@
import { HomePage } from "@/widgets/template-ui"; import { HomePage } from "@/widgets/home-page";
export default function Page() { export default function Page() {
return <HomePage />; return <HomePage />;

View File

@@ -1,4 +1,4 @@
import { ProcessPage } from "@/widgets/template-ui"; import { ProcessPage } from "@/widgets/process-page";
export default function Page() { export default function Page() {
return <ProcessPage />; return <ProcessPage />;

View File

@@ -1,4 +1,4 @@
import { ServicesPage } from "@/widgets/template-ui"; import { ServicesPage } from "@/widgets/services-page";
export default function Page() { export default function Page() {
return <ServicesPage />; return <ServicesPage />;

View File

@@ -1,4 +1,4 @@
import { StudioPage } from "@/widgets/template-ui"; import { StudioPage } from "@/widgets/studio-page";
export default function Page() { export default function Page() {
return <StudioPage />; return <StudioPage />;

View File

@@ -1,4 +1,4 @@
import { ProjectDetailPage } from "@/widgets/template-ui"; import { ProjectDetailPage } from "@/widgets/project-detail-page";
export default function Page() { export default function Page() {
return <ProjectDetailPage />; return <ProjectDetailPage />;

View File

@@ -1,4 +1,4 @@
import { WorkPage } from "@/widgets/template-ui"; import { WorkPage } from "@/widgets/work-page";
export default function Page() { export default function Page() {
return <WorkPage />; return <WorkPage />;

View File

@@ -0,0 +1,13 @@
export function PageTitle({ code, title, text }: { code: string; title: string; text: string }) {
return (
<section className="border-b-2 border-foreground px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[180px_1fr]">
<div className="text-sm font-black uppercase">{code}</div>
<div>
<h1 className="max-w-5xl text-5xl font-black uppercase leading-[0.88] md:text-8xl">{title}</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,31 @@
import { MailIcon, RulerIcon } from "lucide-react";
import { ProjectBriefForm } from "@/features/project-brief/ui/project-brief-form";
import { site } from "@/entities/site-content";
import { PageTitle } from "@/shared/ui/page-title";
import { SiteShell } from "@/widgets/site-shell";
export function ContactPage() {
return (
<SiteShell>
<PageTitle code="Brief / 05" title="Начать проект с ограничений" text="Расскажите о задаче коротко. На первом звонке мы проверим масштаб, сроки, бюджет и поймем, где можем быть полезны." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[0.8fr_1.2fr]">
<div className="space-y-6">
<div className="border-2 border-foreground p-5">
<MailIcon className="mb-8 size-7" />
<div className="text-3xl font-black uppercase">{site.email}</div>
<p className="mt-3 text-muted-foreground">Ответим с вопросами по объекту и предложим формат первой консультации.</p>
</div>
<div className="border-2 border-foreground p-5">
<RulerIcon className="mb-8 size-7" />
<div className="text-3xl font-black uppercase">Что приложить</div>
<p className="mt-3 text-muted-foreground">План БТИ, фото объекта, референсы, примерный бюджет и желаемый срок запуска стройки.</p>
</div>
</div>
<ProjectBriefForm />
</div>
</section>
</SiteShell>
);
}

64
src/widgets/home-page.tsx Normal file
View File

@@ -0,0 +1,64 @@
import Link from "next/link";
import { ArrowUpRightIcon } from "lucide-react";
import { projects, site } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { SiteShell } from "@/widgets/site-shell";
import { ProjectImage } from "@/widgets/project-image";
export function HomePage() {
const featured = projects[0];
return (
<SiteShell>
<section className="lineform-grid border-b-2 border-foreground px-4 py-10 md:px-8 md:py-16">
<div className="mx-auto max-w-[1500px]">
<div className="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
<div>
<div className="mb-4 flex flex-wrap gap-2">
<Badge className="rounded-none border-2 border-foreground bg-background text-foreground">{site.descriptor}</Badge>
<Badge className="rounded-none border-2 border-foreground bg-accent text-foreground">{site.city}</Badge>
</div>
<h1 className="max-w-6xl text-[18vw] font-black uppercase leading-[0.78] tracking-normal md:text-[10vw]">
Жесткая форма для живой среды
</h1>
</div>
<p className="max-w-xl text-xl leading-8 md:justify-self-end">
Проектируем дома, интерьеры и рабочие пространства без декора ради декора: сценарии, свет, материалы и контроль стройки.
</p>
</div>
</div>
</section>
<section className="grid border-b-2 border-foreground md:grid-cols-[0.9fr_1.1fr]">
<div className="border-b-2 border-foreground p-4 md:border-b-0 md:border-r-2 md:p-8">
<ProjectImage project={featured} priority />
</div>
<div className="flex flex-col justify-between p-4 md:p-8">
<div>
<div className="mb-12 text-sm font-black uppercase">Featured / {featured.year}</div>
<h2 className="text-5xl font-black uppercase leading-none md:text-7xl">{featured.title}</h2>
<p className="mt-5 max-w-2xl text-lg leading-8 text-muted-foreground">{featured.summary}</p>
</div>
<div className="mt-12 grid gap-3 border-t-2 border-foreground pt-5 text-sm uppercase md:grid-cols-3">
<div>{featured.type}</div>
<div>{featured.location}</div>
<Link href="/work/courtyard-house" className="font-black underline md:text-right">Смотреть кейс</Link>
</div>
</div>
</section>
<section className="px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-4 md:grid-cols-4">
{projects.map((project) => (
<Link key={project.title} href={project.slug === "courtyard-house" ? "/work/courtyard-house" : "/work"} className="group border-2 border-foreground p-4 transition hover:bg-foreground hover:text-background">
<div className="mb-16 flex justify-between text-sm font-black uppercase">
<span>{project.index}</span>
<ArrowUpRightIcon className="size-5 transition group-hover:translate-x-1 group-hover:-translate-y-1" />
</div>
<h3 className="text-3xl font-black uppercase leading-none">{project.title}</h3>
<p className="mt-3 text-sm opacity-75">{project.type} / {project.location}</p>
</Link>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,22 @@
import { processSteps } from "@/entities/site-content";
import { PageTitle } from "@/shared/ui/page-title";
import { SiteShell } from "@/widgets/site-shell";
export function ProcessPage() {
return (
<SiteShell>
<PageTitle code="Method / 03" title="Процесс без тумана" text="На каждой стадии есть входные данные, результат и решения, которые должны быть утверждены до следующего шага." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto max-w-[1500px] divide-y-2 divide-foreground border-y-2 border-foreground">
{processSteps.map((item) => (
<div key={item.step} className="grid gap-4 py-6 md:grid-cols-[140px_0.8fr_1.2fr]">
<div className="text-5xl font-black">{item.step}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item.title}</h2>
<p className="leading-7 text-muted-foreground">{item.text}</p>
</div>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,35 @@
import { projects } from "@/entities/site-content";
import { SiteShell } from "@/widgets/site-shell";
import { ProjectImage } from "@/widgets/project-image";
export function ProjectDetailPage() {
const project = projects[0];
return (
<SiteShell>
<section className="grid min-h-[calc(100vh-58px)] border-b-2 border-foreground md:grid-cols-[0.95fr_1.05fr]">
<div className="flex flex-col justify-between p-4 md:p-8">
<div>
<div className="text-sm font-black uppercase">{project.index} / {project.location}</div>
<h1 className="mt-8 text-6xl font-black uppercase leading-none md:text-9xl">{project.title}</h1>
<p className="mt-6 max-w-xl text-xl leading-8 text-muted-foreground">{project.summary}</p>
</div>
<div className="mt-10 grid gap-2 text-sm uppercase md:grid-cols-3">
<span>{project.type}</span><span>{project.status}</span><span>{project.year}</span>
</div>
</div>
<div className="p-4 md:p-8"><ProjectImage project={project} priority /></div>
</section>
<section className="lineform-grid px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[1fr_1fr_1fr]">
{["Двор как приватная комната", "Галерея вместо коридора", "Материалы стареют достойно"].map((item, index) => (
<div key={item} className="border-2 border-foreground bg-background p-5">
<div className="mb-12 text-5xl font-black">0{index + 1}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item}</h2>
<p className="mt-4 leading-7 text-muted-foreground">Решение зафиксировано в планировке, узлах и сценариях света, чтобы стройка не превращала проект в набор компромиссов.</p>
</div>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,12 @@
import Image from "next/image";
import { projects } from "@/entities/site-content";
export function ProjectImage({ project, priority = false }: { project: (typeof projects)[number]; priority?: boolean }) {
return (
<div className="relative min-h-[360px] overflow-hidden border-2 border-foreground bg-muted md:min-h-[520px]">
<Image src={project.image} alt={project.title} fill priority={priority} className="arch-photo object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
<div className="absolute left-0 top-0 bg-background px-3 py-2 text-xs font-black uppercase">{project.index}</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import { DraftingCompassIcon } from "lucide-react";
import { services } from "@/entities/site-content";
import { PageTitle } from "@/shared/ui/page-title";
import { SiteShell } from "@/widgets/site-shell";
export function ServicesPage() {
return (
<SiteShell>
<PageTitle code="Service / 02" title="Не пакет услуг, а контроль решений" text="Каждый этап закрывает конкретный риск: неверная планировка, разъехавшийся бюджет, плохие узлы или стройка без авторского контроля." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto grid max-w-[1500px] gap-4 md:grid-cols-4">
{services.map((service) => (
<article key={service.title} className="min-h-80 border-2 border-foreground p-5">
<DraftingCompassIcon className="mb-12 size-8" />
<h2 className="text-3xl font-black uppercase leading-none">{service.title}</h2>
<p className="mt-4 leading-7 text-muted-foreground">{service.text}</p>
</article>
))}
</div>
</section>
</SiteShell>
);
}

View File

@@ -0,0 +1,30 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { navItems, site } from "@/entities/site-content";
import { Button } from "@/shared/ui/button";
export function SiteShell({ children }: { children: ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b-2 border-foreground bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-[1500px] items-center justify-between px-4 py-3 md:px-8">
<Link href="/" className="text-xl font-black uppercase tracking-normal">
{site.name}
</Link>
<nav className="hidden items-center gap-7 text-sm font-semibold uppercase md:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:underline">
{item.label}
</Link>
))}
</nav>
<Button asChild size="sm" className="rounded-none">
<Link href="/contact">Бриф</Link>
</Button>
</div>
</header>
{children}
</main>
);
}

View File

@@ -0,0 +1,35 @@
import Image from "next/image";
import { press, studioFacts } from "@/entities/site-content";
import { Badge } from "@/shared/ui/badge";
import { PageTitle } from "@/shared/ui/page-title";
import { SiteShell } from "@/widgets/site-shell";
export function StudioPage() {
return (
<SiteShell>
<PageTitle code="Studio / 04" title="Маленькая команда, строгая методика" text="Lineform объединяет архитекторов, интерьерных дизайнеров и менеджеров комплектации вокруг одной таблицы решений, сроков и ответственности." />
<section className="grid border-b-2 border-foreground md:grid-cols-[1fr_1fr]">
<div className="p-4 md:p-8">
<div className="relative min-h-[520px] overflow-hidden border-2 border-foreground">
<Image src="https://images.unsplash.com/photo-1600607687920-4e2a09cf159d?auto=format&fit=crop&w=1400&q=82" alt="Студия Lineform" fill className="arch-photo object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</div>
</div>
<div className="grid content-between gap-8 p-4 md:p-8">
<div className="grid gap-4">
{studioFacts.map((fact) => (
<div key={fact.value} className="border-2 border-foreground p-5">
<div className="text-6xl font-black">{fact.value}</div>
<p className="mt-2 max-w-md text-muted-foreground">{fact.label}</p>
</div>
))}
</div>
<div>
<h2 className="mb-3 text-sm font-black uppercase">Публикации и премии</h2>
<div className="flex flex-wrap gap-2">{press.map((item) => <Badge key={item} className="rounded-none border-2 border-foreground bg-background text-foreground">{item}</Badge>)}</div>
</div>
</div>
</section>
</SiteShell>
);
}

View File

@@ -1,257 +0,0 @@
import Image from "next/image";
import Link from "next/link";
import { ArrowUpRightIcon, DraftingCompassIcon, MailIcon, RulerIcon } from "lucide-react";
import { ProjectBriefForm } from "@/features/project-brief/ui/project-brief-form";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { navItems, press, processSteps, projects, services, site, studioFacts } from "@/entities/site-content";
function Shell({ children }: { children: React.ReactNode }) {
return (
<main className="min-h-screen bg-background text-foreground">
<header className="sticky top-0 z-30 border-b-2 border-foreground bg-background/95 backdrop-blur">
<div className="mx-auto flex max-w-[1500px] items-center justify-between px-4 py-3 md:px-8">
<Link href="/" className="text-xl font-black uppercase tracking-normal">
{site.name}
</Link>
<nav className="hidden items-center gap-7 text-sm font-semibold uppercase md:flex">
{navItems.map((item) => (
<Link key={item.href} href={item.href} className="hover:underline">
{item.label}
</Link>
))}
</nav>
<Button asChild size="sm" className="rounded-none">
<Link href="/contact">Бриф</Link>
</Button>
</div>
</header>
{children}
</main>
);
}
function PageTitle({ code, title, text }: { code: string; title: string; text: string }) {
return (
<section className="border-b-2 border-foreground px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[180px_1fr]">
<div className="text-sm font-black uppercase">{code}</div>
<div>
<h1 className="max-w-5xl text-5xl font-black uppercase leading-[0.88] md:text-8xl">{title}</h1>
<p className="mt-6 max-w-2xl text-lg leading-8 text-muted-foreground">{text}</p>
</div>
</div>
</section>
);
}
function ProjectImage({ project, priority = false }: { project: (typeof projects)[number]; priority?: boolean }) {
return (
<div className="relative min-h-[360px] overflow-hidden border-2 border-foreground bg-muted md:min-h-[520px]">
<Image src={project.image} alt={project.title} fill priority={priority} className="arch-photo object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
<div className="absolute left-0 top-0 bg-background px-3 py-2 text-xs font-black uppercase">{project.index}</div>
</div>
);
}
export function HomePage() {
const featured = projects[0];
return (
<Shell>
<section className="lineform-grid border-b-2 border-foreground px-4 py-10 md:px-8 md:py-16">
<div className="mx-auto max-w-[1500px]">
<div className="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
<div>
<div className="mb-4 flex flex-wrap gap-2">
<Badge className="rounded-none border-2 border-foreground bg-background text-foreground">{site.descriptor}</Badge>
<Badge className="rounded-none border-2 border-foreground bg-accent text-foreground">{site.city}</Badge>
</div>
<h1 className="max-w-6xl text-[18vw] font-black uppercase leading-[0.78] tracking-normal md:text-[10vw]">
Жесткая форма для живой среды
</h1>
</div>
<p className="max-w-xl text-xl leading-8 md:justify-self-end">
Проектируем дома, интерьеры и рабочие пространства без декора ради декора: сценарии, свет, материалы и контроль стройки.
</p>
</div>
</div>
</section>
<section className="grid border-b-2 border-foreground md:grid-cols-[0.9fr_1.1fr]">
<div className="border-b-2 border-foreground p-4 md:border-b-0 md:border-r-2 md:p-8">
<ProjectImage project={featured} priority />
</div>
<div className="flex flex-col justify-between p-4 md:p-8">
<div>
<div className="mb-12 text-sm font-black uppercase">Featured / {featured.year}</div>
<h2 className="text-5xl font-black uppercase leading-none md:text-7xl">{featured.title}</h2>
<p className="mt-5 max-w-2xl text-lg leading-8 text-muted-foreground">{featured.summary}</p>
</div>
<div className="mt-12 grid gap-3 border-t-2 border-foreground pt-5 text-sm uppercase md:grid-cols-3">
<div>{featured.type}</div>
<div>{featured.location}</div>
<Link href="/work/courtyard-house" className="font-black underline md:text-right">Смотреть кейс</Link>
</div>
</div>
</section>
<section className="px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-4 md:grid-cols-4">
{projects.map((project) => (
<Link key={project.title} href={project.slug === "courtyard-house" ? "/work/courtyard-house" : "/work"} className="group border-2 border-foreground p-4 transition hover:bg-foreground hover:text-background">
<div className="mb-16 flex justify-between text-sm font-black uppercase">
<span>{project.index}</span>
<ArrowUpRightIcon className="size-5 transition group-hover:translate-x-1 group-hover:-translate-y-1" />
</div>
<h3 className="text-3xl font-black uppercase leading-none">{project.title}</h3>
<p className="mt-3 text-sm opacity-75">{project.type} / {project.location}</p>
</Link>
))}
</div>
</section>
</Shell>
);
}
export function WorkPage() {
return (
<Shell>
<PageTitle code="Index / 01" title="Работы как система решений" text="Портфолио организовано не по красоте картинок, а по типам задач: частная жизнь, офисная среда, hospitality и предметные решения." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto max-w-[1500px] divide-y-2 divide-foreground border-y-2 border-foreground">
{projects.map((project) => (
<Link key={project.title} href={project.slug === "courtyard-house" ? "/work/courtyard-house" : "/work"} className="grid gap-4 py-5 transition hover:bg-foreground hover:px-4 hover:text-background md:grid-cols-[100px_1fr_200px_180px]">
<span className="font-black">{project.index}</span>
<span className="text-3xl font-black uppercase md:text-5xl">{project.title}</span>
<span className="self-center text-sm uppercase">{project.type}</span>
<span className="self-center text-sm uppercase md:text-right">{project.year}</span>
</Link>
))}
</div>
</section>
</Shell>
);
}
export function ProjectDetailPage() {
const project = projects[0];
return (
<Shell>
<section className="grid min-h-[calc(100vh-58px)] border-b-2 border-foreground md:grid-cols-[0.95fr_1.05fr]">
<div className="flex flex-col justify-between p-4 md:p-8">
<div>
<div className="text-sm font-black uppercase">{project.index} / {project.location}</div>
<h1 className="mt-8 text-6xl font-black uppercase leading-none md:text-9xl">{project.title}</h1>
<p className="mt-6 max-w-xl text-xl leading-8 text-muted-foreground">{project.summary}</p>
</div>
<div className="mt-10 grid gap-2 text-sm uppercase md:grid-cols-3">
<span>{project.type}</span><span>{project.status}</span><span>{project.year}</span>
</div>
</div>
<div className="p-4 md:p-8"><ProjectImage project={project} priority /></div>
</section>
<section className="lineform-grid px-4 py-12 md:px-8 md:py-20">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[1fr_1fr_1fr]">
{["Двор как приватная комната", "Галерея вместо коридора", "Материалы стареют достойно"].map((item, index) => (
<div key={item} className="border-2 border-foreground bg-background p-5">
<div className="mb-12 text-5xl font-black">0{index + 1}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item}</h2>
<p className="mt-4 leading-7 text-muted-foreground">Решение зафиксировано в планировке, узлах и сценариях света, чтобы стройка не превращала проект в набор компромиссов.</p>
</div>
))}
</div>
</section>
</Shell>
);
}
export function ServicesPage() {
return (
<Shell>
<PageTitle code="Service / 02" title="Не пакет услуг, а контроль решений" text="Каждый этап закрывает конкретный риск: неверная планировка, разъехавшийся бюджет, плохие узлы или стройка без авторского контроля." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto grid max-w-[1500px] gap-4 md:grid-cols-4">
{services.map((service) => (
<article key={service.title} className="min-h-80 border-2 border-foreground p-5">
<DraftingCompassIcon className="mb-12 size-8" />
<h2 className="text-3xl font-black uppercase leading-none">{service.title}</h2>
<p className="mt-4 leading-7 text-muted-foreground">{service.text}</p>
</article>
))}
</div>
</section>
</Shell>
);
}
export function ProcessPage() {
return (
<Shell>
<PageTitle code="Method / 03" title="Процесс без тумана" text="На каждой стадии есть входные данные, результат и решения, которые должны быть утверждены до следующего шага." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto max-w-[1500px] divide-y-2 divide-foreground border-y-2 border-foreground">
{processSteps.map((item) => (
<div key={item.step} className="grid gap-4 py-6 md:grid-cols-[140px_0.8fr_1.2fr]">
<div className="text-5xl font-black">{item.step}</div>
<h2 className="text-3xl font-black uppercase leading-none">{item.title}</h2>
<p className="leading-7 text-muted-foreground">{item.text}</p>
</div>
))}
</div>
</section>
</Shell>
);
}
export function StudioPage() {
return (
<Shell>
<PageTitle code="Studio / 04" title="Маленькая команда, строгая методика" text="Lineform объединяет архитекторов, интерьерных дизайнеров и менеджеров комплектации вокруг одной таблицы решений, сроков и ответственности." />
<section className="grid border-b-2 border-foreground md:grid-cols-[1fr_1fr]">
<div className="p-4 md:p-8">
<div className="relative min-h-[520px] overflow-hidden border-2 border-foreground">
<Image src="https://images.unsplash.com/photo-1600607687920-4e2a09cf159d?auto=format&fit=crop&w=1400&q=82" alt="Студия Lineform" fill className="arch-photo object-cover" sizes="(min-width: 1024px) 50vw, 100vw" />
</div>
</div>
<div className="grid content-between gap-8 p-4 md:p-8">
<div className="grid gap-4">
{studioFacts.map((fact) => (
<div key={fact.value} className="border-2 border-foreground p-5">
<div className="text-6xl font-black">{fact.value}</div>
<p className="mt-2 max-w-md text-muted-foreground">{fact.label}</p>
</div>
))}
</div>
<div>
<h2 className="mb-3 text-sm font-black uppercase">Публикации и премии</h2>
<div className="flex flex-wrap gap-2">{press.map((item) => <Badge key={item} className="rounded-none border-2 border-foreground bg-background text-foreground">{item}</Badge>)}</div>
</div>
</div>
</section>
</Shell>
);
}
export function ContactPage() {
return (
<Shell>
<PageTitle code="Brief / 05" title="Начать проект с ограничений" text="Расскажите о задаче коротко. На первом звонке мы проверим масштаб, сроки, бюджет и поймем, где можем быть полезны." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto grid max-w-[1500px] gap-8 md:grid-cols-[0.8fr_1.2fr]">
<div className="space-y-6">
<div className="border-2 border-foreground p-5">
<MailIcon className="mb-8 size-7" />
<div className="text-3xl font-black uppercase">{site.email}</div>
<p className="mt-3 text-muted-foreground">Ответим с вопросами по объекту и предложим формат первой консультации.</p>
</div>
<div className="border-2 border-foreground p-5">
<RulerIcon className="mb-8 size-7" />
<div className="text-3xl font-black uppercase">Что приложить</div>
<p className="mt-3 text-muted-foreground">План БТИ, фото объекта, референсы, примерный бюджет и желаемый срок запуска стройки.</p>
</div>
</div>
<ProjectBriefForm />
</div>
</section>
</Shell>
);
}

25
src/widgets/work-page.tsx Normal file
View File

@@ -0,0 +1,25 @@
import Link from "next/link";
import { projects } from "@/entities/site-content";
import { PageTitle } from "@/shared/ui/page-title";
import { SiteShell } from "@/widgets/site-shell";
export function WorkPage() {
return (
<SiteShell>
<PageTitle code="Index / 01" title="Работы как система решений" text="Портфолио организовано не по красоте картинок, а по типам задач: частная жизнь, офисная среда, hospitality и предметные решения." />
<section className="px-4 py-10 md:px-8">
<div className="mx-auto max-w-[1500px] divide-y-2 divide-foreground border-y-2 border-foreground">
{projects.map((project) => (
<Link key={project.title} href={project.slug === "courtyard-house" ? "/work/courtyard-house" : "/work"} className="grid gap-4 py-5 transition hover:bg-foreground hover:px-4 hover:text-background md:grid-cols-[100px_1fr_200px_180px]">
<span className="font-black">{project.index}</span>
<span className="text-3xl font-black uppercase md:text-5xl">{project.title}</span>
<span className="self-center text-sm uppercase">{project.type}</span>
<span className="self-center text-sm uppercase md:text-right">{project.year}</span>
</Link>
))}
</div>
</section>
</SiteShell>
);
}