Browse Source

feat(sprint-2): цвета, типографика, оффлайн элементы

Страницы фундамента:
- /foundation/colors — палитра 7 цветов, HEX/RGB/HSL/CSS-var с копированием,
  WCAG-контраст 7 пар, экспорт JSON-токенов
- /foundation/typography — DINPro (оффлайн) + Fira Sans (веб), шкалы,
  таблица применения, живой пример

Оффлайн элементы (5 страниц):
- /offline/uniform — схема формы, таблица размеров логотипа
- /offline/badges — макеты бейджей 70×30 мм (светлый/тёмный)
- /offline/navigation — 4 шаблона табличек, цвета Oracal
- /offline/transport — CSS-макет трамвая, трёхполосная схема
- /offline/print — макеты визитки и листовки А5, Telegram-бот

Sidebar: убраны «скоро» для Цветов, Типографики, всех Оффлайн
Версия: Sprint 2 · v0.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sprint/2
AR 15 M4 1 week ago
parent
commit
02f561fcd3
  1. 415
      apps/web/app/foundation/colors/page.tsx
  2. 349
      apps/web/app/foundation/typography/page.tsx
  3. 273
      apps/web/app/offline/badges/page.tsx
  4. 252
      apps/web/app/offline/navigation/page.tsx
  5. 331
      apps/web/app/offline/print/page.tsx
  6. 262
      apps/web/app/offline/transport/page.tsx
  7. 207
      apps/web/app/offline/uniform/page.tsx
  8. 16
      apps/web/components/layout/Sidebar.tsx
  9. 49
      docs/SPRINTS.md

415
apps/web/app/foundation/colors/page.tsx

@ -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, string>> = { 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 для крупного (&gt;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>
);
}

349
apps/web/app/foundation/typography/page.tsx

@ -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>
);
}

273
apps/web/app/offline/badges/page.tsx

@ -0,0 +1,273 @@
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 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() {
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-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="Макет бейджа (масштаб: 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
title="Состав текста на бейдже"
subtitle="Строгий порядок элементов. Шрифт — DINPro."
>
<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>
{[
["Логотип", "Логотип клиники", "PNG / SVG", "Левая часть, по центру по высоте"],
["Разделитель", "Вертикальная линия", "1px, 40% прозрачность", "Между логотипом и текстом"],
["ФИО", "Фамилия И.О.", "DINPro Bold 13px", "Правая часть, верхняя строка"],
["Должность", "Полное название должности", "DINPro Regular 10px", "Правая часть, нижняя строка"],
].map(([el, content, font, pos]) => (
<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 font-mono text-xs" style={{ color: "var(--bb-text-muted)" }}>{font}</td>
<td className="px-5 py-3" style={{ color: "var(--bb-text-muted)" }}>{pos}</td>
</tr>
))}
</tbody>
</table>
</div>
</Section>
{/* Цветовые варианты */}
<Section
title="Применение вариантов"
subtitle="Выбор варианта зависит от должности сотрудника."
>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{[
{
variant: "Светлый (бежевый)",
color: "#c4a882",
usage: "Медицинский персонал, санитарки, технический персонал",
oracal: "081M",
},
{
variant: "Тёмный (серо-голубой)",
color: "#5b7b87",
usage: "Административный персонал, менеджеры, главный врач",
oracal: "073M",
},
].map(item => (
<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>
</Section>
</div>
);
}

252
apps/web/app/offline/navigation/page.tsx

@ -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>
);
}

331
apps/web/app/offline/print/page.tsx

@ -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>
);
}

262
apps/web/app/offline/transport/page.tsx

@ -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>
);
}

207
apps/web/app/offline/uniform/page.tsx

@ -0,0 +1,207 @@
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="rounded-xl border p-6"
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}>
Состав формы
</p>
<ul className="space-y-2">
{["Медицинский костюм (куртка + брюки)", "Цвет: бежевый (Oracal 081M)", "Материал: медицинская ткань", "Логотип вышит или нанесён термопечатью"].map(item => (
<li key={item} className="flex items-start gap-2 text-sm" style={{ color: "var(--bb-text-muted)" }}>
<span style={{ color: "var(--brand-053m)" }}></span> {item}
</li>
))}
</ul>
</div>
<div>
<p className="font-medium text-sm mb-3" style={{ color: "var(--bb-text)" }}>
Цветовая схема
</p>
<div className="flex gap-3">
<div className="text-center">
<div className="w-12 h-12 rounded-lg border mb-1"
style={{ background: "#c4a882", borderColor: "var(--bb-border)" }} />
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>081M</p>
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>#c4a882</p>
</div>
<div className="text-center">
<div className="w-12 h-12 rounded-lg border mb-1"
style={{ background: "#5c2e0e", borderColor: "var(--bb-border)" }} />
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>080M</p>
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>#5c2e0e</p>
</div>
</div>
</div>
</div>
</div>
</Section>
{/* Логотип на форме */}
<Section
title="Размещение логотипа"
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
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>
);
}

16
apps/web/components/layout/Sidebar.tsx

@ -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>
);

49
docs/SPRINTS.md

@ -53,30 +53,47 @@
---
## 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**
### Технические решения Sprint 2
- Страница «Цвета» — `"use client"` для clipboard API и экспорта JSON
- WCAG relative luminance вычисляется на клиенте, без зависимостей
- DINPro отображается с фоллбэком Arial (лицензионный шрифт)
- Макеты (бейджи, трамвай, визитки) — чистый CSS/Tailwind без внешних зависимостей
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы.

Loading…
Cancel
Save