diff --git a/AGENTS.md b/AGENTS.md index 414314a..6837f16 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Juniper Table — seasonal restaurant template for booking-driven dining: keep m ## Project Specifics - Source content lives in \`src/entities/site-content.ts\`; keep visible copy there or directly in route JSX. -- \`src/app\` is routing only; page composition belongs to \`src/widgets/template-ui.tsx\`. +- \`src/app\` is routing only; композиция каждой страницы живёт в отдельном widget (\`src/widgets/-page.tsx\`). См. File Map. - Keep cards at 8px radius or less and preserve the restaurant palette in \`src/app/globals.css\`. - This is a static frontend template: do not add real payments, deposits, table-management API, delivery, auth, persistence, external API calls, or backend contracts without an explicit product request. - Keep restaurant specificity: seating, service windows, host confirmation, allergies/dietary requests, pairing, supplier story and private dining packages. @@ -15,3 +15,67 @@ Juniper Table — seasonal restaurant template for booking-driven dining: keep m - Кириллица обязательна для видимого текста и выбранных Google Fonts. - Do not flatten this into a generic cafe landing page. Every page should answer a real restaurant business question. + +## Design System + +Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифты: заголовки (`h1/h2/h3`, `.font-display`) — **Cormorant Garamond** (`--font-display`, элегантный serif), текст/UI — **Manrope** (`--font-sans`). Работай через семантические классы Tailwind (`bg-primary`, `text-foreground`, `border-border`, `text-muted-foreground`, `font-display`), никогда не хардкодь hex/oklch. + +Личность: **тёплый сезонный fine-dining** — кремово-пергаментный фон (тёплый, hue ~75–92), глубокий приглушённый «можжевеловый» зелёный primary (hue ~145) как фирменный цвет, тёплый терракот/обожжённый оранжевый accent (hue ~34). Контраст элегантного serif-заголовка и спокойного гротеска в тексте. Тон ресторанный, тёплый, не «технологичный». + +| Роль | Light | Характер | +|---|---|---| +| `background` | тёплый кремовый пергамент | основной фон страниц | +| `foreground` | тёмный тёплый коричнево-чёрный | текст | +| `primary` | глубокий можжевеловый зелёный | бренд: лого JT, CTA, цены, акценты; footer/CTA-панели `bg-primary text-primary-foreground` | +| `accent` | терракот/обожжённый оранжевый | редкие тёплые точечные акценты (charts) | +| `secondary` | тёплый светлый песочный | мягкие бейджи, плашки | +| `muted` / `muted-foreground` | тёплый бежевый / приглушённый коричневый | вторичный текст, фон секций, hover | +| `card` | чистый белый | панели `RestaurantPanel`, формы, таблицы | +| `border` | тёплый бежевый | тонкие границы (1px) везде | + +Узнаваемые приёмы (держи их — это «лицо» шаблона): +- **Мягкие углы:** `--radius` = 0.5rem; всё на `rounded-md` (карточки, кнопки, фото-панели). Не используй острые `rounded-none` или сильно скруглённые pill-формы. +- **`RestaurantPanel`** (`src/shared/ui/restaurant-panel.tsx`) — фирменная панель: `rounded-md border border-border bg-card` + едва заметная тёплая тень `shadow-[0_1px_0_rgba(67,44,17,0.05)]`. Используй её для секционных карточек вместо голого `div`. +- **`Container`** (`src/shared/ui/container.tsx`) — `max-w-[1320px]` + `px-5 sm:px-8`; единый горизонтальный ритм страниц. +- **Hero-фон:** утилита `.restaurant-tablecloth` (тёплая «скатерть» — зелёно-охровая сетка-льняная фактура 42px с mask-fade) для главного hero. +- **Serif-дисплей:** заголовки через `font-display` (`text-4xl`…`text-[6.2rem]`, плотный `leading-[0.94]`); eyebrow-метки `text-xs font-semibold uppercase tracking-[0.18em] text-primary`. +- **Тонкие разделители:** `Separator` и `border-b border-border` в списках блюд/пакетов; структура на линиях, не на тенях. + +Do / Don't: +- **Do:** держи кремово-зелёную палитру и терракотовый акцент; serif-заголовки + Manrope-текст; оборачивай секции в `Container`/`RestaurantPanel`; продавай ресторанную специфику (посадка, pairing, аллергены, тайминг вечера). +- **Don't:** не вводи холодный синий/чёрный primary, неоновые градиенты, тяжёлые drop-shadow, острые углы или generic-SaaS hero — это ломает тёплый fine-dining тон. + +## File Map + +| Route | Widget | +|---|---| +| `/` | `src/widgets/home-page.tsx` (`HomePage`) | +| `/menu` | `src/widgets/menu-page.tsx` (`MenuPage`) | +| `/reservations` | `src/widgets/reservations-page.tsx` (`ReservationsPage`) | +| `/private-events` | `src/widgets/private-events-page.tsx` (`PrivateEventsPage`) | +| `/about` | `src/widgets/about-page.tsx` (`AboutPage`) | +| `/contact` | `src/widgets/contact-page.tsx` (`ContactPage`) | + +Переиспользуемые блоки: +- `src/widgets/site-shell.tsx` — `SiteHeader` + `SiteFooter` (обёртка из `src/app/layout.tsx`). +- `src/widgets/menu-section-board.tsx` — `MenuSectionBoard` (Home, Menu). +- `src/widgets/dish-showcase.tsx` — `DishShowcase` (Home, Menu; локальный `DishRow` внутри). +- `src/widgets/seating-guide.tsx` — `SeatingGuide` (Home, Reservations). +- `src/widgets/restaurant-evening-plan.tsx` — `RestaurantEveningPlan` (Home, Reservations). +- `src/widgets/private-events-grid.tsx` — `PrivateEventsGrid` (Home, Private events). +- `src/widgets/testimonial-band.tsx` — `TestimonialBand` (Home, About). +- `src/shared/ui/container.tsx` — `Container` (горизонтальный wrapper всех секций). +- `src/shared/ui/restaurant-panel.tsx` — `RestaurantPanel` (фирменная карточка-панель). +- `src/shared/ui/section-header.tsx` — `SectionHeader` (eyebrow + заголовок секции). +- `src/shared/ui/inner-hero.tsx` — `InnerHero` (хедер всех внутренних страниц). +- `src/shared/ui/info-columns.tsx` — `InfoColumns` (правила/dietary notes; используется в Menu и Reservations). +- `src/shared/ui/cta-panel.tsx` — `CtaPanel` (Menu, Private events). +- `src/shared/ui/split-story.tsx` — `SplitStory` (готовый props-блок image+points, сейчас не подключён ни одной страницей). + +Одноразовые блоки колоцированы со своей страницей: +- `PageHero` — внутри `home-page.tsx`. +- `FeaturedGrid`, `PairingBoard` (+ `WineIcon`), `PricingTiles`, `DietaryNotes` — внутри `menu-page.tsx`. +- `ReservationForm`, `ReservationRules` — внутри `reservations-page.tsx`. +- `ChefStory`, `SupplierLedger`, `MetricStrip` — внутри `about-page.tsx`. +- `IconCards`, `HoursBoard`, `ContactForm` — внутри `contact-page.tsx`. +- `EventPlanner` — внутри `private-events-page.tsx`. diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx index d93c4a7..66998db 100644 --- a/src/app/about/page.tsx +++ b/src/app/about/page.tsx @@ -1,19 +1,5 @@ -"use client"; - -import { ChefStory, InnerHero, MetricStrip, SupplierLedger, TestimonialBand } from "@/widgets/template-ui"; +import { AboutPage } from "@/widgets/about-page"; export default function Page() { - return ( - <> - - - - - - - ); + return ; } diff --git a/src/app/contact/page.tsx b/src/app/contact/page.tsx index 06fc439..2e99867 100644 --- a/src/app/contact/page.tsx +++ b/src/app/contact/page.tsx @@ -1,19 +1,5 @@ -"use client"; - -import { ContactForm, HoursBoard, IconCards, InnerHero } from "@/widgets/template-ui"; -import { contactCards } from "@/entities/site-content"; +import { ContactPage } from "@/widgets/contact-page"; export default function Page() { - return ( - <> - - - - - - ); + return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index deb68c7..53fce81 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import { Cormorant_Garamond, Manrope } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/shared/hooks/theme-provider"; import { ThemeMessageListener } from "@/shared/hooks/theme-message-listener"; -import { SiteHeader, SiteFooter } from "@/widgets/template-ui"; +import { SiteHeader, SiteFooter } from "@/widgets/site-shell"; const display = Cormorant_Garamond({ variable: "--font-display", diff --git a/src/app/menu/page.tsx b/src/app/menu/page.tsx index 97c1ec1..b7ba167 100644 --- a/src/app/menu/page.tsx +++ b/src/app/menu/page.tsx @@ -1,35 +1,5 @@ -"use client"; - -import { - CtaPanel, - DietaryNotes, - DishShowcase, - FeaturedGrid, - InnerHero, - MenuSectionBoard, - PairingBoard, - PricingTiles, -} from "@/widgets/template-ui"; +import { MenuPage } from "@/widgets/menu-page"; export default function Page() { - return ( - <> - - - - - - - - - - ); + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index c1d1d7a..5b1660a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,25 +1,5 @@ -"use client"; - -import { - DishShowcase, - MenuSectionBoard, - PageHero, - PrivateEventsGrid, - RestaurantEveningPlan, - SeatingGuide, - TestimonialBand, -} from "@/widgets/template-ui"; +import { HomePage } from "@/widgets/home-page"; export default function Page() { - return ( - <> - - - - - - - - - ); + return ; } diff --git a/src/app/private-events/page.tsx b/src/app/private-events/page.tsx index 5070cf9..9fff995 100644 --- a/src/app/private-events/page.tsx +++ b/src/app/private-events/page.tsx @@ -1,18 +1,5 @@ -"use client"; - -import { CtaPanel, EventPlanner, InnerHero, PrivateEventsGrid } from "@/widgets/template-ui"; +import { PrivateEventsPage } from "@/widgets/private-events-page"; export default function Page() { - return ( - <> - - - - - - ); + return ; } diff --git a/src/app/reservations/page.tsx b/src/app/reservations/page.tsx index 3511f96..14e20b8 100644 --- a/src/app/reservations/page.tsx +++ b/src/app/reservations/page.tsx @@ -1,19 +1,5 @@ -"use client"; - -import { InnerHero, ReservationForm, ReservationRules, RestaurantEveningPlan, SeatingGuide } from "@/widgets/template-ui"; +import { ReservationsPage } from "@/widgets/reservations-page"; export default function Page() { - return ( - <> - - - - - - - ); + return ; } diff --git a/src/shared/ui/container.tsx b/src/shared/ui/container.tsx new file mode 100644 index 0000000..e3dbf53 --- /dev/null +++ b/src/shared/ui/container.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export function Container({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/src/shared/ui/cta-panel.tsx b/src/shared/ui/cta-panel.tsx new file mode 100644 index 0000000..7c22416 --- /dev/null +++ b/src/shared/ui/cta-panel.tsx @@ -0,0 +1,19 @@ +import Link from "next/link"; +import { ArrowRightIcon } from "lucide-react"; + +import { Button } from "@/shared/ui/button"; +import { Container } from "@/shared/ui/container"; + +export function CtaPanel({ title, text, href, label }: { title: string; text: string; href: string; label: string }) { + return ( + +
+

