Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0570b50d9f | |||
| 789f436be1 | |||
| 5298f8c1cd | |||
| ff3f6acc03 | |||
| 39ecd72fde | |||
| a715503ca5 | |||
| 657cc26d3e | |||
| f03599abcf | |||
| 02f561fcd3 | |||
| 1cdf0e7e95 |
@@ -0,0 +1,415 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
/* ─── Утилиты конвертации ──────────────────────────────────────────── */
|
||||
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex);
|
||||
if (!m) return { r: 0, g: 0, b: 0 };
|
||||
return {
|
||||
r: parseInt(m[1].slice(0, 2), 16),
|
||||
g: parseInt(m[1].slice(2, 4), 16),
|
||||
b: parseInt(m[1].slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
|
||||
r /= 255; g /= 255; b /= 255;
|
||||
const max = Math.max(r, g, b), min = Math.min(r, g, b);
|
||||
let h = 0, s = 0;
|
||||
const l = (max + min) / 2;
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) };
|
||||
}
|
||||
|
||||
function luminance(r: number, g: number, b: number): number {
|
||||
const a = [r, g, b].map(v => {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
function contrastRatio(hex1: string, hex2: string): number {
|
||||
const { r: r1, g: g1, b: b1 } = hexToRgb(hex1);
|
||||
const { r: r2, g: g2, b: b2 } = hexToRgb(hex2);
|
||||
const l1 = luminance(r1, g1, b1);
|
||||
const l2 = luminance(r2, g2, b2);
|
||||
const lighter = Math.max(l1, l2);
|
||||
const darker = Math.min(l1, l2);
|
||||
return Math.round(((lighter + 0.05) / (darker + 0.05)) * 10) / 10;
|
||||
}
|
||||
|
||||
/* ─── Данные цветов ────────────────────────────────────────────────── */
|
||||
const BRAND_COLORS = [
|
||||
{
|
||||
oracal: "053M",
|
||||
name: "Основной бирюзовый",
|
||||
hex: "#7ecfca",
|
||||
usage: "Акцентный цвет, CTA-кнопки, иконки, активные состояния",
|
||||
cssVar: "--brand-053m",
|
||||
},
|
||||
{
|
||||
oracal: "073M",
|
||||
name: "Тёмный серо-голубой",
|
||||
hex: "#5b7b87",
|
||||
usage: "Тёмный фон, хедер, акценты на тёмных поверхностях",
|
||||
cssVar: "--brand-073m",
|
||||
},
|
||||
{
|
||||
oracal: "066M",
|
||||
name: "Средний бирюзовый",
|
||||
hex: "#5bb5ad",
|
||||
usage: "Вторичные акценты, фоны секций, иллюстрации",
|
||||
cssVar: "--brand-066m",
|
||||
},
|
||||
{
|
||||
oracal: "050M",
|
||||
name: "Тёмно-синий",
|
||||
hex: "#1b4c72",
|
||||
usage: "Наружная реклама, полиграфия, заголовки на светлом фоне",
|
||||
cssVar: "--brand-050m",
|
||||
},
|
||||
{
|
||||
oracal: "081M",
|
||||
name: "Бежевый",
|
||||
hex: "#c4a882",
|
||||
usage: "Форма сотрудников, оффлайн носители, тёплые акценты",
|
||||
cssVar: "--brand-081m",
|
||||
},
|
||||
{
|
||||
oracal: "080M",
|
||||
name: "Тёмно-коричневый",
|
||||
hex: "#5c2e0e",
|
||||
usage: "Текст на бежевых поверхностях, логотип на форме",
|
||||
cssVar: "--brand-080m",
|
||||
},
|
||||
{
|
||||
oracal: "—",
|
||||
name: "Белый",
|
||||
hex: "#ffffff",
|
||||
usage: "Фон, инвертированный текст, логотип на тёмных фонах",
|
||||
cssVar: "--brand-white",
|
||||
},
|
||||
];
|
||||
|
||||
const CONTRAST_PAIRS = [
|
||||
{ fg: "#ffffff", bg: "#5b7b87", label: "Белый на тёмном серо-голубом" },
|
||||
{ fg: "#ffffff", bg: "#1b4c72", label: "Белый на тёмно-синем" },
|
||||
{ fg: "#ffffff", bg: "#5c2e0e", label: "Белый на тёмно-коричневом" },
|
||||
{ fg: "#ffffff", bg: "#5bb5ad", label: "Белый на среднем бирюзовом" },
|
||||
{ fg: "#111827", bg: "#7ecfca", label: "Тёмный текст на основном бирюзовом" },
|
||||
{ fg: "#111827", bg: "#c4a882", label: "Тёмный текст на бежевом" },
|
||||
{ fg: "#5c2e0e", bg: "#c4a882", label: "Тёмно-коричневый на бежевом (форма)" },
|
||||
];
|
||||
|
||||
/* ─── Компоненты ───────────────────────────────────────────────────── */
|
||||
function CopyBadge({ value, label }: { value: string; label: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const copy = useCallback(() => {
|
||||
navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
}, [value]);
|
||||
return (
|
||||
<button
|
||||
onClick={copy}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs font-mono transition-colors"
|
||||
style={{
|
||||
background: copied ? "#d1fae5" : "#f3f4f6",
|
||||
color: copied ? "#065f46" : "#374151",
|
||||
border: "1px solid",
|
||||
borderColor: copied ? "#6ee7b7" : "#e5e7eb",
|
||||
}}
|
||||
title={`Скопировать ${label}`}
|
||||
>
|
||||
{copied ? "✓" : "⎘"} {value}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorCard({ color }: { color: typeof BRAND_COLORS[0] }) {
|
||||
const { r, g, b } = hexToRgb(color.hex);
|
||||
const { h, s, l } = rgbToHsl(r, g, b);
|
||||
const isLight = l > 55;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{/* Свотч */}
|
||||
<div
|
||||
className="h-32 flex items-end justify-between px-4 pb-3"
|
||||
style={{ background: color.hex }}
|
||||
>
|
||||
{color.oracal !== "—" && (
|
||||
<span
|
||||
className="text-xs font-semibold px-2 py-0.5 rounded"
|
||||
style={{
|
||||
background: isLight ? "rgba(0,0,0,0.15)" : "rgba(255,255,255,0.2)",
|
||||
color: isLight ? "rgba(0,0,0,0.8)" : "rgba(255,255,255,0.9)",
|
||||
}}
|
||||
>
|
||||
Oracal {color.oracal}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Информация */}
|
||||
<div className="p-4" style={{ background: "var(--bb-content-bg)" }}>
|
||||
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
{color.name}
|
||||
</p>
|
||||
<p className="text-xs mb-3" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{color.usage}
|
||||
</p>
|
||||
|
||||
{/* Коды */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<CopyBadge value={color.hex.toUpperCase()} label="HEX" />
|
||||
<CopyBadge value={`rgb(${r}, ${g}, ${b})`} label="RGB" />
|
||||
<CopyBadge value={`hsl(${h}, ${s}%, ${l}%)`} label="HSL" />
|
||||
<CopyBadge value={color.cssVar} label="CSS var" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContrastRow({ pair }: { pair: typeof CONTRAST_PAIRS[0] }) {
|
||||
const ratio = contrastRatio(pair.fg, pair.bg);
|
||||
const aa = ratio >= 4.5;
|
||||
const aaa = ratio >= 7;
|
||||
const aaLarge = ratio >= 3;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-4 p-4 rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{/* Превью */}
|
||||
<div
|
||||
className="w-32 shrink-0 h-12 rounded-lg flex items-center justify-center text-sm font-medium"
|
||||
style={{ background: pair.bg, color: pair.fg }}
|
||||
>
|
||||
Aa Бб Вв
|
||||
</div>
|
||||
|
||||
{/* Описание */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium truncate" style={{ color: "var(--bb-text)" }}>
|
||||
{pair.label}
|
||||
</p>
|
||||
<div className="flex gap-1 mt-1 items-center">
|
||||
<span className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{pair.fg} / {pair.bg}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ratio и бейджи */}
|
||||
<div className="shrink-0 flex items-center gap-3">
|
||||
<span className="font-mono font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
{ratio}:1
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
{[
|
||||
{ label: "AA", pass: aa },
|
||||
{ label: "AAA", pass: aaa },
|
||||
{ label: "AA large", pass: aaLarge },
|
||||
].map(({ label, pass }) => (
|
||||
<span
|
||||
key={label}
|
||||
className="text-[10px] font-semibold px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
background: pass ? "#d1fae5" : "#fee2e2",
|
||||
color: pass ? "#065f46" : "#991b1b",
|
||||
}}
|
||||
>
|
||||
{pass ? "✓" : "✕"} {label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Экспорт токенов ──────────────────────────────────────────────── */
|
||||
function exportTokens() {
|
||||
const tokens: Record<string, Record<string, unknown>> = { colors: {} };
|
||||
BRAND_COLORS.forEach(c => {
|
||||
const key = c.oracal !== "—" ? `brand-${c.oracal.toLowerCase()}` : "brand-white";
|
||||
const { r, g, b } = hexToRgb(c.hex);
|
||||
const { h, s, l } = rgbToHsl(r, g, b);
|
||||
tokens.colors[key] = {
|
||||
oracal: c.oracal,
|
||||
name: c.name,
|
||||
hex: c.hex.toUpperCase(),
|
||||
rgb: `rgb(${r}, ${g}, ${b})`,
|
||||
hsl: `hsl(${h}, ${s}%, ${l}%)`,
|
||||
cssVar: c.cssVar,
|
||||
};
|
||||
});
|
||||
const blob = new Blob([JSON.stringify(tokens, null, 2)], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = "oclinica-brand-tokens.json"; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/* ─── Страница ─────────────────────────────────────────────────────── */
|
||||
export default function ColorsPage() {
|
||||
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.3
|
||||
</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)" }}>
|
||||
Фирменная цветовая палитра основана на кодах плёнки Oracal. HEX-значения
|
||||
подобраны как ближайшие эквиваленты. Для точной печати используйте коды Oracal.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<div
|
||||
className="px-4 py-3 rounded-lg border text-sm flex items-center gap-2"
|
||||
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
|
||||
>
|
||||
<span>⚠️</span>
|
||||
<span>
|
||||
HEX-значения приблизительны. Для оффлайн-носителей используйте
|
||||
официальные коды Oracal.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={exportTokens}
|
||||
className="ml-4 shrink-0 px-4 py-2 rounded-lg text-sm font-medium transition-colors"
|
||||
style={{
|
||||
background: "var(--brand-053m)",
|
||||
color: "#ffffff",
|
||||
}}
|
||||
>
|
||||
↓ Экспорт JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 1. Палитра */}
|
||||
<section className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Фирменные цвета
|
||||
</h2>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Нажмите на значение, чтобы скопировать его в буфер обмена.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{BRAND_COLORS.map(c => (
|
||||
<ColorCard key={c.oracal} color={c} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. Контрастность */}
|
||||
<section className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Контрастность пар (WCAG)
|
||||
</h2>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Проверка соответствия требованиям доступности для основных комбинаций цветов.
|
||||
AA = 4.5:1 для обычного текста, 3:1 для крупного (>18pt).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{CONTRAST_PAIRS.map(pair => (
|
||||
<ContrastRow key={pair.label} pair={pair} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Применение */}
|
||||
<section className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Правила применения
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
icon: "✓",
|
||||
color: "#d1fae5",
|
||||
textColor: "#065f46",
|
||||
title: "Используйте фирменные цвета",
|
||||
text: "Только цвета из палитры обеспечивают узнаваемость бренда на всех носителях.",
|
||||
},
|
||||
{
|
||||
icon: "✓",
|
||||
color: "#d1fae5",
|
||||
textColor: "#065f46",
|
||||
title: "Соблюдайте контрастность",
|
||||
text: "Текст на цветном фоне должен соответствовать минимум AA по WCAG 2.1.",
|
||||
},
|
||||
{
|
||||
icon: "✕",
|
||||
color: "#fee2e2",
|
||||
textColor: "#991b1b",
|
||||
title: "Не смешивайте произвольно",
|
||||
text: "Не используйте фирменные цвета с посторонними цветами без согласования.",
|
||||
},
|
||||
{
|
||||
icon: "✕",
|
||||
color: "#fee2e2",
|
||||
textColor: "#991b1b",
|
||||
title: "Не изменяйте насыщенность",
|
||||
text: "Осветление, затемнение или изменение оттенка недопустимо без необходимости.",
|
||||
},
|
||||
].map(item => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="flex gap-3 p-4 rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<span
|
||||
className="shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold"
|
||||
style={{ background: item.color, color: item.textColor }}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<div>
|
||||
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
{item.title}
|
||||
</p>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -132,31 +132,119 @@ export default function LogoPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 1. Иерархия */}
|
||||
{/* 1. Логотип */}
|
||||
<Section
|
||||
id="hierarchy"
|
||||
title="Иерархия и версии"
|
||||
subtitle="Клиника использует два варианта логотипа в зависимости от контекста применения."
|
||||
title="Логотип"
|
||||
subtitle="Единый логотип клиники. Применяется на всех носителях."
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
||||
<LogoCard
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Основной логотип Клиника УХО ГОРЛО НОС им. проф. Е.Н. Оленевой"
|
||||
label="Основной логотип"
|
||||
description="Локальные версии по направлениям (ЛОР, аллергология и др.). Применяется в точках контакта с клиентами, на лендингах и сайтах направлений."
|
||||
tag="Точки контакта с клиентом"
|
||||
/>
|
||||
<LogoCard
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Общий логотип сети клиник"
|
||||
label="Общий логотип"
|
||||
description="Версия сети клиник. Применяется для онлайн и оффлайн коммуникаций с клиентами, во внутренней документации. Допустимо на общем сайте."
|
||||
tag="Сеть клиник · Документация · Сайт"
|
||||
/>
|
||||
<LogoCard
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Логотип Клиника ухо, горло, нос им. проф. Е.Н. Оленевой"
|
||||
label="Логотип клиники"
|
||||
description="Применяется в точках контакта с клиентами, на лендингах и сайтах направлений. Применяется для онлайн и оффлайн коммуникаций с клиентами, во внутренней документации."
|
||||
tag="Онлайн · Оффлайн · Документация"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
{/* 2. Символика знака */}
|
||||
<Section
|
||||
id="symbol"
|
||||
title="Символика знака"
|
||||
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>
|
||||
</Section>
|
||||
|
||||
{/* 2. Цветовые варианты */}
|
||||
{/* 3. Цветовые варианты */}
|
||||
<Section
|
||||
id="variants"
|
||||
title="Цветовые варианты"
|
||||
@@ -237,8 +325,7 @@ export default function LogoPage() {
|
||||
{/* 4. Минимальные размеры */}
|
||||
<Section
|
||||
id="sizes"
|
||||
title="Минимальные размеры"
|
||||
subtitle="Размеры логотипа для размещения на форме сотрудников."
|
||||
title="Размеры логотипа для размещения на форме сотрудников"
|
||||
>
|
||||
<div className="overflow-hidden rounded-xl border" style={{ borderColor: "var(--bb-border)" }}>
|
||||
<table className="w-full text-sm">
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Типографика | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
/* ─── Шкала типографики ────────────────────────────────────────────── */
|
||||
const DIN_SCALE = [
|
||||
{ token: "h1", size: "40px / 2.5rem", weight: "700", lh: "1.2", sample: "Клиника ухо, горло, нос" },
|
||||
{ token: "h2", size: "32px / 2rem", weight: "700", lh: "1.25", sample: "Наши специалисты" },
|
||||
{ token: "h3", size: "24px / 1.5rem", weight: "700", lh: "1.3", sample: "Запись на приём" },
|
||||
{ token: "h4", size: "20px / 1.25rem",weight: "700", lh: "1.35", sample: "Расписание врачей" },
|
||||
{ token: "h5", size: "16px / 1rem", weight: "700", lh: "1.4", sample: "Услуги и цены" },
|
||||
{ token: "h6", size: "14px / 0.875rem",weight:"700", lh: "1.4", sample: "Контакты клиники" },
|
||||
];
|
||||
|
||||
const FIRA_SCALE = [
|
||||
{ token: "h1", size: "40px / 2.5rem", weight: "600", lh: "1.2", sample: "Клиника ухо, горло, нос" },
|
||||
{ token: "h2", size: "32px / 2rem", weight: "600", lh: "1.25", sample: "Наши специалисты" },
|
||||
{ token: "h3", size: "24px / 1.5rem", weight: "600", lh: "1.3", sample: "Запись на приём" },
|
||||
{ token: "h4", size: "20px / 1.25rem", weight: "500", lh: "1.35", sample: "Расписание врачей" },
|
||||
{ token: "h5", size: "16px / 1rem", weight: "500", lh: "1.4", sample: "Услуги и цены" },
|
||||
{ token: "h6", size: "14px / 0.875rem",weight: "500", lh: "1.4", sample: "Контакты клиники" },
|
||||
{ token: "body", size: "16px / 1rem", weight: "400", lh: "1.6", sample: "ЛОР-клиника предоставляет полный спектр услуг по диагностике и лечению заболеваний уха, горла и носа." },
|
||||
{ token: "body-sm", size: "14px / 0.875rem",weight: "400", lh: "1.6", sample: "Запись по телефону или через форму на сайте. Работаем без выходных." },
|
||||
{ token: "caption", size: "12px / 0.75rem", weight: "400", lh: "1.5", sample: "Лицензия № ЛО-77-01-018234 от 12.01.2022" },
|
||||
{ token: "label", size: "12px / 0.75rem", weight: "500", lh: "1.4", sample: "СПЕЦИАЛИЗАЦИЯ ВРАЧА" },
|
||||
{ token: "overline", size: "10px / 0.625rem",weight: "600", lh: "1.4", sample: "ФУНДАМЕНТ → 1.4" },
|
||||
];
|
||||
|
||||
/* ─── Компоненты ───────────────────────────────────────────────────── */
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="mb-14">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageBadge({ label, color }: { label: string; color: string }) {
|
||||
return (
|
||||
<span
|
||||
className="inline-block px-2 py-0.5 rounded text-xs font-medium"
|
||||
style={{ background: color === "teal" ? "#e0f5f4" : "#fef3c7", color: color === "teal" ? "var(--brand-073m)" : "#92400e" }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeRow({
|
||||
token,
|
||||
size,
|
||||
weight,
|
||||
lh,
|
||||
sample,
|
||||
fontFamily,
|
||||
}: {
|
||||
token: string;
|
||||
size: string;
|
||||
weight: string;
|
||||
lh: string;
|
||||
sample: string;
|
||||
fontFamily: string;
|
||||
}) {
|
||||
const [sizeVal] = size.split(" / ");
|
||||
const pxSize = parseInt(sizeVal);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-t py-4 grid gap-4"
|
||||
style={{
|
||||
borderColor: "var(--bb-border)",
|
||||
gridTemplateColumns: "80px 1fr 140px",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
{/* Токен */}
|
||||
<div className="pt-1">
|
||||
<span
|
||||
className="font-mono text-xs px-2 py-0.5 rounded"
|
||||
style={{ background: "#f3f4f6", color: "#374151" }}
|
||||
>
|
||||
{token}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Образец */}
|
||||
<div
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize: `${Math.min(pxSize, 28)}px`,
|
||||
fontWeight: weight,
|
||||
lineHeight: lh,
|
||||
color: "var(--bb-text)",
|
||||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{sample}
|
||||
</div>
|
||||
|
||||
{/* Метаданные */}
|
||||
<div className="pt-1 space-y-1 text-right">
|
||||
<p className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{size}
|
||||
</p>
|
||||
<p className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
w{weight} · lh{lh}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Страница ─────────────────────────────────────────────────────── */
|
||||
export default function TypographyPage() {
|
||||
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.4
|
||||
</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>
|
||||
|
||||
{/* Карточки шрифтов */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl font-semibold mb-6" style={{ color: "var(--bb-text)" }}>
|
||||
Шрифты бренда
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{/* DINPro */}
|
||||
<div
|
||||
className="rounded-xl border p-6"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<p className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
DINPro
|
||||
</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Бренд-шрифт · не Google Fonts
|
||||
</p>
|
||||
</div>
|
||||
<UsageBadge label="Оффлайн · Бренд" color="yellow" />
|
||||
</div>
|
||||
<div
|
||||
className="text-4xl mb-4 font-bold"
|
||||
style={{ fontFamily: "'DINPro', Arial, sans-serif", color: "var(--bb-text)" }}
|
||||
>
|
||||
Aa Бб Вв
|
||||
</div>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Применяется на: форме сотрудников, бейджах, вывесках, транспорте,
|
||||
полиграфии. Лицензионный шрифт — передаётся дизайнером.
|
||||
</p>
|
||||
<p className="text-xs mt-3 font-mono px-2 py-1 rounded inline-block"
|
||||
style={{ background: "#f3f4f6", color: "#374151" }}>
|
||||
--font-brand
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Fira Sans */}
|
||||
<div
|
||||
className="rounded-xl border p-6"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<p className="font-semibold text-base" style={{ color: "var(--bb-text)" }}>
|
||||
Fira Sans
|
||||
</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Веб-шрифт · Google Fonts
|
||||
</p>
|
||||
</div>
|
||||
<UsageBadge label="Веб · Цифровой" color="teal" />
|
||||
</div>
|
||||
<div
|
||||
className="text-4xl mb-4 font-semibold"
|
||||
style={{ fontFamily: "var(--font-fira-sans)", color: "var(--bb-text)" }}
|
||||
>
|
||||
Aa Бб Вв
|
||||
</div>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Применяется на: сайте oclinica.ru, в цифровом брендбуке, онлайн-материалах.
|
||||
Подключён через next/font/google (кириллица + латиница).
|
||||
</p>
|
||||
<p className="text-xs mt-3 font-mono px-2 py-1 rounded inline-block"
|
||||
style={{ background: "#f3f4f6", color: "#374151" }}>
|
||||
--font-web
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Таблица применения */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-xl font-semibold mb-4" style={{ color: "var(--bb-text)" }}>
|
||||
Правило применения
|
||||
</h2>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Носитель", "Шрифт"].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Сайт, цифровые материалы, брендбук", "Fira Sans"],
|
||||
["Форма сотрудников, бейджи", "DINPro"],
|
||||
["Вывески, таблички, навигация", "DINPro"],
|
||||
["Брендирование транспорта", "DINPro"],
|
||||
["Визитки, листовки, полиграфия", "DINPro"],
|
||||
["Telegram-бот, пуш-уведомления", "Fira Sans (системный)"],
|
||||
].map(([media, font]) => (
|
||||
<tr
|
||||
key={media}
|
||||
className="border-t"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>{media}</td>
|
||||
<td className="px-5 py-3 font-medium" style={{ color: "var(--brand-073m)" }}>
|
||||
{font}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* DINPro Scale */}
|
||||
<Section
|
||||
title="DINPro — шкала заголовков"
|
||||
subtitle="Бренд-шрифт. Используется в оффлайн-носителях. Отображается с системным фоллбэком Arial, если шрифт не установлен."
|
||||
>
|
||||
<div>
|
||||
{DIN_SCALE.map(row => (
|
||||
<TypeRow
|
||||
key={row.token}
|
||||
{...row}
|
||||
fontFamily="'DINPro', 'DIN Pro', Arial, sans-serif"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Fira Sans Scale */}
|
||||
<Section
|
||||
title="Fira Sans — полная шкала"
|
||||
subtitle="Веб-шрифт. Используется на сайте и в цифровых материалах. Загружается через Google Fonts."
|
||||
>
|
||||
<div>
|
||||
{FIRA_SCALE.map(row => (
|
||||
<TypeRow
|
||||
key={row.token}
|
||||
{...row}
|
||||
fontFamily="var(--font-fira-sans, 'Fira Sans', sans-serif)"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Живой пример */}
|
||||
<Section
|
||||
title="Живой пример — блок контента"
|
||||
subtitle="Типичная комбинация заголовка и тела текста на сайте."
|
||||
>
|
||||
<div
|
||||
className="rounded-xl border p-8"
|
||||
style={{
|
||||
borderColor: "var(--bb-border)",
|
||||
background: "var(--bb-sidebar-bg)",
|
||||
fontFamily: "var(--font-fira-sans, 'Fira Sans', sans-serif)",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-xs font-semibold uppercase tracking-widest mb-3"
|
||||
style={{ color: "var(--brand-053m)", letterSpacing: "0.1em" }}
|
||||
>
|
||||
Наши специалисты
|
||||
</p>
|
||||
<h2
|
||||
className="mb-4"
|
||||
style={{ fontSize: 28, fontWeight: 600, lineHeight: 1.3, color: "var(--bb-text)" }}
|
||||
>
|
||||
Запись к ЛОР-врачу<br />без очереди и ожидания
|
||||
</h2>
|
||||
<p
|
||||
className="mb-4 max-w-xl"
|
||||
style={{ fontSize: 16, fontWeight: 400, lineHeight: 1.6, color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
В нашей клинике работают специалисты высшей категории с опытом от 15 лет.
|
||||
Диагностика и лечение заболеваний уха, горла, носа и смежных органов.
|
||||
</p>
|
||||
<p
|
||||
style={{ fontSize: 12, fontWeight: 400, lineHeight: 1.5, color: "#9ca3af" }}
|
||||
>
|
||||
Лицензия № ЛО-77-01-018234 · Москва, ул. Примерная, д. 1
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Бейджи | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section 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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function BadgesPage() {
|
||||
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)" }}
|
||||
>
|
||||
Оффлайн элементы → 6.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)" }}>
|
||||
Именные бейджи для идентификации сотрудников клиники. Размер 70×30 мм,
|
||||
магнитное крепление. Белый фон, чёрный текст.
|
||||
</p>
|
||||
</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
|
||||
title="Технические параметры"
|
||||
subtitle="Единый стандарт для всех сотрудников клиники."
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4 mb-6">
|
||||
{[
|
||||
{ label: "Ширина", value: "70 мм" },
|
||||
{ label: "Высота", value: "30 мм" },
|
||||
{ label: "Материал", value: "Металл / ПВХ" },
|
||||
{ label: "Крепление", value: "Магнитное" },
|
||||
].map(({ label, value }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="rounded-xl border p-4 text-center"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<p className="font-mono text-lg font-semibold" style={{ color: "var(--brand-073m)" }}>
|
||||
{value}
|
||||
</p>
|
||||
<p className="text-xs mt-1" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{label}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Состав текста */}
|
||||
<Section
|
||||
title="Состав текста на бейдже"
|
||||
subtitle="Строгий порядок элементов."
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Элемент", "Содержание", "Оформление"].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["ФИО", "Фамилия Имя Отчество", "Крупный шрифт, первая строка"],
|
||||
["Должность", "Основная должность", "Мелкий шрифт, вторая строка"],
|
||||
["Специализация", "Специализация / учёная степень (если есть)", "Мелкий шрифт, третья строка"],
|
||||
].map(([el, content, style]) => (
|
||||
<tr
|
||||
key={el}
|
||||
className="border-t"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<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)" }}>{style}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Пример из фото */}
|
||||
<div
|
||||
className="mt-4 rounded-xl border p-4 flex items-start gap-3"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<div
|
||||
className="w-1.5 self-stretch rounded-full shrink-0"
|
||||
style={{ background: "var(--brand-053m)" }}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs font-medium mb-1" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Пример из брендбука
|
||||
</p>
|
||||
<p className="text-sm font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Лебединская Елена Александровна
|
||||
</p>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
|
||||
врач оториноларинголог · ведущий хирург · кандидат медицинских наук
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Внутренняя навигация | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section 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>
|
||||
);
|
||||
}
|
||||
|
||||
/* Макет настенной таблички */
|
||||
function SignMockup({
|
||||
type,
|
||||
text,
|
||||
subtext,
|
||||
bgColor,
|
||||
textColor,
|
||||
accentColor,
|
||||
size,
|
||||
}: {
|
||||
type: string;
|
||||
text: string;
|
||||
subtext?: string;
|
||||
bgColor: string;
|
||||
textColor: string;
|
||||
accentColor: string;
|
||||
size: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div
|
||||
className="rounded-lg px-6 py-4 flex items-center gap-4"
|
||||
style={{
|
||||
background: bgColor,
|
||||
width: 260,
|
||||
border: bgColor === "#ffffff" ? "1px solid #e5e7eb" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Цветовая полоса */}
|
||||
<div
|
||||
className="w-1.5 self-stretch rounded-full"
|
||||
style={{ background: accentColor, minHeight: 32 }}
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: textColor,
|
||||
lineHeight: 1.3,
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
{subtext && (
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 10,
|
||||
color: textColor,
|
||||
opacity: 0.65,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
{subtext}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pl-1">
|
||||
<p className="text-xs font-medium" style={{ color: "var(--bb-text)" }}>{type}</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{size}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NavigationPage() {
|
||||
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)" }}
|
||||
>
|
||||
Оффлайн элементы → 6.3
|
||||
</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)" }}>
|
||||
Система навигационных табличек и указателей внутри клиники.
|
||||
Единый стиль с фирменными цветами и шрифтом DINPro.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Шаблоны табличек */}
|
||||
<Section
|
||||
title="Типы табличек"
|
||||
subtitle="Четыре базовых шаблона для разных зон клиники."
|
||||
>
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<SignMockup
|
||||
type="Кабинет врача"
|
||||
text="Кабинет № 101"
|
||||
subtext="Оториноларингология"
|
||||
bgColor="#ffffff"
|
||||
textColor="#1b4c72"
|
||||
accentColor="#7ecfca"
|
||||
size="200 × 80 мм"
|
||||
/>
|
||||
<SignMockup
|
||||
type="Направляющий указатель"
|
||||
text="→ Регистратура"
|
||||
subtext="2-й этаж"
|
||||
bgColor="#5b7b87"
|
||||
textColor="#ffffff"
|
||||
accentColor="#7ecfca"
|
||||
size="300 × 80 мм"
|
||||
/>
|
||||
<SignMockup
|
||||
type="Зона ожидания"
|
||||
text="Зона ожидания"
|
||||
subtext="Пожалуйста, соблюдайте тишину"
|
||||
bgColor="#e0f5f4"
|
||||
textColor="#1b4c72"
|
||||
accentColor="#5bb5ad"
|
||||
size="250 × 80 мм"
|
||||
/>
|
||||
<SignMockup
|
||||
type="Запрещающий"
|
||||
text="Вход только для персонала"
|
||||
bgColor="#1b4c72"
|
||||
textColor="#ffffff"
|
||||
accentColor="#c4a882"
|
||||
size="250 × 60 мм"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Технические требования */}
|
||||
<Section
|
||||
title="Технические требования"
|
||||
subtitle="Материалы и цвета для производства."
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Параметр", "Значение"].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Основной материал", "ПВХ 3мм / ПС зеркальный / акрил"],
|
||||
["Покрытие фона", "Oracal плёнка (053M / 073M / 050M)"],
|
||||
["Шрифт", "DINPro Bold / Regular (DXF для фрезеровки)"],
|
||||
["Крепление", "Двусторонний скотч / шурупы с дистанционным держателем"],
|
||||
["Толщина букв (фрезеровка)", "3 мм от основы"],
|
||||
["Минимальный размер текста", "10 мм по высоте"],
|
||||
].map(([param, value]) => (
|
||||
<tr
|
||||
key={param}
|
||||
className="border-t"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<td className="px-5 py-3 font-medium" style={{ color: "var(--bb-text)" }}>
|
||||
{param}
|
||||
</td>
|
||||
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{value}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Цвета Oracal */}
|
||||
<Section
|
||||
title="Цвета Oracal для навигации"
|
||||
subtitle="Допустимые цвета плёнки по коду Oracal."
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
{[
|
||||
{ code: "053M", hex: "#7ecfca", name: "Акцент / полоса" },
|
||||
{ code: "073M", hex: "#5b7b87", name: "Фон указателей" },
|
||||
{ code: "050M", hex: "#1b4c72", name: "Фон запрещающих" },
|
||||
{ code: "081M", hex: "#c4a882", name: "Акцент на тёмном" },
|
||||
].map(c => (
|
||||
<div
|
||||
key={c.code}
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<div className="h-16" style={{ background: c.hex }} />
|
||||
<div className="p-3">
|
||||
<p className="font-medium text-xs" style={{ color: "var(--bb-text)" }}>
|
||||
Oracal {c.code}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{c.name}
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{c.hex}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Печатные материалы | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section 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>
|
||||
);
|
||||
}
|
||||
|
||||
/* Макет визитки */
|
||||
function BusinessCardMockup({ side }: { side: "front" | "back" }) {
|
||||
/* 90×50 мм → пропорция 9:5. Отображаем 288×160px */
|
||||
if (side === "front") {
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl overflow-hidden shadow-md"
|
||||
style={{ width: 288, height: 160, background: "#5b7b87", flexShrink: 0 }}
|
||||
>
|
||||
{/* Верхняя бирюзовая полоса */}
|
||||
<div style={{ height: 6, background: "#7ecfca" }} />
|
||||
|
||||
<div className="flex h-full">
|
||||
{/* Левая колонка с логотипом */}
|
||||
<div
|
||||
className="flex items-center justify-center px-6"
|
||||
style={{ width: 110, borderRight: "1px solid rgba(255,255,255,0.15)" }}
|
||||
>
|
||||
<Image
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Логотип"
|
||||
width={80}
|
||||
height={28}
|
||||
className="object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правая колонка с данными */}
|
||||
<div className="flex flex-col justify-center px-5 pb-2 gap-1">
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 12,
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
}}
|
||||
>
|
||||
Иванова Анна Викторовна
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 9,
|
||||
color: "rgba(255,255,255,0.7)",
|
||||
}}
|
||||
>
|
||||
Врач-оториноларинголог
|
||||
</p>
|
||||
<div className="mt-2 space-y-0.5">
|
||||
{["+7 (495) 000-00-00", "oclinica.ru"].map(line => (
|
||||
<p
|
||||
key={line}
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 8,
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl overflow-hidden shadow-md flex items-center justify-center"
|
||||
style={{ width: 288, height: 160, background: "#c4a882", flexShrink: 0 }}
|
||||
>
|
||||
<Image
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Логотип на обороте"
|
||||
width={140}
|
||||
height={50}
|
||||
className="object-contain"
|
||||
style={{
|
||||
filter: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* Макет листовки */
|
||||
function LeafletMockup() {
|
||||
/* А5 = 148×210 мм → пропорция ≈ 1:1.42. Отображаем 180×256px */
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl overflow-hidden shadow-md"
|
||||
style={{ width: 180, height: 256, background: "#ffffff", border: "1px solid #e5e7eb", flexShrink: 0 }}
|
||||
>
|
||||
{/* Шапка */}
|
||||
<div
|
||||
className="px-4 pt-4 pb-3 flex items-end justify-between"
|
||||
style={{ background: "#5b7b87" }}
|
||||
>
|
||||
<Image
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Логотип"
|
||||
width={90}
|
||||
height={32}
|
||||
className="object-contain"
|
||||
style={{ filter: "brightness(0) invert(1)" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Тело */}
|
||||
<div className="px-4 pt-3">
|
||||
{/* Плейсхолдер фото */}
|
||||
<div
|
||||
className="w-full rounded mb-3"
|
||||
style={{ height: 70, background: "#e0f5f4" }}
|
||||
/>
|
||||
{/* Заголовок */}
|
||||
<div
|
||||
className="h-3 rounded mb-1.5"
|
||||
style={{ background: "#1b4c72", width: "80%" }}
|
||||
/>
|
||||
<div
|
||||
className="h-3 rounded mb-3"
|
||||
style={{ background: "#1b4c72", width: "60%" }}
|
||||
/>
|
||||
{/* Текст-заглушка */}
|
||||
{[100, 90, 95, 80].map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-1.5 rounded mb-1"
|
||||
style={{ background: "#e5e7eb", width: `${w}%` }}
|
||||
/>
|
||||
))}
|
||||
{/* CTA-кнопка */}
|
||||
<div
|
||||
className="mt-4 rounded py-2 text-center"
|
||||
style={{ background: "#7ecfca" }}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontFamily: "'DINPro', Arial, sans-serif",
|
||||
fontSize: 9,
|
||||
fontWeight: 700,
|
||||
color: "#ffffff",
|
||||
}}
|
||||
>
|
||||
ЗАПИСАТЬСЯ НА ПРИЁМ
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PrintPage() {
|
||||
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)" }}
|
||||
>
|
||||
Оффлайн элементы → 6.5
|
||||
</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)" }}>
|
||||
Листовки, визитки и digital-каналы клиники. Все материалы следуют
|
||||
единому фирменному стилю с использованием шрифта DINPro и цветов Oracal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Визитки */}
|
||||
<Section
|
||||
title="Визитки"
|
||||
subtitle="Стандарт 90×50 мм. Двусторонняя печать. Изображены в масштабе."
|
||||
>
|
||||
<div className="flex flex-wrap gap-6 items-start">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<BusinessCardMockup side="front" />
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Лицевая сторона</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<BusinessCardMockup side="back" />
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Оборотная сторона</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-6 overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Элемент", "Содержание"].map(h => (
|
||||
<th key={h} className="text-left px-5 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}>{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Лицевая — логотип", "Белый инвертированный на тёмно-серо-голубом фоне"],
|
||||
["Лицевая — ФИО", "DINPro Bold, 10pt, белый"],
|
||||
["Лицевая — должность", "DINPro Regular, 8pt, белый 70%"],
|
||||
["Лицевая — контакты", "DINPro Regular, 7pt, белый 60%"],
|
||||
["Оборотная", "Логотип тёмно-коричневый на бежевом фоне (081M)"],
|
||||
["Размер", "90 × 50 мм, 4+4 офсет или цифровая печать"],
|
||||
].map(([el, content]) => (
|
||||
<tr key={el} className="border-t" style={{ borderColor: "var(--bb-border)" }}>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Листовки */}
|
||||
<Section
|
||||
title="Листовки"
|
||||
subtitle="Формат А5 (148×210 мм). Шаблон для акций и информационных материалов."
|
||||
>
|
||||
<div className="flex gap-8 items-start">
|
||||
<LeafletMockup />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}>
|
||||
Структура листовки
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ zone: "Шапка", desc: "Логотип белый на тёмно-серо-голубом (073M)" },
|
||||
{ zone: "Фото", desc: "Фото врача или процедуры, 70% ширины" },
|
||||
{ zone: "Заголовок", desc: "DINPro Bold, тёмно-синий (050M), 18–22pt" },
|
||||
{ zone: "Текст", desc: "DINPro Regular, 9–10pt, серый #374151" },
|
||||
{ zone: "CTA-кнопка", desc: "Основной бирюзовый (053M), DINPro Bold белый" },
|
||||
].map(({ zone, desc }) => (
|
||||
<div key={zone} className="flex gap-3">
|
||||
<span
|
||||
className="shrink-0 text-xs font-semibold px-2 py-0.5 rounded mt-0.5"
|
||||
style={{ background: "#e0f5f4", color: "var(--brand-073m)" }}
|
||||
>
|
||||
{zone}
|
||||
</span>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>{desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Telegram-бот */}
|
||||
<Section
|
||||
title="Telegram-бот"
|
||||
subtitle="Цифровой канал взаимодействия с пациентами."
|
||||
>
|
||||
<div
|
||||
className="rounded-xl border p-6"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
{[
|
||||
{ icon: "🤖", title: "Запись на приём", desc: "Выбор врача, даты и времени через бот" },
|
||||
{ icon: "📋", title: "Результаты анализов", desc: "Уведомления и просмотр результатов" },
|
||||
{ icon: "💬", title: "Напоминания", desc: "Напоминание о предстоящем визите" },
|
||||
].map(({ icon, title, desc }) => (
|
||||
<div key={title} className="flex gap-3">
|
||||
<span className="text-2xl shrink-0">{icon}</span>
|
||||
<div>
|
||||
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
{title}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="mt-4 pt-4 border-t text-xs"
|
||||
style={{ borderColor: "var(--bb-border)", color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
В Telegram-боте используется системный шрифт Telegram. Логотип и цвета бренда
|
||||
применяются в аватаре, приветственном изображении и кнопках меню.
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Брендирование транспорта | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section 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>
|
||||
);
|
||||
}
|
||||
|
||||
/* Макет трамвая (упрощённый силуэт) */
|
||||
function TramMockup() {
|
||||
return (
|
||||
<div
|
||||
className="relative rounded-2xl overflow-hidden"
|
||||
style={{
|
||||
background: "#f8f9fa",
|
||||
border: "1px solid var(--bb-border)",
|
||||
padding: "32px 24px",
|
||||
}}
|
||||
>
|
||||
{/* Кузов трамвая */}
|
||||
<div
|
||||
className="relative mx-auto rounded-xl overflow-hidden"
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: 560,
|
||||
height: 160,
|
||||
background: "#ffffff",
|
||||
border: "2px solid #e5e7eb",
|
||||
}}
|
||||
>
|
||||
{/* Верхняя полоса — бирюзовая */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0"
|
||||
style={{ height: 28, background: "#7ecfca" }}
|
||||
/>
|
||||
|
||||
{/* Основная бежевая полоса */}
|
||||
<div
|
||||
className="absolute left-0 right-0"
|
||||
style={{ top: 28, height: 76, background: "#c4a882" }}
|
||||
/>
|
||||
|
||||
{/* Нижняя полоса — серо-голубая */}
|
||||
<div
|
||||
className="absolute left-0 right-0"
|
||||
style={{ top: 104, height: 56, background: "#5b7b87" }}
|
||||
/>
|
||||
|
||||
{/* Логотип по центру бежевой полосы */}
|
||||
<div
|
||||
className="absolute flex items-center justify-center"
|
||||
style={{ top: 28, left: 0, right: 0, height: 76 }}
|
||||
>
|
||||
<Image
|
||||
src="/logo/logo-transparent.png"
|
||||
alt="Логотип на трамвае"
|
||||
width={180}
|
||||
height={64}
|
||||
className="object-contain"
|
||||
style={{
|
||||
filter: "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Окна (декоративные) */}
|
||||
{[80, 180, 280, 380, 460].map(x => (
|
||||
<div
|
||||
key={x}
|
||||
className="absolute rounded"
|
||||
style={{
|
||||
left: x,
|
||||
top: 40,
|
||||
width: 50,
|
||||
height: 44,
|
||||
background: "rgba(255,255,255,0.6)",
|
||||
border: "1px solid rgba(255,255,255,0.8)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Колёса (декоративные) */}
|
||||
{[60, 200, 360, 500].map(x => (
|
||||
<div
|
||||
key={x}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
left: x,
|
||||
bottom: -8,
|
||||
width: 24,
|
||||
height: 24,
|
||||
background: "#374151",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-center text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Схема цветового решения (превью, не финальный макет)
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TransportPage() {
|
||||
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)" }}
|
||||
>
|
||||
Оффлайн элементы → 6.4
|
||||
</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>
|
||||
|
||||
{/* Макет */}
|
||||
<Section
|
||||
title="Макет трамвая"
|
||||
subtitle="Трёхполосная схема брендирования: верхняя бирюзовая, центральная бежевая с логотипом, нижняя серо-голубая."
|
||||
>
|
||||
<TramMockup />
|
||||
</Section>
|
||||
|
||||
{/* Цветовая схема */}
|
||||
<Section
|
||||
title="Цветовая схема"
|
||||
subtitle="Порядок и размеры полос."
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Зона", "Цвет", "Oracal", "Высота", "Содержимое"].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-4 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Верхняя полоса", "#7ecfca", "053M", "~17% высоты", "Декоративная"],
|
||||
["Центральная полоса", "#c4a882", "081M", "~48% высоты", "Логотип клиники (по центру)"],
|
||||
["Нижняя полоса", "#5b7b87", "073M", "~35% высоты", "Декоративная / контактная информация"],
|
||||
].map(([zone, hex, oracal, height, content]) => (
|
||||
<tr
|
||||
key={zone}
|
||||
className="border-t"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<td className="px-4 py-3 font-medium" style={{ color: "var(--bb-text)" }}>
|
||||
{zone}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-5 h-5 rounded"
|
||||
style={{ background: hex as string, border: "1px solid #e5e7eb" }}
|
||||
/>
|
||||
<span className="font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{hex}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{oracal}
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ color: "var(--bb-text-muted)" }}>{height}</td>
|
||||
<td className="px-4 py-3" style={{ color: "var(--bb-text-muted)" }}>{content}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Логотип на транспорте */}
|
||||
<Section
|
||||
title="Размещение логотипа"
|
||||
subtitle="На центральной полосе, горизонтально по центру борта."
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{[
|
||||
{ label: "Минимальная длина логотипа", value: "600 мм" },
|
||||
{ label: "Отступ от краёв борта", value: "≥ 400 мм" },
|
||||
{ label: "Вертикальное выравнивание", value: "По центру полосы" },
|
||||
{ label: "Цвет логотипа", value: "Тёмно-коричневый (080M)" },
|
||||
].map(({ label, value }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex items-center gap-4 p-4 rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full shrink-0"
|
||||
style={{ background: "var(--brand-053m)" }}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{label}</p>
|
||||
<p className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div
|
||||
className="rounded-xl border p-4 text-sm flex items-start gap-3"
|
||||
style={{ borderColor: "#fde68a", background: "#fffbeb", color: "#92400e" }}
|
||||
>
|
||||
<span className="text-base shrink-0">⚠️</span>
|
||||
<p>
|
||||
Финальные макеты для производства предоставляются дизайнером в форматах AI / PDF с
|
||||
реальными размерами конкретного транспортного средства.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Форма сотрудников | Брендбук О!Клиника",
|
||||
};
|
||||
|
||||
function Section({
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section 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>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UniformPage() {
|
||||
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)" }}
|
||||
>
|
||||
Оффлайн элементы → 6.1
|
||||
</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>
|
||||
|
||||
{/* Фотографии вариантов */}
|
||||
<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/uniform/uniform-1.jpeg"
|
||||
alt="Бежевая форма сотрудника клиники с коричневым логотипом"
|
||||
width={742}
|
||||
height={990}
|
||||
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: "#c4a882", border: "1px solid #e5e7eb" }}
|
||||
/>
|
||||
<span className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Oracal 081M
|
||||
</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: "#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>
|
||||
</Section>
|
||||
|
||||
{/* Размеры логотипа */}
|
||||
<Section
|
||||
title="Размеры логотипа для размещения на форме сотрудников"
|
||||
subtitle="Логотип располагается на левой стороне груди. Размер зависит от размера одежды."
|
||||
>
|
||||
<div
|
||||
className="overflow-hidden rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Размер одежды", "Длина логотипа", "Высота логотипа", "Расположение"].map(h => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-5 py-3 font-medium"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["До 46 р. включительно", "70 мм", "25,5 мм", "Левая сторона груди"],
|
||||
["От 48 р.", "90 мм", "32,8 мм", "Левая сторона груди"],
|
||||
].map(([size, w, h, pos]) => (
|
||||
<tr
|
||||
key={size}
|
||||
className="border-t"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<td className="px-5 py-3" style={{ color: "var(--bb-text)" }}>{size}</td>
|
||||
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>{w}</td>
|
||||
<td className="px-5 py-3 font-mono text-xs" style={{ color: "var(--bb-text)" }}>{h}</td>
|
||||
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{pos}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Правила */}
|
||||
<Section title="Правила использования">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{[
|
||||
{ ok: true, text: "Носить комплект в полном составе" },
|
||||
{ ok: true, text: "Поддерживать чистоту и опрятность формы" },
|
||||
{ ok: true, text: "Логотип только в утверждённых цветовых вариантах" },
|
||||
{ ok: false, text: "Носить форму без логотипа" },
|
||||
{ ok: false, text: "Изменять цвет или материал формы" },
|
||||
{ ok: false, text: "Добавлять сторонние нашивки и знаки" },
|
||||
].map(item => (
|
||||
<div
|
||||
key={item.text}
|
||||
className="flex items-start gap-3 p-4 rounded-lg border"
|
||||
style={{
|
||||
borderColor: item.ok ? "#a7f3d0" : "#fecaca",
|
||||
background: item.ok ? "#f0fdf4" : "#fff5f5",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: item.ok ? "#16a34a" : "#ef4444" }}>
|
||||
{item.ok ? "✓" : "✕"}
|
||||
</span>
|
||||
<p className="text-sm" style={{ color: item.ok ? "#14532d" : "#7f1d1d" }}>
|
||||
{item.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,8 +19,8 @@ const NAV: NavSection[] = [
|
||||
title: "Фундамент",
|
||||
items: [
|
||||
{ label: "Логотип", href: "/foundation/logo" },
|
||||
{ label: "Цвета", href: "/foundation/colors", soon: true },
|
||||
{ label: "Типографика", href: "/foundation/typography", soon: true },
|
||||
{ label: "Цвета", href: "/foundation/colors" },
|
||||
{ label: "Типографика", href: "/foundation/typography" },
|
||||
{ label: "Иконография", href: "/foundation/icons", soon: true },
|
||||
],
|
||||
},
|
||||
@@ -64,11 +64,11 @@ const NAV: NavSection[] = [
|
||||
{
|
||||
title: "Оффлайн элементы",
|
||||
items: [
|
||||
{ label: "Форма сотрудников", href: "/offline/uniform", soon: true },
|
||||
{ label: "Бейджи", href: "/offline/badges", soon: true },
|
||||
{ label: "Навигация", href: "/offline/navigation", soon: true },
|
||||
{ label: "Транспорт", href: "/offline/transport", soon: true },
|
||||
{ label: "Печать", href: "/offline/print", soon: true },
|
||||
{ label: "Форма сотрудников", href: "/offline/uniform" },
|
||||
{ label: "Бейджи", href: "/offline/badges" },
|
||||
{ label: "Навигация", href: "/offline/navigation" },
|
||||
{ label: "Транспорт", href: "/offline/transport" },
|
||||
{ label: "Печать", href: "/offline/print" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -171,7 +171,7 @@ export function Sidebar() {
|
||||
color: "var(--bb-sidebar-text-muted)",
|
||||
}}
|
||||
>
|
||||
Sprint 1 · v0.1.0
|
||||
Sprint 2 · v0.2.0
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "path";
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
turbopack: {
|
||||
root: path.resolve(__dirname, "../.."),
|
||||
},
|
||||
...(isDev && {
|
||||
turbopack: {
|
||||
root: path.resolve(__dirname, "../.."),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
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
@@ -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.
|
||||
+45
-22
@@ -53,30 +53,48 @@
|
||||
|
||||
---
|
||||
|
||||
## Sprint 2 — Цвета, типографика и оффлайн элементы (Brand Foundation)
|
||||
## Sprint 2 — Цвета, типографика и оффлайн элементы (Brand Foundation) ✅ ЗАВЕРШЁН
|
||||
|
||||
**Цель:** Секция «Фундамент бренда» — цвета, оба шрифта, а также первый оффлайн-раздел.
|
||||
|
||||
### Задачи — цвета
|
||||
- [ ] Design: Зафиксировать HEX-эквиваленты всех цветов бренда (053M, 073M, 066M, 050M, 081M, 080M)
|
||||
- [ ] FE: Создать CSS-переменные / дизайн-токены для всей цветовой палитры
|
||||
- [ ] FE: Страница «Цвета» — палитра с кодами Oracal/Pantone, HEX, RGB, HSL, копирование в клик
|
||||
- [ ] FE: Проверка контрастности пар цветов (WCAG AA/AAA)
|
||||
- [ ] FE: Экспорт токенов в JSON (Figma-compatible)
|
||||
- [x] Design: Зафиксировать HEX-эквиваленты всех цветов бренда (053M, 073M, 066M, 050M, 081M, 080M)
|
||||
- [x] FE: Создать CSS-переменные / дизайн-токены для всей цветовой палитры
|
||||
- [x] FE: Страница «Цвета» — палитра с кодами Oracal/Pantone, HEX, RGB, HSL, копирование в клик
|
||||
- [x] FE: Проверка контрастности пар цветов (WCAG AA/AAA)
|
||||
- [x] FE: Экспорт токенов в JSON (Figma-compatible)
|
||||
|
||||
### Задачи — типографика
|
||||
- [ ] FE: Страница «Типографика» — раздел DINPro (бренд/оффлайн) + раздел Fira Sans (веб)
|
||||
- [ ] FE: Шкала размеров для обоих шрифтов: h1–h6, body, caption, label, overline
|
||||
- [ ] FE: Чёткое указание: где DINPro, где Fira Sans
|
||||
- [ ] FE: Живые примеры текста обоими шрифтами
|
||||
- [x] FE: Страница «Типографика» — раздел DINPro (бренд/оффлайн) + раздел Fira Sans (веб)
|
||||
- [x] FE: Шкала размеров для обоих шрифтов: h1–h6, body, caption, label, overline
|
||||
- [x] FE: Чёткое указание: где DINPro, где Fira Sans
|
||||
- [x] FE: Живые примеры текста обоими шрифтами
|
||||
|
||||
### Задачи — оффлайн элементы (справочный раздел)
|
||||
- [ ] FE: Раздел «Оффлайн элементы» в навигации брендбука
|
||||
- [ ] FE: Страница «Форма сотрудников» — фото, правила размещения логотипа, таблица размеров
|
||||
- [ ] FE: Страница «Бейджи» — размеры 70×30 мм, варианты, состав текста
|
||||
- [ ] FE: Страница «Внутренняя навигация» — шаблоны табличек, материалы, цвета Oracal
|
||||
- [ ] FE: Страница «Брендирование транспорта» — макет трамвая, цветовая схема
|
||||
- [ ] FE: Страница «Печатные материалы» — листовки, визитки, Telegram-бот
|
||||
- [x] FE: Раздел «Оффлайн элементы» в навигации брендбука
|
||||
- [x] FE: Страница «Форма сотрудников» — схема размещения логотипа, таблица размеров
|
||||
- [x] FE: Страница «Бейджи» — размеры 70×30 мм, варианты (светлый/тёмный), состав текста
|
||||
- [x] FE: Страница «Внутренняя навигация» — шаблоны табличек, материалы, цвета Oracal
|
||||
- [x] FE: Страница «Брендирование транспорта» — макет трамвая, цветовая схема
|
||||
- [x] FE: Страница «Печатные материалы» — листовки, визитки, Telegram-бот
|
||||
|
||||
### Фактические результаты
|
||||
- Страница `/foundation/colors` — 7 цветов с HEX/RGB/HSL/CSS-var и копированием, WCAG-контраст 7 пар, экспорт JSON
|
||||
- Страница `/foundation/typography` — DINPro (оффлайн) + Fira Sans (веб), таблица применения, полные шкалы, живой пример
|
||||
- Страница `/offline/uniform` — схема формы, таблица размеров логотипа, правила использования
|
||||
- Страница `/offline/badges` — макеты бейджей 70×30 мм (светлый/тёмный), состав текста, применение
|
||||
- Страница `/offline/navigation` — 4 шаблона табличек, технические требования, цвета Oracal
|
||||
- Страница `/offline/transport` — CSS-макет трамвая с трёхполосной схемой, таблица зон
|
||||
- Страница `/offline/print` — макеты визитки (лицо/оборот) и листовки А5, Telegram-бот
|
||||
- Sidebar: убраны «скоро» для Цветов, Типографики и всех 5 страниц Оффлайн
|
||||
- Версия обновлена до **Sprint 2 · v0.2.0**
|
||||
- **Деплой на Vercel:** https://web-oclinica.vercel.app (production, бесплатно)
|
||||
|
||||
### Технические решения Sprint 2
|
||||
- Страница «Цвета» — `"use client"` для clipboard API и экспорта JSON
|
||||
- WCAG relative luminance вычисляется на клиенте, без зависимостей
|
||||
- DINPro отображается с фоллбэком Arial (лицензионный шрифт)
|
||||
- Макеты (бейджи, трамвай, визитки) — чистый CSS/Tailwind без внешних зависимостей
|
||||
|
||||
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы.
|
||||
|
||||
@@ -235,20 +253,25 @@
|
||||
|
||||
---
|
||||
|
||||
## Sprint 12 — Деплой, полировка, документация
|
||||
## Sprint 12 — Полировка, финальный деплой и документация
|
||||
|
||||
**Цель:** Финальный релиз и публикация.
|
||||
**Цель:** Финальный релиз. Фронтенд уже живёт на Vercel с Sprint 2, Sprint 12 — финальная полировка и production-готовность бэкенда.
|
||||
|
||||
### Задачи
|
||||
- [ ] BE + FE: Полный smoke-тест всего брендбука
|
||||
- [ ] FE: Мобильная адаптация — финальная проверка всех страниц
|
||||
- [ ] FE: Accessibility-аудит (WCAG AA)
|
||||
- [ ] Деплой: настройка CI/CD, публикация на сервере
|
||||
- [ ] Docs: Создание `docs/deployment.md`
|
||||
- [ ] Docs: Обновление README.md финальными инструкциями
|
||||
- [ ] Деплой BE: выбрать и настроить хостинг для NestJS + PostgreSQL
|
||||
- [ ] Деплой: настроить автоматический деплой через Gitea Actions → Vercel (при пуше в `main`)
|
||||
- [ ] Docs: Обновление `docs/DEPLOY.md` финальными инструкциями
|
||||
- [ ] Design: Финальный ревью брендбука
|
||||
|
||||
**Результат спринта:** Брендбук задеплоен и доступен по URL.
|
||||
### Текущий статус деплоя
|
||||
- **Фронтенд:** https://web-oclinica.vercel.app (Vercel Hobby, задеплоен в Sprint 2)
|
||||
- **Команда деплоя:** `cd apps/web && vercel --prod --yes`
|
||||
- **Бэкенд:** локально (Docker Compose), хостинг выбирается в Sprint 12
|
||||
|
||||
**Результат спринта:** Брендбук полностью готов, оба сервиса задеплоены, автодеплой настроен.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+34
-19
@@ -37,9 +37,10 @@
|
||||
| Краткое название | oclinica-brandbook |
|
||||
| Сайт клиники | https://oclinica.ru |
|
||||
| Тип системы | Веб-приложение (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 |
|
||||
| Бэкенд | NestJS | 10.x | Типизированный Node.js фреймворк, DI, модули |
|
||||
| Фронтенд | Next.js (App Router) | 16.x | SSR/SSG, оптимизация, экосистема React |
|
||||
| Бэкенд | NestJS | 11.x | Типизированный Node.js фреймворк, DI, модули |
|
||||
| База данных | PostgreSQL | 16.x | Надёжная реляционная БД, JSON-поля для атрибутов |
|
||||
| ORM | Prisma | 5.x | Type-safe запросы, миграции, seed |
|
||||
| Стилизация | CSS Modules | — | Изоляция стилей, нет рантайм-зависимостей |
|
||||
| ORM | Prisma | 7.x | Type-safe запросы, миграции, seed |
|
||||
| Стилизация | Tailwind CSS | 4.x | Utility-first, CSS-переменные, нет рантайм-overhead |
|
||||
| Дизайн-токены | CSS Custom Properties | — | Нативно поддерживается всеми браузерами |
|
||||
| Шрифт (бренд) | DINPro | — | Фирменный шрифт бренда, оффлайн-носители |
|
||||
| Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300 и 400, сайт |
|
||||
| Шрифт (веб) | Fira Sans | — | Google Fonts, кириллица, веса 300/400/500/600 |
|
||||
| Авторизация | JWT + httpOnly cookie | — | Безопасное хранение токена |
|
||||
| Пакетный менеджер | pnpm | 9.x | Monorepo workspaces, скорость |
|
||||
| Контейнеризация | Docker + Compose | — | Единообразное окружение dev/prod |
|
||||
| Пакетный менеджер | pnpm | 10.x | Monorepo workspaces, скорость |
|
||||
| Контейнеризация | Docker + Compose | — | Единообразное окружение локальной разработки |
|
||||
| Хостинг фронтенда | Vercel | — | Нативная поддержка Next.js, бесплатный Hobby-план |
|
||||
|
||||
---
|
||||
|
||||
@@ -480,16 +482,29 @@ oclinica-brandbook/
|
||||
| Docker | >= 24 |
|
||||
| Docker Compose | >= 2 |
|
||||
|
||||
### 12.2 Production-сервер
|
||||
### 12.2 Деплой фронтенда (Vercel)
|
||||
|
||||
> TBD — параметры хостинга будут прописаны отдельно.
|
||||
Фронтенд (`apps/web`) деплоится на Vercel Hobby (бесплатно).
|
||||
|
||||
Минимальные ожидаемые требования:
|
||||
- ОС: Ubuntu 22.04+
|
||||
- RAM: 2 GB
|
||||
- Disk: 20 GB
|
||||
- PostgreSQL 16 (или managed database)
|
||||
- Node.js 20 LTS
|
||||
**Production URL:** https://web-oclinica.vercel.app
|
||||
|
||||
**Команда деплоя** (из директории `apps/web`):
|
||||
```bash
|
||||
vercel --prod --yes
|
||||
```
|
||||
|
||||
**Требования:**
|
||||
- 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 |
|
||||
| ОВ-02 | Параметры хостинга для production-деплоя | Клиника | TBD |
|
||||
| ОВ-02 | ~~Параметры хостинга для production-деплоя~~ **Закрыт:** фронтенд — Vercel Hobby (https://web-oclinica.vercel.app); бэкенд — TBD к Sprint 11 | — | Частично закрыт |
|
||||
| ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 |
|
||||
| ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 |
|
||||
| ОВ-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). Точная калибровка — при получении физических образцов. | — | Закрыт |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user