7 Commits

Author SHA1 Message Date
AR 15 M4 0570b50d9f feat(badges): реальные фото бейджей из PDF-брендбука
- Извлечены 2 фото (стр.12 PDF): лицевая (белый бейдж с ФИО) и оборотная (магнитное крепление)
- Убран CSS-макет, добавлены реальные фото с описанием
- Добавлен пример из брендбука (Лебединская Е.А.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:28:02 +05:00
AR 15 M4 789f436be1 feat(uniform): реальные фото формы из PDF-брендбука
- Извлечены 2 фото (стр.11 PDF): бежевый и синий варианты
- Убран CSS-макет, добавлены реальные фото с подписями и цветовыми чипами
- Обновлено описание: два варианта формы

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:24:51 +05:00
AR 15 M4 5298f8c1cd feat(logo): добавлен раздел «Символика знака»
Три блока: главный тезис + три значения + почему не звезда

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:20:13 +05:00
AR 15 M4 ff3f6acc03 fix(logo): заголовок раздела размеров — убран подзаголовок, текст перенесён в title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:17:53 +05:00
AR 15 M4 39ecd72fde fix(logo): убран второй логотип, одна карточка с объединённым описанием
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:15:22 +05:00
AR 15 M4 a715503ca5 docs: обновлена проектная документация по деплою на Vercel
- TZ.md: хостинг (Vercel), стек (актуальные версии Next.js 16/NestJS 11/Prisma 7/pnpm 10/Tailwind 4),
  раздел 12.2 с командой деплоя, закрыты ОВ-02 и ОВ-06
- SPRINTS.md: в Sprint 2 — ссылка на production URL; Sprint 12 — уточнены задачи деплоя
- docs/DEPLOY.md: новый файл — инструкция по деплою фронтенда, локальной разработке,
  план автодеплоя через Gitea Actions, варианты хостинга бэкенда

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:11:43 +05:00
AR 15 M4 657cc26d3e fix: TypeScript-ошибка в exportTokens, next.config.ts для Vercel
- colors/page.tsx: Record<string, unknown> вместо Record<string, string>
- next.config.ts: turbopack.root только в development (Vercel conflict fix)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:06:54 +05:00
13 changed files with 498 additions and 277 deletions
+1 -1
View File
@@ -247,7 +247,7 @@ function ContrastRow({ pair }: { pair: typeof CONTRAST_PAIRS[0] }) {
/* ─── Экспорт токенов ──────────────────────────────────────────────── */ /* ─── Экспорт токенов ──────────────────────────────────────────────── */
function exportTokens() { function exportTokens() {
const tokens: Record<string, Record<string, string>> = { colors: {} }; const tokens: Record<string, Record<string, unknown>> = { colors: {} };
BRAND_COLORS.forEach(c => { BRAND_COLORS.forEach(c => {
const key = c.oracal !== "—" ? `brand-${c.oracal.toLowerCase()}` : "brand-white"; const key = c.oracal !== "—" ? `brand-${c.oracal.toLowerCase()}` : "brand-white";
const { r, g, b } = hexToRgb(c.hex); const { r, g, b } = hexToRgb(c.hex);
+108 -21
View File
@@ -132,31 +132,119 @@ export default function LogoPage() {
</div> </div>
</div> </div>
{/* 1. Иерархия */} {/* 1. Логотип */}
<Section <Section
id="hierarchy" id="hierarchy"
title="Иерархия и версии" title="Логотип"
subtitle="Клиника использует два варианта логотипа в зависимости от контекста применения." subtitle="Единый логотип клиники. Применяется на всех носителях."
> >
<div className="grid grid-cols-1 gap-8 md:grid-cols-2"> <LogoCard
<LogoCard src="/logo/logo-transparent.png"
src="/logo/logo-transparent.png" alt="Логотип Клиника ухо, горло, нос им. проф. Е.Н. Оленевой"
alt="Основной логотип Клиника УХО ГОРЛО НОС им. проф. Е.Н. Оленевой" label="Логотип клиники"
label="Основной логотип" description="Применяется в точках контакта с клиентами, на лендингах и сайтах направлений. Применяется для онлайн и оффлайн коммуникаций с клиентами, во внутренней документации."
description="Локальные версии по направлениям (ЛОР, аллергология и др.). Применяется в точках контакта с клиентами, на лендингах и сайтах направлений." tag="Онлайн · Оффлайн · Документация"
tag="Точки контакта с клиентом" />
/> </Section>
<LogoCard
src="/logo/logo-transparent.png" {/* 2. Символика знака */}
alt="Общий логотип сети клиник" <Section
label="Общий логотип" id="symbol"
description="Версия сети клиник. Применяется для онлайн и оффлайн коммуникаций с клиентами, во внутренней документации. Допустимо на общем сайте." title="Символика знака"
tag="Сеть клиник · Документация · Сайт" subtitle="О том, что стоит за формой логотипа."
/> >
{/* Главный тезис */}
<div
className="rounded-xl border p-6 mb-6"
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
>
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text)" }}>
В знаке клиники три округлых элемента, расположенных с равной дистанцией от центра.
Это создаёт ощущение симметрии, порядка и движения, но без завершённой формы (звезды или круга).
</p>
<div className="mt-4 flex flex-col gap-2">
{[
"Сохраняет баланс и лёгкость",
"Намекает на естественность и органику",
"Не замыкает символ — оставляет «дыхание», открытость",
].map(item => (
<div key={item} className="flex items-start gap-2.5">
<div
className="w-1.5 h-1.5 rounded-full mt-1.5 shrink-0"
style={{ background: "var(--brand-053m)" }}
/>
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>{item}</p>
</div>
))}
</div>
</div>
{/* Три значения */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3 mb-6">
{[
{
num: "1",
title: "Незамкнутая симметрия",
text: "Это процесс, а не результат. Знак говорит: «мы развиваемся, мы живые, мы не идеальны, но гармоничны».",
},
{
num: "2",
title: "Три элемента",
text: "Классическая структура уха–горла–носа. Триада равновесия, ритм дыхания, символ доверия и открытости.",
},
{
num: "3",
title: "Отсутствие замкнутости",
text: "Нет барьера — есть приглашение. Открытая форма передаёт заботу, доступность и человечность.",
},
].map(item => (
<div
key={item.num}
className="rounded-xl border p-5"
style={{ borderColor: "var(--bb-border)" }}
>
<div
className="w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold mb-3"
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
>
{item.num}
</div>
<p className="font-medium text-sm mb-2" style={{ color: "var(--bb-text)" }}>
{item.title}
</p>
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
{item.text}
</p>
</div>
))}
</div>
{/* Почему не звезда */}
<div
className="rounded-xl border p-5 flex gap-4"
style={{ borderColor: "#e0f5f4", background: "#f8fffe" }}
>
<div
className="w-8 h-8 rounded-full flex items-center justify-center shrink-0 mt-0.5"
style={{ background: "#e0f5f4" }}
>
<span className="text-base leading-none"></span>
</div>
<div>
<p className="font-medium text-sm mb-1.5" style={{ color: "var(--bb-text)" }}>
Почему нет законченной звезды
</p>
<p className="text-sm leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
Звезда символ завершённости и сакрального порядка. Знак клиники символ жизни
и взаимодействия. Он ближе по духу к живой биоморфной форме (капли, клетки, лепестки),
чем к идеальной математической фигуре. Такой дизайн передаёт не «власть формы»,
а заботу, мягкость и человечность.
</p>
</div>
</div> </div>
</Section> </Section>
{/* 2. Цветовые варианты */} {/* 3. Цветовые варианты */}
<Section <Section
id="variants" id="variants"
title="Цветовые варианты" title="Цветовые варианты"
@@ -237,8 +325,7 @@ export default function LogoPage() {
{/* 4. Минимальные размеры */} {/* 4. Минимальные размеры */}
<Section <Section
id="sizes" id="sizes"
title="Минимальные размеры" title="Размеры логотипа для размещения на форме сотрудников"
subtitle="Размеры логотипа для размещения на форме сотрудников."
> >
<div className="overflow-hidden rounded-xl border" style={{ borderColor: "var(--bb-border)" }}> <div className="overflow-hidden rounded-xl border" style={{ borderColor: "var(--bb-border)" }}>
<table className="w-full text-sm"> <table className="w-full text-sm">
+87 -155
View File
@@ -31,79 +31,6 @@ function Section({
); );
} }
/* Компонент бейджа (масштабированный макет) */
function BadgeMockup({
variant,
name,
role,
}: {
variant: "light" | "dark";
name: string;
role: string;
}) {
const isDark = variant === "dark";
/* 70×30 мм → пропорция 7:3. Отображаем в 280×120px */
return (
<div className="flex flex-col items-center gap-3">
<div
className="rounded-lg flex items-center px-5 gap-4"
style={{
width: 280,
height: 120,
background: isDark ? "var(--brand-073m)" : "var(--brand-081m)",
border: isDark ? "none" : "1px solid #d1d5db",
flexShrink: 0,
}}
>
{/* Логотип */}
<Image
src="/logo/logo-transparent.png"
alt="Логотип"
width={72}
height={26}
className="object-contain shrink-0"
style={{
filter: isDark
? "brightness(0) invert(1)"
: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)",
}}
/>
{/* Разделитель */}
<div
className="self-stretch w-px mx-1"
style={{ background: isDark ? "rgba(255,255,255,0.25)" : "rgba(92,46,14,0.2)" }}
/>
{/* Текст */}
<div>
<p
className="font-bold leading-tight"
style={{
fontFamily: "'DINPro', Arial, sans-serif",
fontSize: 13,
color: isDark ? "#ffffff" : "#5c2e0e",
}}
>
{name}
</p>
<p
className="leading-tight mt-0.5"
style={{
fontFamily: "'DINPro', Arial, sans-serif",
fontSize: 10,
color: isDark ? "rgba(255,255,255,0.7)" : "rgba(92,46,14,0.7)",
}}
>
{role}
</p>
</div>
</div>
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
{isDark ? "Тёмный вариант (серо-голубой)" : "Светлый вариант (бежевый)"}
</p>
</div>
);
}
export default function BadgesPage() { export default function BadgesPage() {
return ( return (
<div className="max-w-4xl mx-auto px-8 py-10"> <div className="max-w-4xl mx-auto px-8 py-10">
@@ -120,24 +47,76 @@ export default function BadgesPage() {
Бейджи сотрудников Бейджи сотрудников
</h1> </h1>
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> <p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
Именные бейджи для идентификации сотрудников клиники. Размер 70×30 мм. Именные бейджи для идентификации сотрудников клиники. Размер 70×30 мм,
Два цветовых варианта в зависимости от должности. магнитное крепление. Белый фон, чёрный текст.
</p> </p>
</div> </div>
{/* Размеры и технические требования */} {/* Фотографии */}
<Section
title="Образцы бейджей"
subtitle="Фотографии реальных бейджей из брендбука клиники."
>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{/* Лицевая сторона */}
<div>
<div
className="rounded-xl overflow-hidden border mb-4"
style={{ borderColor: "var(--bb-border)" }}
>
<Image
src="/offline/badges/badge-2.jpeg"
alt="Лицевая сторона бейджа: Лебединская Елена Александровна, врач оториноларинголог"
width={690}
height={347}
className="w-full object-cover"
/>
</div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Лицевая сторона
</p>
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Белый фон, скруглённые углы. ФИО крупный шрифт,
должности мелкий. Металлическая рамка.
</p>
</div>
{/* Оборотная сторона */}
<div>
<div
className="rounded-xl overflow-hidden border mb-4"
style={{ borderColor: "var(--bb-border)" }}
>
<Image
src="/offline/badges/badge-1.jpeg"
alt="Оборотная сторона бейджа с магнитным креплением"
width={657}
height={369}
className="w-full object-cover"
/>
</div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Оборотная сторона
</p>
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
Чёрный пластик, магнитное крепление (CAUTION MAGNETIC).
Не требует проколов в одежде.
</p>
</div>
</div>
</Section>
{/* Технические параметры */}
<Section <Section
title="Технические параметры" title="Технические параметры"
subtitle="Единый стандарт для всех сотрудников клиники." subtitle="Единый стандарт для всех сотрудников клиники."
> >
<div <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6">
className="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6"
>
{[ {[
{ label: "Ширина", value: "70 мм" }, { label: "Ширина", value: "70 мм" },
{ label: "Высота", value: "30 мм" }, { label: "Высота", value: "30 мм" },
{ label: "Материал", value: "ПВХ / металл" }, { label: "Материал", value: "Металл / ПВХ" },
{ label: "Крепление", value: "Булавка / клипса" }, { label: "Крепление", value: "Магнитное" },
].map(({ label, value }) => ( ].map(({ label, value }) => (
<div <div
key={label} key={label}
@@ -155,29 +134,10 @@ export default function BadgesPage() {
</div> </div>
</Section> </Section>
{/* Варианты */}
<Section
title="Варианты бейджей"
subtitle="Макет бейджа (масштаб: 4× от реального размера 70×30 мм)."
>
<div className="flex flex-wrap gap-8 items-start">
<BadgeMockup
variant="light"
name="Иванова А.В."
role="Врач-оториноларинголог"
/>
<BadgeMockup
variant="dark"
name="Петров К.С."
role="Главный врач"
/>
</div>
</Section>
{/* Состав текста */} {/* Состав текста */}
<Section <Section
title="Состав текста на бейдже" title="Состав текста на бейдже"
subtitle="Строгий порядок элементов. Шрифт — DINPro." subtitle="Строгий порядок элементов."
> >
<div <div
className="overflow-hidden rounded-xl border" className="overflow-hidden rounded-xl border"
@@ -186,7 +146,7 @@ export default function BadgesPage() {
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr style={{ background: "var(--bb-sidebar-bg)" }}> <tr style={{ background: "var(--bb-sidebar-bg)" }}>
{["Элемент", "Содержание", "Шрифт / Размер", "Позиция"].map(h => ( {["Элемент", "Содержание", "Оформление"].map(h => (
<th <th
key={h} key={h}
className="text-left px-5 py-3 font-medium" className="text-left px-5 py-3 font-medium"
@@ -199,11 +159,10 @@ export default function BadgesPage() {
</thead> </thead>
<tbody> <tbody>
{[ {[
["Логотип", "Логотип клиники", "PNG / SVG", "Левая часть, по центру по высоте"], ["ФИО", "Фамилия Имя Отчество", "Крупный шрифт, первая строка"],
["Разделитель", "Вертикальная линия", "1px, 40% прозрачность", "Между логотипом и текстом"], ["Должность", "Основная должность", "Мелкий шрифт, вторая строка"],
["ФИО", "Фамилия И.О.", "DINPro Bold 13px", "Правая часть, верхняя строка"], ["Специализация", "Специализация / учёная степень (если есть)", "Мелкий шрифт, третья строка"],
["Должность", "Полное название должности", "DINPro Regular 10px", "Правая часть, нижняя строка"], ].map(([el, content, style]) => (
].map(([el, content, font, pos]) => (
<tr <tr
key={el} key={el}
className="border-t" className="border-t"
@@ -211,60 +170,33 @@ export default function BadgesPage() {
> >
<td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}>{el}</td> <td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}>{el}</td>
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td> <td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td>
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>{font}</td> <td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{style}</td>
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{pos}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
</Section>
{/* Цветовые варианты */} {/* Пример из фото */}
<Section <div
title="Применение вариантов" className="mt-4 rounded-xl border p-4 flex items-start gap-3"
subtitle="Выбор варианта зависит от должности сотрудника." style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
> >
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div
{[ className="w-1.5 self-stretch rounded-full shrink-0"
{ style={{ background: "var(--brand-053m)" }}
variant: "Светлый (бежевый)", />
color: "#c4a882", <div>
usage: "Медицинский персонал, санитарки, технический персонал", <p className="text-xs font-medium mb-1" style={{ color: "var(--bb-text-muted)" }}>
oracal: "081M", Пример из брендбука
}, </p>
{ <p className="text-sm font-semibold" style={{ color: "var(--bb-text)" }}>
variant: "Тёмный (серо-голубой)", Лебединская Елена Александровна
color: "#5b7b87", </p>
usage: "Административный персонал, менеджеры, главный врач", <p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
oracal: "073M", врач оториноларинголог · ведущий хирург · кандидат медицинских наук
}, </p>
].map(item => ( </div>
<div
key={item.variant}
className="flex gap-4 p-4 rounded-xl border"
style={{ borderColor: "var(--bb-border)" }}
>
<div
className="w-8 h-8 rounded-lg shrink-0 mt-0.5"
style={{ background: item.color }}
/>
<div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
{item.variant}
</p>
<p className="text-xs mb-1.5" style={{ color: "var(--bb-text-muted)" }}>
{item.usage}
</p>
<span
className="text-[10px] font-mono px-1.5 py-0.5 rounded"
style={{ background: "#f3f4f6", color: "#374151" }}
>
Oracal {item.oracal}
</span>
</div>
</div>
))}
</div> </div>
</Section> </Section>
+93 -72
View File
@@ -47,92 +47,113 @@ export default function UniformPage() {
Форма сотрудников Форма сотрудников
</h1> </h1>
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}> <p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
Фирменная медицинская одежда сотрудников клиники. Бежевый костюм Фирменная медицинская одежда сотрудников клиники. Два цветовых варианта:
с логотипом клиники на левой стороне груди. бежевый с коричневым логотипом и синий с белым логотипом.
Логотип размещается на левой стороне груди.
</p> </p>
</div> </div>
{/* Описание костюма */} {/* Фотографии вариантов */}
<Section <Section
title="Описание комплекта" title="Варианты формы"
subtitle="Стандартная форма для всех сотрудников клиники." subtitle="Фотографии реальной формы сотрудников с логотипом клиники."
> >
<div <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
className="rounded-xl border p-6" {/* Бежевый вариант */}
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }} <div>
> <div
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> className="rounded-xl overflow-hidden border mb-4"
<div> style={{ borderColor: "var(--bb-border)" }}
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}> >
Состав формы <Image
</p> src="/offline/uniform/uniform-1.jpeg"
<ul className="space-y-2"> alt="Бежевая форма сотрудника клиники с коричневым логотипом"
{["Медицинский костюм (куртка + брюки)", "Цвет: бежевый (Oracal 081M)", "Материал: медицинская ткань", "Логотип вышит или нанесён термопечатью"].map(item => ( width={742}
<li key={item} className="flex items-start gap-2 text-sm" style={{ color: "var(--bb-text-muted)" }}> height={990}
<span style={{ color: "var(--brand-053m)" }}></span> {item} className="w-full object-cover"
</li> style={{ maxHeight: 480, objectPosition: "top" }}
))} />
</ul>
</div> </div>
<div> <p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}> Бежевый вариант
Цветовая схема </p>
</p> <p className="text-sm mb-3" style={{ color: "var(--bb-text-muted)" }}>
<div className="flex gap-3"> Основная форма для медицинского персонала. Логотип тёмно-коричневый.
<div className="text-center"> </p>
<div className="w-12 h-12 rounded-lg border mb-1" <div className="flex gap-2">
style={{ background: "#c4a882", borderColor: "var(--bb-border)" }} /> <div className="flex items-center gap-1.5">
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>081M</p> <div
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>#c4a882</p> className="w-4 h-4 rounded"
</div> style={{ background: "#c4a882", border: "1px solid #e5e7eb" }}
<div className="text-center"> />
<div className="w-12 h-12 rounded-lg border mb-1" <span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
style={{ background: "#5c2e0e", borderColor: "var(--bb-border)" }} /> Oracal 081M
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>080M</p> </span>
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>#5c2e0e</p> </div>
</div> <span style={{ color: "var(--bb-text-muted)" }}>·</span>
<div className="flex items-center gap-1.5">
<div
className="w-4 h-4 rounded"
style={{ background: "#5c2e0e", border: "1px solid #e5e7eb" }}
/>
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
Логотип 080M
</span>
</div>
</div>
</div>
{/* Синий вариант */}
<div>
<div
className="rounded-xl overflow-hidden border mb-4"
style={{ borderColor: "var(--bb-border)" }}
>
<Image
src="/offline/uniform/uniform-2.jpeg"
alt="Синяя форма сотрудника клиники с белым логотипом"
width={580}
height={773}
className="w-full object-cover"
style={{ maxHeight: 480, objectPosition: "top" }}
/>
</div>
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
Синий вариант
</p>
<p className="text-sm mb-3" style={{ color: "var(--bb-text-muted)" }}>
Альтернативный вариант. Логотип белый инвертированный.
</p>
<div className="flex gap-2">
<div className="flex items-center gap-1.5">
<div
className="w-4 h-4 rounded"
style={{ background: "#4a90c4", border: "1px solid #e5e7eb" }}
/>
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
Синий медицинский
</span>
</div>
<span style={{ color: "var(--bb-text-muted)" }}>·</span>
<div className="flex items-center gap-1.5">
<div
className="w-4 h-4 rounded"
style={{ background: "#ffffff", border: "1px solid #e5e7eb" }}
/>
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
Логотип белый
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</Section> </Section>
{/* Логотип на форме */} {/* Размеры логотипа */}
<Section <Section
title="Размещение логотипа" title="Размеры логотипа для размещения на форме сотрудников"
subtitle="Логотип располагается на левой стороне груди. Размер зависит от размера одежды." subtitle="Логотип располагается на левой стороне груди. Размер зависит от размера одежды."
> >
{/* Визуализация размещения */}
<div
className="rounded-xl border p-8 mb-6 flex flex-col items-center justify-center"
style={{ background: "#c4a882", borderColor: "transparent", minHeight: 200 }}
>
<div className="relative w-64 h-48 flex items-center justify-center">
{/* Силуэт куртки (упрощённая схема) */}
<div className="absolute inset-0 rounded-xl opacity-20"
style={{ border: "2px dashed #5c2e0e" }} />
{/* Зона логотипа — левая грудь */}
<div className="absolute top-6 left-10 flex flex-col items-center gap-2">
<Image
src="/logo/logo-transparent.png"
alt="Логотип на форме"
width={100}
height={36}
className="object-contain"
style={{ filter: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)" }}
/>
<div className="border border-dashed rounded px-1"
style={{ borderColor: "#5c2e0e50" }}>
<p className="text-xs" style={{ color: "#5c2e0e80" }}> Левая грудь</p>
</div>
</div>
</div>
<p className="mt-4 text-sm" style={{ color: "rgba(92,46,14,0.7)" }}>
Схема размещения логотипа (превью)
</p>
</div>
{/* Таблица размеров */}
<div <div
className="overflow-hidden rounded-xl border" className="overflow-hidden rounded-xl border"
style={{ borderColor: "var(--bb-border)" }} style={{ borderColor: "var(--bb-border)" }}
@@ -178,7 +199,7 @@ export default function UniformPage() {
{[ {[
{ ok: true, text: "Носить комплект в полном составе" }, { ok: true, text: "Носить комплект в полном составе" },
{ ok: true, text: "Поддерживать чистоту и опрятность формы" }, { ok: true, text: "Поддерживать чистоту и опрятность формы" },
{ ok: true, text: "Логотип только тёмно-коричневый на бежевом" }, { ok: true, text: "Логотип только в утверждённых цветовых вариантах" },
{ ok: false, text: "Носить форму без логотипа" }, { ok: false, text: "Носить форму без логотипа" },
{ ok: false, text: "Изменять цвет или материал формы" }, { ok: false, text: "Изменять цвет или материал формы" },
{ ok: false, text: "Добавлять сторонние нашивки и знаки" }, { ok: false, text: "Добавлять сторонние нашивки и знаки" },
+7 -3
View File
@@ -1,10 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import path from "path"; import path from "path";
const isDev = process.env.NODE_ENV === "development";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
turbopack: { ...(isDev && {
root: path.resolve(__dirname, "../.."), turbopack: {
}, root: path.resolve(__dirname, "../.."),
},
}),
}; };
export default nextConfig; export default nextConfig;
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+156
View File
@@ -0,0 +1,156 @@
# Деплой — oclinica-brandbook
## Текущее состояние
| Сервис | Статус | URL | Платформа |
|------------|-------------|----------------------------------------|---------------|
| Фронтенд | ✅ Активен | https://web-oclinica.vercel.app | Vercel Hobby |
| Бэкенд | локально | http://localhost:3001 | Docker Compose |
| База данных | локально | localhost:5433 | PostgreSQL 16 |
---
## Фронтенд — Vercel
### Первоначальная настройка (уже выполнена)
```bash
# 1. Установить Vercel CLI
npm install -g vercel
# 2. Войти в аккаунт (однократно, открывает браузер)
vercel login
# 3. Первый деплой из директории apps/web
cd apps/web
vercel --yes
```
### Деплой обновлений
```bash
cd apps/web
vercel --prod --yes
```
Деплой занимает ~30 секунд. После завершения изменения сразу доступны по адресу:
**https://web-oclinica.vercel.app**
### Как это работает
- Vercel автоматически определяет Next.js и использует pnpm для сборки
- Каждый `vercel --prod` создаёт новый immutable deployment и привязывает его к production URL
- Предыдущие деплои остаются доступны по уникальным preview URL
- Логи билда: https://vercel.com/oclinica/web
### Ограничения Vercel Hobby (бесплатный план)
| Параметр | Лимит |
|-----------------------|-------------------------|
| Bandwidth | 100 GB / месяц |
| Builds | 6000 минут / месяц |
| Serverless Functions | 100 GB-hours / месяц |
| Тип использования | Только некоммерческие |
Для брендбука (внутренний инструмент) лимитов более чем достаточно.
---
## Локальная разработка
### Запуск фронтенда
```bash
# Из корня monorepo
pnpm dev
# Или только фронтенд
cd apps/web && pnpm dev
```
Доступен на: http://localhost:3001
### Запуск бэкенда + БД
```bash
# Запустить PostgreSQL
docker compose up -d
# Запустить NestJS
cd apps/api && pnpm dev
```
### .env файлы
```bash
# Скопировать и заполнить
cp .env.example .env
```
Содержимое `.env.example`:
```
DATABASE_URL="postgresql://brandbook:brandbook@localhost:5433/brandbook"
API_PORT=3001
NEXT_PUBLIC_API_URL=http://localhost:3001
```
---
## Автодеплой через Gitea Actions (планируется в Sprint 12)
Для автоматического деплоя при пуше в ветку `main` создать файл
`.gitea/workflows/deploy-frontend.yml`:
```yaml
name: Deploy Frontend to Vercel
on:
push:
branches: [main]
paths:
- 'apps/web/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: apps/web/pnpm-lock.yaml
- name: Install Vercel CLI
run: npm install -g vercel
- name: Deploy to Vercel
run: cd apps/web && vercel --prod --yes --token ${{ secrets.VERCEL_TOKEN }}
```
**Настройка:**
1. Получить Vercel Token: https://vercel.com/account/tokens
2. Добавить в Gitea: Settings → Secrets → `VERCEL_TOKEN`
---
## Хостинг бэкенда (планируется к Sprint 11)
Бэкенд (NestJS + PostgreSQL) потребуется для экспериментальной секции (Sprint 11).
Варианты для рассмотрения:
| Платформа | PostgreSQL | Бесплатно | Карта |
|-----------|-----------|-----------|-------|
| Railway | ✅ | $5 кредит / месяц | нужна |
| Supabase | ✅ | ✅ (PostgreSQL managed) | нет |
| Fly.io | ✅ | ✅ ограниченно | нет |
| VPS клиники | ✅ | ✅ (если есть) | нет |
Рекомендация: **Supabase** для БД (бесплатно, managed PostgreSQL) + **Railway** или VPS для NestJS.
+12 -6
View File
@@ -88,6 +88,7 @@
- Страница `/offline/print` — макеты визитки (лицо/оборот) и листовки А5, Telegram-бот - Страница `/offline/print` — макеты визитки (лицо/оборот) и листовки А5, Telegram-бот
- Sidebar: убраны «скоро» для Цветов, Типографики и всех 5 страниц Оффлайн - Sidebar: убраны «скоро» для Цветов, Типографики и всех 5 страниц Оффлайн
- Версия обновлена до **Sprint 2 · v0.2.0** - Версия обновлена до **Sprint 2 · v0.2.0**
- **Деплой на Vercel:** https://web-oclinica.vercel.app (production, бесплатно)
### Технические решения Sprint 2 ### Технические решения Sprint 2
- Страница «Цвета» — `"use client"` для clipboard API и экспорта JSON - Страница «Цвета» — `"use client"` для clipboard API и экспорта JSON
@@ -252,20 +253,25 @@
--- ---
## Sprint 12 — Деплой, полировка, документация ## Sprint 12 — Полировка, финальный деплой и документация
**Цель:** Финальный релиз и публикация. **Цель:** Финальный релиз. Фронтенд уже живёт на Vercel с Sprint 2, Sprint 12 — финальная полировка и production-готовность бэкенда.
### Задачи ### Задачи
- [ ] BE + FE: Полный smoke-тест всего брендбука - [ ] BE + FE: Полный smoke-тест всего брендбука
- [ ] FE: Мобильная адаптация — финальная проверка всех страниц - [ ] FE: Мобильная адаптация — финальная проверка всех страниц
- [ ] FE: Accessibility-аудит (WCAG AA) - [ ] FE: Accessibility-аудит (WCAG AA)
- [ ] Деплой: настройка CI/CD, публикация на сервере - [ ] Деплой BE: выбрать и настроить хостинг для NestJS + PostgreSQL
- [ ] Docs: Создание `docs/deployment.md` - [ ] Деплой: настроить автоматический деплой через Gitea Actions → Vercel (при пуше в `main`)
- [ ] Docs: Обновление README.md финальными инструкциями - [ ] Docs: Обновление `docs/DEPLOY.md` финальными инструкциями
- [ ] Design: Финальный ревью брендбука - [ ] Design: Финальный ревью брендбука
**Результат спринта:** Брендбук задеплоен и доступен по URL. ### Текущий статус деплоя
- **Фронтенд:** https://web-oclinica.vercel.app (Vercel Hobby, задеплоен в Sprint 2)
- **Команда деплоя:** `cd apps/web && vercel --prod --yes`
- **Бэкенд:** локально (Docker Compose), хостинг выбирается в Sprint 12
**Результат спринта:** Брендбук полностью готов, оба сервиса задеплоены, автодеплой настроен.
--- ---
+34 -19
View File
@@ -37,9 +37,10 @@
| Краткое название | oclinica-brandbook | | Краткое название | oclinica-brandbook |
| Сайт клиники | https://oclinica.ru | | Сайт клиники | https://oclinica.ru |
| Тип системы | Веб-приложение (Living Styleguide) | | Тип системы | Веб-приложение (Living Styleguide) |
| Режим работы | Локальная разработка + деплой на сервер | | Режим работы | Локальная разработка + Vercel (preview + production) |
| Аудитория | Внутренние дизайнеры клиники, внешние подрядчики | | Аудитория | Внутренние дизайнеры клиники, внешние подрядчики |
| Хостинг | TBD — будет прописан отдельно | | Хостинг (фронтенд) | Vercel Hobby (бесплатно) — https://web-oclinica.vercel.app |
| Хостинг (бэкенд + БД) | TBD — уточняется при переходе к Sprint 11 (экспериментальная секция) |
--- ---
@@ -393,17 +394,18 @@
| Слой | Технология | Версия | Обоснование | | Слой | Технология | Версия | Обоснование |
|-------------------|-----------------------|----------|--------------------------------------------------| |-------------------|-----------------------|----------|--------------------------------------------------|
| Фронтенд | Next.js (App Router) | 15.x | SSR/SSG, оптимизация, экосистема React | | Фронтенд | Next.js (App Router) | 16.x | SSR/SSG, оптимизация, экосистема React |
| Бэкенд | NestJS | 10.x | Типизированный Node.js фреймворк, DI, модули | | Бэкенд | NestJS | 11.x | Типизированный Node.js фреймворк, DI, модули |
| База данных | PostgreSQL | 16.x | Надёжная реляционная БД, JSON-поля для атрибутов | | База данных | PostgreSQL | 16.x | Надёжная реляционная БД, JSON-поля для атрибутов |
| ORM | Prisma | 5.x | Type-safe запросы, миграции, seed | | ORM | Prisma | 7.x | Type-safe запросы, миграции, seed |
| Стилизация | CSS Modules | | Изоляция стилей, нет рантайм-зависимостей | | Стилизация | Tailwind CSS | 4.x | Utility-first, CSS-переменные, нет рантайм-overhead |
| Дизайн-токены | CSS Custom Properties | — | Нативно поддерживается всеми браузерами | | Дизайн-токены | CSS Custom Properties | — | Нативно поддерживается всеми браузерами |
| Шрифт (бренд) | DINPro | — | Фирменный шрифт бренда, оффлайн-носители | | Шрифт (бренд) | DINPro | — | Фирменный шрифт бренда, оффлайн-носители |
| Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300 и 400, сайт | | Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300/400/500/600 |
| Авторизация | JWT + httpOnly cookie | — | Безопасное хранение токена | | Авторизация | JWT + httpOnly cookie | — | Безопасное хранение токена |
| Пакетный менеджер | pnpm | 9.x | Monorepo workspaces, скорость | | Пакетный менеджер | pnpm | 10.x | Monorepo workspaces, скорость |
| Контейнеризация | Docker + Compose | — | Единообразное окружение dev/prod | | Контейнеризация | Docker + Compose | — | Единообразное окружение локальной разработки |
| Хостинг фронтенда | Vercel | — | Нативная поддержка Next.js, бесплатный Hobby-план |
--- ---
@@ -480,16 +482,29 @@ oclinica-brandbook/
| Docker | >= 24 | | Docker | >= 24 |
| Docker Compose | >= 2 | | Docker Compose | >= 2 |
### 12.2 Production-сервер ### 12.2 Деплой фронтенда (Vercel)
> TBD — параметры хостинга будут прописаны отдельно. Фронтенд (`apps/web`) деплоится на Vercel Hobby (бесплатно).
Минимальные ожидаемые требования: **Production URL:** https://web-oclinica.vercel.app
- ОС: Ubuntu 22.04+
- RAM: 2 GB **Команда деплоя** (из директории `apps/web`):
- Disk: 20 GB ```bash
- PostgreSQL 16 (или managed database) vercel --prod --yes
- Node.js 20 LTS ```
**Требования:**
- Vercel CLI установлен глобально: `npm install -g vercel`
- Выполнен `vercel login` (однократно)
**Деплой занимает ~30 секунд.** После команды изменения сразу доступны по production URL.
### 12.3 Бэкенд и база данных
> TBD — параметры хостинга бэкенда (NestJS + PostgreSQL) будут определены к Sprint 11,
> когда потребуется работающий API для экспериментальной секции.
Варианты для рассмотрения: Railway, Render, VPS клиники.
--- ---
@@ -511,11 +526,11 @@ oclinica-brandbook/
| № | Вопрос | Ответственный | Срок | | № | Вопрос | Ответственный | Срок |
|----|--------------------------------------------------------------------------------------|---------------|----------| |----|--------------------------------------------------------------------------------------|---------------|----------|
| ОВ-01 | Доступен ли JSON API или REST API на oclinica.ru? Каков формат ответов? | Клиника | Sprint 1 | | ОВ-01 | Доступен ли JSON API или REST API на oclinica.ru? Каков формат ответов? | Клиника | Sprint 1 |
| ОВ-02 | Параметры хостинга для production-деплоя | Клиника | TBD | | ОВ-02 | ~~Параметры хостинга для production-деплоя~~ **Закрыт:** фронтенд — Vercel Hobby (https://web-oclinica.vercel.app); бэкенд — TBD к Sprint 11 | — | Частично закрыт |
| ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 | | ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 |
| ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 | | ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 |
| ОВ-05 | ~~Нужен ли раздел «Логотип» в v1.0 или ждём вектор?~~ **Закрыт:** страница логотипа реализуется в Sprint 1 с PNG-версией; вектор будет добавлен позже | — | Закрыт | | ОВ-05 | ~~Нужен ли раздел «Логотип» в v1.0 или ждём вектор?~~ **Закрыт:** страница логотипа реализуется в Sprint 1 с PNG-версией; вектор будет добавлен позже | — | Закрыт |
| ОВ-06 | HEX-эквиваленты цветов Oracal (053M, 073M, 066M, 050M, 081M, 080M) для использования в токенах | Совместно | Sprint 2 | | ОВ-06 | ~~HEX-эквиваленты цветов Oracal~~ **Закрыт:** приблизительные HEX зафиксированы в Sprint 2 и подтверждены как рабочие (053M=#7ecfca, 073M=#5b7b87, 066M=#5bb5ad, 050M=#1b4c72, 081M=#c4a882, 080M=#5c2e0e). Точная калибровка — при получении физических образцов. | — | Закрыт |
--- ---