{title}

+

{text}

+ +
+
+ ); +} diff --git a/src/shared/ui/info-columns.tsx b/src/shared/ui/info-columns.tsx new file mode 100644 index 0000000..290dd3f --- /dev/null +++ b/src/shared/ui/info-columns.tsx @@ -0,0 +1,23 @@ +import { StarIcon } from "lucide-react"; + +import { Container } from "@/shared/ui/container"; +import { RestaurantPanel } from "@/shared/ui/restaurant-panel"; + +type InfoItem = { title: string; text: string }; + +export function InfoColumns({ title, items }: { title: string; items: readonly InfoItem[] }) { + return ( + +

{title}

+
+ {items.map((item) => ( + + +

{item.title}

+

{item.text}

+
+ ))} +
+
+ ); +} diff --git a/src/shared/ui/inner-hero.tsx b/src/shared/ui/inner-hero.tsx new file mode 100644 index 0000000..d648e14 --- /dev/null +++ b/src/shared/ui/inner-hero.tsx @@ -0,0 +1,14 @@ +import { Badge } from "@/shared/ui/badge"; +import { Container } from "@/shared/ui/container"; + +export function InnerHero({ eyebrow, title, text }: { eyebrow: string; title: string; text: string }) { + return ( +
+ + {eyebrow} +

{title}

+

{text}

+
+
+ ); +} diff --git a/src/shared/ui/restaurant-panel.tsx b/src/shared/ui/restaurant-panel.tsx new file mode 100644 index 0000000..5eda08d --- /dev/null +++ b/src/shared/ui/restaurant-panel.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export function RestaurantPanel({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/src/shared/ui/section-header.tsx b/src/shared/ui/section-header.tsx new file mode 100644 index 0000000..6a4bc9f --- /dev/null +++ b/src/shared/ui/section-header.tsx @@ -0,0 +1,11 @@ +export function SectionHeader({ eyebrow, title, text }: { eyebrow: string; title: string; text?: string }) { + return ( +
+
+
{eyebrow}
+

{title}

+
+ {text ?

{text}

: null} +
+ ); +} diff --git a/src/shared/ui/split-story.tsx b/src/shared/ui/split-story.tsx new file mode 100644 index 0000000..1a4e9dc --- /dev/null +++ b/src/shared/ui/split-story.tsx @@ -0,0 +1,28 @@ +import Image from "next/image"; +import { CheckIcon } from "lucide-react"; + +import { Container } from "@/shared/ui/container"; +import { RestaurantPanel } from "@/shared/ui/restaurant-panel"; + +export function SplitStory({ image, eyebrow, title, text, points }: { image: string; eyebrow: string; title: string; text: string; points: readonly string[] }) { + return ( + + + {title} + +
+
{eyebrow}
+

{title}

+

{text}

+
+ {points.map((point) => ( +
+ + {point} +
+ ))} +
+
+
+ ); +} diff --git a/src/widgets/about-page.tsx b/src/widgets/about-page.tsx new file mode 100644 index 0000000..8656dc5 --- /dev/null +++ b/src/widgets/about-page.tsx @@ -0,0 +1,85 @@ +import Image from "next/image"; +import { CheckIcon } from "lucide-react"; + +import { highlights, suppliers } from "@/entities/site-content"; +import { Container } from "@/shared/ui/container"; +import { InnerHero } from "@/shared/ui/inner-hero"; +import { RestaurantPanel } from "@/shared/ui/restaurant-panel"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { TestimonialBand } from "@/widgets/testimonial-band"; + +function ChefStory() { + return ( + + + Kitchen team + +
+
Kitchen story
+

Точная кухня и тихая уверенность сервиса

+

+ Juniper Table строит меню вокруг короткого сезонного окна. Шеф-команда не обещает шоу: она держит продукт, соус, свет и темп вечера. +

+
+ {["поставки под меню недели", "открытая кухня без театральности", "зал держит ритм, а не давит на гостя"].map((point) => ( +
+ + {point} +
+ ))} +
+
+
+ ); +} + +function SupplierLedger() { + return ( + + +
+ {suppliers.map((supplier) => ( +
+
{supplier.name}
+
{supplier.product}
+
{supplier.cadence}
+
+ ))} +
+
+ ); +} + +function MetricStrip() { + return ( + + {highlights.map((item) => { + const Icon = item.icon; + return ( + + +
{item.value}
+

{item.title}

+

{item.text}

+
+ ); + })} +
+ ); +} + +export function AboutPage() { + return ( + <> + + + + + + + ); +} diff --git a/src/widgets/contact-page.tsx b/src/widgets/contact-page.tsx new file mode 100644 index 0000000..d621dcd --- /dev/null +++ b/src/widgets/contact-page.tsx @@ -0,0 +1,82 @@ +import type { ComponentType } from "react"; +import { ClockIcon } from "lucide-react"; + +import { contactCards, serviceWindows } from "@/entities/site-content"; +import { Button } from "@/shared/ui/button"; +import { Container } from "@/shared/ui/container"; +import { InnerHero } from "@/shared/ui/inner-hero"; +import { Input } from "@/shared/ui/input"; +import { RestaurantPanel } from "@/shared/ui/restaurant-panel"; +import { Textarea } from "@/shared/ui/textarea"; + +type IconComponent = ComponentType<{ className?: string }>; +type IconItem = { title: string; text: string; icon: IconComponent }; + +function IconCards({ items }: { items: readonly IconItem[] }) { + return ( + + {items.map((item) => { + const Icon = item.icon; + return ( + + +

{item.title}

+

{item.text}

+
+ ); + })} +
+ ); +} + +function HoursBoard() { + return ( + +
+ {serviceWindows.map((item) => ( + + +

{item.label}

+
{item.time}
+

{item.note}

+
+ ))} +
+
+ ); +} + +function ContactForm() { + return ( + + + +

Связаться с хостом

+

+ Контактная страница закрывает практику: как добраться, когда работает кухня, как подтвердить бронь и где указать ограничения. +

+
+
+ + +