diff --git a/AGENTS.md b/AGENTS.md index a8539ac..603b5f1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,8 +5,57 @@ Volthouse Energy — technical blueprint шаблон инженерной эн ## Project Specifics - Решения, проекты, сервис, financing, процесс и документы отчета описаны в `src/entities/site-content.ts`. -- `src/app` — только route wrappers; композиция страниц находится в `src/widgets/template-ui.tsx`. +- `src/app` — только route wrappers; композиция каждой страницы живёт в отдельном widget (`src/widgets/-page.tsx`). Header/footer-обёртка — `src/widgets/site-shell.tsx` (`SiteShell`). См. File Map. - Mock-калькуляторы держи в `src/features/*/ui`; не подключай реальные тарифы, счетчики, CRM или API без запроса. - Не превращай шаблон в generic clean-tech лендинг: каждая страница должна говорить языком объекта, пиков, теплопотерь, критичных линий, CAPEX/OPEX и сервисной ответственности. - Не добавляй декоративные абстрактные волны/градиентные пятна; визуальная система должна выглядеть как инженерная диспетчерская и проектная документация. - Проверка после правок: `pnpm lint` и `pnpm build`. + +## Design System + +Источник токенов — `src/app/globals.css` (`@theme inline` + `:root`/`.dark`). Шрифт — **Roboto Flex** (`--font-volthouse`), один гарнитур на sans и mono, с `font-feature-settings: "ss01" "cv01" "tnum"` (табличные цифры — это инженерный код данных). Работай через семантические классы Tailwind (`bg-primary`, `text-muted-foreground`, `border-primary/25`), не хардкодь hex/oklch. + +Личность: **dark engineering blueprint / диспетчерская** — глубокий сине-стальной фон (тёмный, low-chroma blue), янтарный `primary` (oklch 0.82 0.16 78) как «питание под напряжением», бирюзовый `accent` (oklch 0.76 0.118 177) для метрик экономии/результата, тёплый кремовый текст. Это шаблон по умолчанию ТЁМНЫЙ (`:root` уже тёмная) — светлой темы как основного режима нет. + +| Роль | Значение | Характер | +|---|---|---| +| `background` | тёмный сине-стальной (0.13) | фон-диспетчерская + grid-overlay в body | +| `foreground` | тёплый кремовый (0.95) | основной текст | +| `primary` | янтарный | «под напряжением»: бейджи, иконки-плашки, метрики, CTA, `border-primary/*` | +| `accent` | бирюзовый | результат/экономия: deliverable-блоки, OPEX, итоги (`bg-accent/10 text-accent`) | +| `secondary` | приглушённый стальной | вторичные плашки | +| `muted` | тёмно-серо-синий / fg 0.73 | подписи, описания | +| `card` | чуть светлее фона (0.18) | панели и карточки | +| `border` | стальной (0.36) | границы, чаще как `border-primary/25` | + +Узнаваемые приёмы (держи их, это и есть «лицо» проекта): +- **Острые углы:** `--radius` = 0.25rem; панели и карточки фактически прямоугольные — это техническая документация, а не мягкий SaaS. +- **Полупрозрачные янтарные границы:** `border border-primary/25` (и `/20`, `/35`, `/40`) повсюду — структура диспетчерской. +- **Grid-фактуры:** `.volt-grid` — миллиметровка под hero/section-header, `.volt-panel` — двухслойная сетка + угловой glow для data-панелей; сам `body` тоже несёт grid-overlay 72px. +- **Glow вместо тени:** `shadow-[0_0_60px_oklch(0.82_0.16_78_/_0.1)]` — мягкое янтарное свечение панели, не drop-shadow. +- **Типографика:** заголовки — `font-semibold uppercase leading-none`, очень крупные (`text-6xl`/`text-7xl`); цифры-метрики крупным `text-primary`/`text-accent`. +- **Цветовое кодирование:** янтарь = вход/мощность/действие, бирюза = выход/экономия/результат. Не смешивай роли. +- **Фото:** всегда `object-cover grayscale` + градиент `from-background` снизу — снимок встроен в тёмную среду. + +Do / Don't: +- **Do:** держи тёмную grid-среду, острые data-панели, янтарь/бирюзу по ролям, табличные цифры, доказательную подачу (мощность, CAPEX/OPEX, SLA). +- **Don't:** светлый фон, скруглённые карточки, drop-shadow вместо glow, пастель/градиентные пятна, generic clean-tech hero — это ломает инженерную личность. + +## File Map + +| Route | Widget | +|---|---| +| `/` | `src/widgets/home-page.tsx` (`HomePage`) | +| `/solutions` | `src/widgets/solutions-page.tsx` (`SolutionsPage`) | +| `/calculator` | `src/widgets/calculator-page.tsx` (`CalculatorPage`) | +| `/projects` | `src/widgets/projects-page.tsx` (`ProjectsPage`) | +| `/maintenance` | `src/widgets/maintenance-page.tsx` (`MaintenancePage`) | +| `/financing` | `src/widgets/financing-page.tsx` (`FinancingPage`) | +| `/contacts` | `src/widgets/contacts-page.tsx` (`ContactsPage`) | + +Переиспользуемые блоки: +- `src/widgets/site-shell.tsx` — `SiteShell` (header + nav + footer, обёртка всех страниц). +- `src/shared/ui/section-header.tsx` — `SectionHeader` (заголовочная секция внутренних страниц: Solutions, Calculator, Projects, Maintenance, Financing, Contacts). +- `src/features/energy-calculator/ui/energy-calculator.tsx` — `EnergyCalculator` (mock-калькулятор, Calculator). + +Одноразовые блоки колоцированы со своей страницей: `MetricStrip`/`EnergyBoard`/`SystemNode`/`StackCard`/`ProcessRail` в `home-page.tsx`, `ContactStat` в `contacts-page.tsx`. diff --git a/src/app/calculator/page.tsx b/src/app/calculator/page.tsx index 0856e17..cb11e07 100644 --- a/src/app/calculator/page.tsx +++ b/src/app/calculator/page.tsx @@ -1,4 +1,4 @@ -import { CalculatorPage } from "@/widgets/template-ui"; +import { CalculatorPage } from "@/widgets/calculator-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/financing/page.tsx b/src/app/financing/page.tsx index ef5d539..caa728e 100644 --- a/src/app/financing/page.tsx +++ b/src/app/financing/page.tsx @@ -1,4 +1,4 @@ -import { FinancingPage } from "@/widgets/template-ui"; +import { FinancingPage } from "@/widgets/financing-page"; export default function Page() { return ; diff --git a/src/app/maintenance/page.tsx b/src/app/maintenance/page.tsx index dc59fb4..a6ad5d6 100644 --- a/src/app/maintenance/page.tsx +++ b/src/app/maintenance/page.tsx @@ -1,4 +1,4 @@ -import { MaintenancePage } from "@/widgets/template-ui"; +import { MaintenancePage } from "@/widgets/maintenance-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/projects/page.tsx b/src/app/projects/page.tsx index 650f0da..dddec0f 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -1,4 +1,4 @@ -import { ProjectsPage } from "@/widgets/template-ui"; +import { ProjectsPage } from "@/widgets/projects-page"; export default function Page() { return ; diff --git a/src/app/solutions/page.tsx b/src/app/solutions/page.tsx index d78a449..fc794f3 100644 --- a/src/app/solutions/page.tsx +++ b/src/app/solutions/page.tsx @@ -1,4 +1,4 @@ -import { SolutionsPage } from "@/widgets/template-ui"; +import { SolutionsPage } from "@/widgets/solutions-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..31bbd9e --- /dev/null +++ b/src/shared/ui/section-header.tsx @@ -0,0 +1,15 @@ +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/calculator-page.tsx b/src/widgets/calculator-page.tsx new file mode 100644 index 0000000..2b203fd --- /dev/null +++ b/src/widgets/calculator-page.tsx @@ -0,0 +1,35 @@ +import { ClipboardCheckIcon, FileTextIcon } from "lucide-react"; + +import { EnergyCalculator } from "@/features/energy-calculator/ui/energy-calculator"; +import { documents } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function CalculatorPage() { + return ( + + +
+
+ + +
+
+
+ ); +} diff --git a/src/widgets/contacts-page.tsx b/src/widgets/contacts-page.tsx new file mode 100644 index 0000000..562c8af --- /dev/null +++ b/src/widgets/contacts-page.tsx @@ -0,0 +1,61 @@ +import { LeafIcon, PhoneCallIcon } from "lucide-react"; + +import { site } from "@/entities/site-content"; +import { Button } from "@/shared/ui/button"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +function ContactStat({ label, value }: { label: string; value: string }) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +export function ContactsPage() { + return ( + + +
+
+
+ +

Инженерный бриф

+
+ {[ + "счет за 12 месяцев", + "адрес и тип объекта", + "фото кровли и щитовой", + "критичные линии", + "график работы", + "желательный срок запуска", + ].map((item) => ( +
{item}
+ ))} +
+
+ + + +
+
+ +
+
+
+ ); +} diff --git a/src/widgets/financing-page.tsx b/src/widgets/financing-page.tsx new file mode 100644 index 0000000..7adf2ec --- /dev/null +++ b/src/widgets/financing-page.tsx @@ -0,0 +1,47 @@ +import { CheckCircle2Icon, ShieldCheckIcon } from "lucide-react"; + +import { financing } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function FinancingPage() { + return ( + + +
+
+ {financing.map((item) => ( +
+ +

{item.title}

+
+
+
аванс
+
{item.upfront}
+
+
+
горизонт
+
{item.term}
+
+
+

{item.text}

+
{item.bestFor}
+
+ {item.points.map((point) => ( +
+ + {point} +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/widgets/home-page.tsx b/src/widgets/home-page.tsx new file mode 100644 index 0000000..9c82ceb --- /dev/null +++ b/src/widgets/home-page.tsx @@ -0,0 +1,196 @@ +import Image from "next/image"; +import Link from "next/link"; +import { + BatteryChargingIcon, + CheckCircle2Icon, + GaugeIcon, + SunIcon, + ThermometerSunIcon, +} from "lucide-react"; + +import { auditMetrics, heroFacts, process, site, solutionStack } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { SiteShell } from "@/widgets/site-shell"; + +function MetricStrip() { + return ( +
+
+ {auditMetrics.map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+
+ ); +} + +function SystemNode({ + icon, + title, + value, + detail, +}: { + icon: React.ReactNode; + title: string; + value: string; + detail: string; +}) { + return ( +
+
{icon}
+
+
{title}
+
{detail}
+
+
{value}
+
+ ); +} + +function EnergyBoard() { + return ( +
+
+
+ Солнечная станция на кровле промышленного объекта +
+
+ {heroFacts.map((item) => ( +
+
{item.label}
+
{item.value}
+
{item.detail}
+
+ ))} +
+
+
+
+
+
object model
+

Cold storage / 126 кВт пик

+
+ live spec +
+
+ } title="PV roof" value="212 кВт" detail="31 строка, 4 инвертора" /> + } title="Battery rack" value="320 кВт⋅ч" detail="резерв холода и IT" /> + } title="Load panel" value="A/B priority" detail="переключение до 1 сек" /> + } title="Heat loop" value="COP 3.8" detail="буфер 2 000 литров" /> +
+
+
+ прогноз OPEX после ввода + -2.8 млн ₽ / год +
+
+
+
+
+
+
+
+ ); +} + +function StackCard({ + item, + icon, +}: { + item: (typeof solutionStack)[number]; + icon: React.ReactNode; +}) { + return ( +
+
+ {icon} + {item.range} +
+

{item.title}

+

{item.text}

+
+ {item.includes.map((point) => ( +
+ + {point} +
+ ))} +
+
+ ); +} + +function ProcessRail() { + return ( +
+
+
+

Процесс без тумана в смете

+

+ Шаблон продает не только панели и насосы. Он объясняет, какие исходные данные нужны, + где инженер проверяет объект и почему монтаж не должен ломать операционный режим. +

+
+
+ {process.map((item) => ( +
+
{item.step}
+

{item.title}

+

{item.text}

+
+ ))} +
+
+
+ ); +} + +export function HomePage() { + return ( + +
+
+
+ {site.tagline} +

+ Сначала расчет, потом оборудование +

+

+ Volthouse проектирует солнечную генерацию, тепловые насосы и батарейный резерв + для объектов, где ошибка в мощности превращается в дорогой простой. +

+
+ + +
+
+ +
+
+ +
+
+ } /> + } /> + } /> +
+
+ +
+ ); +} diff --git a/src/widgets/maintenance-page.tsx b/src/widgets/maintenance-page.tsx new file mode 100644 index 0000000..0351312 --- /dev/null +++ b/src/widgets/maintenance-page.tsx @@ -0,0 +1,55 @@ +import { WrenchIcon } from "lucide-react"; + +import { maintenance } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function MaintenancePage() { + return ( + + +
+
+
+ +

SLA console

+

+ Все события системы собираются в журнал: аварии, просадки выработки, + перегрев, падение COP, ошибки инверторов и ручные работы инженера. +

+
+
+ critical alert + 15 минут +
+
+ monthly report + до 5 числа +
+
+ field visit + по регламенту +
+
+
+
+ {maintenance.map((item, index) => ( +
+
0{index + 1}
+
+

{item.title}

+

{item.text}

+
+
{item.response}
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/widgets/projects-page.tsx b/src/widgets/projects-page.tsx new file mode 100644 index 0000000..f8d270d --- /dev/null +++ b/src/widgets/projects-page.tsx @@ -0,0 +1,54 @@ +import Image from "next/image"; + +import { projects } from "@/entities/site-content"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function ProjectsPage() { + return ( + + +
+
+ {projects.map((project) => ( +
+
+ {project.title} +
+
+
{project.location}
+

{project.title}

+
+
+
+
{project.type}
+

{project.challenge}

+
+ {project.result} +
+
+ {project.stats.map((stat) => ( +
+
{stat.label}
+
{stat.value}
+
+ ))} +
+
+
+ ))} +
+
+
+ ); +} diff --git a/src/widgets/site-shell.tsx b/src/widgets/site-shell.tsx new file mode 100644 index 0000000..cb7c57e --- /dev/null +++ b/src/widgets/site-shell.tsx @@ -0,0 +1,42 @@ +import Link from "next/link"; +import { ZapIcon } from "lucide-react"; + +import { navItems, site } from "@/entities/site-content"; +import { Button } from "@/shared/ui/button"; + +export function SiteShell({ children }: { children: React.ReactNode }) { + return ( +
+
+
+ + + + + {site.name} + + + +
+
+ {children} +
+
+
{site.name} - проектирование, монтаж и сервис энергосистем.
+
+ {site.city} + {site.email} +
+
+
+
+ ); +} diff --git a/src/widgets/solutions-page.tsx b/src/widgets/solutions-page.tsx new file mode 100644 index 0000000..673029b --- /dev/null +++ b/src/widgets/solutions-page.tsx @@ -0,0 +1,46 @@ +import { CheckCircle2Icon } from "lucide-react"; + +import { solutions } from "@/entities/site-content"; +import { Badge } from "@/shared/ui/badge"; +import { SectionHeader } from "@/shared/ui/section-header"; +import { SiteShell } from "@/widgets/site-shell"; + +export function SolutionsPage() { + return ( + + +
+
+ {solutions.map((solution) => ( +
+
+ + {solution.power} + + {solution.fit} +
+

{solution.title}

+

{solution.text}

+
+
deliverable
+
{solution.deliverable}
+
+
+ {solution.includes.map((point) => ( +
+ + {point} +
+ ))} +
+
+ ))} +
+
+
+ ); +} diff --git a/src/widgets/template-ui.tsx b/src/widgets/template-ui.tsx deleted file mode 100644 index 3c4750d..0000000 --- a/src/widgets/template-ui.tsx +++ /dev/null @@ -1,531 +0,0 @@ -import Image from "next/image"; -import Link from "next/link"; -import { - BatteryChargingIcon, - CheckCircle2Icon, - ClipboardCheckIcon, - FileTextIcon, - GaugeIcon, - LeafIcon, - PhoneCallIcon, - ShieldCheckIcon, - SunIcon, - ThermometerSunIcon, - WrenchIcon, - ZapIcon, -} from "lucide-react"; - -import { EnergyCalculator } from "@/features/energy-calculator/ui/energy-calculator"; -import { - auditMetrics, - documents, - financing, - heroFacts, - maintenance, - navItems, - process, - projects, - site, - solutions, - solutionStack, -} from "@/entities/site-content"; -import { Badge } from "@/shared/ui/badge"; -import { Button } from "@/shared/ui/button"; - -function Shell({ children }: { children: React.ReactNode }) { - return ( -
-
-
- - - - - {site.name} - - - -
-
- {children} -
-
-
{site.name} - проектирование, монтаж и сервис энергосистем.
-
- {site.city} - {site.email} -
-
-
-
- ); -} - -function SectionHeader({ label, title, text }: { label: string; title: string; text: string }) { - return ( -
-
- {label} -

- {title} -

-

{text}

-
-
- ); -} - -function MetricStrip() { - return ( -
-
- {auditMetrics.map((item) => ( -
-
{item.label}
-
{item.value}
-
- ))} -
-
- ); -} - -function EnergyBoard() { - return ( -
-
-
- Солнечная станция на кровле промышленного объекта -
-
- {heroFacts.map((item) => ( -
-
{item.label}
-
{item.value}
-
{item.detail}
-
- ))} -
-
-
-
-
-
object model
-

Cold storage / 126 кВт пик

-
- live spec -
-
- } title="PV roof" value="212 кВт" detail="31 строка, 4 инвертора" /> - } title="Battery rack" value="320 кВт⋅ч" detail="резерв холода и IT" /> - } title="Load panel" value="A/B priority" detail="переключение до 1 сек" /> - } title="Heat loop" value="COP 3.8" detail="буфер 2 000 литров" /> -
-
-
- прогноз OPEX после ввода - -2.8 млн ₽ / год -
-
-
-
-
-
-
-
- ); -} - -function SystemNode({ - icon, - title, - value, - detail, -}: { - icon: React.ReactNode; - title: string; - value: string; - detail: string; -}) { - return ( -
-
{icon}
-
-
{title}
-
{detail}
-
-
{value}
-
- ); -} - -function StackCard({ - item, - icon, -}: { - item: (typeof solutionStack)[number]; - icon: React.ReactNode; -}) { - return ( -
-
- {icon} - {item.range} -
-

{item.title}

-

{item.text}

-
- {item.includes.map((point) => ( -
- - {point} -
- ))} -
-
- ); -} - -function ProcessRail() { - return ( -
-
-
-

Процесс без тумана в смете

-

- Шаблон продает не только панели и насосы. Он объясняет, какие исходные данные нужны, - где инженер проверяет объект и почему монтаж не должен ломать операционный режим. -

-
-
- {process.map((item) => ( -
-
{item.step}
-

{item.title}

-

{item.text}

-
- ))} -
-
-
- ); -} - -export function HomePage() { - return ( - -
-
-
- {site.tagline} -

- Сначала расчет, потом оборудование -

-

- Volthouse проектирует солнечную генерацию, тепловые насосы и батарейный резерв - для объектов, где ошибка в мощности превращается в дорогой простой. -

-
- - -
-
- -
-
- -
-
- } /> - } /> - } /> -
-
- -
- ); -} - -export function SolutionsPage() { - return ( - - -
-
- {solutions.map((solution) => ( -
-
- - {solution.power} - - {solution.fit} -
-

{solution.title}

-

{solution.text}

-
-
deliverable
-
{solution.deliverable}
-
-
- {solution.includes.map((point) => ( -
- - {point} -
- ))} -
-
- ))} -
-
-
- ); -} - -export function CalculatorPage() { - return ( - - -
-
- - -
-
-
- ); -} - -export function ProjectsPage() { - return ( - - -
-
- {projects.map((project) => ( -
-
- {project.title} -
-
-
{project.location}
-

{project.title}

-
-
-
-
{project.type}
-

{project.challenge}

-
- {project.result} -
-
- {project.stats.map((stat) => ( -
-
{stat.label}
-
{stat.value}
-
- ))} -
-
-
- ))} -
-
-
- ); -} - -export function MaintenancePage() { - return ( - - -
-
-
- -

SLA console

-

- Все события системы собираются в журнал: аварии, просадки выработки, - перегрев, падение COP, ошибки инверторов и ручные работы инженера. -

-
-
- critical alert - 15 минут -
-
- monthly report - до 5 числа -
-
- field visit - по регламенту -
-
-
-
- {maintenance.map((item, index) => ( -
-
0{index + 1}
-
-

{item.title}

-

{item.text}

-
-
{item.response}
-
- ))} -
-
-
-
- ); -} - -export function FinancingPage() { - return ( - - -
-
- {financing.map((item) => ( -
- -

{item.title}

-
-
-
аванс
-
{item.upfront}
-
-
-
горизонт
-
{item.term}
-
-
-

{item.text}

-
{item.bestFor}
-
- {item.points.map((point) => ( -
- - {point} -
- ))} -
-
- ))} -
-
-
- ); -} - -export function ContactsPage() { - return ( - - -
-
-
- -

Инженерный бриф

-
- {[ - "счет за 12 месяцев", - "адрес и тип объекта", - "фото кровли и щитовой", - "критичные линии", - "график работы", - "желательный срок запуска", - ].map((item) => ( -
{item}
- ))} -
-
- - - -
-
- -
-
-
- ); -} - -function ContactStat({ label, value }: { label: string; value: string }) { - return ( -
-
{label}
-
{value}
-
- ); -}