feat: split big file and update agents.md
This commit is contained in:
50
AGENTS.md
50
AGENTS.md
@@ -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`).
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
13
src/shared/ui/page-title.tsx
Normal file
13
src/shared/ui/page-title.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/widgets/contact-page.tsx
Normal file
31
src/widgets/contact-page.tsx
Normal 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
64
src/widgets/home-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
src/widgets/process-page.tsx
Normal file
22
src/widgets/process-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/widgets/project-detail-page.tsx
Normal file
35
src/widgets/project-detail-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/widgets/project-image.tsx
Normal file
12
src/widgets/project-image.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/widgets/services-page.tsx
Normal file
24
src/widgets/services-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/widgets/site-shell.tsx
Normal file
30
src/widgets/site-shell.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/widgets/studio-page.tsx
Normal file
35
src/widgets/studio-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
25
src/widgets/work-page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user