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>
This commit is contained in:
@@ -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 для крупного (>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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user