feat(sprint-1): инициализация monorepo, Next.js, NestJS, страница логотипа

Инфраструктура:
- pnpm workspaces monorepo (apps/web, apps/api, packages/)
- docker-compose.yml: PostgreSQL 16
- .env.example: DATABASE_URL, API_PORT, NEXT_PUBLIC_API_URL

Backend (apps/api — NestJS 11):
- Инициализирован NestJS с pnpm
- Prisma 7 + prisma.config.ts подключен к PostgreSQL
- Схема: User (role: viewer/editor), ExperimentalComponent (status: draft/review/approved)

Frontend (apps/web — Next.js 16):
- App Router, TypeScript, Tailwind CSS 4, Fira Sans (Google Fonts)
- globals.css: CSS-токены бренда (цвета 053M–080M, шрифты)
- layout.tsx: корневой layout с боковой навигацией
- Sidebar.tsx: навигация по всем разделам (Фундамент, Компоненты, Блоки, Страницы, Оффлайн, Эксперименты)
- page.tsx: редирект → /foundation/logo
- /foundation/logo: полная страница «Логотип»
  - Иерархия и версии (Основной / Общий)
  - Цветовые варианты (основной, инвертированный, на форме)
  - Охранная зона с визуализацией
  - Таблица минимальных размеров (форма сотрудников)
  - Недопустимые варианты (6 правил)
  - Блок скачивания (placeholder до получения вектора)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AR 15 M4
