diff --git a/AGENTS.md b/AGENTS.md index a420faf..0d075e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,56 @@ Northyard Residences — premium real-estate шаблон девелопера: ## Project Specifics - Квартиры, локация, amenities и этапы покупки описаны в `src/entities/site-content.ts`. -- `src/app` — только маршруты; страницы собираются в `src/widgets/template-ui.tsx`. +- `src/app` — только маршруты; композиция каждой страницы живёт в отдельном widget (`src/widgets/-page.tsx`). См. File Map. - Mock-калькуляторы держи в `src/features/*/ui`; не подключай реальные банки, CRM, карты или формы отправки без запроса. - Не превращай шаблон в общий лендинг: каждая страница должна отвечать на конкретный вопрос покупателя недвижимости. - Проверка после правок: `pnpm lint` и `pnpm build`. + +## Design System + +Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифт — **Noto Sans** (`--font-northyard`), один гарнитур на весь UI, с включёнными OpenType-фичами `ss01`/`cv01` на `body`. Работай через семантические классы Tailwind (`bg-primary`, `text-foreground`, `border-border`, `text-muted-foreground`), никогда не хардкодь hex/oklch. + +Личность: **тёплый «каменный» premium real-estate** — палитра построена на тёплых бежево-песочных оттенках (hue ~83–90), почти без насыщенности. Фон — тёплый камень/песок, primary — глубокий тёмно-коричнево-серый «графит» (НЕ синий и НЕ чёрный), secondary и accent — приглушённый шалфейно-зелёный (hue ~120–128). Тон спокойный, дорогой, архитектурный: воздух, светлые карточки, тонкие линии. + +| Роль | Light | Характер | +|---|---|---| +| `background` | тёплый светлый камень/песок | основной фон страниц | +| `foreground` | тёмный тёплый графит | текст | +| `primary` | глубокий графит (тёплый) | бренд: лого-плашка, CTA-кнопки, числа, акцент линий планировок | +| `secondary` | приглушённый светлый шалфей | бейджи, мягкие плашки (`bg-secondary text-foreground`) | +| `accent` / `ring` | шалфейно-зелёный | hover/focus-кольца, точечные акценты | +| `muted` / `muted-foreground` | светлый песок / тёплый серо-бежевый | вторичный текст, подписи, hover-фон строк | +| `card` | почти белый тёплый | карточки, таблицы, sticky-панели на каменном фоне | +| `border` | тёплый бежевый | тонкие границы (1px) повсюду | + +Узнаваемые приёмы (держи их — это «лицо» шаблона): +- **Мягкие углы:** `--radius` = 0.5rem; карточки и панели `rounded-lg`, мелкие плашки `rounded-md`, лого-иконка `rounded-full`. +- **Тонкие тёплые границы:** `border` (1px) на каждой карточке/строке; структура держится на линиях, а не на тенях. +- **Bento-сетки:** крупные `grid` с асимметричными колонками (`lg:grid-cols-[1.1fr_0.9fr]`, `[1fr_380px]`, sticky правая колонка `lg:sticky lg:top-24`). +- **Каменные фоны секций:** утилита `.northyard-stone` (тёплый radial+linear градиент песок/шалфей) для hero и contacts. +- **Планировочная сетка:** утилита `.floor-grid` (32px графитовая сетка-миллиметровка) для floor plan и карты локации; зоны планировки — `border-2 border-primary/*`. +- **Типографика:** крупные `font-semibold` заголовки (`text-5xl`…`text-7xl`, плотный `leading-[0.98]`), мягкие eyebrow-бейджи `variant="outline"`. + +Do / Don't: +- **Do:** держи тёплую каменную палитру и шалфейный акцент; используй bento-сетки, light-карточки, sticky-панель заявки/ипотеки; продавай ежедневный сценарий покупателя (планировка, маршруты, сервис). +- **Don't:** не вводи холодный синий/чёрный primary, насыщенные или неоновые цвета, тяжёлые тени, острые `rounded-none` углы или generic SaaS-hero с градиентом — это ломает дорогой архитектурный тон. + +## File Map + +| Route | Widget | +|---|---| +| `/` | `src/widgets/home-page.tsx` (`HomePage`) | +| `/apartments` | `src/widgets/apartments-page.tsx` (`ApartmentsPage`) | +| `/apartments/a-1204` | `src/widgets/apartment-detail-page.tsx` (`ApartmentDetailPage`) | +| `/location` | `src/widgets/location-page.tsx` (`LocationPage`) | +| `/amenities` | `src/widgets/amenities-page.tsx` (`AmenitiesPage`) | +| `/purchase` | `src/widgets/purchase-page.tsx` (`PurchasePage`) | +| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) | + +Переиспользуемые блоки: +- `src/widgets/site-shell.tsx` — `SiteShell` (header + nav, обёртка всех страниц). +- `src/widgets/apartment-rows.tsx` — `ApartmentRows` (Home, Apartments; рендерит `apartments`). +- `src/shared/ui/section-header.tsx` — `SectionHeader` (заголовок секций: Home, Apartments, Location, Amenities, Purchase). +- `src/features/mortgage-calculator/ui/mortgage-calculator.tsx` — `MortgageCalculator` (mock-калькулятор: Home, Purchase). + +Одноразовые блоки колоцированы со своей страницей: `FloorPlanCard` (план квартиры) живёт внутри `src/widgets/apartment-detail-page.tsx`. diff --git a/src/app/amenities/page.tsx b/src/app/amenities/page.tsx index c0e67ee..d7c6d54 100644 --- a/src/app/amenities/page.tsx +++ b/src/app/amenities/page.tsx @@ -1,4 +1,4 @@ -import { AmenitiesPage } from "@/widgets/template-ui"; +import { AmenitiesPage } from "@/widgets/amenities-page"; export default function Page() { return ; diff --git a/src/app/apartments/a-1204/page.tsx b/src/app/apartments/a-1204/page.tsx index 912d6ed..e986a65 100644 --- a/src/app/apartments/a-1204/page.tsx +++ b/src/app/apartments/a-1204/page.tsx @@ -1,4 +1,4 @@ -import { ApartmentDetailPage } from "@/widgets/template-ui"; +import { ApartmentDetailPage } from "@/widgets/apartment-detail-page"; export default function Page() { return ; diff --git a/src/app/apartments/page.tsx b/src/app/apartments/page.tsx index a986474..de512bb 100644 --- a/src/app/apartments/page.tsx +++ b/src/app/apartments/page.tsx @@ -1,4 +1,4 @@ -import { ApartmentsPage } from "@/widgets/template-ui"; +import { ApartmentsPage } from "@/widgets/apartments-page"; export default function Page() { return ; diff --git a/src/app/contacts/page.tsx b/src/app/contacts/page.tsx index e24f643..870015c 100644 --- a/src/app/contacts/page.tsx +++ b/src/app/contacts/page.tsx @@ -1,4 +1,4 @@ -import { ContactsPage } from "@/widgets/template-ui"; +import { ContactsPage } from "@/widgets/contacts-page"; export default function Page() { return ; diff --git a/src/app/location/page.tsx b/src/app/location/page.tsx index b098a22..d785a48 100644 --- a/src/app/location/page.tsx +++ b/src/app/location/page.tsx @@ -1,4 +1,4 @@ -import { LocationPage } from "@/widgets/template-ui"; +import { LocationPage } from "@/widgets/location-page"; export default function Page() { return ; diff --git a/src/app/page.tsx b/src/app/page.tsx index 2794f3c..5b1660a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,4 +1,4 @@ -import { HomePage } from "@/widgets/template-ui"; +import { HomePage } from "@/widgets/home-page"; export default function Page() { return ; diff --git a/src/app/purchase/page.tsx b/src/app/purchase/page.tsx index d7b64fd..f6c7ad5 100644 --- a/src/app/purchase/page.tsx +++ b/src/app/purchase/page.tsx @@ -1,4 +1,4 @@ -import { PurchasePage } from "@/widgets/template-ui"; +import { PurchasePage } from "@/widgets/purchase-page"; export default function Page() { return ; diff --git a/src/shared/ui/section-header.tsx b/src/shared/ui/section-header.tsx new file mode 100644 index 0000000..4f42640 --- /dev/null +++ b/src/shared/ui/section-header.tsx @@ -0,0 +1,13 @@ +import { Badge } from "@/shared/ui/badge"; + +export function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) { + return ( +
+ {label} +
+

