diff --git a/AGENTS.md b/AGENTS.md index 6c9f516..fba5a25 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ Northline Clinic — planned care clinic template: пациентский мар ## 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` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/-page.tsx`). Header/footer — `src/widgets/site-shell.tsx`. См. File Map. - Keep cards at 8px radius or less and preserve the selected palette in `src/app/globals.css`. - This is a static frontend template: do not add real payments, auth, persistence, external API calls, or backend contracts without an explicit product request. - Keep the domain specific: planned care, triage, preparation, diagnostic route, result review, insurance/privacy and non-emergency boundaries. Do not flatten it into generic wellness cards. @@ -16,3 +16,63 @@ Northline Clinic — planned care clinic template: пациентский мар - Кириллица обязательна для видимого текста и выбранных Google Fonts. - При AI-правках сохраняйте доменные блоки этого шаблона: они отличают проект от generic landing. + +## Design System + +Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифты: заголовки и `.font-display` — **Manrope** (`--font-display`), body — **Source Sans 3** (`--font-sans`); mono замаплен на display. Работай через семантические классы Tailwind (`bg-primary`, `text-muted-foreground`, `border-border`, `border-primary/30`), не хардкодь hex/oklch. + +Личность: **calm clinical / planned care** — тёплый кремово-бежевый фон (low-chroma), приглушённый сине-зелёный teal `primary` (oklch 0.46 0.07 173) как доверие и навигация, мягкий мятный `secondary`, тёплый терракотовый `accent` (oklch 0.72 0.12 34) для редких сигналов. Текст — тёмный teal-ink, а не чистый чёрный. Никакой «больничной» стерильности и никакого агрессивного маркетинга — спокойная, аккуратная среда. + +| Роль | Light | Характер | +|---|---|---| +| `background` | тёплый кремово-бежевый (0.976) | основной фон страниц | +| `foreground` | тёмный teal-ink (0.18) | текст | +| `primary` | приглушённый teal | доверие: иконки, CTA, бейджи (`text-primary`), eyebrow, `border-primary/30` | +| `secondary` | мягкий мятный | вторичные бейджи (теги маршрутов) | +| `accent` | тёплый терракот | редкий сигнал (используется сдержанно) | +| `muted` | тёплый серо-бежевый | чипы, фон-плашки, вторичный текст | +| `card` | почти белый тёплый (0.995) | панели `ClinicalPanel` | +| `border` | мягкий тёплый серый | тонкие границы (`border border-border`) | + +Узнаваемые приёмы (держи их, это и есть «лицо» проекта): +- **Мягкие, но не круглые углы:** `--radius` = 0.5rem; почти всё — `rounded-md`. Спокойная гладкость без пузырей. +- **`ClinicalPanel` как базовый блок:** `rounded-md border border-border bg-card shadow-[0_1px_0_rgba(24,74,68,0.05)]` — почти плоская карточка с едва заметной teal-линией снизу. Это основной строительный кирпич контента. +- **Тонкие границы, плоскость:** `border border-border` (1px), без жёстких теней и offset-shadow. Глубина — минимальная. +- **Цветная семантика teal:** иконки, цифры-метрики, eyebrow и активные подписи — `text-primary`; границы-акценты — `border-primary/30`. +- **Типографика:** заголовки — `font-semibold` (не black), `text-5xl`/`text-6xl`, спокойный `leading-tight`/`leading-[1.04]`; eyebrow — `uppercase tracking-[0.16em] text-primary`. +- **Safety-блоки:** предупреждения о неэкстренном характере — `border-amber-200 bg-amber-50 text-amber-900/950` (единственное место с янтарём, намеренно). +- **Фон-фактура:** `.clinic-soft-grid` — мягкая teal/терракотовая сетка под hero с маской снизу; контент в `Container` (`max-w-[1320px]`), фото — `object-cover` БЕЗ grayscale (живые, тёплые снимки). + +Do / Don't: +- **Do:** держи спокойную кремово-teal палитру, плоские `ClinicalPanel`, тонкие границы, доменный язык (маршрут, подготовка, координатор, неэкстренный формат), amber только для safety. +- **Don't:** яркие/насыщенные цвета, жёсткие тени, grayscale-фото, `font-black`, агрессивные CTA, медицинские обещания/диагнозы — это ломает доверительную клиническую личность. + +## File Map + +| Route | Widget | +|---|---| +| `/` | `src/widgets/home-page.tsx` (`HomePage`) | +| `/services` | `src/widgets/services-page.tsx` (`ServicesPage`) | +| `/specialists` | `src/widgets/specialists-page.tsx` (`SpecialistsPage`) | +| `/booking` | `src/widgets/booking-page.tsx` (`BookingPage`) | +| `/patient-info` | `src/widgets/patient-info-page.tsx` (`PatientInfoPage`) | +| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) | + +Переиспользуемые блоки: +- `src/widgets/site-shell.tsx` — `SiteHeader` + `SiteFooter` (обёртка всех страниц через `app/layout.tsx`). +- `src/widgets/patient-journey.tsx` — `PatientJourney` (Home, Booking). +- `src/widgets/featured-grid.tsx` — `FeaturedGrid` (Home, Services). +- `src/widgets/specialist-schedule.tsx` — `SpecialistSchedule` (Home, Booking, Specialists). +- `src/widgets/patient-checklist.tsx` — `PatientChecklist` (Home, Patient info, Booking). +- `src/widgets/facility-board.tsx` — `FacilityBoard` (Home, Contacts). +- `src/widgets/safety-notes.tsx` — `SafetyNotes` (Home, Patient info, Booking). +- `src/widgets/clinical-disclaimer.tsx` — `ClinicalDisclaimer` (Home, Patient info, Booking, Services). +- `src/widgets/metric-strip.tsx` — `MetricStrip` (переиспользуемый блок-кит, готов к композиции). +- `src/shared/ui/container.tsx` — `Container` (контентная обёртка `max-w-[1320px]`, база всех секций). +- `src/shared/ui/clinical-panel.tsx` — `ClinicalPanel` (базовая карточка-панель). +- `src/shared/ui/section-header.tsx` — `SectionHeader` (eyebrow + заголовок + текст секции). +- `src/shared/ui/inner-hero.tsx` — `InnerHero` (заголовочная секция внутренних страниц). +- `src/shared/ui/icon-cards.tsx` — `IconCards` (Contacts, Services). +- `src/shared/ui/info-columns.tsx` — `InfoColumns`, `src/shared/ui/split-story.tsx` — `SplitStory` (переиспользуемые presentational-блоки-кит). + +Одноразовые блоки колоцированы со своей страницей: `PageHero`/`TestimonialBand` в `home-page.tsx`, `ServiceMatrix`/`PricingTiles` в `services-page.tsx`, `DoctorGrid`/`CtaPanel` в `specialists-page.tsx`, `ReservationForm` в `booking-page.tsx`, `InsurancePanel` в `patient-info-page.tsx`, `ContactForm` в `contacts-page.tsx`. diff --git a/src/app/booking/page.tsx b/src/app/booking/page.tsx index 0cd693d..f911ce8 100644 --- a/src/app/booking/page.tsx +++ b/src/app/booking/page.tsx @@ -1,29 +1,5 @@ -"use client"; - -import { - ClinicalDisclaimer, - InnerHero, - PatientChecklist, - PatientJourney, - ReservationForm, - SafetyNotes, - SpecialistSchedule, -} from "@/widgets/template-ui"; +import { BookingPage } from "@/widgets/booking-page"; export default function Page() { - return ( - <> - - - - - - - - - ); + return ; } diff --git a/src/app/contacts/page.tsx b/src/app/contacts/page.tsx index 9c73170..870015c 100644 --- a/src/app/contacts/page.tsx +++ b/src/app/contacts/page.tsx @@ -1,19 +1,5 @@ -"use client"; - -import { ContactForm, FacilityBoard, IconCards, InnerHero } from "@/widgets/template-ui"; -import { contactCards } from "@/entities/site-content"; +import { ContactsPage } from "@/widgets/contacts-page"; export default function Page() { - return ( - <> - - - - - - ); + return ; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0889cc2..18ef8b7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import { Manrope, Source_Sans_3 } 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 = Manrope({ variable: "--font-display", diff --git a/src/app/page.tsx b/src/app/page.tsx index 62212b4..5b1660a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,33 +1,5 @@ -"use client"; - -import { - ClinicalDisclaimer, - FacilityBoard, - FeaturedGrid, - PageHero, - PatientChecklist, - PatientJourney, - SafetyNotes, - SpecialistSchedule, - TestimonialBand, -} from "@/widgets/template-ui"; +import { HomePage } from "@/widgets/home-page"; export default function Page() { - return ( - <> - - - - - - - - - - - ); + return ; } diff --git a/src/app/patient-info/page.tsx b/src/app/patient-info/page.tsx index a152375..e3bd058 100644 --- a/src/app/patient-info/page.tsx +++ b/src/app/patient-info/page.tsx @@ -1,19 +1,5 @@ -"use client"; - -import { ClinicalDisclaimer, InnerHero, InsurancePanel, PatientChecklist, SafetyNotes } from "@/widgets/template-ui"; +import { PatientInfoPage } from "@/widgets/patient-info-page"; export default function Page() { - return ( - <> - - - - - - - ); + return ; } diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx index 9fce6ef..9414a07 100644 --- a/src/app/services/page.tsx +++ b/src/app/services/page.tsx @@ -1,24 +1,5 @@ -"use client"; - -import { ClinicalDisclaimer, FeaturedGrid, IconCards, InnerHero, PricingTiles, ServiceMatrix } from "@/widgets/template-ui"; +import { ServicesPage } from "@/widgets/services-page"; export default function Page() { - return ( - <> - - - - - - - - ); + return ; } diff --git a/src/app/specialists/page.tsx b/src/app/specialists/page.tsx index e5277a1..ef5f67c 100644 --- a/src/app/specialists/page.tsx +++ b/src/app/specialists/page.tsx @@ -1,23 +1,5 @@ -"use client"; - -import { CtaPanel, DoctorGrid, InnerHero, SpecialistSchedule } from "@/widgets/template-ui"; +import { SpecialistsPage } from "@/widgets/specialists-page"; export default function Page() { - return ( - <> - - - - - - ); + return ; } diff --git a/src/shared/ui/clinical-panel.tsx b/src/shared/ui/clinical-panel.tsx new file mode 100644 index 0000000..0acdc19 --- /dev/null +++ b/src/shared/ui/clinical-panel.tsx @@ -0,0 +1,5 @@ +import type { ReactNode } from "react"; + +export function ClinicalPanel({ children, className = "" }: { children: ReactNode; className?: string }) { + return
{children}
; +} diff --git a/src/shared/ui/container.tsx b/src/shared/ui/container.tsx new file mode 100644 index 0000000..3533df3 --- /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/icon-cards.tsx b/src/shared/ui/icon-cards.tsx new file mode 100644 index 0000000..ac0b73c --- /dev/null +++ b/src/shared/ui/icon-cards.tsx @@ -0,0 +1,25 @@ +import type { ComponentType } from "react"; + +import { carePillars } from "@/entities/site-content"; +import { ClinicalPanel } from "@/shared/ui/clinical-panel"; +import { Container } from "@/shared/ui/container"; + +type IconComponent = ComponentType<{ className?: string }>; +type IconItem = { title: string; text: string; icon: IconComponent }; + +export function IconCards({ items = carePillars }: { items?: readonly IconItem[] }) { + return ( + + {items.map((item) => { + const Icon = item.icon; + return ( + + +

{item.title}

+

{item.text}

+
+ ); + })} +
+ ); +} diff --git a/src/shared/ui/info-columns.tsx b/src/shared/ui/info-columns.tsx new file mode 100644 index 0000000..14c6706 --- /dev/null +++ b/src/shared/ui/info-columns.tsx @@ -0,0 +1,23 @@ +import { FileTextIcon } from "lucide-react"; + +import { ClinicalPanel } from "@/shared/ui/clinical-panel"; +import { Container } from "@/shared/ui/container"; + +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..70776dc --- /dev/null +++ b/src/shared/ui/inner-hero.tsx @@ -0,0 +1,16 @@ +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/section-header.tsx b/src/shared/ui/section-header.tsx new file mode 100644 index 0000000..c89e7f2 --- /dev/null +++ b/src/shared/ui/section-header.tsx @@ -0,0 +1,19 @@ +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..8aa4691 --- /dev/null +++ b/src/shared/ui/split-story.tsx @@ -0,0 +1,40 @@ +import Image from "next/image"; +import { CheckIcon } from "lucide-react"; + +import { ClinicalPanel } from "@/shared/ui/clinical-panel"; +import { Container } from "@/shared/ui/container"; + +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/booking-page.tsx b/src/widgets/booking-page.tsx new file mode 100644 index 0000000..252ce37 --- /dev/null +++ b/src/widgets/booking-page.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { CalendarCheckIcon } from "lucide-react"; + +import { bookingReasons } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +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 { Textarea } from "@/shared/ui/textarea"; +import { ClinicalDisclaimer } from "@/widgets/clinical-disclaimer"; +import { PatientChecklist } from "@/widgets/patient-checklist"; +import { PatientJourney } from "@/widgets/patient-journey"; +import { SafetyNotes } from "@/widgets/safety-notes"; +import { SpecialistSchedule } from "@/widgets/specialist-schedule"; + +function ReservationForm() { + return ( + +
+ + + Triage form + +

Запись начинается с цели визита

+

+ Форма статическая. Она показывает будущий booking flow: причина обращения, желаемый маршрут, документы и безопасное предупреждение + о неэкстренном формате. +

+
+ При резком ухудшении состояния, сильной боли, травме или угрозе жизни нужно обращаться в экстренные службы. +
+
+
+
+ + + + +
+
+
Причина обращения
+
+ {bookingReasons.map((reason) => ( + + ))} +
+
+