2026-03-22 17:13:42 +05:00
parent 2352e99093
commit 5105310c92
44 changed files with 14499 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+466
View File
@@ -0,0 +1,466 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Логотип | Брендбук О!Клиника",
};
/* ─── Компонент: плашка правила ─────────────────────────────── */
function RuleTag({ children }: { children: React.ReactNode }) {
return (
<span
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
>
{children}
</span>
);
}
/* ─── Компонент: секция брендбука ────────────────────────────── */
function Section({
id,
title,
subtitle,
children,
}: {
id?: string;
title: string;
subtitle?: string;
children: React.ReactNode;
}) {
return (
<section id={id} className="mb-12">
<div className="mb-6">
<h2
className="text-xl font-semibold"
style={{ color: "var(--bb-text)" }}
>
{title}
</h2>
{subtitle && (
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
{subtitle}
</p>
)}
</div>
{children}
</section>
);
}
/* ─── SVG-заглушка логотипа (до получения вектора) ─────────── */
function LogoPlaceholder({
variant = "main",
size = "md",
}: {
variant?: "main" | "general" | "inverted" | "brown" | "white";
size?: "sm" | "md" | "lg";
}) {
const sizes = { sm: 160, md: 280, lg: 380 };
const w = sizes[size];
const bg =
variant === "inverted"
? "var(--brand-073m)"
: variant === "white"
? "var(--brand-053m)"
: "#f8f9fa";
const tealColor =
variant === "inverted" || variant === "white"
? "#ffffff"
: "var(--brand-053m)";
const darkColor =
variant === "inverted" || variant === "white"
? "#ffffff"
: "var(--brand-073m)";
const brownColor =
variant === "brown" ? "var(--brand-080m)" : tealColor;
return (
<div
className="flex items-center justify-center rounded-lg border p-6"
style={{
background: bg,
borderColor:
variant === "inverted" || variant === "white"
? "transparent"
: "var(--bb-border)",
width: w,
minHeight: Math.round(w * 0.55),
}}
>
{/* SVG-приближение логотипа */}
<svg
viewBox="0 0 240 120"
width={w - 48}
xmlns="http://www.w3.org/2000/svg"
aria-label="Логотип Клиника УХО ГОРЛО НОС"
>
{/* Графический элемент — три капли */}
<ellipse cx="18" cy="22" rx="8" ry="12" fill={brownColor} />
<ellipse cx="30" cy="10" rx="6" ry="9" fill={brownColor} />
<ellipse cx="10" cy="38" rx="10" ry="7" fill={brownColor} opacity="0.85" />
{/* Текст КЛИНИКА */}
<text
x="46"
y="26"
fontFamily="Arial, sans-serif"
fontWeight="700"
fontSize="20"
fill={darkColor}
letterSpacing="1"
>
КЛИНИКА
</text>
{/* Текст УХО ГОРЛО НОС */}
<text
x="46"
y="54"
fontFamily="Arial, sans-serif"
fontWeight="700"
fontSize="20"
fill={tealColor}
letterSpacing="1"
>
УХОГОРЛОНОС
</text>
{/* Текст ИМ. ПРОФ. */}
<text
x="46"
y="78"
fontFamily="Arial, sans-serif"
fontWeight="400"
fontSize="14"
fill={darkColor}
letterSpacing="0.5"
>
ИМ. ПРОФ. Е.Н. ОЛЕНЕВОЙ
</text>
{/* Метка версии */}
{variant === "general" && (
<text
x="46"
y="100"
fontFamily="Arial, sans-serif"
fontWeight="400"
fontSize="10"
fill={darkColor}
opacity="0.5"
>
Общий (сеть клиник)
</text>
)}
{variant === "main" && (
<text
x="46"
y="100"
fontFamily="Arial, sans-serif"
fontWeight="400"
fontSize="10"
fill={darkColor}
opacity="0.5"
>
Основной (направление)
</text>
)}
</svg>
</div>
);
}
/* ─── Компонент: таблица охранной зоны ─────────────────────── */
function ClearspaceDemo() {
return (
<div className="inline-flex items-center justify-center rounded-lg border p-10 relative"
style={{ borderColor: "var(--bb-border)", background: "#f8f9fa" }}
>
{/* Охранная зона — пунктирная рамка */}
<div
className="absolute inset-6 border-2 border-dashed rounded"
style={{ borderColor: "var(--brand-053m)", opacity: 0.4 }}
/>
{/* Стрелки-обозначения */}
<div className="absolute top-1.5 left-1/2 -translate-x-1/2 text-[10px]"
style={{ color: "var(--brand-053m)" }}>
x
</div>
<div className="absolute bottom-1.5 left-1/2 -translate-x-1/2 text-[10px]"
style={{ color: "var(--brand-053m)" }}>
x
</div>
<div className="absolute left-1.5 top-1/2 -translate-y-1/2 text-[10px]"
style={{ color: "var(--brand-053m)" }}>
x
</div>
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[10px]"
style={{ color: "var(--brand-053m)" }}>
x
</div>
<LogoPlaceholder variant="main" size="sm" />
</div>
);
}
/* ─── Компонент: недопустимое использование ─────────────────── */
function ProhibitedItem({ label }: { label: string }) {
return (
<div
className="flex items-start gap-3 p-4 rounded-lg border"
style={{ borderColor: "#fecaca", background: "#fff5f5" }}
>
<span className="text-red-400 text-lg leading-none mt-0.5"></span>
<p className="text-sm" style={{ color: "#7f1d1d" }}>
{label}
</p>
</div>
);
}
/* ─── Главная страница «Логотип» ────────────────────────────── */
export default function LogoPage() {
return (
<div className="max-w-4xl mx-auto px-8 py-10">
{/* Заголовок страницы */}
<div className="mb-10 pb-6 border-b" style={{ borderColor: "var(--bb-border)" }}>
<p
className="text-xs font-semibold uppercase tracking-widest mb-2"
style={{ color: "var(--brand-053m)" }}
>
Фундамент 1.2
</p>
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
Логотип
</h1>
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
Логотип может быть использован во всех визуальных коммуникациях бренда.
Он не подлежит никаким изменениям и не допускается его сочетание ни с каким
дополнительным текстом.
</p>
<div
className="mt-4 px-4 py-3 rounded-lg border text-sm flex items-center gap-2"
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
>
<span></span>
<span>
Векторный файл логотипа будет добавлен после передачи SVG-файлов.
Ниже SVG-приближение для справки.
</span>
</div>
</div>
{/* 1. Иерархия и версии */}
<Section
id="hierarchy"
title="Иерархия и версии"
subtitle="Клиника использует два варианта логотипа в зависимости от контекста применения."
>
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
<div>
<LogoPlaceholder variant="main" size="md" />
<div className="mt-4">
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Основной логотип
</p>
<p className="text-sm mb-2" style={{ color: "var(--bb-text-muted)" }}>
Локальные версии по направлениям (ЛОР, аллергология и др.).
Применяется в точках контакта с клиентами, на лендингах и сайтах направлений.
</p>
<RuleTag>Точки контакта с клиентом</RuleTag>
</div>
</div>
<div>
<LogoPlaceholder variant="general" size="md" />
<div className="mt-4">
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Общий логотип
</p>
<p className="text-sm mb-2" style={{ color: "var(--bb-text-muted)" }}>
Версия сети клиник. Применяется для онлайн и оффлайн коммуникаций
с клиентами, во внутренней документации. Допустимо на общем сайте.
</p>
<RuleTag>Сеть клиник · Документация · Сайт</RuleTag>
</div>
</div>
</div>
</Section>
{/* 2. Цветовые варианты */}
<Section
id="variants"
title="Цветовые варианты"
subtitle="Логотип существует в нескольких вариантах в зависимости от фона носителя."
>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<div>
<LogoPlaceholder variant="main" size="sm" />
<p className="mt-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
Основной на светлом фоне
</p>
</div>
<div>
<LogoPlaceholder variant="inverted" size="sm" />
<p className="mt-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
Инвертированный на тёмном фоне
</p>
</div>
<div>
<LogoPlaceholder variant="brown" size="sm" />
<p className="mt-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
Коричневый на форме (бежевый костюм)
</p>
</div>
</div>
</Section>
{/* 3. Охранная зона */}
<Section
id="clearspace"
title="Охранная зона"
subtitle="Вокруг логотипа всегда должно быть свободное пространство, равное высоте буквы «x» в названии."
>
<div className="flex flex-wrap gap-8 items-start">
<ClearspaceDemo />
<div className="flex-1 min-w-48 space-y-3 pt-2">
<p className="text-sm" style={{ color: "var(--bb-text)" }}>
Охранная зона минимальное расстояние от логотипа до любого другого
графического элемента или края носителя.
</p>
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Пунктирная рамка обозначает охранную зону. Никакие другие элементы
не должны пересекать её границы.
</p>
</div>
</div>
</Section>
{/* 4. Минимальные размеры */}
<Section
id="sizes"
title="Минимальные размеры"
subtitle="Размеры логотипа для размещения на форме сотрудников."
>
<div
className="overflow-hidden rounded-lg border"
style={{ borderColor: "var(--bb-border)" }}
>
<table className="w-full text-sm">
<thead>
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
<th
className="text-left px-5 py-3 font-medium"
style={{ color: "var(--bb-text-muted)" }}
>
Размер одежды
</th>
<th
className="text-left px-5 py-3 font-medium"
style={{ color: "var(--bb-text-muted)" }}
>
Длина логотипа
</th>
<th
className="text-left px-5 py-3 font-medium"
style={{ color: "var(--bb-text-muted)" }}
>
Высота логотипа
</th>
<th
className="text-left px-5 py-3 font-medium"
style={{ color: "var(--bb-text-muted)" }}
>
Расположение
</th>
</tr>
</thead>
<tbody>
<tr className="border-t" style={{ borderColor: "var(--bb-border)" }}>
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>
До 46 (включительно)
</td>
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>
70 мм
</td>
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>
25,5 мм
</td>
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>
Левая сторона груди
</td>
</tr>
<tr className="border-t" style={{ borderColor: "var(--bb-border)" }}>
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>
От 48
</td>
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>
90 мм
</td>
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>
32,8 мм
</td>
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>
Левая сторона груди
</td>
</tr>
</tbody>
</table>
</div>
</Section>
{/* 5. Недопустимые варианты */}
<Section
id="prohibited"
title="Недопустимые варианты использования"
subtitle="Следующие варианты применения логотипа запрещены."
>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<ProhibitedItem label="Изменять пропорции или искажать логотип" />
<ProhibitedItem label="Изменять цвета элементов логотипа" />
<ProhibitedItem label="Добавлять рядом произвольный текст" />
<ProhibitedItem label="Размещать на фоне, с которым логотип не контрастирует" />
<ProhibitedItem label="Использовать отдельные элементы логотипа без остальных" />
<ProhibitedItem label="Применять тени, обводки, градиенты" />
</div>
</Section>
{/* 6. Скачать файлы */}
<Section
id="download"
title="Скачать файлы"
subtitle="Официальные файлы логотипа для использования в коммуникациях."
>
<div
className="rounded-lg border p-6 flex flex-col sm:flex-row items-start sm:items-center gap-4"
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
>
<div className="flex-1">
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Векторные файлы логотипа (SVG, PNG)
</p>
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Будут доступны после передачи исходных файлов от дизайнера.
</p>
</div>
<button
disabled
className="px-4 py-2 rounded-lg text-sm font-medium cursor-not-allowed"
style={{
background: "#e5e7eb",
color: "#9ca3af",
}}
>
Скачать (скоро)
</button>
</div>
</Section>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
@import "tailwindcss";
/* ─── Бренд-токены О!Клиника ─────────────────────────────────────────── */
/* Цвета уточняются в Sprint 2 по таблице Oracal */
:root {
/* Фирменные цвета (приблизительно — уточнить в Sprint 2) */
--brand-053m: #7ecfca; /* Основной бирюзовый (Oracal 053M) */
--brand-073m: #5b7b87; /* Тёмный серо-голубой (Oracal 073M) */
--brand-066m: #5bb5ad; /* Средний бирюзовый (Oracal 066M) */
--brand-050m: #1b4c72; /* Тёмно-синий, наружная реклама (Oracal 050M) */
--brand-081m: #c4a882; /* Бежевый (Oracal 081M) */
--brand-080m: #5c2e0e; /* Тёмно-коричневый (Oracal 080M) */
--brand-white: #ffffff;
/* UI-цвета брендбука */
--bb-sidebar-bg: #f8f9fa;
--bb-sidebar-border: #e5e7eb;
--bb-sidebar-text: #374151;
--bb-sidebar-text-muted: #6b7280;
--bb-sidebar-active-bg: #e0f5f4;
--bb-sidebar-active-text: var(--brand-053m);
--bb-sidebar-section: #9ca3af;
--bb-content-bg: #ffffff;
--bb-border: #e5e7eb;
--bb-text: #111827;
--bb-text-muted: #6b7280;
/* Шрифты */
--font-brand: 'DINPro', 'DIN Pro', Arial, sans-serif;
--font-web: 'Fira Sans', sans-serif;
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
}
@theme inline {
--color-background: #ffffff;
--color-foreground: var(--bb-text);
}
body {
font-family: var(--font-web);
background: var(--bb-content-bg);
color: var(--bb-text);
-webkit-font-smoothing: antialiased;
}
+33
View File
@@ -0,0 +1,33 @@
import type { Metadata } from "next";
import { Fira_Sans } from "next/font/google";
import "./globals.css";
import { Sidebar } from "@/components/layout/Sidebar";
const firaSans = Fira_Sans({
subsets: ["latin", "cyrillic"],
weight: ["300", "400", "500", "600"],
variable: "--font-fira-sans",
display: "swap",
});
export const metadata: Metadata = {
title: "Цифровой брендбук | Клиника УХО•ГОРЛО•НОС им. проф. Е.Н. Оленевой",
description: "Интерактивный брендбук — Living Styleguide oclinica.ru",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ru" className={firaSans.variable}>
<body className="flex h-screen overflow-hidden bg-white">
<Sidebar />
<main className="flex-1 overflow-y-auto">
{children}
</main>
</body>
</html>
);
}
+5
View File
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function RootPage() {
redirect("/foundation/logo");
}