{title}

+

{text}

+
+
+ ); +} diff --git a/src/widgets/amenities-page.tsx b/src/widgets/amenities-page.tsx new file mode 100644 index 0000000..7f538a7 --- /dev/null +++ b/src/widgets/amenities-page.tsx @@ -0,0 +1,18 @@ +import { TreesIcon } from "lucide-react"; + +import { amenities } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function AmenitiesPage() { + return ( + +
+ +
+ {amenities.map((item, index) =>
0{index + 1}

{item.title}

{item.text}

)} +
+
+
+ ); +} diff --git a/src/widgets/apartment-detail-page.tsx b/src/widgets/apartment-detail-page.tsx new file mode 100644 index 0000000..268b4eb --- /dev/null +++ b/src/widgets/apartment-detail-page.tsx @@ -0,0 +1,49 @@ +import Link from "next/link"; +import { CheckIcon } from "lucide-react"; + +import { apartments } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { SiteShell } from "@/widgets/site-shell"; + +function FloorPlanCard() { + return ( +
+
+
Гостиная 32.4 м²
+
Кухня
+
Мастер
+
Детская
+
С/у
+
Пост.
+
+
A-1204 / 96.4 м²
+
+ ); +} + +export function ApartmentDetailPage() { + const apartment = apartments[0]; + return ( + +
+
+
+ {apartment.status} +

Квартира {apartment.id}

+

{apartment.rooms}, {apartment.area}, {apartment.plan}. Вид на {apartment.view}, этаж {apartment.floor}.

+
+
+ +
+
+
+ ); +} diff --git a/src/widgets/apartment-rows.tsx b/src/widgets/apartment-rows.tsx new file mode 100644 index 0000000..e462281 --- /dev/null +++ b/src/widgets/apartment-rows.tsx @@ -0,0 +1,21 @@ +import Link from "next/link"; + +import { apartments } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; + +export function ApartmentRows() { + return ( +
+ {apartments.map((apartment) => ( + + {apartment.id} + {apartment.rooms} / {apartment.area} + этаж {apartment.floor} + {apartment.view} + {apartment.price} + {apartment.status} + + ))} +
+ ); +} diff --git a/src/widgets/apartments-page.tsx b/src/widgets/apartments-page.tsx new file mode 100644 index 0000000..f9271a2 --- /dev/null +++ b/src/widgets/apartments-page.tsx @@ -0,0 +1,23 @@ +import { HomeIcon } from "lucide-react"; + +import { SectionHeader } from "@/shared/ui/section-header"; +import { ApartmentRows } from "@/widgets/apartment-rows"; +import { SiteShell } from "@/widgets/site-shell"; + +export function ApartmentsPage() { + return ( + +
+ +
+ +
+ +

Фильтры для будущего CMS

+

Комнаты, корпус, вид, отделка, статус брони и диапазон цены уже отражены в интерфейсе как production-ready блоки.

+
+
+
+
+ ); +} diff --git a/src/widgets/contacts-page.tsx b/src/widgets/contacts-page.tsx new file mode 100644 index 0000000..393c1af --- /dev/null +++ b/src/widgets/contacts-page.tsx @@ -0,0 +1,25 @@ +import { site } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { SiteShell } from "@/widgets/site-shell"; + +export function ContactsPage() { + return ( + +
+
+
+ Отдел продаж +

Запишитесь на приватный просмотр

+

Менеджер подготовит подборку квартир, планировки и финансовый сценарий до визита.

+
+
+
Телефон
{site.phone}
+
Email
{site.email}
+ +
+
+
+
+ ); +} diff --git a/src/widgets/home-page.tsx b/src/widgets/home-page.tsx new file mode 100644 index 0000000..d14e6cc --- /dev/null +++ b/src/widgets/home-page.tsx @@ -0,0 +1,48 @@ +import Image from "next/image"; +import Link from "next/link"; +import { ArrowRightIcon } from "lucide-react"; + +import { site } from "@/entities/site-content"; +import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { ApartmentRows } from "@/widgets/apartment-rows"; +import { SiteShell } from "@/widgets/site-shell"; + +export function HomePage() { + return ( + +
+
+
+ Ключи с IV квартала 2026 +

Квартал с приватным двором и городом в пешей доступности

+

{site.name} соединяет клубный сервис, спокойные планировки и инфраструктуру района без ощущения закрытого острова.

+
+ + +
+
+
+
+ Лобби жилого квартала +
+
+ {["214 квартир", "2 корпуса", "0 машин во дворе"].map((item) =>
{item}
)} +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ ); +} diff --git a/src/widgets/location-page.tsx b/src/widgets/location-page.tsx new file mode 100644 index 0000000..a7b612f --- /dev/null +++ b/src/widgets/location-page.tsx @@ -0,0 +1,22 @@ +import { MapPinIcon } from "lucide-react"; + +import { locationPoints, site } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function LocationPage() { + return ( + +
+ +
+
+ +
{site.address}
главный вход с пешеходной улицы
+
+
{locationPoints.map((point) =>
{point.time}

{point.title}

{point.text}

)}
+
+
+
+ ); +} diff --git a/src/widgets/purchase-page.tsx b/src/widgets/purchase-page.tsx new file mode 100644 index 0000000..c6258d5 --- /dev/null +++ b/src/widgets/purchase-page.tsx @@ -0,0 +1,25 @@ +import { construction, purchaseSteps } from "@/entities/site-content"; +import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator"; +import { Progress } from "@/shared/ui/progress"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function PurchasePage() { + return ( + +
+ +
+ +
+ {purchaseSteps.map((step, index) =>
0{index + 1}

{step}

)} +
+

