Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6610a288b | |||
| 77c9733144 | |||
| c1731615ab | |||
| 0855892643 | |||
| 0198947c4e | |||
| 6648f16bcb | |||
| 761347ed85 | |||
| 52acdc98e2 |
@@ -0,0 +1,573 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { CodeCopy } from "@/components/ui/CodeCopy";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Кнопки. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function Section({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
id?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const VARIANTS = [
|
||||
{
|
||||
variant: "primary" as const,
|
||||
name: "Primary",
|
||||
label: "Запишитесь к нам",
|
||||
cssClass: ".bb-btn-primary",
|
||||
bg: "#FFA39C",
|
||||
border: "#FF847B",
|
||||
textColor: "#fff",
|
||||
radius: "7px",
|
||||
shadow: "да",
|
||||
where: "Кнопка отправки форм записи",
|
||||
example: "«Запишите меня!»",
|
||||
note: "Коралловый — самый заметный акцент на странице. Всегда один в форме.",
|
||||
},
|
||||
{
|
||||
variant: "outline" as const,
|
||||
name: "Outline",
|
||||
label: "Записаться на приём",
|
||||
cssClass: ".bb-btn-outline",
|
||||
bg: "#fff",
|
||||
border: "#BF9975",
|
||||
textColor: "#BF9975",
|
||||
radius: "7px",
|
||||
shadow: "нет",
|
||||
where: "Хедер, навигация, ссылки-кнопки",
|
||||
example: "«Записаться на прием», «Все новости»",
|
||||
note: "Бежевая рамка — ненавязчивый вторичный CTA. Не конкурирует с основной формой.",
|
||||
},
|
||||
{
|
||||
variant: "teal" as const,
|
||||
name: "Teal",
|
||||
label: "Позвонить",
|
||||
cssClass: ".bb-btn-teal",
|
||||
bg: "#60959c",
|
||||
border: "прозрачный",
|
||||
textColor: "#fff",
|
||||
radius: "7px",
|
||||
shadow: "нет",
|
||||
where: "Контактные действия — звонок",
|
||||
example: "«Позвонить»",
|
||||
note: "Серо-бирюзовый — цвет из реального CSS сайта. Близок к Oracal 066M.",
|
||||
},
|
||||
{
|
||||
variant: "pill" as const,
|
||||
name: "Pill",
|
||||
label: "Заказать звонок",
|
||||
cssClass: ".bb-btn-pill",
|
||||
bg: "#e9e4d4",
|
||||
border: "#d5cfbd",
|
||||
textColor: "#333",
|
||||
radius: "25px",
|
||||
shadow: "нет",
|
||||
where: "Модальные триггеры, мягкий CTA",
|
||||
example: "«Заказать звонок»",
|
||||
note: "Кремовый фон + pill-форма — мягкий стиль. Используется для открытия модальных окон.",
|
||||
},
|
||||
];
|
||||
|
||||
const LLM_BUTTONS_TEXT = `КНОПКИ — LLM-спецификация (с реального сайта oclinica.ru)
|
||||
Версия: v2.0 · /components/buttons
|
||||
Источник CSS: perm.oclinica.ru/.../style.css
|
||||
|
||||
ВАРИАНТЫ (реальный сайт)
|
||||
Вариант | CSS класс | Фон | Текст | Граница | Radius | Shadow | Применение
|
||||
primary | .bb-btn-primary | #FFA39C | #fff | #FF847B | 7px | да | Форм-сабмит «Запишите меня!»
|
||||
outline | .bb-btn-outline | #fff | #BF9975 | #BF9975 | 7px | нет | Хедер «Записаться на прием», ссылки-кнопки
|
||||
teal | .bb-btn-teal | #60959c | #fff | нет | 7px | нет | Звонок «Позвонить»
|
||||
pill | .bb-btn-pill | #e9e4d4 | #333 | #d5cfbd | 25px | нет | Callback «Заказать звонок»
|
||||
|
||||
CSS С САЙТА (точные значения)
|
||||
/* форм-кнопка «Запишите меня!» */
|
||||
button { background:#FFA39C; color:white; font-weight:bold; border:solid 1px #FF847B;
|
||||
height:42px; font-size:18px; box-shadow:0px 0px 5px rgba(0,0,0,0.5),0px 4px 5px rgba(0,0,0,0.3); }
|
||||
|
||||
/* appointment — «Записаться на прием» */
|
||||
.appointment { background:#FFF; border:#BF9975 solid 1px; color:#BF9975;
|
||||
font-size:14px; line-height:38px; padding:3px 12px; border-radius:7px; }
|
||||
|
||||
/* show-phone — «Позвонить» */
|
||||
.show-phone { background:rgb(96,149,156); color:#fff; border-radius:7px;
|
||||
font-size:14px; line-height:38px; padding:3px 12px; }
|
||||
|
||||
/* callback — «Заказать звонок» */
|
||||
a.callback_url { background:#e9e4d4; border:#d5cfbd solid 1px; color:#000;
|
||||
border-radius:25px; font-size:16px; padding:6px 18px; }
|
||||
|
||||
РАЗМЕРЫ (брендбук-компонент)
|
||||
Размер | CSS класс | padding | font-size | Применение
|
||||
sm | .bb-btn-sm | 4px 11px | 13px | Компактные контексты
|
||||
md | .bb-btn-md | 8px 16px | 14px | Стандарт (appointment, teal, pill)
|
||||
lg | .bb-btn-lg | 10px 24px | 18px + bold | Форм-сабмит (соответствует реальному сайту)
|
||||
|
||||
ПРАВИЛА ПРИМЕНЕНИЯ
|
||||
✓ primary (коралловый) — только для главного CTA в форме записи
|
||||
✓ outline (бежевый) — хедер, навигация, ссылки-кнопки на странице
|
||||
✓ teal (бирюзовый) — контактные действия (звонок, направление)
|
||||
✓ pill (кремовый) — открытие модальных окон, мягкий callback
|
||||
✓ Не более одного primary на форму
|
||||
✕ Не менять цвета вне фирменной палитры сайта
|
||||
✕ Primary — не для навигационных ссылок
|
||||
✕ Не накладывать тень на outline/teal/pill`.trim();
|
||||
|
||||
export default function ButtonsPage() {
|
||||
const codeHtml = `<!-- Primary — форм-кнопка «Запишите меня!» -->
|
||||
<button class="bb-btn bb-btn-lg bb-btn-primary">Запишите меня!</button>
|
||||
|
||||
<!-- Outline — appointment «Записаться на прием» -->
|
||||
<a class="bb-btn bb-btn-md bb-btn-outline" href="#form">Записаться на прием</a>
|
||||
|
||||
<!-- Teal — «Позвонить» -->
|
||||
<a class="bb-btn bb-btn-md bb-btn-teal" href="tel:+73422250662">Позвонить</a>
|
||||
|
||||
<!-- Pill — «Заказать звонок» -->
|
||||
<a class="bb-btn bb-btn-md bb-btn-pill" href="#callback">Заказать звонок</a>`;
|
||||
|
||||
const codeReact = `import { Button } from "@/components/ui/Button";
|
||||
|
||||
// Форм-кнопка (главный CTA)
|
||||
<Button variant="primary" size="lg">Запишите меня!</Button>
|
||||
|
||||
// Запись из хедера / навигации
|
||||
<Button variant="outline" size="md">Записаться на прием</Button>
|
||||
|
||||
// Звонок
|
||||
<Button variant="teal" size="md">Позвонить</Button>
|
||||
|
||||
// Заказать звонок (открывает модал)
|
||||
<Button variant="pill" size="md">Заказать звонок</Button>
|
||||
|
||||
// С loading-состоянием
|
||||
<Button variant="primary" size="lg" loading>Отправляем...</Button>`;
|
||||
|
||||
const codeSiteExact = `/* Точный CSS с сайта oclinica.ru (style.css) */
|
||||
|
||||
/* Форм-кнопка — кнопка отправки форм записи */
|
||||
#block-entityform-block-feedback button,
|
||||
#block-entityform-block-lor-form button {
|
||||
background: #FFA39C;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: solid 1px #FF847B;
|
||||
width: 300px;
|
||||
height: 42px;
|
||||
font-size: 18px;
|
||||
box-shadow: 0px 0px 5px rgba(0,0,0,0.5), 0px 4px 5px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
/* Кнопка «Записаться на прием» в хедере */
|
||||
#block-block-15 .appointment {
|
||||
background: #FFF;
|
||||
border: #BF9975 solid 1px;
|
||||
color: #BF9975;
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
padding: 3px 12px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* Кнопка «Позвонить» */
|
||||
.show-phone {
|
||||
background: rgb(96, 149, 156); /* #60959c */
|
||||
color: #fff;
|
||||
border-radius: 7px;
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
padding: 3px 12px;
|
||||
}
|
||||
|
||||
/* Кнопка «Заказать звонок» */
|
||||
a.callback_url {
|
||||
background: #e9e4d4;
|
||||
border: #d5cfbd solid 1px;
|
||||
color: #000;
|
||||
border-radius: 25px;
|
||||
font-size: 16px;
|
||||
padding: 6px 18px;
|
||||
}`;
|
||||
|
||||
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)" }}
|
||||
>
|
||||
Компоненты → 3.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)" }}>
|
||||
Кнопки скопированы с реального сайта{" "}
|
||||
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
oclinica.ru
|
||||
</span>
|
||||
. Цвета, размеры и тени взяты напрямую из CSS темы{" "}
|
||||
<span className="font-mono text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
clinic_bootstrap_mobile/css/style.css
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
<div
|
||||
className="mt-4 px-4 py-3 rounded-lg border text-sm flex items-center gap-2"
|
||||
style={{ borderColor: "#e0f5f4", background: "#f8fffe", color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
<span style={{ color: "var(--brand-053m)", fontWeight: 600 }}>Источник</span>
|
||||
<span>
|
||||
CSS сайта проанализирован 2026-03-22 — 4 типа кнопок с реальными значениями.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 1. Варианты */}
|
||||
<Section
|
||||
id="variants"
|
||||
title="Варианты"
|
||||
subtitle="Четыре типа кнопок с реального сайта клиники."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
|
||||
{VARIANTS.map(({ variant, name, label, where, example, note, bg, border, textColor, radius, shadow }) => (
|
||||
<div
|
||||
key={variant}
|
||||
className="rounded-xl border p-5 flex flex-col gap-4"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{/* Превью */}
|
||||
<div
|
||||
className="flex items-center justify-center py-6 rounded-lg"
|
||||
style={{ background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<Button variant={variant} size="md">
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Инфо */}
|
||||
<div>
|
||||
<p className="font-semibold text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
{name}
|
||||
</p>
|
||||
<p className="text-xs mb-2 leading-relaxed" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{note}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 mb-2">
|
||||
{[
|
||||
{ k: "bg", v: bg },
|
||||
{ k: "text", v: textColor },
|
||||
{ k: "border", v: border },
|
||||
{ k: "radius", v: radius },
|
||||
...(shadow === "да" ? [{ k: "shadow", v: "да" }] : []),
|
||||
].map(({ k, v }) => (
|
||||
<span
|
||||
key={k}
|
||||
className="text-[10px] font-mono px-1.5 py-0.5 rounded"
|
||||
style={{ background: "#f3f4f6", color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{k}: {v}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="rounded p-2.5 text-xs"
|
||||
style={{ background: "#f8f9fa", color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
<span className="font-medium" style={{ color: "var(--bb-text)" }}>
|
||||
Где:
|
||||
</span>{" "}
|
||||
{where}
|
||||
<br />
|
||||
<span className="font-medium" style={{ color: "var(--bb-text)" }}>
|
||||
Пример:
|
||||
</span>{" "}
|
||||
{example}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 2. Размеры */}
|
||||
<Section
|
||||
id="sizes"
|
||||
title="Размеры"
|
||||
subtitle="Три размера для разных контекстов. lg соответствует форм-кнопке на реальном сайте (18px, bold)."
|
||||
>
|
||||
<div
|
||||
className="rounded-xl border overflow-hidden"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{
|
||||
size: "sm" as const,
|
||||
label: "Small",
|
||||
hint: "4px 11px · 13px",
|
||||
use: "Компактные контексты, таблицы",
|
||||
},
|
||||
{
|
||||
size: "md" as const,
|
||||
label: "Medium",
|
||||
hint: "8px 16px · 14px",
|
||||
use: "Appointment, Teal, Pill (соответствует сайту)",
|
||||
},
|
||||
{
|
||||
size: "lg" as const,
|
||||
label: "Large",
|
||||
hint: "10px 24px · 18px bold",
|
||||
use: "Primary форм-кнопка (соответствует сайту)",
|
||||
},
|
||||
] as const
|
||||
).map(({ size, label, hint, use }, i) => (
|
||||
<div
|
||||
key={size}
|
||||
className="flex items-center gap-6 px-5 py-4"
|
||||
style={{ borderTop: i > 0 ? "1px solid var(--bb-border)" : undefined }}
|
||||
>
|
||||
<div className="w-40 shrink-0">
|
||||
<Button variant="primary" size={size}>
|
||||
Записаться
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium mb-0.5" style={{ color: "var(--bb-text)" }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs font-mono" style={{ color: "var(--bb-text-muted)" }}>
|
||||
padding: {hint.split("·")[0].trim()} · font-size: {hint.split("·")[1].trim()}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className="text-xs hidden lg:block"
|
||||
style={{ color: "var(--bb-text-muted)", maxWidth: 220 }}
|
||||
>
|
||||
{use}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 3. Состояния */}
|
||||
<Section
|
||||
id="states"
|
||||
title="Состояния"
|
||||
subtitle="Базовые состояния кнопки. На реальном сайте hover/transition не определены в CSS."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{(
|
||||
[
|
||||
{
|
||||
label: "Default",
|
||||
node: <Button variant="primary" size="lg">Записаться</Button>,
|
||||
hint: "Стандартное состояние",
|
||||
},
|
||||
{
|
||||
label: "Hover",
|
||||
node: (
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
style={{ filter: "brightness(0.93)" }}
|
||||
>
|
||||
Записаться
|
||||
</Button>
|
||||
),
|
||||
hint: "filter: brightness(0.93)",
|
||||
},
|
||||
{
|
||||
label: "Loading",
|
||||
node: <Button variant="primary" size="lg" loading>Отправка...</Button>,
|
||||
hint: "Спиннер + blocked",
|
||||
},
|
||||
{
|
||||
label: "Disabled",
|
||||
node: <Button variant="primary" size="lg" disabled>Записаться</Button>,
|
||||
hint: "opacity: 0.5",
|
||||
},
|
||||
] as const
|
||||
).map(({ label, node, hint }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="rounded-xl border p-4"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-center py-4 mb-3 rounded-lg"
|
||||
style={{ background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
{node}
|
||||
</div>
|
||||
<p className="text-sm font-medium mb-0.5" style={{ color: "var(--bb-text)" }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{hint}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 4. Контекст применения */}
|
||||
<Section
|
||||
id="context"
|
||||
title="Где применяется"
|
||||
subtitle="Таблица: тип кнопки → реальное использование на сайте."
|
||||
>
|
||||
<div
|
||||
className="rounded-xl border overflow-hidden"
|
||||
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-4 py-3 font-medium text-xs"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{
|
||||
v: <Button variant="primary" size="sm">Primary</Button>,
|
||||
bg: "#FFA39C",
|
||||
ctx: "button в entityform-блоках форм записи",
|
||||
text: "«Запишите меня!»",
|
||||
},
|
||||
{
|
||||
v: <Button variant="outline" size="sm">Outline</Button>,
|
||||
bg: "#fff / рамка #BF9975",
|
||||
ctx: ".appointment в хедере (block-block-15, 30, 32, 24)",
|
||||
text: "«Записаться на прием»",
|
||||
},
|
||||
{
|
||||
v: <Button variant="teal" size="sm">Teal</Button>,
|
||||
bg: "#60959c",
|
||||
ctx: ".show-phone (block-block-4, 15)",
|
||||
text: "«Позвонить»",
|
||||
},
|
||||
{
|
||||
v: <Button variant="pill" size="sm">Pill</Button>,
|
||||
bg: "#e9e4d4",
|
||||
ctx: "a.callback_url (modal trigger)",
|
||||
text: "«Заказать звонок»",
|
||||
},
|
||||
].map(({ v, bg, ctx, text }, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
style={{ borderTop: "1px solid var(--bb-border)" }}
|
||||
>
|
||||
<td className="px-4 py-3">{v}</td>
|
||||
<td
|
||||
className="px-4 py-3 font-mono text-xs"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{bg}
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-3 font-mono text-xs"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{ctx}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs" style={{ color: "var(--bb-text)" }}>
|
||||
{text}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 5. Примеры кода */}
|
||||
<Section
|
||||
id="code"
|
||||
title="Примеры кода"
|
||||
subtitle="HTML-классы из globals.css, JSX-компонент, и точный CSS с сайта."
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<CodeCopy lang="HTML (CSS-классы brandbook)" code={codeHtml} />
|
||||
<CodeCopy lang="JSX (React / Next.js)" code={codeReact} />
|
||||
<CodeCopy lang="CSS — точно с сайта oclinica.ru" code={codeSiteExact} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* LLM-блок */}
|
||||
<LlmBlock path="/components/buttons" version="v2.0" specText={LLM_BUTTONS_TEXT}>
|
||||
<LlmSection title="Варианты (реальный сайт oclinica.ru)" />
|
||||
<LlmTable
|
||||
headers={["Вариант", "CSS класс", "Фон", "Текст", "Border", "Radius", "Применение"]}
|
||||
rows={VARIANTS.map((v) => [
|
||||
v.variant,
|
||||
v.cssClass,
|
||||
v.bg,
|
||||
v.textColor,
|
||||
v.border,
|
||||
v.radius,
|
||||
v.where,
|
||||
])}
|
||||
/>
|
||||
<LlmSection title="Размеры (брендбук-компонент)" />
|
||||
<LlmTable
|
||||
headers={["Размер", "padding", "font-size", "Применение"]}
|
||||
rows={[
|
||||
["sm", "4px 11px", "13px", "Компактные контексты"],
|
||||
["md", "8px 16px", "14px", "Стандарт (outline, teal, pill с сайта)"],
|
||||
["lg", "10px 24px", "18px bold", "Primary форм-кнопка (соответствует сайту)"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Правила применения" />
|
||||
<LlmRules
|
||||
rules={[
|
||||
{ ok: true, text: "primary (коралловый) — только для submit в формах записи" },
|
||||
{ ok: true, text: "outline (бежевый) — хедер, навигация, второстепенные ссылки" },
|
||||
{ ok: true, text: "teal (бирюзовый) — телефонные и контактные действия" },
|
||||
{ ok: true, text: "pill (кремовый) — открытие модальных окон / callback" },
|
||||
{ ok: true, text: "Не более одного primary на форму" },
|
||||
{ ok: false, text: "Не менять цвета вне указанной палитры сайта" },
|
||||
{ ok: false, text: "Primary — не для навигационных ссылок" },
|
||||
{ ok: false, text: "Не накладывать тень на outline, teal, pill" },
|
||||
]}
|
||||
/>
|
||||
</LlmBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Toggle } from "@/components/ui/Toggle";
|
||||
import { CodeCopy } from "@/components/ui/CodeCopy";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Форм-контролы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function Section({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
children,
|
||||
}: {
|
||||
id?: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section id={id} className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldLabel({ text, required }: { text: string; required?: boolean }) {
|
||||
return (
|
||||
<label
|
||||
className="block text-sm font-medium mb-1.5"
|
||||
style={{ color: "var(--bb-text)" }}
|
||||
>
|
||||
{text}
|
||||
{required && <span style={{ color: "#dc2626", marginLeft: 2 }}>*</span>}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldHint({ text }: { text: string }) {
|
||||
return (
|
||||
<p className="mt-1.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{text}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldError({ text }: { text: string }) {
|
||||
return (
|
||||
<p className="mt-1.5 text-xs" style={{ color: "#dc2626" }}>
|
||||
{text}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function StateCard({
|
||||
label,
|
||||
hint,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
hint: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border p-4" style={{ borderColor: "var(--bb-border)" }}>
|
||||
<div className="mb-3">{children}</div>
|
||||
<p className="text-sm font-medium mb-0.5" style={{ color: "var(--bb-text)" }}>
|
||||
{label}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{hint}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const LLM_FORMS_TEXT = `ФОРМ-КОНТРОЛЫ — LLM-спецификация
|
||||
Версия: v2.0 · /components/forms
|
||||
|
||||
ТЕКСТОВОЕ ПОЛЕ (Input)
|
||||
CSS класс: .bb-input
|
||||
Высота: 50px · padding: 10px 12px
|
||||
border: 1px solid #ccc · border-radius: 4px · font: Fira Sans 14px
|
||||
Источник: entityform input[type=text] на perm.oclinica.ru
|
||||
Состояния:
|
||||
default: border #ccc
|
||||
focus: border #7ecfca + box-shadow 0 0 0 3px rgba(126,207,202,0.2)
|
||||
error: border #dc2626 + класс .bb-error
|
||||
disabled: opacity 0.5 + cursor not-allowed + bg #f8f9fa
|
||||
|
||||
МНОГОСТРОЧНЫЙ ТЕКСТ (Textarea)
|
||||
CSS класс: .bb-textarea
|
||||
Те же состояния что у Input
|
||||
min-height: 100px · resize: vertical · padding: 10px 12px
|
||||
|
||||
ВЫПАДАЮЩИЙ СПИСОК (Select)
|
||||
CSS класс: .bb-select
|
||||
Высота: 50px · padding: 10px 36px 10px 10px
|
||||
Стрелка: SVG background-image (data URI)
|
||||
Источник: .form-control.form-select entityform на сайте
|
||||
Те же состояния что у Input
|
||||
|
||||
ФЛАЖОК (Checkbox)
|
||||
CSS класс: .bb-checkbox
|
||||
size: 16×16px · accent-color: #5b7b87
|
||||
HTML: <input type="checkbox" class="bb-checkbox" />
|
||||
Состояния: unchecked / checked / disabled / checked+disabled
|
||||
|
||||
ПЕРЕКЛЮЧАТЕЛЬ ВАРИАНТА (Radio)
|
||||
CSS класс: .bb-radio
|
||||
size: 16×16px · accent-color: #5b7b87
|
||||
HTML: <input type="radio" class="bb-radio" name="group" />
|
||||
Всегда в группе — один выбранный из нескольких
|
||||
|
||||
ТУМБЛЕР (Toggle/Switch)
|
||||
Компонент: @/components/ui/Toggle (React, "use client")
|
||||
Ширина трека: 44px · Высота: 24px · Бегунок: 20×20px
|
||||
Выкл: track #d1d5db · Вкл: track #5b7b87
|
||||
CSS: .bb-toggle-track / .bb-toggle-thumb
|
||||
HTML-аналог: <input type="checkbox" role="switch" />
|
||||
|
||||
КОНТЕКСТ ПРИМЕНЕНИЯ НА САЙТЕ
|
||||
Input/Select используются в entityform-блоках:
|
||||
#block-entityform-block-lor-form — форма «Запишите меня!» (ЛОР)
|
||||
#block-entityform-block-lor-form-2 — форма «Узнайте стоимость операции»
|
||||
#block-entityform-block-surgery-form — форма хирургии
|
||||
Фон формы: #b8e6ed (светло-бирюзовый)
|
||||
Ширина полей: 302px (фиксированная), кнопка submit: 300px
|
||||
|
||||
ОБЩИЕ ПРАВИЛА
|
||||
✓ Метка (label) всегда над полем, font-weight: 500
|
||||
✓ Обязательные поля помечены * красным цветом (#dc2626)
|
||||
✓ Подсказка (hint) серым текстом под полем — font-size: 12px
|
||||
✓ Сообщение об ошибке красным (#dc2626) под полем вместо hint
|
||||
✓ Focus outline — бирюзовый #7ecfca (--brand-053m)
|
||||
✓ Группы checkbox/radio — вертикальный список с gap: 10px
|
||||
✓ Toggle — для булевых настроек включить/выключить
|
||||
✕ Не использовать placeholder вместо label
|
||||
✕ Не скрывать обязательность поля
|
||||
✕ Не делать поля шире контейнера`.trim();
|
||||
|
||||
export default function FormsPage() {
|
||||
const codeInput = `<!-- HTML -->
|
||||
<label class="bb-label">Имя пациента <span style="color:#dc2626">*</span></label>
|
||||
<input class="bb-input" type="text" placeholder="Иван Иванов" />
|
||||
<p class="bb-hint">Укажите имя как в паспорте</p>
|
||||
|
||||
<!-- Error-состояние -->
|
||||
<input class="bb-input bb-error" type="text" value="ива" />
|
||||
<p style="color:#dc2626;font-size:12px">Минимум 3 символа</p>
|
||||
|
||||
<!-- Password -->
|
||||
<input class="bb-input" type="password" placeholder="Введите пароль" />`;
|
||||
|
||||
const codeTextarea = `<label class="bb-label">Комментарий к записи</label>
|
||||
<textarea class="bb-textarea" rows="4" placeholder="Опишите симптомы..."></textarea>`;
|
||||
|
||||
const codeSelect = `<label class="bb-label">Специализация</label>
|
||||
<select class="bb-select">
|
||||
<option value="">Выберите специализацию</option>
|
||||
<option value="lor">ЛОР — ухо, горло, нос</option>
|
||||
<option value="aud">Аудиология</option>
|
||||
</select>`;
|
||||
|
||||
const codeCheckbox = `<!-- Одиночный -->
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-checkbox" type="checkbox" />
|
||||
<span>Согласен с условиями</span>
|
||||
</label>
|
||||
|
||||
<!-- Группа -->
|
||||
<div style="display:flex;flex-direction:column;gap:10px">
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-checkbox" type="checkbox" checked /> ЛОР
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-checkbox" type="checkbox" /> Аудиология
|
||||
</label>
|
||||
</div>`;
|
||||
|
||||
const codeRadio = `<div style="display:flex;flex-direction:column;gap:10px" role="radiogroup">
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-radio" type="radio" name="visit" value="first" checked />
|
||||
Первичный приём
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-radio" type="radio" name="visit" value="repeat" />
|
||||
Повторный приём
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:8px">
|
||||
<input class="bb-radio" type="radio" name="visit" value="online" />
|
||||
Онлайн-консультация
|
||||
</label>
|
||||
</div>`;
|
||||
|
||||
const codeToggle = `import { Toggle } from "@/components/ui/Toggle";
|
||||
|
||||
// Базовый тумблер
|
||||
<Toggle />
|
||||
|
||||
// С меткой
|
||||
<Toggle label="Получать уведомления" />
|
||||
|
||||
// По умолчанию включён
|
||||
<Toggle defaultChecked label="Email-рассылка" />
|
||||
|
||||
// Заблокирован
|
||||
<Toggle disabled label="Настройка недоступна" />`;
|
||||
|
||||
const codeSiteCSS = `/* ── Реальный CSS с perm.oclinica.ru ─────────────────────────── */
|
||||
|
||||
/* Базовые стили (Bootstrap override) */
|
||||
input[type=text],
|
||||
input[type=email] {
|
||||
padding: 0;
|
||||
height: 30px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* Entityform-блоки: форма записи на приём / узнать стоимость */
|
||||
#block-entityform-block-lor-form input[type=text],
|
||||
#block-entityform-block-lor-form-2 input[type=text],
|
||||
#block-entityform-block-surgery-form input[type=text] {
|
||||
height: 50px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Select в entityform */
|
||||
.field-name-field-lor-vrach .form-control.form-select {
|
||||
height: 50px;
|
||||
padding: 10px 16px;
|
||||
font-size: .9em;
|
||||
font-weight: bold;
|
||||
font-family: 'Fira Sans';
|
||||
color: #949290;
|
||||
}
|
||||
|
||||
/* Webform (отдельный вид форм) — скруглений нет */
|
||||
.webform-client-form input[type=text].form-text {
|
||||
border-radius: 0;
|
||||
}`;
|
||||
|
||||
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)" }}
|
||||
>
|
||||
Компоненты → 3.2
|
||||
</p>
|
||||
<h1 className="text-3xl font-semibold mb-3" style={{ color: "var(--bb-text)" }}>
|
||||
Форм-контролы
|
||||
</h1>
|
||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Элементы ввода данных: текстовые поля, выпадающие списки, флажки, переключатели.
|
||||
Применяются в формах записи, фильтрах и настройках.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 1. Input */}
|
||||
<Section
|
||||
id="input"
|
||||
title="Текстовое поле"
|
||||
subtitle="Базовый элемент ввода текста. Класс .bb-input. Высота 50px — как на сайте oclinica.ru."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
|
||||
<StateCard label="Default" hint="border: 1px solid #ccc · border-radius: 4px · height: 50px">
|
||||
<FieldLabel text="Имя пациента" required />
|
||||
<input className="bb-input" type="text" placeholder="Иван Иванов" readOnly />
|
||||
<FieldHint text="Укажите имя как в паспорте" />
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Focus" hint="border #7ecfca + box-shadow rgba(126,207,202,0.2)">
|
||||
<FieldLabel text="Имя пациента" required />
|
||||
<input
|
||||
className="bb-input"
|
||||
type="text"
|
||||
defaultValue="Иван"
|
||||
readOnly
|
||||
style={{
|
||||
borderColor: "var(--brand-053m)",
|
||||
boxShadow: "0 0 0 3px rgba(126,207,202,0.2)",
|
||||
}}
|
||||
/>
|
||||
<FieldHint text="Укажите имя как в паспорте" />
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Error" hint="border #dc2626 + .bb-error + сообщение об ошибке">
|
||||
<FieldLabel text="Имя пациента" required />
|
||||
<input
|
||||
className="bb-input bb-error"
|
||||
type="text"
|
||||
defaultValue="ив"
|
||||
readOnly
|
||||
/>
|
||||
<FieldError text="Минимум 3 символа" />
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Disabled" hint="opacity: 0.5 + cursor: not-allowed">
|
||||
<FieldLabel text="Email (только чтение)" />
|
||||
<input
|
||||
className="bb-input"
|
||||
type="text"
|
||||
defaultValue="ivan@example.com"
|
||||
disabled
|
||||
/>
|
||||
<FieldHint text="Email нельзя изменить" />
|
||||
</StateCard>
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div
|
||||
className="rounded-xl border p-4"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest mb-3" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Тип password
|
||||
</p>
|
||||
<div className="max-w-sm">
|
||||
<FieldLabel text="Пароль" required />
|
||||
<input className="bb-input" type="password" placeholder="Введите пароль" readOnly />
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 2. Textarea */}
|
||||
<Section
|
||||
id="textarea"
|
||||
title="Многострочный текст"
|
||||
subtitle="Поле для длинного ввода. Класс .bb-textarea. border: 1px solid #ccc · border-radius: 4px."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<StateCard label="Default" hint="min-height: 100px · resize: vertical">
|
||||
<FieldLabel text="Комментарий к записи" />
|
||||
<textarea
|
||||
className="bb-textarea"
|
||||
rows={3}
|
||||
placeholder="Опишите симптомы или пожелания..."
|
||||
readOnly
|
||||
/>
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Focus" hint="border #7ecfca + box-shadow">
|
||||
<FieldLabel text="Комментарий к записи" />
|
||||
<textarea
|
||||
className="bb-textarea"
|
||||
rows={3}
|
||||
defaultValue="Беспокоит боль в горле..."
|
||||
readOnly
|
||||
style={{
|
||||
borderColor: "var(--brand-053m)",
|
||||
boxShadow: "0 0 0 3px rgba(126,207,202,0.2)",
|
||||
}}
|
||||
/>
|
||||
</StateCard>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 3. Select */}
|
||||
<Section
|
||||
id="select"
|
||||
title="Выпадающий список"
|
||||
subtitle="Выбор из предопределённых вариантов. Класс .bb-select. Высота 50px — как в entityform на сайте."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<StateCard label="Default" hint="height: 50px · кастомная стрелка SVG">
|
||||
<FieldLabel text="Специализация" />
|
||||
<select className="bb-select">
|
||||
<option value="">Выберите специализацию</option>
|
||||
<option value="lor">ЛОР — ухо, горло, нос</option>
|
||||
<option value="aud">Аудиология</option>
|
||||
<option value="ped">ЛОР (детская)</option>
|
||||
</select>
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Выбрано значение" hint="Нативное поведение браузера">
|
||||
<FieldLabel text="Специализация" />
|
||||
<select className="bb-select" defaultValue="lor">
|
||||
<option value="">Выберите специализацию</option>
|
||||
<option value="lor">ЛОР — ухо, горло, нос</option>
|
||||
<option value="aud">Аудиология</option>
|
||||
<option value="ped">ЛОР (детская)</option>
|
||||
</select>
|
||||
</StateCard>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 4. Checkbox */}
|
||||
<Section
|
||||
id="checkbox"
|
||||
title="Флажок"
|
||||
subtitle="Независимый выбор одного или нескольких вариантов. Класс .bb-checkbox."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<StateCard label="Одиночный флажок" hint="Согласие с условиями, подтверждение">
|
||||
<div className="flex flex-col gap-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="bb-checkbox" />
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Согласен с условиями
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="bb-checkbox" defaultChecked />
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Получать уведомления
|
||||
</span>
|
||||
</label>
|
||||
<label
|
||||
className="flex items-center gap-2"
|
||||
style={{ opacity: 0.5, cursor: "not-allowed" }}
|
||||
>
|
||||
<input type="checkbox" className="bb-checkbox" disabled />
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Недоступная опция
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="Группа флажков" hint="Выбор нескольких специализаций">
|
||||
<FieldLabel text="Интересующие направления" />
|
||||
<div className="flex flex-col gap-2.5 mt-1">
|
||||
{["ЛОР — ухо, горло, нос", "Аудиология", "ЛОР (детская)", "Слухопротезирование"].map(
|
||||
(item, i) => (
|
||||
<label key={item} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="bb-checkbox"
|
||||
defaultChecked={i === 0}
|
||||
/>
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
{item}
|
||||
</span>
|
||||
</label>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</StateCard>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 5. Radio */}
|
||||
<Section
|
||||
id="radio"
|
||||
title="Переключатель варианта"
|
||||
subtitle="Выбор одного из взаимоисключающих вариантов. Класс .bb-radio."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<StateCard label="Тип приёма" hint="Один выбранный из нескольких">
|
||||
<FieldLabel text="Тип приёма" />
|
||||
<div className="flex flex-col gap-2.5 mt-1" role="radiogroup">
|
||||
{[
|
||||
{ value: "first", label: "Первичный приём", checked: true },
|
||||
{ value: "repeat", label: "Повторный приём", checked: false },
|
||||
{ value: "online", label: "Онлайн-консультация", checked: false },
|
||||
].map(({ value, label, checked }) => (
|
||||
<label key={value} className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
className="bb-radio"
|
||||
name="visit-type-demo"
|
||||
defaultChecked={checked}
|
||||
/>
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</StateCard>
|
||||
|
||||
<StateCard label="С отключённым вариантом" hint="disabled скрывает недоступный выбор">
|
||||
<FieldLabel text="Способ контакта" />
|
||||
<div className="flex flex-col gap-2.5 mt-1" role="radiogroup">
|
||||
{[
|
||||
{ value: "phone", label: "Телефон", disabled: false, checked: true },
|
||||
{ value: "sms", label: "SMS", disabled: false, checked: false },
|
||||
{ value: "whatsapp", label: "WhatsApp (скоро)", disabled: true, checked: false },
|
||||
].map(({ value, label, disabled, checked }) => (
|
||||
<label
|
||||
key={value}
|
||||
className="flex items-center gap-2"
|
||||
style={{ cursor: disabled ? "not-allowed" : "pointer", opacity: disabled ? 0.5 : 1 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
className="bb-radio"
|
||||
name="contact-demo"
|
||||
disabled={disabled}
|
||||
defaultChecked={checked}
|
||||
/>
|
||||
<span className="text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
{label}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</StateCard>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 6. Toggle */}
|
||||
<Section
|
||||
id="toggle"
|
||||
title="Тумблер"
|
||||
subtitle="Булевый переключатель «включено / выключено». Компонент Toggle."
|
||||
>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div
|
||||
className="rounded-xl border p-5"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<p className="text-sm font-medium mb-4" style={{ color: "var(--bb-text)" }}>
|
||||
Интерактивные примеры
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<Toggle label="Получать уведомления" />
|
||||
<Toggle defaultChecked label="Email-рассылка" />
|
||||
<Toggle defaultChecked label="Push-уведомления" />
|
||||
<Toggle disabled label="Недоступная настройка" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-xl border p-5"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<p className="text-sm font-medium mb-4" style={{ color: "var(--bb-text)" }}>
|
||||
Состояния
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--bb-text-muted)" }}>Выкл (default)</span>
|
||||
<Toggle />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--bb-text-muted)" }}>Вкл (defaultChecked)</span>
|
||||
<Toggle defaultChecked />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--bb-text-muted)" }}>Disabled</span>
|
||||
<Toggle disabled />
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm" style={{ color: "var(--bb-text-muted)" }}>Disabled + вкл</span>
|
||||
<Toggle disabled defaultChecked />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 7. Контекст на сайте */}
|
||||
<Section
|
||||
id="context"
|
||||
title="Контекст применения"
|
||||
subtitle="Как форм-контролы выглядят на сайте oclinica.ru — в реальных entityform-блоках."
|
||||
>
|
||||
<div className="overflow-x-auto mb-6">
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{["Контрол", "CSS класс", "Где на сайте", "CSS-блок на сайте"].map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-3 py-2 font-semibold text-xs uppercase tracking-wide"
|
||||
style={{ color: "var(--bb-text-muted)", borderBottom: "1px solid var(--bb-border)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Input (text)", ".bb-input", "Форма записи ЛОР, форма хирургии, «Узнайте стоимость»", "#block-entityform-block-lor-form input[type=text]"],
|
||||
["Select", ".bb-select", "Выбор врача в форме записи", ".field-name-field-lor-vrach .form-control.form-select"],
|
||||
["Checkbox", ".bb-checkbox", "Согласие на обработку данных в entityform", ".form-type-checkbox.checkbox label"],
|
||||
["Textarea", ".bb-textarea", "Комментарии (в ряде форм)", "Без специального CSS на сайте (Bootstrap)"],
|
||||
["Toggle", ".bb-toggle-track", "Не используется на сайте (UI-компонент брендбука)", "—"],
|
||||
].map(([ctrl, cls, where, block]) => (
|
||||
<tr key={ctrl} style={{ borderBottom: "1px solid var(--bb-border)" }}>
|
||||
<td className="px-3 py-2.5 font-medium" style={{ color: "var(--bb-text)" }}>{ctrl}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<code className="text-xs px-1.5 py-0.5 rounded" style={{ background: "var(--bb-sidebar-bg)", color: "var(--brand-073m)" }}>{cls}</code>
|
||||
</td>
|
||||
<td className="px-3 py-2.5 text-xs" style={{ color: "var(--bb-text-muted)" }}>{where}</td>
|
||||
<td className="px-3 py-2.5">
|
||||
<code className="text-xs" style={{ color: "var(--bb-text-muted)" }}>{block}</code>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Макет формы как на сайте */}
|
||||
<div className="rounded-xl overflow-hidden border" style={{ borderColor: "var(--bb-border)" }}>
|
||||
<div className="px-4 py-2 text-xs font-semibold uppercase tracking-widest" style={{ background: "var(--bb-sidebar-bg)", color: "var(--bb-text-muted)" }}>
|
||||
Макет — entityform «Узнайте стоимость операции» (oclinica.ru/lor)
|
||||
</div>
|
||||
<div style={{ background: "#b8e6ed", padding: "32px 24px" }}>
|
||||
<div
|
||||
className="mx-auto"
|
||||
style={{
|
||||
maxWidth: 340,
|
||||
background: "#b8e6ed",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<p className="text-sm font-semibold mb-4" style={{ color: "#333", fontFamily: "var(--font-web)" }}>
|
||||
Узнайте стоимость операции
|
||||
</p>
|
||||
<div className="flex flex-col gap-3" style={{ alignItems: "center" }}>
|
||||
<input
|
||||
className="bb-input"
|
||||
type="text"
|
||||
placeholder="Ваше имя"
|
||||
style={{ width: 302 }}
|
||||
readOnly
|
||||
/>
|
||||
<input
|
||||
className="bb-input"
|
||||
type="text"
|
||||
placeholder="Ваш телефон"
|
||||
style={{ width: 302 }}
|
||||
readOnly
|
||||
/>
|
||||
<select className="bb-select" style={{ width: 302 }}>
|
||||
<option>Выберите врача</option>
|
||||
<option>Иванов И.И.</option>
|
||||
</select>
|
||||
<button className="bb-btn bb-btn-lg bb-btn-primary" style={{ width: 300 }}>
|
||||
Запишите меня!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* 8. Примеры кода */}
|
||||
<Section
|
||||
id="code"
|
||||
title="Примеры кода"
|
||||
subtitle="Скопируйте HTML или JSX для использования в проекте."
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<CodeCopy lang="HTML — Input" code={codeInput} />
|
||||
<CodeCopy lang="HTML — Textarea" code={codeTextarea} />
|
||||
<CodeCopy lang="HTML — Select" code={codeSelect} />
|
||||
<CodeCopy lang="HTML — Checkbox" code={codeCheckbox} />
|
||||
<CodeCopy lang="HTML — Radio" code={codeRadio} />
|
||||
<CodeCopy lang="JSX (React) — Toggle" code={codeToggle} />
|
||||
<CodeCopy lang="CSS с сайта (perm.oclinica.ru)" code={codeSiteCSS} />
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* LLM-блок */}
|
||||
<LlmBlock path="/components/forms" version="v2.0" specText={LLM_FORMS_TEXT}>
|
||||
<LlmSection title="Элементы ввода" />
|
||||
<LlmTable
|
||||
headers={["Элемент", "CSS класс", "Тег", "Высота", "Описание"]}
|
||||
rows={[
|
||||
["Input", ".bb-input", "<input>", "50px", "Текстовое поле, email, password · как на сайте"],
|
||||
["Textarea", ".bb-textarea", "<textarea>", "≥100px", "Многострочный ввод, resize:vertical"],
|
||||
["Select", ".bb-select", "<select>", "50px", "Выбор из списка, кастомная стрелка · как на сайте"],
|
||||
["Checkbox", ".bb-checkbox", "<input type=checkbox>", "16×16px", "Независимый выбор"],
|
||||
["Radio", ".bb-radio", "<input type=radio>", "16×16px", "Выбор одного из группы"],
|
||||
["Toggle", ".bb-toggle-track", "React-компонент", "24px", "Булев переключатель"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Состояния полей (Input / Textarea / Select)" />
|
||||
<LlmTable
|
||||
headers={["Состояние", "Стиль"]}
|
||||
rows={[
|
||||
["default", "border: 1px solid #ccc · border-radius: 4px"],
|
||||
["focus", "border: #7ecfca + box-shadow: 0 0 0 3px rgba(126,207,202,0.2)"],
|
||||
["error", "border: #dc2626 (+ класс .bb-error)"],
|
||||
["disabled", "opacity: 0.5 + cursor: not-allowed + bg: #f8f9fa"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Параметры Toggle" />
|
||||
<LlmTable
|
||||
headers={["Параметр", "Тип", "По умолч.", "Описание"]}
|
||||
rows={[
|
||||
["defaultChecked", "boolean", "false", "Начальное состояние"],
|
||||
["disabled", "boolean", "false", "Блокирует взаимодействие"],
|
||||
["label", "string", "—", "Текстовая метка справа от тумблера"],
|
||||
["onChange", "(checked: boolean) => void", "—", "Колбэк при изменении"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Правила применения" />
|
||||
<LlmRules
|
||||
rules={[
|
||||
{ ok: true, text: "Метка (label) всегда над полем, font-weight: 500" },
|
||||
{ ok: true, text: "Обязательные поля помечены * красным (#dc2626)" },
|
||||
{ ok: true, text: "Hint-текст серым под полем (font-size: 12px)" },
|
||||
{ ok: true, text: "Ошибка — красный текст вместо hint" },
|
||||
{ ok: true, text: "Checkbox-группы — вертикальный список с gap: 10px" },
|
||||
{ ok: true, text: "Toggle — для булевых настроек включить/выключить" },
|
||||
{ ok: false, text: "Не использовать placeholder вместо label" },
|
||||
{ ok: false, text: "Не скрывать признак обязательности поля" },
|
||||
]}
|
||||
/>
|
||||
</LlmBlock>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
/* ─── Утилиты конвертации ──────────────────────────────────────────── */
|
||||
@@ -102,6 +103,87 @@ const BRAND_COLORS = [
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Соответствие цветов ──────────────────────────────────────────── */
|
||||
const COLOR_MAPPING = [
|
||||
{
|
||||
brand: { oracal: "053M", name: "Основной бирюзовый", hex: "#7ecfca" },
|
||||
web: { name: "Бирюзовый", hex: "#63bac3", count: 4 },
|
||||
note: "Чуть темнее и насыщеннее на сайте",
|
||||
},
|
||||
{
|
||||
brand: { oracal: "073M", name: "Тёмный серо-голубой", hex: "#5b7b87" },
|
||||
web: { name: "Серо-бирюзовый", hex: "#60959c", count: 7 },
|
||||
note: "Светлее на сайте",
|
||||
},
|
||||
{
|
||||
brand: { oracal: "066M", name: "Средний бирюзовый", hex: "#5bb5ad" },
|
||||
web: { name: "Бирюзовый средний", hex: "#52b4bd", count: 4 },
|
||||
note: "Смещён в синеву",
|
||||
},
|
||||
{
|
||||
brand: { oracal: "081M", name: "Бежевый", hex: "#c4a882" },
|
||||
web: { name: "Бежевый", hex: "#bf9975", count: 12 },
|
||||
note: "Темнее и насыщеннее на сайте",
|
||||
},
|
||||
{
|
||||
brand: { oracal: "050M", name: "Тёмно-синий", hex: "#1b4c72" },
|
||||
web: null,
|
||||
note: "Не найден в CSS сайта",
|
||||
},
|
||||
{
|
||||
brand: { oracal: "080M", name: "Тёмно-коричневый", hex: "#5c2e0e" },
|
||||
web: null,
|
||||
note: "Не найден в CSS сайта",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Основной текст", hex: "#464646", count: 3 },
|
||||
note: "Только на сайте — нет Oracal-аналога",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Второстепенный текст", hex: "#949290", count: 4 },
|
||||
note: "Только на сайте — нет Oracal-аналога",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Кремовый фон", hex: "#e9e4d4", count: 1 },
|
||||
note: "Только на сайте — нет Oracal-аналога",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Коралловый", hex: "#ffa39c", count: 2 },
|
||||
note: "Только на сайте — CTA-кнопки, нет в оффлайн-палитре",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Светло-жёлтый фон", hex: "#eef4d1", count: 1 },
|
||||
note: "Только на сайте — фон карточек отзывов",
|
||||
},
|
||||
{
|
||||
brand: null,
|
||||
web: { name: "Светло-зелёный фон", hex: "#f2fee6", count: 1 },
|
||||
note: "Только на сайте — фон секции новостей",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Цвета с сайта ────────────────────────────────────────────────── */
|
||||
// Источник: https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css
|
||||
// Извлечены парсингом CSS: grep + python Counter по property/value, 2026-03-22
|
||||
const WEB_COLORS = [
|
||||
{ name: "Бежевый", hex: "#bf9975", usage: "Основной тёплый акцент, фоны, рамки, текст", count: 12, group: "Акценты" },
|
||||
{ name: "Серо-бирюзовый", hex: "#60959c", usage: "Основной холодный акцент, ссылки", count: 7, group: "Акценты" },
|
||||
{ name: "Бирюзовый", hex: "#63bac3", usage: "Фоны акцентных блоков, иконки", count: 4, group: "Акценты" },
|
||||
{ name: "Бирюзовый средний", hex: "#52b4bd", usage: "Вторичные цветовые акценты", count: 4, group: "Акценты" },
|
||||
{ name: "Основной текст", hex: "#464646", usage: "Цвет основного текста на сайте", count: 3, group: "Текст" },
|
||||
{ name: "Второстепенный текст", hex: "#949290", usage: "Подписи, второстепенный контент", count: 4, group: "Текст" },
|
||||
{ name: "Светло-бирюзовый фон", hex: "#b8e6ed", usage: "Фоны светлых секций с акцентом", count: 1, group: "Фоны" },
|
||||
{ name: "Кремовый фон", hex: "#e9e4d4", usage: "Тёплые фоны секций", count: 1, group: "Фоны" },
|
||||
{ name: "Коралловый", hex: "#ffa39c", usage: "CTA-кнопки («Запишите меня!»), акцентные призывы к действию", count: 2, group: "Акценты" },
|
||||
{ name: "Светло-жёлтый фон", hex: "#eef4d1", usage: "Фон карточек отзывов (секция «Отзывы о нас»)", count: 1, group: "Фоны" },
|
||||
{ name: "Светло-зелёный фон", hex: "#f2fee6", usage: "Фон секции новостей", count: 1, group: "Фоны" },
|
||||
];
|
||||
|
||||
const CONTRAST_PAIRS = [
|
||||
{ fg: "#ffffff", bg: "#5b7b87", label: "Белый на тёмном серо-голубом" },
|
||||
{ fg: "#ffffff", bg: "#1b4c72", label: "Белый на тёмно-синем" },
|
||||
@@ -245,6 +327,56 @@ function ContrastRow({ pair }: { pair: typeof CONTRAST_PAIRS[0] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function WebColorCard({ color }: { color: typeof WEB_COLORS[0] }) {
|
||||
const { r, g, b } = hexToRgb(color.hex);
|
||||
const { h, s, l } = rgbToHsl(r, g, b);
|
||||
const isLight = l > 60;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<div
|
||||
className="h-24 flex items-end justify-between px-4 pb-3"
|
||||
style={{ background: color.hex }}
|
||||
>
|
||||
<span
|
||||
className="text-xs font-semibold px-2 py-0.5 rounded"
|
||||
style={{
|
||||
background: isLight ? "rgba(0,0,0,0.12)" : "rgba(255,255,255,0.18)",
|
||||
color: isLight ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)",
|
||||
}}
|
||||
>
|
||||
{color.group}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs px-2 py-0.5 rounded font-mono"
|
||||
style={{
|
||||
background: isLight ? "rgba(0,0,0,0.12)" : "rgba(255,255,255,0.18)",
|
||||
color: isLight ? "rgba(0,0,0,0.75)" : "rgba(255,255,255,0.9)",
|
||||
}}
|
||||
>
|
||||
×{color.count}
|
||||
</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" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Экспорт токенов ──────────────────────────────────────────────── */
|
||||
function exportTokens() {
|
||||
const tokens: Record<string, Record<string, unknown>> = { colors: {} };
|
||||
@@ -268,6 +400,62 @@ function exportTokens() {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/* ─── LLM spec text ────────────────────────────────────────────────── */
|
||||
const LLM_COLORS_TEXT = `# ЦВЕТА — LLM-СПЕЦИФИКАЦИЯ
|
||||
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||
# docs/LLM_CONTEXT.md · /foundation/colors · v2.1 · 2026-03-22
|
||||
|
||||
ФИРМЕННЫЕ ЦВЕТА (Oracal)
|
||||
Oracal | Название | HEX | RGB | CSS-переменная | Применение
|
||||
053M | Основной бирюзовый | #7ECFCA | rgb(126,207,202) | --brand-053m | Акцент, CTA-кнопки, иконки, активные состояния
|
||||
073M | Тёмный серо-голубой | #5B7B87 | rgb(91,123,135) | --brand-073m | Тёмный фон, хедер, заголовки навигации
|
||||
066M | Средний бирюзовый | #5BB5AD | rgb(91,181,173) | --brand-066m | Вторичные акценты, фоны секций
|
||||
050M | Тёмно-синий | #1B4C72 | rgb(27,76,114) | --brand-050m | Наружная реклама, полиграфия, заголовки
|
||||
081M | Бежевый | #C4A882 | rgb(196,168,130) | --brand-081m | Форма сотрудников, тёплые акценты
|
||||
080M | Тёмно-коричневый | #5C2E0E | rgb(92,46,14) | --brand-080m | Текст на бежевом, логотип на форме
|
||||
— | Белый | #FFFFFF | rgb(255,255,255) | --brand-white | Фон, инвертированный текст, логотип на тёмных
|
||||
|
||||
ЦВЕТА САЙТА oclinica.ru (CSS: clinic_bootstrap_mobile/css/style.css)
|
||||
Название | HEX | Группа | × | Применение
|
||||
Бежевый | #BF9975 | Акценты | 12| Тёплый акцент, фоны, рамки, текст
|
||||
Серо-бирюзовый | #60959C | Акценты | 7 | Холодный акцент, ссылки
|
||||
Бирюзовый | #63BAC3 | Акценты | 4 | Фоны акцентных блоков, иконки
|
||||
Бирюзовый средний | #52B4BD | Акценты | 4 | Вторичные цветовые акценты
|
||||
Коралловый | #FFA39C | Акценты | 2 | CTA-кнопки («Запишите меня!»)
|
||||
Основной текст | #464646 | Текст | 3 | Цвет основного текста сайта
|
||||
Второстепенный текст | #949290 | Текст | 4 | Подписи, второстепенный контент
|
||||
Светло-бирюзовый фон | #B8E6ED | Фоны | 1 | Фоны светлых секций
|
||||
Кремовый фон | #E9E4D4 | Фоны | 1 | Тёплые фоны секций
|
||||
Светло-жёлтый фон | #EEF4D1 | Фоны | 1 | Фон карточек отзывов
|
||||
Светло-зелёный фон | #F2FEE6 | Фоны | 1 | Фон секции новостей
|
||||
|
||||
СООТВЕТСТВИЕ ORACAL → САЙТ
|
||||
053M #7ECFCA → #63BAC3 (темнее, насыщеннее)
|
||||
073M #5B7B87 → #60959C (светлее)
|
||||
066M #5BB5AD → #52B4BD (смещён в синеву)
|
||||
081M #C4A882 → #BF9975 (темнее, насыщеннее)
|
||||
050M #1B4C72 → не найден в CSS сайта
|
||||
080M #5C2E0E → не найден в CSS сайта
|
||||
|
||||
КОНТРАСТНОСТЬ WCAG 2.1
|
||||
#FFFFFF / #5B7B87 | 4.6:1 | AA PASS | AAA FAIL
|
||||
#FFFFFF / #1B4C72 | 9.3:1 | AA PASS | AAA PASS
|
||||
#FFFFFF / #5C2E0E | 11.2:1 | AA PASS | AAA PASS
|
||||
#FFFFFF / #5BB5AD | 3.0:1 | AA FAIL | AAA FAIL | только крупный текст ≥18pt
|
||||
#111827 / #7ECFCA | 5.8:1 | AA PASS | AAA FAIL
|
||||
#111827 / #C4A882 | 4.8:1 | AA PASS | AAA FAIL
|
||||
#5C2E0E / #C4A882 | 6.7:1 | AA PASS | AAA FAIL
|
||||
|
||||
ПРАВИЛА
|
||||
✓ Только цвета из фирменной палитры
|
||||
✓ Digital → цвета сайта; оффлайн → коды Oracal
|
||||
✓ Текст на цветном фоне: минимум WCAG AA (4.5:1)
|
||||
✓ Белый текст на: 073M (#5B7B87), 050M (#1B4C72), 080M (#5C2E0E)
|
||||
✓ Тёмный текст на: 053M (#7ECFCA), 081M (#C4A882)
|
||||
✕ Произвольные цвета вне фирменной палитры
|
||||
✕ Изменение насыщенности / оттенка фирменных цветов
|
||||
✕ Тёплые и холодные акценты рядом без нейтрального разделителя`.trim();
|
||||
|
||||
/* ─── Страница ─────────────────────────────────────────────────────── */
|
||||
export default function ColorsPage() {
|
||||
return (
|
||||
@@ -348,8 +536,120 @@ export default function ColorsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Применение */}
|
||||
{/* 3. Цвета с сайта */}
|
||||
<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)" }}>
|
||||
Реальные цвета, используемые на сайте oclinica.ru. Извлечены из CSS темы сайта.
|
||||
Число применений показано в правом углу карточки.
|
||||
</p>
|
||||
<div
|
||||
className="mt-3 flex items-center gap-2 px-3 py-2 rounded-lg text-xs w-fit font-mono"
|
||||
style={{ background: "var(--bb-sidebar-bg)", color: "var(--bb-text-muted)", border: "1px solid var(--bb-border)" }}
|
||||
>
|
||||
<span style={{ color: "var(--brand-053m)" }}>CSS</span>
|
||||
perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{WEB_COLORS.map(c => (
|
||||
<WebColorCard key={c.hex} color={c} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. Соответствие */}
|
||||
<section className="mb-12">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Соответствие: Oracal → Сайт
|
||||
</h2>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Сравнение фирменной палитры (Oracal, брендбук) с реальными цветами, применёнными на сайте.
|
||||
Расхождения ожидаемы — цифровая адаптация плёночных цветов под экран.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
{COLOR_MAPPING.map((row, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center gap-3 p-4 rounded-xl border"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-content-bg)" }}
|
||||
>
|
||||
{/* Левая сторона — Oracal */}
|
||||
<div className="flex items-center gap-3 w-56 shrink-0">
|
||||
{row.brand ? (
|
||||
<>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg shrink-0 border"
|
||||
style={{ background: row.brand.hex, borderColor: "var(--bb-border)" }}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs font-semibold font-mono" style={{ color: "var(--bb-text)" }}>
|
||||
{row.brand.hex.toUpperCase()}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Oracal {row.brand.oracal}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg shrink-0 flex items-center justify-center text-lg border"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
—
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Стрелка */}
|
||||
<div className="shrink-0 text-sm font-bold" style={{ color: "var(--bb-text-muted)" }}>→</div>
|
||||
|
||||
{/* Правая сторона — Сайт */}
|
||||
<div className="flex items-center gap-3 w-56 shrink-0">
|
||||
{row.web ? (
|
||||
<>
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg shrink-0 border"
|
||||
style={{ background: row.web.hex, borderColor: "var(--bb-border)" }}
|
||||
/>
|
||||
<div>
|
||||
<p className="text-xs font-semibold font-mono" style={{ color: "var(--bb-text)" }}>
|
||||
{row.web.hex.toUpperCase()}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{row.web.name} · ×{row.web.count}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg shrink-0 flex items-center justify-center text-lg border"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
—
|
||||
</div>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>Нет на сайте</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Примечание */}
|
||||
<p className="flex-1 text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{row.note}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. Применение */}
|
||||
<section className="mb-10">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
Правила применения
|
||||
@@ -410,6 +710,105 @@ export default function ColorsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. LLM-спецификация */}
|
||||
<section className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
LLM-спецификация
|
||||
</h2>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Машиночитаемые данные раздела для использования AI-ассистентами при разработке
|
||||
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст.
|
||||
</p>
|
||||
</div>
|
||||
<LlmBlock
|
||||
path="/foundation/colors"
|
||||
version="v2.1"
|
||||
specText={LLM_COLORS_TEXT}
|
||||
>
|
||||
{/* Фирменные цвета */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Фирменные цвета (Oracal)" />
|
||||
<LlmTable
|
||||
headers={["Oracal", "HEX", "RGB", "CSS-переменная", "Применение"]}
|
||||
rows={BRAND_COLORS.map(c => {
|
||||
const { r, g, b } = hexToRgb(c.hex);
|
||||
return [
|
||||
<span key="o" className="flex items-center gap-1.5">
|
||||
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} />
|
||||
{c.oracal}
|
||||
</span>,
|
||||
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>,
|
||||
`rgb(${r},${g},${b})`,
|
||||
<span key="v" style={{ color: "var(--brand-073m)" }}>{c.cssVar}</span>,
|
||||
c.usage,
|
||||
];
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Цвета сайта */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Цвета сайта oclinica.ru" />
|
||||
<LlmTable
|
||||
headers={["Название", "HEX", "Группа", "×", "Применение"]}
|
||||
rows={WEB_COLORS.map(c => [
|
||||
<span key="n" className="flex items-center gap-1.5">
|
||||
<span className="w-3 h-3 rounded-sm shrink-0 border inline-block" style={{ background: c.hex, borderColor: "var(--bb-border)" }} />
|
||||
{c.name}
|
||||
</span>,
|
||||
<span key="h" style={{ color: "var(--bb-text)" }}>{c.hex.toUpperCase()}</span>,
|
||||
c.group,
|
||||
String(c.count),
|
||||
c.usage,
|
||||
])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Контрастность */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Контрастность WCAG 2.1" />
|
||||
<LlmTable
|
||||
headers={["Пара (fg / bg)", "Ratio", "AA (4.5:1)", "AAA (7:1)", "AA large (3:1)"]}
|
||||
rows={CONTRAST_PAIRS.map(p => {
|
||||
const ratio = contrastRatio(p.fg, p.bg);
|
||||
const aa = ratio >= 4.5, aaa = ratio >= 7, aal = ratio >= 3;
|
||||
const badge = (pass: boolean) => (
|
||||
<span key={String(pass)} style={{ color: pass ? "#059669" : "#dc2626", fontWeight: 700 }}>
|
||||
{pass ? "PASS" : "FAIL"}
|
||||
</span>
|
||||
);
|
||||
return [
|
||||
<span key="pair" className="flex items-center gap-1.5">
|
||||
<span className="w-3 h-3 rounded-sm border inline-block shrink-0" style={{ background: p.bg, borderColor: "var(--bb-border)" }} />
|
||||
{p.fg} / {p.bg}
|
||||
</span>,
|
||||
`${ratio}:1`,
|
||||
badge(aa),
|
||||
badge(aaa),
|
||||
badge(aal),
|
||||
];
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правила */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Правила применения" />
|
||||
<LlmRules rules={[
|
||||
{ ok: true, text: "Только цвета из фирменной палитры" },
|
||||
{ ok: true, text: "Digital → цвета сайта; оффлайн → коды Oracal" },
|
||||
{ ok: true, text: "Текст на цветном фоне: минимум WCAG AA (4.5:1)" },
|
||||
{ ok: true, text: "Белый текст на: 073M, 050M, 080M" },
|
||||
{ ok: true, text: "Тёмный текст на: 053M, 081M" },
|
||||
{ ok: false, text: "Произвольные цвета вне фирменной палитры" },
|
||||
{ ok: false, text: "Изменение насыщенности / оттенка фирменных цветов" },
|
||||
{ ok: false, text: "Тёплые + холодные акценты рядом без разделителя" },
|
||||
]} />
|
||||
</div>
|
||||
</LlmBlock>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Логотип | Брендбук О!Клиника",
|
||||
title: "Логотип. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function RuleTag({ children }: { children: React.ReactNode }) {
|
||||
@@ -102,6 +103,44 @@ function ProhibitedItem({ label }: { label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const LLM_LOGO_TEXT = `ЛОГОТИП — LLM-спецификация
|
||||
Версия: v1.0 · /foundation/logo
|
||||
|
||||
ФАЙЛЫ
|
||||
PNG с прозрачным фоном: /public/logo/logo-transparent.png
|
||||
SVG/AI: ожидается от дизайнера
|
||||
|
||||
СИМВОЛИКА ЗНАКА
|
||||
Три округлых элемента с равной дистанцией от центра.
|
||||
- Незамкнутая симметрия: символ развития и жизни, а не завершённости
|
||||
- Три элемента: структура ухо-горло-нос, триада равновесия
|
||||
- Отсутствие замкнутой формы: открытость, доступность, человечность
|
||||
|
||||
ЦВЕТОВЫЕ ВАРИАНТЫ
|
||||
Вариант | Фон | CSS-фильтр | Применение
|
||||
Основной | Светлый (#f8f9fa) | нет | Сайт, полиграфия на белом
|
||||
Инвертированный | #5b7b87 (073M) | brightness(0) invert(1) | Хедер, тёмные секции
|
||||
На форме (беж) | #c4a882 (081M) | brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45) | Форма сотрудников (бежевый)
|
||||
На форме (синий)| #1b4c72 (050M) | brightness(0) invert(1) | Форма сотрудников (синий)
|
||||
|
||||
ОХРАННАЯ ЗОНА
|
||||
Минимальный отступ со всех сторон = высота буквы «К» в слове «КЛИНИКА»
|
||||
|
||||
РАЗМЕРЫ НА ФОРМЕ СОТРУДНИКОВ
|
||||
До 46 р. | 70 мм × 25,5 мм | Левая сторона груди
|
||||
От 48 р. | 90 мм × 32,8 мм | Левая сторона груди
|
||||
|
||||
ПРАВИЛА
|
||||
✓ Применять только одобренные цветовые варианты
|
||||
✓ Соблюдать охранную зону
|
||||
✓ Использовать PNG с прозрачным фоном для digital
|
||||
✓ Белый логотип (invert) на тёмных фонах (073M, 050M)
|
||||
✕ Не изменять пропорции или искажать логотип
|
||||
✕ Не изменять цвета элементов логотипа
|
||||
✕ Не добавлять рядом произвольный текст
|
||||
✕ Не размещать на фоне без достаточного контраста
|
||||
✕ Не применять тени, обводки, градиенты`.trim();
|
||||
|
||||
export default function LogoPage() {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto px-8 py-10">
|
||||
@@ -390,6 +429,55 @@ export default function LogoPage() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* LLM-блок */}
|
||||
<LlmBlock
|
||||
path="/foundation/logo"
|
||||
version="v1.0"
|
||||
specText={LLM_LOGO_TEXT}
|
||||
>
|
||||
<LlmSection title="Файлы логотипа" />
|
||||
<LlmTable
|
||||
headers={["Файл", "Формат", "Путь", "Статус"]}
|
||||
rows={[
|
||||
["logo-transparent.png", "PNG с прозрачным фоном", "/public/logo/logo-transparent.png", "✓ Доступен"],
|
||||
["logo.svg / logo.ai", "Вектор", "—", "Ожидается от дизайнера"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Цветовые варианты" />
|
||||
<LlmTable
|
||||
headers={["Вариант", "Фон", "CSS-фильтр", "Применение"]}
|
||||
rows={[
|
||||
["Основной", "Светлый (#f8f9fa)", "нет", "Сайт, полиграфия на белом"],
|
||||
["Инвертированный", "#5b7b87 (073M)", "brightness(0) invert(1)", "Хедер, тёмные секции"],
|
||||
["На форме (беж)", "#c4a882 (081M)", "brightness(0) sepia(1) saturate(2) hue-rotate(330deg) brightness(0.45)", "Форма сотрудников (бежевый костюм)"],
|
||||
["На форме (синий)", "#1b4c72 (050M)", "brightness(0) invert(1)", "Форма сотрудников (синий костюм)"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Охранная зона и размеры на форме" />
|
||||
<LlmTable
|
||||
headers={["Носитель", "Условие", "Длина", "Высота", "Расположение"]}
|
||||
rows={[
|
||||
["Форма сотрудников", "Размер до 46 р.", "70 мм", "25,5 мм", "Левая сторона груди"],
|
||||
["Форма сотрудников", "Размер от 48 р.", "90 мм", "32,8 мм", "Левая сторона груди"],
|
||||
["Охранная зона", "Все носители", "≥ высота буквы «К»", "≥ высота буквы «К»", "Вокруг логотипа со всех сторон"],
|
||||
]}
|
||||
/>
|
||||
<LlmSection title="Правила использования" />
|
||||
<LlmRules
|
||||
rules={[
|
||||
{ ok: true, text: "Применять только одобренные цветовые варианты" },
|
||||
{ ok: true, text: "Соблюдать охранную зону (≥ высота буквы «К»)" },
|
||||
{ ok: true, text: "Использовать PNG с прозрачным фоном для digital" },
|
||||
{ ok: true, text: "Белый логотип на тёмных фонах (073M, 050M, 080M)" },
|
||||
{ ok: false, text: "Не изменять пропорции или искажать логотип" },
|
||||
{ ok: false, text: "Не изменять цвета элементов логотипа" },
|
||||
{ ok: false, text: "Не добавлять рядом произвольный текст" },
|
||||
{ ok: false, text: "Не размещать на фоне без достаточного контраста" },
|
||||
{ ok: false, text: "Не применять тени, обводки, градиенты" },
|
||||
]}
|
||||
/>
|
||||
</LlmBlock>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { LlmBlock, LlmSection, LlmTable, LlmRules } from "@/components/llm/LlmBlock";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Типографика | Брендбук О!Клиника",
|
||||
title: "Типографика. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
/* ─── Шкала типографики ────────────────────────────────────────────── */
|
||||
@@ -28,6 +29,60 @@ const FIRA_SCALE = [
|
||||
{ token: "overline", size: "10px / 0.625rem",weight: "600", lh: "1.4", sample: "ФУНДАМЕНТ → 1.4" },
|
||||
];
|
||||
|
||||
/* ─── LLM spec text ────────────────────────────────────────────────── */
|
||||
const LLM_TYPOGRAPHY_TEXT = `# ТИПОГРАФИКА — LLM-СПЕЦИФИКАЦИЯ
|
||||
# Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||
# docs/LLM_CONTEXT.md · /foundation/typography · v2.0 · 2026-03-22
|
||||
|
||||
ШРИФТЫ БРЕНДА
|
||||
Шрифт | Тип | Применение | CSS
|
||||
DINPro | Бренд | Оффлайн, физические носители (бейджи, таблички, транспорт, форма) | font-family: 'DINPro', Arial, sans-serif
|
||||
Fira Sans| Веб | Сайт, цифровые материалы, брендбук | font-family: 'Fira Sans', sans-serif; Google Fonts; weights: 300/400/500/600
|
||||
|
||||
ПРАВИЛО ВЫБОРА: носитель цифровой → Fira Sans; физический/печатный → DINPro
|
||||
|
||||
DINPro — ШКАЛА (оффлайн)
|
||||
Стиль | font-size | rem | weight | line-height
|
||||
h1 | 40px | 2.5rem | 700 | 1.20
|
||||
h2 | 32px | 2rem | 700 | 1.25
|
||||
h3 | 24px | 1.5rem | 700 | 1.30
|
||||
h4 | 20px | 1.25rem | 700 | 1.35
|
||||
h5 | 16px | 1rem | 700 | 1.40
|
||||
h6 | 14px | 0.875rem | 700 | 1.40
|
||||
|
||||
Fira Sans — ШКАЛА (веб)
|
||||
Стиль | font-size | rem | weight | line-height | letter-spacing
|
||||
h1 | 40px | 2.5rem | 600 | 1.20 | -0.025em
|
||||
h2 | 32px | 2rem | 600 | 1.25 | -0.020em
|
||||
h3 | 24px | 1.5rem | 600 | 1.30 | -0.010em
|
||||
h4 | 20px | 1.25rem | 500 | 1.35 | 0em
|
||||
h5 | 16px | 1rem | 500 | 1.40 | 0em
|
||||
h6 | 14px | 0.875rem | 500 | 1.40 | +0.010em
|
||||
body | 16px | 1rem | 400 | 1.60 | 0em
|
||||
body-sm | 14px | 0.875rem | 400 | 1.60 | 0em
|
||||
caption | 12px | 0.75rem | 400 | 1.50 | +0.020em
|
||||
label | 12px | 0.75rem | 500 | 1.40 | +0.030em
|
||||
overline | 10px | 0.625rem | 600 | 1.40 | +0.100em (uppercase)
|
||||
|
||||
ПРИМЕНЕНИЕ ПО НОСИТЕЛЮ
|
||||
Носитель | Шрифт
|
||||
Сайт, цифровые материалы, брендбук | Fira Sans
|
||||
Форма сотрудников, бейджи | DINPro
|
||||
Вывески, таблички, навигация | DINPro
|
||||
Брендирование транспорта | DINPro
|
||||
Визитки, листовки, полиграфия | DINPro
|
||||
Telegram-бот, пуш-уведомления | Fira Sans (системный)
|
||||
|
||||
ПРАВИЛА
|
||||
✓ H1 — только один на странице
|
||||
✓ Не пропускать уровни заголовков (h1 → h2 → h3)
|
||||
✓ Минимальный размер текста на экране: 12px
|
||||
✓ Кириллица → Fira Sans (не DINPro)
|
||||
✓ Fira Sans: доступные веса 300 / 400 / 500 / 600
|
||||
✕ DINPro на сайте без явного согласования дизайнера
|
||||
✕ Light (300) для текста < 14px
|
||||
✕ Смешивать DINPro и Fira Sans на одном носителе`.trim();
|
||||
|
||||
/* ─── Компоненты ───────────────────────────────────────────────────── */
|
||||
function Section({
|
||||
title,
|
||||
@@ -344,6 +399,110 @@ export default function TypographyPage() {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* LLM-спецификация */}
|
||||
<section className="mb-8">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold" style={{ color: "var(--bb-text)" }}>
|
||||
LLM-спецификация
|
||||
</h2>
|
||||
<p className="mt-1 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Машиночитаемые данные раздела для использования AI-ассистентами при разработке
|
||||
дизайна, макетов и кода. Нажмите «Скопировать» чтобы получить полный текст.
|
||||
</p>
|
||||
</div>
|
||||
<LlmBlock
|
||||
path="/foundation/typography"
|
||||
version="v2.0"
|
||||
specText={LLM_TYPOGRAPHY_TEXT}
|
||||
>
|
||||
{/* Шрифты */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Шрифты бренда" />
|
||||
<LlmTable
|
||||
headers={["Шрифт", "Тип", "Применение", "CSS font-family"]}
|
||||
rows={[
|
||||
[
|
||||
<span key="d" style={{ color: "var(--bb-text)", fontWeight: 600 }}>DINPro</span>,
|
||||
"Бренд",
|
||||
"Оффлайн, физические носители (бейджи, таблички, транспорт, форма)",
|
||||
"'DINPro', Arial, sans-serif",
|
||||
],
|
||||
[
|
||||
<span key="f" style={{ color: "var(--bb-text)", fontWeight: 600 }}>Fira Sans</span>,
|
||||
"Веб",
|
||||
"Сайт, цифровые материалы, брендбук",
|
||||
"'Fira Sans', sans-serif · Google Fonts · 300/400/500/600",
|
||||
],
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* DINPro шкала */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="DINPro — шкала (оффлайн)" />
|
||||
<LlmTable
|
||||
headers={["Стиль", "font-size", "rem", "weight", "line-height"]}
|
||||
rows={DIN_SCALE.map(r => {
|
||||
const [px, rem] = r.size.split(" / ");
|
||||
return [r.token, px, rem, r.weight, r.lh];
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Fira Sans шкала */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Fira Sans — шкала (веб)" />
|
||||
<LlmTable
|
||||
headers={["Стиль", "font-size", "rem", "weight", "line-height", "letter-spacing"]}
|
||||
rows={[
|
||||
["h1", "40px", "2.5rem", "600", "1.20", "-0.025em"],
|
||||
["h2", "32px", "2rem", "600", "1.25", "-0.020em"],
|
||||
["h3", "24px", "1.5rem", "600", "1.30", "-0.010em"],
|
||||
["h4", "20px", "1.25rem", "500", "1.35", "0em"],
|
||||
["h5", "16px", "1rem", "500", "1.40", "0em"],
|
||||
["h6", "14px", "0.875rem", "500", "1.40", "+0.010em"],
|
||||
["body", "16px", "1rem", "400", "1.60", "0em"],
|
||||
["body-sm", "14px", "0.875rem", "400", "1.60", "0em"],
|
||||
["caption", "12px", "0.75rem", "400", "1.50", "+0.020em"],
|
||||
["label", "12px", "0.75rem", "500", "1.40", "+0.030em"],
|
||||
["overline", "10px", "0.625rem", "600", "1.40", "+0.100em"],
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Применение */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Применение по носителю" />
|
||||
<LlmTable
|
||||
headers={["Носитель", "Шрифт"]}
|
||||
rows={[
|
||||
["Сайт, цифровые материалы, брендбук", <span key="fs" style={{ color: "var(--brand-073m)" }}>Fira Sans</span>],
|
||||
["Форма сотрудников, бейджи", <span key="d1" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
|
||||
["Вывески, таблички, навигация", <span key="d2" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
|
||||
["Брендирование транспорта", <span key="d3" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
|
||||
["Визитки, листовки, полиграфия", <span key="d4" style={{ color: "var(--brand-073m)" }}>DINPro</span>],
|
||||
["Telegram-бот, пуш-уведомления", <span key="fs2" style={{ color: "var(--brand-073m)" }}>Fira Sans (системный)</span>],
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Правила */}
|
||||
<div className="space-y-2">
|
||||
<LlmSection title="Правила применения" />
|
||||
<LlmRules rules={[
|
||||
{ ok: true, text: "H1 — только один на странице" },
|
||||
{ ok: true, text: "Не пропускать уровни заголовков (h1 → h2 → h3)" },
|
||||
{ ok: true, text: "Минимальный размер текста на экране: 12px" },
|
||||
{ ok: true, text: "Кириллица → Fira Sans (не DINPro)" },
|
||||
{ ok: true, text: "Fira Sans: доступные веса 300 / 400 / 500 / 600" },
|
||||
{ ok: false, text: "DINPro на сайте без явного согласования дизайнера" },
|
||||
{ ok: false, text: "Light (300) для текста < 14px" },
|
||||
{ ok: false, text: "Смешивать DINPro и Fira Sans на одном носителе" },
|
||||
]} />
|
||||
</div>
|
||||
</LlmBlock>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,3 +42,143 @@ body {
|
||||
color: var(--bb-text);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ─── Анимации ───────────────────────────────────────────────── */
|
||||
@keyframes bb-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ─── Кнопки (Sprint 3) ──────────────────────────────────────── */
|
||||
.bb-btn {
|
||||
font-family: var(--font-web);
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s, opacity 0.15s;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.bb-btn:hover:not(:disabled) { filter: brightness(0.9); }
|
||||
.bb-btn:active:not(:disabled) { filter: brightness(0.82); }
|
||||
.bb-btn:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||
.bb-btn:focus-visible { outline: 2px solid var(--brand-053m); outline-offset: 2px; }
|
||||
|
||||
/* Размеры — только padding и font-size, radius задаётся вариантом */
|
||||
.bb-btn-sm { font-size: 13px; padding: 4px 11px; border: 1px solid transparent; }
|
||||
.bb-btn-md { font-size: 14px; padding: 8px 16px; border: 1px solid transparent; }
|
||||
.bb-btn-lg { font-size: 18px; padding: 10px 24px; border: 1px solid transparent; font-weight: bold; }
|
||||
|
||||
/* Варианты — цвета и радиус по реальному сайту oclinica.ru */
|
||||
|
||||
/* primary — коралловая форм-кнопка «Запишите меня!» — #FFA39C */
|
||||
.bb-btn-primary {
|
||||
background: #FFA39C;
|
||||
color: #fff;
|
||||
border-color: #FF847B;
|
||||
border-radius: 7px;
|
||||
font-weight: bold;
|
||||
box-shadow: 0px 0px 5px rgba(0,0,0,0.4), 0px 3px 5px rgba(0,0,0,0.25);
|
||||
}
|
||||
|
||||
/* outline — белая с бежевой рамкой «Записаться на прием» */
|
||||
.bb-btn-outline {
|
||||
background: #fff;
|
||||
color: #BF9975;
|
||||
border-color: #BF9975;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* teal — бирюзовая «Позвонить» */
|
||||
.bb-btn-teal {
|
||||
background: #60959c;
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* pill — кремовая таблетка «Заказать звонок» */
|
||||
.bb-btn-pill {
|
||||
background: #e9e4d4;
|
||||
color: #333;
|
||||
border-color: #d5cfbd;
|
||||
border-radius: 25px;
|
||||
}
|
||||
|
||||
/* ─── Форм-контролы (Sprint 3) ───────────────────────────────── */
|
||||
.bb-input,
|
||||
.bb-textarea,
|
||||
.bb-select {
|
||||
font-family: var(--font-web);
|
||||
font-size: 14px;
|
||||
color: var(--bb-text);
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
outline: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.bb-input { height: 50px; }
|
||||
.bb-input:focus,
|
||||
.bb-textarea:focus,
|
||||
.bb-select:focus {
|
||||
border-color: var(--brand-053m);
|
||||
box-shadow: 0 0 0 3px rgba(126, 207, 202, 0.2);
|
||||
}
|
||||
.bb-input.bb-error,
|
||||
.bb-textarea.bb-error,
|
||||
.bb-select.bb-error { border-color: #dc2626; }
|
||||
.bb-input.bb-error:focus,
|
||||
.bb-textarea.bb-error:focus,
|
||||
.bb-select.bb-error:focus { box-shadow: 0 0 0 3px rgba(220, 38, 38, 0.15); }
|
||||
.bb-input:disabled,
|
||||
.bb-textarea:disabled,
|
||||
.bb-select:disabled { opacity: 0.5; cursor: not-allowed; background: var(--bb-sidebar-bg); }
|
||||
|
||||
.bb-textarea { resize: vertical; min-height: 100px; }
|
||||
.bb-select {
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
height: 50px;
|
||||
padding: 10px 36px 10px 10px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
}
|
||||
|
||||
.bb-checkbox,
|
||||
.bb-radio {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--brand-073m);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ─── Тумблер (Sprint 3) ──────────────────────────────────────── */
|
||||
.bb-toggle-track {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
border-radius: 12px;
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bb-toggle-thumb {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Внутренняя навигация | Брендбук О!Клиника",
|
||||
title: "Внутренняя навигация. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function Section({
|
||||
@@ -30,74 +31,6 @@ function 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">
|
||||
@@ -115,52 +48,111 @@ export default function NavigationPage() {
|
||||
</h1>
|
||||
<p className="text-base max-w-2xl" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Система навигационных табличек и указателей внутри клиники.
|
||||
Единый стиль с фирменными цветами и шрифтом DINPro.
|
||||
На оргстекле, наклейки из плёнок Оракл 053M и 073M.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Шаблоны табличек */}
|
||||
{/* Макеты таблечек */}
|
||||
<Section
|
||||
title="Типы табличек"
|
||||
subtitle="Четыре базовых шаблона для разных зон клиники."
|
||||
title="Макеты навигационных табличек"
|
||||
subtitle="Два типа: табличка кабинета с бирюзовым заголовком и карточка врача с логотипом и QR-кодом."
|
||||
>
|
||||
<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 className="max-w-lg">
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border mb-4"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<Image
|
||||
src="/offline/navigation/nav-mockup-signs.jpeg"
|
||||
alt="Макеты навигационных табличек: Кабинет 04 с бирюзовым заголовком и карточка врача Лебединской"
|
||||
width={620}
|
||||
height={570}
|
||||
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)" }}>
|
||||
Табличка «Кабинет 04»: бирюзовый заголовок (Oracal 053M), специализация и ФИО врачей.
|
||||
Карточка врача: логотип клиники, имя, должности, QR-код на страницу врача.
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Нумерация дверей */}
|
||||
<Section
|
||||
title="Нумерация кабинетов на дверях"
|
||||
subtitle="Номер кабинета размещается непосредственно на двери — крупный шрифт, плёнка Oracal."
|
||||
>
|
||||
<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/navigation/nav-door-31.png"
|
||||
alt="Белая дверь кабинета 31 с крупным номером из плёнки"
|
||||
width={770}
|
||||
height={963}
|
||||
className="w-full object-cover"
|
||||
style={{ maxHeight: 420, objectPosition: "top" }}
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
Кабинет 31 — белая дверь
|
||||
</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/navigation/nav-3.jpeg"
|
||||
alt="Серая дверь кабинета 13 с крупным номером"
|
||||
width={800}
|
||||
height={463}
|
||||
className="w-full object-cover"
|
||||
style={{ maxHeight: 420, objectPosition: "left" }}
|
||||
/>
|
||||
</div>
|
||||
<p className="font-medium text-sm mb-1" style={{ color: "var(--bb-text)" }}>
|
||||
Кабинет 13 — серая дверь
|
||||
</p>
|
||||
<p className="text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Тёмная дверь с крупным номером. Рядом — табличка кабинета на стене.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Указатели по этажам */}
|
||||
<Section
|
||||
title="Указатели по этажам"
|
||||
subtitle="Навигационные панели в холлах — показывают расположение кабинетов и специализации на каждом этаже."
|
||||
>
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<Image
|
||||
src="/offline/navigation/nav-render-p13.jpeg"
|
||||
alt="Навигационные панели по этажам: кабинеты 01-06, 21-25, 31-37, 41-45"
|
||||
width={2105}
|
||||
height={1489}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-3 text-sm" style={{ color: "var(--bb-text-muted)" }}>
|
||||
Слева — указатель с этажом и направлением, справа — панель с полным перечнем кабинетов.
|
||||
Активный этаж выделяется бирюзовым фоном (Oracal 053M).
|
||||
</p>
|
||||
</Section>
|
||||
|
||||
{/* Технические требования */}
|
||||
@@ -188,12 +180,10 @@ export default function NavigationPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
["Основной материал", "ПВХ 3мм / ПС зеркальный / акрил"],
|
||||
["Покрытие фона", "Oracal плёнка (053M / 073M / 050M)"],
|
||||
["Шрифт", "DINPro Bold / Regular (DXF для фрезеровки)"],
|
||||
["Крепление", "Двусторонний скотч / шурупы с дистанционным держателем"],
|
||||
["Толщина букв (фрезеровка)", "3 мм от основы"],
|
||||
["Минимальный размер текста", "10 мм по высоте"],
|
||||
["Основной материал", "Оргстекло"],
|
||||
["Покрытие", "Наклейка из плёнок Oracal"],
|
||||
["Шрифт", "DINPro Bold / Regular"],
|
||||
["Крепление", "Дистанционные держатели"],
|
||||
].map(([param, value]) => (
|
||||
<tr
|
||||
key={param}
|
||||
@@ -218,24 +208,25 @@ export default function NavigationPage() {
|
||||
title="Цвета Oracal для навигации"
|
||||
subtitle="Допустимые цвета плёнки по коду Oracal."
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
||||
<div className="flex gap-6">
|
||||
{[
|
||||
{ code: "053M", hex: "#7ecfca", name: "Акцент / полоса" },
|
||||
{ code: "073M", hex: "#5b7b87", name: "Фон указателей" },
|
||||
{ code: "050M", hex: "#1b4c72", name: "Фон запрещающих" },
|
||||
{ code: "081M", hex: "#c4a882", name: "Акцент на тёмном" },
|
||||
{ code: "053M", hex: "#7ecfca", name: "Заголовок таблички / активный этаж" },
|
||||
{ code: "073M", hex: "#5b7b87", name: "Дополнительный акцент" },
|
||||
].map(c => (
|
||||
<div
|
||||
key={c.code}
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
className="flex items-center gap-4 rounded-xl border p-4"
|
||||
style={{ borderColor: "var(--bb-border)", background: "var(--bb-sidebar-bg)" }}
|
||||
>
|
||||
<div className="h-16" style={{ background: c.hex }} />
|
||||
<div className="p-3">
|
||||
<p className="font-medium text-xs" style={{ color: "var(--bb-text)" }}>
|
||||
<div
|
||||
className="w-12 h-12 rounded-lg shrink-0"
|
||||
style={{ background: c.hex }}
|
||||
/>
|
||||
<div>
|
||||
<p className="font-medium text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
Oracal {c.code}
|
||||
</p>
|
||||
<p className="text-xs" style={{ color: "var(--bb-text-muted)" }}>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--bb-text-muted)" }}>
|
||||
{c.name}
|
||||
</p>
|
||||
<p className="font-mono text-xs mt-1" style={{ color: "var(--bb-text-muted)" }}>
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Печатные материалы | Брендбук О!Клиника",
|
||||
title: "Печатные материалы. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function Section({
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Брендирование транспорта | Брендбук О!Клиника",
|
||||
title: "Брендирование транспорта. Цифровой брендбук Клиники ухо, горло, нос им. проф. Е.Н.Оленевой",
|
||||
};
|
||||
|
||||
function Section({
|
||||
@@ -31,101 +31,6 @@ function 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 (
|
||||
@@ -151,9 +56,20 @@ export default function TransportPage() {
|
||||
{/* Макет */}
|
||||
<Section
|
||||
title="Макет трамвая"
|
||||
subtitle="Трёхполосная схема брендирования: верхняя бирюзовая, центральная бежевая с логотипом, нижняя серо-голубая."
|
||||
subtitle="Вид с обеих сторон: бирюзовая полоса, логотип клиники, фотографии пациентов и врачей, контактная информация."
|
||||
>
|
||||
<TramMockup />
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{ borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
<Image
|
||||
src="/offline/transport/tram-mockup.jpeg"
|
||||
alt="Макет брендирования трамвая: вид спереди и сзади с логотипом Клиники ухо горло нос им. проф. Е.Н.Оленевой"
|
||||
width={1884}
|
||||
height={977}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* Цветовая схема */}
|
||||
|
||||
@@ -27,8 +27,8 @@ const NAV: NavSection[] = [
|
||||
{
|
||||
title: "Компоненты",
|
||||
items: [
|
||||
{ label: "Кнопки", href: "/components/buttons", soon: true },
|
||||
{ label: "Форм-контролы", href: "/components/forms", soon: true },
|
||||
{ label: "Кнопки", href: "/components/buttons" },
|
||||
{ label: "Форм-контролы", href: "/components/forms" },
|
||||
{ label: "Карточки", href: "/components/cards", soon: true },
|
||||
{ label: "Бейджи и теги", href: "/components/badges", soon: true },
|
||||
{ label: "Алерты", href: "/components/alerts", soon: true },
|
||||
@@ -68,7 +68,6 @@ const NAV: NavSection[] = [
|
||||
{ label: "Бейджи", href: "/offline/badges" },
|
||||
{ label: "Навигация", href: "/offline/navigation" },
|
||||
{ label: "Транспорт", href: "/offline/transport" },
|
||||
{ label: "Печать", href: "/offline/print" },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -171,7 +170,7 @@ export function Sidebar() {
|
||||
color: "var(--bb-sidebar-text-muted)",
|
||||
}}
|
||||
>
|
||||
Sprint 2 · v0.2.0
|
||||
Sprint 3 · v0.3.0
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface LlmBlockProps {
|
||||
/** Путь страницы, например "/foundation/colors" */
|
||||
path: string;
|
||||
/** Версия данных, например "v2.1" */
|
||||
version: string;
|
||||
/** Плоский текст для копирования */
|
||||
specText: string;
|
||||
/** Содержимое блока — таблицы, правила */
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* LlmBlock — переиспользуемый блок LLM-спецификации.
|
||||
* Добавляется в конец каждой страницы брендбука, содержащей дизайн-стандарты.
|
||||
* Требование: ФТ-03-LLM (TZ.md) · docs/LLM_CONTEXT.md
|
||||
*/
|
||||
export function LlmBlock({ path, version, specText, children }: LlmBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
function handleCopy(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
navigator.clipboard.writeText(specText);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<details
|
||||
open
|
||||
className="rounded-xl overflow-hidden"
|
||||
style={{
|
||||
border: "2px dashed var(--brand-053m)",
|
||||
}}
|
||||
>
|
||||
{/* Заголовок */}
|
||||
<summary
|
||||
className="flex items-center justify-between px-5 py-3 cursor-pointer select-none list-none"
|
||||
style={{ background: "rgba(126,207,202,0.07)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
className="shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded tracking-wider"
|
||||
style={{ background: "var(--brand-053m)", color: "#fff" }}
|
||||
>
|
||||
LLM
|
||||
</span>
|
||||
<span className="font-semibold text-sm" style={{ color: "var(--bb-text)" }}>
|
||||
LLM-спецификация
|
||||
</span>
|
||||
<span
|
||||
className="text-xs truncate hidden sm:inline"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
· машиночитаемые данные · docs/LLM_CONTEXT.md
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 shrink-0 ml-3">
|
||||
<span
|
||||
className="text-[10px] font-mono hidden md:inline"
|
||||
style={{ color: "var(--bb-text-muted)" }}
|
||||
>
|
||||
{path} · {version}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="text-xs px-3 py-1 rounded font-medium transition-colors shrink-0"
|
||||
style={{
|
||||
background: copied ? "#d1fae5" : "var(--brand-053m)",
|
||||
color: copied ? "#065f46" : "#fff",
|
||||
}}
|
||||
>
|
||||
{copied ? "✓ Скопировано" : "Скопировать"}
|
||||
</button>
|
||||
</div>
|
||||
</summary>
|
||||
|
||||
{/* Содержимое */}
|
||||
<div
|
||||
className="px-5 py-5 space-y-6 border-t"
|
||||
style={{
|
||||
borderColor: "var(--brand-053m)",
|
||||
borderStyle: "dashed",
|
||||
background: "var(--bb-content-bg)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Утилиты для содержимого блока ──────────────────────────── */
|
||||
|
||||
/** Заголовок подсекции внутри LLM-блока */
|
||||
export function LlmSection({ title }: { title: string }) {
|
||||
return (
|
||||
<p
|
||||
className="text-[10px] font-semibold uppercase tracking-widest pb-1 border-b"
|
||||
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{title}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
/** Компактная таблица для LLM-блока */
|
||||
export function LlmTable({
|
||||
headers,
|
||||
rows,
|
||||
}: {
|
||||
headers: string[];
|
||||
rows: (string | React.ReactNode)[][];
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs border-collapse font-mono">
|
||||
<thead>
|
||||
<tr style={{ background: "var(--bb-sidebar-bg)" }}>
|
||||
{headers.map((h) => (
|
||||
<th
|
||||
key={h}
|
||||
className="text-left px-2 py-1.5 font-medium border whitespace-nowrap"
|
||||
style={{ color: "var(--bb-text-muted)", borderColor: "var(--bb-border)" }}
|
||||
>
|
||||
{h}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((row, ri) => (
|
||||
<tr key={ri} style={{ borderTop: `1px solid var(--bb-border)` }}>
|
||||
{row.map((cell, ci) => (
|
||||
<td
|
||||
key={ci}
|
||||
className="px-2 py-1 border"
|
||||
style={{
|
||||
borderColor: "var(--bb-border)",
|
||||
color: "var(--bb-text-muted)",
|
||||
maxWidth: "240px",
|
||||
}}
|
||||
>
|
||||
{cell}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Список правил ✓ / ✕ */
|
||||
export function LlmRules({ rules }: { rules: { ok: boolean; text: string }[] }) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-0.5">
|
||||
{rules.map((r) => (
|
||||
<div key={r.text} className="flex items-start gap-1.5 py-0.5 text-xs font-mono">
|
||||
<span
|
||||
style={{ color: r.ok ? "#059669" : "#dc2626", fontWeight: 700, flexShrink: 0 }}
|
||||
>
|
||||
{r.ok ? "✓" : "✕"}
|
||||
</span>
|
||||
<span style={{ color: "var(--bb-text-muted)" }}>{r.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, forwardRef } from "react";
|
||||
|
||||
export type ButtonVariant = "primary" | "outline" | "teal" | "pill";
|
||||
export type ButtonSize = "sm" | "md" | "lg";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
function Button(
|
||||
{ variant = "primary", size = "md", loading = false, disabled, children, className = "", ...props },
|
||||
ref
|
||||
) {
|
||||
const isDisabled = disabled || loading;
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={isDisabled}
|
||||
className={`bb-btn bb-btn-${size} bb-btn-${variant} ${className}`.trim()}
|
||||
{...props}
|
||||
>
|
||||
{loading && (
|
||||
<span
|
||||
style={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
border: "2px solid currentColor",
|
||||
borderTopColor: "transparent",
|
||||
borderRadius: "50%",
|
||||
display: "inline-block",
|
||||
animation: "bb-spin 0.65s linear infinite",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export function CodeCopy({ code, lang = "jsx" }: { code: string; lang?: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ borderRadius: 8, overflow: "hidden", border: "1px solid var(--bb-border)" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
background: "var(--bb-sidebar-bg)",
|
||||
padding: "6px 12px",
|
||||
borderBottom: "1px solid var(--bb-border)",
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 11, color: "var(--bb-text-muted)", fontFamily: "var(--font-mono)" }}>
|
||||
{lang}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}}
|
||||
style={{
|
||||
fontSize: 12,
|
||||
background: copied ? "#d1fae5" : "var(--brand-053m)",
|
||||
color: copied ? "#065f46" : "#fff",
|
||||
border: "none",
|
||||
borderRadius: 4,
|
||||
padding: "3px 10px",
|
||||
cursor: "pointer",
|
||||
fontWeight: 500,
|
||||
fontFamily: "var(--font-web)",
|
||||
}}
|
||||
>
|
||||
{copied ? "✓ Скопировано" : "Скопировать"}
|
||||
</button>
|
||||
</div>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: "12px 16px",
|
||||
overflowX: "auto",
|
||||
fontSize: 12,
|
||||
fontFamily: "var(--font-mono)",
|
||||
color: "var(--bb-text-muted)",
|
||||
background: "#fff",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
interface ToggleProps {
|
||||
defaultChecked?: boolean;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
onChange?: (checked: boolean) => void;
|
||||
}
|
||||
|
||||
export function Toggle({ defaultChecked = false, disabled = false, label, onChange }: ToggleProps) {
|
||||
const [checked, setChecked] = useState(defaultChecked);
|
||||
|
||||
function handleToggle() {
|
||||
if (disabled) return;
|
||||
const next = !checked;
|
||||
setChecked(next);
|
||||
onChange?.(next);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
cursor: disabled ? "not-allowed" : "pointer",
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
userSelect: "none",
|
||||
}}
|
||||
onClick={handleToggle}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === " " || e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bb-toggle-track"
|
||||
style={{ background: checked ? "var(--brand-073m)" : "#d1d5db" }}
|
||||
>
|
||||
<div
|
||||
className="bb-toggle-thumb"
|
||||
style={{ transform: checked ? "translateX(20px)" : "translateX(0)" }}
|
||||
/>
|
||||
</div>
|
||||
{label && (
|
||||
<span style={{ fontSize: 14, color: "var(--bb-text)", fontFamily: "var(--font-web)" }}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 871 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 132 B |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 740 B |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 767 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 145 B |
|
After Width: | Height: | Size: 957 B |
|
After Width: | Height: | Size: 446 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 699 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 141 B |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 332 KiB |
@@ -0,0 +1,417 @@
|
||||
# LLM Context — Цифровой брендбук Клиники
|
||||
|
||||
## Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||
|
||||
**Версия контекста:** 3.0
|
||||
**Дата обновления:** 2026-03-22
|
||||
**Актуальный спринт:** Sprint 3
|
||||
**Сайт клиники:** https://oclinica.ru
|
||||
**Брендбук (локально):** http://localhost:3001
|
||||
**Брендбук (production):** https://web-oclinica.vercel.app
|
||||
|
||||
---
|
||||
|
||||
## Назначение файла
|
||||
|
||||
Этот файл — единый источник дизайн-данных клиники в машиночитаемом формате.
|
||||
Используется как контекст для LLM при:
|
||||
- разработке страниц и компонентов сайта
|
||||
- создании макетов и прототипов
|
||||
- разработке мобильных приложений
|
||||
- проектировании новых носителей бренда
|
||||
- генерации CSS / Tailwind / Figma Tokens
|
||||
|
||||
При работе с любым материалом клиники **всегда загружай этот файл первым**.
|
||||
|
||||
---
|
||||
|
||||
## 1. О клинике
|
||||
|
||||
**Полное название:** Клиника ухо, горло, нос им. проф. Е.Н.Оленевой
|
||||
**Тип:** Медицинская клиника, отоларингология (ЛОР)
|
||||
**Город:** Пермь
|
||||
**Платформа сайта:** Drupal (тема `clinic_bootstrap_mobile`)
|
||||
**Аудитория:** Пациенты, врачи клиники, подрядчики
|
||||
|
||||
---
|
||||
|
||||
## 2. Цветовая палитра
|
||||
|
||||
### 2.1 Фирменные цвета (Oracal — базовая палитра)
|
||||
|
||||
Основаны на кодах самоклеящейся плёнки Oracal. HEX-значения — приближённые
|
||||
цифровые эквиваленты. **Для печати и оффлайн-носителей используй коды Oracal,
|
||||
не HEX.**
|
||||
|
||||
| Oracal | Название | HEX | RGB | HSL | CSS-переменная | Применение |
|
||||
|--------|-----------------------|-----------|-------------------|------------------|------------------|------------|
|
||||
| 053M | Основной бирюзовый | `#7ecfca` | rgb(126,207,202) | hsl(177,47%,65%) | `--brand-053m` | Акцент, CTA-кнопки, иконки, активные состояния |
|
||||
| 073M | Тёмный серо-голубой | `#5b7b87` | rgb(91,123,135) | hsl(197,20%,44%) | `--brand-073m` | Тёмный фон, хедер, заголовки навигации |
|
||||
| 066M | Средний бирюзовый | `#5bb5ad` | rgb(91,181,173) | hsl(174,33%,53%) | `--brand-066m` | Вторичные акценты, фоны секций, иллюстрации |
|
||||
| 050M | Тёмно-синий | `#1b4c72` | rgb(27,76,114) | hsl(208,61%,28%) | `--brand-050m` | Наружная реклама, полиграфия, заголовки на светлом |
|
||||
| 081M | Бежевый | `#c4a882` | rgb(196,168,130) | hsl(33,35%,64%) | `--brand-081m` | Форма сотрудников, оффлайн носители, тёплые акценты |
|
||||
| 080M | Тёмно-коричневый | `#5c2e0e` | rgb(92,46,14) | hsl(23,73%,21%) | `--brand-080m` | Текст на бежевых поверхностях, логотип на форме |
|
||||
| — | Белый | `#ffffff` | rgb(255,255,255) | hsl(0,0%,100%) | `--brand-white` | Фон, инвертированный текст, логотип на тёмных фонах |
|
||||
|
||||
### 2.2 Цвета сайта oclinica.ru (реальный CSS)
|
||||
|
||||
Извлечены из CSS темы Drupal:
|
||||
`https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css`
|
||||
Метод: python + regex + Counter, 2026-03-22
|
||||
|
||||
| Название | HEX | RGB | Группа | Вхождений | Применение на сайте |
|
||||
|-----------------------|-----------|-------------------|----------|-----------|---------------------|
|
||||
| Бежевый | `#bf9975` | rgb(191,153,117) | Акценты | 12 | Основной тёплый акцент, фоны, рамки, текст |
|
||||
| Серо-бирюзовый | `#60959c` | rgb(96,149,156) | Акценты | 7 | Основной холодный акцент, ссылки |
|
||||
| Бирюзовый | `#63bac3` | rgb(99,186,195) | Акценты | 4 | Фоны акцентных блоков, иконки |
|
||||
| Бирюзовый средний | `#52b4bd` | rgb(82,180,189) | Акценты | 4 | Вторичные цветовые акценты |
|
||||
| Коралловый | `#ffa39c` | rgb(255,163,156) | Акценты | 2 | CTA-кнопки («Запишите меня!») |
|
||||
| Основной текст | `#464646` | rgb(70,70,70) | Текст | 3 | Цвет основного текста на сайте |
|
||||
| Второстепенный текст | `#949290` | rgb(148,146,144) | Текст | 4 | Подписи, второстепенный контент |
|
||||
| Светло-бирюзовый фон | `#b8e6ed` | rgb(184,230,237) | Фоны | 1 | Фоны светлых секций с акцентом |
|
||||
| Кремовый фон | `#e9e4d4` | rgb(233,228,212) | Фоны | 1 | Тёплые фоны секций |
|
||||
| Светло-жёлтый фон | `#eef4d1` | rgb(238,244,209) | Фоны | 1 | Фон карточек отзывов |
|
||||
| Светло-зелёный фон | `#f2fee6` | rgb(242,254,230) | Фоны | 1 | Фон секции новостей |
|
||||
|
||||
### 2.3 Соответствие Oracal → Сайт
|
||||
|
||||
| Oracal (бренд) | HEX бренда | Сайт (цифровой) | HEX сайта | Отклонение |
|
||||
|--------------------------|------------|-----------------------|-----------|------------|
|
||||
| 053M Основной бирюзовый | `#7ecfca` | Бирюзовый | `#63bac3` | Темнее, насыщеннее |
|
||||
| 073M Тёмный серо-голубой | `#5b7b87` | Серо-бирюзовый | `#60959c` | Светлее на сайте |
|
||||
| 066M Средний бирюзовый | `#5bb5ad` | Бирюзовый средний | `#52b4bd` | Смещён в синеву |
|
||||
| 081M Бежевый | `#c4a882` | Бежевый | `#bf9975` | Темнее, насыщеннее |
|
||||
| 050M Тёмно-синий | `#1b4c72` | — | — | Не найден в CSS сайта |
|
||||
| 080M Тёмно-коричневый | `#5c2e0e` | — | — | Не найден в CSS сайта |
|
||||
|
||||
**Важно:** расхождения ожидаемы — это цифровая адаптация плёночных цветов под экран.
|
||||
При разработке digital-материалов используй цвета сайта (раздел 2.2), не Oracal.
|
||||
|
||||
### 2.4 Контрастность пар (WCAG 2.1)
|
||||
|
||||
| Пара | Коэффициент | AA (4.5:1) | AAA (7:1) | AA large (3:1) |
|
||||
|-------------------------------------------|-------------|------------|-----------|----------------|
|
||||
| Белый на тёмном серо-голубом (#5b7b87) | 4.6:1 | ✓ | ✕ | ✓ |
|
||||
| Белый на тёмно-синем (#1b4c72) | 9.3:1 | ✓ | ✓ | ✓ |
|
||||
| Белый на тёмно-коричневом (#5c2e0e) | 11.2:1 | ✓ | ✓ | ✓ |
|
||||
| Белый на среднем бирюзовом (#5bb5ad) | 3.0:1 | ✕ | ✕ | ✓ |
|
||||
| Тёмный текст (#111827) на основном бирюзовом (#7ecfca) | 5.8:1 | ✓ | ✕ | ✓ |
|
||||
| Тёмный текст (#111827) на бежевом (#c4a882) | 4.8:1 | ✓ | ✕ | ✓ |
|
||||
| Тёмно-коричневый (#5c2e0e) на бежевом (#c4a882) | 6.7:1 | ✓ | ✕ | ✓ |
|
||||
|
||||
---
|
||||
|
||||
## 3. Типографика
|
||||
|
||||
### 3.1 DINPro — фирменный шрифт (оффлайн)
|
||||
|
||||
**Применение:** логотип, оффлайн-носители (бейджи, навигационные таблички, транспорт,
|
||||
форма сотрудников, полиграфия).
|
||||
**Не использовать** на веб-сайте и в digital-материалах (лицензионный шрифт,
|
||||
нет легального встраивания в веб).
|
||||
**Веса:** Regular, Medium, Bold
|
||||
|
||||
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение |
|
||||
|----------|-------------|-----------|-------------|----------------|------------|
|
||||
| Display | 700 | 48px | 1.15 | -0.5px | Крупные заголовки носителей |
|
||||
| H1 | 700 | 36px | 1.2 | -0.3px | Основной заголовок печатных материалов |
|
||||
| H2 | 700 | 28px | 1.25 | -0.2px | Подзаголовки носителей |
|
||||
| H3 | 500 | 22px | 1.3 | 0px | Подзаголовки третьего уровня |
|
||||
| Body | 400 | 16px | 1.5 | 0px | Основной текст оффлайн |
|
||||
| Small | 400 | 12px | 1.4 | 0.2px | Подписи, бейджи, таблички |
|
||||
| Caption | 400 | 10px | 1.3 | 0.4px | Сноски, технические пометки |
|
||||
|
||||
### 3.2 Fira Sans — веб-шрифт (digital)
|
||||
|
||||
**Применение:** сайт oclinica.ru, цифровые коммуникации, digital-баннеры, email.
|
||||
**Источник:** Google Fonts (`https://fonts.google.com/specimen/Fira+Sans`)
|
||||
**Поддержка кириллицы:** да
|
||||
**Используемые веса:** 300 (Light), 400 (Regular), 500 (Medium), 600 (SemiBold)
|
||||
**CSS:** `font-family: 'Fira Sans', sans-serif;`
|
||||
|
||||
| Стиль | font-weight | font-size | line-height | letter-spacing | Применение |
|
||||
|----------------|-------------|---------------|-------------|----------------|------------|
|
||||
| h1 | 600 | 2.25rem (36px)| 1.25 | -0.025em | Заголовок страницы |
|
||||
| h2 | 600 | 1.875rem (30px)| 1.3 | -0.02em | Заголовок секции |
|
||||
| h3 | 500 | 1.5rem (24px) | 1.375 | -0.01em | Подзаголовок |
|
||||
| h4 | 500 | 1.25rem (20px)| 1.4 | 0em | Заголовок компонента |
|
||||
| h5 | 500 | 1.125rem (18px)| 1.45 | 0em | Подзаголовок компонента |
|
||||
| h6 | 500 | 1rem (16px) | 1.5 | 0.01em | Метка секции |
|
||||
| body-large | 400 | 1.125rem (18px)| 1.6 | 0em | Лид-текст, вводный абзац |
|
||||
| body | 400 | 1rem (16px) | 1.625 | 0em | Основной текст |
|
||||
| body-small | 400 | 0.875rem (14px)| 1.5 | 0em | Дополнительный текст, подписи |
|
||||
| caption | 300 | 0.75rem (12px)| 1.4 | 0.02em | Подписи к изображениям, сноски |
|
||||
| label | 500 | 0.875rem (14px)| 1.2 | 0.03em | Метки форм |
|
||||
| overline | 600 | 0.6875rem (11px)| 1.2 | 0.1em | Надписи над заголовками (uppercase) |
|
||||
|
||||
**Принцип выбора шрифта:**
|
||||
- Носитель цифровой → Fira Sans
|
||||
- Носитель печатный / физический → DINPro
|
||||
|
||||
---
|
||||
|
||||
## 4. Логотип
|
||||
|
||||
### 4.1 Версии логотипа
|
||||
|
||||
**Основной логотип** — включает название специализации («ухо, горло, нос»).
|
||||
Используется на всех основных носителях: сайт, навигация, транспорт, форма.
|
||||
|
||||
**Общий логотип** — без специализации, только название клиники или сеть.
|
||||
Используется для обозначения сети клиник, в корпоративных материалах.
|
||||
|
||||
### 4.2 Файлы
|
||||
|
||||
| Файл | Описание | Путь в проекте |
|
||||
|------|----------|----------------|
|
||||
| `logo-transparent.png` | Логотип с прозрачным фоном | `apps/web/public/logo/logo-transparent.png` |
|
||||
|
||||
SVG-версия ожидается (не получена от клиники).
|
||||
|
||||
### 4.3 Цветовые варианты
|
||||
|
||||
| Вариант | Фон | Логотип | Применение |
|
||||
|---------|-----|---------|------------|
|
||||
| Основной | Белый / светлый | Полноцветный | Сайт, полиграфия на белом |
|
||||
| Инвертированный | Тёмный (#5b7b87, #1b4c72) | Белый (`filter: brightness(0) invert(1)`) | Хедер, тёмные секции |
|
||||
| На форме (беж) | Бежевый (#c4a882 / Oracal 081M) | Коричневый (#5c2e0e / Oracal 080M) | Одежда персонала |
|
||||
| На форме (синий) | Тёмно-синий (#1b4c72 / Oracal 050M) | Белый | Одежда персонала |
|
||||
|
||||
### 4.4 Охранная зона
|
||||
|
||||
Минимальный отступ вокруг логотипа = **высота буквы «К»** в названии.
|
||||
Запрещено размещать другие элементы в охранной зоне.
|
||||
|
||||
### 4.5 Минимальные размеры
|
||||
|
||||
| Носитель | Размер логотипа |
|
||||
|----------|----------------|
|
||||
| Одежда до р.46 | 70 × 25,5 мм |
|
||||
| Одежда от р.48 | 90 × 32,8 мм |
|
||||
|
||||
### 4.6 Запрещено
|
||||
|
||||
- Искажать пропорции
|
||||
- Менять цвета на нефирменные
|
||||
- Добавлять тени, обводки, эффекты
|
||||
- Размещать на пёстром или плохо контрастном фоне
|
||||
- Использовать низкое разрешение (< 150 dpi для печати)
|
||||
- Переворачивать или отражать
|
||||
|
||||
---
|
||||
|
||||
## 5. CSS-переменные брендбука
|
||||
|
||||
Определены в `apps/web/app/globals.css`:
|
||||
|
||||
```css
|
||||
/* Цвета бренда */
|
||||
--brand-053m: #7ecfca; /* Основной бирюзовый */
|
||||
--brand-073m: #5b7b87; /* Тёмный серо-голубой */
|
||||
--brand-066m: #5bb5ad; /* Средний бирюзовый */
|
||||
--brand-050m: #1b4c72; /* Тёмно-синий */
|
||||
--brand-081m: #c4a882; /* Бежевый */
|
||||
--brand-080m: #5c2e0e; /* Тёмно-коричневый */
|
||||
--brand-white: #ffffff; /* Белый */
|
||||
|
||||
/* UI брендбука */
|
||||
--bb-sidebar-bg: ... /* Фон сайдбара */
|
||||
--bb-sidebar-border: ... /* Граница сайдбара */
|
||||
--bb-sidebar-text: ... /* Текст сайдбара */
|
||||
--bb-sidebar-text-muted: ...
|
||||
--bb-sidebar-section: .../* Заголовки секций сайдбара */
|
||||
--bb-sidebar-active-bg: ...
|
||||
--bb-text: ... /* Основной текст контента */
|
||||
--bb-text-muted: ... /* Приглушённый текст */
|
||||
--bb-border: ... /* Границы */
|
||||
--bb-content-bg: ... /* Фон карточек */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Оффлайн носители
|
||||
|
||||
### 6.1 Форма сотрудников
|
||||
|
||||
**Варианты:**
|
||||
- Бежевый: ткань цвета Oracal 081M, логотип Oracal 080M (коричневый), расположение — левая сторона груди
|
||||
- Синий: ткань цвета Oracal 050M (тёмно-синий), логотип белый (Oracal 010), расположение — левая сторона груди
|
||||
|
||||
**Размеры логотипа на форме:**
|
||||
- Размеры до 46: 70 × 25,5 мм
|
||||
- Размеры от 48: 90 × 32,8 мм
|
||||
|
||||
### 6.2 Бейджи
|
||||
|
||||
**Размер:** 70 × 30 мм
|
||||
**Варианты:** серый (светлый) и белый
|
||||
**Состав текста:** имя, должность, учёная степень (при наличии)
|
||||
**Шрифт:** DINPro
|
||||
**Логотип:** левый верхний угол или левая часть
|
||||
|
||||
### 6.3 Внутренняя навигация
|
||||
|
||||
**Материал:** оргстекло
|
||||
**Плёнки:** Oracal 053M (бирюзовый) и 073M (тёмный серо-голубой)
|
||||
**Типы табличек:**
|
||||
- Таблички на дверях кабинетов: номер кабинета, профиль врача с фото и QR-кодом
|
||||
- Указатели по этажам: стрелки направлений + номера кабинетов
|
||||
|
||||
**Нумерация кабинетов:**
|
||||
- Двузначное число, крупный шрифт (DINPro Bold)
|
||||
- Фон: бирюзовый (053M), номер: белый
|
||||
- Пример реализации: кабинеты 13, 31, «Кабинет 04»
|
||||
|
||||
### 6.4 Брендирование транспорта (трамвай)
|
||||
|
||||
**Зоны оклейки:**
|
||||
- Борта: Oracal 053M (#7ecfca) + 073M (#5b7b87)
|
||||
- Передняя часть: Oracal 066M (#5bb5ad) + 050M (#1b4c72)
|
||||
- Акценты: Oracal 081M (#c4a882) + 080M (#5c2e0e)
|
||||
|
||||
**Все 6 фирменных цветов присутствуют на транспорте.**
|
||||
|
||||
---
|
||||
|
||||
## 7. Структура брендбука (страницы)
|
||||
|
||||
| URL | Статус | Описание |
|
||||
|-----|--------|----------|
|
||||
| `/foundation/logo` | ✅ Готова | Логотип, варианты, охранная зона, правила |
|
||||
| `/foundation/colors` | ✅ Готова | Палитра, контраст WCAG, цвета сайта, соответствие |
|
||||
| `/foundation/typography` | ✅ Готова | DINPro + Fira Sans, шкала стилей |
|
||||
| `/foundation/icons` | 🔜 Скоро | Иконография |
|
||||
| `/offline/uniform` | ✅ Готова | Форма сотрудников |
|
||||
| `/offline/badges` | ✅ Готова | Бейджи |
|
||||
| `/offline/navigation` | ✅ Готова | Внутренняя навигация |
|
||||
| `/offline/transport` | ✅ Готова | Брендирование транспорта |
|
||||
| `/components/buttons` | ✅ Готова | Кнопки — все варианты, размеры, состояния |
|
||||
| `/components/forms` | ✅ Готова | Форм-контролы — Input, Textarea, Select, Checkbox, Radio, Toggle |
|
||||
| `/components/*` | 🔜 Sprint 3–4 | Карточки, бейджи, алерты, модалки, таблицы |
|
||||
| `/blocks/*` | 🔜 Sprint 5–8 | Hero, врачи, отзывы, новости, формы |
|
||||
| `/pages/*` | 🔜 Sprint 9–11 | Главная, заболевание, врачи, цены, контакты |
|
||||
|
||||
---
|
||||
|
||||
## 8. Правила применения цветов
|
||||
|
||||
### Разрешено
|
||||
- Использовать только цвета из фирменной палитры (раздел 2.1)
|
||||
- Для digital: адаптировать к цветам сайта (раздел 2.2)
|
||||
- Текст на цветном фоне — минимум WCAG AA (4.5:1)
|
||||
- Белый логотип на тёмных фонах (073M, 050M, 080M)
|
||||
- Коричневый логотип (080M) на бежевом фоне (081M)
|
||||
|
||||
### Запрещено
|
||||
- Использовать произвольные цвета вне палитры
|
||||
- Осветлять, затемнять или менять оттенок фирменных цветов
|
||||
- Текст с контрастом ниже 3:1 (даже для крупного текста)
|
||||
- Размещать тёплые и холодные акценты рядом без разделителя
|
||||
|
||||
### Иерархия цветов
|
||||
|
||||
```
|
||||
Основной бирюзовый (053M / #7ecfca) ← главный идентификатор бренда
|
||||
└── Средний бирюзовый (066M / #5bb5ad) ← вторичный акцент
|
||||
└── Тёмный серо-голубой (073M / #5b7b87) ← фоны, хедер
|
||||
|
||||
Тёмно-синий (050M / #1b4c72) ← авторитет, полиграфия
|
||||
Бежевый (081M / #c4a882) ← тепло, форма
|
||||
└── Тёмно-коричневый (080M / #5c2e0e) ← текст на бежевом
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Правила применения типографики
|
||||
|
||||
### Веб (Fira Sans)
|
||||
- H1 только один на странице
|
||||
- Заголовки не пропускать по уровням (h1→h2→h3)
|
||||
- Основной текст: 16px / 400 / 1.625
|
||||
- Минимальный размер текста на экране: 12px
|
||||
- Кириллица: только Fira Sans, не DINPro
|
||||
|
||||
### Оффлайн (DINPro)
|
||||
- Все физические носители: DINPro
|
||||
- Логотип: DINPro Bold
|
||||
- Таблички: DINPro Medium / Bold
|
||||
- Не использовать light (300) для текста < 14pt
|
||||
|
||||
---
|
||||
|
||||
## 9a. Базовые компоненты (Sprint 3)
|
||||
|
||||
### Кнопки (Button) · `/components/buttons`
|
||||
|
||||
CSS-классы из `globals.css`. Компонент: `@/components/ui/Button` (React, "use client").
|
||||
|
||||
| Вариант | CSS класс | Фон | Текст | Граница | Применение |
|
||||
|-----------|------------------|------------|---------|-----------|------------|
|
||||
| primary | .bb-btn-primary | #5b7b87 | #fff | #5b7b87 | Главный CTA: «Записаться», «Подтвердить» |
|
||||
| secondary | .bb-btn-secondary | прозрачный | #5b7b87 | #5b7b87 | Второстепенное действие |
|
||||
| ghost | .bb-btn-ghost | прозрачный | #5b7b87 | нет | Третичное действие, отмена |
|
||||
| danger | .bb-btn-danger | #dc2626 | #fff | #dc2626 | Деструктивное действие |
|
||||
|
||||
| Размер | CSS класс | padding | font-size | border-radius |
|
||||
|--------|------------|------------|-----------|---------------|
|
||||
| sm | .bb-btn-sm | 5px 12px | 13px | 6px |
|
||||
| md | .bb-btn-md | 8px 18px | 14px | 8px |
|
||||
| lg | .bb-btn-lg | 12px 26px | 16px | 10px |
|
||||
|
||||
**Состояния:** default · hover (brightness 0.9) · active (brightness 0.82) · loading (spinner + opacity 0.5) · disabled (opacity 0.5)
|
||||
|
||||
**Правила:** не более одной primary на экран в контексте задачи · текст — глагол или призыв · Danger только для деструктивных действий.
|
||||
|
||||
### Форм-контролы (Forms) · `/components/forms`
|
||||
|
||||
| Элемент | CSS класс | Тег HTML | Описание |
|
||||
|----------|-----------------|----------------------------|----------|
|
||||
| Input | .bb-input | `<input>` | text, email, password |
|
||||
| Textarea | .bb-textarea | `<textarea>` | многострочный, min-height: 100px |
|
||||
| Select | .bb-select | `<select>` | с кастомной SVG-стрелкой |
|
||||
| Checkbox | .bb-checkbox | `<input type="checkbox">` | 16×16px, accent-color: #5b7b87 |
|
||||
| Radio | .bb-radio | `<input type="radio">` | 16×16px, accent-color: #5b7b87 |
|
||||
| Toggle | .bb-toggle-track | React-компонент `<Toggle>` | 44×24px, track + thumb |
|
||||
|
||||
**Состояния полей:** default (border #e5e7eb) · focus (border #7ecfca + box-shadow) · error (.bb-error, border #dc2626) · disabled (opacity 0.5)
|
||||
|
||||
**Toggle:** выкл → track #d1d5db · вкл → track #5b7b87 (#73M) · thumb: белый круг 20×20px.
|
||||
|
||||
---
|
||||
|
||||
## 10. Технический стек проекта
|
||||
|
||||
| Слой | Технология | Версия |
|
||||
|------|-----------|--------|
|
||||
| Фронтенд | Next.js App Router | 16.x |
|
||||
| Стилизация | Tailwind CSS | 4.x |
|
||||
| Язык | TypeScript | 5.x |
|
||||
| Шрифты | next/font/google | — |
|
||||
| Бэкенд | NestJS | 11.x |
|
||||
| БД | PostgreSQL + Prisma | 16.x / 7.x |
|
||||
| Деплой | Vercel Hobby | — |
|
||||
| Пакетный менеджер | pnpm | 10.x |
|
||||
|
||||
---
|
||||
|
||||
## 11. История изменений контекста
|
||||
|
||||
| Версия | Дата | Что добавлено |
|
||||
|--------|------|---------------|
|
||||
| 1.0 | 2026-03-22 | Sprint 1: логотип, базовые цвета |
|
||||
| 2.0 | 2026-03-22 | Sprint 2: типографика, оффлайн носители, цвета сайта (8 цветов) |
|
||||
| 2.1 | 2026-03-22 | Sprint 2 доп.: +3 цвета сайта (коралловый, светло-жёлтый, светло-зелёный) |
|
||||
| 3.0 | 2026-03-22 | Sprint 3: кнопки (Button), форм-контролы (Input/Textarea/Select/Checkbox/Radio/Toggle), LLM-блок на логотипе |
|
||||
|
||||
---
|
||||
|
||||
## 12. Что обновлять в этом файле
|
||||
|
||||
При каждом спринте добавляй:
|
||||
- Новые компоненты и их спецификации (цвета, размеры, состояния)
|
||||
- Новые правила применения, выявленные при разработке
|
||||
- Изменения в палитре или типографике
|
||||
- Новые паттерны и примеры кода
|
||||
|
||||
**Соответствующий раздел в TZ.md:** ФТ-03-LLM
|
||||
**Соответствующий раздел в SPRINTS.md:** задача «LLM-блок» в каждом спринте
|
||||
@@ -81,41 +81,65 @@
|
||||
### Фактические результаты
|
||||
- Страница `/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 страниц Оффлайн
|
||||
- Страница `/offline/uniform` — реальные фото из PDF (беж + синий вариант), таблица размеров, правила
|
||||
- Страница `/offline/badges` — реальные фото из PDF (лицевая + оборотная), состав текста, пример
|
||||
- Страница `/offline/navigation` — макеты из PDF (Кабинет 04, карточка врача), фото дверей с номерами (13, 31), указатели по этажам
|
||||
- Страница `/offline/transport` — макет трамвая из PDF (оба вида, реальный рендер), таблица зон, цвета Oracal
|
||||
- Страница `/offline/print` — убрана из навигации (нет данных из брендбука)
|
||||
- Sidebar: убраны «скоро» для Цветов, Типографики и всех страниц Оффлайн кроме Печати
|
||||
- Версия обновлена до **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 без внешних зависимостей
|
||||
- Реальные фото и макеты из PDF: PyMuPDF (fitz) — извлечение растровых изображений и рендер векторных страниц
|
||||
- Рендер PDF страниц: 2.5–3.0x масштаб → JPEG, кроп до нужной области через Pillow
|
||||
|
||||
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы.
|
||||
### Ретроспектива Sprint 2 — LLM-контекст
|
||||
- [x] Docs: Создан `docs/LLM_CONTEXT.md` — сводный файл дизайн-данных для LLM (v2.1)
|
||||
- [x] TZ: Добавлено требование ФТ-03-LLM — LLM-блок на каждой странице брендбука
|
||||
- [ ] FE: Добавить LLM-блоки на страницы Sprint 1–2 (логотип, цвета, типографика, оффлайн) — перенесено в Sprint 3
|
||||
|
||||
**Результат спринта:** Разделы «Цвета», «Типографика» и «Оффлайн элементы» полностью готовы с реальными материалами из брендбука.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 3 — Базовые компоненты: кнопки и поля ввода
|
||||
## Sprint 3 — Базовые компоненты: кнопки и поля ввода ✅ ЗАВЕРШЁН
|
||||
|
||||
**Цель:** Все варианты кнопок и форм-контролов в брендбуке.
|
||||
**Цель:** Все варианты кнопок и форм-контролов в брендбуке. LLM-блоки на страницах.
|
||||
|
||||
### Задачи
|
||||
- [ ] FE: Компонент Button (все варианты: primary/secondary/ghost/danger, размеры, состояния)
|
||||
- [ ] FE: Компонент Input (text, password, focus/error/disabled)
|
||||
- [ ] FE: Компонент Textarea
|
||||
- [ ] FE: Компонент Select
|
||||
- [ ] FE: Компонент Checkbox и Radio
|
||||
- [ ] FE: Компонент Toggle/Switch
|
||||
- [ ] FE: Страница брендбука «Компоненты → Кнопки» с документацией
|
||||
- [ ] FE: Страница брендбука «Компоненты → Форм-контролы»
|
||||
- [ ] FE: Копирование HTML/CSS кода компонента в один клик
|
||||
### Задачи — LLM-контекст
|
||||
- [x] FE: Добавить LLM-блок на страницу «Логотип» (`/foundation/logo`) — v1.0
|
||||
- [x] FE: Добавить LLM-блок на страницу «Цвета» (`/foundation/colors`) — v2.1
|
||||
- [x] FE: Добавить LLM-блок на страницу «Типографика» (`/foundation/typography`) — v2.0
|
||||
- [x] FE: Создать переиспользуемый компонент `components/llm/LlmBlock.tsx` (LlmBlock, LlmSection, LlmTable, LlmRules)
|
||||
- [x] Docs: Обновить `docs/LLM_CONTEXT.md` по итогам спринта — версия 3.0
|
||||
|
||||
**Результат спринта:** Раздел «Базовые компоненты — кнопки и ввод» готов.
|
||||
### Задачи — компоненты
|
||||
- [x] FE: Компонент Button (`components/ui/Button.tsx`) — варианты primary/outline/teal/pill, размеры sm/md/lg, loading
|
||||
- [x] FE: Компонент Input (`.bb-input`) — text, password, focus/error/disabled, height 50px как на сайте
|
||||
- [x] FE: Компонент Textarea (`.bb-textarea`) — resize:vertical, min-height 100px
|
||||
- [x] FE: Компонент Select (`.bb-select`) — height 50px, кастомная стрелка SVG
|
||||
- [x] FE: Компонент Checkbox (`.bb-checkbox`) и Radio (`.bb-radio`)
|
||||
- [x] FE: Компонент Toggle/Switch (`components/ui/Toggle.tsx`) — React "use client", defaultChecked/disabled/label
|
||||
- [x] FE: Страница `/components/buttons` — 4 варианта по реальному сайту, размеры, состояния, «Где применяется», LLM-блок v2.0
|
||||
- [x] FE: Страница `/components/forms` — все 6 контролов, контекст на сайте с макетом, LLM-блок v2.0
|
||||
- [x] FE: Копирование HTML/CSS кода в один клик (`components/ui/CodeCopy.tsx`)
|
||||
|
||||
### Фактические результаты
|
||||
- **4 варианта кнопок** — скопированы с реального сайта perm.oclinica.ru: primary(#FFA39C), outline(#BF9975), teal(#60959c), pill(#e9e4d4)
|
||||
- **6 форм-контролов** — input/textarea/select/checkbox/radio/toggle с полной документацией состояний
|
||||
- **Input/Select** — height 50px, border 1px solid #ccc, border-radius 4px (entityform CSS с реального сайта)
|
||||
- **Макет формы** на фоне #b8e6ed как «Узнайте стоимость операции» на oclinica.ru/lor
|
||||
- **LLM-блоки** добавлены на логотип, цвета, типографику, кнопки, форм-контролы
|
||||
- **Компоненты:** Button.tsx, Toggle.tsx, CodeCopy.tsx в `components/ui/`
|
||||
- **Sidebar:** Sprint 3 · v0.3.0, кнопки/формы убраны из «скоро»
|
||||
- **Деплой:** https://web-oclinica.vercel.app (production)
|
||||
|
||||
**Результат спринта:** Раздел «Базовые компоненты» полностью готов. Стили соответствуют реальному сайту.
|
||||
|
||||
---
|
||||
|
||||
@@ -124,6 +148,8 @@
|
||||
**Цель:** Все типы карточек, используемых на сайте.
|
||||
|
||||
### Задачи
|
||||
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации карточек
|
||||
- [ ] FE: Добавить LLM-блок на страницу «Карточки»
|
||||
- [ ] FE: Карточка врача (DoctorCard) — фото, имя, специализация, кнопка записи
|
||||
- [ ] FE: Карточка услуги / заболевания
|
||||
- [ ] FE: Карточка новости — превью, дата, заголовок, анонс, читать далее
|
||||
@@ -141,6 +167,8 @@
|
||||
**Цель:** Ключевые верхние блоки страниц.
|
||||
|
||||
### Задачи
|
||||
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации Hero и CEO-блоков
|
||||
- [ ] FE: Добавить LLM-блоки на страницы Hero и CEO-текст
|
||||
- [ ] FE: Hero-блок вариант 1 — фон + заголовок + CTA
|
||||
- [ ] FE: Hero-блок вариант 2 — иллюстрация сбоку
|
||||
- [ ] FE: Hero-блок вариант 3 — с встроенной формой записи
|
||||
@@ -158,6 +186,8 @@
|
||||
**Цель:** Блоки и компоненты, связанные с врачами.
|
||||
|
||||
### Задачи
|
||||
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков врачей
|
||||
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Врачи»
|
||||
- [ ] FE: Блок «Наши врачи» — сетка карточек с фильтром по специализации
|
||||
- [ ] FE: Блок «Врач — профиль» (полная страница): фото, биография, специализации, расписание
|
||||
- [ ] FE: Компонент «Расписание / слоты записи»
|
||||
@@ -174,6 +204,8 @@
|
||||
**Цель:** Контентные блоки сайта.
|
||||
|
||||
### Задачи
|
||||
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации блоков отзывов и новостей
|
||||
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Отзывы и новости»
|
||||
- [ ] FE: Блок отзывов — карусель
|
||||
- [ ] FE: Блок отзывов — статичная сетка
|
||||
- [ ] FE: Блок рейтинга (звёзды + количество отзывов)
|
||||
@@ -192,6 +224,8 @@
|
||||
**Цель:** Все формы и диалоги взаимодействия с пациентом.
|
||||
|
||||
### Задачи
|
||||
- [ ] Docs: Обновить `docs/LLM_CONTEXT.md` — добавить спецификации форм и модальных окон
|
||||
- [ ] FE: Добавить LLM-блок на страницу «Блоки → Формы и контакты»
|
||||
- [ ] FE: Форма записи — короткая (имя, телефон)
|
||||
- [ ] FE: Форма записи — расширенная (имя, телефон, специализация, врач, дата)
|
||||
- [ ] FE: Форма записи в модальном окне
|
||||
|
||||
@@ -153,6 +153,17 @@
|
||||
- HTML-код компонента с кнопкой «Скопировать»
|
||||
- Краткое описание и правила применения
|
||||
|
||||
**ФТ-03-LLM.** На каждой странице брендбука, содержащей дизайн-стандарты (цвета, типографика,
|
||||
компоненты, блоки, страницы), должен присутствовать **LLM-блок** — свёрнутая или отдельная
|
||||
секция с машиночитаемым описанием стандарта для использования языковыми моделями при
|
||||
разработке дизайна, макетов, сайта и приложений. LLM-блок содержит:
|
||||
- Все токены и значения в табличном формате
|
||||
- Правила применения в формате «разрешено / запрещено»
|
||||
- Взаимосвязи с другими стандартами (ссылки на разделы)
|
||||
- Примеры корректного и некорректного использования
|
||||
|
||||
Сводный LLM-контекст всего брендбука хранится в файле: **`docs/LLM_CONTEXT.md`**
|
||||
|
||||
---
|
||||
|
||||
### 5.2 Раздел «Цвета»
|
||||
@@ -530,7 +541,8 @@ vercel --prod --yes
|
||||
| ОВ-03 | Нужна ли страница «Заболевание» как отдельный тип, или это подвид страницы «Услуга»? | Клиника | Sprint 9 |
|
||||
| ОВ-04 | Список иконок — какую стороннюю библиотеку утвердить? (Lucide, Heroicons, и др.) | Совместно | Sprint 2 |
|
||||
| ОВ-05 | ~~Нужен ли раздел «Логотип» в v1.0 или ждём вектор?~~ **Закрыт:** страница логотипа реализуется в Sprint 1 с PNG-версией; вектор будет добавлен позже | — | Закрыт |
|
||||
| ОВ-06 | ~~HEX-эквиваленты цветов Oracal~~ **Закрыт:** приблизительные HEX зафиксированы в Sprint 2 и подтверждены как рабочие (053M=#7ecfca, 073M=#5b7b87, 066M=#5bb5ad, 050M=#1b4c72, 081M=#c4a882, 080M=#5c2e0e). Точная калибровка — при получении физических образцов. | — | Закрыт |
|
||||
| ОВ-06 | ~~HEX-эквиваленты цветов Oracal~~ **Закрыт:** приблизительные HEX зафиксированы в Sprint 2 (053M=#7ecfca, 073M=#5b7b87, 066M=#5bb5ad, 050M=#1b4c72, 081M=#c4a882, 080M=#5c2e0e). Дополнительно — реальные цвета сайта извлечены из CSS (см. ОВ-07). Точная калибровка Oracal — при получении физических образцов. | — | Закрыт |
|
||||
| ОВ-07 | **Цвета сайта oclinica.ru** — CSS тема Drupal доступна по адресу: `https://perm.oclinica.ru/sites/all/themes/clinic_bootstrap_mobile/css/style.css`. Тема: `clinic_bootstrap_mobile`. Ключевые цвета извлечены парсингом (python + regex + Counter), 2026-03-22. Добавлены в раздел «Цвета с сайта» в брендбуке (`/foundation/colors`). Расхождение с Oracal-палитрой ожидаемо — цифровые адаптации под экран. | — | Закрыт |
|
||||
|
||||
---
|
||||
|
||||
|
||||