Ход строительства

+
{construction.map((item) =>
{item.label}{item.value}%
)}
+
+
+
+
+
+ ); +} diff --git a/src/widgets/site-shell.tsx b/src/widgets/site-shell.tsx new file mode 100644 index 0000000..ee1d248 --- /dev/null +++ b/src/widgets/site-shell.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react"; +import Link from "next/link"; +import { Building2Icon } from "lucide-react"; + +import { navItems, site } from "@/entities/site-content"; +import { Button } from "@/shared/ui/button"; + +export function SiteShell({ children }: { children: ReactNode }) { + return ( +
+
+
+ + + {site.name} + + + +
+
+ {children} +
+ ); +} diff --git a/src/widgets/template-ui.tsx b/src/widgets/template-ui.tsx deleted file mode 100644 index 89b2b6d..0000000 --- a/src/widgets/template-ui.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { ArrowRightIcon, Building2Icon, CheckIcon, HomeIcon, MapPinIcon, TreesIcon } from "lucide-react"; - -import { MortgageCalculator } from "@/features/mortgage-calculator/ui/mortgage-calculator"; -import { apartments, amenities, construction, locationPoints, navItems, purchaseSteps, site } from "@/entities/site-content"; -import { Badge } from "@/shared/ui/badge"; -import { Button } from "@/shared/ui/button"; -import { Progress } from "@/shared/ui/progress"; - -function Shell({ children }: { children: React.ReactNode }) { - return ( -
-
-
- - - {site.name} - - - -
-
- {children} -
- ); -} - -function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) { - return ( -
- {label} -
-

{title}

-

{text}

-
-
- ); -} - -function FloorPlanCard() { - return ( -
-
-
Гостиная 32.4 м²
-
Кухня
-
Мастер
-
Детская
-
С/у
-
Пост.
-
-
A-1204 / 96.4 м²
-
- ); -} - -function ApartmentRows() { - return ( -
- {apartments.map((apartment) => ( - - {apartment.id} - {apartment.rooms} / {apartment.area} - этаж {apartment.floor} - {apartment.view} - {apartment.price} - {apartment.status} - - ))} -
- ); -} - -export function HomePage() { - return ( - -
-
-
- Ключи с IV квартала 2026 -

Квартал с приватным двором и городом в пешей доступности

-

{site.name} соединяет клубный сервис, спокойные планировки и инфраструктуру района без ощущения закрытого острова.

-
- - -
-
-
-
- Лобби жилого квартала -
-
- {["214 квартир", "2 корпуса", "0 машин во дворе"].map((item) =>
{item}
)} -
-
-
-
-
-
-
- -
-
-
-
-
-
- ); -} - -export function ApartmentsPage() { - return ( - -
- -
- -
- -

Фильтры для будущего CMS

-

Комнаты, корпус, вид, отделка, статус брони и диапазон цены уже отражены в интерфейсе как production-ready блоки.

-
-
-
-
- ); -} - -export function ApartmentDetailPage() { - const apartment = apartments[0]; - return ( - -
-
-
- {apartment.status} -

Квартира {apartment.id}

-

{apartment.rooms}, {apartment.area}, {apartment.plan}. Вид на {apartment.view}, этаж {apartment.floor}.

-
-
- -
-
-
- ); -} - -export function LocationPage() { - return ( - -
- -
-
- -
{site.address}
главный вход с пешеходной улицы
-
-
{locationPoints.map((point) =>
{point.time}

{point.title}

{point.text}

)}
-
-
-
- ); -} - -export function AmenitiesPage() { - return ( - -
- -
- {amenities.map((item, index) =>
0{index + 1}

{item.title}

{item.text}

)} -
-
-
- ); -} - -export function PurchasePage() { - return ( - -
- -
- -
- {purchaseSteps.map((step, index) =>
0{index + 1}

{step}

)} -
-

Ход строительства

-
{construction.map((item) =>
{item.label}{item.value}%
)}
-
-
-
-
-
- ); -} - -export function ContactsPage() { - return ( - -
-
-
- Отдел продаж -

Запишитесь на приватный просмотр

-

Менеджер подготовит подборку квартир, планировки и финансовый сценарий до визита.

-
-
-
Телефон
{site.phone}
-
Email
{site.email}
- -
-
-
-
- ); -}